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')} · ${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')} · ${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} · ${c.rhodecode_name} | |
|
6 | </%def> | |
|
7 | ||
|
8 | <%def name="breadcrumbs_links()"> | |
|
9 | ${_('Gist')} · 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 |
# |
|
|
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 |
|
|
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,11 +532,33 b' class ScmModel(BaseModel):' | |||
|
531 | 532 | revisions=[tip.raw_id]) |
|
532 | 533 | return tip |
|
533 | 534 | |
|
534 |
def create_node(self, repo, |
|
|
535 |
|
|
|
535 | def create_nodes(self, user, repo, message, nodes, parent_cs=None, | |
|
536 | author=None, trigger_push_hook=True): | |
|
537 | """ | |
|
538 | Commits given multiple nodes into repo | |
|
539 | ||
|
540 | :param user: RhodeCode User object or user_id, the commiter | |
|
541 | :param repo: RhodeCode Repository object | |
|
542 | :param message: commit message | |
|
543 | :param nodes: mapping {filename:{'content':content},...} | |
|
544 | :param parent_cs: parent changeset, can be empty than it's initial commit | |
|
545 | :param author: author of commit, cna be different that commiter only for git | |
|
546 | :param trigger_push_hook: trigger push hooks | |
|
547 | ||
|
548 | :returns: new commited changeset | |
|
549 | """ | |
|
550 | ||
|
536 | 551 | user = self._get_user(user) |
|
537 | IMC = self._get_IMC_module(repo.alias) | |
|
552 | scm_instance = repo.scm_instance_no_cache() | |
|
538 | 553 | |
|
554 | processed_nodes = [] | |
|
555 | for f_path in nodes: | |
|
556 | if f_path.startswith('/') or f_path.startswith('.') or '../' in f_path: | |
|
557 | raise NonRelativePathError('%s is not an relative path' % f_path) | |
|
558 | if f_path: | |
|
559 | f_path = os.path.normpath(f_path) | |
|
560 | f_path = safe_str(f_path) | |
|
561 | content = nodes[f_path]['content'] | |
|
539 | 562 | # decoding here will force that we have proper encoded values |
|
540 | 563 | # in any other case this will throw exceptions and deny commit |
|
541 | 564 | if isinstance(content, (basestring,)): |
@@ -546,28 +569,38 b' class ScmModel(BaseModel):' | |||
|
546 | 569 | raise Exception('Content is of unrecognized type %s' % ( |
|
547 | 570 | type(content) |
|
548 | 571 | )) |
|
572 | processed_nodes.append((f_path, content)) | |
|
549 | 573 | |
|
550 | 574 | message = safe_unicode(message) |
|
551 | author = safe_unicode(author) | |
|
552 | path = safe_str(f_path) | |
|
553 | m = IMC(repo) | |
|
575 | commiter = user.full_contact | |
|
576 | author = safe_unicode(author) if author else commiter | |
|
554 | 577 | |
|
555 | if isinstance(cs, EmptyChangeset): | |
|
578 | IMC = self._get_IMC_module(scm_instance.alias) | |
|
579 | imc = IMC(scm_instance) | |
|
580 | ||
|
581 | if not parent_cs: | |
|
582 | parent_cs = EmptyChangeset(alias=scm_instance.alias) | |
|
583 | ||
|
584 | if isinstance(parent_cs, EmptyChangeset): | |
|
556 | 585 | # EmptyChangeset means we we're editing empty repository |
|
557 | 586 | parents = None |
|
558 | 587 | else: |
|
559 | parents = [cs] | |
|
588 | parents = [parent_cs] | |
|
589 | # add multiple nodes | |
|
590 | for path, content in processed_nodes: | |
|
591 | imc.add(FileNode(path, content=content)) | |
|
560 | 592 | |
|
561 | m.add(FileNode(path, content=content)) | |
|
562 | tip = m.commit(message=message, | |
|
593 | tip = imc.commit(message=message, | |
|
563 | 594 | author=author, |
|
564 |
parents=parents, |
|
|
595 | parents=parents, | |
|
596 | branch=parent_cs.branch) | |
|
565 | 597 | |
|
566 | self.mark_for_invalidation(repo_name) | |
|
567 | self._handle_push(repo, | |
|
598 | self.mark_for_invalidation(repo.repo_name) | |
|
599 | if trigger_push_hook: | |
|
600 | self._handle_push(scm_instance, | |
|
568 | 601 | username=user.username, |
|
569 | 602 | action='push_local', |
|
570 | repo_name=repo_name, | |
|
603 | repo_name=repo.repo_name, | |
|
571 | 604 | revisions=[tip.raw_id]) |
|
572 | 605 | return tip |
|
573 | 606 | |
@@ -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 |
|
|
17 | padding: 10px 0 5px 0; | |
|
18 | 18 | } |
|
19 | 19 | |
|
20 | 20 | div.codeblock .code-header .stats { |
|
21 | 21 | clear: both; |
|
22 |
padding: |
|
|
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: |
|
|
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: |
|
|
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: |
|
|
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 -1 |
|
|
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 -1 |
|
|
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' |
General Comments 0
You need to be logged in to leave comments.
Login now