##// 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))
@@ -35,7 +35,6 b' def api_call(apikey, apihost, format, me'
35 Builds API data with given random ID
35 Builds API data with given random ID
36
36
37 :param random_id:
37 :param random_id:
38 :type random_id:
39 """
38 """
40 return {
39 return {
41 "id": random_id,
40 "id": random_id,
@@ -80,7 +79,9 b' class RcConf(object):'
80
79
81 def __init__(self, config_location=None, autoload=True, autocreate=False,
80 def __init__(self, config_location=None, autoload=True, autocreate=False,
82 config=None):
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 self._conf = {}
85 self._conf = {}
85 if autocreate:
86 if autocreate:
86 self.make_config(config)
87 self.make_config(config)
@@ -106,7 +107,6 b' class RcConf(object):'
106 Saves given config as a JSON dump in the _conf_name location
107 Saves given config as a JSON dump in the _conf_name location
107
108
108 :param config:
109 :param config:
109 :type config:
110 """
110 """
111 update = False
111 update = False
112 if os.path.exists(self._conf_name):
112 if os.path.exists(self._conf_name):
@@ -66,7 +66,6 b' def main(argv=None):'
66 Main execution function for cli
66 Main execution function for cli
67
67
68 :param argv:
68 :param argv:
69 :type argv:
70 """
69 """
71 if argv is None:
70 if argv is None:
72 argv = sys.argv
71 argv = sys.argv
@@ -391,6 +391,9 b' def make_map(config):'
391 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
391 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
392 action='add_repo')
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 # API V2
398 # API V2
396 #==========================================================================
399 #==========================================================================
@@ -42,9 +42,10 b' from rhodecode.model.repo import RepoMod'
42 from rhodecode.model.user import UserModel
42 from rhodecode.model.user import UserModel
43 from rhodecode.model.users_group import UserGroupModel
43 from rhodecode.model.users_group import UserGroupModel
44 from rhodecode.model.db import Repository, RhodeCodeSetting, UserIpMap,\
44 from rhodecode.model.db import Repository, RhodeCodeSetting, UserIpMap,\
45 Permission, User
45 Permission, User, Gist
46 from rhodecode.lib.compat import json
46 from rhodecode.lib.compat import json
47 from rhodecode.lib.exceptions import DefaultUserException
47 from rhodecode.lib.exceptions import DefaultUserException
48 from rhodecode.model.gist import GistModel
48
49
49 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
50
51
@@ -888,6 +889,7 b' class ApiController(JSONRPCController):'
888 fork_name)
889 fork_name)
889 )
890 )
890
891
892 # perms handled inside
891 def delete_repo(self, apiuser, repoid, forks=Optional(None)):
893 def delete_repo(self, apiuser, repoid, forks=Optional(None)):
892 """
894 """
893 Deletes a given repository
895 Deletes a given repository
@@ -1064,3 +1066,44 b' class ApiController(JSONRPCController):'
1064 users_group.users_group_name, repo.repo_name
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
@@ -57,6 +57,7 b' from rhodecode.model.db import Repositor'
57 from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
57 from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
58 _context_url, get_line_ctx, get_ignore_ws
58 _context_url, get_line_ctx, get_ignore_ws
59 from webob.exc import HTTPNotFound
59 from webob.exc import HTTPNotFound
60 from rhodecode.lib.exceptions import NonRelativePathError
60
61
61
62
62 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
@@ -371,25 +372,32 b' class FilesController(BaseRepoController'
371 h.flash(_('No filename'), category='warning')
372 h.flash(_('No filename'), category='warning')
372 return redirect(url('changeset_home', repo_name=c.repo_name,
373 return redirect(url('changeset_home', repo_name=c.repo_name,
373 revision='tip'))
374 revision='tip'))
374 if location.startswith('/') or location.startswith('.') or '../' in location:
375 #strip all crap out of file, just leave the basename
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)
381 filename = os.path.basename(filename)
376 filename = os.path.basename(filename)
382 node_path = os.path.join(location, filename)
377 node_path = os.path.join(location, filename)
383 author = self.rhodecode_user.full_contact
378 author = self.rhodecode_user.full_contact
384
379
385 try:
380 try:
386 self.scm_model.create_node(repo=c.rhodecode_repo,
381 nodes = {
387 repo_name=repo_name, cs=c.cs,
382 node_path: {
388 user=self.rhodecode_user.user_id,
383 'content': content
389 author=author, message=message,
384 }
390 content=content, f_path=node_path)
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 h.flash(_('Successfully committed to %s') % node_path,
394 h.flash(_('Successfully committed to %s') % node_path,
392 category='success')
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 except (NodeError, NodeAlreadyExistsError), e:
401 except (NodeError, NodeAlreadyExistsError), e:
394 h.flash(_(e), category='error')
402 h.flash(_(e), category='error')
395 except Exception:
403 except Exception:
@@ -165,7 +165,6 b' class PullrequestsController(BaseRepoCon'
165 Load context data needed for generating compare diff
165 Load context data needed for generating compare diff
166
166
167 :param pull_request:
167 :param pull_request:
168 :type pull_request:
169 """
168 """
170 org_repo = pull_request.org_repo
169 org_repo = pull_request.org_repo
171 (org_ref_type,
170 (org_ref_type,
@@ -558,7 +558,6 b' class DbManage(object):'
558 bad permissions, we must clean them up
558 bad permissions, we must clean them up
559
559
560 :param username:
560 :param username:
561 :type username:
562 """
561 """
563 default_user = User.get_by_username(username)
562 default_user = User.get_by_username(username)
564 if not default_user:
563 if not default_user:
@@ -39,6 +39,13 b' def upgrade(migrate_engine):'
39 tbl.create()
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 # UserGroup
49 # UserGroup
43 #==========================================================================
50 #==========================================================================
44 from rhodecode.lib.dbmigrate.schema.db_1_7_0 import UserGroup
51 from rhodecode.lib.dbmigrate.schema.db_1_7_0 import UserGroup
@@ -48,7 +55,7 b' def upgrade(migrate_engine):'
48 user_id.create(table=tbl)
55 user_id.create(table=tbl)
49
56
50 #==========================================================================
57 #==========================================================================
51 # UserGroup
58 # RepoGroup
52 #==========================================================================
59 #==========================================================================
53 from rhodecode.lib.dbmigrate.schema.db_1_7_0 import RepoGroup
60 from rhodecode.lib.dbmigrate.schema.db_1_7_0 import RepoGroup
54 tbl = RepoGroup.__table__
61 tbl = RepoGroup.__table__
@@ -236,7 +236,6 b' class DiffProcessor(object):'
236 Escaper for diff escapes special chars and checks the diff limit
236 Escaper for diff escapes special chars and checks the diff limit
237
237
238 :param string:
238 :param string:
239 :type string:
240 """
239 """
241
240
242 self.cur_diff_size += len(string)
241 self.cur_diff_size += len(string)
@@ -331,7 +330,6 b' class DiffProcessor(object):'
331 a_blob_id, b_blob_id, b_mode, a_file, b_file
330 a_blob_id, b_blob_id, b_mode, a_file, b_file
332
331
333 :param diff_chunk:
332 :param diff_chunk:
334 :type diff_chunk:
335 """
333 """
336
334
337 if self.vcs == 'git':
335 if self.vcs == 'git':
@@ -66,6 +66,10 b' class RepoGroupAssignmentError(Exception'
66 pass
66 pass
67
67
68
68
69 class NonRelativePathError(Exception):
70 pass
71
72
69 class HTTPLockedRC(HTTPClientError):
73 class HTTPLockedRC(HTTPClientError):
70 """
74 """
71 Special Exception For locked Repos in RhodeCode, the return code can
75 Special Exception For locked Repos in RhodeCode, the return code can
@@ -306,11 +306,8 b' def handle_git_receive(repo_path, revs, '
306 connect to database and run the logging code. Hacky as sh*t but works.
306 connect to database and run the logging code. Hacky as sh*t but works.
307
307
308 :param repo_path:
308 :param repo_path:
309 :type repo_path:
310 :param revs:
309 :param revs:
311 :type revs:
312 :param env:
310 :param env:
313 :type env:
314 """
311 """
315 from paste.deploy import appconfig
312 from paste.deploy import appconfig
316 from sqlalchemy import engine_from_config
313 from sqlalchemy import engine_from_config
@@ -59,7 +59,6 b' class GitRepository(object):'
59 Small fix for repo_path
59 Small fix for repo_path
60
60
61 :param path:
61 :param path:
62 :type path:
63 """
62 """
64 return path.split(self.repo_name, 1)[-1].strip('/')
63 return path.split(self.repo_name, 1)[-1].strip('/')
65
64
@@ -27,6 +27,7 b' import os'
27 import re
27 import re
28 import sys
28 import sys
29 import time
29 import time
30 import uuid
30 import datetime
31 import datetime
31 import traceback
32 import traceback
32 import webob
33 import webob
@@ -607,3 +608,39 b' def _extract_extras(env=None):'
607
608
608 def _set_extras(extras):
609 def _set_extras(extras):
609 os.environ['RC_SCM_DATA'] = json.dumps(extras)
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]
@@ -104,8 +104,7 b' class BaseModel(object):'
104 """
104 """
105 Helper method to get user by ID, or username fallback
105 Helper method to get user by ID, or username fallback
106
106
107 :param user:
107 :param user: UserID, username, or User instance
108 :type user: UserID, username, or User instance
109 """
108 """
110 from rhodecode.model.db import User
109 from rhodecode.model.db import User
111 return self._get_instance(User, user,
110 return self._get_instance(User, user,
@@ -115,8 +114,7 b' class BaseModel(object):'
115 """
114 """
116 Helper method to get repository by ID, or repository name
115 Helper method to get repository by ID, or repository name
117
116
118 :param repository:
117 :param repository: RepoID, repository name or Repository Instance
119 :type repository: RepoID, repository name or Repository Instance
120 """
118 """
121 from rhodecode.model.db import Repository
119 from rhodecode.model.db import Repository
122 return self._get_instance(Repository, repository,
120 return self._get_instance(Repository, repository,
@@ -126,8 +124,7 b' class BaseModel(object):'
126 """
124 """
127 Helper method to get permission by ID, or permission name
125 Helper method to get permission by ID, or permission name
128
126
129 :param permission:
127 :param permission: PermissionID, permission_name or Permission instance
130 :type permission: PermissionID, permission_name or Permission instance
131 """
128 """
132 from rhodecode.model.db import Permission
129 from rhodecode.model.db import Permission
133 return self._get_instance(Permission, permission,
130 return self._get_instance(Permission, permission,
@@ -1130,7 +1130,6 b' class Repository(Base, BaseModel):'
1130 Returns statuses for this repository
1130 Returns statuses for this repository
1131
1131
1132 :param revisions: list of revisions to get statuses for
1132 :param revisions: list of revisions to get statuses for
1133 :type revisions: list
1134 """
1133 """
1135
1134
1136 statuses = ChangesetStatus.query()\
1135 statuses = ChangesetStatus.query()\
@@ -2122,6 +2121,44 b' class UserNotification(Base, BaseModel):'
2122 Session().add(self)
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 class DbMigrateVersion(Base, BaseModel):
2162 class DbMigrateVersion(Base, BaseModel):
2126 __tablename__ = 'db_migrate_version'
2163 __tablename__ = 'db_migrate_version'
2127 __table_args__ = (
2164 __table_args__ = (
@@ -419,3 +419,16 b' def PullRequestForm(repo_id):'
419 merge_rev = v.UnicodeString(strip=True, required=True)
419 merge_rev = v.UnicodeString(strip=True, required=True)
420
420
421 return _PullRequestForm
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
@@ -115,7 +115,6 b' class RepoModel(BaseModel):'
115 Get's all repositories that user have at least read access
115 Get's all repositories that user have at least read access
116
116
117 :param user:
117 :param user:
118 :type user:
119 """
118 """
120 from rhodecode.lib.auth import AuthUser
119 from rhodecode.lib.auth import AuthUser
121 user = self._get_user(user)
120 user = self._get_user(user)
@@ -652,7 +651,13 b' class RepoModel(BaseModel):'
652 log.error(traceback.format_exc())
651 log.error(traceback.format_exc())
653 raise
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 makes repository on filesystem. It's group aware means it'll create
662 makes repository on filesystem. It's group aware means it'll create
658 a repository within a group, and alter the paths accordingly of
663 a repository within a group, and alter the paths accordingly of
@@ -662,6 +667,7 b' class RepoModel(BaseModel):'
662 :param alias:
667 :param alias:
663 :param parent_id:
668 :param parent_id:
664 :param clone_uri:
669 :param clone_uri:
670 :param repo_path:
665 """
671 """
666 from rhodecode.lib.utils import is_valid_repo, is_valid_repos_group
672 from rhodecode.lib.utils import is_valid_repo, is_valid_repos_group
667 from rhodecode.model.scm import ScmModel
673 from rhodecode.model.scm import ScmModel
@@ -670,10 +676,12 b' class RepoModel(BaseModel):'
670 new_parent_path = os.sep.join(parent.full_path_splitted)
676 new_parent_path = os.sep.join(parent.full_path_splitted)
671 else:
677 else:
672 new_parent_path = ''
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 # we need to make it str for mercurial
683 # we need to make it str for mercurial
675 repo_path = os.path.join(*map(lambda x: safe_str(x),
684 repo_path = os.path.join(*map(lambda x: safe_str(x), _paths))
676 [self.repos_path, new_parent_path, repo_name]))
677
685
678 # check if this path is not a repository
686 # check if this path is not a repository
679 if is_valid_repo(repo_path, self.repos_path):
687 if is_valid_repo(repo_path, self.repos_path):
@@ -690,13 +698,14 b' class RepoModel(BaseModel):'
690 )
698 )
691 backend = get_backend(alias)
699 backend = get_backend(alias)
692 if alias == 'hg':
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 elif alias == 'git':
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 # add rhodecode hook into this repo
704 # add rhodecode hook into this repo
697 ScmModel().install_git_hook(repo=r)
705 ScmModel().install_git_hook(repo=repo)
698 else:
706 else:
699 raise Exception('Undefined alias %s' % alias)
707 raise Exception('Undefined alias %s' % alias)
708 return repo
700
709
701 def __rename_repo(self, old, new):
710 def __rename_repo(self, old, new):
702 """
711 """
@@ -54,6 +54,7 b' from rhodecode.model import BaseModel'
54 from rhodecode.model.db import Repository, RhodeCodeUi, CacheInvalidation, \
54 from rhodecode.model.db import Repository, RhodeCodeUi, CacheInvalidation, \
55 UserFollowing, UserLog, User, RepoGroup, PullRequest
55 UserFollowing, UserLog, User, RepoGroup, PullRequest
56 from rhodecode.lib.hooks import log_push_action
56 from rhodecode.lib.hooks import log_push_action
57 from rhodecode.lib.exceptions import NonRelativePathError
57
58
58 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
59
60
@@ -531,11 +532,33 b' class ScmModel(BaseModel):'
531 revisions=[tip.raw_id])
532 revisions=[tip.raw_id])
532 return tip
533 return tip
533
534
534 def create_node(self, repo, repo_name, cs, user, author, message, content,
535 def create_nodes(self, user, repo, message, nodes, parent_cs=None,
535 f_path):
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 user = self._get_user(user)
551 user = self._get_user(user)
537 IMC = self._get_IMC_module(repo.alias)
552 scm_instance = repo.scm_instance_no_cache()
538
553
554 processed_nodes = []
555 for f_path in nodes:
556 if f_path.startswith('/') or f_path.startswith('.') or '../' in f_path:
557 raise NonRelativePathError('%s is not an relative path' % f_path)
558 if f_path:
559 f_path = os.path.normpath(f_path)
560 f_path = safe_str(f_path)
561 content = nodes[f_path]['content']
539 # decoding here will force that we have proper encoded values
562 # decoding here will force that we have proper encoded values
540 # in any other case this will throw exceptions and deny commit
563 # in any other case this will throw exceptions and deny commit
541 if isinstance(content, (basestring,)):
564 if isinstance(content, (basestring,)):
@@ -546,28 +569,38 b' class ScmModel(BaseModel):'
546 raise Exception('Content is of unrecognized type %s' % (
569 raise Exception('Content is of unrecognized type %s' % (
547 type(content)
570 type(content)
548 ))
571 ))
572 processed_nodes.append((f_path, content))
549
573
550 message = safe_unicode(message)
574 message = safe_unicode(message)
551 author = safe_unicode(author)
575 commiter = user.full_contact
552 path = safe_str(f_path)
576 author = safe_unicode(author) if author else commiter
553 m = IMC(repo)
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 # EmptyChangeset means we we're editing empty repository
585 # EmptyChangeset means we we're editing empty repository
557 parents = None
586 parents = None
558 else:
587 else:
559 parents = [cs]
588 parents = [parent_cs]
589 # add multiple nodes
590 for path, content in processed_nodes:
591 imc.add(FileNode(path, content=content))
560
592
561 m.add(FileNode(path, content=content))
593 tip = imc.commit(message=message,
562 tip = m.commit(message=message,
563 author=author,
594 author=author,
564 parents=parents, branch=cs.branch)
595 parents=parents,
596 branch=parent_cs.branch)
565
597
566 self.mark_for_invalidation(repo_name)
598 self.mark_for_invalidation(repo.repo_name)
567 self._handle_push(repo,
599 if trigger_push_hook:
600 self._handle_push(scm_instance,
568 username=user.username,
601 username=user.username,
569 action='push_local',
602 action='push_local',
570 repo_name=repo_name,
603 repo_name=repo.repo_name,
571 revisions=[tip.raw_id])
604 revisions=[tip.raw_id])
572 return tip
605 return tip
573
606
@@ -610,7 +643,6 b' class ScmModel(BaseModel):'
610 grouped by type
643 grouped by type
611
644
612 :param repo:
645 :param repo:
613 :type repo:
614 """
646 """
615
647
616 hist_l = []
648 hist_l = []
@@ -11,7 +11,7 b' from webhelpers.pylonslib.secure_form im'
11
11
12 from formencode.validators import (
12 from formencode.validators import (
13 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set,
13 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set,
14 NotEmpty, IPAddress, CIDR
14 NotEmpty, IPAddress, CIDR, String, FancyValidator
15 )
15 )
16 from rhodecode.lib.compat import OrderedSet
16 from rhodecode.lib.compat import OrderedSet
17 from rhodecode.lib import ipaddr
17 from rhodecode.lib import ipaddr
@@ -25,7 +25,7 b' from rhodecode.lib.auth import HasReposG'
25
25
26 # silence warnings and pylint
26 # silence warnings and pylint
27 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
27 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
28 NotEmpty, IPAddress, CIDR
28 NotEmpty, IPAddress, CIDR, String, FancyValidator
29
29
30 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
31
31
@@ -4,6 +4,9 b''
4
4
5 #quick .repo_switcher { background-image: url("../images/icons/database.png"); }
5 #quick .repo_switcher { background-image: url("../images/icons/database.png"); }
6 #quick .journal { background-image: url("../images/icons/book.png"); }
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 #quick .search { background-image: url("../images/icons/search_16.png"); }
10 #quick .search { background-image: url("../images/icons/search_16.png"); }
8 #quick .admin { background-image: url("../images/icons/cog_edit.png"); }
11 #quick .admin { background-image: url("../images/icons/cog_edit.png"); }
9
12
@@ -25,6 +28,9 b''
25 #context-bar a.admin { background-image: url("../images/icons/cog_edit.png"); }
28 #context-bar a.admin { background-image: url("../images/icons/cog_edit.png"); }
26
29
27 #context-bar a.journal { background-image: url("../images/icons/book.png"); }
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 #context-bar a.repos { background-image: url("../images/icons/database_edit.png"); }
34 #context-bar a.repos { background-image: url("../images/icons/database_edit.png"); }
29 #context-bar a.repos_groups { background-image: url("../images/icons/database_link.png"); }
35 #context-bar a.repos_groups { background-image: url("../images/icons/database_link.png"); }
30 #context-bar a.users { background-image: url("../images/icons/user_edit.png"); }
36 #context-bar a.users { background-image: url("../images/icons/user_edit.png"); }
@@ -14,12 +14,12 b' div.codeblock {'
14 div.codeblock .code-header {
14 div.codeblock .code-header {
15 border-bottom: 1px solid #CCCCCC;
15 border-bottom: 1px solid #CCCCCC;
16 background: #EEEEEE;
16 background: #EEEEEE;
17 padding: 10px 0 10px 0;
17 padding: 10px 0 5px 0;
18 }
18 }
19
19
20 div.codeblock .code-header .stats {
20 div.codeblock .code-header .stats {
21 clear: both;
21 clear: both;
22 padding: 6px 8px 6px 10px;
22 padding: 2px 8px 2px 14px;
23 border-bottom: 1px solid rgb(204, 204, 204);
23 border-bottom: 1px solid rgb(204, 204, 204);
24 height: 23px;
24 height: 23px;
25 margin-bottom: 6px;
25 margin-bottom: 6px;
@@ -47,7 +47,7 b' div.codeblock .code-header .stats .butto'
47 }
47 }
48
48
49 div.codeblock .code-header .author {
49 div.codeblock .code-header .author {
50 margin-left: 25px;
50 margin-left: 15px;
51 font-weight: bold;
51 font-weight: bold;
52 height: 25px;
52 height: 25px;
53 }
53 }
@@ -55,18 +55,22 b' div.codeblock .code-header .author .user'
55 padding-top: 3px;
55 padding-top: 3px;
56 }
56 }
57 div.codeblock .code-header .commit {
57 div.codeblock .code-header .commit {
58 margin-left: 25px;
58 margin-left: 15px;
59 font-weight: normal;
59 font-weight: normal;
60 white-space: pre;
60 white-space: pre;
61 }
61 }
62
62
63 .code-highlighttable,
63 div.codeblock .code-body table {
64 div.codeblock .code-body table {
64 width: 0 !important;
65 width: 0 !important;
65 border: 0px !important;
66 border: 0px !important;
66 }
67 }
68
69 .code-highlighttable,
67 div.codeblock .code-body table td {
70 div.codeblock .code-body table td {
68 border: 0px !important;
71 border: 0px !important;
69 }
72 }
73
70 div.code-body {
74 div.code-body {
71 background-color: #FFFFFF;
75 background-color: #FFFFFF;
72 }
76 }
@@ -97,19 +101,19 b' div.annotatediv {'
97 padding: 0px;
101 padding: 0px;
98 margin-top: 5px;
102 margin-top: 5px;
99 margin-bottom: 5px;
103 margin-bottom: 5px;
100 border-left: 2px solid #ccc;
104 border-left: 1px solid #ccc;
101 }
105 }
102 .code-highlight pre, .linenodiv pre {
106 .code-highlight pre, .linenodiv pre {
103 padding: 5px;
107 padding: 5px 2px 0px 5px;
104 margin: 0;
108 margin: 0;
105 }
109 }
106 .code-highlight pre div:target {
110 .code-highlight pre div:target {
107 background-color: #FFFFBE !important;
111 background-color: #FFFFBE !important;
108 }
112 }
109
113 .linenos { padding: 0px !important; border:0px !important;}
110 .linenos a { text-decoration: none; }
114 .linenos a { text-decoration: none; }
111
115
112 .code { display: block; }
116 .code { display: block; border:0px !important; }
113 .code-highlight .hll, .codehilite .hll { background-color: #ffffcc }
117 .code-highlight .hll, .codehilite .hll { background-color: #ffffcc }
114 .code-highlight .c, .codehilite .c { color: #408080; font-style: italic } /* Comment */
118 .code-highlight .c, .codehilite .c { color: #408080; font-style: italic } /* Comment */
115 .code-highlight .err, .codehilite .err { border: 1px solid #FF0000 } /* Error */
119 .code-highlight .err, .codehilite .err { border: 1px solid #FF0000 } /* Error */
@@ -2306,6 +2306,11 b' h3.files_location {'
2306 padding: 5px !important;
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 .file_history {
2314 .file_history {
2310 padding-top: 10px;
2315 padding-top: 10px;
2311 font-size: 16px;
2316 font-size: 16px;
@@ -3566,8 +3571,12 b' div.gravatar img {'
3566 border-radius: 4px 4px 4px 4px !important;
3571 border-radius: 4px 4px 4px 4px !important;
3567 cursor: pointer !important;
3572 cursor: pointer !important;
3568 padding: 3px 3px 3px 3px;
3573 padding: 3px 3px 3px 3px;
3569 background-position: 0 -15px;
3574 background-position: 0 -100px;
3570
3575
3576 }
3577
3578 .ui-btn.badge {
3579 cursor: default !important;
3571 }
3580 }
3572
3581
3573 .ui-btn.disabled {
3582 .ui-btn.disabled {
@@ -3598,12 +3607,14 b' div.gravatar img {'
3598 outline: none;
3607 outline: none;
3599 }
3608 }
3600 .ui-btn:hover {
3609 .ui-btn:hover {
3601 background-position: 0 -15px;
3610 background-position: 0 -100px;
3602 text-decoration: none;
3611 text-decoration: none;
3603 color: #515151;
3612 color: #515151;
3604 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25), 0 0 3px #FFFFFF !important;
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 .ui-btn.disabled:hover {
3618 .ui-btn.disabled:hover {
3608 background-position: 0;
3619 background-position: 0;
3609 color: #999;
3620 color: #999;
@@ -3645,6 +3656,7 b' div.gravatar img {'
3645 }
3656 }
3646
3657
3647 .ui-btn.green {
3658 .ui-btn.green {
3659 color: #fff;
3648 background-color: #57a957;
3660 background-color: #57a957;
3649 background-repeat: repeat-x;
3661 background-repeat: repeat-x;
3650 background-image: -khtml-gradient(linear, left top, left bottom, from(#62c462), to(#57a957));
3662 background-image: -khtml-gradient(linear, left top, left bottom, from(#62c462), to(#57a957));
@@ -3659,6 +3671,22 b' div.gravatar img {'
3659 border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
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 .ui-btn.blue.hidden {
3690 .ui-btn.blue.hidden {
3663 display: none;
3691 display: none;
3664 }
3692 }
@@ -286,6 +286,18 b''
286 </a>
286 </a>
287 </li>
287 </li>
288 %endif
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 <li ${is_current('search')}>
301 <li ${is_current('search')}>
290 <a class="menu_link search" title="${_('Search in repositories')}" href="${h.url('search')}">
302 <a class="menu_link search" title="${_('Search in repositories')}" href="${h.url('search')}">
291 ${_('Search')}
303 ${_('Search')}
@@ -26,7 +26,6 b' def _build_data(apikey, method, **kw):'
26 Builds API data with given random ID
26 Builds API data with given random ID
27
27
28 :param random_id:
28 :param random_id:
29 :type random_id:
30 """
29 """
31 random_id = random.randrange(1, 9999)
30 random_id = random.randrange(1, 9999)
32 return random_id, json.dumps({
31 return random_id, json.dumps({
@@ -16,13 +16,17 b' def _commit_change(repo, filename, conte'
16 _cs = EmptyChangeset(alias=vcs_type)
16 _cs = EmptyChangeset(alias=vcs_type)
17
17
18 if newfile:
18 if newfile:
19 cs = ScmModel().create_node(
19 nodes = {
20 repo=repo.scm_instance, repo_name=repo.repo_name,
20 filename: {
21 cs=_cs, user=TEST_USER_ADMIN_LOGIN,
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 author=TEST_USER_ADMIN_LOGIN,
29 author=TEST_USER_ADMIN_LOGIN,
23 message=message,
24 content=content,
25 f_path=filename
26 )
30 )
27 else:
31 else:
28 cs = ScmModel().commit_change(
32 cs = ScmModel().commit_change(
@@ -317,15 +321,9 b' class TestCompareController(TestControll'
317 self.r1_id = repo1.repo_id
321 self.r1_id = repo1.repo_id
318 r1_name = repo1.repo_name
322 r1_name = repo1.repo_name
319
323
320 #commit something initially !
324 cs0 = _commit_change(repo=r1_name, filename='file1',
321 cs0 = ScmModel().create_node(
325 content='line1', message='commit1', vcs_type='hg',
322 repo=repo1.scm_instance, repo_name=r1_name,
326 newfile=True)
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 )
329 Session().commit()
327 Session().commit()
330 self.assertEqual(repo1.scm_instance.revisions, [cs0.raw_id])
328 self.assertEqual(repo1.scm_instance.revisions, [cs0.raw_id])
331 #fork the repo1
329 #fork the repo1
@@ -339,32 +337,20 b' class TestCompareController(TestControll'
339 self.r2_id = repo2.repo_id
337 self.r2_id = repo2.repo_id
340 r2_name = repo2.repo_name
338 r2_name = repo2.repo_name
341
339
342 #make 3 new commits in fork
340
343 cs1 = ScmModel().create_node(
341 cs1 = _commit_change(repo=r2_name, filename='file1-fork',
344 repo=repo2.scm_instance, repo_name=r2_name,
342 content='file1-line1-from-fork', message='commit1-fork',
345 cs=repo2.scm_instance[-1], user=TEST_USER_ADMIN_LOGIN,
343 vcs_type='hg', parent=repo2.scm_instance[-1],
346 author=TEST_USER_ADMIN_LOGIN,
344 newfile=True)
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 )
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 #compare !
354 #compare !
369 rev1 = 'default'
355 rev1 = 'default'
370 rev2 = 'default'
356 rev2 = 'default'
@@ -383,14 +369,18 b' class TestCompareController(TestControll'
383 response.mustcontain('No changesets')
369 response.mustcontain('No changesets')
384
370
385 #add new commit into parent !
371 #add new commit into parent !
386 cs0 = ScmModel().create_node(
372 # cs0 = ScmModel().create_node(
387 repo=repo1.scm_instance, repo_name=r1_name,
373 # repo=repo1.scm_instance, repo_name=r1_name,
388 cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
374 # cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
389 author=TEST_USER_ADMIN_LOGIN,
375 # author=TEST_USER_ADMIN_LOGIN,
390 message='commit2-parent',
376 # message='commit2-parent',
391 content='line1-added-after-fork',
377 # content='line1-added-after-fork',
392 f_path='file2'
378 # f_path='file2'
393 )
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 #compare !
384 #compare !
395 rev1 = 'default'
385 rev1 = 'default'
396 rev2 = 'default'
386 rev2 = 'default'
@@ -151,6 +151,7 b' setup('
151 entry_points="""
151 entry_points="""
152 [console_scripts]
152 [console_scripts]
153 rhodecode-api = rhodecode.bin.rhodecode_api:main
153 rhodecode-api = rhodecode.bin.rhodecode_api:main
154 rhodecode-gist = rhodecode.bin.rhodecode_gist:main
154
155
155 [paste.app_factory]
156 [paste.app_factory]
156 main = rhodecode.config.middleware:make_app
157 main = rhodecode.config.middleware:make_app
General Comments 0
You need to be logged in to leave comments. Login now