diff --git a/rhodecode/apps/home/__init__.py b/rhodecode/apps/home/__init__.py --- a/rhodecode/apps/home/__init__.py +++ b/rhodecode/apps/home/__init__.py @@ -68,6 +68,10 @@ def includeme(config): pattern='/_markup_preview') config.add_route( + name='file_preview', + pattern='/_file_preview') + + config.add_route( name='store_user_session_value', pattern='/_store_session_attr') diff --git a/rhodecode/apps/home/views.py b/rhodecode/apps/home/views.py --- a/rhodecode/apps/home/views.py +++ b/rhodecode/apps/home/views.py @@ -27,11 +27,12 @@ from pyramid.view import view_config from rhodecode.apps._base import BaseAppView from rhodecode.lib import helpers as h from rhodecode.lib.auth import ( - LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator, - CSRFRequired) + LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator, CSRFRequired) +from rhodecode.lib.codeblocks import filenode_as_lines_tokens from rhodecode.lib.index import searcher_from_config from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int from rhodecode.lib.ext_json import json +from rhodecode.lib.vcs.nodes import FileNode from rhodecode.model.db import ( func, true, or_, case, in_filter_generator, Repository, RepoGroup, User, UserGroup) from rhodecode.model.repo import RepoModel @@ -743,6 +744,34 @@ class HomeView(BaseAppView): @LoginRequired() @CSRFRequired() @view_config( + route_name='file_preview', request_method='POST', + renderer='string', xhr=True) + def file_preview(self): + # Technically a CSRF token is not needed as no state changes with this + # call. However, as this is a POST is better to have it, so automated + # tools don't flag it as potential CSRF. + # Post is required because the payload could be bigger than the maximum + # allowed by GET. + + text = self.request.POST.get('text') + file_path = self.request.POST.get('file_path') + + renderer = h.renderer_from_filename(file_path) + + if renderer: + return h.render(text, renderer=renderer, mentions=True) + else: + self.load_default_context() + _render = self.request.get_partial_renderer( + 'rhodecode:templates/files/file_content.mako') + + lines = filenode_as_lines_tokens(FileNode(file_path, text)) + + return _render('render_lines', lines) + + @LoginRequired() + @CSRFRequired() + @view_config( route_name='store_user_session_value', request_method='POST', renderer='string', xhr=True) def store_user_session_attr(self): diff --git a/rhodecode/apps/repository/views/repo_files.py b/rhodecode/apps/repository/views/repo_files.py --- a/rhodecode/apps/repository/views/repo_files.py +++ b/rhodecode/apps/repository/views/repo_files.py @@ -25,6 +25,7 @@ import shutil import tempfile import collections import urllib +import pathlib2 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound from pyramid.view import view_config @@ -42,7 +43,7 @@ from rhodecode.lib.exceptions import Non from rhodecode.lib.codeblocks import ( filenode_as_lines_tokens, filenode_as_annotated_lines_tokens) from rhodecode.lib.utils2 import ( - convert_line_endings, detect_mode, safe_str, str2bool, safe_int, sha1) + convert_line_endings, detect_mode, safe_str, str2bool, safe_int, sha1, safe_unicode) from rhodecode.lib.auth import ( LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired) from rhodecode.lib.vcs import path as vcspath @@ -87,7 +88,7 @@ class RepoFilesView(RepoAppView): c.enable_downloads = self.db_repo.enable_downloads return c - def _ensure_not_locked(self): + def _ensure_not_locked(self, commit_id='tip'): _ = self.request.translate repo = self.db_repo @@ -98,21 +99,40 @@ class RepoFilesView(RepoAppView): 'warning') files_url = h.route_path( 'repo_files:default_path', - repo_name=self.db_repo_name, commit_id='tip') + repo_name=self.db_repo_name, commit_id=commit_id) raise HTTPFound(files_url) - def check_branch_permission(self, branch_name): + def forbid_non_head(self, is_head, f_path, commit_id='tip', json_mode=False): + _ = self.request.translate + + if not is_head: + message = _('You can only modify files with commit being a valid branch head.') + h.flash(message, category='warning') + + if json_mode: + return message + + files_url = h.route_path( + 'repo_files', repo_name=self.db_repo_name, commit_id=commit_id, + f_path=f_path) + raise HTTPFound(files_url) + + def check_branch_permission(self, branch_name, commit_id='tip', json_mode=False): _ = self.request.translate rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission( self.db_repo_name, branch_name) if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']: - h.flash( - _('Branch `{}` changes forbidden by rule {}.').format(branch_name, rule), - 'warning') + message = _('Branch `{}` changes forbidden by rule {}.').format( + branch_name, rule) + h.flash(message, 'warning') + + if json_mode: + return message + files_url = h.route_path( - 'repo_files:default_path', - repo_name=self.db_repo_name, commit_id='tip') + 'repo_files:default_path', repo_name=self.db_repo_name, commit_id=commit_id) + raise HTTPFound(files_url) def _get_commit_and_path(self): @@ -146,8 +166,7 @@ class RepoFilesView(RepoAppView): _url = h.route_path( 'repo_files_add_file', - repo_name=self.db_repo_name, commit_id=0, f_path='', - _anchor='edit') + repo_name=self.db_repo_name, commit_id=0, f_path='') if h.HasRepoPermissionAny( 'repository.write', 'repository.admin')(self.db_repo_name): @@ -217,7 +236,7 @@ class RepoFilesView(RepoAppView): break # checked branches, means we only need to try to get the branch/commit_sha - if not repo.is_empty: + if not repo.is_empty(): commit = repo.get_commit(commit_id=commit_id) if commit: branch_name = commit.branch @@ -276,6 +295,15 @@ class RepoFilesView(RepoAppView): return commit_id, ext, fileformat, content_type + def create_pure_path(self, *parts): + # Split paths and sanitize them, removing any ../ etc + sanitized_path = [ + x for x in pathlib2.PurePath(*parts).parts + if x not in ['.', '..']] + + pure_path = pathlib2.PurePath(*sanitized_path) + return pure_path + @LoginRequired() @HasRepoPermissionAnyDecorator( 'repository.read', 'repository.write', 'repository.admin') @@ -1054,15 +1082,9 @@ class RepoFilesView(RepoAppView): _branch_name, _sha_commit_id, is_head = \ self._is_valid_head(commit_id, self.rhodecode_vcs_repo) - if not is_head: - h.flash(_('You can only delete files with commit ' - 'being a valid branch head.'), category='warning') - raise HTTPFound( - h.route_path('repo_files', - repo_name=self.db_repo_name, commit_id='tip', - f_path=f_path)) + self.forbid_non_head(is_head, f_path) + self.check_branch_permission(_branch_name) - self.check_branch_permission(_branch_name) c.commit = self._get_commit_or_redirect(commit_id) c.file = self._get_filenode_or_redirect(c.commit, f_path) @@ -1088,13 +1110,7 @@ class RepoFilesView(RepoAppView): _branch_name, _sha_commit_id, is_head = \ self._is_valid_head(commit_id, self.rhodecode_vcs_repo) - if not is_head: - h.flash(_('You can only delete files with commit ' - 'being a valid branch head.'), category='warning') - raise HTTPFound( - h.route_path('repo_files', - repo_name=self.db_repo_name, commit_id='tip', - f_path=f_path)) + self.forbid_non_head(is_head, f_path) self.check_branch_permission(_branch_name) c.commit = self._get_commit_or_redirect(commit_id) @@ -1144,14 +1160,8 @@ class RepoFilesView(RepoAppView): _branch_name, _sha_commit_id, is_head = \ self._is_valid_head(commit_id, self.rhodecode_vcs_repo) - if not is_head: - h.flash(_('You can only edit files with commit ' - 'being a valid branch head.'), category='warning') - raise HTTPFound( - h.route_path('repo_files', - repo_name=self.db_repo_name, commit_id='tip', - f_path=f_path)) - self.check_branch_permission(_branch_name) + self.forbid_non_head(is_head, f_path, commit_id=commit_id) + self.check_branch_permission(_branch_name, commit_id=commit_id) c.commit = self._get_commit_or_redirect(commit_id) c.file = self._get_filenode_or_redirect(c.commit, f_path) @@ -1163,8 +1173,7 @@ class RepoFilesView(RepoAppView): commit_id=c.commit.raw_id, f_path=f_path) raise HTTPFound(files_url) - c.default_message = _( - 'Edited file {} via RhodeCode Enterprise').format(f_path) + c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path) c.f_path = f_path return self._get_template_context(c) @@ -1181,32 +1190,23 @@ class RepoFilesView(RepoAppView): commit_id, f_path = self._get_commit_and_path() self._ensure_not_locked() - _branch_name, _sha_commit_id, is_head = \ - self._is_valid_head(commit_id, self.rhodecode_vcs_repo) - - if not is_head: - h.flash(_('You can only edit files with commit ' - 'being a valid branch head.'), category='warning') - raise HTTPFound( - h.route_path('repo_files', - repo_name=self.db_repo_name, commit_id='tip', - f_path=f_path)) - - self.check_branch_permission(_branch_name) c.commit = self._get_commit_or_redirect(commit_id) c.file = self._get_filenode_or_redirect(c.commit, f_path) if c.file.is_binary: - raise HTTPFound( - h.route_path('repo_files', - repo_name=self.db_repo_name, - commit_id=c.commit.raw_id, - f_path=f_path)) + raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name, + commit_id=c.commit.raw_id, f_path=f_path)) + + _branch_name, _sha_commit_id, is_head = \ + self._is_valid_head(commit_id, self.rhodecode_vcs_repo) - c.default_message = _( - 'Edited file {} via RhodeCode Enterprise').format(f_path) + self.forbid_non_head(is_head, f_path, commit_id=commit_id) + self.check_branch_permission(_branch_name, commit_id=commit_id) + + c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path) c.f_path = f_path + old_content = c.file.content sl = old_content.splitlines(1) first_line = sl[0] if sl else '' @@ -1217,20 +1217,25 @@ class RepoFilesView(RepoAppView): content = convert_line_endings(r_post.get('content', ''), line_ending_mode) message = r_post.get('message') or c.default_message - org_f_path = c.file.unicode_path + org_node_path = c.file.unicode_path filename = r_post['filename'] - org_filename = c.file.name + + root_path = c.file.dir_path + pure_path = self.create_pure_path(root_path, filename) + node_path = safe_unicode(bytes(pure_path)) - if content == old_content and filename == org_filename: - h.flash(_('No changes'), category='warning') - raise HTTPFound( - h.route_path('repo_commit', repo_name=self.db_repo_name, - commit_id='tip')) + default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name, + commit_id=commit_id) + if content == old_content and node_path == org_node_path: + h.flash(_('No changes detected on {}').format(org_node_path), + category='warning') + raise HTTPFound(default_redirect_url) + try: mapping = { - org_f_path: { - 'org_filename': org_f_path, - 'filename': os.path.join(c.file.dir_path, filename), + org_node_path: { + 'org_filename': org_node_path, + 'filename': node_path, 'content': content, 'lexer': '', 'op': 'mod', @@ -1238,7 +1243,7 @@ class RepoFilesView(RepoAppView): } } - ScmModel().update_nodes( + commit = ScmModel().update_nodes( user=self._rhodecode_db_user.user_id, repo=self.db_repo, message=message, @@ -1246,15 +1251,16 @@ class RepoFilesView(RepoAppView): parent_commit=c.commit, ) - h.flash( - _('Successfully committed changes to file `{}`').format( + h.flash(_('Successfully committed changes to file `{}`').format( h.escape(f_path)), category='success') + default_redirect_url = h.route_path( + 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id) + except Exception: log.exception('Error occurred during commit') h.flash(_('Error occurred during commit'), category='error') - raise HTTPFound( - h.route_path('repo_commit', repo_name=self.db_repo_name, - commit_id='tip')) + + raise HTTPFound(default_redirect_url) @LoginRequired() @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') @@ -1274,10 +1280,8 @@ class RepoFilesView(RepoAppView): c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False) if c.commit is None: c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias) - c.default_message = (_('Added file via RhodeCode Enterprise')) - c.f_path = f_path.lstrip('/') # ensure not relative path - if self.rhodecode_vcs_repo.is_empty: + if self.rhodecode_vcs_repo.is_empty(): # for empty repository we cannot check for current branch, we rely on # c.commit.branch instead _branch_name = c.commit.branch @@ -1286,15 +1290,11 @@ class RepoFilesView(RepoAppView): _branch_name, _sha_commit_id, is_head = \ self._is_valid_head(commit_id, self.rhodecode_vcs_repo) - if not is_head: - h.flash(_('You can only add files with commit ' - 'being a valid branch head.'), category='warning') - raise HTTPFound( - h.route_path('repo_files', - repo_name=self.db_repo_name, commit_id='tip', - f_path=f_path)) + self.forbid_non_head(is_head, f_path, commit_id=commit_id) + self.check_branch_permission(_branch_name, commit_id=commit_id) - self.check_branch_permission(_branch_name) + c.default_message = (_('Added file via RhodeCode Enterprise')) + c.f_path = f_path.lstrip('/') # ensure not relative path return self._get_template_context(c) @@ -1311,14 +1311,19 @@ class RepoFilesView(RepoAppView): self._ensure_not_locked() - r_post = self.request.POST - - c.commit = self._get_commit_or_redirect( - commit_id, redirect_after=False) + c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False) if c.commit is None: c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias) - if self.rhodecode_vcs_repo.is_empty: + # calculate redirect URL + if self.rhodecode_vcs_repo.is_empty(): + default_redirect_url = h.route_path( + 'repo_summary', repo_name=self.db_repo_name) + else: + default_redirect_url = h.route_path( + 'repo_commit', repo_name=self.db_repo_name, commit_id='tip') + + if self.rhodecode_vcs_repo.is_empty(): # for empty repository we cannot check for current branch, we rely on # c.commit.branch instead _branch_name = c.commit.branch @@ -1327,70 +1332,42 @@ class RepoFilesView(RepoAppView): _branch_name, _sha_commit_id, is_head = \ self._is_valid_head(commit_id, self.rhodecode_vcs_repo) - if not is_head: - h.flash(_('You can only add files with commit ' - 'being a valid branch head.'), category='warning') - raise HTTPFound( - h.route_path('repo_files', - repo_name=self.db_repo_name, commit_id='tip', - f_path=f_path)) - - self.check_branch_permission(_branch_name) + self.forbid_non_head(is_head, f_path, commit_id=commit_id) + self.check_branch_permission(_branch_name, commit_id=commit_id) c.default_message = (_('Added file via RhodeCode Enterprise')) c.f_path = f_path + + r_post = self.request.POST + message = r_post.get('message') or c.default_message + filename = r_post.get('filename') unix_mode = 0 content = convert_line_endings(r_post.get('content', ''), unix_mode) - message = r_post.get('message') or c.default_message - filename = r_post.get('filename') - location = r_post.get('location', '') # dir location - file_obj = r_post.get('upload_file', None) - - if file_obj is not None and hasattr(file_obj, 'filename'): - filename = r_post.get('filename_upload') - content = file_obj.file - - if hasattr(content, 'file'): - # non posix systems store real file under file attr - content = content.file - - if self.rhodecode_vcs_repo.is_empty: - default_redirect_url = h.route_path( - 'repo_summary', repo_name=self.db_repo_name) - else: - default_redirect_url = h.route_path( - 'repo_commit', repo_name=self.db_repo_name, commit_id='tip') - - # If there's no commit, redirect to repo summary - if type(c.commit) is EmptyCommit: - redirect_url = h.route_path( - 'repo_summary', repo_name=self.db_repo_name) - else: - redirect_url = default_redirect_url - if not filename: - h.flash(_('No filename'), category='warning') + # If there's no commit, redirect to repo summary + if type(c.commit) is EmptyCommit: + redirect_url = h.route_path( + 'repo_summary', repo_name=self.db_repo_name) + else: + redirect_url = default_redirect_url + h.flash(_('No filename specified'), category='warning') raise HTTPFound(redirect_url) - # extract the location from filename, - # allows using foo/bar.txt syntax to create subdirectories - subdir_loc = filename.rsplit('/', 1) - if len(subdir_loc) == 2: - location = os.path.join(location, subdir_loc[0]) + root_path = f_path + pure_path = self.create_pure_path(root_path, filename) + node_path = safe_unicode(bytes(pure_path).lstrip('/')) - # strip all crap out of file, just leave the basename - filename = os.path.basename(filename) - node_path = os.path.join(location, filename) author = self._rhodecode_db_user.full_contact + nodes = { + node_path: { + 'content': content + } + } try: - nodes = { - node_path: { - 'content': content - } - } - ScmModel().create_nodes( + + commit = ScmModel().create_nodes( user=self._rhodecode_db_user.user_id, repo=self.db_repo, message=message, @@ -1399,14 +1376,16 @@ class RepoFilesView(RepoAppView): author=author, ) - h.flash( - _('Successfully committed new file `{}`').format( + h.flash(_('Successfully committed new file `{}`').format( h.escape(node_path)), category='success') + + default_redirect_url = h.route_path( + 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id) + except NonRelativePathError: log.exception('Non Relative path found') - h.flash(_( - 'The location specified must be a relative path and must not ' - 'contain .. in the path'), category='warning') + h.flash(_('The location specified must be a relative path and must not ' + 'contain .. in the path'), category='warning') raise HTTPFound(default_redirect_url) except (NodeError, NodeAlreadyExistsError) as e: h.flash(_(h.escape(e)), category='error') @@ -1415,3 +1394,135 @@ class RepoFilesView(RepoAppView): h.flash(_('Error occurred during commit'), category='error') raise HTTPFound(default_redirect_url) + + @LoginRequired() + @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') + @CSRFRequired() + @view_config( + route_name='repo_files_upload_file', request_method='POST', + renderer='json_ext') + def repo_files_upload_file(self): + _ = self.request.translate + c = self.load_default_context() + commit_id, f_path = self._get_commit_and_path() + + self._ensure_not_locked() + + c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False) + if c.commit is None: + c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias) + + # calculate redirect URL + if self.rhodecode_vcs_repo.is_empty(): + default_redirect_url = h.route_path( + 'repo_summary', repo_name=self.db_repo_name) + else: + default_redirect_url = h.route_path( + 'repo_commit', repo_name=self.db_repo_name, commit_id='tip') + + if self.rhodecode_vcs_repo.is_empty(): + # for empty repository we cannot check for current branch, we rely on + # c.commit.branch instead + _branch_name = c.commit.branch + is_head = True + else: + _branch_name, _sha_commit_id, is_head = \ + self._is_valid_head(commit_id, self.rhodecode_vcs_repo) + + error = self.forbid_non_head(is_head, f_path, json_mode=True) + if error: + return { + 'error': error, + 'redirect_url': default_redirect_url + } + error = self.check_branch_permission(_branch_name, json_mode=True) + if error: + return { + 'error': error, + 'redirect_url': default_redirect_url + } + + c.default_message = (_('Uploaded file via RhodeCode Enterprise')) + c.f_path = f_path + + r_post = self.request.POST + + message = c.default_message + user_message = r_post.getall('message') + if isinstance(user_message, list) and user_message: + # we take the first from duplicated results if it's not empty + message = user_message[0] if user_message[0] else message + + nodes = {} + + for file_obj in r_post.getall('files_upload') or []: + content = file_obj.file + filename = file_obj.filename + + root_path = f_path + pure_path = self.create_pure_path(root_path, filename) + node_path = safe_unicode(bytes(pure_path).lstrip('/')) + + nodes[node_path] = { + 'content': content + } + + if not nodes: + error = 'missing files' + return { + 'error': error, + 'redirect_url': default_redirect_url + } + + author = self._rhodecode_db_user.full_contact + + try: + commit = ScmModel().create_nodes( + user=self._rhodecode_db_user.user_id, + repo=self.db_repo, + message=message, + nodes=nodes, + parent_commit=c.commit, + author=author, + ) + if len(nodes) == 1: + flash_message = _('Successfully committed {} new files').format(len(nodes)) + else: + flash_message = _('Successfully committed 1 new file') + + h.flash(flash_message, category='success') + + default_redirect_url = h.route_path( + 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id) + + except NonRelativePathError: + log.exception('Non Relative path found') + error = _('The location specified must be a relative path and must not ' + 'contain .. in the path') + h.flash(error, category='warning') + + return { + 'error': error, + 'redirect_url': default_redirect_url + } + except (NodeError, NodeAlreadyExistsError) as e: + error = h.escape(e) + h.flash(error, category='error') + + return { + 'error': error, + 'redirect_url': default_redirect_url + } + except Exception: + log.exception('Error occurred during commit') + error = _('Error occurred during commit') + h.flash(error, category='error') + return { + 'error': error, + 'redirect_url': default_redirect_url + } + + return { + 'error': None, + 'redirect_url': default_redirect_url + } diff --git a/rhodecode/lib/helpers.py b/rhodecode/lib/helpers.py --- a/rhodecode/lib/helpers.py +++ b/rhodecode/lib/helpers.py @@ -1939,23 +1939,26 @@ def secure_form(form_url, method="POST", def dropdownmenu(name, selected, options, enable_filter=False, **attrs): select_html = select(name, selected, options, **attrs) + select2 = """ """ + filter_option = """, minimumResultsForSearch: -1 """ input_id = attrs.get('id') or name + extra_classes = ' '.join(attrs.pop('extra_classes', [])) filter_enabled = "" if enable_filter else filter_option - select_script = literal(select2 % (input_id, filter_enabled)) + select_script = literal(select2 % (input_id, extra_classes, filter_enabled)) return literal(select_html+select_script) diff --git a/rhodecode/model/scm.py b/rhodecode/model/scm.py --- a/rhodecode/model/scm.py +++ b/rhodecode/model/scm.py @@ -818,6 +818,8 @@ class ScmModel(BaseModel): repo_name=repo.repo_name, repo_alias=scm_instance.alias, commit_ids=[tip.raw_id]) + return tip + def delete_nodes(self, user, repo, message, nodes, parent_commit=None, author=None, trigger_push_hook=True): """ diff --git a/rhodecode/public/css/codemirror.less b/rhodecode/public/css/codemirror.less --- a/rhodecode/public/css/codemirror.less +++ b/rhodecode/public/css/codemirror.less @@ -27,7 +27,7 @@ .CodeMirror-gutters { border-right: 1px solid #ddd; - background-color: @grey6; + background-color: white; white-space: nowrap; } .CodeMirror-linenumbers {} diff --git a/rhodecode/public/css/forms.less b/rhodecode/public/css/forms.less --- a/rhodecode/public/css/forms.less +++ b/rhodecode/public/css/forms.less @@ -231,6 +231,11 @@ form.rcform { .drop-menu { float: left; + + & + .last-item { + margin: 0; + } + margin: 0 @input-padding 0 0; } diff --git a/rhodecode/public/css/main.less b/rhodecode/public/css/main.less --- a/rhodecode/public/css/main.less +++ b/rhodecode/public/css/main.less @@ -277,7 +277,9 @@ input.inline[type="file"] { // Gists #files_data { clear: both; //for firefox + padding-top: 10px; } + #gistid { margin-right: @padding; } @@ -1327,9 +1329,9 @@ table.integrations { } } - #editor_container{ - position: relative; - margin: @padding; + #editor_container { + position: relative; + margin: @padding 10px; } } @@ -2063,15 +2065,15 @@ BIN_FILENODE = 7 // Files .edit-file-title { - border-bottom: @border-thickness solid @border-default-color; - - .breadcrumbs { - margin-bottom: 0; + font-size: 16px; + + .title-heading { + padding: 2px; } } .edit-file-fieldset { - margin-top: @sidebarpadding; + margin: @sidebarpadding 0; .fieldset { .left-label { @@ -2120,6 +2122,27 @@ BIN_FILENODE = 7 margin: 0 0 0 10px; } +.file-upload-transaction-wrapper { + margin-top: 57px; + clear: both; +} + +.file-upload-transaction-wrapper .error { + color: @color5; +} + +.file-upload-transaction { + min-height: 200px; + padding: 54px; + border: 1px solid @grey5; + text-align: center; + clear: both; +} + +.file-upload-transaction i { + font-size: 48px +} + h3.files_location{ line-height: 2.4em; } @@ -2308,6 +2331,101 @@ h3.files_location{ } +.edit-file-fieldset #location, +.edit-file-fieldset #filename { + display: flex; + width: -moz-available; /* WebKit-based browsers will ignore this. */ + width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */ + width: fill-available; + border: 0; +} + +.path-items { + display: flex; + padding: 0; + border: 1px solid #eeeeee; + width: 100%; + float: left; + + .breadcrumb-path { + line-height: 30px; + padding: 0 4px; + white-space: nowrap; + } + + .location-path { + width: -moz-available; /* WebKit-based browsers will ignore this. */ + width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */ + width: fill-available; + + .file-name-input { + padding: 0.5em 0; + } + + } + + ul { + display: flex; + margin: 0; + padding: 0; + } + li { + list-style-type: none; + } +} + +.editor-items { + height: 40px; + margin: 10px 0 -17px 10px; + + .editor-action { + cursor: pointer; + } + + .editor-action.active { + border-bottom: 2px solid #5C5C5C; + } + + li { + list-style-type: none; + } +} + +.edit-file-fieldset .message textarea { + border: 1px solid #eeeeee; +} + +#files_data .codeblock { + background-color: #F5F5F5; +} + +#editor_preview { + background: white; +} + +.show-editor { + padding: 10px; + background-color: white; + +} + +.show-preview { + padding: 10px; + background-color: white; + border-left: 1px solid #eeeeee; +} + + + + + + + + + + + + // Search .search-form{ @@ -2577,27 +2695,28 @@ form.markup-form { padding: 20px; } -.dropzone { +.dropzone, +.dropzone-pure { border: 2px dashed @grey5; border-radius: 5px; background: white; min-height: 200px; padding: 54px; -} -.dropzone .dz-message { - font-weight: 700; -} - -.dropzone .dz-message { - text-align: center; - margin: 2em 0; + + .dz-message { + font-weight: 700; + text-align: center; + margin: 2em 0; + } + } .dz-preview { - margin: 10px 0px !important; + margin: 10px 0 !important; position: relative; vertical-align: top; padding: 10px; + border-bottom: 1px solid @grey5; } .dz-filename { @@ -2605,6 +2724,10 @@ form.markup-form { float:left; } +.dz-sending { + float: right; +} + .dz-response { clear:both } @@ -2615,4 +2738,6 @@ form.markup-form { .dz-error-message { color: @alert2; -} \ No newline at end of file + padding-top: 10px; + clear: both; +} diff --git a/rhodecode/public/js/rhodecode/routes.js b/rhodecode/public/js/rhodecode/routes.js --- a/rhodecode/public/js/rhodecode/routes.js +++ b/rhodecode/public/js/rhodecode/routes.js @@ -149,6 +149,7 @@ function registerRCRoutes() { pyroutes.register('repo_group_list_data', '/_repo_groups', []); pyroutes.register('goto_switcher_data', '/_goto_data', []); pyroutes.register('markup_preview', '/_markup_preview', []); + pyroutes.register('file_preview', '/_file_preview', []); pyroutes.register('store_user_session_value', '/_store_session_attr', []); pyroutes.register('journal', '/_admin/journal', []); pyroutes.register('journal_rss', '/_admin/journal/rss', []); diff --git a/rhodecode/public/js/src/rhodecode/codemirror.js b/rhodecode/public/js/src/rhodecode/codemirror.js --- a/rhodecode/public/js/src/rhodecode/codemirror.js +++ b/rhodecode/public/js/src/rhodecode/codemirror.js @@ -204,7 +204,12 @@ var CodeMirrorCompleteAfter = function(c }; var initCodeMirror = function(textAreadId, resetUrl, focus, options) { - var ta = $('#' + textAreadId).get(0); + if (textAreadId.substr(0,1) === "#"){ + var ta = $(textAreadId).get(0); + }else { + var ta = $('#' + textAreadId).get(0); + } + if (focus === undefined) { focus = true; } @@ -644,18 +649,6 @@ var fillCodeMirrorOptions = function(tar } }; -var CodeMirrorPreviewEnable = function(edit_mode) { - // in case it a preview enabled mode enable the button - if (['markdown', 'rst', 'gfm'].indexOf(edit_mode) !== -1) { - $('#render_preview').removeClass('hidden'); - } - else { - if (!$('#render_preview').hasClass('hidden')) { - $('#render_preview').addClass('hidden'); - } - } -}; - /* markup form */ (function(mod) { diff --git a/rhodecode/public/js/src/rhodecode/files.js b/rhodecode/public/js/src/rhodecode/files.js --- a/rhodecode/public/js/src/rhodecode/files.js +++ b/rhodecode/public/js/src/rhodecode/files.js @@ -409,3 +409,109 @@ var showAuthors = function(elem, annotat $('#file_authors_title').html(_gettext('All Authors')) }) }; + + +(function (mod) { + + if (typeof exports == "object" && typeof module == "object") { + // CommonJS + module.exports = mod(); + } else { + // Plain browser env + (this || window).FileEditor = mod(); + } + +})(function () { + "use strict"; + + function FileEditor(textAreaElement, options) { + if (!(this instanceof FileEditor)) { + return new FileEditor(textAreaElement, options); + } + // bind the element instance to our Form + var te = $(textAreaElement).get(0); + if (te !== undefined) { + te.FileEditor = this; + } + + this.modes_select = '#set_mode'; + this.filename_selector = '#filename'; + this.commit_btn_selector = '#commit_btn'; + this.line_wrap_selector = '#line_wrap'; + this.editor_preview_selector = '#editor_preview'; + + if (te !== undefined) { + this.cm = initCodeMirror(textAreaElement, null, false); + } + + // FUNCTIONS and helpers + var self = this; + + this.submitHandler = function() { + $(self.commit_btn_selector).on('click', function(e) { + + var filename = $(self.filename_selector).val(); + if (filename === "") { + alert("Missing filename"); + e.preventDefault(); + } + + var button = $(this); + if (button.hasClass('clicked')) { + button.attr('disabled', true); + } else { + button.addClass('clicked'); + } + }); + }; + this.submitHandler(); + + // on select line wraps change the editor + this.lineWrapHandler = function () { + $(self.line_wrap_selector).on('change', function (e) { + var selected = e.currentTarget; + var line_wraps = {'on': true, 'off': false}[selected.value]; + setCodeMirrorLineWrap(self.cm, line_wraps) + }); + }; + this.lineWrapHandler(); + + + this.showPreview = function () { + + var _text = self.cm.getValue(); + var _file_path = $(self.filename_selector).val(); + if (_text && _file_path) { + $('.show-preview').addClass('active'); + $('.show-editor').removeClass('active'); + + $(self.editor_preview_selector).show(); + $(self.cm.getWrapperElement()).hide(); + + + var post_data = {'text': _text, 'file_path': _file_path, 'csrf_token': CSRF_TOKEN}; + $(self.editor_preview_selector).html(_gettext('Loading ...')); + + var url = pyroutes.url('file_preview'); + + ajaxPOST(url, post_data, function (o) { + $(self.editor_preview_selector).html(o); + }) + } + + }; + + this.showEditor = function () { + $(self.editor_preview_selector).hide(); + $('.show-editor').addClass('active'); + $('.show-preview').removeClass('active'); + + $(self.cm.getWrapperElement()).show(); + }; + + + } + + return FileEditor; +}); + diff --git a/rhodecode/templates/files/files_add.mako b/rhodecode/templates/files/files_add.mako --- a/rhodecode/templates/files/files_add.mako +++ b/rhodecode/templates/files/files_add.mako @@ -1,7 +1,7 @@ <%inherit file="/base/base.mako"/> <%def name="title()"> - ${_('%s Files Add') % c.repo_name} + ${_('{} Files Add').format(c.repo_name)} %if c.rhodecode_name: · ${h.branding(c.rhodecode_name)} %endif @@ -11,58 +11,60 @@ ${self.menu_items(active='repositories')} -<%def name="breadcrumbs_links()"> - ${_('Add new file')} @ ${h.show_id(c.commit)} ${_('Branch')}: ${c.commit.branch} - +<%def name="breadcrumbs_links()"> <%def name="menu_bar_subnav()"> ${self.repo_menu(active='files')} <%def name="main()"> +
- ${self.breadcrumbs()} + ${_('Add new file')} @ ${h.show_id(c.commit)} + ${c.commit.branch}
- ${h.secure_form(h.route_path('repo_files_create_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', enctype="multipart/form-data", class_="form-horizontal", request=request)} + ${h.secure_form(h.route_path('repo_files_create_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', request=request)}
-
-
- ${_('Path')}: -
-
+
+
    +
-
-
-
- ${_('Filename')}: -
-
- - -
+ +
  • + +
  • +
    +
    -
    -
    - ${h.dropdownmenu('set_mode','plain',[('plain',_('plain'))],enable_filter=True)} - - ${h.dropdownmenu('line_wrap', 'off', [('on', _('on')), ('off', _('off')),])} +
    +
    + ${_('Edit')} +
    + +
    + ${_('Preview')} +
    - +
    + ${h.dropdownmenu('line_wrap', 'off', [('on', _('Line wraps on')), ('off', _('line wraps off'))], extra_classes=['last-item'])} +
    +
    + ${h.dropdownmenu('set_mode','plain',[('plain', _('plain'))], enable_filter=True)} +
    -
    +
    
                         
    @@ -74,109 +76,35 @@
     
         
    -
    - ${_('Commit Message')}: -
    -
    -
    - -
    +
    +
    -
    - ${h.reset('reset',_('Cancel'),class_="btn btn-small")} - ${h.submit('commit_btn',_('Commit changes'),class_="btn btn-small btn-success")} +
    + ${h.submit('commit_btn',_('Commit changes'), class_="btn btn-small btn-success")}
    ${h.end_form()}
    + diff --git a/rhodecode/templates/files/files_delete.mako b/rhodecode/templates/files/files_delete.mako --- a/rhodecode/templates/files/files_delete.mako +++ b/rhodecode/templates/files/files_delete.mako @@ -1,7 +1,7 @@ <%inherit file="/base/base.mako"/> <%def name="title()"> - ${_('%s Files Delete') % c.repo_name} + ${_('{} Files Delete').format(c.repo_name)} %if c.rhodecode_name: · ${h.branding(c.rhodecode_name)} %endif @@ -11,29 +11,36 @@ ${self.menu_items(active='repositories')} -<%def name="breadcrumbs_links()"> - ${_('Delete file')} @ ${h.show_id(c.commit)} - +<%def name="breadcrumbs_links()"> <%def name="menu_bar_subnav()"> ${self.repo_menu(active='files')} <%def name="main()"> +
    +
    - ${self.breadcrumbs()} + ${_('Delete file')} @ ${h.show_id(c.commit)} + ${c.commit.branch}
    - ${h.secure_form(h.route_path('repo_files_delete_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', class_="form-horizontal", request=request)} + + ${h.secure_form(h.route_path('repo_files_delete_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', request=request)}
    -
    -
    - ${_('Path')}: -
    -
    - ${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.f_path, request.GET.get('at'))} -
    +
    + +
  • + + +
  • +
    @@ -53,20 +60,26 @@
    -
    - ${_('Commit Message')}: -
    -
    -
    - -
    +
    +
    -
    - ${h.reset('reset',_('Cancel'),class_="btn btn-small btn-danger")} - ${h.submit('commit',_('Delete File'),class_="btn btn-small btn-danger-action")} +
    + ${h.submit('commit',_('Commit changes'),class_="btn btn-small btn-danger-action")}
    ${h.end_form()}
    + + + + diff --git a/rhodecode/templates/files/files_edit.mako b/rhodecode/templates/files/files_edit.mako --- a/rhodecode/templates/files/files_edit.mako +++ b/rhodecode/templates/files/files_edit.mako @@ -1,7 +1,7 @@ <%inherit file="/base/base.mako"/> <%def name="title()"> - ${_('%s File Edit') % c.repo_name} + ${_('{} Files Edit').format(c.repo_name)} %if c.rhodecode_name: · ${h.branding(c.rhodecode_name)} %endif @@ -11,184 +11,110 @@ ${self.menu_items(active='repositories')} -<%def name="breadcrumbs_links()"> - ${_('Edit file')} @ ${h.show_id(c.commit)} - +<%def name="breadcrumbs_links()"> <%def name="menu_bar_subnav()"> ${self.repo_menu(active='files')} <%def name="main()"> -<% renderer = h.renderer_from_filename(c.f_path)%> +
    +
    - ${self.breadcrumbs()} + ${_('Edit file')} @ ${h.show_id(c.commit)} + ${c.commit.branch}
    + + ${h.secure_form(h.route_path('repo_files_update_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', request=request)}
    -
    -
    - ${_('Path')}: -
    -
    -
    - ${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.f_path, request.GET.get('at'))} +
    +
    + +
  • + + +
  • +
    +
    - ${h.secure_form(h.route_path('repo_files_update_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', request=request)} -
    -
    -
    - - ${h.link_to("r%s:%s" % (c.file.commit.idx,h.short_id(c.file.commit.raw_id)),h.route_path('repo_commit',repo_name=c.repo_name,commit_id=c.file.commit.raw_id))} - ${h.format_byte_size_binary(c.file.size)} - ${c.file.mimetype} -
    - - ${_('history')} - +
    - % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name): - % if not c.file.is_binary: - %if True: - ${h.link_to(_('source'), h.route_path('repo_files', repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path),class_="btn btn-mini")} - %else: - ${h.link_to(_('annotation'),h.route_path('repo_files:annotated',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path),class_="btn btn-mini")} - %endif +
    +
    +
    + ${_('Edit')} +
    + +
    + ${_('Preview')} +
    - - ${_('raw')} - - - ${_('download')} - - % endif - % endif +
    + ${h.dropdownmenu('line_wrap', 'off', [('on', _('Line wraps on')), ('off', _('line wraps off')),])} +
    +
    + ${h.dropdownmenu('set_mode','plain',[('plain', _('plain'))],enable_filter=True)} +
    -
    -
    - - ${'%s /' % c.file.dir_path if c.file.dir_path else c.file.dir_path} - - ${h.dropdownmenu('set_mode','plain',[('plain',_('plain'))],enable_filter=True)} - - ${h.dropdownmenu('line_wrap', 'off', [('on', _('on')), ('off', _('off')),])} - - -
    -
    -
    -
    
    -                
    -                
    +
    +
    
    +                    
    +                    
    +
    -
    - ${_('Commit Message')}: -
    -
    -
    - -
    +
    +
    -
    - ${h.reset('reset',_('Cancel'),class_="btn btn-small")} - ${h.submit('commit',_('Commit changes'),class_="btn btn-small btn-success")} +
    + ${h.submit('commit_btn',_('Commit changes'), class_="btn btn-small btn-success")}
    ${h.end_form()}
    diff --git a/rhodecode/templates/files/files_source.mako b/rhodecode/templates/files/files_source.mako --- a/rhodecode/templates/files/files_source.mako +++ b/rhodecode/templates/files/files_source.mako @@ -23,17 +23,17 @@ %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name): ## on branch head, can edit files - %if c.on_branch_head and c.branch_or_raw_id and not c.file.is_binary: + %if c.on_branch_head and c.branch_or_raw_id: ## binary files are delete only % if c.file.is_binary: ${h.link_to(_('Edit'), '#Edit', class_="btn btn-default disabled tooltip", title=_('Editing binary files not allowed'))} - ${h.link_to(_('Delete'), h.route_path('repo_files_remove_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path, _anchor='edit'),class_="btn btn-danger")} + ${h.link_to(_('Delete'), h.route_path('repo_files_remove_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path),class_="btn btn-danger")} % else: - + ${_('Edit on branch: ')}${c.branch_name} - + ${_('Delete')} % endif diff --git a/rhodecode/templates/files/files_upload.mako b/rhodecode/templates/files/files_upload.mako --- a/rhodecode/templates/files/files_upload.mako +++ b/rhodecode/templates/files/files_upload.mako @@ -1,7 +1,7 @@ <%inherit file="/base/base.mako"/> <%def name="title()"> - ${_('%s Files Add') % c.repo_name} + ${_('{} Files Upload').format(c.repo_name)} %if c.rhodecode_name: · ${h.branding(c.rhodecode_name)} %endif @@ -11,180 +11,197 @@ ${self.menu_items(active='repositories')} -<%def name="breadcrumbs_links()"> - ${_('Add new file')} @ ${h.show_id(c.commit)} ${_('Branch')}: ${c.commit.branch} - +<%def name="breadcrumbs_links()"> <%def name="menu_bar_subnav()"> ${self.repo_menu(active='files')} <%def name="main()"> +
    + ## Template for uploads +