|
|
// # Copyright (C) 2010-2020 RhodeCode GmbH
|
|
|
// #
|
|
|
// # This program is free software: you can redistribute it and/or modify
|
|
|
// # it under the terms of the GNU Affero General Public License, version 3
|
|
|
// # (only), as published by the Free Software Foundation.
|
|
|
// #
|
|
|
// # This program is distributed in the hope that it will be useful,
|
|
|
// # but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
// # GNU General Public License for more details.
|
|
|
// #
|
|
|
// # You should have received a copy of the GNU Affero General Public License
|
|
|
// # along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
// #
|
|
|
// # This program is dual-licensed. If you wish to learn more about the
|
|
|
// # RhodeCode Enterprise Edition, including its added features, Support services,
|
|
|
// # and proprietary license terms, please see https://rhodecode.com/licenses/
|
|
|
|
|
|
/**
|
|
|
* Search file list
|
|
|
*/
|
|
|
|
|
|
var NodeFilter = {};
|
|
|
|
|
|
var fileBrowserListeners = function (node_list_url, url_base) {
|
|
|
var $filterInput = $('#node_filter');
|
|
|
var n_filter = $filterInput.get(0);
|
|
|
|
|
|
NodeFilter.filterTimeout = null;
|
|
|
var nodes = null;
|
|
|
|
|
|
NodeFilter.focus = function () {
|
|
|
$filterInput.focus()
|
|
|
};
|
|
|
|
|
|
NodeFilter.fetchNodes = function (callback) {
|
|
|
$.ajax(
|
|
|
{url: node_list_url, headers: {'X-PARTIAL-XHR': true}})
|
|
|
.done(function (data) {
|
|
|
nodes = data.nodes;
|
|
|
if (callback) {
|
|
|
callback();
|
|
|
}
|
|
|
})
|
|
|
.fail(function (data) {
|
|
|
console.log('failed to load');
|
|
|
});
|
|
|
};
|
|
|
|
|
|
NodeFilter.initFilter = function (e) {
|
|
|
if ($filterInput.hasClass('loading')) {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// in case we are already loaded, do nothing
|
|
|
if (!$filterInput.hasClass('init')) {
|
|
|
return NodeFilter.handleKey(e);
|
|
|
}
|
|
|
var iconLoading = 'icon-spin animate-spin';
|
|
|
var iconSearch = 'icon-search';
|
|
|
$('.files-filter-box-path i').removeClass(iconSearch).addClass(iconLoading);
|
|
|
$filterInput.addClass('loading');
|
|
|
|
|
|
var callback = function (org) {
|
|
|
return function () {
|
|
|
if ($filterInput.hasClass('init')) {
|
|
|
$filterInput.removeClass('init');
|
|
|
$filterInput.removeClass('loading');
|
|
|
}
|
|
|
$('.files-filter-box-path i').removeClass(iconLoading).addClass(iconSearch);
|
|
|
|
|
|
// auto re-filter if we filled in the input
|
|
|
if (n_filter.value !== "") {
|
|
|
NodeFilter.updateFilter(n_filter, e)()
|
|
|
}
|
|
|
|
|
|
}
|
|
|
};
|
|
|
// load node data
|
|
|
NodeFilter.fetchNodes(callback());
|
|
|
|
|
|
};
|
|
|
|
|
|
NodeFilter.resetFilter = function () {
|
|
|
$('#tbody').show();
|
|
|
$('#tbody_filtered').hide();
|
|
|
$filterInput.val('');
|
|
|
};
|
|
|
|
|
|
NodeFilter.handleKey = function (e) {
|
|
|
var scrollDown = function (element) {
|
|
|
var elementBottom = element.offset().top + $(element).outerHeight();
|
|
|
var windowBottom = window.innerHeight + $(window).scrollTop();
|
|
|
if (elementBottom > windowBottom) {
|
|
|
var offset = elementBottom - window.innerHeight;
|
|
|
$('html,body').scrollTop(offset);
|
|
|
return false;
|
|
|
}
|
|
|
return true;
|
|
|
};
|
|
|
|
|
|
var scrollUp = function (element) {
|
|
|
if (element.offset().top < $(window).scrollTop()) {
|
|
|
$('html,body').scrollTop(element.offset().top);
|
|
|
return false;
|
|
|
}
|
|
|
return true;
|
|
|
};
|
|
|
var $hlElem = $('.browser-highlight');
|
|
|
|
|
|
if (e.keyCode === 40) { // Down
|
|
|
if ($hlElem.length === 0) {
|
|
|
$('.browser-result').first().addClass('browser-highlight');
|
|
|
} else {
|
|
|
var next = $hlElem.next();
|
|
|
if (next.length !== 0) {
|
|
|
$hlElem.removeClass('browser-highlight');
|
|
|
next.addClass('browser-highlight');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if ($hlElem.get(0) !== undefined){
|
|
|
scrollDown($hlElem);
|
|
|
}
|
|
|
}
|
|
|
if (e.keyCode === 38) { // Up
|
|
|
e.preventDefault();
|
|
|
if ($hlElem.length !== 0) {
|
|
|
var next = $hlElem.prev();
|
|
|
if (next.length !== 0) {
|
|
|
$('.browser-highlight').removeClass('browser-highlight');
|
|
|
next.addClass('browser-highlight');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if ($hlElem.get(0) !== undefined){
|
|
|
scrollUp($hlElem);
|
|
|
}
|
|
|
|
|
|
}
|
|
|
if (e.keyCode === 13) { // Enter
|
|
|
if ($('.browser-highlight').length !== 0) {
|
|
|
var url = $('.browser-highlight').find('.match-link').attr('href');
|
|
|
window.location = url;
|
|
|
}
|
|
|
}
|
|
|
if (e.keyCode === 27) { // Esc
|
|
|
NodeFilter.resetFilter();
|
|
|
$('html,body').scrollTop(0);
|
|
|
}
|
|
|
|
|
|
var capture_keys = [
|
|
|
40, // ArrowDown
|
|
|
38, // ArrowUp
|
|
|
39, // ArrowRight
|
|
|
37, // ArrowLeft
|
|
|
13, // Enter
|
|
|
27 // Esc
|
|
|
];
|
|
|
|
|
|
if ($.inArray(e.keyCode, capture_keys) === -1) {
|
|
|
clearTimeout(NodeFilter.filterTimeout);
|
|
|
NodeFilter.filterTimeout = setTimeout(NodeFilter.updateFilter(n_filter, e), 200);
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
NodeFilter.fuzzy_match = function (filepath, query) {
|
|
|
var highlight = [];
|
|
|
var order = 0;
|
|
|
for (var i = 0; i < query.length; i++) {
|
|
|
var match_position = filepath.indexOf(query[i]);
|
|
|
if (match_position !== -1) {
|
|
|
var prev_match_position = highlight[highlight.length - 1];
|
|
|
if (prev_match_position === undefined) {
|
|
|
highlight.push(match_position);
|
|
|
} else {
|
|
|
var current_match_position = prev_match_position + match_position + 1;
|
|
|
highlight.push(current_match_position);
|
|
|
order = order + current_match_position - prev_match_position;
|
|
|
}
|
|
|
filepath = filepath.substring(match_position + 1);
|
|
|
} else {
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
return {
|
|
|
'order': order,
|
|
|
'highlight': highlight
|
|
|
};
|
|
|
};
|
|
|
|
|
|
NodeFilter.sortPredicate = function (a, b) {
|
|
|
if (a.order < b.order) return -1;
|
|
|
if (a.order > b.order) return 1;
|
|
|
if (a.filepath < b.filepath) return -1;
|
|
|
if (a.filepath > b.filepath) return 1;
|
|
|
return 0;
|
|
|
};
|
|
|
|
|
|
NodeFilter.updateFilter = function (elem, e) {
|
|
|
return function () {
|
|
|
// Reset timeout
|
|
|
NodeFilter.filterTimeout = null;
|
|
|
var query = elem.value.toLowerCase();
|
|
|
var match = [];
|
|
|
var matches_max = 20;
|
|
|
if (query !== "") {
|
|
|
var results = [];
|
|
|
for (var k = 0; k < nodes.length; k++) {
|
|
|
var result = NodeFilter.fuzzy_match(
|
|
|
nodes[k].name.toLowerCase(), query);
|
|
|
if (result) {
|
|
|
result.type = nodes[k].type;
|
|
|
result.filepath = nodes[k].name;
|
|
|
results.push(result);
|
|
|
}
|
|
|
}
|
|
|
results = results.sort(NodeFilter.sortPredicate);
|
|
|
var limit = matches_max;
|
|
|
if (results.length < matches_max) {
|
|
|
limit = results.length;
|
|
|
}
|
|
|
for (var i = 0; i < limit; i++) {
|
|
|
if (query && results.length > 0) {
|
|
|
var n = results[i].filepath;
|
|
|
var t = results[i].type;
|
|
|
var n_hl = n.split("");
|
|
|
var pos = results[i].highlight;
|
|
|
for (var j = 0; j < pos.length; j++) {
|
|
|
n_hl[pos[j]] = "<em>" + n_hl[pos[j]] + "</em>";
|
|
|
}
|
|
|
n_hl = n_hl.join("");
|
|
|
var new_url = url_base.replace('__FPATH__', n);
|
|
|
|
|
|
var typeObj = {
|
|
|
dir: 'icon-directory browser-dir',
|
|
|
file: 'icon-file-text browser-file'
|
|
|
};
|
|
|
|
|
|
var typeIcon = '<i class="{0}"></i>'.format(typeObj[t]);
|
|
|
match.push('<tr class="browser-result"><td><a class="match-link" href="{0}">{1}{2}</a></td><td colspan="5"></td></tr>'.format(new_url, typeIcon, n_hl));
|
|
|
}
|
|
|
}
|
|
|
if (results.length > limit) {
|
|
|
var truncated_count = results.length - matches_max;
|
|
|
if (truncated_count === 1) {
|
|
|
match.push('<tr><td>{0} {1}</td><td colspan="5"></td></tr>'.format(truncated_count, _gettext('truncated result')));
|
|
|
} else {
|
|
|
match.push('<tr><td>{0} {1}</td><td colspan="5"></td></tr>'.format(truncated_count, _gettext('truncated results')));
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
if (query !== "") {
|
|
|
$('#tbody').hide();
|
|
|
$('#tbody_filtered').show();
|
|
|
|
|
|
if (match.length === 0) {
|
|
|
match.push('<tr><td>{0}</td><td colspan="5"></td></tr>'.format(_gettext('No matching files')));
|
|
|
}
|
|
|
$('#tbody_filtered').html(match.join(""));
|
|
|
} else {
|
|
|
$('#tbody').show();
|
|
|
$('#tbody_filtered').hide();
|
|
|
}
|
|
|
|
|
|
};
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
var getIdentNode = function(n){
|
|
|
// iterate through nodes until matched interesting node
|
|
|
if (typeof n === 'undefined'){
|
|
|
return -1;
|
|
|
}
|
|
|
if(typeof n.id !== "undefined" && n.id.match('L[0-9]+')){
|
|
|
return n;
|
|
|
}
|
|
|
else{
|
|
|
return getIdentNode(n.parentNode);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
var getSelectionLink = function(e) {
|
|
|
// get selection from start/to nodes
|
|
|
if (typeof window.getSelection !== "undefined") {
|
|
|
s = window.getSelection();
|
|
|
|
|
|
from = getIdentNode(s.anchorNode);
|
|
|
till = getIdentNode(s.focusNode);
|
|
|
|
|
|
f_int = parseInt(from.id.replace('L',''));
|
|
|
t_int = parseInt(till.id.replace('L',''));
|
|
|
|
|
|
if (f_int > t_int){
|
|
|
// highlight from bottom
|
|
|
offset = -35;
|
|
|
ranges = [t_int,f_int];
|
|
|
}
|
|
|
else{
|
|
|
// highligth from top
|
|
|
offset = 35;
|
|
|
ranges = [f_int,t_int];
|
|
|
}
|
|
|
// if we select more than 2 lines
|
|
|
if (ranges[0] !== ranges[1]){
|
|
|
if($('#linktt').length === 0){
|
|
|
hl_div = document.createElement('div');
|
|
|
hl_div.id = 'linktt';
|
|
|
}
|
|
|
hl_div.innerHTML = '';
|
|
|
|
|
|
anchor = '#L'+ranges[0]+'-'+ranges[1];
|
|
|
var link = document.createElement('a');
|
|
|
link.href = location.href.substring(0,location.href.indexOf('#'))+anchor;
|
|
|
link.innerHTML = _gettext('Selection link');
|
|
|
hl_div.appendChild(link);
|
|
|
$('#codeblock').append(hl_div);
|
|
|
|
|
|
var xy = $(till).offset();
|
|
|
$('#linktt').addClass('hl-tip-box tip-box');
|
|
|
$('#linktt').offset({top: xy.top + offset, left: xy.left});
|
|
|
$('#linktt').css('visibility','visible');
|
|
|
}
|
|
|
else{
|
|
|
$('#linktt').css('visibility','hidden');
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
|
|
|
var getFileState = function() {
|
|
|
// relies on a global set filesUrlData
|
|
|
var f_path = filesUrlData['f_path'];
|
|
|
var commit_id = filesUrlData['commit_id'];
|
|
|
|
|
|
var url_params = {
|
|
|
repo_name: templateContext.repo_name,
|
|
|
commit_id: commit_id,
|
|
|
f_path:'__FPATH__'
|
|
|
};
|
|
|
if (atRef !== '') {
|
|
|
url_params['at'] = atRef
|
|
|
}
|
|
|
|
|
|
var _url_base = pyroutes.url('repo_files', url_params);
|
|
|
var _node_list_url = pyroutes.url('repo_files_nodelist',
|
|
|
{repo_name: templateContext.repo_name,
|
|
|
commit_id: commit_id, f_path: f_path});
|
|
|
|
|
|
return {
|
|
|
f_path: f_path,
|
|
|
commit_id: commit_id,
|
|
|
node_list_url: _node_list_url,
|
|
|
url_base: _url_base
|
|
|
};
|
|
|
};
|
|
|
|
|
|
var getFilesMetadata = function() {
|
|
|
// relies on metadataRequest global state
|
|
|
if (metadataRequest && metadataRequest.readyState != 4) {
|
|
|
metadataRequest.abort();
|
|
|
}
|
|
|
|
|
|
if ($('#file-tree-wrapper').hasClass('full-load')) {
|
|
|
// in case our HTML wrapper has full-load class we don't
|
|
|
// trigger the async load of metadata
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
var state = getFileState();
|
|
|
var url_data = {
|
|
|
'repo_name': templateContext.repo_name,
|
|
|
'commit_id': state.commit_id,
|
|
|
'f_path': state.f_path,
|
|
|
};
|
|
|
|
|
|
if (atRef !== '') {
|
|
|
url_data['at'] = atRef
|
|
|
}
|
|
|
|
|
|
var url = pyroutes.url('repo_nodetree_full', url_data);
|
|
|
|
|
|
metadataRequest = $.ajax({url: url});
|
|
|
|
|
|
metadataRequest.done(function(data) {
|
|
|
$('#file-tree').html(data);
|
|
|
timeagoActivate();
|
|
|
tooltipActivate();
|
|
|
});
|
|
|
metadataRequest.fail(function (data, textStatus, errorThrown) {
|
|
|
if (data.status != 0) {
|
|
|
alert("Error while fetching metadata.\nError code {0} ({1}).Please consider reloading the page".format(data.status,data.statusText));
|
|
|
}
|
|
|
});
|
|
|
};
|
|
|
|
|
|
// show more authors
|
|
|
var showAuthors = function(elem, annotate) {
|
|
|
var state = getFileState('callbacks');
|
|
|
|
|
|
var url = pyroutes.url('repo_file_authors',
|
|
|
{'repo_name': templateContext.repo_name,
|
|
|
'commit_id': state.commit_id, 'f_path': state.f_path});
|
|
|
|
|
|
$.pjax({
|
|
|
url: url,
|
|
|
data: 'annotate={0}'.format(annotate),
|
|
|
container: '#file_authors',
|
|
|
push: false,
|
|
|
timeout: 5000
|
|
|
}).complete(function(){
|
|
|
$(elem).hide();
|
|
|
$('#file_authors_title').html(_gettext('All Authors'));
|
|
|
tooltipActivate();
|
|
|
})
|
|
|
};
|
|
|
|
|
|
|
|
|
(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;
|
|
|
});
|
|
|
|
|
|
|
|
|
var checkFileHead = function($editForm, commit_id, f_path, operation) {
|
|
|
function getFormData($form){
|
|
|
var unindexed_array = $form.serializeArray();
|
|
|
var indexed_array = {};
|
|
|
|
|
|
$.map(unindexed_array, function(n, i){
|
|
|
indexed_array[n['name']] = n['value'];
|
|
|
});
|
|
|
|
|
|
return indexed_array;
|
|
|
}
|
|
|
|
|
|
$editForm.on('submit', function (e) {
|
|
|
|
|
|
var validHead = $editForm.find('#commit_btn').data('validHead');
|
|
|
if (validHead === true){
|
|
|
return true
|
|
|
}
|
|
|
|
|
|
// no marker, we do "checks"
|
|
|
e.preventDefault();
|
|
|
var formData = getFormData($editForm);
|
|
|
var new_path = formData['filename'];
|
|
|
|
|
|
var success = function(data) {
|
|
|
|
|
|
if (data['is_head'] === true && data['path_exists'] === "") {
|
|
|
|
|
|
$editForm.find('#commit_btn').data('validHead', true);
|
|
|
$editForm.find('#commit_btn').val('Committing...');
|
|
|
$editForm.submit();
|
|
|
|
|
|
} else {
|
|
|
var message = '';
|
|
|
var urlTmpl = '<a target="_blank" href="{0}">here</a>';
|
|
|
$editForm.find('#commit_btn').val('Commit aborted');
|
|
|
|
|
|
if (operation === 'edit') {
|
|
|
var new_url = urlTmpl.format(pyroutes.url('repo_files_edit_file', {"repo_name": templateContext.repo_name, "commit_id": data['branch'], "f_path": f_path}));
|
|
|
message = _gettext('File `{0}` has a newer version available, or has been removed. Click {1} to see the latest version.'.format(f_path, new_url));
|
|
|
} else if (operation === 'delete') {
|
|
|
var new_url = urlTmpl.format(pyroutes.url('repo_files_remove_file', {"repo_name": templateContext.repo_name, "commit_id": data['branch'], "f_path": f_path}));
|
|
|
message = _gettext('File `{0}` has a newer version available, or has been removed. Click {1} to see the latest version.'.format(f_path, new_url));
|
|
|
} else if (operation === 'create') {
|
|
|
if (data['path_exists'] !== "") {
|
|
|
message = _gettext('There is an existing path `{0}` at this commit.'.format(data['path_exists']));
|
|
|
} else {
|
|
|
var new_url = urlTmpl.format(pyroutes.url('repo_files_add_file', {"repo_name": templateContext.repo_name, "commit_id": data['branch'], "f_path": f_path, "filename": new_path}));
|
|
|
message = _gettext('There is a later version of file tree available. Click {0} to create a file at the latest tree.'.format(new_url));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
var payload = {
|
|
|
message: {
|
|
|
message: message,
|
|
|
level: 'warning',
|
|
|
force: true
|
|
|
}
|
|
|
};
|
|
|
$.Topic('/notifications').publish(payload);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// lock button
|
|
|
$editForm.find('#commit_btn').attr('disabled', 'disabled');
|
|
|
$editForm.find('#commit_btn').val('Checking commit...');
|
|
|
|
|
|
var postData = {'csrf_token': CSRF_TOKEN, 'path': new_path, 'operation': operation};
|
|
|
ajaxPOST(pyroutes.url('repo_files_check_head',
|
|
|
{'repo_name': templateContext.repo_name, 'commit_id': commit_id, 'f_path': f_path}),
|
|
|
postData, success);
|
|
|
|
|
|
return false
|
|
|
|
|
|
});
|
|
|
};
|
|
|
|