##// 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 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,
@@ -80,7 +79,9 b' class RcConf(object):'
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)
@@ -106,7 +107,6 b' class RcConf(object):'
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):
@@ -66,7 +66,6 b' def main(argv=None):'
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
@@ -391,6 +391,9 b' def make_map(config):'
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 #==========================================================================
@@ -42,9 +42,10 b' from rhodecode.model.repo import RepoMod'
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
@@ -888,6 +889,7 b' class ApiController(JSONRPCController):'
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
@@ -1064,3 +1066,44 b' class ApiController(JSONRPCController):'
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
@@ -57,6 +57,7 b' from rhodecode.model.db import Repositor'
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__)
@@ -371,25 +372,32 b' class FilesController(BaseRepoController'
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:
@@ -165,7 +165,6 b' class PullrequestsController(BaseRepoCon'
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,
@@ -558,7 +558,6 b' class DbManage(object):'
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:
@@ -39,6 +39,13 b' def upgrade(migrate_engine):'
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
@@ -48,7 +55,7 b' def upgrade(migrate_engine):'
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__
@@ -236,7 +236,6 b' class DiffProcessor(object):'
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)
@@ -331,7 +330,6 b' class DiffProcessor(object):'
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':
@@ -66,6 +66,10 b' 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
@@ -306,11 +306,8 b' def handle_git_receive(repo_path, revs, '
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
@@ -59,7 +59,6 b' class GitRepository(object):'
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
@@ -27,6 +27,7 b' 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
@@ -607,3 +608,39 b' def _extract_extras(env=None):'
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]
@@ -104,8 +104,7 b' class BaseModel(object):'
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,
@@ -115,8 +114,7 b' class BaseModel(object):'
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,
@@ -126,8 +124,7 b' class BaseModel(object):'
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,
@@ -1130,7 +1130,6 b' class Repository(Base, BaseModel):'
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()\
@@ -2122,6 +2121,44 b' class UserNotification(Base, BaseModel):'
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__ = (
@@ -419,3 +419,16 b' def PullRequestForm(repo_id):'
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
@@ -115,7 +115,6 b' class RepoModel(BaseModel):'
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)
@@ -652,7 +651,13 b' class RepoModel(BaseModel):'
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
@@ -662,6 +667,7 b' class RepoModel(BaseModel):'
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
@@ -670,10 +676,12 b' class RepoModel(BaseModel):'
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):
@@ -690,13 +698,14 b' class RepoModel(BaseModel):'
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 """
@@ -54,6 +54,7 b' 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
@@ -531,44 +532,76 b' class ScmModel(BaseModel):'
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):
@@ -610,7 +643,6 b' class ScmModel(BaseModel):'
610 643 grouped by type
611 644
612 645 :param repo:
613 :type repo:
614 646 """
615 647
616 648 hist_l = []
@@ -11,7 +11,7 b' from webhelpers.pylonslib.secure_form im'
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
@@ -25,7 +25,7 b' from rhodecode.lib.auth import HasReposG'
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
@@ -4,6 +4,9 b''
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
@@ -25,6 +28,9 b''
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"); }
@@ -14,12 +14,12 b' div.codeblock {'
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;
@@ -47,7 +47,7 b' div.codeblock .code-header .stats .butto'
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 }
@@ -55,18 +55,22 b' 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 }
@@ -97,19 +101,19 b' div.annotatediv {'
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 */
@@ -2306,6 +2306,11 b' h3.files_location {'
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;
@@ -3566,8 +3571,12 b' div.gravatar img {'
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 {
@@ -3598,12 +3607,14 b' div.gravatar img {'
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;
@@ -3645,6 +3656,7 b' div.gravatar img {'
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));
@@ -3659,6 +3671,22 b' div.gravatar img {'
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 }
@@ -286,6 +286,18 b''
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')}
@@ -26,7 +26,6 b' def _build_data(apikey, method, **kw):'
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({
@@ -16,13 +16,17 b' def _commit_change(repo, filename, conte'
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(
@@ -317,15 +321,9 b' class TestCompareController(TestControll'
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
@@ -339,32 +337,20 b' class TestCompareController(TestControll'
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'
@@ -383,14 +369,18 b' class TestCompareController(TestControll'
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'
@@ -151,6 +151,7 b' setup('
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
General Comments 0
You need to be logged in to leave comments. Login now