##// END OF EJS Templates
Implemented simple gist functionality ref #530....
marcink -
r3840:dc464486 beta
parent child Browse files
Show More
@@ -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
@@ -0,0 +1,68 b''
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.html"/>
3
4 <%def name="title()">
5 ${_('Gists')} &middot; ${c.rhodecode_name}
6 </%def>
7
8 <%def name="breadcrumbs_links()">
9 %if c.show_private:
10 ${_('Private Gists for user %s') % c.rhodecode_user.username}
11 %else:
12 ${_('Public Gists')}
13 %endif
14 - ${c.gists_pager.item_count}
15 </%def>
16
17 <%def name="page_nav()">
18 ${self.menu('gists')}
19 </%def>
20
21 <%def name="main()">
22 <div class="box">
23 <!-- box / title -->
24 <div class="title">
25 ${self.breadcrumbs()}
26 %if c.rhodecode_user.username != 'default':
27 <ul class="links">
28 <li>
29 <span>${h.link_to(_(u'Create new gist'), h.url('new_gist'))}</span>
30 </li>
31 </ul>
32 %endif
33 </div>
34 %if c.gists_pager.item_count>0:
35 % for gist in c.gists_pager:
36 <div class="gist-item" style="padding:10px 20px 10px 15px">
37
38 <div class="gravatar">
39 <img alt="gravatar" src="${h.gravatar_url(h.email_or_none(gist.owner.full_contact),24)}"/>
40 </div>
41 <div title="${gist.owner.full_contact}" class="user">
42 <b>${h.person(gist.owner.full_contact)}</b> /
43 <b><a href="${h.url('gist',id=gist.gist_access_id)}">gist:${gist.gist_access_id}</a></b>
44 <span style="color: #AAA">
45 %if gist.gist_expires == -1:
46 ${_('Expires')}: ${_('never')}
47 %else:
48 ${_('Expires')}: ${h.age(h.time_to_datetime(gist.gist_expires))}
49 %endif
50 </span>
51 </div>
52 <div>${_('Created')} ${h.age(gist.created_on)}
53 </div>
54
55 <div style="border:0px;padding:10px 0px 0px 35px;color:#AAA">${gist.gist_description}</div>
56 </div>
57 % endfor
58
59 <div class="notification-paginator">
60 <div class="pagination-wh pagination-left">
61 ${c.gists_pager.pager('$link_previous ~2~ $link_next')}
62 </div>
63 </div>
64 %else:
65 <div class="table">${_('There are no gists yet')}</div>
66 %endif
67 </div>
68 </%def>
@@ -0,0 +1,64 b''
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.html"/>
3
4 <%def name="title()">
5 ${_('New gist')} &middot; ${c.rhodecode_name}
6 </%def>
7
8 <%def name="js_extra()">
9 <script type="text/javascript" src="${h.url('/js/codemirror.js')}"></script>
10 </%def>
11 <%def name="css_extra()">
12 <link rel="stylesheet" type="text/css" href="${h.url('/css/codemirror.css')}"/>
13 </%def>
14
15 <%def name="breadcrumbs_links()">
16 ${_('New gist')}
17 </%def>
18
19 <%def name="page_nav()">
20 ${self.menu('gists')}
21 </%def>
22
23 <%def name="main()">
24 <div class="box">
25 <!-- box / title -->
26 <div class="title">
27 ${self.breadcrumbs()}
28 </div>
29
30 <div class="table">
31 <div id="files_data">
32 ${h.form(h.url('gists'), method='post',id='eform')}
33 <div>
34 <div class="gravatar">
35 <img alt="gravatar" src="${h.gravatar_url(h.email_or_none(c.rhodecode_user.full_contact),32)}"/>
36 </div>
37 <textarea style="resize:vertical; width:400px;border: 1px solid #ccc;border-radius: 3px;" id="description" name="description" placeholder="${_('Gist description ...')}"></textarea>
38 </div>
39 <div id="body" class="codeblock">
40 <div style="padding: 10px 10px 10px 22px;color:#666666">
41 ##<input type="text" value="" size="30" name="filename" id="filename" placeholder="gistfile1.txt">
42 ${h.text('filename', size=30, placeholder='gistfile1.txt')}
43 ##<input type="text" value="" size="30" name="filename" id="filename" placeholder="gistfile1.txt">
44 ${h.select('lifetime', '', c.lifetime_options)}
45 </div>
46 <div id="editor_container">
47 <pre id="editor_pre"></pre>
48 <textarea id="editor" name="content" style="display:none"></textarea>
49 </div>
50 </div>
51 <div style="padding-top: 5px">
52 ${h.submit('private',_('Create private gist'),class_="ui-btn yellow")}
53 ${h.submit('public',_('Create public gist'),class_="ui-btn")}
54 ${h.reset('reset',_('Reset'),class_="ui-btn")}
55 </div>
56 ${h.end_form()}
57 <script type="text/javascript">
58 initCodeMirror('editor','');
59 </script>
60 </div>
61 </div>
62
63 </div>
64 </%def>
@@ -0,0 +1,87 b''
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.html"/>
3
4 <%def name="title()">
5 ${_('gist')}:${c.gist.gist_access_id} &middot; ${c.rhodecode_name}
6 </%def>
7
8 <%def name="breadcrumbs_links()">
9 ${_('Gist')} &middot; gist:${c.gist.gist_access_id}
10 </%def>
11
12 <%def name="page_nav()">
13 ${self.menu('gists')}
14 </%def>
15
16 <%def name="main()">
17 <div class="box">
18 <!-- box / title -->
19 <div class="title">
20 ${self.breadcrumbs()}
21 %if c.rhodecode_user.username != 'default':
22 <ul class="links">
23 <li>
24 <span>${h.link_to(_(u'Create new gist'), h.url('new_gist'))}</span>
25 </li>
26 </ul>
27 %endif
28 </div>
29 <div class="table">
30 <div id="files_data">
31 <div id="body" class="codeblock">
32 <div class="code-header">
33 <div class="stats">
34 <div class="left" style="margin: -4px 0px 0px 0px">
35 %if c.gist.gist_type == 'public':
36 <div class="ui-btn green badge">${_('Public gist')}</div>
37 %else:
38 <div class="ui-btn yellow badge">${_('Private gist')}</div>
39 %endif
40 </div>
41 <span style="color: #AAA">
42 %if c.gist.gist_expires == -1:
43 ${_('Expires')}: ${_('never')}
44 %else:
45 ${_('Expires')}: ${h.age(h.time_to_datetime(c.gist.gist_expires))}
46 %endif
47 </span>
48 <div class="left item last">${c.gist.gist_description}</div>
49 <div class="buttons">
50 ## only owner should see that
51 %if c.gist.owner.username == c.rhodecode_user.username:
52 ##${h.link_to(_('Edit'),h.url(''),class_="ui-btn")}
53 ##${h.link_to(_('Delete'),h.url(''),class_="ui-btn red")}
54 %endif
55 </div>
56 </div>
57
58 <div class="author">
59 <div class="gravatar">
60 <img alt="gravatar" src="${h.gravatar_url(h.email_or_none(c.file_changeset.author),16)}"/>
61 </div>
62 <div title="${c.file_changeset.author}" class="user">${h.person(c.file_changeset.author)} - ${_('created')} ${h.age(c.file_changeset.date)}</div>
63 </div>
64 <div class="commit">${h.urlify_commit(c.file_changeset.message,c.repo_name)}</div>
65 </div>
66 </div>
67
68 ## iterate over the files
69 % for file in c.files:
70 <div style="border: 1px solid #EEE;margin-top:20px">
71 <div id="${h.FID('G', file.path)}" class="stats" style="border-bottom: 1px solid #DDD;padding: 8px 14px;">
72 <b>${file.path}</b>
73 ##<div class="buttons">
74 ## ${h.link_to(_('Show as raw'),h.url(''),class_="ui-btn")}
75 ##</div>
76 </div>
77 <div class="code-body">
78 ${h.pygmentize(file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")}
79 </div>
80 </div>
81 %endfor
82 </div>
83 </div>
84
85
86 </div>
87 </%def>
@@ -0,0 +1,125 b''
1 from rhodecode.tests import *
2 from rhodecode.model.gist import GistModel
3 from rhodecode.model.meta import Session
4 from rhodecode.model.db import User, Gist
5
6
7 def _create_gist(f_name, content='some gist', lifetime=-1,
8 description='gist-desc', gist_type='public'):
9 gist_mapping = {
10 f_name: {'content': content}
11 }
12 user = User.get_by_username(TEST_USER_ADMIN_LOGIN)
13 gist = GistModel().create(description, owner=user,
14 gist_mapping=gist_mapping, gist_type=gist_type,
15 lifetime=lifetime)
16 Session().commit()
17 return gist
18
19
20 class TestGistsController(TestController):
21
22 def tearDown(self):
23 for g in Gist.get_all():
24 GistModel().delete(g)
25 Session().commit()
26
27 def test_index(self):
28 self.log_user()
29 response = self.app.get(url('gists'))
30 # Test response...
31 response.mustcontain('There are no gists yet')
32
33 _create_gist('gist1')
34 _create_gist('gist2', lifetime=1400)
35 _create_gist('gist3', description='gist3-desc')
36 _create_gist('gist4', gist_type='private')
37 response = self.app.get(url('gists'))
38 # Test response...
39 response.mustcontain('gist:1')
40 response.mustcontain('gist:2')
41 response.mustcontain('Expires: in 23 hours') # we don't care about the end
42 response.mustcontain('gist:3')
43 response.mustcontain('gist3-desc')
44 response.mustcontain(no=['gist:4'])
45
46 def test_index_private_gists(self):
47 self.log_user()
48 gist = _create_gist('gist5', gist_type='private')
49 response = self.app.get(url('gists', private=1))
50 # Test response...
51
52 #and privates
53 response.mustcontain('gist:%s' % gist.gist_access_id)
54
55 def test_create_missing_description(self):
56 self.log_user()
57 response = self.app.post(url('gists'),
58 params={'lifetime': -1}, status=200)
59
60 response.mustcontain('Missing value')
61
62 def test_create(self):
63 self.log_user()
64 response = self.app.post(url('gists'),
65 params={'lifetime': -1,
66 'content': 'gist test',
67 'filename': 'foo',
68 'public': 'public'},
69 status=302)
70 response = response.follow()
71 response.mustcontain('added file: foo')
72 response.mustcontain('gist test')
73 response.mustcontain('<div class="ui-btn green badge">Public gist</div>')
74
75 def test_create_private(self):
76 self.log_user()
77 response = self.app.post(url('gists'),
78 params={'lifetime': -1,
79 'content': 'private gist test',
80 'filename': 'private-foo',
81 'private': 'private'},
82 status=302)
83 response = response.follow()
84 response.mustcontain('added file: private-foo<')
85 response.mustcontain('private gist test')
86 response.mustcontain('<div class="ui-btn yellow badge">Private gist</div>')
87
88 def test_create_with_description(self):
89 self.log_user()
90 response = self.app.post(url('gists'),
91 params={'lifetime': -1,
92 'content': 'gist test',
93 'filename': 'foo-desc',
94 'description': 'gist-desc',
95 'public': 'public'},
96 status=302)
97 response = response.follow()
98 response.mustcontain('added file: foo-desc')
99 response.mustcontain('gist test')
100 response.mustcontain('gist-desc')
101 response.mustcontain('<div class="ui-btn green badge">Public gist</div>')
102
103 def test_new(self):
104 self.log_user()
105 response = self.app.get(url('new_gist'))
106
107 def test_update(self):
108 self.skipTest('not implemented')
109 response = self.app.put(url('gist', id=1))
110
111 def test_delete(self):
112 self.skipTest('not implemented')
113 response = self.app.delete(url('gist', id=1))
114
115 def test_show(self):
116 gist = _create_gist('gist-show-me')
117 response = self.app.get(url('gist', id=gist.gist_access_id))
118 response.mustcontain('added file: gist-show-me<')
119 response.mustcontain('test_admin (RhodeCode Admin) - created just now')
120 response.mustcontain('gist-desc')
121 response.mustcontain('<div class="ui-btn green badge">Public gist</div>')
122
123 def test_edit(self):
124 self.skipTest('not implemented')
125 response = self.app.get(url('edit_gist', id=1))
@@ -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
539 # decoding here will force that we have proper encoded values
540 # in any other case this will throw exceptions and deny commit
541 if isinstance(content, (basestring,)):
542 content = safe_str(content)
543 elif isinstance(content, (file, cStringIO.OutputType,)):
544 content = content.read()
545 else:
546 raise Exception('Content is of unrecognized type %s' % (
547 type(content)
548 ))
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']
562 # decoding here will force that we have proper encoded values
563 # in any other case this will throw exceptions and deny commit
564 if isinstance(content, (basestring,)):
565 content = safe_str(content)
566 elif isinstance(content, (file, cStringIO.OutputType,)):
567 content = content.read()
568 else:
569 raise Exception('Content is of unrecognized type %s' % (
570 type(content)
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]
560
561 m.add(FileNode(path, content=content))
562 tip = m.commit(message=message,
563 author=author,
564 parents=parents, branch=cs.branch)
588 parents = [parent_cs]
589 # add multiple nodes
590 for path, content in processed_nodes:
591 imc.add(FileNode(path, content=content))
565 592
566 self.mark_for_invalidation(repo_name)
567 self._handle_push(repo,
568 username=user.username,
569 action='push_local',
570 repo_name=repo_name,
571 revisions=[tip.raw_id])
593 tip = imc.commit(message=message,
594 author=author,
595 parents=parents,
596 branch=parent_cs.branch)
597
598 self.mark_for_invalidation(repo.repo_name)
599 if trigger_push_hook:
600 self._handle_push(scm_instance,
601 username=user.username,
602 action='push_local',
603 repo_name=repo.repo_name,
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,4914 +1,4942 b''
1 1 html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td {
2 2 border: 0;
3 3 outline: 0;
4 4 font-size: 100%;
5 5 vertical-align: baseline;
6 6 background: transparent;
7 7 margin: 0;
8 8 padding: 0;
9 9 }
10 10
11 11 body {
12 12 line-height: 1;
13 13 height: 100%;
14 14 background: url("../images/background.png") repeat scroll 0 0 #B0B0B0;
15 15 font-family: Lucida Grande, Verdana, Lucida Sans Regular,
16 16 Lucida Sans Unicode, Arial, sans-serif; font-size : 12px;
17 17 color: #000;
18 18 margin: 0;
19 19 padding: 0;
20 20 font-size: 12px;
21 21 }
22 22
23 23 ol, ul {
24 24 list-style: none;
25 25 }
26 26
27 27 blockquote, q {
28 28 quotes: none;
29 29 }
30 30
31 31 blockquote:before, blockquote:after, q:before, q:after {
32 32 content: none;
33 33 }
34 34
35 35 :focus {
36 36 outline: 0;
37 37 }
38 38
39 39 del {
40 40 text-decoration: line-through;
41 41 }
42 42
43 43 table {
44 44 border-collapse: collapse;
45 45 border-spacing: 0;
46 46 }
47 47
48 48 html {
49 49 height: 100%;
50 50 }
51 51
52 52 a {
53 53 color: #003367;
54 54 text-decoration: none;
55 55 cursor: pointer;
56 56 }
57 57
58 58 a:hover {
59 59 color: #316293;
60 60 text-decoration: underline;
61 61 }
62 62
63 63 h1, h2, h3, h4, h5, h6,
64 64 div.h1, div.h2, div.h3, div.h4, div.h5, div.h6 {
65 65 color: #292929;
66 66 font-weight: 700;
67 67 }
68 68
69 69 h1, div.h1 {
70 70 font-size: 22px;
71 71 }
72 72
73 73 h2, div.h2 {
74 74 font-size: 20px;
75 75 }
76 76
77 77 h3, div.h3 {
78 78 font-size: 18px;
79 79 }
80 80
81 81 h4, div.h4 {
82 82 font-size: 16px;
83 83 }
84 84
85 85 h5, div.h5 {
86 86 font-size: 14px;
87 87 }
88 88
89 89 h6, div.h6 {
90 90 font-size: 11px;
91 91 }
92 92
93 93 ul.circle {
94 94 list-style-type: circle;
95 95 }
96 96
97 97 ul.disc {
98 98 list-style-type: disc;
99 99 }
100 100
101 101 ul.square {
102 102 list-style-type: square;
103 103 }
104 104
105 105 ol.lower-roman {
106 106 list-style-type: lower-roman;
107 107 }
108 108
109 109 ol.upper-roman {
110 110 list-style-type: upper-roman;
111 111 }
112 112
113 113 ol.lower-alpha {
114 114 list-style-type: lower-alpha;
115 115 }
116 116
117 117 ol.upper-alpha {
118 118 list-style-type: upper-alpha;
119 119 }
120 120
121 121 ol.decimal {
122 122 list-style-type: decimal;
123 123 }
124 124
125 125 div.color {
126 126 clear: both;
127 127 overflow: hidden;
128 128 position: absolute;
129 129 background: #FFF;
130 130 margin: 7px 0 0 60px;
131 131 padding: 1px 1px 1px 0;
132 132 }
133 133
134 134 div.color a {
135 135 width: 15px;
136 136 height: 15px;
137 137 display: block;
138 138 float: left;
139 139 margin: 0 0 0 1px;
140 140 padding: 0;
141 141 }
142 142
143 143 div.options {
144 144 clear: both;
145 145 overflow: hidden;
146 146 position: absolute;
147 147 background: #FFF;
148 148 margin: 7px 0 0 162px;
149 149 padding: 0;
150 150 }
151 151
152 152 div.options a {
153 153 height: 1%;
154 154 display: block;
155 155 text-decoration: none;
156 156 margin: 0;
157 157 padding: 3px 8px;
158 158 }
159 159
160 160 .top-left-rounded-corner {
161 161 -webkit-border-top-left-radius: 8px;
162 162 -khtml-border-radius-topleft: 8px;
163 163 border-top-left-radius: 8px;
164 164 }
165 165
166 166 .top-right-rounded-corner {
167 167 -webkit-border-top-right-radius: 8px;
168 168 -khtml-border-radius-topright: 8px;
169 169 border-top-right-radius: 8px;
170 170 }
171 171
172 172 .bottom-left-rounded-corner {
173 173 -webkit-border-bottom-left-radius: 8px;
174 174 -khtml-border-radius-bottomleft: 8px;
175 175 border-bottom-left-radius: 8px;
176 176 }
177 177
178 178 .bottom-right-rounded-corner {
179 179 -webkit-border-bottom-right-radius: 8px;
180 180 -khtml-border-radius-bottomright: 8px;
181 181 border-bottom-right-radius: 8px;
182 182 }
183 183
184 184 .top-left-rounded-corner-mid {
185 185 -webkit-border-top-left-radius: 4px;
186 186 -khtml-border-radius-topleft: 4px;
187 187 border-top-left-radius: 4px;
188 188 }
189 189
190 190 .top-right-rounded-corner-mid {
191 191 -webkit-border-top-right-radius: 4px;
192 192 -khtml-border-radius-topright: 4px;
193 193 border-top-right-radius: 4px;
194 194 }
195 195
196 196 .bottom-left-rounded-corner-mid {
197 197 -webkit-border-bottom-left-radius: 4px;
198 198 -khtml-border-radius-bottomleft: 4px;
199 199 border-bottom-left-radius: 4px;
200 200 }
201 201
202 202 .bottom-right-rounded-corner-mid {
203 203 -webkit-border-bottom-right-radius: 4px;
204 204 -khtml-border-radius-bottomright: 4px;
205 205 border-bottom-right-radius: 4px;
206 206 }
207 207
208 208 .help-block {
209 209 color: #999999;
210 210 display: block;
211 211 margin-bottom: 0;
212 212 margin-top: 5px;
213 213 }
214 214
215 215 .empty_data {
216 216 color: #B9B9B9;
217 217 }
218 218
219 219 a.permalink {
220 220 visibility: hidden;
221 221 position: absolute;
222 222 margin: 3px 4px;
223 223 }
224 224
225 225 a.permalink:hover {
226 226 text-decoration: none;
227 227 }
228 228
229 229 h1:hover > a.permalink,
230 230 h2:hover > a.permalink,
231 231 h3:hover > a.permalink,
232 232 h4:hover > a.permalink,
233 233 h5:hover > a.permalink,
234 234 h6:hover > a.permalink,
235 235 div:hover > a.permalink {
236 236 visibility: visible;
237 237 }
238 238
239 239 #header {
240 240 }
241 241 #header ul#logged-user {
242 242 margin-bottom: 5px !important;
243 243 -webkit-border-radius: 0px 0px 8px 8px;
244 244 -khtml-border-radius: 0px 0px 8px 8px;
245 245 border-radius: 0px 0px 8px 8px;
246 246 height: 37px;
247 247 background-color: #003B76;
248 248 background-repeat: repeat-x;
249 249 background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) );
250 250 background-image: -moz-linear-gradient(top, #003b76, #00376e);
251 251 background-image: -ms-linear-gradient(top, #003b76, #00376e);
252 252 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) );
253 253 background-image: -webkit-linear-gradient(top, #003b76, #00376e);
254 254 background-image: -o-linear-gradient(top, #003b76, #00376e);
255 255 background-image: linear-gradient(to bottom, #003b76, #00376e);
256 256 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76',endColorstr='#00376e', GradientType=0 );
257 257 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
258 258 }
259 259
260 260 #header ul#logged-user li {
261 261 list-style: none;
262 262 float: left;
263 263 margin: 8px 0 0;
264 264 padding: 4px 12px;
265 265 border-left: 1px solid #316293;
266 266 }
267 267
268 268 #header ul#logged-user li.first {
269 269 border-left: none;
270 270 margin: 4px;
271 271 }
272 272
273 273 #header ul#logged-user li.first div.gravatar {
274 274 margin-top: -2px;
275 275 }
276 276
277 277 #header ul#logged-user li.first div.account {
278 278 padding-top: 4px;
279 279 float: left;
280 280 }
281 281
282 282 #header ul#logged-user li.last {
283 283 border-right: none;
284 284 }
285 285
286 286 #header ul#logged-user li a {
287 287 color: #fff;
288 288 font-weight: 700;
289 289 text-decoration: none;
290 290 }
291 291
292 292 #header ul#logged-user li a:hover {
293 293 text-decoration: underline;
294 294 }
295 295
296 296 #header ul#logged-user li.highlight a {
297 297 color: #fff;
298 298 }
299 299
300 300 #header ul#logged-user li.highlight a:hover {
301 301 color: #FFF;
302 302 }
303 303 #header-dd {
304 304 clear: both;
305 305 position: fixed !important;
306 306 background-color: #003B76;
307 307 opacity: 0.01;
308 308 cursor: pointer;
309 309 min-height: 10px;
310 310 width: 100% !important;
311 311 -webkit-border-radius: 0px 0px 4px 4px;
312 312 -khtml-border-radius: 0px 0px 4px 4px;
313 313 border-radius: 0px 0px 4px 4px;
314 314 }
315 315
316 316 #header-dd:hover {
317 317 opacity: 0.2;
318 318 -webkit-transition: opacity 0.5s ease-in-out;
319 319 -moz-transition: opacity 0.5s ease-in-out;
320 320 transition: opacity 0.5s ease-in-out;
321 321 }
322 322
323 323 #header #header-inner {
324 324 min-height: 44px;
325 325 clear: both;
326 326 position: relative;
327 327 background-color: #003B76;
328 328 background-repeat: repeat-x;
329 329 background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) );
330 330 background-image: -moz-linear-gradient(top, #003b76, #00376e);
331 331 background-image: -ms-linear-gradient(top, #003b76, #00376e);
332 332 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76),color-stop(100%, #00376e) );
333 333 background-image: -webkit-linear-gradient(top, #003b76, #00376e);
334 334 background-image: -o-linear-gradient(top, #003b76, #00376e);
335 335 background-image: linear-gradient(to bottom, #003b76, #00376e);
336 336 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76',endColorstr='#00376e', GradientType=0 );
337 337 margin: 0;
338 338 padding: 0;
339 339 display: block;
340 340 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
341 341 -webkit-border-radius: 0px 0px 4px 4px;
342 342 -khtml-border-radius: 0px 0px 4px 4px;
343 343 border-radius: 0px 0px 4px 4px;
344 344 }
345 345 #header #header-inner.hover {
346 346 width: 100% !important;
347 347 -webkit-border-radius: 0px 0px 0px 0px;
348 348 -khtml-border-radius: 0px 0px 0px 0px;
349 349 border-radius: 0px 0px 0px 0px;
350 350 position: fixed !important;
351 351 z-index: 10000;
352 352 }
353 353
354 354 .ie7 #header #header-inner.hover,
355 355 .ie8 #header #header-inner.hover,
356 356 .ie9 #header #header-inner.hover
357 357 {
358 358 z-index: auto !important;
359 359 }
360 360
361 361 .header-pos-fix, .anchor {
362 362 margin-top: -46px;
363 363 padding-top: 46px;
364 364 }
365 365
366 366 #header #header-inner #home a {
367 367 height: 40px;
368 368 width: 46px;
369 369 display: block;
370 370 background: url("../images/button_home.png");
371 371 background-position: 0 0;
372 372 margin: 0;
373 373 padding: 0;
374 374 }
375 375
376 376 #header #header-inner #home a:hover {
377 377 background-position: 0 -40px;
378 378 }
379 379
380 380 #header #header-inner #logo {
381 381 float: left;
382 382 position: absolute;
383 383 }
384 384
385 385 #header #header-inner #logo h1 {
386 386 color: #FFF;
387 387 font-size: 20px;
388 388 margin: 12px 0 0 13px;
389 389 padding: 0;
390 390 }
391 391
392 392 #header #header-inner #logo a {
393 393 color: #fff;
394 394 text-decoration: none;
395 395 }
396 396
397 397 #header #header-inner #logo a:hover {
398 398 color: #bfe3ff;
399 399 }
400 400
401 401 #header #header-inner #quick {
402 402 position: relative;
403 403 float: right;
404 404 list-style-type: none;
405 405 list-style-position: outside;
406 406 margin: 4px 8px 0 0;
407 407 padding: 0;
408 408 border-radius: 4px;
409 409 }
410 410
411 411 #header #header-inner #quick li span.short {
412 412 padding: 9px 6px 8px 6px;
413 413 }
414 414
415 415 #header #header-inner #quick li span {
416 416 display: inline;
417 417 margin: 0;
418 418 }
419 419
420 420 #header #header-inner #quick li span.normal {
421 421 border: none;
422 422 padding: 10px 12px 8px;
423 423 }
424 424
425 425 #header #header-inner #quick li span.icon {
426 426 border-left: none;
427 427 padding-left: 10px;
428 428 }
429 429
430 430 #header #header-inner #quick li span.icon_short {
431 431 top: 0;
432 432 left: 0;
433 433 border-left: none;
434 434 border-right: 1px solid #2e5c89;
435 435 padding: 8px 6px 4px;
436 436 }
437 437
438 438 #header #header-inner #quick li span.icon img, #header #header-inner #quick li span.icon_short img {
439 439 vertical-align: middle;
440 440 margin-bottom: 2px;
441 441 }
442 442
443 443 #header #header-inner #quick ul.repo_switcher {
444 444 max-height: 275px;
445 445 overflow-x: hidden;
446 446 overflow-y: auto;
447 447 }
448 448
449 449 #header #header-inner #quick ul.repo_switcher li.qfilter_rs {
450 450 padding: 2px 3px;
451 451 padding-right: 17px;
452 452 }
453 453
454 454 #header #header-inner #quick ul.repo_switcher li.qfilter_rs input {
455 455 width: 100%;
456 456 border-radius: 10px;
457 457 padding: 2px 7px;
458 458 }
459 459
460 460 #header #header-inner #quick .repo_switcher_type {
461 461 position: absolute;
462 462 left: 0;
463 463 top: 9px;
464 464 margin: 0px 2px 0px 2px;
465 465 }
466 466
467 467 #header #header-inner #quick li ul li a.journal, #header #header-inner #quick li ul li a.journal:hover {
468 468 background-image: url("../images/icons/book.png");
469 469 }
470 470
471 471 #header #header-inner #quick li ul li a.private_repo, #header #header-inner #quick li ul li a.private_repo:hover {
472 472 background-image: url("../images/icons/lock.png")
473 473 }
474 474
475 475 #header #header-inner #quick li ul li a.public_repo, #header #header-inner #quick li ul li a.public_repo:hover {
476 476 background-image: url("../images/icons/lock_open.png");
477 477 }
478 478
479 479 #header #header-inner #quick li ul li a.hg, #header #header-inner #quick li ul li a.hg:hover {
480 480 background-image: url("../images/icons/hgicon.png");
481 481 padding-left: 42px;
482 482 background-position: 20px 9px;
483 483 }
484 484
485 485 #header #header-inner #quick li ul li a.git, #header #header-inner #quick li ul li a.git:hover {
486 486 background-image: url("../images/icons/giticon.png");
487 487 padding-left: 42px;
488 488 background-position: 20px 9px;
489 489 }
490 490
491 491 #header #header-inner #quick li ul li a.repos, #header #header-inner #quick li ul li a.repos:hover {
492 492 background-image: url("../images/icons/database_edit.png");
493 493 }
494 494
495 495 #header #header-inner #quick li ul li a.repos_groups, #header #header-inner #quick li ul li a.repos_groups:hover {
496 496 background-image: url("../images/icons/database_link.png");
497 497 }
498 498
499 499 #header #header-inner #quick li ul li a.users, #header #header-inner #quick li ul li a.users:hover {
500 500 background-image: url("../images/icons/user_edit.png");
501 501 }
502 502
503 503 #header #header-inner #quick li ul li a.groups, #header #header-inner #quick li ul li a.groups:hover {
504 504 background-image: url("../images/icons/group_edit.png");
505 505 }
506 506
507 507 #header #header-inner #quick li ul li a.defaults, #header #header-inner #quick li ul li a.defaults:hover {
508 508 background-image: url("../images/icons/wrench.png");
509 509 }
510 510
511 511 #header #header-inner #quick li ul li a.settings, #header #header-inner #quick li ul li a.settings:hover {
512 512 background-image: url("../images/icons/cog.png");
513 513 }
514 514
515 515 #header #header-inner #quick li ul li a.permissions, #header #header-inner #quick li ul li a.permissions:hover {
516 516 background-image: url("../images/icons/key.png");
517 517 }
518 518
519 519 #header #header-inner #quick li ul li a.ldap, #header #header-inner #quick li ul li a.ldap:hover {
520 520 background-image: url("../images/icons/server_key.png");
521 521 }
522 522
523 523 #header #header-inner #quick li ul li a.fork, #header #header-inner #quick li ul li a.fork:hover {
524 524 background-image: url("../images/icons/arrow_divide.png");
525 525 }
526 526
527 527 #header #header-inner #quick li ul li a.locking_add, #header #header-inner #quick li ul li a.locking_add:hover {
528 528 background-image: url("../images/icons/lock_add.png");
529 529 }
530 530
531 531 #header #header-inner #quick li ul li a.locking_del, #header #header-inner #quick li ul li a.locking_del:hover {
532 532 background-image: url("../images/icons/lock_delete.png");
533 533 }
534 534
535 535 #header #header-inner #quick li ul li a.pull_request, #header #header-inner #quick li ul li a.pull_request:hover {
536 536 background-image: url("../images/icons/arrow_join.png") ;
537 537 }
538 538
539 539 #header #header-inner #quick li ul li a.compare_request, #header #header-inner #quick li ul li a.compare_request:hover {
540 540 background-image: url("../images/icons/arrow_inout.png");
541 541 }
542 542
543 543 #header #header-inner #quick li ul li a.search, #header #header-inner #quick li ul li a.search:hover {
544 544 background-image: url("../images/icons/search_16.png");
545 545 }
546 546
547 547 #header #header-inner #quick li ul li a.delete, #header #header-inner #quick li ul li a.delete:hover {
548 548 background-image: url("../images/icons/delete.png");
549 549 }
550 550
551 551 #header #header-inner #quick li ul li a.branches, #header #header-inner #quick li ul li a.branches:hover {
552 552 background-image: url("../images/icons/arrow_branch.png");
553 553 }
554 554
555 555 #header #header-inner #quick li ul li a.tags,
556 556 #header #header-inner #quick li ul li a.tags:hover {
557 557 background: #FFF url("../images/icons/tag_blue.png") no-repeat 4px 9px;
558 558 width: 167px;
559 559 margin: 0;
560 560 padding: 12px 9px 7px 24px;
561 561 }
562 562
563 563 #header #header-inner #quick li ul li a.bookmarks,
564 564 #header #header-inner #quick li ul li a.bookmarks:hover {
565 565 background: #FFF url("../images/icons/tag_green.png") no-repeat 4px 9px;
566 566 width: 167px;
567 567 margin: 0;
568 568 padding: 12px 9px 7px 24px;
569 569 }
570 570
571 571 #header #header-inner #quick li ul li a.admin,
572 572 #header #header-inner #quick li ul li a.admin:hover {
573 573 background: #FFF url("../images/icons/cog_edit.png") no-repeat 4px 9px;
574 574 width: 167px;
575 575 margin: 0;
576 576 padding: 12px 9px 7px 24px;
577 577 }
578 578
579 579 .groups_breadcrumbs a {
580 580 color: #fff;
581 581 }
582 582
583 583 .groups_breadcrumbs a:hover {
584 584 color: #bfe3ff;
585 585 text-decoration: none;
586 586 }
587 587
588 588 td.quick_repo_menu {
589 589 background: #FFF url("../images/vertical-indicator.png") 8px 50% no-repeat !important;
590 590 cursor: pointer;
591 591 width: 8px;
592 592 border: 1px solid transparent;
593 593 }
594 594
595 595 td.quick_repo_menu.active {
596 596 background: url("../images/dt-arrow-dn.png") no-repeat scroll 5px 50% #FFFFFF !important;
597 597 border: 1px solid #003367;
598 598 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
599 599 cursor: pointer;
600 600 }
601 601
602 602 td.quick_repo_menu .menu_items {
603 603 margin-top: 10px;
604 604 margin-left: -6px;
605 605 width: 150px;
606 606 position: absolute;
607 607 background-color: #FFF;
608 608 background: none repeat scroll 0 0 #FFFFFF;
609 609 border-color: #003367 #666666 #666666;
610 610 border-right: 1px solid #666666;
611 611 border-style: solid;
612 612 border-width: 1px;
613 613 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
614 614 border-top-style: none;
615 615 }
616 616
617 617 td.quick_repo_menu .menu_items li {
618 618 padding: 0 !important;
619 619 }
620 620
621 621 td.quick_repo_menu .menu_items a {
622 622 display: block;
623 623 padding: 4px 12px 4px 8px;
624 624 }
625 625
626 626 td.quick_repo_menu .menu_items a:hover {
627 627 background-color: #EEE;
628 628 text-decoration: none;
629 629 }
630 630
631 631 td.quick_repo_menu .menu_items .icon img {
632 632 margin-bottom: -2px;
633 633 }
634 634
635 635 td.quick_repo_menu .menu_items.hidden {
636 636 display: none;
637 637 }
638 638
639 639 .yui-dt-first th {
640 640 text-align: left;
641 641 }
642 642
643 643 /*
644 644 Copyright (c) 2011, Yahoo! Inc. All rights reserved.
645 645 Code licensed under the BSD License:
646 646 http://developer.yahoo.com/yui/license.html
647 647 version: 2.9.0
648 648 */
649 649 .yui-skin-sam .yui-dt-mask {
650 650 position: absolute;
651 651 z-index: 9500;
652 652 }
653 653 .yui-dt-tmp {
654 654 position: absolute;
655 655 left: -9000px;
656 656 }
657 657 .yui-dt-scrollable .yui-dt-bd { overflow: auto }
658 658 .yui-dt-scrollable .yui-dt-hd {
659 659 overflow: hidden;
660 660 position: relative;
661 661 }
662 662 .yui-dt-scrollable .yui-dt-bd thead tr,
663 663 .yui-dt-scrollable .yui-dt-bd thead th {
664 664 position: absolute;
665 665 left: -1500px;
666 666 }
667 667 .yui-dt-scrollable tbody { -moz-outline: 0 }
668 668 .yui-skin-sam thead .yui-dt-sortable { cursor: pointer }
669 669 .yui-skin-sam thead .yui-dt-draggable { cursor: move }
670 670 .yui-dt-coltarget {
671 671 position: absolute;
672 672 z-index: 999;
673 673 }
674 674 .yui-dt-hd { zoom: 1 }
675 675 th.yui-dt-resizeable .yui-dt-resizerliner { position: relative }
676 676 .yui-dt-resizer {
677 677 position: absolute;
678 678 right: 0;
679 679 bottom: 0;
680 680 height: 100%;
681 681 cursor: e-resize;
682 682 cursor: col-resize;
683 683 background-color: #CCC;
684 684 opacity: 0;
685 685 filter: alpha(opacity=0);
686 686 }
687 687 .yui-dt-resizerproxy {
688 688 visibility: hidden;
689 689 position: absolute;
690 690 z-index: 9000;
691 691 background-color: #CCC;
692 692 opacity: 0;
693 693 filter: alpha(opacity=0);
694 694 }
695 695 th.yui-dt-hidden .yui-dt-liner,
696 696 td.yui-dt-hidden .yui-dt-liner,
697 697 th.yui-dt-hidden .yui-dt-resizer { display: none }
698 698 .yui-dt-editor,
699 699 .yui-dt-editor-shim {
700 700 position: absolute;
701 701 z-index: 9000;
702 702 }
703 703 .yui-skin-sam .yui-dt table {
704 704 margin: 0;
705 705 padding: 0;
706 706 font-family: arial;
707 707 font-size: inherit;
708 708 border-collapse: separate;
709 709 *border-collapse: collapse;
710 710 border-spacing: 0;
711 711 border: 1px solid #7f7f7f;
712 712 }
713 713 .yui-skin-sam .yui-dt thead { border-spacing: 0 }
714 714 .yui-skin-sam .yui-dt caption {
715 715 color: #000;
716 716 font-size: 85%;
717 717 font-weight: normal;
718 718 font-style: italic;
719 719 line-height: 1;
720 720 padding: 1em 0;
721 721 text-align: center;
722 722 }
723 723 .yui-skin-sam .yui-dt th { background: #d8d8da url(../images/sprite.png) repeat-x 0 0 }
724 724 .yui-skin-sam .yui-dt th,
725 725 .yui-skin-sam .yui-dt th a {
726 726 font-weight: normal;
727 727 text-decoration: none;
728 728 color: #000;
729 729 vertical-align: bottom;
730 730 }
731 731 .yui-skin-sam .yui-dt th {
732 732 margin: 0;
733 733 padding: 0;
734 734 border: 0;
735 735 border-right: 1px solid #cbcbcb;
736 736 }
737 737 .yui-skin-sam .yui-dt tr.yui-dt-first td { border-top: 1px solid #7f7f7f }
738 738 .yui-skin-sam .yui-dt th .yui-dt-liner { white-space: nowrap }
739 739 .yui-skin-sam .yui-dt-liner {
740 740 margin: 0;
741 741 padding: 0;
742 742 }
743 743 .yui-skin-sam .yui-dt-coltarget {
744 744 width: 5px;
745 745 background-color: red;
746 746 }
747 747 .yui-skin-sam .yui-dt td {
748 748 margin: 0;
749 749 padding: 0;
750 750 border: 0;
751 751 border-right: 1px solid #cbcbcb;
752 752 text-align: left;
753 753 }
754 754 .yui-skin-sam .yui-dt-list td { border-right: 0 }
755 755 .yui-skin-sam .yui-dt-resizer { width: 6px }
756 756 .yui-skin-sam .yui-dt-mask {
757 757 background-color: #000;
758 758 opacity: .25;
759 759 filter: alpha(opacity=25);
760 760 }
761 761 .yui-skin-sam .yui-dt-message { background-color: #FFF }
762 762 .yui-skin-sam .yui-dt-scrollable table { border: 0 }
763 763 .yui-skin-sam .yui-dt-scrollable .yui-dt-hd {
764 764 border-left: 1px solid #7f7f7f;
765 765 border-top: 1px solid #7f7f7f;
766 766 border-right: 1px solid #7f7f7f;
767 767 }
768 768 .yui-skin-sam .yui-dt-scrollable .yui-dt-bd {
769 769 border-left: 1px solid #7f7f7f;
770 770 border-bottom: 1px solid #7f7f7f;
771 771 border-right: 1px solid #7f7f7f;
772 772 background-color: #FFF;
773 773 }
774 774 .yui-skin-sam .yui-dt-scrollable .yui-dt-data tr.yui-dt-last td { border-bottom: 1px solid #7f7f7f }
775 775 .yui-skin-sam th.yui-dt-asc,
776 776 .yui-skin-sam th.yui-dt-desc { background: url(../images/sprite.png) repeat-x 0 -100px }
777 777 .yui-skin-sam th.yui-dt-sortable .yui-dt-label { margin-right: 10px }
778 778 .yui-skin-sam th.yui-dt-asc .yui-dt-liner { background: url(../images/dt-arrow-up.png) no-repeat right }
779 779 .yui-skin-sam th.yui-dt-desc .yui-dt-liner { background: url(../images/dt-arrow-dn.png) no-repeat right }
780 780 tbody .yui-dt-editable { cursor: pointer }
781 781 .yui-dt-editor {
782 782 text-align: left;
783 783 background-color: #f2f2f2;
784 784 border: 1px solid #808080;
785 785 padding: 6px;
786 786 }
787 787 .yui-dt-editor label {
788 788 padding-left: 4px;
789 789 padding-right: 6px;
790 790 }
791 791 .yui-dt-editor .yui-dt-button {
792 792 padding-top: 6px;
793 793 text-align: right;
794 794 }
795 795 .yui-dt-editor .yui-dt-button button {
796 796 background: url(../images/sprite.png) repeat-x 0 0;
797 797 border: 1px solid #999;
798 798 width: 4em;
799 799 height: 1.8em;
800 800 margin-left: 6px;
801 801 }
802 802 .yui-dt-editor .yui-dt-button button.yui-dt-default {
803 803 background: url(../images/sprite.png) repeat-x 0 -1400px;
804 804 background-color: #5584e0;
805 805 border: 1px solid #304369;
806 806 color: #FFF;
807 807 }
808 808 .yui-dt-editor .yui-dt-button button:hover {
809 809 background: url(../images/sprite.png) repeat-x 0 -1300px;
810 810 color: #000;
811 811 }
812 812 .yui-dt-editor .yui-dt-button button:active {
813 813 background: url(../images/sprite.png) repeat-x 0 -1700px;
814 814 color: #000;
815 815 }
816 816 .yui-skin-sam tr.yui-dt-even { background-color: #FFF }
817 817 .yui-skin-sam tr.yui-dt-odd { background-color: #edf5ff }
818 818 .yui-skin-sam tr.yui-dt-even td.yui-dt-asc,
819 819 .yui-skin-sam tr.yui-dt-even td.yui-dt-desc { background-color: #edf5ff }
820 820 .yui-skin-sam tr.yui-dt-odd td.yui-dt-asc,
821 821 .yui-skin-sam tr.yui-dt-odd td.yui-dt-desc { background-color: #dbeaff }
822 822 .yui-skin-sam .yui-dt-list tr.yui-dt-even { background-color: #FFF }
823 823 .yui-skin-sam .yui-dt-list tr.yui-dt-odd { background-color: #FFF }
824 824 .yui-skin-sam .yui-dt-list tr.yui-dt-even td.yui-dt-asc,
825 825 .yui-skin-sam .yui-dt-list tr.yui-dt-even td.yui-dt-desc { background-color: #edf5ff }
826 826 .yui-skin-sam .yui-dt-list tr.yui-dt-odd td.yui-dt-asc,
827 827 .yui-skin-sam .yui-dt-list tr.yui-dt-odd td.yui-dt-desc { background-color: #edf5ff }
828 828 .yui-skin-sam th.yui-dt-highlighted,
829 829 .yui-skin-sam th.yui-dt-highlighted a { background-color: #b2d2ff }
830 830 .yui-skin-sam tr.yui-dt-highlighted,
831 831 .yui-skin-sam tr.yui-dt-highlighted td.yui-dt-asc,
832 832 .yui-skin-sam tr.yui-dt-highlighted td.yui-dt-desc,
833 833 .yui-skin-sam tr.yui-dt-even td.yui-dt-highlighted,
834 834 .yui-skin-sam tr.yui-dt-odd td.yui-dt-highlighted {
835 835 cursor: pointer;
836 836 background-color: #b2d2ff;
837 837 }
838 838 .yui-skin-sam .yui-dt-list th.yui-dt-highlighted,
839 839 .yui-skin-sam .yui-dt-list th.yui-dt-highlighted a { background-color: #b2d2ff }
840 840 .yui-skin-sam .yui-dt-list tr.yui-dt-highlighted,
841 841 .yui-skin-sam .yui-dt-list tr.yui-dt-highlighted td.yui-dt-asc,
842 842 .yui-skin-sam .yui-dt-list tr.yui-dt-highlighted td.yui-dt-desc,
843 843 .yui-skin-sam .yui-dt-list tr.yui-dt-even td.yui-dt-highlighted,
844 844 .yui-skin-sam .yui-dt-list tr.yui-dt-odd td.yui-dt-highlighted {
845 845 cursor: pointer;
846 846 background-color: #b2d2ff;
847 847 }
848 848 .yui-skin-sam th.yui-dt-selected,
849 849 .yui-skin-sam th.yui-dt-selected a { background-color: #446cd7 }
850 850 .yui-skin-sam tr.yui-dt-selected td,
851 851 .yui-skin-sam tr.yui-dt-selected td.yui-dt-asc,
852 852 .yui-skin-sam tr.yui-dt-selected td.yui-dt-desc {
853 853 background-color: #426fd9;
854 854 color: #FFF;
855 855 }
856 856 .yui-skin-sam tr.yui-dt-even td.yui-dt-selected,
857 857 .yui-skin-sam tr.yui-dt-odd td.yui-dt-selected {
858 858 background-color: #446cd7;
859 859 color: #FFF;
860 860 }
861 861 .yui-skin-sam .yui-dt-list th.yui-dt-selected,
862 862 .yui-skin-sam .yui-dt-list th.yui-dt-selected a { background-color: #446cd7 }
863 863 .yui-skin-sam .yui-dt-list tr.yui-dt-selected td,
864 864 .yui-skin-sam .yui-dt-list tr.yui-dt-selected td.yui-dt-asc,
865 865 .yui-skin-sam .yui-dt-list tr.yui-dt-selected td.yui-dt-desc {
866 866 background-color: #426fd9;
867 867 color: #FFF;
868 868 }
869 869 .yui-skin-sam .yui-dt-list tr.yui-dt-even td.yui-dt-selected,
870 870 .yui-skin-sam .yui-dt-list tr.yui-dt-odd td.yui-dt-selected {
871 871 background-color: #446cd7;
872 872 color: #FFF;
873 873 }
874 874 .yui-skin-sam .yui-dt-paginator {
875 875 display: block;
876 876 margin: 6px 0;
877 877 white-space: nowrap;
878 878 }
879 879 .yui-skin-sam .yui-dt-paginator .yui-dt-first,
880 880 .yui-skin-sam .yui-dt-paginator .yui-dt-last,
881 881 .yui-skin-sam .yui-dt-paginator .yui-dt-selected { padding: 2px 6px }
882 882 .yui-skin-sam .yui-dt-paginator a.yui-dt-first,
883 883 .yui-skin-sam .yui-dt-paginator a.yui-dt-last { text-decoration: none }
884 884 .yui-skin-sam .yui-dt-paginator .yui-dt-previous,
885 885 .yui-skin-sam .yui-dt-paginator .yui-dt-next { display: none }
886 886 .yui-skin-sam a.yui-dt-page {
887 887 border: 1px solid #cbcbcb;
888 888 padding: 2px 6px;
889 889 text-decoration: none;
890 890 background-color: #fff;
891 891 }
892 892 .yui-skin-sam .yui-dt-selected {
893 893 border: 1px solid #fff;
894 894 background-color: #fff;
895 895 }
896 896
897 897 #content #left {
898 898 left: 0;
899 899 width: 280px;
900 900 position: absolute;
901 901 }
902 902
903 903 #content #right {
904 904 margin: 0 60px 10px 290px;
905 905 }
906 906
907 907 #content div.box {
908 908 clear: both;
909 909 background: #fff;
910 910 margin: 0 0 10px;
911 911 padding: 0 0 10px;
912 912 -webkit-border-radius: 4px 4px 4px 4px;
913 913 -khtml-border-radius: 4px 4px 4px 4px;
914 914 border-radius: 4px 4px 4px 4px;
915 915 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
916 916 }
917 917
918 918 #content div.box-left {
919 919 width: 49%;
920 920 clear: none;
921 921 float: left;
922 922 margin: 0 0 10px;
923 923 }
924 924
925 925 #content div.box-right {
926 926 width: 49%;
927 927 clear: none;
928 928 float: right;
929 929 margin: 0 0 10px;
930 930 }
931 931
932 932 #content div.box div.title {
933 933 clear: both;
934 934 overflow: hidden;
935 935 background-color: #003B76;
936 936 background-repeat: repeat-x;
937 937 background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) );
938 938 background-image: -moz-linear-gradient(top, #003b76, #00376e);
939 939 background-image: -ms-linear-gradient(top, #003b76, #00376e);
940 940 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) );
941 941 background-image: -webkit-linear-gradient(top, #003b76, #00376e);
942 942 background-image: -o-linear-gradient(top, #003b76, #00376e);
943 943 background-image: linear-gradient(to bottom, #003b76, #00376e);
944 944 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76', endColorstr='#00376e', GradientType=0 );
945 945 margin: 0 0 20px;
946 946 padding: 0;
947 947 border-radius: 4px 4px 0 0;
948 948 }
949 949
950 950 #content div.box div.title h5 {
951 951 float: left;
952 952 border: none;
953 953 color: #fff;
954 954 margin: 0;
955 955 padding: 11px 0 11px 10px;
956 956 }
957 957
958 958 #content div.box div.title .link-white {
959 959 color: #FFFFFF;
960 960 }
961 961
962 962 #content div.box div.title .link-white.current {
963 963 color: #BFE3FF;
964 964 }
965 965
966 966 #content div.box div.title ul.links li {
967 967 list-style: none;
968 968 float: left;
969 969 margin: 0;
970 970 padding: 0;
971 971 }
972 972
973 973 #content div.box div.title ul.links li a {
974 974 border-left: 1px solid #316293;
975 975 color: #FFFFFF;
976 976 display: block;
977 977 float: left;
978 978 font-size: 13px;
979 979 font-weight: 700;
980 980 height: 1%;
981 981 margin: 0;
982 982 padding: 11px 22px 12px;
983 983 text-decoration: none;
984 984 }
985 985
986 986 #content div.box h1, #content div.box h2, #content div.box h3, #content div.box h4, #content div.box h5, #content div.box h6,
987 987 #content div.box div.h1, #content div.box div.h2, #content div.box div.h3, #content div.box div.h4, #content div.box div.h5, #content div.box div.h6 {
988 988 clear: both;
989 989 overflow: hidden;
990 990 border-bottom: 1px solid #DDD;
991 991 margin: 10px 20px;
992 992 padding: 0 0 15px;
993 993 }
994 994
995 995 #content div.box p {
996 996 color: #5f5f5f;
997 997 font-size: 12px;
998 998 line-height: 150%;
999 999 margin: 0 24px 10px;
1000 1000 padding: 0;
1001 1001 }
1002 1002
1003 1003 #content div.box blockquote {
1004 1004 border-left: 4px solid #DDD;
1005 1005 color: #5f5f5f;
1006 1006 font-size: 11px;
1007 1007 line-height: 150%;
1008 1008 margin: 0 34px;
1009 1009 padding: 0 0 0 14px;
1010 1010 }
1011 1011
1012 1012 #content div.box blockquote p {
1013 1013 margin: 10px 0;
1014 1014 padding: 0;
1015 1015 }
1016 1016
1017 1017 #content div.box dl {
1018 1018 margin: 10px 0px;
1019 1019 }
1020 1020
1021 1021 #content div.box dt {
1022 1022 font-size: 12px;
1023 1023 margin: 0;
1024 1024 }
1025 1025
1026 1026 #content div.box dd {
1027 1027 font-size: 12px;
1028 1028 margin: 0;
1029 1029 padding: 8px 0 8px 15px;
1030 1030 }
1031 1031
1032 1032 #content div.box li {
1033 1033 font-size: 12px;
1034 1034 padding: 4px 0;
1035 1035 }
1036 1036
1037 1037 #content div.box ul.disc, #content div.box ul.circle {
1038 1038 margin: 10px 24px 10px 38px;
1039 1039 }
1040 1040
1041 1041 #content div.box ul.square {
1042 1042 margin: 10px 24px 10px 40px;
1043 1043 }
1044 1044
1045 1045 #content div.box img.left {
1046 1046 border: none;
1047 1047 float: left;
1048 1048 margin: 10px 10px 10px 0;
1049 1049 }
1050 1050
1051 1051 #content div.box img.right {
1052 1052 border: none;
1053 1053 float: right;
1054 1054 margin: 10px 0 10px 10px;
1055 1055 }
1056 1056
1057 1057 #content div.box div.messages {
1058 1058 clear: both;
1059 1059 overflow: hidden;
1060 1060 margin: 0 20px;
1061 1061 padding: 0;
1062 1062 }
1063 1063
1064 1064 #content div.box div.message {
1065 1065 clear: both;
1066 1066 overflow: hidden;
1067 1067 margin: 0;
1068 1068 padding: 5px 0;
1069 1069 white-space: pre-wrap;
1070 1070 }
1071 1071 #content div.box div.expand {
1072 1072 width: 110%;
1073 1073 height: 14px;
1074 1074 font-size: 10px;
1075 1075 text-align: center;
1076 1076 cursor: pointer;
1077 1077 color: #666;
1078 1078
1079 1079 background: -webkit-gradient(linear,0% 50%,100% 50%,color-stop(0%,rgba(255,255,255,0)),color-stop(100%,rgba(64,96,128,0.1)));
1080 1080 background: -webkit-linear-gradient(top,rgba(255,255,255,0),rgba(64,96,128,0.1));
1081 1081 background: -moz-linear-gradient(top,rgba(255,255,255,0),rgba(64,96,128,0.1));
1082 1082 background: -o-linear-gradient(top,rgba(255,255,255,0),rgba(64,96,128,0.1));
1083 1083 background: -ms-linear-gradient(top,rgba(255,255,255,0),rgba(64,96,128,0.1));
1084 1084 background: linear-gradient(to bottom,rgba(255,255,255,0),rgba(64,96,128,0.1));
1085 1085
1086 1086 display: none;
1087 1087 overflow: hidden;
1088 1088 }
1089 1089 #content div.box div.expand .expandtext {
1090 1090 background-color: #ffffff;
1091 1091 padding: 2px;
1092 1092 border-radius: 2px;
1093 1093 }
1094 1094
1095 1095 #content div.box div.message a {
1096 1096 font-weight: 400 !important;
1097 1097 }
1098 1098
1099 1099 #content div.box div.message div.image {
1100 1100 float: left;
1101 1101 margin: 9px 0 0 5px;
1102 1102 padding: 6px;
1103 1103 }
1104 1104
1105 1105 #content div.box div.message div.image img {
1106 1106 vertical-align: middle;
1107 1107 margin: 0;
1108 1108 }
1109 1109
1110 1110 #content div.box div.message div.text {
1111 1111 float: left;
1112 1112 margin: 0;
1113 1113 padding: 9px 6px;
1114 1114 }
1115 1115
1116 1116 #content div.box div.message div.dismiss a {
1117 1117 height: 16px;
1118 1118 width: 16px;
1119 1119 display: block;
1120 1120 background: url("../images/icons/cross.png") no-repeat;
1121 1121 margin: 15px 14px 0 0;
1122 1122 padding: 0;
1123 1123 }
1124 1124
1125 1125 #content div.box div.message div.text h1, #content div.box div.message div.text h2, #content div.box div.message div.text h3, #content div.box div.message div.text h4, #content div.box div.message div.text h5, #content div.box div.message div.text h6 {
1126 1126 border: none;
1127 1127 margin: 0;
1128 1128 padding: 0;
1129 1129 }
1130 1130
1131 1131 #content div.box div.message div.text span {
1132 1132 height: 1%;
1133 1133 display: block;
1134 1134 margin: 0;
1135 1135 padding: 5px 0 0;
1136 1136 }
1137 1137
1138 1138 #content div.box div.message-error {
1139 1139 height: 1%;
1140 1140 clear: both;
1141 1141 overflow: hidden;
1142 1142 background: #FBE3E4;
1143 1143 border: 1px solid #FBC2C4;
1144 1144 color: #860006;
1145 1145 }
1146 1146
1147 1147 #content div.box div.message-error h6 {
1148 1148 color: #860006;
1149 1149 }
1150 1150
1151 1151 #content div.box div.message-warning {
1152 1152 height: 1%;
1153 1153 clear: both;
1154 1154 overflow: hidden;
1155 1155 background: #FFF6BF;
1156 1156 border: 1px solid #FFD324;
1157 1157 color: #5f5200;
1158 1158 }
1159 1159
1160 1160 #content div.box div.message-warning h6 {
1161 1161 color: #5f5200;
1162 1162 }
1163 1163
1164 1164 #content div.box div.message-notice {
1165 1165 height: 1%;
1166 1166 clear: both;
1167 1167 overflow: hidden;
1168 1168 background: #8FBDE0;
1169 1169 border: 1px solid #6BACDE;
1170 1170 color: #003863;
1171 1171 }
1172 1172
1173 1173 #content div.box div.message-notice h6 {
1174 1174 color: #003863;
1175 1175 }
1176 1176
1177 1177 #content div.box div.message-success {
1178 1178 height: 1%;
1179 1179 clear: both;
1180 1180 overflow: hidden;
1181 1181 background: #E6EFC2;
1182 1182 border: 1px solid #C6D880;
1183 1183 color: #4e6100;
1184 1184 }
1185 1185
1186 1186 #content div.box div.message-success h6 {
1187 1187 color: #4e6100;
1188 1188 }
1189 1189
1190 1190 #content div.box div.form div.fields div.field {
1191 1191 height: 1%;
1192 1192 min-height: 12px;
1193 1193 border-bottom: 1px solid #DDD;
1194 1194 clear: both;
1195 1195 margin: 0;
1196 1196 padding: 10px 0;
1197 1197 }
1198 1198
1199 1199 #content div.box div.form div.fields div.field-first {
1200 1200 padding: 0 0 10px;
1201 1201 }
1202 1202
1203 1203 #content div.box div.form div.fields div.field-noborder {
1204 1204 border-bottom: 0 !important;
1205 1205 }
1206 1206
1207 1207 #content div.box div.form div.fields div.field span.error-message {
1208 1208 height: 1%;
1209 1209 display: inline-block;
1210 1210 color: red;
1211 1211 margin: 8px 0 0 4px;
1212 1212 padding: 0;
1213 1213 }
1214 1214
1215 1215 #content div.box div.form div.fields div.field span.success {
1216 1216 height: 1%;
1217 1217 display: block;
1218 1218 color: #316309;
1219 1219 margin: 8px 0 0;
1220 1220 padding: 0;
1221 1221 }
1222 1222
1223 1223 #content div.box div.form div.fields div.field div.label {
1224 1224 left: 70px;
1225 1225 width: 155px;
1226 1226 position: absolute;
1227 1227 margin: 0;
1228 1228 padding: 5px 0 0 0px;
1229 1229 }
1230 1230
1231 1231 #content div.box div.form div.fields div.field div.label-summary {
1232 1232 left: 30px;
1233 1233 width: 155px;
1234 1234 position: absolute;
1235 1235 margin: 0;
1236 1236 padding: 0px 0 0 0px;
1237 1237 }
1238 1238
1239 1239 #content div.box-left div.form div.fields div.field div.label,
1240 1240 #content div.box-right div.form div.fields div.field div.label,
1241 1241 #content div.box-left div.form div.fields div.field div.label,
1242 1242 #content div.box-left div.form div.fields div.field div.label-summary,
1243 1243 #content div.box-right div.form div.fields div.field div.label-summary,
1244 1244 #content div.box-left div.form div.fields div.field div.label-summary {
1245 1245 clear: both;
1246 1246 overflow: hidden;
1247 1247 left: 0;
1248 1248 width: auto;
1249 1249 position: relative;
1250 1250 margin: 0;
1251 1251 padding: 0 0 8px;
1252 1252 }
1253 1253
1254 1254 #content div.box div.form div.fields div.field div.label-select {
1255 1255 padding: 5px 0 0 5px;
1256 1256 }
1257 1257
1258 1258 #content div.box-left div.form div.fields div.field div.label-select,
1259 1259 #content div.box-right div.form div.fields div.field div.label-select {
1260 1260 padding: 0 0 8px;
1261 1261 }
1262 1262
1263 1263 #content div.box-left div.form div.fields div.field div.label-textarea,
1264 1264 #content div.box-right div.form div.fields div.field div.label-textarea {
1265 1265 padding: 0 0 8px !important;
1266 1266 }
1267 1267
1268 1268 #content div.box div.form div.fields div.field div.label label, div.label label {
1269 1269 color: #393939;
1270 1270 font-weight: 700;
1271 1271 }
1272 1272 #content div.box div.form div.fields div.field div.label label, div.label-summary label {
1273 1273 color: #393939;
1274 1274 font-weight: 700;
1275 1275 }
1276 1276 #content div.box div.form div.fields div.field div.input {
1277 1277 margin: 0 0 0 200px;
1278 1278 }
1279 1279
1280 1280 #content div.box div.form div.fields div.field div.input.summary {
1281 1281 margin: 0 0 0 110px;
1282 1282 }
1283 1283 #content div.box div.form div.fields div.field div.input.summary-short {
1284 1284 margin: 0 0 0 110px;
1285 1285 }
1286 1286 #content div.box div.form div.fields div.field div.file {
1287 1287 margin: 0 0 0 200px;
1288 1288 }
1289 1289
1290 1290 #content div.box-left div.form div.fields div.field div.input, #content div.box-right div.form div.fields div.field div.input {
1291 1291 margin: 0 0 0 0px;
1292 1292 }
1293 1293
1294 1294 #content div.box div.form div.fields div.field div.input input,
1295 1295 .reviewer_ac input {
1296 1296 background: #FFF;
1297 1297 border-top: 1px solid #b3b3b3;
1298 1298 border-left: 1px solid #b3b3b3;
1299 1299 border-right: 1px solid #eaeaea;
1300 1300 border-bottom: 1px solid #eaeaea;
1301 1301 color: #000;
1302 1302 font-size: 11px;
1303 1303 margin: 0;
1304 1304 padding: 7px 7px 6px;
1305 1305 }
1306 1306
1307 1307 #content div.box div.form div.fields div.field div.input input#clone_url,
1308 1308 #content div.box div.form div.fields div.field div.input input#clone_url_id
1309 1309 {
1310 1310 font-size: 16px;
1311 1311 padding: 2px;
1312 1312 }
1313 1313
1314 1314 #content div.box div.form div.fields div.field div.file input {
1315 1315 background: none repeat scroll 0 0 #FFFFFF;
1316 1316 border-color: #B3B3B3 #EAEAEA #EAEAEA #B3B3B3;
1317 1317 border-style: solid;
1318 1318 border-width: 1px;
1319 1319 color: #000000;
1320 1320 font-size: 11px;
1321 1321 margin: 0;
1322 1322 padding: 7px 7px 6px;
1323 1323 }
1324 1324
1325 1325 input.disabled {
1326 1326 background-color: #F5F5F5 !important;
1327 1327 }
1328 1328 #content div.box div.form div.fields div.field div.input input.small {
1329 1329 width: 30%;
1330 1330 }
1331 1331
1332 1332 #content div.box div.form div.fields div.field div.input input.medium {
1333 1333 width: 55%;
1334 1334 }
1335 1335
1336 1336 #content div.box div.form div.fields div.field div.input input.large {
1337 1337 width: 85%;
1338 1338 }
1339 1339
1340 1340 #content div.box div.form div.fields div.field div.input input.date {
1341 1341 width: 177px;
1342 1342 }
1343 1343
1344 1344 #content div.box div.form div.fields div.field div.input input.button {
1345 1345 background: #D4D0C8;
1346 1346 border-top: 1px solid #FFF;
1347 1347 border-left: 1px solid #FFF;
1348 1348 border-right: 1px solid #404040;
1349 1349 border-bottom: 1px solid #404040;
1350 1350 color: #000;
1351 1351 margin: 0;
1352 1352 padding: 4px 8px;
1353 1353 }
1354 1354
1355 1355 #content div.box div.form div.fields div.field div.textarea {
1356 1356 border-top: 1px solid #b3b3b3;
1357 1357 border-left: 1px solid #b3b3b3;
1358 1358 border-right: 1px solid #eaeaea;
1359 1359 border-bottom: 1px solid #eaeaea;
1360 1360 margin: 0 0 0 200px;
1361 1361 padding: 10px;
1362 1362 }
1363 1363
1364 1364 #content div.box div.form div.fields div.field div.textarea-editor {
1365 1365 border: 1px solid #ddd;
1366 1366 padding: 0;
1367 1367 }
1368 1368
1369 1369 #content div.box div.form div.fields div.field div.textarea textarea {
1370 1370 width: 100%;
1371 1371 height: 220px;
1372 1372 overflow: hidden;
1373 1373 background: #FFF;
1374 1374 color: #000;
1375 1375 font-size: 11px;
1376 1376 outline: none;
1377 1377 border-width: 0;
1378 1378 margin: 0;
1379 1379 padding: 0;
1380 1380 }
1381 1381
1382 1382 #content div.box-left div.form div.fields div.field div.textarea textarea, #content div.box-right div.form div.fields div.field div.textarea textarea {
1383 1383 width: 100%;
1384 1384 height: 100px;
1385 1385 }
1386 1386
1387 1387 #content div.box div.form div.fields div.field div.textarea table {
1388 1388 width: 100%;
1389 1389 border: none;
1390 1390 margin: 0;
1391 1391 padding: 0;
1392 1392 }
1393 1393
1394 1394 #content div.box div.form div.fields div.field div.textarea table td {
1395 1395 background: #DDD;
1396 1396 border: none;
1397 1397 padding: 0;
1398 1398 }
1399 1399
1400 1400 #content div.box div.form div.fields div.field div.textarea table td table {
1401 1401 width: auto;
1402 1402 border: none;
1403 1403 margin: 0;
1404 1404 padding: 0;
1405 1405 }
1406 1406
1407 1407 #content div.box div.form div.fields div.field div.textarea table td table td {
1408 1408 font-size: 11px;
1409 1409 padding: 5px 5px 5px 0;
1410 1410 }
1411 1411
1412 1412 #content div.box div.form div.fields div.field input[type=text]:focus,
1413 1413 #content div.box div.form div.fields div.field input[type=password]:focus,
1414 1414 #content div.box div.form div.fields div.field input[type=file]:focus,
1415 1415 #content div.box div.form div.fields div.field textarea:focus,
1416 1416 #content div.box div.form div.fields div.field select:focus,
1417 1417 .reviewer_ac input:focus {
1418 1418 background: #f6f6f6;
1419 1419 border-color: #666;
1420 1420 }
1421 1421
1422 1422 .reviewer_ac {
1423 1423 padding: 10px
1424 1424 }
1425 1425
1426 1426 div.form div.fields div.field div.button {
1427 1427 margin: 0;
1428 1428 padding: 0 0 0 8px;
1429 1429 }
1430 1430 #content div.box table.noborder {
1431 1431 border: 1px solid transparent;
1432 1432 }
1433 1433
1434 1434 #content div.box table {
1435 1435 width: 100%;
1436 1436 border-collapse: separate;
1437 1437 margin: 0;
1438 1438 padding: 0;
1439 1439 border: 1px solid #eee;
1440 1440 -webkit-border-radius: 4px;
1441 1441 border-radius: 4px;
1442 1442 }
1443 1443
1444 1444 #content div.box table th {
1445 1445 background: #eee;
1446 1446 border-bottom: 1px solid #ddd;
1447 1447 padding: 5px 0px 5px 5px;
1448 1448 text-align: left;
1449 1449 }
1450 1450
1451 1451 #content div.box table th.left {
1452 1452 text-align: left;
1453 1453 }
1454 1454
1455 1455 #content div.box table th.right {
1456 1456 text-align: right;
1457 1457 }
1458 1458
1459 1459 #content div.box table th.center {
1460 1460 text-align: center;
1461 1461 }
1462 1462
1463 1463 #content div.box table th.selected {
1464 1464 vertical-align: middle;
1465 1465 padding: 0;
1466 1466 }
1467 1467
1468 1468 #content div.box table td {
1469 1469 background: #fff;
1470 1470 border-bottom: 1px solid #cdcdcd;
1471 1471 vertical-align: middle;
1472 1472 padding: 5px;
1473 1473 }
1474 1474
1475 1475 #content div.box table tr.selected td {
1476 1476 background: #FFC;
1477 1477 }
1478 1478
1479 1479 #content div.box table td.selected {
1480 1480 width: 3%;
1481 1481 text-align: center;
1482 1482 vertical-align: middle;
1483 1483 padding: 0;
1484 1484 }
1485 1485
1486 1486 #content div.box table td.action {
1487 1487 width: 45%;
1488 1488 text-align: left;
1489 1489 }
1490 1490
1491 1491 #content div.box table td.date {
1492 1492 width: 33%;
1493 1493 text-align: center;
1494 1494 }
1495 1495
1496 1496 #content div.box div.action {
1497 1497 float: right;
1498 1498 background: #FFF;
1499 1499 text-align: right;
1500 1500 margin: 10px 0 0;
1501 1501 padding: 0;
1502 1502 }
1503 1503
1504 1504 #content div.box div.action select {
1505 1505 font-size: 11px;
1506 1506 margin: 0;
1507 1507 }
1508 1508
1509 1509 #content div.box div.action .ui-selectmenu {
1510 1510 margin: 0;
1511 1511 padding: 0;
1512 1512 }
1513 1513
1514 1514 #content div.box div.pagination {
1515 1515 height: 1%;
1516 1516 clear: both;
1517 1517 overflow: hidden;
1518 1518 margin: 10px 0 0;
1519 1519 padding: 0;
1520 1520 }
1521 1521
1522 1522 #content div.box div.pagination ul.pager {
1523 1523 float: right;
1524 1524 text-align: right;
1525 1525 margin: 0;
1526 1526 padding: 0;
1527 1527 }
1528 1528
1529 1529 #content div.box div.pagination ul.pager li {
1530 1530 height: 1%;
1531 1531 float: left;
1532 1532 list-style: none;
1533 1533 background: #ebebeb url("../images/pager.png") repeat-x;
1534 1534 border-top: 1px solid #dedede;
1535 1535 border-left: 1px solid #cfcfcf;
1536 1536 border-right: 1px solid #c4c4c4;
1537 1537 border-bottom: 1px solid #c4c4c4;
1538 1538 color: #4A4A4A;
1539 1539 font-weight: 700;
1540 1540 margin: 0 0 0 4px;
1541 1541 padding: 0;
1542 1542 }
1543 1543
1544 1544 #content div.box div.pagination ul.pager li.separator {
1545 1545 padding: 6px;
1546 1546 }
1547 1547
1548 1548 #content div.box div.pagination ul.pager li.current {
1549 1549 background: #b4b4b4 url("../images/pager_selected.png") repeat-x;
1550 1550 border-top: 1px solid #ccc;
1551 1551 border-left: 1px solid #bebebe;
1552 1552 border-right: 1px solid #b1b1b1;
1553 1553 border-bottom: 1px solid #afafaf;
1554 1554 color: #515151;
1555 1555 padding: 6px;
1556 1556 }
1557 1557
1558 1558 #content div.box div.pagination ul.pager li a {
1559 1559 height: 1%;
1560 1560 display: block;
1561 1561 float: left;
1562 1562 color: #515151;
1563 1563 text-decoration: none;
1564 1564 margin: 0;
1565 1565 padding: 6px;
1566 1566 }
1567 1567
1568 1568 #content div.box div.pagination ul.pager li a:hover,
1569 1569 #content div.box div.pagination ul.pager li a:active {
1570 1570 background: #b4b4b4 url("../images/pager_selected.png") repeat-x;
1571 1571 border-top: 1px solid #ccc;
1572 1572 border-left: 1px solid #bebebe;
1573 1573 border-right: 1px solid #b1b1b1;
1574 1574 border-bottom: 1px solid #afafaf;
1575 1575 margin: -1px;
1576 1576 }
1577 1577
1578 1578 #content div.box div.pagination-right {
1579 1579 float: right;
1580 1580 }
1581 1581
1582 1582 #content div.box div.pagination-wh {
1583 1583 height: 1%;
1584 1584 overflow: hidden;
1585 1585 text-align: right;
1586 1586 margin: 10px 0 0;
1587 1587 padding: 0;
1588 1588 }
1589 1589
1590 1590 #content div.box div.pagination-wh > :first-child {
1591 1591 border-radius: 4px 0px 0px 4px;
1592 1592 }
1593 1593
1594 1594 #content div.box div.pagination-wh > :last-child {
1595 1595 border-radius: 0px 4px 4px 0px;
1596 1596 border-right: 1px solid #cfcfcf;
1597 1597 }
1598 1598
1599 1599 #content div.box div.pagination-wh a,
1600 1600 #content div.box div.pagination-wh span.pager_dotdot,
1601 1601 #content div.box div.pagination-wh span.yui-pg-previous,
1602 1602 #content div.box div.pagination-wh span.yui-pg-last,
1603 1603 #content div.box div.pagination-wh span.yui-pg-next,
1604 1604 #content div.box div.pagination-wh span.yui-pg-first {
1605 1605 height: 1%;
1606 1606 float: left;
1607 1607 background: #ebebeb url("../images/pager.png") repeat-x;
1608 1608 border-top: 1px solid #dedede;
1609 1609 border-left: 1px solid #cfcfcf;
1610 1610 border-bottom: 1px solid #c4c4c4;
1611 1611 color: #4A4A4A;
1612 1612 font-weight: 700;
1613 1613 padding: 6px;
1614 1614 }
1615 1615
1616 1616 #content div.box div.pagination-wh span.pager_curpage {
1617 1617 height: 1%;
1618 1618 float: left;
1619 1619 background: #b4b4b4 url("../images/pager_selected.png") repeat-x;
1620 1620 border-top: 1px solid #ccc;
1621 1621 border-left: 1px solid #bebebe;
1622 1622 border-bottom: 1px solid #afafaf;
1623 1623 color: #515151;
1624 1624 font-weight: 700;
1625 1625 padding: 6px;
1626 1626 }
1627 1627
1628 1628 #content div.box div.pagination-wh a:hover, #content div.box div.pagination-wh a:active {
1629 1629 background: #b4b4b4 url("../images/pager_selected.png") repeat-x;
1630 1630 border-top: 1px solid #ccc;
1631 1631 border-left: 1px solid #bebebe;
1632 1632 border-bottom: 1px solid #afafaf;
1633 1633 text-decoration: none;
1634 1634 }
1635 1635
1636 1636 #content div.box div.traffic div.legend {
1637 1637 clear: both;
1638 1638 overflow: hidden;
1639 1639 border-bottom: 1px solid #ddd;
1640 1640 margin: 0 0 10px;
1641 1641 padding: 0 0 10px;
1642 1642 }
1643 1643
1644 1644 #content div.box div.traffic div.legend h6 {
1645 1645 float: left;
1646 1646 border: none;
1647 1647 margin: 0;
1648 1648 padding: 0;
1649 1649 }
1650 1650
1651 1651 #content div.box div.traffic div.legend li {
1652 1652 list-style: none;
1653 1653 float: left;
1654 1654 font-size: 11px;
1655 1655 margin: 0;
1656 1656 padding: 0 8px 0 4px;
1657 1657 }
1658 1658
1659 1659 #content div.box div.traffic div.legend li.visits {
1660 1660 border-left: 12px solid #edc240;
1661 1661 }
1662 1662
1663 1663 #content div.box div.traffic div.legend li.pageviews {
1664 1664 border-left: 12px solid #afd8f8;
1665 1665 }
1666 1666
1667 1667 #content div.box div.traffic table {
1668 1668 width: auto;
1669 1669 }
1670 1670
1671 1671 #content div.box div.traffic table td {
1672 1672 background: transparent;
1673 1673 border: none;
1674 1674 padding: 2px 3px 3px;
1675 1675 }
1676 1676
1677 1677 #content div.box div.traffic table td.legendLabel {
1678 1678 padding: 0 3px 2px;
1679 1679 }
1680 1680
1681 1681 #content div.box #summary {
1682 1682 margin-right: 200px;
1683 1683 }
1684 1684
1685 1685 #summary-menu-stats {
1686 1686 float: left;
1687 1687 width: 180px;
1688 1688 position: absolute;
1689 1689 top: 0;
1690 1690 right: 0;
1691 1691 }
1692 1692
1693 1693 #summary-menu-stats ul {
1694 1694 margin: 0 10px;
1695 1695 display: block;
1696 1696 background-color: #f9f9f9;
1697 1697 border: 1px solid #d1d1d1;
1698 1698 border-radius: 4px;
1699 1699 }
1700 1700
1701 1701 #content #summary-menu-stats li {
1702 1702 border-top: 1px solid #d1d1d1;
1703 1703 padding: 0;
1704 1704 }
1705 1705
1706 1706 #content #summary-menu-stats li:hover {
1707 1707 background: #f0f0f0;
1708 1708 }
1709 1709
1710 1710 #content #summary-menu-stats li:first-child {
1711 1711 border-top: none;
1712 1712 }
1713 1713
1714 1714 #summary-menu-stats a.followers { background-image: url('../images/icons/heart.png')}
1715 1715 #summary-menu-stats a.forks { background-image: url('../images/icons/arrow_divide.png')}
1716 1716 #summary-menu-stats a.settings { background-image: url('../images/icons/cog_edit.png')}
1717 1717 #summary-menu-stats a.feed { background-image: url('../images/icons/rss_16.png')}
1718 1718 #summary-menu-stats a.repo-size { background-image: url('../images/icons/server.png')}
1719 1719
1720 1720 #summary-menu-stats a {
1721 1721 display: block;
1722 1722 padding: 12px 30px;
1723 1723 background-repeat: no-repeat;
1724 1724 background-position: 10px 50%;
1725 1725 padding-right: 10px;
1726 1726 }
1727 1727
1728 1728 #repo_size_2.loaded {
1729 1729 margin-left: 30px;
1730 1730 display: block;
1731 1731 padding-right: 10px;
1732 1732 padding-bottom: 7px;
1733 1733 }
1734 1734
1735 1735 #summary-menu-stats a:hover {
1736 1736 text-decoration: none;
1737 1737 }
1738 1738
1739 1739 #summary-menu-stats a span {
1740 1740 background-color: #DEDEDE;
1741 1741 color: 888 !important;
1742 1742 border-radius: 4px;
1743 1743 padding: 2px 4px;
1744 1744 font-size: 10px;
1745 1745 }
1746 1746
1747 1747 #summary .metatag {
1748 1748 display: inline-block;
1749 1749 padding: 3px 5px;
1750 1750 margin-bottom: 3px;
1751 1751 margin-right: 1px;
1752 1752 border-radius: 5px;
1753 1753 }
1754 1754
1755 1755 #content div.box #summary p {
1756 1756 margin-bottom: -5px;
1757 1757 width: 600px;
1758 1758 white-space: pre-wrap;
1759 1759 }
1760 1760
1761 1761 #content div.box #summary p:last-child {
1762 1762 margin-bottom: 9px;
1763 1763 }
1764 1764
1765 1765 #content div.box #summary p:first-of-type {
1766 1766 margin-top: 9px;
1767 1767 }
1768 1768
1769 1769 .metatag {
1770 1770 display: inline-block;
1771 1771 margin-right: 1px;
1772 1772 -webkit-border-radius: 4px 4px 4px 4px;
1773 1773 -khtml-border-radius: 4px 4px 4px 4px;
1774 1774 border-radius: 4px 4px 4px 4px;
1775 1775
1776 1776 border: solid 1px #9CF;
1777 1777 padding: 2px 3px 2px 3px !important;
1778 1778 background-color: #DEF;
1779 1779 }
1780 1780
1781 1781 .metatag[tag="dead"] {
1782 1782 background-color: #E44;
1783 1783 }
1784 1784
1785 1785 .metatag[tag="stale"] {
1786 1786 background-color: #EA4;
1787 1787 }
1788 1788
1789 1789 .metatag[tag="featured"] {
1790 1790 background-color: #AEA;
1791 1791 }
1792 1792
1793 1793 .metatag[tag="requires"] {
1794 1794 background-color: #9CF;
1795 1795 }
1796 1796
1797 1797 .metatag[tag="recommends"] {
1798 1798 background-color: #BDF;
1799 1799 }
1800 1800
1801 1801 .metatag[tag="lang"] {
1802 1802 background-color: #FAF474;
1803 1803 }
1804 1804
1805 1805 .metatag[tag="license"] {
1806 1806 border: solid 1px #9CF;
1807 1807 background-color: #DEF;
1808 1808 target-new: tab !important;
1809 1809 }
1810 1810 .metatag[tag="see"] {
1811 1811 border: solid 1px #CBD;
1812 1812 background-color: #EDF;
1813 1813 }
1814 1814
1815 1815 a.metatag[tag="license"]:hover {
1816 1816 background-color: #003367;
1817 1817 color: #FFF;
1818 1818 text-decoration: none;
1819 1819 }
1820 1820
1821 1821 #summary .desc {
1822 1822 white-space: pre;
1823 1823 width: 100%;
1824 1824 }
1825 1825
1826 1826 #summary .repo_name {
1827 1827 font-size: 1.6em;
1828 1828 font-weight: bold;
1829 1829 vertical-align: baseline;
1830 1830 clear: right
1831 1831 }
1832 1832
1833 1833 #footer {
1834 1834 clear: both;
1835 1835 overflow: hidden;
1836 1836 text-align: right;
1837 1837 margin: 0;
1838 1838 padding: 0 10px 4px;
1839 1839 margin: -10px 0 0;
1840 1840 }
1841 1841
1842 1842 #footer div#footer-inner {
1843 1843 background-color: #003B76;
1844 1844 background-repeat: repeat-x;
1845 1845 background-image: -khtml-gradient( linear, left top, left bottom, from(#003B76), to(#00376E));
1846 1846 background-image: -moz-linear-gradient(top, #003b76, #00376e);
1847 1847 background-image: -ms-linear-gradient( top, #003b76, #00376e);
1848 1848 background-image: -webkit-gradient( linear, left top, left bottom, color-stop( 0%, #003b76), color-stop( 100%, #00376e));
1849 1849 background-image: -webkit-linear-gradient( top, #003b76, #00376e));
1850 1850 background-image: -o-linear-gradient( top, #003b76, #00376e));
1851 1851 background-image: linear-gradient(to bottom, #003b76, #00376e);
1852 1852 filter: progid : DXImageTransform.Microsoft.gradient ( startColorstr = '#003b76', endColorstr = '#00376e', GradientType = 0);
1853 1853 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
1854 1854 -webkit-border-radius: 4px 4px 4px 4px;
1855 1855 -khtml-border-radius: 4px 4px 4px 4px;
1856 1856 border-radius: 4px 4px 4px 4px;
1857 1857 }
1858 1858
1859 1859 #footer div#footer-inner p {
1860 1860 padding: 15px 25px 15px 0;
1861 1861 color: #FFF;
1862 1862 font-weight: 700;
1863 1863 }
1864 1864
1865 1865 #footer div#footer-inner .footer-link {
1866 1866 float: left;
1867 1867 padding-left: 10px;
1868 1868 }
1869 1869
1870 1870 #footer div#footer-inner .footer-link a, #footer div#footer-inner .footer-link-right a {
1871 1871 color: #FFF;
1872 1872 }
1873 1873
1874 1874 #login div.title {
1875 1875 clear: both;
1876 1876 overflow: hidden;
1877 1877 position: relative;
1878 1878 background-color: #003B76;
1879 1879 background-repeat: repeat-x;
1880 1880 background-image: -khtml-gradient( linear, left top, left bottom, from(#003B76), to(#00376E));
1881 1881 background-image: -moz-linear-gradient( top, #003b76, #00376e);
1882 1882 background-image: -ms-linear-gradient( top, #003b76, #00376e);
1883 1883 background-image: -webkit-gradient( linear, left top, left bottom, color-stop( 0%, #003b76), color-stop( 100%, #00376e));
1884 1884 background-image: -webkit-linear-gradient( top, #003b76, #00376e));
1885 1885 background-image: -o-linear-gradient( top, #003b76, #00376e));
1886 1886 background-image: linear-gradient(to bottom, #003b76, #00376e);
1887 1887 filter: progid : DXImageTransform.Microsoft.gradient ( startColorstr = '#003b76', endColorstr = '#00376e', GradientType = 0);
1888 1888 margin: 0 auto;
1889 1889 padding: 0;
1890 1890 }
1891 1891
1892 1892 #login div.inner {
1893 1893 background: #FFF url("../images/login.png") no-repeat top left;
1894 1894 border-top: none;
1895 1895 border-bottom: none;
1896 1896 margin: 0 auto;
1897 1897 padding: 20px;
1898 1898 }
1899 1899
1900 1900 #login div.form div.fields div.field div.label {
1901 1901 width: 173px;
1902 1902 float: left;
1903 1903 text-align: right;
1904 1904 margin: 2px 10px 0 0;
1905 1905 padding: 5px 0 0 5px;
1906 1906 }
1907 1907
1908 1908 #login div.form div.fields div.field div.input input {
1909 1909 background: #FFF;
1910 1910 border-top: 1px solid #b3b3b3;
1911 1911 border-left: 1px solid #b3b3b3;
1912 1912 border-right: 1px solid #eaeaea;
1913 1913 border-bottom: 1px solid #eaeaea;
1914 1914 color: #000;
1915 1915 font-size: 11px;
1916 1916 margin: 0;
1917 1917 padding: 7px 7px 6px;
1918 1918 }
1919 1919
1920 1920 #login div.form div.fields div.buttons {
1921 1921 clear: both;
1922 1922 overflow: hidden;
1923 1923 border-top: 1px solid #DDD;
1924 1924 text-align: right;
1925 1925 margin: 0;
1926 1926 padding: 10px 0 0;
1927 1927 }
1928 1928
1929 1929 #login div.form div.links {
1930 1930 clear: both;
1931 1931 overflow: hidden;
1932 1932 margin: 10px 0 0;
1933 1933 padding: 0 0 2px;
1934 1934 }
1935 1935
1936 1936 .user-menu {
1937 1937 margin: 0px !important;
1938 1938 float: left;
1939 1939 }
1940 1940
1941 1941 .user-menu .container {
1942 1942 padding: 0px 4px 0px 4px;
1943 1943 margin: 0px 0px 0px 0px;
1944 1944 }
1945 1945
1946 1946 .user-menu .gravatar {
1947 1947 margin: 0px 0px 0px 0px;
1948 1948 cursor: pointer;
1949 1949 }
1950 1950 .user-menu .gravatar.enabled {
1951 1951 background-color: #FDF784 !important;
1952 1952 }
1953 1953 .user-menu .gravatar:hover {
1954 1954 background-color: #FDF784 !important;
1955 1955 }
1956 1956 #quick_login {
1957 1957 min-height: 110px;
1958 1958 padding: 4px;
1959 1959 position: absolute;
1960 1960 right: 0;
1961 1961 background-color: #003B76;
1962 1962 background-repeat: repeat-x;
1963 1963 background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) );
1964 1964 background-image: -moz-linear-gradient(top, #003b76, #00376e);
1965 1965 background-image: -ms-linear-gradient(top, #003b76, #00376e);
1966 1966 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) );
1967 1967 background-image: -webkit-linear-gradient(top, #003b76, #00376e);
1968 1968 background-image: -o-linear-gradient(top, #003b76, #00376e);
1969 1969 background-image: linear-gradient(to bottom, #003b76, #00376e);
1970 1970 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76', endColorstr='#00376e', GradientType=0 );
1971 1971
1972 1972 z-index: 999;
1973 1973 -webkit-border-radius: 0px 0px 4px 4px;
1974 1974 -khtml-border-radius: 0px 0px 4px 4px;
1975 1975 border-radius: 0px 0px 4px 4px;
1976 1976 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
1977 1977
1978 1978 overflow: hidden;
1979 1979 }
1980 1980 #quick_login h4 {
1981 1981 color: #fff;
1982 1982 padding: 5px 0px 5px 14px;
1983 1983 }
1984 1984
1985 1985 #quick_login .password_forgoten {
1986 1986 padding-right: 0px;
1987 1987 padding-top: 0px;
1988 1988 text-align: left;
1989 1989 }
1990 1990
1991 1991 #quick_login .password_forgoten a {
1992 1992 font-size: 10px;
1993 1993 color: #fff;
1994 1994 padding: 0px !important;
1995 1995 line-height: 20px !important;
1996 1996 }
1997 1997
1998 1998 #quick_login .register {
1999 1999 padding-right: 10px;
2000 2000 padding-top: 5px;
2001 2001 text-align: left;
2002 2002 }
2003 2003
2004 2004 #quick_login .register a {
2005 2005 font-size: 10px;
2006 2006 color: #fff;
2007 2007 padding: 0px !important;
2008 2008 line-height: 20px !important;
2009 2009 }
2010 2010
2011 2011 #quick_login .submit {
2012 2012 margin: -20px 0 0 0px;
2013 2013 position: absolute;
2014 2014 right: 15px;
2015 2015 }
2016 2016
2017 2017 #quick_login .links_left {
2018 2018 float: left;
2019 2019 margin-right: 130px;
2020 2020 width: 170px;
2021 2021 }
2022 2022 #quick_login .links_right {
2023 2023
2024 2024 position: absolute;
2025 2025 right: 0;
2026 2026 }
2027 2027 #quick_login .full_name {
2028 2028 color: #FFFFFF;
2029 2029 font-weight: bold;
2030 2030 padding: 3px 3px 3px 6px;
2031 2031 }
2032 2032 #quick_login .big_gravatar {
2033 2033 padding: 4px 0px 0px 6px;
2034 2034 }
2035 2035 #quick_login .notifications {
2036 2036 padding: 2px 0px 0px 6px;
2037 2037 color: #FFFFFF;
2038 2038 font-weight: bold;
2039 2039 line-height: 10px !important;
2040 2040 }
2041 2041 #quick_login .notifications a,
2042 2042 #quick_login .unread a {
2043 2043 color: #FFFFFF;
2044 2044 display: block;
2045 2045 padding: 0px !important;
2046 2046 }
2047 2047 #quick_login .notifications a:hover,
2048 2048 #quick_login .unread a:hover {
2049 2049 background-color: inherit !important;
2050 2050 }
2051 2051 #quick_login .email, #quick_login .unread {
2052 2052 color: #FFFFFF;
2053 2053 padding: 3px 3px 3px 6px;
2054 2054 }
2055 2055 #quick_login .links .logout {
2056 2056 }
2057 2057
2058 2058 #quick_login div.form div.fields {
2059 2059 padding-top: 2px;
2060 2060 padding-left: 10px;
2061 2061 }
2062 2062
2063 2063 #quick_login div.form div.fields div.field {
2064 2064 padding: 5px;
2065 2065 }
2066 2066
2067 2067 #quick_login div.form div.fields div.field div.label label {
2068 2068 color: #fff;
2069 2069 padding-bottom: 3px;
2070 2070 }
2071 2071
2072 2072 #quick_login div.form div.fields div.field div.input input {
2073 2073 width: 236px;
2074 2074 background: #FFF;
2075 2075 border-top: 1px solid #b3b3b3;
2076 2076 border-left: 1px solid #b3b3b3;
2077 2077 border-right: 1px solid #eaeaea;
2078 2078 border-bottom: 1px solid #eaeaea;
2079 2079 color: #000;
2080 2080 font-size: 11px;
2081 2081 margin: 0;
2082 2082 padding: 5px 7px 4px;
2083 2083 }
2084 2084
2085 2085 #quick_login div.form div.fields div.buttons {
2086 2086 clear: both;
2087 2087 overflow: hidden;
2088 2088 text-align: right;
2089 2089 margin: 0;
2090 2090 padding: 5px 14px 0px 5px;
2091 2091 }
2092 2092
2093 2093 #quick_login div.form div.links {
2094 2094 clear: both;
2095 2095 overflow: hidden;
2096 2096 margin: 10px 0 0;
2097 2097 padding: 0 0 2px;
2098 2098 }
2099 2099
2100 2100 #quick_login ol.links {
2101 2101 display: block;
2102 2102 font-weight: bold;
2103 2103 list-style: none outside none;
2104 2104 text-align: right;
2105 2105 }
2106 2106 #quick_login ol.links li {
2107 2107 line-height: 27px;
2108 2108 margin: 0;
2109 2109 padding: 0;
2110 2110 color: #fff;
2111 2111 display: block;
2112 2112 float: none !important;
2113 2113 }
2114 2114
2115 2115 #quick_login ol.links li a {
2116 2116 color: #fff;
2117 2117 display: block;
2118 2118 padding: 2px;
2119 2119 }
2120 2120 #quick_login ol.links li a:HOVER {
2121 2121 background-color: inherit !important;
2122 2122 }
2123 2123
2124 2124 #register div.title {
2125 2125 clear: both;
2126 2126 overflow: hidden;
2127 2127 position: relative;
2128 2128 background-color: #003B76;
2129 2129 background-repeat: repeat-x;
2130 2130 background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) );
2131 2131 background-image: -moz-linear-gradient(top, #003b76, #00376e);
2132 2132 background-image: -ms-linear-gradient(top, #003b76, #00376e);
2133 2133 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) );
2134 2134 background-image: -webkit-linear-gradient(top, #003b76, #00376e);
2135 2135 background-image: -o-linear-gradient(top, #003b76, #00376e);
2136 2136 background-image: linear-gradient(to bottom, #003b76, #00376e);
2137 2137 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76',
2138 2138 endColorstr='#00376e', GradientType=0 );
2139 2139 margin: 0 auto;
2140 2140 padding: 0;
2141 2141 }
2142 2142
2143 2143 #register div.inner {
2144 2144 background: #FFF;
2145 2145 border-top: none;
2146 2146 border-bottom: none;
2147 2147 margin: 0 auto;
2148 2148 padding: 20px;
2149 2149 }
2150 2150
2151 2151 #register div.form div.fields div.field div.label {
2152 2152 width: 135px;
2153 2153 float: left;
2154 2154 text-align: right;
2155 2155 margin: 2px 10px 0 0;
2156 2156 padding: 5px 0 0 5px;
2157 2157 }
2158 2158
2159 2159 #register div.form div.fields div.field div.input input {
2160 2160 width: 300px;
2161 2161 background: #FFF;
2162 2162 border-top: 1px solid #b3b3b3;
2163 2163 border-left: 1px solid #b3b3b3;
2164 2164 border-right: 1px solid #eaeaea;
2165 2165 border-bottom: 1px solid #eaeaea;
2166 2166 color: #000;
2167 2167 font-size: 11px;
2168 2168 margin: 0;
2169 2169 padding: 7px 7px 6px;
2170 2170 }
2171 2171
2172 2172 #register div.form div.fields div.buttons {
2173 2173 clear: both;
2174 2174 overflow: hidden;
2175 2175 border-top: 1px solid #DDD;
2176 2176 text-align: left;
2177 2177 margin: 0;
2178 2178 padding: 10px 0 0 150px;
2179 2179 }
2180 2180
2181 2181 #register div.form div.activation_msg {
2182 2182 padding-top: 4px;
2183 2183 padding-bottom: 4px;
2184 2184 }
2185 2185
2186 2186 #journal .journal_day {
2187 2187 font-size: 20px;
2188 2188 padding: 10px 0px;
2189 2189 border-bottom: 2px solid #DDD;
2190 2190 margin-left: 10px;
2191 2191 margin-right: 10px;
2192 2192 }
2193 2193
2194 2194 #journal .journal_container {
2195 2195 padding: 5px;
2196 2196 clear: both;
2197 2197 margin: 0px 5px 0px 10px;
2198 2198 }
2199 2199
2200 2200 #journal .journal_action_container {
2201 2201 padding-left: 38px;
2202 2202 }
2203 2203
2204 2204 #journal .journal_user {
2205 2205 color: #747474;
2206 2206 font-size: 14px;
2207 2207 font-weight: bold;
2208 2208 height: 30px;
2209 2209 }
2210 2210
2211 2211 #journal .journal_user.deleted {
2212 2212 color: #747474;
2213 2213 font-size: 14px;
2214 2214 font-weight: normal;
2215 2215 height: 30px;
2216 2216 font-style: italic;
2217 2217 }
2218 2218
2219 2219
2220 2220 #journal .journal_icon {
2221 2221 clear: both;
2222 2222 float: left;
2223 2223 padding-right: 4px;
2224 2224 padding-top: 3px;
2225 2225 }
2226 2226
2227 2227 #journal .journal_action {
2228 2228 padding-top: 4px;
2229 2229 min-height: 2px;
2230 2230 float: left
2231 2231 }
2232 2232
2233 2233 #journal .journal_action_params {
2234 2234 clear: left;
2235 2235 padding-left: 22px;
2236 2236 }
2237 2237
2238 2238 #journal .journal_repo {
2239 2239 float: left;
2240 2240 margin-left: 6px;
2241 2241 padding-top: 3px;
2242 2242 }
2243 2243
2244 2244 #journal .date {
2245 2245 clear: both;
2246 2246 color: #777777;
2247 2247 font-size: 11px;
2248 2248 padding-left: 22px;
2249 2249 }
2250 2250
2251 2251 #journal .journal_repo .journal_repo_name {
2252 2252 font-weight: bold;
2253 2253 font-size: 1.1em;
2254 2254 }
2255 2255
2256 2256 #journal .compare_view {
2257 2257 padding: 5px 0px 5px 0px;
2258 2258 width: 95px;
2259 2259 }
2260 2260
2261 2261 .journal_highlight {
2262 2262 font-weight: bold;
2263 2263 padding: 0 2px;
2264 2264 vertical-align: bottom;
2265 2265 }
2266 2266
2267 2267 .trending_language_tbl, .trending_language_tbl td {
2268 2268 border: 0 !important;
2269 2269 margin: 0 !important;
2270 2270 padding: 0 !important;
2271 2271 }
2272 2272
2273 2273 .trending_language_tbl, .trending_language_tbl tr {
2274 2274 border-spacing: 1px;
2275 2275 }
2276 2276
2277 2277 .trending_language {
2278 2278 background-color: #003367;
2279 2279 color: #FFF;
2280 2280 display: block;
2281 2281 min-width: 20px;
2282 2282 text-decoration: none;
2283 2283 height: 12px;
2284 2284 margin-bottom: 0px;
2285 2285 margin-left: 5px;
2286 2286 white-space: pre;
2287 2287 padding: 3px;
2288 2288 }
2289 2289
2290 2290 h3.files_location {
2291 2291 font-size: 1.8em;
2292 2292 font-weight: 700;
2293 2293 border-bottom: none !important;
2294 2294 margin: 10px 0 !important;
2295 2295 }
2296 2296
2297 2297 #files_data dl dt {
2298 2298 float: left;
2299 2299 width: 60px;
2300 2300 margin: 0 !important;
2301 2301 padding: 5px;
2302 2302 }
2303 2303
2304 2304 #files_data dl dd {
2305 2305 margin: 0 !important;
2306 2306 padding: 5px !important;
2307 2307 }
2308 2308
2309 #files_data .codeblock #editor_container .error-message {
2310 color: red;
2311 padding: 10px 10px 10px 26px
2312 }
2313
2309 2314 .file_history {
2310 2315 padding-top: 10px;
2311 2316 font-size: 16px;
2312 2317 }
2313 2318 .file_author {
2314 2319 float: left;
2315 2320 }
2316 2321
2317 2322 .file_author .item {
2318 2323 float: left;
2319 2324 padding: 5px;
2320 2325 color: #888;
2321 2326 }
2322 2327
2323 2328 .tablerow0 {
2324 2329 background-color: #F8F8F8;
2325 2330 }
2326 2331
2327 2332 .tablerow1 {
2328 2333 background-color: #FFFFFF;
2329 2334 }
2330 2335
2331 2336 .changeset_id {
2332 2337 color: #666666;
2333 2338 margin-right: -3px;
2334 2339 }
2335 2340
2336 2341 .changeset_hash {
2337 2342 color: #000000;
2338 2343 }
2339 2344
2340 2345 #changeset_content {
2341 2346 border-left: 1px solid #CCC;
2342 2347 border-right: 1px solid #CCC;
2343 2348 border-bottom: 1px solid #CCC;
2344 2349 padding: 5px;
2345 2350 }
2346 2351
2347 2352 #changeset_compare_view_content {
2348 2353 border: 1px solid #CCC;
2349 2354 padding: 5px;
2350 2355 }
2351 2356
2352 2357 #changeset_content .container {
2353 2358 min-height: 100px;
2354 2359 font-size: 1.2em;
2355 2360 overflow: hidden;
2356 2361 }
2357 2362
2358 2363 #changeset_compare_view_content .compare_view_commits {
2359 2364 width: auto !important;
2360 2365 }
2361 2366
2362 2367 #changeset_compare_view_content .compare_view_commits td {
2363 2368 padding: 0px 0px 0px 12px !important;
2364 2369 }
2365 2370
2366 2371 #changeset_content .container .right {
2367 2372 float: right;
2368 2373 width: 20%;
2369 2374 text-align: right;
2370 2375 }
2371 2376
2372 2377 #changeset_content .container .message {
2373 2378 white-space: pre-wrap;
2374 2379 }
2375 2380 #changeset_content .container .message a:hover {
2376 2381 text-decoration: none;
2377 2382 }
2378 2383 .cs_files .cur_cs {
2379 2384 margin: 10px 2px;
2380 2385 font-weight: bold;
2381 2386 }
2382 2387
2383 2388 .cs_files .node {
2384 2389 float: left;
2385 2390 }
2386 2391
2387 2392 .cs_files .changes {
2388 2393 float: right;
2389 2394 color: #003367;
2390 2395 }
2391 2396
2392 2397 .cs_files .changes .added {
2393 2398 background-color: #BBFFBB;
2394 2399 float: left;
2395 2400 text-align: center;
2396 2401 font-size: 9px;
2397 2402 padding: 2px 0px 2px 0px;
2398 2403 }
2399 2404
2400 2405 .cs_files .changes .deleted {
2401 2406 background-color: #FF8888;
2402 2407 float: left;
2403 2408 text-align: center;
2404 2409 font-size: 9px;
2405 2410 padding: 2px 0px 2px 0px;
2406 2411 }
2407 2412 /*new binary
2408 2413 NEW_FILENODE = 1
2409 2414 DEL_FILENODE = 2
2410 2415 MOD_FILENODE = 3
2411 2416 RENAMED_FILENODE = 4
2412 2417 CHMOD_FILENODE = 5
2413 2418 BIN_FILENODE = 6
2414 2419 */
2415 2420 .cs_files .changes .bin {
2416 2421 background-color: #BBFFBB;
2417 2422 float: left;
2418 2423 text-align: center;
2419 2424 font-size: 9px;
2420 2425 padding: 2px 0px 2px 0px;
2421 2426 }
2422 2427 .cs_files .changes .bin.bin1 {
2423 2428 background-color: #BBFFBB;
2424 2429 }
2425 2430
2426 2431 /*deleted binary*/
2427 2432 .cs_files .changes .bin.bin2 {
2428 2433 background-color: #FF8888;
2429 2434 }
2430 2435
2431 2436 /*mod binary*/
2432 2437 .cs_files .changes .bin.bin3 {
2433 2438 background-color: #DDDDDD;
2434 2439 }
2435 2440
2436 2441 /*rename file*/
2437 2442 .cs_files .changes .bin.bin4 {
2438 2443 background-color: #6D99FF;
2439 2444 }
2440 2445
2441 2446 /*rename file*/
2442 2447 .cs_files .changes .bin.bin4 {
2443 2448 background-color: #6D99FF;
2444 2449 }
2445 2450
2446 2451 /*chmod file*/
2447 2452 .cs_files .changes .bin.bin5 {
2448 2453 background-color: #6D99FF;
2449 2454 }
2450 2455
2451 2456 .cs_files .cs_added, .cs_files .cs_A {
2452 2457 background: url("../images/icons/page_white_add.png") no-repeat scroll
2453 2458 3px;
2454 2459 height: 16px;
2455 2460 padding-left: 20px;
2456 2461 margin-top: 7px;
2457 2462 text-align: left;
2458 2463 }
2459 2464
2460 2465 .cs_files .cs_changed, .cs_files .cs_M {
2461 2466 background: url("../images/icons/page_white_edit.png") no-repeat scroll
2462 2467 3px;
2463 2468 height: 16px;
2464 2469 padding-left: 20px;
2465 2470 margin-top: 7px;
2466 2471 text-align: left;
2467 2472 }
2468 2473
2469 2474 .cs_files .cs_removed, .cs_files .cs_D {
2470 2475 background: url("../images/icons/page_white_delete.png") no-repeat
2471 2476 scroll 3px;
2472 2477 height: 16px;
2473 2478 padding-left: 20px;
2474 2479 margin-top: 7px;
2475 2480 text-align: left;
2476 2481 }
2477 2482
2478 2483 .table {
2479 2484 position: relative;
2480 2485 }
2481 2486
2482 2487 #graph {
2483 2488 position: relative;
2484 2489 overflow: hidden;
2485 2490 }
2486 2491
2487 2492 #graph_nodes {
2488 2493 position: absolute;
2489 2494 }
2490 2495
2491 2496 #graph_content,
2492 2497 #graph .info_box,
2493 2498 #graph .container_header {
2494 2499 margin-left: 100px;
2495 2500 }
2496 2501
2497 2502 #graph_content {
2498 2503 position: relative;
2499 2504 }
2500 2505
2501 2506 #graph .container_header {
2502 2507 padding: 10px;
2503 2508 height: 25px;
2504 2509 }
2505 2510
2506 2511 #graph_content #rev_range_container {
2507 2512 float: left;
2508 2513 margin: 0px 0px 0px 3px;
2509 2514 }
2510 2515
2511 2516 #graph_content #rev_range_clear {
2512 2517 float: left;
2513 2518 margin: 0px 0px 0px 3px;
2514 2519 }
2515 2520
2516 2521 #graph_content #changesets {
2517 2522 table-layout: fixed;
2518 2523 border-collapse: collapse;
2519 2524 border-left: none;
2520 2525 border-right: none;
2521 2526 border-color: #cdcdcd;
2522 2527 }
2523 2528
2524 2529 #graph_content #changesets td {
2525 2530 overflow: hidden;
2526 2531 text-overflow: ellipsis;
2527 2532 white-space: nowrap;
2528 2533 height: 31px;
2529 2534 border-color: #cdcdcd;
2530 2535 text-align: left;
2531 2536 }
2532 2537
2533 2538 #graph_content .container .checkbox {
2534 2539 width: 12px;
2535 2540 font-size: 0.85em;
2536 2541 }
2537 2542
2538 2543 #graph_content .container .status {
2539 2544 width: 14px;
2540 2545 font-size: 0.85em;
2541 2546 }
2542 2547
2543 2548 #graph_content .container .author {
2544 2549 width: 105px;
2545 2550 }
2546 2551
2547 2552 #graph_content .container .hash {
2548 2553 width: 100px;
2549 2554 font-size: 0.85em;
2550 2555 }
2551 2556
2552 2557 #graph_content #changesets .container .date {
2553 2558 width: 76px;
2554 2559 color: #666;
2555 2560 font-size: 10px;
2556 2561 }
2557 2562
2558 2563 #graph_content #changesets .container .right {
2559 2564 width: 120px;
2560 2565 padding-right: 0px;
2561 2566 overflow: visible;
2562 2567 position: relative;
2563 2568 }
2564 2569
2565 2570 #graph_content .container .mid {
2566 2571 padding: 0;
2567 2572 }
2568 2573
2569 2574 #graph_content .log-container {
2570 2575 position: relative;
2571 2576 }
2572 2577
2573 2578 #graph_content .container .changeset_range {
2574 2579 float: left;
2575 2580 margin: 6px 3px;
2576 2581 }
2577 2582
2578 2583 #graph_content .container .author img {
2579 2584 vertical-align: middle;
2580 2585 }
2581 2586
2582 2587 #graph_content .container .author .user {
2583 2588 color: #444444;
2584 2589 }
2585 2590
2586 2591 #graph_content .container .mid .message {
2587 2592 white-space: pre-wrap;
2588 2593 padding: 0;
2589 2594 overflow: hidden;
2590 2595 height: 1.1em;
2591 2596 }
2592 2597
2593 2598 #graph_content .container .extra-container {
2594 2599 display: block;
2595 2600 position: absolute;
2596 2601 top: -15px;
2597 2602 right: 0;
2598 2603 padding-left: 5px;
2599 2604 background: #FFFFFF;
2600 2605 height: 41px;
2601 2606 }
2602 2607
2603 2608 #graph_content .comments-container,
2604 2609 #shortlog_data .comments-container,
2605 2610 #graph_content .logtags {
2606 2611 display: block;
2607 2612 float: left;
2608 2613 overflow: hidden;
2609 2614 padding: 0;
2610 2615 margin: 0;
2611 2616 }
2612 2617
2613 2618 #graph_content .comments-container {
2614 2619 margin: 0.8em 0;
2615 2620 margin-right: 0.5em;
2616 2621 }
2617 2622
2618 2623 #graph_content .tagcontainer {
2619 2624 width: 80px;
2620 2625 position: relative;
2621 2626 float: right;
2622 2627 height: 100%;
2623 2628 top: 7px;
2624 2629 margin-left: 0.5em;
2625 2630 }
2626 2631
2627 2632 #graph_content .logtags {
2628 2633 min-width: 80px;
2629 2634 height: 1.1em;
2630 2635 position: absolute;
2631 2636 left: 0px;
2632 2637 width: auto;
2633 2638 top: 0px;
2634 2639 }
2635 2640
2636 2641 #graph_content .logtags.tags {
2637 2642 top: 14px;
2638 2643 }
2639 2644
2640 2645 #graph_content .logtags:hover {
2641 2646 overflow: visible;
2642 2647 position: absolute;
2643 2648 width: auto;
2644 2649 right: 0;
2645 2650 left: initial;
2646 2651 }
2647 2652
2648 2653 #graph_content .logtags .booktag,
2649 2654 #graph_content .logtags .tagtag {
2650 2655 float: left;
2651 2656 line-height: 1em;
2652 2657 margin-bottom: 1px;
2653 2658 margin-right: 1px;
2654 2659 padding: 1px 3px;
2655 2660 font-size: 10px;
2656 2661 }
2657 2662
2658 2663 #graph_content .container .mid .message a:hover {
2659 2664 text-decoration: none;
2660 2665 }
2661 2666
2662 2667 .revision-link {
2663 2668 color: #3F6F9F;
2664 2669 font-weight: bold !important;
2665 2670 }
2666 2671
2667 2672 .issue-tracker-link {
2668 2673 color: #3F6F9F;
2669 2674 font-weight: bold !important;
2670 2675 }
2671 2676
2672 2677 .changeset-status-container {
2673 2678 padding-right: 5px;
2674 2679 margin-top: 1px;
2675 2680 float: right;
2676 2681 height: 14px;
2677 2682 }
2678 2683 .code-header .changeset-status-container {
2679 2684 float: left;
2680 2685 padding: 2px 0px 0px 2px;
2681 2686 }
2682 2687 .changeset-status-container .changeset-status-lbl {
2683 2688 color: rgb(136, 136, 136);
2684 2689 float: left;
2685 2690 padding: 3px 4px 0px 0px
2686 2691 }
2687 2692 .code-header .changeset-status-container .changeset-status-lbl {
2688 2693 float: left;
2689 2694 padding: 0px 4px 0px 0px;
2690 2695 }
2691 2696 .changeset-status-container .changeset-status-ico {
2692 2697 float: left;
2693 2698 }
2694 2699 .code-header .changeset-status-container .changeset-status-ico, .container .changeset-status-ico {
2695 2700 float: left;
2696 2701 }
2697 2702
2698 2703 #graph_content .comments-cnt {
2699 2704 color: rgb(136, 136, 136);
2700 2705 padding: 5px 0;
2701 2706 }
2702 2707
2703 2708 #shortlog_data .comments-cnt {
2704 2709 color: rgb(136, 136, 136);
2705 2710 padding: 3px 0;
2706 2711 }
2707 2712
2708 2713 #graph_content .comments-cnt a,
2709 2714 #shortlog_data .comments-cnt a {
2710 2715 background-image: url('../images/icons/comments.png');
2711 2716 background-repeat: no-repeat;
2712 2717 background-position: 100% 50%;
2713 2718 padding: 5px 0;
2714 2719 padding-right: 20px;
2715 2720 }
2716 2721
2717 2722 .right .changes {
2718 2723 clear: both;
2719 2724 }
2720 2725
2721 2726 .right .changes .changed_total {
2722 2727 display: block;
2723 2728 float: right;
2724 2729 text-align: center;
2725 2730 min-width: 45px;
2726 2731 cursor: pointer;
2727 2732 color: #444444;
2728 2733 background: #FEA;
2729 2734 -webkit-border-radius: 0px 0px 0px 6px;
2730 2735 border-radius: 0px 0px 0px 6px;
2731 2736 padding: 1px;
2732 2737 }
2733 2738
2734 2739 .right .changes .added, .changed, .removed {
2735 2740 display: block;
2736 2741 padding: 1px;
2737 2742 color: #444444;
2738 2743 float: right;
2739 2744 text-align: center;
2740 2745 min-width: 15px;
2741 2746 }
2742 2747
2743 2748 .right .changes .added {
2744 2749 background: #CFC;
2745 2750 }
2746 2751
2747 2752 .right .changes .changed {
2748 2753 background: #FEA;
2749 2754 }
2750 2755
2751 2756 .right .changes .removed {
2752 2757 background: #FAA;
2753 2758 }
2754 2759
2755 2760 .right .merge {
2756 2761 padding: 1px 3px 1px 3px;
2757 2762 background-color: #fca062;
2758 2763 font-size: 10px;
2759 2764 color: #ffffff;
2760 2765 text-transform: uppercase;
2761 2766 white-space: nowrap;
2762 2767 -webkit-border-radius: 3px;
2763 2768 border-radius: 3px;
2764 2769 margin-right: 2px;
2765 2770 }
2766 2771
2767 2772 .right .parent {
2768 2773 color: #666666;
2769 2774 clear: both;
2770 2775 }
2771 2776 .right .logtags {
2772 2777 line-height: 2.2em;
2773 2778 }
2774 2779 .branchtag, .logtags .tagtag, .logtags .booktag {
2775 2780 margin: 0px 2px;
2776 2781 }
2777 2782
2778 2783 .branchtag,
2779 2784 .tagtag,
2780 2785 .booktag,
2781 2786 .spantag {
2782 2787 padding: 1px 3px 1px 3px;
2783 2788 font-size: 10px;
2784 2789 color: #336699;
2785 2790 white-space: nowrap;
2786 2791 -webkit-border-radius: 4px;
2787 2792 border-radius: 4px;
2788 2793 border: 1px solid #d9e8f8;
2789 2794 line-height: 1.5em;
2790 2795 }
2791 2796
2792 2797 #graph_content .branchtag,
2793 2798 #graph_content .tagtag,
2794 2799 #graph_content .booktag {
2795 2800 margin: 1.1em 0;
2796 2801 margin-right: 0.5em;
2797 2802 }
2798 2803
2799 2804 .branchtag,
2800 2805 .tagtag,
2801 2806 .booktag {
2802 2807 float: left;
2803 2808 }
2804 2809
2805 2810 .right .logtags .branchtag,
2806 2811 .right .logtags .tagtag,
2807 2812 .right .logtags .booktag,
2808 2813 .right .merge {
2809 2814 float: right;
2810 2815 line-height: 1em;
2811 2816 margin: 1px 1px !important;
2812 2817 display: block;
2813 2818 }
2814 2819
2815 2820 .booktag {
2816 2821 border-color: #46A546;
2817 2822 color: #46A546;
2818 2823 }
2819 2824
2820 2825 .tagtag {
2821 2826 border-color: #62cffc;
2822 2827 color: #62cffc;
2823 2828 }
2824 2829
2825 2830 .logtags .branchtag a:hover,
2826 2831 .logtags .branchtag a,
2827 2832 .branchtag a,
2828 2833 .branchtag a:hover {
2829 2834 text-decoration: none;
2830 2835 color: inherit;
2831 2836 }
2832 2837 .logtags .tagtag {
2833 2838 padding: 1px 3px 1px 3px;
2834 2839 background-color: #62cffc;
2835 2840 font-size: 10px;
2836 2841 color: #ffffff;
2837 2842 white-space: nowrap;
2838 2843 -webkit-border-radius: 3px;
2839 2844 border-radius: 3px;
2840 2845 }
2841 2846
2842 2847 .tagtag a,
2843 2848 .tagtag a:hover,
2844 2849 .logtags .tagtag a,
2845 2850 .logtags .tagtag a:hover {
2846 2851 text-decoration: none;
2847 2852 color: inherit;
2848 2853 }
2849 2854 .logbooks .booktag, .logbooks .booktag, .logtags .booktag, .logtags .booktag {
2850 2855 padding: 1px 3px 1px 3px;
2851 2856 background-color: #46A546;
2852 2857 font-size: 10px;
2853 2858 color: #ffffff;
2854 2859 white-space: nowrap;
2855 2860 -webkit-border-radius: 3px;
2856 2861 border-radius: 3px;
2857 2862 }
2858 2863 .logbooks .booktag, .logbooks .booktag a, .right .logtags .booktag, .logtags .booktag a {
2859 2864 color: #ffffff;
2860 2865 }
2861 2866
2862 2867 .logbooks .booktag, .logbooks .booktag a:hover,
2863 2868 .logtags .booktag, .logtags .booktag a:hover,
2864 2869 .booktag a,
2865 2870 .booktag a:hover {
2866 2871 text-decoration: none;
2867 2872 color: inherit;
2868 2873 }
2869 2874 div.browserblock {
2870 2875 overflow: hidden;
2871 2876 border: 1px solid #ccc;
2872 2877 background: #f8f8f8;
2873 2878 font-size: 100%;
2874 2879 line-height: 125%;
2875 2880 padding: 0;
2876 2881 -webkit-border-radius: 6px 6px 0px 0px;
2877 2882 border-radius: 6px 6px 0px 0px;
2878 2883 }
2879 2884
2880 2885 div.browserblock .browser-header {
2881 2886 background: #FFF;
2882 2887 padding: 10px 0px 15px 0px;
2883 2888 width: 100%;
2884 2889 }
2885 2890
2886 2891 div.browserblock .browser-nav {
2887 2892 float: left
2888 2893 }
2889 2894
2890 2895 div.browserblock .browser-branch {
2891 2896 float: left;
2892 2897 }
2893 2898
2894 2899 div.browserblock .browser-branch label {
2895 2900 color: #4A4A4A;
2896 2901 vertical-align: text-top;
2897 2902 }
2898 2903
2899 2904 div.browserblock .browser-header span {
2900 2905 margin-left: 5px;
2901 2906 font-weight: 700;
2902 2907 }
2903 2908
2904 2909 div.browserblock .browser-search {
2905 2910 clear: both;
2906 2911 padding: 8px 8px 0px 5px;
2907 2912 height: 20px;
2908 2913 }
2909 2914
2910 2915 div.browserblock #node_filter_box {
2911 2916 }
2912 2917
2913 2918 div.browserblock .search_activate {
2914 2919 float: left
2915 2920 }
2916 2921
2917 2922 div.browserblock .add_node {
2918 2923 float: left;
2919 2924 padding-left: 5px;
2920 2925 }
2921 2926
2922 2927 div.browserblock .search_activate a:hover, div.browserblock .add_node a:hover {
2923 2928 text-decoration: none !important;
2924 2929 }
2925 2930
2926 2931 div.browserblock .browser-body {
2927 2932 background: #EEE;
2928 2933 border-top: 1px solid #CCC;
2929 2934 }
2930 2935
2931 2936 table.code-browser {
2932 2937 border-collapse: collapse;
2933 2938 width: 100%;
2934 2939 }
2935 2940
2936 2941 table.code-browser tr {
2937 2942 margin: 3px;
2938 2943 }
2939 2944
2940 2945 table.code-browser thead th {
2941 2946 background-color: #EEE;
2942 2947 height: 20px;
2943 2948 font-size: 1.1em;
2944 2949 font-weight: 700;
2945 2950 text-align: left;
2946 2951 padding-left: 10px;
2947 2952 }
2948 2953
2949 2954 table.code-browser tbody td {
2950 2955 padding-left: 10px;
2951 2956 height: 20px;
2952 2957 }
2953 2958
2954 2959 table.code-browser .browser-file {
2955 2960 background: url("../images/icons/document_16.png") no-repeat scroll 3px;
2956 2961 height: 16px;
2957 2962 padding-left: 20px;
2958 2963 text-align: left;
2959 2964 }
2960 2965 .diffblock .changeset_header {
2961 2966 height: 16px;
2962 2967 }
2963 2968 .diffblock .changeset_file {
2964 2969 background: url("../images/icons/file.png") no-repeat scroll 3px;
2965 2970 text-align: left;
2966 2971 float: left;
2967 2972 padding: 2px 0px 2px 22px;
2968 2973 }
2969 2974 .diffblock .diff-menu-wrapper {
2970 2975 float: left;
2971 2976 }
2972 2977
2973 2978 .diffblock .diff-menu {
2974 2979 position: absolute;
2975 2980 background: none repeat scroll 0 0 #FFFFFF;
2976 2981 border-color: #003367 #666666 #666666;
2977 2982 border-right: 1px solid #666666;
2978 2983 border-style: solid solid solid;
2979 2984 border-width: 1px;
2980 2985 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
2981 2986 margin-top: 5px;
2982 2987 margin-left: 1px;
2983 2988
2984 2989 }
2985 2990 .diffblock .diff-actions {
2986 2991 padding: 2px 0px 0px 2px;
2987 2992 float: left;
2988 2993 }
2989 2994 .diffblock .diff-menu ul li {
2990 2995 padding: 0px 0px 0px 0px !important;
2991 2996 }
2992 2997 .diffblock .diff-menu ul li a {
2993 2998 display: block;
2994 2999 padding: 3px 8px 3px 8px !important;
2995 3000 }
2996 3001 .diffblock .diff-menu ul li a:hover {
2997 3002 text-decoration: none;
2998 3003 background-color: #EEEEEE;
2999 3004 }
3000 3005 table.code-browser .browser-dir {
3001 3006 background: url("../images/icons/folder_16.png") no-repeat scroll 3px;
3002 3007 height: 16px;
3003 3008 padding-left: 20px;
3004 3009 text-align: left;
3005 3010 }
3006 3011
3007 3012 table.code-browser .submodule-dir {
3008 3013 background: url("../images/icons/disconnect.png") no-repeat scroll 3px;
3009 3014 height: 16px;
3010 3015 padding-left: 20px;
3011 3016 text-align: left;
3012 3017 }
3013 3018
3014 3019
3015 3020 .box .search {
3016 3021 clear: both;
3017 3022 overflow: hidden;
3018 3023 margin: 0;
3019 3024 padding: 0 20px 10px;
3020 3025 }
3021 3026
3022 3027 .box .search div.search_path {
3023 3028 background: none repeat scroll 0 0 #EEE;
3024 3029 border: 1px solid #CCC;
3025 3030 color: blue;
3026 3031 margin-bottom: 10px;
3027 3032 padding: 10px 0;
3028 3033 }
3029 3034
3030 3035 .box .search div.search_path div.link {
3031 3036 font-weight: 700;
3032 3037 margin-left: 25px;
3033 3038 }
3034 3039
3035 3040 .box .search div.search_path div.link a {
3036 3041 color: #003367;
3037 3042 cursor: pointer;
3038 3043 text-decoration: none;
3039 3044 }
3040 3045
3041 3046 #path_unlock {
3042 3047 color: red;
3043 3048 font-size: 1.2em;
3044 3049 padding-left: 4px;
3045 3050 }
3046 3051
3047 3052 .info_box span {
3048 3053 margin-left: 3px;
3049 3054 margin-right: 3px;
3050 3055 }
3051 3056
3052 3057 .info_box .rev {
3053 3058 color: #003367;
3054 3059 font-size: 1.6em;
3055 3060 font-weight: bold;
3056 3061 vertical-align: sub;
3057 3062 }
3058 3063
3059 3064 .info_box input#at_rev, .info_box input#size {
3060 3065 background: #FFF;
3061 3066 border-top: 1px solid #b3b3b3;
3062 3067 border-left: 1px solid #b3b3b3;
3063 3068 border-right: 1px solid #eaeaea;
3064 3069 border-bottom: 1px solid #eaeaea;
3065 3070 color: #000;
3066 3071 font-size: 12px;
3067 3072 margin: 0;
3068 3073 padding: 1px 5px 1px;
3069 3074 }
3070 3075
3071 3076 .info_box input#view {
3072 3077 text-align: center;
3073 3078 padding: 4px 3px 2px 2px;
3074 3079 }
3075 3080
3076 3081 .yui-overlay, .yui-panel-container {
3077 3082 visibility: hidden;
3078 3083 position: absolute;
3079 3084 z-index: 2;
3080 3085 }
3081 3086
3082 3087 #tip-box {
3083 3088 position: absolute;
3084 3089
3085 3090 background-color: #FFF;
3086 3091 border: 2px solid #003367;
3087 3092 font: 100% sans-serif;
3088 3093 width: auto;
3089 3094 opacity: 1;
3090 3095 padding: 8px;
3091 3096
3092 3097 white-space: pre-wrap;
3093 3098 -webkit-border-radius: 8px 8px 8px 8px;
3094 3099 -khtml-border-radius: 8px 8px 8px 8px;
3095 3100 border-radius: 8px 8px 8px 8px;
3096 3101 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
3097 3102 -webkit-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
3098 3103 }
3099 3104
3100 3105 .hl-tip-box {
3101 3106 visibility: hidden;
3102 3107 position: absolute;
3103 3108 color: #666;
3104 3109 background-color: #FFF;
3105 3110 border: 2px solid #003367;
3106 3111 font: 100% sans-serif;
3107 3112 width: auto;
3108 3113 opacity: 1;
3109 3114 padding: 8px;
3110 3115 white-space: pre-wrap;
3111 3116 -webkit-border-radius: 8px 8px 8px 8px;
3112 3117 -khtml-border-radius: 8px 8px 8px 8px;
3113 3118 border-radius: 8px 8px 8px 8px;
3114 3119 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
3115 3120 }
3116 3121
3117 3122
3118 3123 .mentions-container {
3119 3124 width: 90% !important;
3120 3125 }
3121 3126 .mentions-container .yui-ac-content {
3122 3127 width: 100% !important;
3123 3128 }
3124 3129
3125 3130 .ac {
3126 3131 vertical-align: top;
3127 3132 }
3128 3133
3129 3134 .ac .yui-ac {
3130 3135 position: inherit;
3131 3136 font-size: 100%;
3132 3137 }
3133 3138
3134 3139 .ac .perm_ac {
3135 3140 width: 20em;
3136 3141 }
3137 3142
3138 3143 .ac .yui-ac-input {
3139 3144 width: 100%;
3140 3145 }
3141 3146
3142 3147 .ac .yui-ac-container {
3143 3148 position: absolute;
3144 3149 top: 1.6em;
3145 3150 width: auto;
3146 3151 }
3147 3152
3148 3153 .ac .yui-ac-content {
3149 3154 position: absolute;
3150 3155 border: 1px solid gray;
3151 3156 background: #fff;
3152 3157 z-index: 9050;
3153 3158 }
3154 3159
3155 3160 .ac .yui-ac-shadow {
3156 3161 position: absolute;
3157 3162 width: 100%;
3158 3163 background: #000;
3159 3164 opacity: .10;
3160 3165 filter: alpha(opacity = 10);
3161 3166 z-index: 9049;
3162 3167 margin: .3em;
3163 3168 }
3164 3169
3165 3170 .ac .yui-ac-content ul {
3166 3171 width: 100%;
3167 3172 margin: 0;
3168 3173 padding: 0;
3169 3174 z-index: 9050;
3170 3175 }
3171 3176
3172 3177 .ac .yui-ac-content li {
3173 3178 cursor: default;
3174 3179 white-space: nowrap;
3175 3180 margin: 0;
3176 3181 padding: 2px 5px;
3177 3182 height: 18px;
3178 3183 z-index: 9050;
3179 3184 display: block;
3180 3185 width: auto !important;
3181 3186 }
3182 3187
3183 3188 .ac .yui-ac-content li .ac-container-wrap {
3184 3189 width: auto;
3185 3190 }
3186 3191
3187 3192 .ac .yui-ac-content li.yui-ac-prehighlight {
3188 3193 background: #B3D4FF;
3189 3194 z-index: 9050;
3190 3195 }
3191 3196
3192 3197 .ac .yui-ac-content li.yui-ac-highlight {
3193 3198 background: #556CB5;
3194 3199 color: #FFF;
3195 3200 z-index: 9050;
3196 3201 }
3197 3202 .ac .yui-ac-bd {
3198 3203 z-index: 9050;
3199 3204 }
3200 3205
3201 3206 .reposize {
3202 3207 background: url("../images/icons/server.png") no-repeat scroll 3px;
3203 3208 height: 16px;
3204 3209 width: 20px;
3205 3210 cursor: pointer;
3206 3211 display: block;
3207 3212 float: right;
3208 3213 margin-top: 2px;
3209 3214 }
3210 3215
3211 3216 #repo_size {
3212 3217 display: block;
3213 3218 margin-top: 4px;
3214 3219 color: #666;
3215 3220 float: right;
3216 3221 }
3217 3222
3218 3223 .locking_locked {
3219 3224 background: #FFF url("../images/icons/block_16.png") no-repeat scroll 3px;
3220 3225 height: 16px;
3221 3226 width: 20px;
3222 3227 cursor: pointer;
3223 3228 display: block;
3224 3229 float: right;
3225 3230 margin-top: 2px;
3226 3231 }
3227 3232
3228 3233 .locking_unlocked {
3229 3234 background: #FFF url("../images/icons/accept.png") no-repeat scroll 3px;
3230 3235 height: 16px;
3231 3236 width: 20px;
3232 3237 cursor: pointer;
3233 3238 display: block;
3234 3239 float: right;
3235 3240 margin-top: 2px;
3236 3241 }
3237 3242
3238 3243 .currently_following {
3239 3244 padding-left: 10px;
3240 3245 padding-bottom: 5px;
3241 3246 }
3242 3247
3243 3248 .add_icon {
3244 3249 background: url("../images/icons/add.png") no-repeat scroll 3px;
3245 3250 padding-left: 20px;
3246 3251 padding-top: 0px;
3247 3252 text-align: left;
3248 3253 }
3249 3254
3250 3255 .accept_icon {
3251 3256 background: url("../images/icons/accept.png") no-repeat scroll 3px;
3252 3257 padding-left: 20px;
3253 3258 padding-top: 0px;
3254 3259 text-align: left;
3255 3260 }
3256 3261
3257 3262 .edit_icon {
3258 3263 background: url("../images/icons/application_form_edit.png") no-repeat scroll 3px;
3259 3264 padding-left: 20px;
3260 3265 padding-top: 0px;
3261 3266 text-align: left;
3262 3267 }
3263 3268
3264 3269 .delete_icon {
3265 3270 background: url("../images/icons/delete.png") no-repeat scroll 3px;
3266 3271 padding-left: 20px;
3267 3272 padding-top: 0px;
3268 3273 text-align: left;
3269 3274 }
3270 3275
3271 3276 .refresh_icon {
3272 3277 background: url("../images/icons/arrow_refresh.png") no-repeat scroll
3273 3278 3px;
3274 3279 padding-left: 20px;
3275 3280 padding-top: 0px;
3276 3281 text-align: left;
3277 3282 }
3278 3283
3279 3284 .pull_icon {
3280 3285 background: url("../images/icons/connect.png") no-repeat scroll 3px;
3281 3286 padding-left: 20px;
3282 3287 padding-top: 0px;
3283 3288 text-align: left;
3284 3289 }
3285 3290
3286 3291 .rss_icon {
3287 3292 background: url("../images/icons/rss_16.png") no-repeat scroll 3px;
3288 3293 padding-left: 20px;
3289 3294 padding-top: 4px;
3290 3295 text-align: left;
3291 3296 font-size: 8px
3292 3297 }
3293 3298
3294 3299 .atom_icon {
3295 3300 background: url("../images/icons/rss_16.png") no-repeat scroll 3px;
3296 3301 padding-left: 20px;
3297 3302 padding-top: 4px;
3298 3303 text-align: left;
3299 3304 font-size: 8px
3300 3305 }
3301 3306
3302 3307 .archive_icon {
3303 3308 background: url("../images/icons/compress.png") no-repeat scroll 3px;
3304 3309 padding-left: 20px;
3305 3310 text-align: left;
3306 3311 padding-top: 1px;
3307 3312 }
3308 3313
3309 3314 .start_following_icon {
3310 3315 background: url("../images/icons/heart_add.png") no-repeat scroll 3px;
3311 3316 padding-left: 20px;
3312 3317 text-align: left;
3313 3318 padding-top: 0px;
3314 3319 }
3315 3320
3316 3321 .stop_following_icon {
3317 3322 background: url("../images/icons/heart_delete.png") no-repeat scroll 3px;
3318 3323 padding-left: 20px;
3319 3324 text-align: left;
3320 3325 padding-top: 0px;
3321 3326 }
3322 3327
3323 3328 .action_button {
3324 3329 border: 0;
3325 3330 display: inline;
3326 3331 }
3327 3332
3328 3333 .action_button:hover {
3329 3334 border: 0;
3330 3335 text-decoration: underline;
3331 3336 cursor: pointer;
3332 3337 }
3333 3338
3334 3339 #switch_repos {
3335 3340 position: absolute;
3336 3341 height: 25px;
3337 3342 z-index: 1;
3338 3343 }
3339 3344
3340 3345 #switch_repos select {
3341 3346 min-width: 150px;
3342 3347 max-height: 250px;
3343 3348 z-index: 1;
3344 3349 }
3345 3350
3346 3351 .breadcrumbs {
3347 3352 border: medium none;
3348 3353 color: #FFF;
3349 3354 float: left;
3350 3355 font-weight: 700;
3351 3356 font-size: 14px;
3352 3357 margin: 0;
3353 3358 padding: 11px 0 11px 10px;
3354 3359 }
3355 3360
3356 3361 .breadcrumbs .hash {
3357 3362 text-transform: none;
3358 3363 color: #fff;
3359 3364 }
3360 3365
3361 3366 .breadcrumbs a {
3362 3367 color: #FFF;
3363 3368 }
3364 3369
3365 3370 .flash_msg {
3366 3371 }
3367 3372
3368 3373 .flash_msg ul {
3369 3374 }
3370 3375
3371 3376 .error_red {
3372 3377 color: red;
3373 3378 }
3374 3379
3375 3380 .error_msg {
3376 3381 background-color: #c43c35;
3377 3382 background-repeat: repeat-x;
3378 3383 background-image: -khtml-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35) );
3379 3384 background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35);
3380 3385 background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35);
3381 3386 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), color-stop(100%, #c43c35) );
3382 3387 background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35);
3383 3388 background-image: -o-linear-gradient(top, #ee5f5b, #c43c35);
3384 3389 background-image: linear-gradient(to bottom, #ee5f5b, #c43c35);
3385 3390 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b',endColorstr='#c43c35', GradientType=0 );
3386 3391 border-color: #c43c35 #c43c35 #882a25;
3387 3392 }
3388 3393
3389 3394 .error_msg a {
3390 3395 text-decoration: underline;
3391 3396 }
3392 3397
3393 3398 .warning_msg {
3394 3399 color: #404040 !important;
3395 3400 background-color: #eedc94;
3396 3401 background-repeat: repeat-x;
3397 3402 background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1), to(#eedc94) );
3398 3403 background-image: -moz-linear-gradient(top, #fceec1, #eedc94);
3399 3404 background-image: -ms-linear-gradient(top, #fceec1, #eedc94);
3400 3405 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fceec1), color-stop(100%, #eedc94) );
3401 3406 background-image: -webkit-linear-gradient(top, #fceec1, #eedc94);
3402 3407 background-image: -o-linear-gradient(top, #fceec1, #eedc94);
3403 3408 background-image: linear-gradient(to bottom, #fceec1, #eedc94);
3404 3409 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fceec1', endColorstr='#eedc94', GradientType=0 );
3405 3410 border-color: #eedc94 #eedc94 #e4c652;
3406 3411 }
3407 3412
3408 3413 .warning_msg a {
3409 3414 text-decoration: underline;
3410 3415 }
3411 3416
3412 3417 .success_msg {
3413 3418 background-color: #57a957;
3414 3419 background-repeat: repeat-x !important;
3415 3420 background-image: -khtml-gradient(linear, left top, left bottom, from(#62c462), to(#57a957) );
3416 3421 background-image: -moz-linear-gradient(top, #62c462, #57a957);
3417 3422 background-image: -ms-linear-gradient(top, #62c462, #57a957);
3418 3423 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #62c462), color-stop(100%, #57a957) );
3419 3424 background-image: -webkit-linear-gradient(top, #62c462, #57a957);
3420 3425 background-image: -o-linear-gradient(top, #62c462, #57a957);
3421 3426 background-image: linear-gradient(to bottom, #62c462, #57a957);
3422 3427 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0 );
3423 3428 border-color: #57a957 #57a957 #3d773d;
3424 3429 }
3425 3430
3426 3431 .success_msg a {
3427 3432 text-decoration: underline;
3428 3433 color: #FFF !important;
3429 3434 }
3430 3435
3431 3436 .notice_msg {
3432 3437 background-color: #339bb9;
3433 3438 background-repeat: repeat-x;
3434 3439 background-image: -khtml-gradient(linear, left top, left bottom, from(#5bc0de), to(#339bb9) );
3435 3440 background-image: -moz-linear-gradient(top, #5bc0de, #339bb9);
3436 3441 background-image: -ms-linear-gradient(top, #5bc0de, #339bb9);
3437 3442 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #5bc0de), color-stop(100%, #339bb9) );
3438 3443 background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9);
3439 3444 background-image: -o-linear-gradient(top, #5bc0de, #339bb9);
3440 3445 background-image: linear-gradient(to bottom, #5bc0de, #339bb9);
3441 3446 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0 );
3442 3447 border-color: #339bb9 #339bb9 #22697d;
3443 3448 }
3444 3449
3445 3450 .notice_msg a {
3446 3451 text-decoration: underline;
3447 3452 }
3448 3453
3449 3454 .success_msg, .error_msg, .notice_msg, .warning_msg {
3450 3455 font-size: 12px;
3451 3456 font-weight: 700;
3452 3457 min-height: 14px;
3453 3458 line-height: 14px;
3454 3459 margin-bottom: 10px;
3455 3460 margin-top: 0;
3456 3461 display: block;
3457 3462 overflow: auto;
3458 3463 padding: 6px 10px 6px 10px;
3459 3464 border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
3460 3465 position: relative;
3461 3466 color: #FFF;
3462 3467 border-width: 1px;
3463 3468 border-style: solid;
3464 3469 -webkit-border-radius: 4px;
3465 3470 border-radius: 4px;
3466 3471 -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
3467 3472 box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
3468 3473 }
3469 3474
3470 3475 #msg_close {
3471 3476 background: transparent url("../icons/cross_grey_small.png") no-repeat scroll 0 0;
3472 3477 cursor: pointer;
3473 3478 height: 16px;
3474 3479 position: absolute;
3475 3480 right: 5px;
3476 3481 top: 5px;
3477 3482 width: 16px;
3478 3483 }
3479 3484 div#legend_data {
3480 3485 padding-left: 10px;
3481 3486 }
3482 3487 div#legend_container table {
3483 3488 border: none !important;
3484 3489 }
3485 3490 div#legend_container table, div#legend_choices table {
3486 3491 width: auto !important;
3487 3492 }
3488 3493
3489 3494 table#permissions_manage {
3490 3495 width: 0 !important;
3491 3496 }
3492 3497
3493 3498 table#permissions_manage span.private_repo_msg {
3494 3499 font-size: 0.8em;
3495 3500 opacity: 0.6;
3496 3501 }
3497 3502
3498 3503 table#permissions_manage td.private_repo_msg {
3499 3504 font-size: 0.8em;
3500 3505 }
3501 3506
3502 3507 table#permissions_manage tr#add_perm_input td {
3503 3508 vertical-align: middle;
3504 3509 }
3505 3510
3506 3511 div.gravatar {
3507 3512 background-color: #FFF;
3508 3513 float: left;
3509 3514 margin-right: 0.7em;
3510 3515 padding: 1px 1px 1px 1px;
3511 3516 line-height: 0;
3512 3517 -webkit-border-radius: 3px;
3513 3518 -khtml-border-radius: 3px;
3514 3519 border-radius: 3px;
3515 3520 }
3516 3521
3517 3522 div.gravatar img {
3518 3523 -webkit-border-radius: 2px;
3519 3524 -khtml-border-radius: 2px;
3520 3525 border-radius: 2px;
3521 3526 }
3522 3527
3523 3528 #header, #content, #footer {
3524 3529 min-width: 978px;
3525 3530 }
3526 3531
3527 3532 #content {
3528 3533 clear: both;
3529 3534 padding: 10px 10px 14px 10px;
3530 3535 }
3531 3536
3532 3537 #content.hover {
3533 3538 padding: 55px 10px 14px 10px !important;
3534 3539 }
3535 3540
3536 3541 #content div.box div.title div.search {
3537 3542 border-left: 1px solid #316293;
3538 3543 }
3539 3544
3540 3545 #content div.box div.title div.search div.input input {
3541 3546 border: 1px solid #316293;
3542 3547 }
3543 3548
3544 3549 .ui-btn {
3545 3550 color: #515151;
3546 3551 background-color: #DADADA;
3547 3552 background-repeat: repeat-x;
3548 3553 background-image: -khtml-gradient(linear, left top, left bottom, from(#F4F4F4),to(#DADADA) );
3549 3554 background-image: -moz-linear-gradient(top, #F4F4F4, #DADADA);
3550 3555 background-image: -ms-linear-gradient(top, #F4F4F4, #DADADA);
3551 3556 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #F4F4F4),color-stop(100%, #DADADA) );
3552 3557 background-image: -webkit-linear-gradient(top, #F4F4F4, #DADADA) );
3553 3558 background-image: -o-linear-gradient(top, #F4F4F4, #DADADA) );
3554 3559 background-image: linear-gradient(to bottom, #F4F4F4, #DADADA);
3555 3560 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#F4F4F4', endColorstr='#DADADA', GradientType=0);
3556 3561
3557 3562 border-top: 1px solid #DDD;
3558 3563 border-left: 1px solid #c6c6c6;
3559 3564 border-right: 1px solid #DDD;
3560 3565 border-bottom: 1px solid #c6c6c6;
3561 3566 color: #515151;
3562 3567 outline: none;
3563 3568 margin: 0px 3px 3px 0px;
3564 3569 -webkit-border-radius: 4px 4px 4px 4px !important;
3565 3570 -khtml-border-radius: 4px 4px 4px 4px !important;
3566 3571 border-radius: 4px 4px 4px 4px !important;
3567 3572 cursor: pointer !important;
3568 3573 padding: 3px 3px 3px 3px;
3569 background-position: 0 -15px;
3570
3574 background-position: 0 -100px;
3575
3576 }
3577
3578 .ui-btn.badge {
3579 cursor: default !important;
3571 3580 }
3572 3581
3573 3582 .ui-btn.disabled {
3574 3583 color: #999;
3575 3584 }
3576 3585
3577 3586 .ui-btn.xsmall {
3578 3587 padding: 1px 2px 1px 1px;
3579 3588 }
3580 3589
3581 3590 .ui-btn.large {
3582 3591 padding: 6px 12px;
3583 3592 }
3584 3593
3585 3594 .ui-btn.clone {
3586 3595 padding: 5px 2px 6px 1px;
3587 3596 margin: 0px 0px 3px -4px;
3588 3597 -webkit-border-radius: 0px 4px 4px 0px !important;
3589 3598 -khtml-border-radius: 0px 4px 4px 0px !important;
3590 3599 border-radius: 0px 4px 4px 0px !important;
3591 3600 width: 100px;
3592 3601 text-align: center;
3593 3602 display: inline-block;
3594 3603 position: relative;
3595 3604 top: -2px;
3596 3605 }
3597 3606 .ui-btn:focus {
3598 3607 outline: none;
3599 3608 }
3600 3609 .ui-btn:hover {
3601 background-position: 0 -15px;
3610 background-position: 0 -100px;
3602 3611 text-decoration: none;
3603 3612 color: #515151;
3604 3613 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25), 0 0 3px #FFFFFF !important;
3605 3614 }
3606
3615 .ui-btn.badge:hover {
3616 box-shadow: none !important;
3617 }
3607 3618 .ui-btn.disabled:hover {
3608 3619 background-position: 0;
3609 3620 color: #999;
3610 3621 text-decoration: none;
3611 3622 box-shadow: none !important;
3612 3623 }
3613 3624
3614 3625 .ui-btn.red {
3615 3626 color: #fff;
3616 3627 background-color: #c43c35;
3617 3628 background-repeat: repeat-x;
3618 3629 background-image: -khtml-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35));
3619 3630 background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35);
3620 3631 background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35);
3621 3632 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), color-stop(100%, #c43c35));
3622 3633 background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35);
3623 3634 background-image: -o-linear-gradient(top, #ee5f5b, #c43c35);
3624 3635 background-image: linear-gradient(to bottom, #ee5f5b, #c43c35);
3625 3636 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);
3626 3637 border-color: #c43c35 #c43c35 #882a25;
3627 3638 border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
3628 3639 }
3629 3640
3630 3641
3631 3642 .ui-btn.blue {
3632 3643 color: #fff;
3633 3644 background-color: #339bb9;
3634 3645 background-repeat: repeat-x;
3635 3646 background-image: -khtml-gradient(linear, left top, left bottom, from(#5bc0de), to(#339bb9));
3636 3647 background-image: -moz-linear-gradient(top, #5bc0de, #339bb9);
3637 3648 background-image: -ms-linear-gradient(top, #5bc0de, #339bb9);
3638 3649 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #5bc0de), color-stop(100%, #339bb9));
3639 3650 background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9);
3640 3651 background-image: -o-linear-gradient(top, #5bc0de, #339bb9);
3641 3652 background-image: linear-gradient(to bottom, #5bc0de, #339bb9);
3642 3653 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0);
3643 3654 border-color: #339bb9 #339bb9 #22697d;
3644 3655 border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
3645 3656 }
3646 3657
3647 3658 .ui-btn.green {
3659 color: #fff;
3648 3660 background-color: #57a957;
3649 3661 background-repeat: repeat-x;
3650 3662 background-image: -khtml-gradient(linear, left top, left bottom, from(#62c462), to(#57a957));
3651 3663 background-image: -moz-linear-gradient(top, #62c462, #57a957);
3652 3664 background-image: -ms-linear-gradient(top, #62c462, #57a957);
3653 3665 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #62c462), color-stop(100%, #57a957));
3654 3666 background-image: -webkit-linear-gradient(top, #62c462, #57a957);
3655 3667 background-image: -o-linear-gradient(top, #62c462, #57a957);
3656 3668 background-image: linear-gradient(to bottom, #62c462, #57a957);
3657 3669 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0);
3658 3670 border-color: #57a957 #57a957 #3d773d;
3659 3671 border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
3660 3672 }
3661 3673
3674 .ui-btn.yellow {
3675 color: #fff;
3676 background-color: #faa732;
3677 background-repeat: repeat-x;
3678 background-image: -khtml-gradient(linear, left top, left bottom, from(#fbb450), to(#f89406));
3679 background-image: -moz-linear-gradient(top, #fbb450, #f89406);
3680 background-image: -ms-linear-gradient(top, #fbb450, #f89406);
3681 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fbb450), color-stop(100%, #f89406));
3682 background-image: -webkit-linear-gradient(top, #fbb450, #f89406);
3683 background-image: -o-linear-gradient(top, #fbb450, #f89406);
3684 background-image: linear-gradient(to bottom, #fbb450, #f89406);
3685 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0);
3686 border-color: #f89406 #f89406 #ad6704;
3687 border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
3688 }
3689
3662 3690 .ui-btn.blue.hidden {
3663 3691 display: none;
3664 3692 }
3665 3693
3666 3694 .ui-btn.active {
3667 3695 font-weight: bold;
3668 3696 }
3669 3697
3670 3698 ins, div.options a:hover {
3671 3699 text-decoration: none;
3672 3700 }
3673 3701
3674 3702 img,
3675 3703 #header #header-inner #quick li a:hover span.normal,
3676 3704 #content div.box div.form div.fields div.field div.textarea table td table td a,
3677 3705 #clone_url,
3678 3706 #clone_url_id
3679 3707 {
3680 3708 border: none;
3681 3709 }
3682 3710
3683 3711 img.icon, .right .merge img {
3684 3712 vertical-align: bottom;
3685 3713 }
3686 3714
3687 3715 #header ul#logged-user, #content div.box div.title ul.links,
3688 3716 #content div.box div.message div.dismiss,
3689 3717 #content div.box div.traffic div.legend ul {
3690 3718 float: right;
3691 3719 margin: 0;
3692 3720 padding: 0;
3693 3721 }
3694 3722
3695 3723 #header #header-inner #home, #header #header-inner #logo,
3696 3724 #content div.box ul.left, #content div.box ol.left,
3697 3725 div#commit_history,
3698 3726 div#legend_data, div#legend_container, div#legend_choices {
3699 3727 float: left;
3700 3728 }
3701 3729
3702 3730 #header #header-inner #quick li #quick_login,
3703 3731 #header #header-inner #quick li:hover ul ul,
3704 3732 #header #header-inner #quick li:hover ul ul ul,
3705 3733 #header #header-inner #quick li:hover ul ul ul ul,
3706 3734 #content #left #menu ul.closed, #content #left #menu li ul.collapsed, .yui-tt-shadow {
3707 3735 display: none;
3708 3736 }
3709 3737
3710 3738 #header #header-inner #quick li:hover #quick_login,
3711 3739 #header #header-inner #quick li:hover ul, #header #header-inner #quick li li:hover ul, #header #header-inner #quick li li li:hover ul, #header #header-inner #quick li li li li:hover ul, #content #left #menu ul.opened, #content #left #menu li ul.expanded {
3712 3740 display: block;
3713 3741 }
3714 3742
3715 3743 #content div.graph {
3716 3744 padding: 0 10px 10px;
3717 3745 }
3718 3746
3719 3747 #content div.box div.title ul.links li a:hover,
3720 3748 #content div.box div.title ul.links li.ui-tabs-selected a {
3721 3749
3722 3750 background: #6388ad; /* Old browsers */
3723 3751 background: -moz-linear-gradient(top, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%); /* FF3.6+ */
3724 3752 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+ */
3725 3753 background: -webkit-linear-gradient(top, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%); /* Chrome10+,Safari5.1+ */
3726 3754 background: -o-linear-gradient(top, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%); /* Opera 11.10+ */
3727 3755 background: -ms-linear-gradient(top, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%); /* IE10+ */
3728 3756 background: linear-gradient(to bottom, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%); /* W3C */
3729 3757 /*filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#88bfe8', endColorstr='#70b0e0',GradientType=0 ); /* IE6-9 */*/
3730 3758 }
3731 3759
3732 3760 #content div.box ol.lower-roman, #content div.box ol.upper-roman, #content div.box ol.lower-alpha, #content div.box ol.upper-alpha, #content div.box ol.decimal {
3733 3761 margin: 10px 24px 10px 44px;
3734 3762 }
3735 3763
3736 3764 #content div.box div.form, #content div.box div.table, #content div.box div.traffic {
3737 3765 position: relative;
3738 3766 clear: both;
3739 3767 margin: 0;
3740 3768 padding: 0 20px 10px;
3741 3769 }
3742 3770
3743 3771 #content div.box div.form div.fields, #login div.form, #login div.form div.fields, #register div.form, #register div.form div.fields {
3744 3772 clear: both;
3745 3773 overflow: hidden;
3746 3774 margin: 0;
3747 3775 padding: 0;
3748 3776 }
3749 3777
3750 3778 #content div.box div.form div.fields div.field div.label span, #login div.form div.fields div.field div.label span, #register div.form div.fields div.field div.label span {
3751 3779 height: 1%;
3752 3780 display: block;
3753 3781 color: #363636;
3754 3782 margin: 0;
3755 3783 padding: 2px 0 0;
3756 3784 }
3757 3785
3758 3786 #content div.box div.form div.fields div.field div.input input.error, #login div.form div.fields div.field div.input input.error, #register div.form div.fields div.field div.input input.error {
3759 3787 background: #FBE3E4;
3760 3788 border-top: 1px solid #e1b2b3;
3761 3789 border-left: 1px solid #e1b2b3;
3762 3790 border-right: 1px solid #FBC2C4;
3763 3791 border-bottom: 1px solid #FBC2C4;
3764 3792 }
3765 3793
3766 3794 #content div.box div.form div.fields div.field div.input input.success, #login div.form div.fields div.field div.input input.success, #register div.form div.fields div.field div.input input.success {
3767 3795 background: #E6EFC2;
3768 3796 border-top: 1px solid #cebb98;
3769 3797 border-left: 1px solid #cebb98;
3770 3798 border-right: 1px solid #c6d880;
3771 3799 border-bottom: 1px solid #c6d880;
3772 3800 }
3773 3801
3774 3802 #content div.box-left div.form div.fields div.field div.textarea, #content div.box-right div.form div.fields div.field div.textarea, #content div.box div.form div.fields div.field div.select select, #content div.box table th.selected input, #content div.box table td.selected input {
3775 3803 margin: 0;
3776 3804 }
3777 3805
3778 3806 #content div.box-left div.form div.fields div.field div.select, #content div.box-left div.form div.fields div.field div.checkboxes, #content div.box-left div.form div.fields div.field div.radios, #content div.box-right div.form div.fields div.field div.select, #content div.box-right div.form div.fields div.field div.checkboxes, #content div.box-right div.form div.fields div.field div.radios {
3779 3807 margin: 0 0 0 0px !important;
3780 3808 padding: 0;
3781 3809 }
3782 3810
3783 3811 #content div.box div.form div.fields div.field div.select, #content div.box div.form div.fields div.field div.checkboxes, #content div.box div.form div.fields div.field div.radios {
3784 3812 margin: 0 0 0 200px;
3785 3813 padding: 0;
3786 3814 }
3787 3815
3788 3816 #content div.box div.form div.fields div.field div.select a:hover, #content div.box div.form div.fields div.field div.select a.ui-selectmenu:hover, #content div.box div.action a:hover {
3789 3817 color: #000;
3790 3818 text-decoration: none;
3791 3819 }
3792 3820
3793 3821 #content div.box div.form div.fields div.field div.select a.ui-selectmenu-focus, #content div.box div.action a.ui-selectmenu-focus {
3794 3822 border: 1px solid #666;
3795 3823 }
3796 3824
3797 3825 #content div.box div.form div.fields div.field div.checkboxes div.checkbox, #content div.box div.form div.fields div.field div.radios div.radio {
3798 3826 clear: both;
3799 3827 overflow: hidden;
3800 3828 margin: 0;
3801 3829 padding: 8px 0 2px;
3802 3830 }
3803 3831
3804 3832 #content div.box div.form div.fields div.field div.checkboxes div.checkbox input, #content div.box div.form div.fields div.field div.radios div.radio input {
3805 3833 float: left;
3806 3834 margin: 0;
3807 3835 }
3808 3836
3809 3837 #content div.box div.form div.fields div.field div.checkboxes div.checkbox label, #content div.box div.form div.fields div.field div.radios div.radio label {
3810 3838 height: 1%;
3811 3839 display: block;
3812 3840 float: left;
3813 3841 margin: 2px 0 0 4px;
3814 3842 }
3815 3843
3816 3844 div.form div.fields div.field div.button input,
3817 3845 #content div.box div.form div.fields div.buttons input
3818 3846 div.form div.fields div.buttons input,
3819 3847 #content div.box div.action div.button input {
3820 3848 font-size: 11px;
3821 3849 font-weight: 700;
3822 3850 margin: 0;
3823 3851 }
3824 3852
3825 3853 input.ui-button {
3826 3854 background: #e5e3e3 url("../images/button.png") repeat-x;
3827 3855 border-top: 1px solid #DDD;
3828 3856 border-left: 1px solid #c6c6c6;
3829 3857 border-right: 1px solid #DDD;
3830 3858 border-bottom: 1px solid #c6c6c6;
3831 3859 color: #515151 !important;
3832 3860 outline: none;
3833 3861 margin: 0;
3834 3862 padding: 6px 12px;
3835 3863 -webkit-border-radius: 4px 4px 4px 4px;
3836 3864 -khtml-border-radius: 4px 4px 4px 4px;
3837 3865 border-radius: 4px 4px 4px 4px;
3838 3866 box-shadow: 0 1px 0 #ececec;
3839 3867 cursor: pointer;
3840 3868 }
3841 3869
3842 3870 input.ui-button:hover {
3843 3871 background: #b4b4b4 url("../images/button_selected.png") repeat-x;
3844 3872 border-top: 1px solid #ccc;
3845 3873 border-left: 1px solid #bebebe;
3846 3874 border-right: 1px solid #b1b1b1;
3847 3875 border-bottom: 1px solid #afafaf;
3848 3876 }
3849 3877
3850 3878 div.form div.fields div.field div.highlight, #content div.box div.form div.fields div.buttons div.highlight {
3851 3879 display: inline;
3852 3880 }
3853 3881
3854 3882 #content div.box div.form div.fields div.buttons, div.form div.fields div.buttons {
3855 3883 margin: 10px 0 0 200px;
3856 3884 padding: 0;
3857 3885 }
3858 3886
3859 3887 #content div.box-left div.form div.fields div.buttons, #content div.box-right div.form div.fields div.buttons, div.box-left div.form div.fields div.buttons, div.box-right div.form div.fields div.buttons {
3860 3888 margin: 10px 0 0;
3861 3889 }
3862 3890
3863 3891 #content div.box table td.user, #content div.box table td.address {
3864 3892 width: 10%;
3865 3893 text-align: center;
3866 3894 }
3867 3895
3868 3896 #content div.box div.action div.button, #login div.form div.fields div.field div.input div.link, #register div.form div.fields div.field div.input div.link {
3869 3897 text-align: right;
3870 3898 margin: 6px 0 0;
3871 3899 padding: 0;
3872 3900 }
3873 3901
3874 3902 #content div.box div.action div.button input.ui-state-hover, #login div.form div.fields div.buttons input.ui-state-hover, #register div.form div.fields div.buttons input.ui-state-hover {
3875 3903 background: #b4b4b4 url("../images/button_selected.png") repeat-x;
3876 3904 border-top: 1px solid #ccc;
3877 3905 border-left: 1px solid #bebebe;
3878 3906 border-right: 1px solid #b1b1b1;
3879 3907 border-bottom: 1px solid #afafaf;
3880 3908 color: #515151;
3881 3909 margin: 0;
3882 3910 padding: 6px 12px;
3883 3911 }
3884 3912
3885 3913 #content div.box div.pagination div.results, #content div.box div.pagination-wh div.results {
3886 3914 text-align: left;
3887 3915 float: left;
3888 3916 margin: 0;
3889 3917 padding: 0;
3890 3918 }
3891 3919
3892 3920 #content div.box div.pagination div.results span, #content div.box div.pagination-wh div.results span {
3893 3921 height: 1%;
3894 3922 display: block;
3895 3923 float: left;
3896 3924 background: #ebebeb url("../images/pager.png") repeat-x;
3897 3925 border-top: 1px solid #dedede;
3898 3926 border-left: 1px solid #cfcfcf;
3899 3927 border-right: 1px solid #c4c4c4;
3900 3928 border-bottom: 1px solid #c4c4c4;
3901 3929 color: #4A4A4A;
3902 3930 font-weight: 700;
3903 3931 margin: 0;
3904 3932 padding: 6px 8px;
3905 3933 }
3906 3934
3907 3935 #content div.box div.pagination ul.pager li.disabled, #content div.box div.pagination-wh a.disabled {
3908 3936 color: #B4B4B4;
3909 3937 padding: 6px;
3910 3938 }
3911 3939
3912 3940 #login, #register {
3913 3941 width: 520px;
3914 3942 margin: 10% auto 0;
3915 3943 padding: 0;
3916 3944 }
3917 3945
3918 3946 #login div.color, #register div.color {
3919 3947 clear: both;
3920 3948 overflow: hidden;
3921 3949 background: #FFF;
3922 3950 margin: 10px auto 0;
3923 3951 padding: 3px 3px 3px 0;
3924 3952 }
3925 3953
3926 3954 #login div.color a, #register div.color a {
3927 3955 width: 20px;
3928 3956 height: 20px;
3929 3957 display: block;
3930 3958 float: left;
3931 3959 margin: 0 0 0 3px;
3932 3960 padding: 0;
3933 3961 }
3934 3962
3935 3963 #login div.title h5, #register div.title h5 {
3936 3964 color: #fff;
3937 3965 margin: 10px;
3938 3966 padding: 0;
3939 3967 }
3940 3968
3941 3969 #login div.form div.fields div.field, #register div.form div.fields div.field {
3942 3970 clear: both;
3943 3971 overflow: hidden;
3944 3972 margin: 0;
3945 3973 padding: 0 0 10px;
3946 3974 }
3947 3975
3948 3976 #login div.form div.fields div.field span.error-message, #register div.form div.fields div.field span.error-message {
3949 3977 height: 1%;
3950 3978 display: block;
3951 3979 color: red;
3952 3980 margin: 8px 0 0;
3953 3981 padding: 0;
3954 3982 max-width: 320px;
3955 3983 }
3956 3984
3957 3985 #login div.form div.fields div.field div.label label, #register div.form div.fields div.field div.label label {
3958 3986 color: #000;
3959 3987 font-weight: 700;
3960 3988 }
3961 3989
3962 3990 #login div.form div.fields div.field div.input, #register div.form div.fields div.field div.input {
3963 3991 float: left;
3964 3992 margin: 0;
3965 3993 padding: 0;
3966 3994 }
3967 3995
3968 3996 #login div.form div.fields div.field div.input input.large {
3969 3997 width: 250px;
3970 3998 }
3971 3999
3972 4000 #login div.form div.fields div.field div.checkbox, #register div.form div.fields div.field div.checkbox {
3973 4001 margin: 0 0 0 184px;
3974 4002 padding: 0;
3975 4003 }
3976 4004
3977 4005 #login div.form div.fields div.field div.checkbox label, #register div.form div.fields div.field div.checkbox label {
3978 4006 color: #565656;
3979 4007 font-weight: 700;
3980 4008 }
3981 4009
3982 4010 #login div.form div.fields div.buttons input, #register div.form div.fields div.buttons input {
3983 4011 color: #000;
3984 4012 font-size: 1em;
3985 4013 font-weight: 700;
3986 4014 margin: 0;
3987 4015 }
3988 4016
3989 4017 #changeset_content .container .wrapper, #graph_content .container .wrapper {
3990 4018 width: 600px;
3991 4019 }
3992 4020
3993 4021 #changeset_content .container .date, .ac .match {
3994 4022 font-weight: 700;
3995 4023 padding-top: 5px;
3996 4024 padding-bottom: 5px;
3997 4025 }
3998 4026
3999 4027 div#legend_container table td, div#legend_choices table td {
4000 4028 border: none !important;
4001 4029 height: 20px !important;
4002 4030 padding: 0 !important;
4003 4031 }
4004 4032
4005 4033 .q_filter_box {
4006 4034 -webkit-box-shadow: rgba(0,0,0,0.07) 0 1px 2px inset;
4007 4035 -webkit-border-radius: 4px;
4008 4036 border-radius: 4px;
4009 4037 border: 0 none;
4010 4038 color: #AAAAAA;
4011 4039 margin-bottom: -4px;
4012 4040 margin-top: -4px;
4013 4041 padding-left: 3px;
4014 4042 }
4015 4043
4016 4044 #node_filter {
4017 4045 border: 0px solid #545454;
4018 4046 color: #AAAAAA;
4019 4047 padding-left: 3px;
4020 4048 }
4021 4049
4022 4050
4023 4051 .group_members_wrap {
4024 4052 min-height: 85px;
4025 4053 padding-left: 20px;
4026 4054 }
4027 4055
4028 4056 .group_members .group_member {
4029 4057 height: 30px;
4030 4058 padding: 0px 0px 0px 0px;
4031 4059 }
4032 4060
4033 4061 .reviewers_member {
4034 4062 height: 15px;
4035 4063 padding: 0px 0px 0px 10px;
4036 4064 }
4037 4065
4038 4066 .emails_wrap {
4039 4067 padding: 0px 20px;
4040 4068 }
4041 4069
4042 4070 .emails_wrap .email_entry {
4043 4071 height: 30px;
4044 4072 padding: 0px 0px 0px 10px;
4045 4073 }
4046 4074 .emails_wrap .email_entry .email {
4047 4075 float: left
4048 4076 }
4049 4077 .emails_wrap .email_entry .email_action {
4050 4078 float: left
4051 4079 }
4052 4080
4053 4081 .ips_wrap {
4054 4082 padding: 0px 20px;
4055 4083 }
4056 4084
4057 4085 .ips_wrap .ip_entry {
4058 4086 height: 30px;
4059 4087 padding: 0px 0px 0px 10px;
4060 4088 }
4061 4089 .ips_wrap .ip_entry .ip {
4062 4090 float: left
4063 4091 }
4064 4092 .ips_wrap .ip_entry .ip_action {
4065 4093 float: left
4066 4094 }
4067 4095
4068 4096
4069 4097 /*README STYLE*/
4070 4098
4071 4099 div.readme {
4072 4100 padding: 0px;
4073 4101 }
4074 4102
4075 4103 div.readme h2 {
4076 4104 font-weight: normal;
4077 4105 }
4078 4106
4079 4107 div.readme .readme_box {
4080 4108 background-color: #fafafa;
4081 4109 }
4082 4110
4083 4111 div.readme .readme_box {
4084 4112 clear: both;
4085 4113 overflow: hidden;
4086 4114 margin: 0;
4087 4115 padding: 0 20px 10px;
4088 4116 }
4089 4117
4090 4118 div.readme .readme_box h1, div.readme .readme_box h2, div.readme .readme_box h3, div.readme .readme_box h4, div.readme .readme_box h5, div.readme .readme_box h6 {
4091 4119 border-bottom: 0 !important;
4092 4120 margin: 0 !important;
4093 4121 padding: 0 !important;
4094 4122 line-height: 1.5em !important;
4095 4123 }
4096 4124
4097 4125
4098 4126 div.readme .readme_box h1:first-child {
4099 4127 padding-top: .25em !important;
4100 4128 }
4101 4129
4102 4130 div.readme .readme_box h2, div.readme .readme_box h3 {
4103 4131 margin: 1em 0 !important;
4104 4132 }
4105 4133
4106 4134 div.readme .readme_box h2 {
4107 4135 margin-top: 1.5em !important;
4108 4136 border-top: 4px solid #e0e0e0 !important;
4109 4137 padding-top: .5em !important;
4110 4138 }
4111 4139
4112 4140 div.readme .readme_box p {
4113 4141 color: black !important;
4114 4142 margin: 1em 0 !important;
4115 4143 line-height: 1.5em !important;
4116 4144 }
4117 4145
4118 4146 div.readme .readme_box ul {
4119 4147 list-style: disc !important;
4120 4148 margin: 1em 0 1em 2em !important;
4121 4149 }
4122 4150
4123 4151 div.readme .readme_box ol {
4124 4152 list-style: decimal;
4125 4153 margin: 1em 0 1em 2em !important;
4126 4154 }
4127 4155
4128 4156 div.readme .readme_box pre, code {
4129 4157 font: 12px "Bitstream Vera Sans Mono","Courier",monospace;
4130 4158 }
4131 4159
4132 4160 div.readme .readme_box code {
4133 4161 font-size: 12px !important;
4134 4162 background-color: ghostWhite !important;
4135 4163 color: #444 !important;
4136 4164 padding: 0 .2em !important;
4137 4165 border: 1px solid #dedede !important;
4138 4166 }
4139 4167
4140 4168 div.readme .readme_box pre code {
4141 4169 padding: 0 !important;
4142 4170 font-size: 12px !important;
4143 4171 background-color: #eee !important;
4144 4172 border: none !important;
4145 4173 }
4146 4174
4147 4175 div.readme .readme_box pre {
4148 4176 margin: 1em 0;
4149 4177 font-size: 12px;
4150 4178 background-color: #eee;
4151 4179 border: 1px solid #ddd;
4152 4180 padding: 5px;
4153 4181 color: #444;
4154 4182 overflow: auto;
4155 4183 -webkit-box-shadow: rgba(0,0,0,0.07) 0 1px 2px inset;
4156 4184 -webkit-border-radius: 3px;
4157 4185 border-radius: 3px;
4158 4186 }
4159 4187
4160 4188 div.readme .readme_box table {
4161 4189 display: table;
4162 4190 border-collapse: separate;
4163 4191 border-spacing: 2px;
4164 4192 border-color: gray;
4165 4193 width: auto !important;
4166 4194 }
4167 4195
4168 4196
4169 4197 /** RST STYLE **/
4170 4198
4171 4199
4172 4200 div.rst-block {
4173 4201 padding: 0px;
4174 4202 }
4175 4203
4176 4204 div.rst-block h2 {
4177 4205 font-weight: normal;
4178 4206 }
4179 4207
4180 4208 div.rst-block {
4181 4209 background-color: #fafafa;
4182 4210 }
4183 4211
4184 4212 div.rst-block {
4185 4213 clear: both;
4186 4214 overflow: hidden;
4187 4215 margin: 0;
4188 4216 padding: 0 20px 10px;
4189 4217 }
4190 4218
4191 4219 div.rst-block h1, div.rst-block h2, div.rst-block h3, div.rst-block h4, div.rst-block h5, div.rst-block h6 {
4192 4220 border-bottom: 0 !important;
4193 4221 margin: 0 !important;
4194 4222 padding: 0 !important;
4195 4223 line-height: 1.5em !important;
4196 4224 }
4197 4225
4198 4226
4199 4227 div.rst-block h1:first-child {
4200 4228 padding-top: .25em !important;
4201 4229 }
4202 4230
4203 4231 div.rst-block h2, div.rst-block h3 {
4204 4232 margin: 1em 0 !important;
4205 4233 }
4206 4234
4207 4235 div.rst-block h2 {
4208 4236 margin-top: 1.5em !important;
4209 4237 border-top: 4px solid #e0e0e0 !important;
4210 4238 padding-top: .5em !important;
4211 4239 }
4212 4240
4213 4241 div.rst-block p {
4214 4242 color: black !important;
4215 4243 margin: 1em 0 !important;
4216 4244 line-height: 1.5em !important;
4217 4245 }
4218 4246
4219 4247 div.rst-block ul {
4220 4248 list-style: disc !important;
4221 4249 margin: 1em 0 1em 2em !important;
4222 4250 }
4223 4251
4224 4252 div.rst-block ol {
4225 4253 list-style: decimal;
4226 4254 margin: 1em 0 1em 2em !important;
4227 4255 }
4228 4256
4229 4257 div.rst-block pre, code {
4230 4258 font: 12px "Bitstream Vera Sans Mono","Courier",monospace;
4231 4259 }
4232 4260
4233 4261 div.rst-block code {
4234 4262 font-size: 12px !important;
4235 4263 background-color: ghostWhite !important;
4236 4264 color: #444 !important;
4237 4265 padding: 0 .2em !important;
4238 4266 border: 1px solid #dedede !important;
4239 4267 }
4240 4268
4241 4269 div.rst-block pre code {
4242 4270 padding: 0 !important;
4243 4271 font-size: 12px !important;
4244 4272 background-color: #eee !important;
4245 4273 border: none !important;
4246 4274 }
4247 4275
4248 4276 div.rst-block pre {
4249 4277 margin: 1em 0;
4250 4278 font-size: 12px;
4251 4279 background-color: #eee;
4252 4280 border: 1px solid #ddd;
4253 4281 padding: 5px;
4254 4282 color: #444;
4255 4283 overflow: auto;
4256 4284 -webkit-box-shadow: rgba(0,0,0,0.07) 0 1px 2px inset;
4257 4285 -webkit-border-radius: 3px;
4258 4286 border-radius: 3px;
4259 4287 }
4260 4288
4261 4289
4262 4290 /** comment main **/
4263 4291 .comments {
4264 4292 padding: 10px 20px;
4265 4293 }
4266 4294
4267 4295 .comments .comment {
4268 4296 border: 1px solid #ddd;
4269 4297 margin-top: 10px;
4270 4298 -webkit-border-radius: 4px;
4271 4299 border-radius: 4px;
4272 4300 }
4273 4301
4274 4302 .comments .comment .meta {
4275 4303 background: #f8f8f8;
4276 4304 padding: 4px;
4277 4305 border-bottom: 1px solid #ddd;
4278 4306 height: 18px;
4279 4307 }
4280 4308
4281 4309 .comments .comment .meta img {
4282 4310 vertical-align: middle;
4283 4311 }
4284 4312
4285 4313 .comments .comment .meta .user {
4286 4314 font-weight: bold;
4287 4315 float: left;
4288 4316 padding: 4px 2px 2px 2px;
4289 4317 }
4290 4318
4291 4319 .comments .comment .meta .date {
4292 4320 float: left;
4293 4321 padding: 4px 4px 0px 4px;
4294 4322 }
4295 4323
4296 4324 .comments .comment .text {
4297 4325 background-color: #FAFAFA;
4298 4326 }
4299 4327 .comment .text div.rst-block p {
4300 4328 margin: 0.5em 0px !important;
4301 4329 }
4302 4330
4303 4331 .comments .comments-number {
4304 4332 padding: 0px 0px 10px 0px;
4305 4333 font-weight: bold;
4306 4334 color: #666;
4307 4335 font-size: 16px;
4308 4336 }
4309 4337
4310 4338 /** comment form **/
4311 4339
4312 4340 .status-block {
4313 4341 min-height: 80px;
4314 4342 clear: both
4315 4343 }
4316 4344
4317 4345
4318 4346 div.comment-form {
4319 4347 margin-top: 20px;
4320 4348 }
4321 4349
4322 4350 .comment-form strong {
4323 4351 display: block;
4324 4352 margin-bottom: 15px;
4325 4353 }
4326 4354
4327 4355 .comment-form textarea {
4328 4356 width: 100%;
4329 4357 height: 100px;
4330 4358 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
4331 4359 }
4332 4360
4333 4361 form.comment-form {
4334 4362 margin-top: 10px;
4335 4363 margin-left: 10px;
4336 4364 }
4337 4365
4338 4366 .comment-inline-form .comment-block-ta,
4339 4367 .comment-form .comment-block-ta {
4340 4368 border: 1px solid #ccc;
4341 4369 border-radius: 3px;
4342 4370 box-sizing: border-box;
4343 4371 }
4344 4372
4345 4373 .comment-form-submit {
4346 4374 margin-top: 5px;
4347 4375 margin-left: 525px;
4348 4376 }
4349 4377
4350 4378 .file-comments {
4351 4379 display: none;
4352 4380 }
4353 4381
4354 4382 .comment-form .comment {
4355 4383 margin-left: 10px;
4356 4384 }
4357 4385
4358 4386 .comment-form .comment-help {
4359 4387 padding: 5px 5px 5px 5px;
4360 4388 color: #666;
4361 4389 }
4362 4390 .comment-form .comment-help .preview-btn,
4363 4391 .comment-form .comment-help .edit-btn {
4364 4392 float: right;
4365 4393 margin: -6px 0px 0px 0px;
4366 4394 }
4367 4395
4368 4396 .comment-form .preview-box.unloaded,
4369 4397 .comment-inline-form .preview-box.unloaded {
4370 4398 height: 50px;
4371 4399 text-align: center;
4372 4400 padding: 20px;
4373 4401 background-color: #fafafa;
4374 4402 }
4375 4403
4376 4404 .comment-form .comment-button {
4377 4405 padding-top: 5px;
4378 4406 }
4379 4407
4380 4408 .add-another-button {
4381 4409 margin-left: 10px;
4382 4410 margin-top: 10px;
4383 4411 margin-bottom: 10px;
4384 4412 }
4385 4413
4386 4414 .comment .buttons {
4387 4415 float: right;
4388 4416 margin: -1px 0px 0px 0px;
4389 4417 }
4390 4418
4391 4419
4392 4420 .show-inline-comments {
4393 4421 position: relative;
4394 4422 top: 1px
4395 4423 }
4396 4424
4397 4425 /** comment inline form **/
4398 4426 .comment-inline-form {
4399 4427 margin: 4px;
4400 4428 }
4401 4429 .comment-inline-form .overlay {
4402 4430 display: none;
4403 4431 }
4404 4432 .comment-inline-form .overlay.submitting {
4405 4433 display: block;
4406 4434 background: none repeat scroll 0 0 white;
4407 4435 font-size: 16px;
4408 4436 opacity: 0.5;
4409 4437 position: absolute;
4410 4438 text-align: center;
4411 4439 vertical-align: top;
4412 4440
4413 4441 }
4414 4442 .comment-inline-form .overlay.submitting .overlay-text {
4415 4443 width: 100%;
4416 4444 margin-top: 5%;
4417 4445 }
4418 4446
4419 4447 .comment-inline-form .clearfix,
4420 4448 .comment-form .clearfix {
4421 4449 background: #EEE;
4422 4450 -webkit-border-radius: 4px;
4423 4451 border-radius: 4px;
4424 4452 padding: 5px;
4425 4453 margin: 0px;
4426 4454 }
4427 4455
4428 4456 div.comment-inline-form {
4429 4457 padding: 4px 0px 6px 0px;
4430 4458 }
4431 4459
4432 4460 .comment-inline-form strong {
4433 4461 display: block;
4434 4462 margin-bottom: 15px;
4435 4463 }
4436 4464
4437 4465 .comment-inline-form textarea {
4438 4466 width: 100%;
4439 4467 height: 100px;
4440 4468 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
4441 4469 }
4442 4470
4443 4471 form.comment-inline-form {
4444 4472 margin-top: 10px;
4445 4473 margin-left: 10px;
4446 4474 }
4447 4475
4448 4476 .comment-inline-form-submit {
4449 4477 margin-top: 5px;
4450 4478 margin-left: 525px;
4451 4479 }
4452 4480
4453 4481 .file-comments {
4454 4482 display: none;
4455 4483 }
4456 4484
4457 4485 .comment-inline-form .comment {
4458 4486 margin-left: 10px;
4459 4487 }
4460 4488
4461 4489 .comment-inline-form .comment-help {
4462 4490 padding: 5px 5px 5px 5px;
4463 4491 color: #666;
4464 4492 }
4465 4493
4466 4494 .comment-inline-form .comment-help .preview-btn,
4467 4495 .comment-inline-form .comment-help .edit-btn {
4468 4496 float: right;
4469 4497 margin: -6px 0px 0px 0px;
4470 4498 }
4471 4499
4472 4500 .comment-inline-form .comment-button {
4473 4501 padding-top: 5px;
4474 4502 }
4475 4503
4476 4504 /** comment inline **/
4477 4505 .inline-comments {
4478 4506 padding: 10px 20px;
4479 4507 }
4480 4508
4481 4509 .inline-comments div.rst-block {
4482 4510 clear: both;
4483 4511 overflow: hidden;
4484 4512 margin: 0;
4485 4513 padding: 0 20px 0px;
4486 4514 }
4487 4515 .inline-comments .comment {
4488 4516 border: 1px solid #ddd;
4489 4517 -webkit-border-radius: 4px;
4490 4518 border-radius: 4px;
4491 4519 margin: 3px 3px 5px 5px;
4492 4520 background-color: #FAFAFA;
4493 4521 }
4494 4522 .inline-comments .add-comment {
4495 4523 padding: 2px 4px 8px 5px;
4496 4524 }
4497 4525
4498 4526 .inline-comments .comment-wrapp {
4499 4527 padding: 1px;
4500 4528 }
4501 4529 .inline-comments .comment .meta {
4502 4530 background: #f8f8f8;
4503 4531 padding: 4px;
4504 4532 border-bottom: 1px solid #ddd;
4505 4533 height: 20px;
4506 4534 }
4507 4535
4508 4536 .inline-comments .comment .meta img {
4509 4537 vertical-align: middle;
4510 4538 }
4511 4539
4512 4540 .inline-comments .comment .meta .user {
4513 4541 font-weight: bold;
4514 4542 float: left;
4515 4543 padding: 3px;
4516 4544 }
4517 4545
4518 4546 .inline-comments .comment .meta .date {
4519 4547 float: left;
4520 4548 padding: 3px;
4521 4549 }
4522 4550
4523 4551 .inline-comments .comment .text {
4524 4552 background-color: #FAFAFA;
4525 4553 }
4526 4554
4527 4555 .inline-comments .comments-number {
4528 4556 padding: 0px 0px 10px 0px;
4529 4557 font-weight: bold;
4530 4558 color: #666;
4531 4559 font-size: 16px;
4532 4560 }
4533 4561 .inline-comments-button .add-comment {
4534 4562 margin: 2px 0px 8px 5px !important
4535 4563 }
4536 4564
4537 4565 .notification-paginator {
4538 4566 padding: 0px 0px 4px 16px;
4539 4567 }
4540 4568
4541 4569 #context-pages .pull-request span,
4542 4570 .menu_link_notifications {
4543 4571 padding: 4px 4px !important;
4544 4572 text-align: center;
4545 4573 color: #888 !important;
4546 4574 background-color: #DEDEDE !important;
4547 4575 border-radius: 4px !important;
4548 4576 -webkit-border-radius: 4px !important;
4549 4577 }
4550 4578
4551 4579 #context-pages .forks span,
4552 4580 .menu_link_notifications {
4553 4581 padding: 4px 4px !important;
4554 4582 text-align: center;
4555 4583 color: #888 !important;
4556 4584 background-color: #DEDEDE !important;
4557 4585 border-radius: 4px !important;
4558 4586 -webkit-border-radius: 4px !important;
4559 4587 }
4560 4588
4561 4589
4562 4590 .notification-header {
4563 4591 padding-top: 6px;
4564 4592 }
4565 4593 .notification-header .desc {
4566 4594 font-size: 16px;
4567 4595 height: 24px;
4568 4596 float: left
4569 4597 }
4570 4598 .notification-list .container.unread {
4571 4599 background: none repeat scroll 0 0 rgba(255, 255, 180, 0.6);
4572 4600 }
4573 4601 .notification-header .gravatar {
4574 4602 background: none repeat scroll 0 0 transparent;
4575 4603 padding: 0px 0px 0px 8px;
4576 4604 }
4577 4605 .notification-list .container .notification-header .desc {
4578 4606 font-weight: bold;
4579 4607 font-size: 17px;
4580 4608 }
4581 4609 .notification-table {
4582 4610 border: 1px solid #ccc;
4583 4611 -webkit-border-radius: 6px 6px 6px 6px;
4584 4612 border-radius: 6px 6px 6px 6px;
4585 4613 clear: both;
4586 4614 margin: 0px 20px 0px 20px;
4587 4615 }
4588 4616 .notification-header .delete-notifications {
4589 4617 float: right;
4590 4618 padding-top: 8px;
4591 4619 cursor: pointer;
4592 4620 }
4593 4621 .notification-header .read-notifications {
4594 4622 float: right;
4595 4623 padding-top: 8px;
4596 4624 cursor: pointer;
4597 4625 }
4598 4626 .notification-subject {
4599 4627 clear: both;
4600 4628 border-bottom: 1px solid #eee;
4601 4629 padding: 5px 0px 5px 38px;
4602 4630 }
4603 4631
4604 4632 .notification-body {
4605 4633 clear: both;
4606 4634 margin: 34px 2px 2px 8px
4607 4635 }
4608 4636
4609 4637 /****
4610 4638 PULL REQUESTS
4611 4639 *****/
4612 4640 .pullrequests_section_head {
4613 4641 padding: 10px 10px 10px 0px;
4614 4642 font-size: 16px;
4615 4643 font-weight: bold;
4616 4644 }
4617 4645
4618 4646 h3.closed,
4619 4647 #pullrequests_container li.closed a
4620 4648 {
4621 4649 color: #555;
4622 4650 background: #eee;
4623 4651 }
4624 4652
4625 4653 div.pr-title {
4626 4654 font-size: 1.6em;
4627 4655 }
4628 4656
4629 4657 div.pr {
4630 4658 border-bottom: 1px solid #DDD;
4631 4659 margin: 0px 20px;
4632 4660 padding: 10px 4px;
4633 4661 }
4634 4662 div.pr-closed {
4635 4663 background-color: rgba(245,245,245,0.5);
4636 4664 }
4637 4665
4638 4666 span.pr-closed-tag {
4639 4667 margin-bottom: 1px;
4640 4668 margin-right: 1px;
4641 4669 padding: 1px 3px;
4642 4670 font-size: 10px;
4643 4671 padding: 1px 3px 1px 3px;
4644 4672 font-size: 10px;
4645 4673 color: #336699;
4646 4674 white-space: nowrap;
4647 4675 -webkit-border-radius: 4px;
4648 4676 border-radius: 4px;
4649 4677 border: 1px solid #d9e8f8;
4650 4678 line-height: 1.5em;
4651 4679 }
4652 4680
4653 4681 /****
4654 4682 PERMS
4655 4683 *****/
4656 4684 #perms .perms_section_head {
4657 4685 padding: 10px 10px 10px 0px;
4658 4686 font-size: 16px;
4659 4687 font-weight: bold;
4660 4688 }
4661 4689
4662 4690 #perms .perm_tag {
4663 4691 padding: 1px 3px 1px 3px;
4664 4692 font-size: 10px;
4665 4693 font-weight: bold;
4666 4694 text-transform: uppercase;
4667 4695 white-space: nowrap;
4668 4696 -webkit-border-radius: 3px;
4669 4697 border-radius: 3px;
4670 4698 }
4671 4699
4672 4700 #perms .perm_tag.admin {
4673 4701 background-color: #B94A48;
4674 4702 color: #ffffff;
4675 4703 }
4676 4704
4677 4705 #perms .perm_tag.write {
4678 4706 background-color: #DB7525;
4679 4707 color: #ffffff;
4680 4708 }
4681 4709
4682 4710 #perms .perm_tag.read {
4683 4711 background-color: #468847;
4684 4712 color: #ffffff;
4685 4713 }
4686 4714
4687 4715 #perms .perm_tag.none {
4688 4716 background-color: #bfbfbf;
4689 4717 color: #ffffff;
4690 4718 }
4691 4719
4692 4720 .perm-gravatar {
4693 4721 vertical-align: middle;
4694 4722 padding: 2px;
4695 4723 }
4696 4724 .perm-gravatar-ac {
4697 4725 vertical-align: middle;
4698 4726 padding: 2px;
4699 4727 width: 14px;
4700 4728 height: 14px;
4701 4729 }
4702 4730
4703 4731 /*****************************************************************************
4704 4732 DIFFS CSS
4705 4733 ******************************************************************************/
4706 4734 .diff-collapse {
4707 4735 text-align: center;
4708 4736 margin-bottom: -15px;
4709 4737 }
4710 4738 .diff-collapse-button {
4711 4739 cursor: pointer;
4712 4740 color: #666;
4713 4741 font-size: 16px;
4714 4742 }
4715 4743 .diff-container {
4716 4744
4717 4745 }
4718 4746
4719 4747 .diff-container.hidden {
4720 4748 display: none;
4721 4749 overflow: hidden;
4722 4750 }
4723 4751
4724 4752
4725 4753 div.diffblock {
4726 4754 overflow: auto;
4727 4755 padding: 0px;
4728 4756 border: 1px solid #ccc;
4729 4757 background: #f8f8f8;
4730 4758 font-size: 100%;
4731 4759 line-height: 100%;
4732 4760 /* new */
4733 4761 line-height: 125%;
4734 4762 -webkit-border-radius: 6px 6px 0px 0px;
4735 4763 border-radius: 6px 6px 0px 0px;
4736 4764 }
4737 4765 div.diffblock.margined {
4738 4766 margin: 0px 20px 0px 20px;
4739 4767 }
4740 4768 div.diffblock .code-header {
4741 4769 border-bottom: 1px solid #CCCCCC;
4742 4770 background: #EEEEEE;
4743 4771 padding: 10px 0 10px 0;
4744 4772 height: 14px;
4745 4773 }
4746 4774
4747 4775 div.diffblock .code-header.banner {
4748 4776 border-bottom: 1px solid #CCCCCC;
4749 4777 background: #EEEEEE;
4750 4778 height: 14px;
4751 4779 margin: 0px 95px 0px 95px;
4752 4780 padding: 3px 3px 11px 3px;
4753 4781 }
4754 4782
4755 4783 div.diffblock .code-header-title {
4756 4784 padding: 0px 0px 10px 5px !important;
4757 4785 margin: 0 !important;
4758 4786 }
4759 4787 div.diffblock .code-header .hash {
4760 4788 float: left;
4761 4789 padding: 2px 0 0 2px;
4762 4790 }
4763 4791 div.diffblock .code-header .date {
4764 4792 float: left;
4765 4793 text-transform: uppercase;
4766 4794 padding: 2px 0px 0px 2px;
4767 4795 }
4768 4796 div.diffblock .code-header div {
4769 4797 margin-left: 4px;
4770 4798 font-weight: bold;
4771 4799 font-size: 14px;
4772 4800 }
4773 4801
4774 4802 div.diffblock .parents {
4775 4803 float: left;
4776 4804 height: 26px;
4777 4805 width: 100px;
4778 4806 font-size: 10px;
4779 4807 font-weight: 400;
4780 4808 vertical-align: middle;
4781 4809 padding: 0px 2px 2px 2px;
4782 4810 background-color: #eeeeee;
4783 4811 border-bottom: 1px solid #CCCCCC;
4784 4812 }
4785 4813
4786 4814 div.diffblock .children {
4787 4815 float: right;
4788 4816 height: 26px;
4789 4817 width: 100px;
4790 4818 font-size: 10px;
4791 4819 font-weight: 400;
4792 4820 vertical-align: middle;
4793 4821 text-align: right;
4794 4822 padding: 0px 2px 2px 2px;
4795 4823 background-color: #eeeeee;
4796 4824 border-bottom: 1px solid #CCCCCC;
4797 4825 }
4798 4826
4799 4827 div.diffblock .code-body {
4800 4828 background: #FFFFFF;
4801 4829 }
4802 4830 div.diffblock pre.raw {
4803 4831 background: #FFFFFF;
4804 4832 color: #000000;
4805 4833 }
4806 4834 table.code-difftable {
4807 4835 border-collapse: collapse;
4808 4836 width: 99%;
4809 4837 border-radius: 0px !important;
4810 4838 }
4811 4839 table.code-difftable td {
4812 4840 padding: 0 !important;
4813 4841 background: none !important;
4814 4842 border: 0 !important;
4815 4843 vertical-align: baseline !important
4816 4844 }
4817 4845 table.code-difftable .context {
4818 4846 background: none repeat scroll 0 0 #DDE7EF;
4819 4847 }
4820 4848 table.code-difftable .add {
4821 4849 background: none repeat scroll 0 0 #DDFFDD;
4822 4850 }
4823 4851 table.code-difftable .add ins {
4824 4852 background: none repeat scroll 0 0 #AAFFAA;
4825 4853 text-decoration: none;
4826 4854 }
4827 4855 table.code-difftable .del {
4828 4856 background: none repeat scroll 0 0 #FFDDDD;
4829 4857 }
4830 4858 table.code-difftable .del del {
4831 4859 background: none repeat scroll 0 0 #FFAAAA;
4832 4860 text-decoration: none;
4833 4861 }
4834 4862
4835 4863 /** LINE NUMBERS **/
4836 4864 table.code-difftable .lineno {
4837 4865
4838 4866 padding-left: 2px;
4839 4867 padding-right: 2px;
4840 4868 text-align: right;
4841 4869 width: 32px;
4842 4870 -moz-user-select: none;
4843 4871 -webkit-user-select: none;
4844 4872 border-right: 1px solid #CCC !important;
4845 4873 border-left: 0px solid #CCC !important;
4846 4874 border-top: 0px solid #CCC !important;
4847 4875 border-bottom: none !important;
4848 4876 vertical-align: middle !important;
4849 4877
4850 4878 }
4851 4879 table.code-difftable .lineno.new {
4852 4880 }
4853 4881 table.code-difftable .lineno.old {
4854 4882 }
4855 4883 table.code-difftable .lineno a {
4856 4884 color: #747474 !important;
4857 4885 font: 11px "Bitstream Vera Sans Mono",Monaco,"Courier New",Courier,monospace !important;
4858 4886 letter-spacing: -1px;
4859 4887 text-align: right;
4860 4888 padding-right: 2px;
4861 4889 cursor: pointer;
4862 4890 display: block;
4863 4891 width: 32px;
4864 4892 }
4865 4893
4866 4894 table.code-difftable .lineno-inline {
4867 4895 background: none repeat scroll 0 0 #FFF !important;
4868 4896 padding-left: 2px;
4869 4897 padding-right: 2px;
4870 4898 text-align: right;
4871 4899 width: 30px;
4872 4900 -moz-user-select: none;
4873 4901 -webkit-user-select: none;
4874 4902 }
4875 4903
4876 4904 /** CODE **/
4877 4905 table.code-difftable .code {
4878 4906 display: block;
4879 4907 width: 100%;
4880 4908 }
4881 4909 table.code-difftable .code td {
4882 4910 margin: 0;
4883 4911 padding: 0;
4884 4912 }
4885 4913 table.code-difftable .code pre {
4886 4914 margin: 0;
4887 4915 padding: 0;
4888 4916 height: 17px;
4889 4917 line-height: 17px;
4890 4918 }
4891 4919
4892 4920
4893 4921 .diffblock.margined.comm .line .code:hover {
4894 4922 background-color: #FFFFCC !important;
4895 4923 cursor: pointer !important;
4896 4924 background-image: url("../images/icons/comment_add.png") !important;
4897 4925 background-repeat: no-repeat !important;
4898 4926 background-position: right !important;
4899 4927 background-position: 0% 50% !important;
4900 4928 }
4901 4929 .diffblock.margined.comm .line .code.no-comment:hover {
4902 4930 background-image: none !important;
4903 4931 cursor: auto !important;
4904 4932 background-color: inherit !important;
4905 4933 }
4906 4934
4907 4935 div.comment:target>.comment-wrapp {
4908 4936 border: solid 2px #ee0 !important;
4909 4937 }
4910 4938
4911 4939 .lineno:target a {
4912 4940 border: solid 2px #ee0 !important;
4913 4941 margin: -2px;
4914 4942 }
@@ -1,341 +1,353 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="root.html"/>
3 3
4 4 <!-- HEADER -->
5 5 <div id="header-dd"></div>
6 6 <div id="header">
7 7 <div id="header-inner" class="title">
8 8 <div id="logo">
9 9 <h1><a href="${h.url('home')}">${c.rhodecode_name}</a></h1>
10 10 </div>
11 11 <!-- MENU -->
12 12 ${self.page_nav()}
13 13 <!-- END MENU -->
14 14 ${self.body()}
15 15 </div>
16 16 </div>
17 17 <!-- END HEADER -->
18 18
19 19 <!-- CONTENT -->
20 20 <div id="content">
21 21 <div class="flash_msg">
22 22 <% messages = h.flash.pop_messages() %>
23 23 % if messages:
24 24 <ul id="flash-messages">
25 25 % for message in messages:
26 26 <li class="${message.category}_msg">${message}</li>
27 27 % endfor
28 28 </ul>
29 29 % endif
30 30 </div>
31 31 <div id="main">
32 32 ${next.main()}
33 33 </div>
34 34 </div>
35 35 <!-- END CONTENT -->
36 36
37 37 <!-- FOOTER -->
38 38 <div id="footer">
39 39 <div id="footer-inner" class="title">
40 40 <div>
41 41 <p class="footer-link">
42 42 ${_('Server instance: %s') % c.rhodecode_instanceid if c.rhodecode_instanceid else ''}
43 43 </p>
44 44 <p class="footer-link-right">
45 45 <a href="${h.url('rhodecode_official')}">RhodeCode ${c.rhodecode_version}</a>
46 46 &copy; 2010-${h.datetime.today().year} by Marcin Kuzminski and others
47 47 &ndash; <a href="${h.url('bugtracker')}">${_('Report a bug')}</a>
48 48 </p>
49 49 </div>
50 50 </div>
51 51 </div>
52 52
53 53 <!-- END FOOTER -->
54 54
55 55 ### MAKO DEFS ###
56 56 <%def name="breadcrumbs()">
57 57 <div class="breadcrumbs">
58 58 ${self.breadcrumbs_links()}
59 59 </div>
60 60 </%def>
61 61
62 62 <%def name="context_bar(current)">
63 63 ${repo_context_bar(current)}
64 64 </%def>
65 65
66 66 <%def name="admin_menu()">
67 67 <ul class="admin_menu">
68 68 <li>${h.link_to(_('Admin journal'),h.url('admin_home'),class_='journal ')}</li>
69 69 <li>${h.link_to(_('Repositories'),h.url('repos'),class_='repos')}</li>
70 70 <li>${h.link_to(_('Repository groups'),h.url('repos_groups'),class_='repos_groups')}</li>
71 71 <li>${h.link_to(_('Users'),h.url('users'),class_='users')}</li>
72 72 <li>${h.link_to(_('User groups'),h.url('users_groups'),class_='groups')}</li>
73 73 <li>${h.link_to(_('Permissions'),h.url('edit_permission',id='default'),class_='permissions')}</li>
74 74 <li>${h.link_to(_('LDAP'),h.url('ldap_home'),class_='ldap')}</li>
75 75 <li>${h.link_to(_('Defaults'),h.url('defaults'),class_='defaults')}</li>
76 76 <li class="last">${h.link_to(_('Settings'),h.url('admin_settings'),class_='settings')}</li>
77 77 </ul>
78 78 </%def>
79 79
80 80 <%def name="admin_menu_simple(repository_groups=None, user_groups=None)">
81 81 <ul>
82 82 %if repository_groups:
83 83 <li>${h.link_to(_('Repository groups'),h.url('repos_groups'),class_='repos_groups')}</li>
84 84 %endif:
85 85 %if user_groups:
86 86 <li>${h.link_to(_('User groups'),h.url('users_groups'),class_='groups')}</li>
87 87 %endif
88 88 </ul>
89 89 </%def>
90 90
91 91 <%def name="repo_context_bar(current=None)">
92 92 <%
93 93 def follow_class():
94 94 if c.repository_following:
95 95 return h.literal('following')
96 96 else:
97 97 return h.literal('follow')
98 98 %>
99 99 <%
100 100 def is_current(selected):
101 101 if selected == current:
102 102 return h.literal('class="current"')
103 103 %>
104 104
105 105 <!--- CONTEXT BAR -->
106 106 <div id="context-bar" class="box">
107 107 <div id="breadcrumbs">
108 108 ${h.link_to(_(u'Repositories'),h.url('home'))}
109 109 &raquo;
110 110 ${h.repo_link(c.rhodecode_db_repo.groups_and_repo)}
111 111 </div>
112 112 <ul id="context-pages" class="horizontal-list">
113 113 <li ${is_current('summary')}><a href="${h.url('summary_home', repo_name=c.repo_name)}" class="summary">${_('Summary')}</a></li>
114 114 <li ${is_current('changelog')}><a href="${h.url('changelog_home', repo_name=c.repo_name)}" class="changelogs">${_('Changelog')}</a></li>
115 115 <li ${is_current('files')}><a href="${h.url('files_home', repo_name=c.repo_name)}" class="files"></span>${_('Files')}</a></li>
116 116 <li ${is_current('switch-to')}>
117 117 <a href="#" id="branch_tag_switcher_2" class="dropdown switch-to"></span>${_('Switch To')}</a>
118 118 <ul id="switch_to_list_2" class="switch_to submenu">
119 119 <li><a href="#">${_('loading...')}</a></li>
120 120 </ul>
121 121 </li>
122 122 <li ${is_current('options')}>
123 123 <a href="#" class="dropdown options"></span>${_('Options')}</a>
124 124 <ul>
125 125 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
126 126 <li>${h.link_to(_('Settings'),h.url('edit_repo',repo_name=c.repo_name),class_='settings')}</li>
127 127 %endif
128 128 %if c.rhodecode_db_repo.fork:
129 129 <li>${h.link_to(_('Compare fork'),h.url('compare_url',repo_name=c.rhodecode_db_repo.fork.repo_name,org_ref_type='branch',org_ref='default',other_repo=c.repo_name,other_ref_type='branch',other_ref=request.GET.get('branch') or 'default', merge=1),class_='compare_request')}</li>
130 130 %endif
131 131 <li>${h.link_to(_('Search'),h.url('search_repo',repo_name=c.repo_name),class_='search')}</li>
132 132
133 133 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking:
134 134 %if c.rhodecode_db_repo.locked[0]:
135 135 <li>${h.link_to(_('Unlock'), h.url('toggle_locking',repo_name=c.repo_name),class_='locking_del')}</li>
136 136 %else:
137 137 <li>${h.link_to(_('Lock'), h.url('toggle_locking',repo_name=c.repo_name),class_='locking_add')}</li>
138 138 %endif
139 139 %endif
140 140 ## TODO: this check feels wrong, it would be better to have a check for permissions
141 141 ## also it feels like a job for the controller
142 142 %if c.rhodecode_user.username != 'default':
143 143 <li>
144 144 <a class="${follow_class()}" onclick="javascript:toggleFollowingRepo(this,${c.rhodecode_db_repo.repo_id},'${str(h.get_token())}');">
145 145 <span class="show-follow">${_('Follow')}</span>
146 146 <span class="show-following">${_('Unfollow')}</span>
147 147 </a>
148 148 </li>
149 149 <li><a href="${h.url('repo_fork_home',repo_name=c.repo_name)}" class="fork">${_('Fork')}</a></li>
150 150 %if h.is_hg(c.rhodecode_repo):
151 151 <li><a href="${h.url('pullrequest_home',repo_name=c.repo_name)}" class="pull-request">${_('Create Pull Request')}</a></li>
152 152 %endif
153 153 %endif
154 154 </ul>
155 155 </li>
156 156 <li ${is_current('showpullrequest')}>
157 157 <a href="${h.url('pullrequest_show_all',repo_name=c.repo_name)}" title="${_('Show Pull Requests')}" class="pull-request">${_('Pull Requests')}
158 158 %if c.repository_pull_requests:
159 159 <span>${c.repository_pull_requests}</span>
160 160 %endif
161 161 </a>
162 162 </li>
163 163 </ul>
164 164 </div>
165 165 <script type="text/javascript">
166 166 YUE.on('branch_tag_switcher_2','mouseover',function(){
167 167 var loaded = YUD.hasClass('branch_tag_switcher_2','loaded');
168 168 if(!loaded){
169 169 YUD.addClass('branch_tag_switcher_2','loaded');
170 170 ypjax("${h.url('branch_tag_switcher',repo_name=c.repo_name)}",'switch_to_list_2',
171 171 function(o){},
172 172 function(o){YUD.removeClass('branch_tag_switcher_2','loaded');}
173 173 ,null);
174 174 }
175 175 return false;
176 176 });
177 177 </script>
178 178 <!--- END CONTEXT BAR -->
179 179 </%def>
180 180
181 181 <%def name="usermenu()">
182 182 ## USER MENU
183 183 <li>
184 184 <a class="menu_link childs" id="quick_login_link">
185 185 <span class="icon">
186 186 <img src="${h.gravatar_url(c.rhodecode_user.email,20)}" alt="avatar">
187 187 </span>
188 188 %if c.rhodecode_user.username != 'default':
189 189 <span class="menu_link_user">${c.rhodecode_user.username}</span>
190 190 %if c.unread_notifications != 0:
191 191 <span class="menu_link_notifications">${c.unread_notifications}</span>
192 192 %endif
193 193 %else:
194 194 <span>${_('Not logged in')}</span>
195 195 %endif
196 196 </a>
197 197
198 198 <div class="user-menu">
199 199 <div id="quick_login">
200 200 %if c.rhodecode_user.username == 'default':
201 201 <h4>${_('Login to your account')}</h4>
202 202 ${h.form(h.url('login_home',came_from=h.url.current()))}
203 203 <div class="form">
204 204 <div class="fields">
205 205 <div class="field">
206 206 <div class="label">
207 207 <label for="username">${_('Username')}:</label>
208 208 </div>
209 209 <div class="input">
210 210 ${h.text('username',class_='focus')}
211 211 </div>
212 212
213 213 </div>
214 214 <div class="field">
215 215 <div class="label">
216 216 <label for="password">${_('Password')}:</label>
217 217 </div>
218 218 <div class="input">
219 219 ${h.password('password',class_='focus')}
220 220 </div>
221 221
222 222 </div>
223 223 <div class="buttons">
224 224 <div class="password_forgoten">${h.link_to(_('Forgot password ?'),h.url('reset_password'))}</div>
225 225 <div class="register">
226 226 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
227 227 ${h.link_to(_("Don't have an account ?"),h.url('register'))}
228 228 %endif
229 229 </div>
230 230 <div class="submit">
231 231 ${h.submit('sign_in',_('Log In'),class_="ui-btn xsmall")}
232 232 </div>
233 233 </div>
234 234 </div>
235 235 </div>
236 236 ${h.end_form()}
237 237 %else:
238 238 <div class="links_left">
239 239 <div class="big_gravatar"><img alt="gravatar" src="${h.gravatar_url(c.rhodecode_user.email,48)}" /></div>
240 240 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
241 241 <div class="email">${c.rhodecode_user.email}</div>
242 242 </div>
243 243 <div class="links_right">
244 244 <ol class="links">
245 245 <li><a href="${h.url('notifications')}">${_('Notifications')}: ${c.unread_notifications}</a></li>
246 246 <li>${h.link_to(_(u'My account'),h.url('admin_settings_my_account'))}</li>
247 247 <li class="logout">${h.link_to(_(u'Log Out'),h.url('logout_home'))}</li>
248 248 </ol>
249 249 </div>
250 250 %endif
251 251 </div>
252 252 </div>
253 253
254 254 </li>
255 255 </%def>
256 256
257 257 <%def name="menu(current=None)">
258 258 <%
259 259 def is_current(selected):
260 260 if selected == current:
261 261 return h.literal('class="current"')
262 262 %>
263 263 <ul id="quick" class="horizontal-list">
264 264 <!-- repo switcher -->
265 265 <li ${is_current('repositories')}>
266 266 <a class="menu_link repo_switcher childs" id="repo_switcher" title="${_('Switch repository')}" href="${h.url('home')}">
267 267 ${_('Repositories')}
268 268 </a>
269 269 <ul id="repo_switcher_list" class="repo_switcher">
270 270 <li>
271 271 <a href="#">${_('loading...')}</a>
272 272 </li>
273 273 </ul>
274 274 </li>
275 275 ##ROOT MENU
276 276 %if c.rhodecode_user.username != 'default':
277 277 <li ${is_current('journal')}>
278 278 <a class="menu_link journal" title="${_('Show recent activity')}" href="${h.url('journal')}">
279 279 ${_('Journal')}
280 280 </a>
281 281 </li>
282 282 %else:
283 283 <li ${is_current('journal')}>
284 284 <a class="menu_link journal" title="${_('Public journal')}" href="${h.url('public_journal')}">
285 285 ${_('Public journal')}
286 286 </a>
287 287 </li>
288 288 %endif
289 <li ${is_current('gists')}>
290 <a class="menu_link gists childs" title="${_('Show public gists')}" href="${h.url('gists')}">
291 ${_('Gists')}
292 </a>
293 <ul class="admin_menu">
294 <li>${h.link_to(_('Create new gist'),h.url('new_gist'),class_='gists-new ')}</li>
295 <li>${h.link_to(_('Public gists'),h.url('gists'),class_='gists ')}</li>
296 %if c.rhodecode_user.username != 'default':
297 <li>${h.link_to(_('My private gists'),h.url('gists', private=1),class_='gists-private ')}</li>
298 %endif
299 </ul>
300 </li>
289 301 <li ${is_current('search')}>
290 302 <a class="menu_link search" title="${_('Search in repositories')}" href="${h.url('search')}">
291 303 ${_('Search')}
292 304 </a>
293 305 </li>
294 306 % if h.HasPermissionAll('hg.admin')('access admin main page'):
295 307 <li ${is_current('admin')}>
296 308 <a class="menu_link admin childs" title="${_('Admin')}" href="${h.url('admin_home')}">
297 309 ${_('Admin')}
298 310 </a>
299 311 ${admin_menu()}
300 312 </li>
301 313 % elif c.rhodecode_user.repository_groups_admin or c.rhodecode_user.user_groups_admin:
302 314 <li ${is_current('admin')}>
303 315 <a class="menu_link admin childs" title="${_('Admin')}">
304 316 ${_('Admin')}
305 317 </a>
306 318 ${admin_menu_simple(c.rhodecode_user.repository_groups_admin,
307 319 c.rhodecode_user.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
308 320 </li>
309 321 % endif
310 322 ${usermenu()}
311 323 <script type="text/javascript">
312 324 YUE.on('repo_switcher','mouseover',function(){
313 325 var target = 'q_filter_rs';
314 326 var qfilter_activate = function(){
315 327 var nodes = YUQ('ul#repo_switcher_list li a.repo_name');
316 328 var func = function(node){
317 329 return node.parentNode;
318 330 }
319 331 q_filter(target,nodes,func);
320 332 }
321 333
322 334 var loaded = YUD.hasClass('repo_switcher','loaded');
323 335 if(!loaded){
324 336 YUD.addClass('repo_switcher','loaded');
325 337 ypjax("${h.url('repo_switcher')}",'repo_switcher_list',
326 338 function(o){qfilter_activate();YUD.get(target).focus()},
327 339 function(o){YUD.removeClass('repo_switcher','loaded');}
328 340 ,null);
329 341 }else{
330 342 YUD.get(target).focus();
331 343 }
332 344 return false;
333 345 });
334 346
335 347 YUE.on('header-dd', 'click',function(e){
336 348 YUD.addClass('header-inner', 'hover');
337 349 YUD.addClass('content', 'hover');
338 350 });
339 351
340 352 </script>
341 353 </%def>
@@ -1,1368 +1,1367 b''
1 1 from __future__ import with_statement
2 2 import random
3 3 import mock
4 4
5 5 from rhodecode.tests import *
6 6 from rhodecode.tests.fixture import Fixture
7 7 from rhodecode.lib.compat import json
8 8 from rhodecode.lib.auth import AuthUser
9 9 from rhodecode.model.user import UserModel
10 10 from rhodecode.model.users_group import UserGroupModel
11 11 from rhodecode.model.repo import RepoModel
12 12 from rhodecode.model.meta import Session
13 13 from rhodecode.model.scm import ScmModel
14 14 from rhodecode.model.db import Repository, User
15 15 from rhodecode.lib.utils2 import time_to_datetime
16 16
17 17
18 18 API_URL = '/_admin/api'
19 19 TEST_USER_GROUP = 'test_users_group'
20 20
21 21 fixture = Fixture()
22 22
23 23
24 24 def _build_data(apikey, method, **kw):
25 25 """
26 26 Builds API data with given random ID
27 27
28 28 :param random_id:
29 :type random_id:
30 29 """
31 30 random_id = random.randrange(1, 9999)
32 31 return random_id, json.dumps({
33 32 "id": random_id,
34 33 "api_key": apikey,
35 34 "method": method,
36 35 "args": kw
37 36 })
38 37
39 38 jsonify = lambda obj: json.loads(json.dumps(obj))
40 39
41 40
42 41 def crash(*args, **kwargs):
43 42 raise Exception('Total Crash !')
44 43
45 44
46 45 def api_call(test_obj, params):
47 46 response = test_obj.app.post(API_URL, content_type='application/json',
48 47 params=params)
49 48 return response
50 49
51 50
52 51 ## helpers
53 52 def make_users_group(name=TEST_USER_GROUP):
54 53 gr = fixture.create_user_group(name, cur_user=TEST_USER_ADMIN_LOGIN)
55 54 UserGroupModel().add_user_to_group(users_group=gr,
56 55 user=TEST_USER_ADMIN_LOGIN)
57 56 Session().commit()
58 57 return gr
59 58
60 59
61 60 def destroy_users_group(name=TEST_USER_GROUP):
62 61 UserGroupModel().delete(users_group=name, force=True)
63 62 Session().commit()
64 63
65 64
66 65 class BaseTestApi(object):
67 66 REPO = None
68 67 REPO_TYPE = None
69 68
70 69 @classmethod
71 70 def setUpClass(self):
72 71 self.usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
73 72 self.apikey = self.usr.api_key
74 73 self.test_user = UserModel().create_or_update(
75 74 username='test-api',
76 75 password='test',
77 76 email='test@api.rhodecode.org',
78 77 firstname='first',
79 78 lastname='last'
80 79 )
81 80 Session().commit()
82 81 self.TEST_USER_LOGIN = self.test_user.username
83 82 self.apikey_regular = self.test_user.api_key
84 83
85 84 @classmethod
86 85 def teardownClass(self):
87 86 pass
88 87
89 88 def setUp(self):
90 89 self.maxDiff = None
91 90 make_users_group()
92 91
93 92 def tearDown(self):
94 93 destroy_users_group()
95 94
96 95 def _compare_ok(self, id_, expected, given):
97 96 expected = jsonify({
98 97 'id': id_,
99 98 'error': None,
100 99 'result': expected
101 100 })
102 101 given = json.loads(given)
103 102 self.assertEqual(expected, given)
104 103
105 104 def _compare_error(self, id_, expected, given):
106 105 expected = jsonify({
107 106 'id': id_,
108 107 'error': expected,
109 108 'result': None
110 109 })
111 110 given = json.loads(given)
112 111 self.assertEqual(expected, given)
113 112
114 113 # def test_Optional(self):
115 114 # from rhodecode.controllers.api.api import Optional
116 115 # option1 = Optional(None)
117 116 # self.assertEqual('<Optional:%s>' % None, repr(option1))
118 117 #
119 118 # self.assertEqual(1, Optional.extract(Optional(1)))
120 119 # self.assertEqual('trololo', Optional.extract('trololo'))
121 120
122 121 def test_api_wrong_key(self):
123 122 id_, params = _build_data('trololo', 'get_user')
124 123 response = api_call(self, params)
125 124
126 125 expected = 'Invalid API KEY'
127 126 self._compare_error(id_, expected, given=response.body)
128 127
129 128 def test_api_missing_non_optional_param(self):
130 129 id_, params = _build_data(self.apikey, 'get_repo')
131 130 response = api_call(self, params)
132 131
133 132 expected = 'Missing non optional `repoid` arg in JSON DATA'
134 133 self._compare_error(id_, expected, given=response.body)
135 134
136 135 def test_api_missing_non_optional_param_args_null(self):
137 136 id_, params = _build_data(self.apikey, 'get_repo')
138 137 params = params.replace('"args": {}', '"args": null')
139 138 response = api_call(self, params)
140 139
141 140 expected = 'Missing non optional `repoid` arg in JSON DATA'
142 141 self._compare_error(id_, expected, given=response.body)
143 142
144 143 def test_api_missing_non_optional_param_args_bad(self):
145 144 id_, params = _build_data(self.apikey, 'get_repo')
146 145 params = params.replace('"args": {}', '"args": 1')
147 146 response = api_call(self, params)
148 147
149 148 expected = 'Missing non optional `repoid` arg in JSON DATA'
150 149 self._compare_error(id_, expected, given=response.body)
151 150
152 151 def test_api_args_is_null(self):
153 152 id_, params = _build_data(self.apikey, 'get_users',)
154 153 params = params.replace('"args": {}', '"args": null')
155 154 response = api_call(self, params)
156 155 self.assertEqual(response.status, '200 OK')
157 156
158 157 def test_api_args_is_bad(self):
159 158 id_, params = _build_data(self.apikey, 'get_users',)
160 159 params = params.replace('"args": {}', '"args": 1')
161 160 response = api_call(self, params)
162 161 self.assertEqual(response.status, '200 OK')
163 162
164 163 def test_api_get_users(self):
165 164 id_, params = _build_data(self.apikey, 'get_users',)
166 165 response = api_call(self, params)
167 166 ret_all = []
168 167 _users = User.query().filter(User.username != User.DEFAULT_USER)\
169 168 .order_by(User.username).all()
170 169 for usr in _users:
171 170 ret = usr.get_api_data()
172 171 ret_all.append(jsonify(ret))
173 172 expected = ret_all
174 173 self._compare_ok(id_, expected, given=response.body)
175 174
176 175 def test_api_get_user(self):
177 176 id_, params = _build_data(self.apikey, 'get_user',
178 177 userid=TEST_USER_ADMIN_LOGIN)
179 178 response = api_call(self, params)
180 179
181 180 usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
182 181 ret = usr.get_api_data()
183 182 ret['permissions'] = AuthUser(usr.user_id).permissions
184 183
185 184 expected = ret
186 185 self._compare_ok(id_, expected, given=response.body)
187 186
188 187 def test_api_get_user_that_does_not_exist(self):
189 188 id_, params = _build_data(self.apikey, 'get_user',
190 189 userid='trololo')
191 190 response = api_call(self, params)
192 191
193 192 expected = "user `%s` does not exist" % 'trololo'
194 193 self._compare_error(id_, expected, given=response.body)
195 194
196 195 def test_api_get_user_without_giving_userid(self):
197 196 id_, params = _build_data(self.apikey, 'get_user')
198 197 response = api_call(self, params)
199 198
200 199 usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
201 200 ret = usr.get_api_data()
202 201 ret['permissions'] = AuthUser(usr.user_id).permissions
203 202
204 203 expected = ret
205 204 self._compare_ok(id_, expected, given=response.body)
206 205
207 206 def test_api_get_user_without_giving_userid_non_admin(self):
208 207 id_, params = _build_data(self.apikey_regular, 'get_user')
209 208 response = api_call(self, params)
210 209
211 210 usr = UserModel().get_by_username(self.TEST_USER_LOGIN)
212 211 ret = usr.get_api_data()
213 212 ret['permissions'] = AuthUser(usr.user_id).permissions
214 213
215 214 expected = ret
216 215 self._compare_ok(id_, expected, given=response.body)
217 216
218 217 def test_api_get_user_with_giving_userid_non_admin(self):
219 218 id_, params = _build_data(self.apikey_regular, 'get_user',
220 219 userid=self.TEST_USER_LOGIN)
221 220 response = api_call(self, params)
222 221
223 222 expected = 'userid is not the same as your user'
224 223 self._compare_error(id_, expected, given=response.body)
225 224
226 225 def test_api_pull(self):
227 226 #TODO: issues with rhodecode_extras here.. not sure why !
228 227 pass
229 228
230 229 # repo_name = 'test_pull'
231 230 # r = fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
232 231 # r.clone_uri = TEST_self.REPO
233 232 # Session.add(r)
234 233 # Session.commit()
235 234 #
236 235 # id_, params = _build_data(self.apikey, 'pull',
237 236 # repoid=repo_name,)
238 237 # response = self.app.post(API_URL, content_type='application/json',
239 238 # params=params)
240 239 #
241 240 # expected = 'Pulled from `%s`' % repo_name
242 241 # self._compare_ok(id_, expected, given=response.body)
243 242 #
244 243 # fixture.destroy_repo(repo_name)
245 244
246 245 def test_api_pull_error(self):
247 246 id_, params = _build_data(self.apikey, 'pull',
248 247 repoid=self.REPO,)
249 248 response = api_call(self, params)
250 249
251 250 expected = 'Unable to pull changes from `%s`' % self.REPO
252 251 self._compare_error(id_, expected, given=response.body)
253 252
254 253 def test_api_rescan_repos(self):
255 254 id_, params = _build_data(self.apikey, 'rescan_repos')
256 255 response = api_call(self, params)
257 256
258 257 expected = {'added': [], 'removed': []}
259 258 self._compare_ok(id_, expected, given=response.body)
260 259
261 260 @mock.patch.object(ScmModel, 'repo_scan', crash)
262 261 def test_api_rescann_error(self):
263 262 id_, params = _build_data(self.apikey, 'rescan_repos',)
264 263 response = api_call(self, params)
265 264
266 265 expected = 'Error occurred during rescan repositories action'
267 266 self._compare_error(id_, expected, given=response.body)
268 267
269 268 def test_api_invalidate_cache(self):
270 269 repo = RepoModel().get_by_repo_name(self.REPO)
271 270 repo.scm_instance_cached() # seed cache
272 271
273 272 id_, params = _build_data(self.apikey, 'invalidate_cache',
274 273 repoid=self.REPO)
275 274 response = api_call(self, params)
276 275
277 276 expected = ("Caches of repository `%s` was invalidated" % (self.REPO))
278 277 self._compare_ok(id_, expected, given=response.body)
279 278
280 279 @mock.patch.object(ScmModel, 'mark_for_invalidation', crash)
281 280 def test_api_invalidate_cache_error(self):
282 281 id_, params = _build_data(self.apikey, 'invalidate_cache',
283 282 repoid=self.REPO)
284 283 response = api_call(self, params)
285 284
286 285 expected = 'Error occurred during cache invalidation action'
287 286 self._compare_error(id_, expected, given=response.body)
288 287
289 288 def test_api_lock_repo_lock_aquire(self):
290 289 id_, params = _build_data(self.apikey, 'lock',
291 290 userid=TEST_USER_ADMIN_LOGIN,
292 291 repoid=self.REPO,
293 292 locked=True)
294 293 response = api_call(self, params)
295 294 expected = {
296 295 'repo': self.REPO,
297 296 'locked': True,
298 297 'locked_since': None,
299 298 'locked_by': TEST_USER_ADMIN_LOGIN,
300 299 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
301 300 % (TEST_USER_ADMIN_LOGIN, self.REPO, True))
302 301 }
303 302 expected['locked_since'] = json.loads(response.body)['result']['locked_since']
304 303 self._compare_ok(id_, expected, given=response.body)
305 304
306 305 def test_api_lock_repo_lock_aquire_by_non_admin(self):
307 306 repo_name = 'api_delete_me'
308 307 fixture.create_repo(repo_name, repo_type=self.REPO_TYPE,
309 308 cur_user=self.TEST_USER_LOGIN)
310 309 try:
311 310 id_, params = _build_data(self.apikey_regular, 'lock',
312 311 repoid=repo_name,
313 312 locked=True)
314 313 response = api_call(self, params)
315 314 expected = {
316 315 'repo': repo_name,
317 316 'locked': True,
318 317 'locked_since': None,
319 318 'locked_by': self.TEST_USER_LOGIN,
320 319 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
321 320 % (self.TEST_USER_LOGIN, repo_name, True))
322 321 }
323 322 expected['locked_since'] = json.loads(response.body)['result']['locked_since']
324 323 self._compare_ok(id_, expected, given=response.body)
325 324 finally:
326 325 fixture.destroy_repo(repo_name)
327 326
328 327 def test_api_lock_repo_lock_aquire_non_admin_with_userid(self):
329 328 repo_name = 'api_delete_me'
330 329 fixture.create_repo(repo_name, repo_type=self.REPO_TYPE,
331 330 cur_user=self.TEST_USER_LOGIN)
332 331 try:
333 332 id_, params = _build_data(self.apikey_regular, 'lock',
334 333 userid=TEST_USER_ADMIN_LOGIN,
335 334 repoid=repo_name,
336 335 locked=True)
337 336 response = api_call(self, params)
338 337 expected = 'userid is not the same as your user'
339 338 self._compare_error(id_, expected, given=response.body)
340 339 finally:
341 340 fixture.destroy_repo(repo_name)
342 341
343 342 def test_api_lock_repo_lock_aquire_non_admin_not_his_repo(self):
344 343 id_, params = _build_data(self.apikey_regular, 'lock',
345 344 repoid=self.REPO,
346 345 locked=True)
347 346 response = api_call(self, params)
348 347 expected = 'repository `%s` does not exist' % (self.REPO)
349 348 self._compare_error(id_, expected, given=response.body)
350 349
351 350 def test_api_lock_repo_lock_release(self):
352 351 id_, params = _build_data(self.apikey, 'lock',
353 352 userid=TEST_USER_ADMIN_LOGIN,
354 353 repoid=self.REPO,
355 354 locked=False)
356 355 response = api_call(self, params)
357 356 expected = {
358 357 'repo': self.REPO,
359 358 'locked': False,
360 359 'locked_since': None,
361 360 'locked_by': TEST_USER_ADMIN_LOGIN,
362 361 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
363 362 % (TEST_USER_ADMIN_LOGIN, self.REPO, False))
364 363 }
365 364 self._compare_ok(id_, expected, given=response.body)
366 365
367 366 def test_api_lock_repo_lock_aquire_optional_userid(self):
368 367 id_, params = _build_data(self.apikey, 'lock',
369 368 repoid=self.REPO,
370 369 locked=True)
371 370 response = api_call(self, params)
372 371 expected = {
373 372 'repo': self.REPO,
374 373 'locked': True,
375 374 'locked_since': None,
376 375 'locked_by': TEST_USER_ADMIN_LOGIN,
377 376 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
378 377 % (TEST_USER_ADMIN_LOGIN, self.REPO, True))
379 378 }
380 379 expected['locked_since'] = json.loads(response.body)['result']['locked_since']
381 380 self._compare_ok(id_, expected, given=response.body)
382 381
383 382 def test_api_lock_repo_lock_optional_locked(self):
384 383 id_, params = _build_data(self.apikey, 'lock',
385 384 repoid=self.REPO)
386 385 response = api_call(self, params)
387 386 time_ = json.loads(response.body)['result']['locked_since']
388 387 expected = {
389 388 'repo': self.REPO,
390 389 'locked': True,
391 390 'locked_since': None,
392 391 'locked_by': TEST_USER_ADMIN_LOGIN,
393 392 'msg': ('Repo `%s` locked by `%s`. '
394 393 % (self.REPO,
395 394 json.dumps(time_to_datetime(time_))))
396 395
397 396 }
398 397 expected['locked_since'] = time_
399 398 self._compare_ok(id_, expected, given=response.body)
400 399
401 400 @mock.patch.object(Repository, 'lock', crash)
402 401 def test_api_lock_error(self):
403 402 id_, params = _build_data(self.apikey, 'lock',
404 403 userid=TEST_USER_ADMIN_LOGIN,
405 404 repoid=self.REPO,
406 405 locked=True)
407 406 response = api_call(self, params)
408 407
409 408 expected = 'Error occurred locking repository `%s`' % self.REPO
410 409 self._compare_error(id_, expected, given=response.body)
411 410
412 411 def test_api_get_locks_regular_user(self):
413 412 id_, params = _build_data(self.apikey_regular, 'get_locks')
414 413 response = api_call(self, params)
415 414 expected = []
416 415 self._compare_ok(id_, expected, given=response.body)
417 416
418 417 def test_api_get_locks_with_userid_regular_user(self):
419 418 id_, params = _build_data(self.apikey_regular, 'get_locks',
420 419 userid=TEST_USER_ADMIN_LOGIN)
421 420 response = api_call(self, params)
422 421 expected = 'userid is not the same as your user'
423 422 self._compare_error(id_, expected, given=response.body)
424 423
425 424 def test_api_get_locks(self):
426 425 id_, params = _build_data(self.apikey, 'get_locks')
427 426 response = api_call(self, params)
428 427 expected = []
429 428 self._compare_ok(id_, expected, given=response.body)
430 429
431 430 def test_api_get_locks_with_userid(self):
432 431 id_, params = _build_data(self.apikey, 'get_locks',
433 432 userid=TEST_USER_REGULAR_LOGIN)
434 433 response = api_call(self, params)
435 434 expected = []
436 435 self._compare_ok(id_, expected, given=response.body)
437 436
438 437 def test_api_create_existing_user(self):
439 438 id_, params = _build_data(self.apikey, 'create_user',
440 439 username=TEST_USER_ADMIN_LOGIN,
441 440 email='test@foo.com',
442 441 password='trololo')
443 442 response = api_call(self, params)
444 443
445 444 expected = "user `%s` already exist" % TEST_USER_ADMIN_LOGIN
446 445 self._compare_error(id_, expected, given=response.body)
447 446
448 447 def test_api_create_user_with_existing_email(self):
449 448 id_, params = _build_data(self.apikey, 'create_user',
450 449 username=TEST_USER_ADMIN_LOGIN + 'new',
451 450 email=TEST_USER_REGULAR_EMAIL,
452 451 password='trololo')
453 452 response = api_call(self, params)
454 453
455 454 expected = "email `%s` already exist" % TEST_USER_REGULAR_EMAIL
456 455 self._compare_error(id_, expected, given=response.body)
457 456
458 457 def test_api_create_user(self):
459 458 username = 'test_new_api_user'
460 459 email = username + "@foo.com"
461 460
462 461 id_, params = _build_data(self.apikey, 'create_user',
463 462 username=username,
464 463 email=email,
465 464 password='trololo')
466 465 response = api_call(self, params)
467 466
468 467 usr = UserModel().get_by_username(username)
469 468 ret = dict(
470 469 msg='created new user `%s`' % username,
471 470 user=jsonify(usr.get_api_data())
472 471 )
473 472
474 473 expected = ret
475 474 self._compare_ok(id_, expected, given=response.body)
476 475
477 476 UserModel().delete(usr.user_id)
478 477 Session().commit()
479 478
480 479 def test_api_create_user_without_password(self):
481 480 username = 'test_new_api_user_passwordless'
482 481 email = username + "@foo.com"
483 482
484 483 id_, params = _build_data(self.apikey, 'create_user',
485 484 username=username,
486 485 email=email)
487 486 response = api_call(self, params)
488 487
489 488 usr = UserModel().get_by_username(username)
490 489 ret = dict(
491 490 msg='created new user `%s`' % username,
492 491 user=jsonify(usr.get_api_data())
493 492 )
494 493
495 494 expected = ret
496 495 self._compare_ok(id_, expected, given=response.body)
497 496
498 497 UserModel().delete(usr.user_id)
499 498 Session().commit()
500 499
501 500 @mock.patch.object(UserModel, 'create_or_update', crash)
502 501 def test_api_create_user_when_exception_happened(self):
503 502
504 503 username = 'test_new_api_user'
505 504 email = username + "@foo.com"
506 505
507 506 id_, params = _build_data(self.apikey, 'create_user',
508 507 username=username,
509 508 email=email,
510 509 password='trololo')
511 510 response = api_call(self, params)
512 511 expected = 'failed to create user `%s`' % username
513 512 self._compare_error(id_, expected, given=response.body)
514 513
515 514 def test_api_delete_user(self):
516 515 usr = UserModel().create_or_update(username=u'test_user',
517 516 password=u'qweqwe',
518 517 email=u'u232@rhodecode.org',
519 518 firstname=u'u1', lastname=u'u1')
520 519 Session().commit()
521 520 username = usr.username
522 521 email = usr.email
523 522 usr_id = usr.user_id
524 523 ## DELETE THIS USER NOW
525 524
526 525 id_, params = _build_data(self.apikey, 'delete_user',
527 526 userid=username,)
528 527 response = api_call(self, params)
529 528
530 529 ret = {'msg': 'deleted user ID:%s %s' % (usr_id, username),
531 530 'user': None}
532 531 expected = ret
533 532 self._compare_ok(id_, expected, given=response.body)
534 533
535 534 @mock.patch.object(UserModel, 'delete', crash)
536 535 def test_api_delete_user_when_exception_happened(self):
537 536 usr = UserModel().create_or_update(username=u'test_user',
538 537 password=u'qweqwe',
539 538 email=u'u232@rhodecode.org',
540 539 firstname=u'u1', lastname=u'u1')
541 540 Session().commit()
542 541 username = usr.username
543 542
544 543 id_, params = _build_data(self.apikey, 'delete_user',
545 544 userid=username,)
546 545 response = api_call(self, params)
547 546 ret = 'failed to delete ID:%s %s' % (usr.user_id,
548 547 usr.username)
549 548 expected = ret
550 549 self._compare_error(id_, expected, given=response.body)
551 550
552 551 @parameterized.expand([('firstname', 'new_username'),
553 552 ('lastname', 'new_username'),
554 553 ('email', 'new_username'),
555 554 ('admin', True),
556 555 ('admin', False),
557 556 ('ldap_dn', 'test'),
558 557 ('ldap_dn', None),
559 558 ('active', False),
560 559 ('active', True),
561 560 ('password', 'newpass')
562 561 ])
563 562 def test_api_update_user(self, name, expected):
564 563 usr = UserModel().get_by_username(self.TEST_USER_LOGIN)
565 564 kw = {name: expected,
566 565 'userid': usr.user_id}
567 566 id_, params = _build_data(self.apikey, 'update_user', **kw)
568 567 response = api_call(self, params)
569 568
570 569 ret = {
571 570 'msg': 'updated user ID:%s %s' % (usr.user_id, self.TEST_USER_LOGIN),
572 571 'user': jsonify(UserModel()\
573 572 .get_by_username(self.TEST_USER_LOGIN)\
574 573 .get_api_data())
575 574 }
576 575
577 576 expected = ret
578 577 self._compare_ok(id_, expected, given=response.body)
579 578
580 579 def test_api_update_user_no_changed_params(self):
581 580 usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
582 581 ret = jsonify(usr.get_api_data())
583 582 id_, params = _build_data(self.apikey, 'update_user',
584 583 userid=TEST_USER_ADMIN_LOGIN)
585 584
586 585 response = api_call(self, params)
587 586 ret = {
588 587 'msg': 'updated user ID:%s %s' % (usr.user_id, TEST_USER_ADMIN_LOGIN),
589 588 'user': ret
590 589 }
591 590 expected = ret
592 591 self._compare_ok(id_, expected, given=response.body)
593 592
594 593 def test_api_update_user_by_user_id(self):
595 594 usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
596 595 ret = jsonify(usr.get_api_data())
597 596 id_, params = _build_data(self.apikey, 'update_user',
598 597 userid=usr.user_id)
599 598
600 599 response = api_call(self, params)
601 600 ret = {
602 601 'msg': 'updated user ID:%s %s' % (usr.user_id, TEST_USER_ADMIN_LOGIN),
603 602 'user': ret
604 603 }
605 604 expected = ret
606 605 self._compare_ok(id_, expected, given=response.body)
607 606
608 607 @mock.patch.object(UserModel, 'update_user', crash)
609 608 def test_api_update_user_when_exception_happens(self):
610 609 usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
611 610 ret = jsonify(usr.get_api_data())
612 611 id_, params = _build_data(self.apikey, 'update_user',
613 612 userid=usr.user_id)
614 613
615 614 response = api_call(self, params)
616 615 ret = 'failed to update user `%s`' % usr.user_id
617 616
618 617 expected = ret
619 618 self._compare_error(id_, expected, given=response.body)
620 619
621 620 def test_api_get_repo(self):
622 621 new_group = 'some_new_group'
623 622 make_users_group(new_group)
624 623 RepoModel().grant_users_group_permission(repo=self.REPO,
625 624 group_name=new_group,
626 625 perm='repository.read')
627 626 Session().commit()
628 627 id_, params = _build_data(self.apikey, 'get_repo',
629 628 repoid=self.REPO)
630 629 response = api_call(self, params)
631 630
632 631 repo = RepoModel().get_by_repo_name(self.REPO)
633 632 ret = repo.get_api_data()
634 633
635 634 members = []
636 635 followers = []
637 636 for user in repo.repo_to_perm:
638 637 perm = user.permission.permission_name
639 638 user = user.user
640 639 user_data = user.get_api_data()
641 640 user_data['type'] = "user"
642 641 user_data['permission'] = perm
643 642 members.append(user_data)
644 643
645 644 for users_group in repo.users_group_to_perm:
646 645 perm = users_group.permission.permission_name
647 646 users_group = users_group.users_group
648 647 users_group_data = users_group.get_api_data()
649 648 users_group_data['type'] = "users_group"
650 649 users_group_data['permission'] = perm
651 650 members.append(users_group_data)
652 651
653 652 for user in repo.followers:
654 653 followers.append(user.user.get_api_data())
655 654
656 655 ret['members'] = members
657 656 ret['followers'] = followers
658 657
659 658 expected = ret
660 659 self._compare_ok(id_, expected, given=response.body)
661 660 destroy_users_group(new_group)
662 661
663 662 def test_api_get_repo_by_non_admin(self):
664 663 id_, params = _build_data(self.apikey, 'get_repo',
665 664 repoid=self.REPO)
666 665 response = api_call(self, params)
667 666
668 667 repo = RepoModel().get_by_repo_name(self.REPO)
669 668 ret = repo.get_api_data()
670 669
671 670 members = []
672 671 followers = []
673 672 for user in repo.repo_to_perm:
674 673 perm = user.permission.permission_name
675 674 user = user.user
676 675 user_data = user.get_api_data()
677 676 user_data['type'] = "user"
678 677 user_data['permission'] = perm
679 678 members.append(user_data)
680 679
681 680 for users_group in repo.users_group_to_perm:
682 681 perm = users_group.permission.permission_name
683 682 users_group = users_group.users_group
684 683 users_group_data = users_group.get_api_data()
685 684 users_group_data['type'] = "users_group"
686 685 users_group_data['permission'] = perm
687 686 members.append(users_group_data)
688 687
689 688 for user in repo.followers:
690 689 followers.append(user.user.get_api_data())
691 690
692 691 ret['members'] = members
693 692 ret['followers'] = followers
694 693
695 694 expected = ret
696 695 self._compare_ok(id_, expected, given=response.body)
697 696
698 697 def test_api_get_repo_by_non_admin_no_permission_to_repo(self):
699 698 RepoModel().grant_user_permission(repo=self.REPO,
700 699 user=self.TEST_USER_LOGIN,
701 700 perm='repository.none')
702 701
703 702 id_, params = _build_data(self.apikey_regular, 'get_repo',
704 703 repoid=self.REPO)
705 704 response = api_call(self, params)
706 705
707 706 expected = 'repository `%s` does not exist' % (self.REPO)
708 707 self._compare_error(id_, expected, given=response.body)
709 708
710 709 def test_api_get_repo_that_doesn_not_exist(self):
711 710 id_, params = _build_data(self.apikey, 'get_repo',
712 711 repoid='no-such-repo')
713 712 response = api_call(self, params)
714 713
715 714 ret = 'repository `%s` does not exist' % 'no-such-repo'
716 715 expected = ret
717 716 self._compare_error(id_, expected, given=response.body)
718 717
719 718 def test_api_get_repos(self):
720 719 id_, params = _build_data(self.apikey, 'get_repos')
721 720 response = api_call(self, params)
722 721
723 722 result = []
724 723 for repo in RepoModel().get_all():
725 724 result.append(repo.get_api_data())
726 725 ret = jsonify(result)
727 726
728 727 expected = ret
729 728 self._compare_ok(id_, expected, given=response.body)
730 729
731 730 def test_api_get_repos_non_admin(self):
732 731 id_, params = _build_data(self.apikey_regular, 'get_repos')
733 732 response = api_call(self, params)
734 733
735 734 result = []
736 735 for repo in RepoModel().get_all_user_repos(self.TEST_USER_LOGIN):
737 736 result.append(repo.get_api_data())
738 737 ret = jsonify(result)
739 738
740 739 expected = ret
741 740 self._compare_ok(id_, expected, given=response.body)
742 741
743 742 @parameterized.expand([('all', 'all'),
744 743 ('dirs', 'dirs'),
745 744 ('files', 'files'), ])
746 745 def test_api_get_repo_nodes(self, name, ret_type):
747 746 rev = 'tip'
748 747 path = '/'
749 748 id_, params = _build_data(self.apikey, 'get_repo_nodes',
750 749 repoid=self.REPO, revision=rev,
751 750 root_path=path,
752 751 ret_type=ret_type)
753 752 response = api_call(self, params)
754 753
755 754 # we don't the actual return types here since it's tested somewhere
756 755 # else
757 756 expected = json.loads(response.body)['result']
758 757 self._compare_ok(id_, expected, given=response.body)
759 758
760 759 def test_api_get_repo_nodes_bad_revisions(self):
761 760 rev = 'i-dont-exist'
762 761 path = '/'
763 762 id_, params = _build_data(self.apikey, 'get_repo_nodes',
764 763 repoid=self.REPO, revision=rev,
765 764 root_path=path,)
766 765 response = api_call(self, params)
767 766
768 767 expected = 'failed to get repo: `%s` nodes' % self.REPO
769 768 self._compare_error(id_, expected, given=response.body)
770 769
771 770 def test_api_get_repo_nodes_bad_path(self):
772 771 rev = 'tip'
773 772 path = '/idontexits'
774 773 id_, params = _build_data(self.apikey, 'get_repo_nodes',
775 774 repoid=self.REPO, revision=rev,
776 775 root_path=path,)
777 776 response = api_call(self, params)
778 777
779 778 expected = 'failed to get repo: `%s` nodes' % self.REPO
780 779 self._compare_error(id_, expected, given=response.body)
781 780
782 781 def test_api_get_repo_nodes_bad_ret_type(self):
783 782 rev = 'tip'
784 783 path = '/'
785 784 ret_type = 'error'
786 785 id_, params = _build_data(self.apikey, 'get_repo_nodes',
787 786 repoid=self.REPO, revision=rev,
788 787 root_path=path,
789 788 ret_type=ret_type)
790 789 response = api_call(self, params)
791 790
792 791 expected = 'ret_type must be one of %s' % (['files', 'dirs', 'all'])
793 792 self._compare_error(id_, expected, given=response.body)
794 793
795 794 def test_api_create_repo(self):
796 795 repo_name = 'api-repo'
797 796 id_, params = _build_data(self.apikey, 'create_repo',
798 797 repo_name=repo_name,
799 798 owner=TEST_USER_ADMIN_LOGIN,
800 799 repo_type='hg',
801 800 )
802 801 response = api_call(self, params)
803 802
804 803 repo = RepoModel().get_by_repo_name(repo_name)
805 804 ret = {
806 805 'msg': 'Created new repository `%s`' % repo_name,
807 806 'repo': jsonify(repo.get_api_data())
808 807 }
809 808 expected = ret
810 809 self._compare_ok(id_, expected, given=response.body)
811 810 fixture.destroy_repo(repo_name)
812 811
813 812 def test_api_create_repo_unknown_owner(self):
814 813 repo_name = 'api-repo'
815 814 owner = 'i-dont-exist'
816 815 id_, params = _build_data(self.apikey, 'create_repo',
817 816 repo_name=repo_name,
818 817 owner=owner,
819 818 repo_type='hg',
820 819 )
821 820 response = api_call(self, params)
822 821 expected = 'user `%s` does not exist' % owner
823 822 self._compare_error(id_, expected, given=response.body)
824 823
825 824 def test_api_create_repo_dont_specify_owner(self):
826 825 repo_name = 'api-repo'
827 826 owner = 'i-dont-exist'
828 827 id_, params = _build_data(self.apikey, 'create_repo',
829 828 repo_name=repo_name,
830 829 repo_type='hg',
831 830 )
832 831 response = api_call(self, params)
833 832
834 833 repo = RepoModel().get_by_repo_name(repo_name)
835 834 ret = {
836 835 'msg': 'Created new repository `%s`' % repo_name,
837 836 'repo': jsonify(repo.get_api_data())
838 837 }
839 838 expected = ret
840 839 self._compare_ok(id_, expected, given=response.body)
841 840 fixture.destroy_repo(repo_name)
842 841
843 842 def test_api_create_repo_by_non_admin(self):
844 843 repo_name = 'api-repo'
845 844 owner = 'i-dont-exist'
846 845 id_, params = _build_data(self.apikey_regular, 'create_repo',
847 846 repo_name=repo_name,
848 847 repo_type='hg',
849 848 )
850 849 response = api_call(self, params)
851 850
852 851 repo = RepoModel().get_by_repo_name(repo_name)
853 852 ret = {
854 853 'msg': 'Created new repository `%s`' % repo_name,
855 854 'repo': jsonify(repo.get_api_data())
856 855 }
857 856 expected = ret
858 857 self._compare_ok(id_, expected, given=response.body)
859 858 fixture.destroy_repo(repo_name)
860 859
861 860 def test_api_create_repo_by_non_admin_specify_owner(self):
862 861 repo_name = 'api-repo'
863 862 owner = 'i-dont-exist'
864 863 id_, params = _build_data(self.apikey_regular, 'create_repo',
865 864 repo_name=repo_name,
866 865 repo_type='hg',
867 866 owner=owner
868 867 )
869 868 response = api_call(self, params)
870 869
871 870 expected = 'Only RhodeCode admin can specify `owner` param'
872 871 self._compare_error(id_, expected, given=response.body)
873 872 fixture.destroy_repo(repo_name)
874 873
875 874 def test_api_create_repo_exists(self):
876 875 repo_name = self.REPO
877 876 id_, params = _build_data(self.apikey, 'create_repo',
878 877 repo_name=repo_name,
879 878 owner=TEST_USER_ADMIN_LOGIN,
880 879 repo_type='hg',
881 880 )
882 881 response = api_call(self, params)
883 882 expected = "repo `%s` already exist" % repo_name
884 883 self._compare_error(id_, expected, given=response.body)
885 884
886 885 @mock.patch.object(RepoModel, 'create_repo', crash)
887 886 def test_api_create_repo_exception_occurred(self):
888 887 repo_name = 'api-repo'
889 888 id_, params = _build_data(self.apikey, 'create_repo',
890 889 repo_name=repo_name,
891 890 owner=TEST_USER_ADMIN_LOGIN,
892 891 repo_type='hg',
893 892 )
894 893 response = api_call(self, params)
895 894 expected = 'failed to create repository `%s`' % repo_name
896 895 self._compare_error(id_, expected, given=response.body)
897 896
898 897 def test_api_delete_repo(self):
899 898 repo_name = 'api_delete_me'
900 899 fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
901 900
902 901 id_, params = _build_data(self.apikey, 'delete_repo',
903 902 repoid=repo_name,)
904 903 response = api_call(self, params)
905 904
906 905 ret = {
907 906 'msg': 'Deleted repository `%s`' % repo_name,
908 907 'success': True
909 908 }
910 909 expected = ret
911 910 self._compare_ok(id_, expected, given=response.body)
912 911
913 912 def test_api_delete_repo_by_non_admin(self):
914 913 repo_name = 'api_delete_me'
915 914 fixture.create_repo(repo_name, repo_type=self.REPO_TYPE,
916 915 cur_user=self.TEST_USER_LOGIN)
917 916 try:
918 917 id_, params = _build_data(self.apikey_regular, 'delete_repo',
919 918 repoid=repo_name,)
920 919 response = api_call(self, params)
921 920
922 921 ret = {
923 922 'msg': 'Deleted repository `%s`' % repo_name,
924 923 'success': True
925 924 }
926 925 expected = ret
927 926 self._compare_ok(id_, expected, given=response.body)
928 927 finally:
929 928 fixture.destroy_repo(repo_name)
930 929
931 930 def test_api_delete_repo_by_non_admin_no_permission(self):
932 931 repo_name = 'api_delete_me'
933 932 fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
934 933 try:
935 934 id_, params = _build_data(self.apikey_regular, 'delete_repo',
936 935 repoid=repo_name,)
937 936 response = api_call(self, params)
938 937 expected = 'repository `%s` does not exist' % (repo_name)
939 938 self._compare_error(id_, expected, given=response.body)
940 939 finally:
941 940 fixture.destroy_repo(repo_name)
942 941
943 942 def test_api_delete_repo_exception_occurred(self):
944 943 repo_name = 'api_delete_me'
945 944 fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
946 945 try:
947 946 with mock.patch.object(RepoModel, 'delete', crash):
948 947 id_, params = _build_data(self.apikey, 'delete_repo',
949 948 repoid=repo_name,)
950 949 response = api_call(self, params)
951 950
952 951 expected = 'failed to delete repository `%s`' % repo_name
953 952 self._compare_error(id_, expected, given=response.body)
954 953 finally:
955 954 fixture.destroy_repo(repo_name)
956 955
957 956 def test_api_fork_repo(self):
958 957 fork_name = 'api-repo-fork'
959 958 id_, params = _build_data(self.apikey, 'fork_repo',
960 959 repoid=self.REPO,
961 960 fork_name=fork_name,
962 961 owner=TEST_USER_ADMIN_LOGIN,
963 962 )
964 963 response = api_call(self, params)
965 964
966 965 ret = {
967 966 'msg': 'Created fork of `%s` as `%s`' % (self.REPO,
968 967 fork_name),
969 968 'success': True
970 969 }
971 970 expected = ret
972 971 self._compare_ok(id_, expected, given=response.body)
973 972 fixture.destroy_repo(fork_name)
974 973
975 974 def test_api_fork_repo_non_admin(self):
976 975 fork_name = 'api-repo-fork'
977 976 id_, params = _build_data(self.apikey_regular, 'fork_repo',
978 977 repoid=self.REPO,
979 978 fork_name=fork_name,
980 979 )
981 980 response = api_call(self, params)
982 981
983 982 ret = {
984 983 'msg': 'Created fork of `%s` as `%s`' % (self.REPO,
985 984 fork_name),
986 985 'success': True
987 986 }
988 987 expected = ret
989 988 self._compare_ok(id_, expected, given=response.body)
990 989 fixture.destroy_repo(fork_name)
991 990
992 991 def test_api_fork_repo_non_admin_specify_owner(self):
993 992 fork_name = 'api-repo-fork'
994 993 id_, params = _build_data(self.apikey_regular, 'fork_repo',
995 994 repoid=self.REPO,
996 995 fork_name=fork_name,
997 996 owner=TEST_USER_ADMIN_LOGIN,
998 997 )
999 998 response = api_call(self, params)
1000 999 expected = 'Only RhodeCode admin can specify `owner` param'
1001 1000 self._compare_error(id_, expected, given=response.body)
1002 1001 fixture.destroy_repo(fork_name)
1003 1002
1004 1003 def test_api_fork_repo_non_admin_no_permission_to_fork(self):
1005 1004 RepoModel().grant_user_permission(repo=self.REPO,
1006 1005 user=self.TEST_USER_LOGIN,
1007 1006 perm='repository.none')
1008 1007 fork_name = 'api-repo-fork'
1009 1008 id_, params = _build_data(self.apikey_regular, 'fork_repo',
1010 1009 repoid=self.REPO,
1011 1010 fork_name=fork_name,
1012 1011 )
1013 1012 response = api_call(self, params)
1014 1013 expected = 'repository `%s` does not exist' % (self.REPO)
1015 1014 self._compare_error(id_, expected, given=response.body)
1016 1015 fixture.destroy_repo(fork_name)
1017 1016
1018 1017 def test_api_fork_repo_unknown_owner(self):
1019 1018 fork_name = 'api-repo-fork'
1020 1019 owner = 'i-dont-exist'
1021 1020 id_, params = _build_data(self.apikey, 'fork_repo',
1022 1021 repoid=self.REPO,
1023 1022 fork_name=fork_name,
1024 1023 owner=owner,
1025 1024 )
1026 1025 response = api_call(self, params)
1027 1026 expected = 'user `%s` does not exist' % owner
1028 1027 self._compare_error(id_, expected, given=response.body)
1029 1028
1030 1029 def test_api_fork_repo_fork_exists(self):
1031 1030 fork_name = 'api-repo-fork'
1032 1031 fixture.create_fork(self.REPO, fork_name)
1033 1032
1034 1033 try:
1035 1034 fork_name = 'api-repo-fork'
1036 1035
1037 1036 id_, params = _build_data(self.apikey, 'fork_repo',
1038 1037 repoid=self.REPO,
1039 1038 fork_name=fork_name,
1040 1039 owner=TEST_USER_ADMIN_LOGIN,
1041 1040 )
1042 1041 response = api_call(self, params)
1043 1042
1044 1043 expected = "fork `%s` already exist" % fork_name
1045 1044 self._compare_error(id_, expected, given=response.body)
1046 1045 finally:
1047 1046 fixture.destroy_repo(fork_name)
1048 1047
1049 1048 def test_api_fork_repo_repo_exists(self):
1050 1049 fork_name = self.REPO
1051 1050
1052 1051 id_, params = _build_data(self.apikey, 'fork_repo',
1053 1052 repoid=self.REPO,
1054 1053 fork_name=fork_name,
1055 1054 owner=TEST_USER_ADMIN_LOGIN,
1056 1055 )
1057 1056 response = api_call(self, params)
1058 1057
1059 1058 expected = "repo `%s` already exist" % fork_name
1060 1059 self._compare_error(id_, expected, given=response.body)
1061 1060
1062 1061 @mock.patch.object(RepoModel, 'create_fork', crash)
1063 1062 def test_api_fork_repo_exception_occurred(self):
1064 1063 fork_name = 'api-repo-fork'
1065 1064 id_, params = _build_data(self.apikey, 'fork_repo',
1066 1065 repoid=self.REPO,
1067 1066 fork_name=fork_name,
1068 1067 owner=TEST_USER_ADMIN_LOGIN,
1069 1068 )
1070 1069 response = api_call(self, params)
1071 1070
1072 1071 expected = 'failed to fork repository `%s` as `%s`' % (self.REPO,
1073 1072 fork_name)
1074 1073 self._compare_error(id_, expected, given=response.body)
1075 1074
1076 1075 def test_api_get_users_group(self):
1077 1076 id_, params = _build_data(self.apikey, 'get_users_group',
1078 1077 usersgroupid=TEST_USER_GROUP)
1079 1078 response = api_call(self, params)
1080 1079
1081 1080 users_group = UserGroupModel().get_group(TEST_USER_GROUP)
1082 1081 members = []
1083 1082 for user in users_group.members:
1084 1083 user = user.user
1085 1084 members.append(user.get_api_data())
1086 1085
1087 1086 ret = users_group.get_api_data()
1088 1087 ret['members'] = members
1089 1088 expected = ret
1090 1089 self._compare_ok(id_, expected, given=response.body)
1091 1090
1092 1091 def test_api_get_users_groups(self):
1093 1092
1094 1093 make_users_group('test_users_group2')
1095 1094
1096 1095 id_, params = _build_data(self.apikey, 'get_users_groups',)
1097 1096 response = api_call(self, params)
1098 1097
1099 1098 expected = []
1100 1099 for gr_name in [TEST_USER_GROUP, 'test_users_group2']:
1101 1100 users_group = UserGroupModel().get_group(gr_name)
1102 1101 ret = users_group.get_api_data()
1103 1102 expected.append(ret)
1104 1103 self._compare_ok(id_, expected, given=response.body)
1105 1104
1106 1105 UserGroupModel().delete(users_group='test_users_group2')
1107 1106 Session().commit()
1108 1107
1109 1108 def test_api_create_users_group(self):
1110 1109 group_name = 'some_new_group'
1111 1110 id_, params = _build_data(self.apikey, 'create_users_group',
1112 1111 group_name=group_name)
1113 1112 response = api_call(self, params)
1114 1113
1115 1114 ret = {
1116 1115 'msg': 'created new user group `%s`' % group_name,
1117 1116 'users_group': jsonify(UserGroupModel()\
1118 1117 .get_by_name(group_name)\
1119 1118 .get_api_data())
1120 1119 }
1121 1120 expected = ret
1122 1121 self._compare_ok(id_, expected, given=response.body)
1123 1122
1124 1123 destroy_users_group(group_name)
1125 1124
1126 1125 def test_api_get_users_group_that_exist(self):
1127 1126 id_, params = _build_data(self.apikey, 'create_users_group',
1128 1127 group_name=TEST_USER_GROUP)
1129 1128 response = api_call(self, params)
1130 1129
1131 1130 expected = "user group `%s` already exist" % TEST_USER_GROUP
1132 1131 self._compare_error(id_, expected, given=response.body)
1133 1132
1134 1133 @mock.patch.object(UserGroupModel, 'create', crash)
1135 1134 def test_api_get_users_group_exception_occurred(self):
1136 1135 group_name = 'exception_happens'
1137 1136 id_, params = _build_data(self.apikey, 'create_users_group',
1138 1137 group_name=group_name)
1139 1138 response = api_call(self, params)
1140 1139
1141 1140 expected = 'failed to create group `%s`' % group_name
1142 1141 self._compare_error(id_, expected, given=response.body)
1143 1142
1144 1143 def test_api_add_user_to_users_group(self):
1145 1144 gr_name = 'test_group'
1146 1145 fixture.create_user_group(gr_name)
1147 1146 id_, params = _build_data(self.apikey, 'add_user_to_users_group',
1148 1147 usersgroupid=gr_name,
1149 1148 userid=TEST_USER_ADMIN_LOGIN)
1150 1149 response = api_call(self, params)
1151 1150
1152 1151 expected = {
1153 1152 'msg': 'added member `%s` to user group `%s`' % (
1154 1153 TEST_USER_ADMIN_LOGIN, gr_name
1155 1154 ),
1156 1155 'success': True}
1157 1156 self._compare_ok(id_, expected, given=response.body)
1158 1157
1159 1158 UserGroupModel().delete(users_group=gr_name)
1160 1159 Session().commit()
1161 1160
1162 1161 def test_api_add_user_to_users_group_that_doesnt_exist(self):
1163 1162 id_, params = _build_data(self.apikey, 'add_user_to_users_group',
1164 1163 usersgroupid='false-group',
1165 1164 userid=TEST_USER_ADMIN_LOGIN)
1166 1165 response = api_call(self, params)
1167 1166
1168 1167 expected = 'user group `%s` does not exist' % 'false-group'
1169 1168 self._compare_error(id_, expected, given=response.body)
1170 1169
1171 1170 @mock.patch.object(UserGroupModel, 'add_user_to_group', crash)
1172 1171 def test_api_add_user_to_users_group_exception_occurred(self):
1173 1172 gr_name = 'test_group'
1174 1173 fixture.create_user_group(gr_name)
1175 1174 id_, params = _build_data(self.apikey, 'add_user_to_users_group',
1176 1175 usersgroupid=gr_name,
1177 1176 userid=TEST_USER_ADMIN_LOGIN)
1178 1177 response = api_call(self, params)
1179 1178
1180 1179 expected = 'failed to add member to user group `%s`' % gr_name
1181 1180 self._compare_error(id_, expected, given=response.body)
1182 1181
1183 1182 UserGroupModel().delete(users_group=gr_name)
1184 1183 Session().commit()
1185 1184
1186 1185 def test_api_remove_user_from_users_group(self):
1187 1186 gr_name = 'test_group_3'
1188 1187 gr = fixture.create_user_group(gr_name)
1189 1188 UserGroupModel().add_user_to_group(gr, user=TEST_USER_ADMIN_LOGIN)
1190 1189 Session().commit()
1191 1190 id_, params = _build_data(self.apikey, 'remove_user_from_users_group',
1192 1191 usersgroupid=gr_name,
1193 1192 userid=TEST_USER_ADMIN_LOGIN)
1194 1193 response = api_call(self, params)
1195 1194
1196 1195 expected = {
1197 1196 'msg': 'removed member `%s` from user group `%s`' % (
1198 1197 TEST_USER_ADMIN_LOGIN, gr_name
1199 1198 ),
1200 1199 'success': True}
1201 1200 self._compare_ok(id_, expected, given=response.body)
1202 1201
1203 1202 UserGroupModel().delete(users_group=gr_name)
1204 1203 Session().commit()
1205 1204
1206 1205 @mock.patch.object(UserGroupModel, 'remove_user_from_group', crash)
1207 1206 def test_api_remove_user_from_users_group_exception_occurred(self):
1208 1207 gr_name = 'test_group_3'
1209 1208 gr = fixture.create_user_group(gr_name)
1210 1209 UserGroupModel().add_user_to_group(gr, user=TEST_USER_ADMIN_LOGIN)
1211 1210 Session().commit()
1212 1211 id_, params = _build_data(self.apikey, 'remove_user_from_users_group',
1213 1212 usersgroupid=gr_name,
1214 1213 userid=TEST_USER_ADMIN_LOGIN)
1215 1214 response = api_call(self, params)
1216 1215
1217 1216 expected = 'failed to remove member from user group `%s`' % gr_name
1218 1217 self._compare_error(id_, expected, given=response.body)
1219 1218
1220 1219 UserGroupModel().delete(users_group=gr_name)
1221 1220 Session().commit()
1222 1221
1223 1222 @parameterized.expand([('none', 'repository.none'),
1224 1223 ('read', 'repository.read'),
1225 1224 ('write', 'repository.write'),
1226 1225 ('admin', 'repository.admin')])
1227 1226 def test_api_grant_user_permission(self, name, perm):
1228 1227 id_, params = _build_data(self.apikey, 'grant_user_permission',
1229 1228 repoid=self.REPO,
1230 1229 userid=TEST_USER_ADMIN_LOGIN,
1231 1230 perm=perm)
1232 1231 response = api_call(self, params)
1233 1232
1234 1233 ret = {
1235 1234 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1236 1235 perm, TEST_USER_ADMIN_LOGIN, self.REPO
1237 1236 ),
1238 1237 'success': True
1239 1238 }
1240 1239 expected = ret
1241 1240 self._compare_ok(id_, expected, given=response.body)
1242 1241
1243 1242 def test_api_grant_user_permission_wrong_permission(self):
1244 1243 perm = 'haha.no.permission'
1245 1244 id_, params = _build_data(self.apikey, 'grant_user_permission',
1246 1245 repoid=self.REPO,
1247 1246 userid=TEST_USER_ADMIN_LOGIN,
1248 1247 perm=perm)
1249 1248 response = api_call(self, params)
1250 1249
1251 1250 expected = 'permission `%s` does not exist' % perm
1252 1251 self._compare_error(id_, expected, given=response.body)
1253 1252
1254 1253 @mock.patch.object(RepoModel, 'grant_user_permission', crash)
1255 1254 def test_api_grant_user_permission_exception_when_adding(self):
1256 1255 perm = 'repository.read'
1257 1256 id_, params = _build_data(self.apikey, 'grant_user_permission',
1258 1257 repoid=self.REPO,
1259 1258 userid=TEST_USER_ADMIN_LOGIN,
1260 1259 perm=perm)
1261 1260 response = api_call(self, params)
1262 1261
1263 1262 expected = 'failed to edit permission for user: `%s` in repo: `%s`' % (
1264 1263 TEST_USER_ADMIN_LOGIN, self.REPO
1265 1264 )
1266 1265 self._compare_error(id_, expected, given=response.body)
1267 1266
1268 1267 def test_api_revoke_user_permission(self):
1269 1268 id_, params = _build_data(self.apikey, 'revoke_user_permission',
1270 1269 repoid=self.REPO,
1271 1270 userid=TEST_USER_ADMIN_LOGIN,)
1272 1271 response = api_call(self, params)
1273 1272
1274 1273 expected = {
1275 1274 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1276 1275 TEST_USER_ADMIN_LOGIN, self.REPO
1277 1276 ),
1278 1277 'success': True
1279 1278 }
1280 1279 self._compare_ok(id_, expected, given=response.body)
1281 1280
1282 1281 @mock.patch.object(RepoModel, 'revoke_user_permission', crash)
1283 1282 def test_api_revoke_user_permission_exception_when_adding(self):
1284 1283 id_, params = _build_data(self.apikey, 'revoke_user_permission',
1285 1284 repoid=self.REPO,
1286 1285 userid=TEST_USER_ADMIN_LOGIN,)
1287 1286 response = api_call(self, params)
1288 1287
1289 1288 expected = 'failed to edit permission for user: `%s` in repo: `%s`' % (
1290 1289 TEST_USER_ADMIN_LOGIN, self.REPO
1291 1290 )
1292 1291 self._compare_error(id_, expected, given=response.body)
1293 1292
1294 1293 @parameterized.expand([('none', 'repository.none'),
1295 1294 ('read', 'repository.read'),
1296 1295 ('write', 'repository.write'),
1297 1296 ('admin', 'repository.admin')])
1298 1297 def test_api_grant_users_group_permission(self, name, perm):
1299 1298 id_, params = _build_data(self.apikey, 'grant_users_group_permission',
1300 1299 repoid=self.REPO,
1301 1300 usersgroupid=TEST_USER_GROUP,
1302 1301 perm=perm)
1303 1302 response = api_call(self, params)
1304 1303
1305 1304 ret = {
1306 1305 'msg': 'Granted perm: `%s` for user group: `%s` in repo: `%s`' % (
1307 1306 perm, TEST_USER_GROUP, self.REPO
1308 1307 ),
1309 1308 'success': True
1310 1309 }
1311 1310 expected = ret
1312 1311 self._compare_ok(id_, expected, given=response.body)
1313 1312
1314 1313 def test_api_grant_users_group_permission_wrong_permission(self):
1315 1314 perm = 'haha.no.permission'
1316 1315 id_, params = _build_data(self.apikey, 'grant_users_group_permission',
1317 1316 repoid=self.REPO,
1318 1317 usersgroupid=TEST_USER_GROUP,
1319 1318 perm=perm)
1320 1319 response = api_call(self, params)
1321 1320
1322 1321 expected = 'permission `%s` does not exist' % perm
1323 1322 self._compare_error(id_, expected, given=response.body)
1324 1323
1325 1324 @mock.patch.object(RepoModel, 'grant_users_group_permission', crash)
1326 1325 def test_api_grant_users_group_permission_exception_when_adding(self):
1327 1326 perm = 'repository.read'
1328 1327 id_, params = _build_data(self.apikey, 'grant_users_group_permission',
1329 1328 repoid=self.REPO,
1330 1329 usersgroupid=TEST_USER_GROUP,
1331 1330 perm=perm)
1332 1331 response = api_call(self, params)
1333 1332
1334 1333 expected = 'failed to edit permission for user group: `%s` in repo: `%s`' % (
1335 1334 TEST_USER_GROUP, self.REPO
1336 1335 )
1337 1336 self._compare_error(id_, expected, given=response.body)
1338 1337
1339 1338 def test_api_revoke_users_group_permission(self):
1340 1339 RepoModel().grant_users_group_permission(repo=self.REPO,
1341 1340 group_name=TEST_USER_GROUP,
1342 1341 perm='repository.read')
1343 1342 Session().commit()
1344 1343 id_, params = _build_data(self.apikey, 'revoke_users_group_permission',
1345 1344 repoid=self.REPO,
1346 1345 usersgroupid=TEST_USER_GROUP,)
1347 1346 response = api_call(self, params)
1348 1347
1349 1348 expected = {
1350 1349 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1351 1350 TEST_USER_GROUP, self.REPO
1352 1351 ),
1353 1352 'success': True
1354 1353 }
1355 1354 self._compare_ok(id_, expected, given=response.body)
1356 1355
1357 1356 @mock.patch.object(RepoModel, 'revoke_users_group_permission', crash)
1358 1357 def test_api_revoke_users_group_permission_exception_when_adding(self):
1359 1358
1360 1359 id_, params = _build_data(self.apikey, 'revoke_users_group_permission',
1361 1360 repoid=self.REPO,
1362 1361 usersgroupid=TEST_USER_GROUP,)
1363 1362 response = api_call(self, params)
1364 1363
1365 1364 expected = 'failed to edit permission for user group: `%s` in repo: `%s`' % (
1366 1365 TEST_USER_GROUP, self.REPO
1367 1366 )
1368 1367 self._compare_error(id_, expected, given=response.body)
@@ -1,411 +1,401 b''
1 1 from rhodecode.tests import *
2 2 from rhodecode.model.repo import RepoModel
3 3 from rhodecode.model.meta import Session
4 4 from rhodecode.model.db import Repository
5 5 from rhodecode.model.scm import ScmModel
6 6 from rhodecode.lib.vcs.backends.base import EmptyChangeset
7 7 from rhodecode.tests.fixture import Fixture
8 8
9 9 fixture = Fixture()
10 10
11 11
12 12 def _commit_change(repo, filename, content, message, vcs_type, parent=None, newfile=False):
13 13 repo = Repository.get_by_repo_name(repo)
14 14 _cs = parent
15 15 if not parent:
16 16 _cs = EmptyChangeset(alias=vcs_type)
17 17
18 18 if newfile:
19 cs = ScmModel().create_node(
20 repo=repo.scm_instance, repo_name=repo.repo_name,
21 cs=_cs, user=TEST_USER_ADMIN_LOGIN,
19 nodes = {
20 filename: {
21 'content': content
22 }
23 }
24 cs = ScmModel().create_nodes(
25 user=TEST_USER_ADMIN_LOGIN, repo=repo,
26 message=message,
27 nodes=nodes,
28 parent_cs=_cs,
22 29 author=TEST_USER_ADMIN_LOGIN,
23 message=message,
24 content=content,
25 f_path=filename
26 30 )
27 31 else:
28 32 cs = ScmModel().commit_change(
29 33 repo=repo.scm_instance, repo_name=repo.repo_name,
30 34 cs=parent, user=TEST_USER_ADMIN_LOGIN,
31 35 author=TEST_USER_ADMIN_LOGIN,
32 36 message=message,
33 37 content=content,
34 38 f_path=filename
35 39 )
36 40 return cs
37 41
38 42
39 43 class TestCompareController(TestController):
40 44
41 45 def setUp(self):
42 46 self.r1_id = None
43 47 self.r2_id = None
44 48
45 49 def tearDown(self):
46 50 if self.r2_id:
47 51 RepoModel().delete(self.r2_id)
48 52 if self.r1_id:
49 53 RepoModel().delete(self.r1_id)
50 54 Session().commit()
51 55 Session.remove()
52 56
53 57 def test_compare_forks_on_branch_extra_commits_hg(self):
54 58 self.log_user()
55 59 repo1 = fixture.create_repo('one', repo_type='hg',
56 60 repo_description='diff-test',
57 61 cur_user=TEST_USER_ADMIN_LOGIN)
58 62 self.r1_id = repo1.repo_id
59 63 #commit something !
60 64 cs0 = _commit_change(repo1.repo_name, filename='file1', content='line1\n',
61 65 message='commit1', vcs_type='hg', parent=None, newfile=True)
62 66
63 67 #fork this repo
64 68 repo2 = fixture.create_fork('one', 'one-fork')
65 69 self.r2_id = repo2.repo_id
66 70
67 71 #add two extra commit into fork
68 72 cs1 = _commit_change(repo2.repo_name, filename='file1', content='line1\nline2\n',
69 73 message='commit2', vcs_type='hg', parent=cs0)
70 74
71 75 cs2 = _commit_change(repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
72 76 message='commit3', vcs_type='hg', parent=cs1)
73 77
74 78 rev1 = 'default'
75 79 rev2 = 'default'
76 80
77 81 response = self.app.get(url(controller='compare', action='index',
78 82 repo_name=repo1.repo_name,
79 83 org_ref_type="branch",
80 84 org_ref=rev2,
81 85 other_repo=repo2.repo_name,
82 86 other_ref_type="branch",
83 87 other_ref=rev1,
84 88 merge='1',
85 89 ))
86 90
87 91 response.mustcontain('%s@%s -&gt; %s@%s' % (repo1.repo_name, rev2, repo2.repo_name, rev1))
88 92 response.mustcontain("""Showing 2 commits""")
89 93 response.mustcontain("""1 file changed with 2 insertions and 0 deletions""")
90 94
91 95 response.mustcontain("""<div class="message tooltip" title="commit2" style="white-space:normal">commit2</div>""")
92 96 response.mustcontain("""<div class="message tooltip" title="commit3" style="white-space:normal">commit3</div>""")
93 97
94 98 response.mustcontain("""<a href="/%s/changeset/%s">r1:%s</a>""" % (repo2.repo_name, cs1.raw_id, cs1.short_id))
95 99 response.mustcontain("""<a href="/%s/changeset/%s">r2:%s</a>""" % (repo2.repo_name, cs2.raw_id, cs2.short_id))
96 100 ## files
97 101 response.mustcontain("""<a href="/%s/compare/branch@%s...branch@%s?other_repo=%s&amp;merge=1#C--826e8142e6ba">file1</a>""" % (repo1.repo_name, rev2, rev1, repo2.repo_name))
98 102 #swap
99 103 response.mustcontain("""<a href="/%s/compare/branch@%s...branch@%s?other_repo=%s&amp;merge=True">[swap]</a>""" % (repo2.repo_name, rev1, rev2, repo1.repo_name))
100 104
101 105 def test_compare_forks_on_branch_extra_commits_origin_has_incomming_hg(self):
102 106 self.log_user()
103 107
104 108 repo1 = fixture.create_repo('one', repo_type='hg',
105 109 repo_description='diff-test',
106 110 cur_user=TEST_USER_ADMIN_LOGIN)
107 111
108 112 self.r1_id = repo1.repo_id
109 113
110 114 #commit something !
111 115 cs0 = _commit_change(repo1.repo_name, filename='file1', content='line1\n',
112 116 message='commit1', vcs_type='hg', parent=None, newfile=True)
113 117
114 118 #fork this repo
115 119 repo2 = fixture.create_fork('one', 'one-fork')
116 120 self.r2_id = repo2.repo_id
117 121
118 122 #now commit something to origin repo
119 123 cs1_prim = _commit_change(repo1.repo_name, filename='file2', content='line1file2\n',
120 124 message='commit2', vcs_type='hg', parent=cs0, newfile=True)
121 125
122 126 #add two extra commit into fork
123 127 cs1 = _commit_change(repo2.repo_name, filename='file1', content='line1\nline2\n',
124 128 message='commit2', vcs_type='hg', parent=cs0)
125 129
126 130 cs2 = _commit_change(repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
127 131 message='commit3', vcs_type='hg', parent=cs1)
128 132
129 133 rev1 = 'default'
130 134 rev2 = 'default'
131 135
132 136 response = self.app.get(url(controller='compare', action='index',
133 137 repo_name=repo1.repo_name,
134 138 org_ref_type="branch",
135 139 org_ref=rev2,
136 140 other_repo=repo2.repo_name,
137 141 other_ref_type="branch",
138 142 other_ref=rev1,
139 143 merge='x',
140 144 ))
141 145 response.mustcontain('%s@%s -&gt; %s@%s' % (repo1.repo_name, rev2, repo2.repo_name, rev1))
142 146 response.mustcontain("""Showing 2 commits""")
143 147 response.mustcontain("""1 file changed with 2 insertions and 0 deletions""")
144 148
145 149 response.mustcontain("""<div class="message tooltip" title="commit2" style="white-space:normal">commit2</div>""")
146 150 response.mustcontain("""<div class="message tooltip" title="commit3" style="white-space:normal">commit3</div>""")
147 151
148 152 response.mustcontain("""<a href="/%s/changeset/%s">r1:%s</a>""" % (repo2.repo_name, cs1.raw_id, cs1.short_id))
149 153 response.mustcontain("""<a href="/%s/changeset/%s">r2:%s</a>""" % (repo2.repo_name, cs2.raw_id, cs2.short_id))
150 154 ## files
151 155 response.mustcontain("""<a href="/%s/compare/branch@%s...branch@%s?other_repo=%s&amp;merge=x#C--826e8142e6ba">file1</a>""" % (repo1.repo_name, rev2, rev1, repo2.repo_name))
152 156 #swap
153 157 response.mustcontain("""<a href="/%s/compare/branch@%s...branch@%s?other_repo=%s&amp;merge=True">[swap]</a>""" % (repo2.repo_name, rev1, rev2, repo1.repo_name))
154 158
155 159 def test_compare_cherry_pick_changesets_from_bottom(self):
156 160
157 161 # repo1:
158 162 # cs0:
159 163 # cs1:
160 164 # repo1-fork- in which we will cherry pick bottom changesets
161 165 # cs0:
162 166 # cs1:
163 167 # cs2: x
164 168 # cs3: x
165 169 # cs4: x
166 170 # cs5:
167 171 #make repo1, and cs1+cs2
168 172 self.log_user()
169 173
170 174 repo1 = fixture.create_repo('repo1', repo_type='hg',
171 175 repo_description='diff-test',
172 176 cur_user=TEST_USER_ADMIN_LOGIN)
173 177 self.r1_id = repo1.repo_id
174 178
175 179 #commit something !
176 180 cs0 = _commit_change(repo1.repo_name, filename='file1', content='line1\n',
177 181 message='commit1', vcs_type='hg', parent=None,
178 182 newfile=True)
179 183 cs1 = _commit_change(repo1.repo_name, filename='file1', content='line1\nline2\n',
180 184 message='commit2', vcs_type='hg', parent=cs0)
181 185 #fork this repo
182 186 repo2 = fixture.create_fork('repo1', 'repo1-fork')
183 187 self.r2_id = repo2.repo_id
184 188 #now make cs3-6
185 189 cs2 = _commit_change(repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
186 190 message='commit3', vcs_type='hg', parent=cs1)
187 191 cs3 = _commit_change(repo1.repo_name, filename='file1', content='line1\nline2\nline3\nline4\n',
188 192 message='commit4', vcs_type='hg', parent=cs2)
189 193 cs4 = _commit_change(repo1.repo_name, filename='file1', content='line1\nline2\nline3\nline4\nline5\n',
190 194 message='commit5', vcs_type='hg', parent=cs3)
191 195 cs5 = _commit_change(repo1.repo_name, filename='file1', content='line1\nline2\nline3\nline4\nline5\nline6\n',
192 196 message='commit6', vcs_type='hg', parent=cs4)
193 197
194 198 response = self.app.get(url(controller='compare', action='index',
195 199 repo_name=repo2.repo_name,
196 200 org_ref_type="rev",
197 201 org_ref=cs1.short_id, # parent of cs2, in repo2
198 202 other_repo=repo1.repo_name,
199 203 other_ref_type="rev",
200 204 other_ref=cs4.short_id,
201 205 merge='True',
202 206 ))
203 207 response.mustcontain('%s@%s -&gt; %s@%s' % (repo2.repo_name, cs1.short_id, repo1.repo_name, cs4.short_id))
204 208 response.mustcontain("""Showing 3 commits""")
205 209 response.mustcontain("""1 file changed with 3 insertions and 0 deletions""")
206 210
207 211 response.mustcontain("""<div class="message tooltip" title="commit3" style="white-space:normal">commit3</div>""")
208 212 response.mustcontain("""<div class="message tooltip" title="commit4" style="white-space:normal">commit4</div>""")
209 213 response.mustcontain("""<div class="message tooltip" title="commit5" style="white-space:normal">commit5</div>""")
210 214
211 215 response.mustcontain("""<a href="/%s/changeset/%s">r2:%s</a>""" % (repo1.repo_name, cs2.raw_id, cs2.short_id))
212 216 response.mustcontain("""<a href="/%s/changeset/%s">r3:%s</a>""" % (repo1.repo_name, cs3.raw_id, cs3.short_id))
213 217 response.mustcontain("""<a href="/%s/changeset/%s">r4:%s</a>""" % (repo1.repo_name, cs4.raw_id, cs4.short_id))
214 218 ## files
215 219 response.mustcontain("""#C--826e8142e6ba">file1</a>""")
216 220
217 221 def test_compare_cherry_pick_changesets_from_top(self):
218 222 # repo1:
219 223 # cs0:
220 224 # cs1:
221 225 # repo1-fork- in which we will cherry pick bottom changesets
222 226 # cs0:
223 227 # cs1:
224 228 # cs2:
225 229 # cs3: x
226 230 # cs4: x
227 231 # cs5: x
228 232 #
229 233 #make repo1, and cs1+cs2
230 234 self.log_user()
231 235 repo1 = fixture.create_repo('repo1', repo_type='hg',
232 236 repo_description='diff-test',
233 237 cur_user=TEST_USER_ADMIN_LOGIN)
234 238 self.r1_id = repo1.repo_id
235 239
236 240 #commit something !
237 241 cs0 = _commit_change(repo1.repo_name, filename='file1', content='line1\n',
238 242 message='commit1', vcs_type='hg', parent=None,
239 243 newfile=True)
240 244 cs1 = _commit_change(repo1.repo_name, filename='file1', content='line1\nline2\n',
241 245 message='commit2', vcs_type='hg', parent=cs0)
242 246 #fork this repo
243 247 repo2 = fixture.create_fork('repo1', 'repo1-fork')
244 248 self.r2_id = repo2.repo_id
245 249 #now make cs3-6
246 250 cs2 = _commit_change(repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
247 251 message='commit3', vcs_type='hg', parent=cs1)
248 252 cs3 = _commit_change(repo1.repo_name, filename='file1', content='line1\nline2\nline3\nline4\n',
249 253 message='commit4', vcs_type='hg', parent=cs2)
250 254 cs4 = _commit_change(repo1.repo_name, filename='file1', content='line1\nline2\nline3\nline4\nline5\n',
251 255 message='commit5', vcs_type='hg', parent=cs3)
252 256 cs5 = _commit_change(repo1.repo_name, filename='file1', content='line1\nline2\nline3\nline4\nline5\nline6\n',
253 257 message='commit6', vcs_type='hg', parent=cs4)
254 258 response = self.app.get(url(controller='compare', action='index',
255 259 repo_name=repo1.repo_name,
256 260 org_ref_type="rev",
257 261 org_ref=cs2.short_id, # parent of cs3, not in repo2
258 262 other_ref_type="rev",
259 263 other_ref=cs5.short_id,
260 264 merge='1',
261 265 ))
262 266
263 267 response.mustcontain('%s@%s -&gt; %s@%s' % (repo1.repo_name, cs2.short_id, repo1.repo_name, cs5.short_id))
264 268 response.mustcontain("""Showing 3 commits""")
265 269 response.mustcontain("""1 file changed with 3 insertions and 0 deletions""")
266 270
267 271 response.mustcontain("""<div class="message tooltip" title="commit4" style="white-space:normal">commit4</div>""")
268 272 response.mustcontain("""<div class="message tooltip" title="commit5" style="white-space:normal">commit5</div>""")
269 273 response.mustcontain("""<div class="message tooltip" title="commit6" style="white-space:normal">commit6</div>""")
270 274
271 275 response.mustcontain("""<a href="/%s/changeset/%s">r3:%s</a>""" % (repo1.repo_name, cs3.raw_id, cs3.short_id))
272 276 response.mustcontain("""<a href="/%s/changeset/%s">r4:%s</a>""" % (repo1.repo_name, cs4.raw_id, cs4.short_id))
273 277 response.mustcontain("""<a href="/%s/changeset/%s">r5:%s</a>""" % (repo1.repo_name, cs5.raw_id, cs5.short_id))
274 278 ## files
275 279 response.mustcontain("""#C--826e8142e6ba">file1</a>""")
276 280
277 281 def test_compare_cherry_pick_changeset_mixed_branches(self):
278 282 pass
279 283 #TODO write this tastecase
280 284
281 285 def test_compare_remote_branches_hg(self):
282 286 self.log_user()
283 287
284 288 repo2 = fixture.create_fork(HG_REPO, HG_FORK)
285 289 self.r2_id = repo2.repo_id
286 290 rev1 = '56349e29c2af'
287 291 rev2 = '7d4bc8ec6be5'
288 292
289 293 response = self.app.get(url(controller='compare', action='index',
290 294 repo_name=HG_REPO,
291 295 org_ref_type="rev",
292 296 org_ref=rev1,
293 297 other_ref_type="rev",
294 298 other_ref=rev2,
295 299 other_repo=HG_FORK,
296 300 merge='1',
297 301 ))
298 302 response.mustcontain('%s@%s -&gt; %s@%s' % (HG_REPO, rev1, HG_FORK, rev2))
299 303 ## outgoing changesets between those revisions
300 304
301 305 response.mustcontain("""<a href="/%s/changeset/2dda4e345facb0ccff1a191052dd1606dba6781d">r4:2dda4e345fac</a>""" % (HG_FORK))
302 306 response.mustcontain("""<a href="/%s/changeset/6fff84722075f1607a30f436523403845f84cd9e">r5:6fff84722075</a>""" % (HG_FORK))
303 307 response.mustcontain("""<a href="/%s/changeset/7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7">r6:%s</a>""" % (HG_FORK, rev2))
304 308
305 309 ## files
306 310 response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s?other_repo=%s&amp;merge=1#C--9c390eb52cd6">vcs/backends/hg.py</a>""" % (HG_REPO, rev1, rev2, HG_FORK))
307 311 response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s?other_repo=%s&amp;merge=1#C--41b41c1f2796">vcs/backends/__init__.py</a>""" % (HG_REPO, rev1, rev2, HG_FORK))
308 312 response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s?other_repo=%s&amp;merge=1#C--2f574d260608">vcs/backends/base.py</a>""" % (HG_REPO, rev1, rev2, HG_FORK))
309 313
310 314 def test_org_repo_new_commits_after_forking_simple_diff(self):
311 315 self.log_user()
312 316
313 317 repo1 = fixture.create_repo('one', repo_type='hg',
314 318 repo_description='diff-test',
315 319 cur_user=TEST_USER_ADMIN_LOGIN)
316 320
317 321 self.r1_id = repo1.repo_id
318 322 r1_name = repo1.repo_name
319 323
320 #commit something initially !
321 cs0 = ScmModel().create_node(
322 repo=repo1.scm_instance, repo_name=r1_name,
323 cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
324 author=TEST_USER_ADMIN_LOGIN,
325 message='commit1',
326 content='line1',
327 f_path='file1'
328 )
324 cs0 = _commit_change(repo=r1_name, filename='file1',
325 content='line1', message='commit1', vcs_type='hg',
326 newfile=True)
329 327 Session().commit()
330 328 self.assertEqual(repo1.scm_instance.revisions, [cs0.raw_id])
331 329 #fork the repo1
332 330 repo2 = fixture.create_repo('one-fork', repo_type='hg',
333 331 repo_description='diff-test',
334 332 cur_user=TEST_USER_ADMIN_LOGIN,
335 333 clone_uri=repo1.repo_full_path,
336 334 fork_of='one')
337 335 Session().commit()
338 336 self.assertEqual(repo2.scm_instance.revisions, [cs0.raw_id])
339 337 self.r2_id = repo2.repo_id
340 338 r2_name = repo2.repo_name
341 339
342 #make 3 new commits in fork
343 cs1 = ScmModel().create_node(
344 repo=repo2.scm_instance, repo_name=r2_name,
345 cs=repo2.scm_instance[-1], user=TEST_USER_ADMIN_LOGIN,
346 author=TEST_USER_ADMIN_LOGIN,
347 message='commit1-fork',
348 content='file1-line1-from-fork',
349 f_path='file1-fork'
350 )
351 cs2 = ScmModel().create_node(
352 repo=repo2.scm_instance, repo_name=r2_name,
353 cs=cs1, user=TEST_USER_ADMIN_LOGIN,
354 author=TEST_USER_ADMIN_LOGIN,
355 message='commit2-fork',
356 content='file2-line1-from-fork',
357 f_path='file2-fork'
358 )
359 cs3 = ScmModel().create_node(
360 repo=repo2.scm_instance, repo_name=r2_name,
361 cs=cs2, user=TEST_USER_ADMIN_LOGIN,
362 author=TEST_USER_ADMIN_LOGIN,
363 message='commit3-fork',
364 content='file3-line1-from-fork',
365 f_path='file3-fork'
366 )
340
341 cs1 = _commit_change(repo=r2_name, filename='file1-fork',
342 content='file1-line1-from-fork', message='commit1-fork',
343 vcs_type='hg', parent=repo2.scm_instance[-1],
344 newfile=True)
367 345
346 cs2 = _commit_change(repo=r2_name, filename='file2-fork',
347 content='file2-line1-from-fork', message='commit2-fork',
348 vcs_type='hg', parent=cs1,
349 newfile=True)
350
351 cs3 = _commit_change(repo=r2_name, filename='file3-fork',
352 content='file3-line1-from-fork', message='commit3-fork',
353 vcs_type='hg', parent=cs2, newfile=True)
368 354 #compare !
369 355 rev1 = 'default'
370 356 rev2 = 'default'
371 357
372 358 response = self.app.get(url(controller='compare', action='index',
373 359 repo_name=r2_name,
374 360 org_ref_type="branch",
375 361 org_ref=rev1,
376 362 other_ref_type="branch",
377 363 other_ref=rev2,
378 364 other_repo=r1_name,
379 365 merge='1',
380 366 ))
381 367 response.mustcontain('%s@%s -&gt; %s@%s' % (r2_name, rev1, r1_name, rev2))
382 368 response.mustcontain('No files')
383 369 response.mustcontain('No changesets')
384 370
385 371 #add new commit into parent !
386 cs0 = ScmModel().create_node(
387 repo=repo1.scm_instance, repo_name=r1_name,
388 cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
389 author=TEST_USER_ADMIN_LOGIN,
390 message='commit2-parent',
391 content='line1-added-after-fork',
392 f_path='file2'
393 )
372 # cs0 = ScmModel().create_node(
373 # repo=repo1.scm_instance, repo_name=r1_name,
374 # cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
375 # author=TEST_USER_ADMIN_LOGIN,
376 # message='commit2-parent',
377 # content='line1-added-after-fork',
378 # f_path='file2'
379 # )
380 cs0 = _commit_change(repo=r1_name, filename='file2',
381 content='line1-added-after-fork', message='commit2-parent',
382 vcs_type='hg', parent=None, newfile=True)
383
394 384 #compare !
395 385 rev1 = 'default'
396 386 rev2 = 'default'
397 387 response = self.app.get(url(controller='compare', action='index',
398 388 repo_name=r2_name,
399 389 org_ref_type="branch",
400 390 org_ref=rev1,
401 391 other_ref_type="branch",
402 392 other_ref=rev2,
403 393 other_repo=r1_name,
404 394 merge='1',
405 395 ))
406 396
407 397 response.mustcontain('%s@%s -&gt; %s@%s' % (r2_name, rev1, r1_name, rev2))
408 398
409 399 response.mustcontain("""commit2-parent""")
410 400 response.mustcontain("""1 file changed with 1 insertions and 0 deletions""")
411 401 response.mustcontain("""line1-added-after-fork""")
@@ -1,173 +1,174 b''
1 1 import os
2 2 import sys
3 3 import platform
4 4
5 5 if sys.version_info < (2, 5):
6 6 raise Exception('RhodeCode requires python 2.5 or later')
7 7
8 8
9 9 here = os.path.abspath(os.path.dirname(__file__))
10 10
11 11
12 12 def _get_meta_var(name, data, callback_handler=None):
13 13 import re
14 14 matches = re.compile(r'(?:%s)\s*=\s*(.*)' % name).search(data)
15 15 if matches:
16 16 if not callable(callback_handler):
17 17 callback_handler = lambda v: v
18 18
19 19 return callback_handler(eval(matches.groups()[0]))
20 20
21 21 _meta = open(os.path.join(here, 'rhodecode', '__init__.py'), 'rb')
22 22 _metadata = _meta.read()
23 23 _meta.close()
24 24
25 25 callback = lambda V: ('.'.join(map(str, V[:3])) + '.'.join(V[3:]))
26 26 __version__ = _get_meta_var('VERSION', _metadata, callback)
27 27 __license__ = _get_meta_var('__license__', _metadata)
28 28 __author__ = _get_meta_var('__author__', _metadata)
29 29 __url__ = _get_meta_var('__url__', _metadata)
30 30 # defines current platform
31 31 __platform__ = platform.system()
32 32
33 33 is_windows = __platform__ in ('Windows')
34 34
35 35 requirements = [
36 36 "waitress==0.8.2",
37 37 "webob==1.0.8",
38 38 "webtest==1.4.3",
39 39 "Pylons==1.0.0",
40 40 "Beaker==1.6.4",
41 41 "WebHelpers==1.3",
42 42 "formencode==1.2.4",
43 43 "SQLAlchemy==0.7.10",
44 44 "Mako==0.7.3",
45 45 "pygments>=1.5",
46 46 "whoosh>=2.4.0,<2.5",
47 47 "celery>=2.2.5,<2.3",
48 48 "babel",
49 49 "python-dateutil>=1.5.0,<2.0.0",
50 50 "dulwich>=0.8.7,<0.9.0",
51 51 "markdown==2.2.1",
52 52 "docutils==0.8.1",
53 53 "simplejson==2.5.2",
54 54 "mock",
55 55 ]
56 56
57 57 if sys.version_info < (2, 6):
58 58 requirements.append("pysqlite")
59 59
60 60 if sys.version_info < (2, 7):
61 61 requirements.append("unittest2")
62 62 requirements.append("argparse")
63 63
64 64 if is_windows:
65 65 requirements.append("mercurial==2.6.0")
66 66 else:
67 67 requirements.append("py-bcrypt")
68 68 requirements.append("mercurial==2.6.0")
69 69
70 70
71 71 dependency_links = [
72 72 ]
73 73
74 74 classifiers = [
75 75 'Development Status :: 4 - Beta',
76 76 'Environment :: Web Environment',
77 77 'Framework :: Pylons',
78 78 'Intended Audience :: Developers',
79 79 'License :: OSI Approved :: GNU General Public License (GPL)',
80 80 'Operating System :: OS Independent',
81 81 'Programming Language :: Python',
82 82 'Programming Language :: Python :: 2.5',
83 83 'Programming Language :: Python :: 2.6',
84 84 'Programming Language :: Python :: 2.7',
85 85 ]
86 86
87 87
88 88 # additional files from project that goes somewhere in the filesystem
89 89 # relative to sys.prefix
90 90 data_files = []
91 91
92 92 # additional files that goes into package itself
93 93 package_data = {'rhodecode': ['i18n/*/LC_MESSAGES/*.mo', ], }
94 94
95 95 description = ('RhodeCode is a fast and powerful management tool '
96 96 'for Mercurial and GIT with a built in push/pull server, '
97 97 'full text search and code-review.')
98 98 keywords = ' '.join(['rhodecode', 'rhodiumcode', 'mercurial', 'git',
99 99 'code review', 'repo groups', 'ldap'
100 100 'repository management', 'hgweb replacement'
101 101 'hgwebdir', 'gitweb replacement', 'serving hgweb', ])
102 102 # long description
103 103 try:
104 104 readme_file = 'README.rst'
105 105 changelog_file = 'docs/changelog.rst'
106 106 long_description = open(readme_file).read() + '\n\n' + \
107 107 open(changelog_file).read()
108 108
109 109 except IOError, err:
110 110 sys.stderr.write("[WARNING] Cannot find file specified as "
111 111 "long_description (%s)\n or changelog (%s) skipping that file" \
112 112 % (readme_file, changelog_file))
113 113 long_description = description
114 114
115 115
116 116 try:
117 117 from setuptools import setup, find_packages
118 118 except ImportError:
119 119 from ez_setup import use_setuptools
120 120 use_setuptools()
121 121 from setuptools import setup, find_packages
122 122 # packages
123 123 packages = find_packages(exclude=['ez_setup'])
124 124
125 125 setup(
126 126 name='RhodeCode',
127 127 version=__version__,
128 128 description=description,
129 129 long_description=long_description,
130 130 keywords=keywords,
131 131 license=__license__,
132 132 author=__author__,
133 133 author_email='marcin@python-works.com',
134 134 dependency_links=dependency_links,
135 135 url=__url__,
136 136 install_requires=requirements,
137 137 classifiers=classifiers,
138 138 setup_requires=["PasteScript>=1.6.3"],
139 139 data_files=data_files,
140 140 packages=packages,
141 141 include_package_data=True,
142 142 test_suite='nose.collector',
143 143 package_data=package_data,
144 144 message_extractors={'rhodecode': [
145 145 ('**.py', 'python', None),
146 146 ('templates/**.mako', 'mako', {'input_encoding': 'utf-8'}),
147 147 ('templates/**.html', 'mako', {'input_encoding': 'utf-8'}),
148 148 ('public/**', 'ignore', None)]},
149 149 zip_safe=False,
150 150 paster_plugins=['PasteScript', 'Pylons'],
151 151 entry_points="""
152 152 [console_scripts]
153 153 rhodecode-api = rhodecode.bin.rhodecode_api:main
154 rhodecode-gist = rhodecode.bin.rhodecode_gist:main
154 155
155 156 [paste.app_factory]
156 157 main = rhodecode.config.middleware:make_app
157 158
158 159 [paste.app_install]
159 160 main = pylons.util:PylonsInstaller
160 161
161 162 [paste.global_paster_command]
162 163 setup-rhodecode=rhodecode.lib.paster_commands.setup_rhodecode:Command
163 164 cleanup-repos=rhodecode.lib.paster_commands.cleanup:Command
164 165 update-repoinfo=rhodecode.lib.paster_commands.update_repoinfo:Command
165 166 make-rcext=rhodecode.lib.paster_commands.make_rcextensions:Command
166 167 repo-scan=rhodecode.lib.paster_commands.repo_scan:Command
167 168 cache-keys=rhodecode.lib.paster_commands.cache_keys:Command
168 169 ishell=rhodecode.lib.paster_commands.ishell:Command
169 170 make-index=rhodecode.lib.indexers:MakeIndex
170 171 upgrade-db=rhodecode.lib.dbmigrate:UpgradeDb
171 172 celeryd=rhodecode.lib.celerypylons.commands:CeleryDaemonCommand
172 173 """,
173 174 )
General Comments 0
You need to be logged in to leave comments. Login now