##// END OF EJS Templates
diffs: added diff navigation to improve UX when browisng the full context diffs.
marcink -
r4441:114e65cb default
parent child Browse files
Show More
@@ -0,0 +1,91 b''
1 // jQuery Scrollstop Plugin v1.2.0
2 // https://github.com/ssorallen/jquery-scrollstop
3
4 (function (factory) {
5 // UMD[2] wrapper for jQuery plugins to work in AMD or in CommonJS.
6 //
7 // [2] https://github.com/umdjs/umd
8
9 if (typeof define === 'function' && define.amd) {
10 // AMD. Register as an anonymous module.
11 define(['jquery'], factory);
12 } else if (typeof exports === 'object') {
13 // Node/CommonJS
14 module.exports = factory(require('jquery'));
15 } else {
16 // Browser globals
17 factory(jQuery);
18 }
19 }(function ($) {
20 // $.event.dispatch was undocumented and was deprecated in jQuery 1.7[1]. It
21 // was replaced by $.event.handle in jQuery 1.9.
22 //
23 // Use the first of the available functions to support jQuery <1.8.
24 //
25 // [1] https://github.com/jquery/jquery-migrate/blob/master/src/event.js#L25
26 var dispatch = $.event.dispatch || $.event.handle;
27
28 var special = $.event.special,
29 uid1 = 'D' + (+new Date()),
30 uid2 = 'D' + (+new Date() + 1);
31
32 special.scrollstart = {
33 setup: function(data) {
34 var _data = $.extend({
35 latency: special.scrollstop.latency
36 }, data);
37
38 var timer,
39 handler = function(evt) {
40 var _self = this,
41 _args = arguments;
42
43 if (timer) {
44 clearTimeout(timer);
45 } else {
46 evt.type = 'scrollstart';
47 dispatch.apply(_self, _args);
48 }
49
50 timer = setTimeout(function() {
51 timer = null;
52 }, _data.latency);
53 };
54
55 $(this).bind('scroll', handler).data(uid1, handler);
56 },
57 teardown: function() {
58 $(this).unbind('scroll', $(this).data(uid1));
59 }
60 };
61
62 special.scrollstop = {
63 latency: 250,
64 setup: function(data) {
65 var _data = $.extend({
66 latency: special.scrollstop.latency
67 }, data);
68
69 var timer,
70 handler = function(evt) {
71 var _self = this,
72 _args = arguments;
73
74 if (timer) {
75 clearTimeout(timer);
76 }
77
78 timer = setTimeout(function() {
79 timer = null;
80 evt.type = 'scrollstop';
81 dispatch.apply(_self, _args);
82 }, _data.latency);
83 };
84
85 $(this).bind('scroll', handler).data(uid2, handler);
86 },
87 teardown: function() {
88 $(this).unbind('scroll', $(this).data(uid2));
89 }
90 };
91 }));
@@ -0,0 +1,171 b''
1 /**
2 * Within Viewport jQuery Plugin
3 *
4 * @description Companion plugin for withinviewport.js - determines whether an element is completely within the browser viewport
5 * @author Craig Patik, http://patik.com/
6 * @version 2.1.2
7 * @date 2019-08-16
8 */
9 (function ($) {
10 /**
11 * $.withinviewport()
12 * @description jQuery method
13 * @param {Object} [settings] optional settings
14 * @return {Collection} Contains all elements that were within the viewport
15 */
16 $.fn.withinviewport = function (settings) {
17 var opts;
18 var elems;
19
20 if (typeof settings === 'string') {
21 settings = {
22 sides: settings
23 };
24 }
25
26 opts = $.extend({}, settings, {
27 sides: 'all'
28 });
29 elems = [];
30
31 this.each(function () {
32 if (withinviewport(this, opts)) {
33 elems.push(this);
34 }
35 });
36
37 return $(elems);
38 };
39
40 // Main custom selector
41 $.extend($.expr[':'], {
42 'within-viewport': function (element) {
43 return withinviewport(element, 'all');
44 }
45 });
46
47 /**
48 * Optional enhancements and shortcuts
49 *
50 * @description Uncomment or comment these pieces as they apply to your project and coding preferences
51 */
52
53 // Shorthand jQuery methods
54
55 $.fn.withinviewporttop = function (settings) {
56 var opts;
57 var elems;
58
59 if (typeof settings === 'string') {
60 settings = {
61 sides: settings
62 };
63 }
64
65 opts = $.extend({}, settings, {
66 sides: 'top'
67 });
68 elems = [];
69
70 this.each(function () {
71 if (withinviewport(this, opts)) {
72 elems.push(this);
73 }
74 });
75
76 return $(elems);
77 };
78
79 $.fn.withinviewportright = function (settings) {
80 var opts;
81 var elems;
82
83 if (typeof settings === 'string') {
84 settings = {
85 sides: settings
86 };
87 }
88
89 opts = $.extend({}, settings, {
90 sides: 'right'
91 });
92 elems = [];
93
94 this.each(function () {
95 if (withinviewport(this, opts)) {
96 elems.push(this);
97 }
98 });
99
100 return $(elems);
101 };
102
103 $.fn.withinviewportbottom = function (settings) {
104 var opts;
105 var elems;
106
107 if (typeof settings === 'string') {
108 settings = {
109 sides: settings
110 };
111 }
112
113 opts = $.extend({}, settings, {
114 sides: 'bottom'
115 });
116 elems = [];
117
118 this.each(function () {
119 if (withinviewport(this, opts)) {
120 elems.push(this);
121 }
122 });
123
124 return $(elems);
125 };
126
127 $.fn.withinviewportleft = function (settings) {
128 var opts;
129 var elems;
130
131 if (typeof settings === 'string') {
132 settings = {
133 sides: settings
134 };
135 }
136
137 opts = $.extend({}, settings, {
138 sides: 'left'
139 });
140 elems = [];
141
142 this.each(function () {
143 if (withinviewport(this, opts)) {
144 elems.push(this);
145 }
146 });
147
148 return $(elems);
149 };
150
151 // Custom jQuery selectors
152 $.extend($.expr[':'], {
153 'within-viewport-top': function (element) {
154 return withinviewport(element, 'top');
155 },
156 'within-viewport-right': function (element) {
157 return withinviewport(element, 'right');
158 },
159 'within-viewport-bottom': function (element) {
160 return withinviewport(element, 'bottom');
161 },
162 'within-viewport-left': function (element) {
163 return withinviewport(element, 'left');
164 }
165 // Example custom selector:
166 //,
167 // 'within-viewport-top-left-45': function (element) {
168 // return withinviewport(element, {sides:'top left', top: 45, left: 45});
169 // }
170 });
171 }(jQuery)); No newline at end of file
@@ -0,0 +1,235 b''
1 /**
2 * Within Viewport
3 *
4 * @description Determines whether an element is completely within the browser viewport
5 * @author Craig Patik, http://patik.com/
6 * @version 2.1.2
7 * @date 2019-08-16
8 */
9 (function (root, name, factory) {
10 // AMD
11 if (typeof define === 'function' && define.amd) {
12 define([], factory);
13 }
14 // Node and CommonJS-like environments
15 else if (typeof module !== 'undefined' && typeof exports === 'object') {
16 module.exports = factory();
17 }
18 // Browser global
19 else {
20 root[name] = factory();
21 }
22 }(this, 'withinviewport', function () {
23 var canUseWindowDimensions = typeof window !== 'undefined' && window.innerHeight !== undefined; // IE 8 and lower fail this
24
25 /**
26 * Determines whether an element is within the viewport
27 * @param {Object} elem DOM Element (required)
28 * @param {Object} options Optional settings
29 * @return {Boolean} Whether the element was completely within the viewport
30 */
31 var withinviewport = function withinviewport(elem, options) {
32 var result = false;
33 var metadata = {};
34 var config = {};
35 var settings;
36 var isWithin;
37 var isContainerTheWindow;
38 var elemBoundingRect;
39 var containerBoundingRect;
40 var containerScrollTop;
41 var containerScrollLeft;
42 var scrollBarWidths = [0, 0];
43 var sideNamesPattern;
44 var sides;
45 var side;
46 var i;
47
48 // If invoked by the jQuery plugin, get the actual DOM element
49 if (typeof jQuery !== 'undefined' && elem instanceof jQuery) {
50 elem = elem.get(0);
51 }
52
53 if (typeof elem !== 'object' || elem.nodeType !== 1) {
54 throw new Error('First argument must be an element');
55 }
56
57 // Look for inline settings on the element
58 if (elem.getAttribute('data-withinviewport-settings') && window.JSON) {
59 metadata = JSON.parse(elem.getAttribute('data-withinviewport-settings'));
60 }
61
62 // Settings argument may be a simple string (`top`, `right`, etc)
63 if (typeof options === 'string') {
64 settings = {
65 sides: options
66 };
67 } else {
68 settings = options || {};
69 }
70
71 // Build configuration from defaults and user-provided settings and metadata
72 config.container = settings.container || metadata.container || withinviewport.defaults.container || window;
73 config.sides = settings.sides || metadata.sides || withinviewport.defaults.sides || 'all';
74 config.top = settings.top || metadata.top || withinviewport.defaults.top || 0;
75 config.right = settings.right || metadata.right || withinviewport.defaults.right || 0;
76 config.bottom = settings.bottom || metadata.bottom || withinviewport.defaults.bottom || 0;
77 config.left = settings.left || metadata.left || withinviewport.defaults.left || 0;
78
79 // Extract the DOM node from a jQuery collection
80 if (typeof jQuery !== 'undefined' && config.container instanceof jQuery) {
81 config.container = config.container.get(0);
82 }
83
84 // Use the window as the container if the user specified the body or a non-element
85 if (config.container === document.body || config.container.nodeType !== 1) {
86 config.container = window;
87 }
88
89 isContainerTheWindow = (config.container === window);
90
91 // Element testing methods
92 isWithin = {
93 // Element is below the top edge of the viewport
94 top: function _isWithin_top() {
95 if (isContainerTheWindow) {
96 return (elemBoundingRect.top >= config.top);
97 } else {
98 return (elemBoundingRect.top >= containerScrollTop - (containerScrollTop - containerBoundingRect.top) + config.top);
99 }
100 },
101
102 // Element is to the left of the right edge of the viewport
103 right: function _isWithin_right() {
104 // Note that `elemBoundingRect.right` is the distance from the *left* of the viewport to the element's far right edge
105
106 if (isContainerTheWindow) {
107 return (elemBoundingRect.right <= (containerBoundingRect.right + containerScrollLeft) - config.right);
108 } else {
109 return (elemBoundingRect.right <= containerBoundingRect.right - scrollBarWidths[0] - config.right);
110 }
111 },
112
113 // Element is above the bottom edge of the viewport
114 bottom: function _isWithin_bottom() {
115 var containerHeight = 0;
116
117 if (isContainerTheWindow) {
118 if (canUseWindowDimensions) {
119 containerHeight = config.container.innerHeight;
120 } else if (document && document.documentElement) {
121 containerHeight = document.documentElement.clientHeight;
122 }
123 } else {
124 containerHeight = containerBoundingRect.bottom;
125 }
126
127 // Note that `elemBoundingRect.bottom` is the distance from the *top* of the viewport to the element's bottom edge
128 return (elemBoundingRect.bottom <= containerHeight - scrollBarWidths[1] - config.bottom);
129 },
130
131 // Element is to the right of the left edge of the viewport
132 left: function _isWithin_left() {
133 if (isContainerTheWindow) {
134 return (elemBoundingRect.left >= config.left);
135 } else {
136 return (elemBoundingRect.left >= containerScrollLeft - (containerScrollLeft - containerBoundingRect.left) + config.left);
137 }
138 },
139
140 // Element is within all four boundaries
141 all: function _isWithin_all() {
142 // Test each boundary in order of efficiency and likeliness to be false. This way we can avoid running all four functions on most elements.
143 // 1. Top: Quickest to calculate + most likely to be false
144 // 2. Bottom: Note quite as quick to calculate, but also very likely to be false
145 // 3-4. Left and right are both equally unlikely to be false since most sites only scroll vertically, but left is faster to calculate
146 return (isWithin.top() && isWithin.bottom() && isWithin.left() && isWithin.right());
147 }
148 };
149
150 // Get the element's bounding rectangle with respect to the viewport
151 elemBoundingRect = elem.getBoundingClientRect();
152
153 // Get viewport dimensions and offsets
154 if (isContainerTheWindow) {
155 containerBoundingRect = document.documentElement.getBoundingClientRect();
156 containerScrollTop = document.body.scrollTop;
157 containerScrollLeft = window.scrollX || document.body.scrollLeft;
158 } else {
159 containerBoundingRect = config.container.getBoundingClientRect();
160 containerScrollTop = config.container.scrollTop;
161 containerScrollLeft = config.container.scrollLeft;
162 }
163
164 // Don't count the space consumed by scrollbars
165 if (containerScrollLeft) {
166 scrollBarWidths[0] = 18;
167 }
168
169 if (containerScrollTop) {
170 scrollBarWidths[1] = 16;
171 }
172
173 // Test the element against each side of the viewport that was requested
174 sideNamesPattern = /^top$|^right$|^bottom$|^left$|^all$/;
175
176 // Loop through all of the sides
177 sides = config.sides.split(' ');
178 i = sides.length;
179
180 while (i--) {
181 side = sides[i].toLowerCase();
182
183 if (sideNamesPattern.test(side)) {
184 if (isWithin[side]()) {
185 result = true;
186 } else {
187 result = false;
188
189 // Quit as soon as the first failure is found
190 break;
191 }
192 }
193 }
194
195 return result;
196 };
197
198 // Default settings
199 withinviewport.prototype.defaults = {
200 container: typeof document !== 'undefined' ? document.body : {},
201 sides: 'all',
202 top: 0,
203 right: 0,
204 bottom: 0,
205 left: 0
206 };
207
208 withinviewport.defaults = withinviewport.prototype.defaults;
209
210 /**
211 * Optional enhancements and shortcuts
212 *
213 * @description Uncomment or comment these pieces as they apply to your project and coding preferences
214 */
215
216 // Shortcut methods for each side of the viewport
217 // Example: `withinviewport.top(elem)` is the same as `withinviewport(elem, 'top')`
218 withinviewport.prototype.top = function _withinviewport_top(element) {
219 return withinviewport(element, 'top');
220 };
221
222 withinviewport.prototype.right = function _withinviewport_right(element) {
223 return withinviewport(element, 'right');
224 };
225
226 withinviewport.prototype.bottom = function _withinviewport_bottom(element) {
227 return withinviewport(element, 'bottom');
228 };
229
230 withinviewport.prototype.left = function _withinviewport_left(element) {
231 return withinviewport(element, 'left');
232 };
233
234 return withinviewport;
235 })); No newline at end of file
@@ -1,184 +1,187 b''
1 1 {
2 2 "dirs": {
3 3 "css": {
4 4 "src": "rhodecode/public/css",
5 5 "dest": "rhodecode/public/css"
6 6 },
7 7 "js": {
8 8 "src": "rhodecode/public/js/src",
9 9 "src_rc": "rhodecode/public/js/rhodecode",
10 10 "dest": "rhodecode/public/js",
11 11 "node_modules": "node_modules"
12 12 }
13 13 },
14 14 "copy": {
15 15 "main": {
16 16 "files": [
17 17 {
18 18 "expand": true,
19 19 "cwd": "node_modules/@webcomponents",
20 20 "src": "webcomponentsjs/*.*",
21 21 "dest": "<%= dirs.js.dest %>/vendors"
22 22 },
23 23 {
24 24 "src": "<%= dirs.css.src %>/style-polymer.css",
25 25 "dest": "<%= dirs.js.dest %>/src/components/style-polymer.css"
26 26 }
27 27 ]
28 28 }
29 29 },
30 30 "concat": {
31 31 "dist": {
32 32 "src": [
33 33 "<%= dirs.js.node_modules %>/jquery/dist/jquery.min.js",
34 34 "<%= dirs.js.node_modules %>/mousetrap/mousetrap.min.js",
35 35 "<%= dirs.js.node_modules %>/moment/min/moment.min.js",
36 36 "<%= dirs.js.node_modules %>/clipboard/dist/clipboard.min.js",
37 37 "<%= dirs.js.node_modules %>/favico.js/favico-0.3.10.min.js",
38 38 "<%= dirs.js.node_modules %>/dropzone/dist/min/dropzone.min.js",
39 39 "<%= dirs.js.node_modules %>/sweetalert2/dist/sweetalert2.min.js",
40 40 "<%= dirs.js.node_modules %>/sticky-sidebar/dist/sticky-sidebar.min.js",
41 41 "<%= dirs.js.node_modules %>/sticky-sidebar/dist/jquery.sticky-sidebar.min.js",
42 42 "<%= dirs.js.node_modules %>/waypoints/lib/noframework.waypoints.min.js",
43 43 "<%= dirs.js.node_modules %>/waypoints/lib/jquery.waypoints.min.js",
44 44 "<%= dirs.js.node_modules %>/appenlight-client/appenlight-client.min.js",
45 45 "<%= dirs.js.src %>/logging.js",
46 46 "<%= dirs.js.src %>/bootstrap.js",
47 47 "<%= dirs.js.src %>/i18n_utils.js",
48 48 "<%= dirs.js.src %>/deform.js",
49 49 "<%= dirs.js.src %>/ejs.js",
50 50 "<%= dirs.js.src %>/ejs_templates/utils.js",
51 51 "<%= dirs.js.src %>/plugins/jquery.pjax.js",
52 52 "<%= dirs.js.src %>/plugins/jquery.dataTables.js",
53 53 "<%= dirs.js.src %>/plugins/flavoured_checkbox.js",
54 "<%= dirs.js.src %>/plugins/within_viewport.js",
54 55 "<%= dirs.js.src %>/plugins/jquery.auto-grow-input.js",
55 56 "<%= dirs.js.src %>/plugins/jquery.autocomplete.js",
56 57 "<%= dirs.js.src %>/plugins/jquery.debounce.js",
58 "<%= dirs.js.src %>/plugins/jquery.scrollstop.js",
59 "<%= dirs.js.src %>/plugins/jquery.within-viewport.js",
57 60 "<%= dirs.js.node_modules %>/mark.js/dist/jquery.mark.min.js",
58 61 "<%= dirs.js.src %>/plugins/jquery.timeago.js",
59 62 "<%= dirs.js.src %>/plugins/jquery.timeago-extension.js",
60 63 "<%= dirs.js.src %>/select2/select2.js",
61 64 "<%= dirs.js.src %>/codemirror/codemirror.js",
62 65 "<%= dirs.js.src %>/codemirror/codemirror_loadmode.js",
63 66 "<%= dirs.js.src %>/codemirror/codemirror_hint.js",
64 67 "<%= dirs.js.src %>/codemirror/codemirror_overlay.js",
65 68 "<%= dirs.js.src %>/codemirror/codemirror_placeholder.js",
66 69 "<%= dirs.js.src %>/codemirror/codemirror_simplemode.js",
67 70 "<%= dirs.js.dest %>/mode/meta.js",
68 71 "<%= dirs.js.dest %>/mode/meta_ext.js",
69 72 "<%= dirs.js.src_rc %>/i18n/select2/translations.js",
70 73 "<%= dirs.js.src %>/rhodecode/utils/array.js",
71 74 "<%= dirs.js.src %>/rhodecode/utils/string.js",
72 75 "<%= dirs.js.src %>/rhodecode/utils/pyroutes.js",
73 76 "<%= dirs.js.src %>/rhodecode/utils/ajax.js",
74 77 "<%= dirs.js.src %>/rhodecode/utils/autocomplete.js",
75 78 "<%= dirs.js.src %>/rhodecode/utils/colorgenerator.js",
76 79 "<%= dirs.js.src %>/rhodecode/utils/ie.js",
77 80 "<%= dirs.js.src %>/rhodecode/utils/os.js",
78 81 "<%= dirs.js.src %>/rhodecode/utils/topics.js",
79 82 "<%= dirs.js.src %>/rhodecode/init.js",
80 83 "<%= dirs.js.src %>/rhodecode/changelog.js",
81 84 "<%= dirs.js.src %>/rhodecode/codemirror.js",
82 85 "<%= dirs.js.src %>/rhodecode/comments.js",
83 86 "<%= dirs.js.src %>/rhodecode/constants.js",
84 87 "<%= dirs.js.src %>/rhodecode/files.js",
85 88 "<%= dirs.js.src %>/rhodecode/followers.js",
86 89 "<%= dirs.js.src %>/rhodecode/menus.js",
87 90 "<%= dirs.js.src %>/rhodecode/notifications.js",
88 91 "<%= dirs.js.src %>/rhodecode/permissions.js",
89 92 "<%= dirs.js.src %>/rhodecode/pjax.js",
90 93 "<%= dirs.js.src %>/rhodecode/pullrequests.js",
91 94 "<%= dirs.js.src %>/rhodecode/settings.js",
92 95 "<%= dirs.js.src %>/rhodecode/select2_widgets.js",
93 96 "<%= dirs.js.src %>/rhodecode/tooltips.js",
94 97 "<%= dirs.js.src %>/rhodecode/users.js",
95 98 "<%= dirs.js.src %>/rhodecode/appenlight.js",
96 99 "<%= dirs.js.src %>/rhodecode.js",
97 100 "<%= dirs.js.dest %>/rhodecode-components.js"
98 101 ],
99 102 "dest": "<%= dirs.js.dest %>/scripts.js",
100 103 "nonull": true
101 104 }
102 105 },
103 106 "uglify": {
104 107 "dist": {
105 108 "src": "<%= dirs.js.dest %>/scripts.js",
106 109 "dest": "<%= dirs.js.dest %>/scripts.min.js"
107 110 }
108 111 },
109 112 "less": {
110 113 "development": {
111 114 "options": {
112 115 "compress": false,
113 116 "yuicompress": false,
114 117 "optimization": 0
115 118 },
116 119 "files": {
117 120 "<%= dirs.css.dest %>/style.css": "<%= dirs.css.src %>/main.less",
118 121 "<%= dirs.css.dest %>/style-polymer.css": "<%= dirs.css.src %>/polymer.less",
119 122 "<%= dirs.css.dest %>/style-ipython.css": "<%= dirs.css.src %>/ipython.less"
120 123 }
121 124 },
122 125 "production": {
123 126 "options": {
124 127 "compress": true,
125 128 "yuicompress": true,
126 129 "optimization": 2
127 130 },
128 131 "files": {
129 132 "<%= dirs.css.dest %>/style.css": "<%= dirs.css.src %>/main.less",
130 133 "<%= dirs.css.dest %>/style-polymer.css": "<%= dirs.css.src %>/polymer.less",
131 134 "<%= dirs.css.dest %>/style-ipython.css": "<%= dirs.css.src %>/ipython.less"
132 135 }
133 136 },
134 137 "components": {
135 138 "files": [
136 139 {
137 140 "cwd": "<%= dirs.js.src %>/components/",
138 141 "dest": "<%= dirs.js.src %>/components/",
139 142 "src": [
140 143 "**/*.less"
141 144 ],
142 145 "expand": true,
143 146 "ext": ".css"
144 147 }
145 148 ]
146 149 }
147 150 },
148 151 "watch": {
149 152 "less": {
150 153 "files": [
151 154 "<%= dirs.css.src %>/**/*.less",
152 155 "<%= dirs.js.src %>/components/**/*.less"
153 156 ],
154 157 "tasks": [
155 158 "less:development",
156 159 "less:components",
157 160 "concat:polymercss",
158 161 "webpack",
159 162 "concat:dist"
160 163 ]
161 164 },
162 165 "js": {
163 166 "files": [
164 167 "!<%= dirs.js.src %>/components/root-styles.gen.html",
165 168 "<%= dirs.js.src %>/**/*.js",
166 169 "<%= dirs.js.src %>/components/**/*.html"
167 170 ],
168 171 "tasks": [
169 172 "less:components",
170 173 "concat:polymercss",
171 174 "webpack",
172 175 "concat:dist"
173 176 ]
174 177 }
175 178 },
176 179 "jshint": {
177 180 "rhodecode": {
178 181 "src": "<%= dirs.js.src %>/rhodecode/**/*.js",
179 182 "options": {
180 183 "jshintrc": ".jshintrc"
181 184 }
182 185 }
183 186 }
184 187 }
@@ -1,792 +1,793 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import difflib
23 23 from itertools import groupby
24 24
25 25 from pygments import lex
26 26 from pygments.formatters.html import _get_ttype_class as pygment_token_class
27 27 from pygments.lexers.special import TextLexer, Token
28 28 from pygments.lexers import get_lexer_by_name
29 29 from pyramid import compat
30 30
31 31 from rhodecode.lib.helpers import (
32 32 get_lexer_for_filenode, html_escape, get_custom_lexer)
33 33 from rhodecode.lib.utils2 import AttributeDict, StrictAttributeDict, safe_unicode
34 34 from rhodecode.lib.vcs.nodes import FileNode
35 35 from rhodecode.lib.vcs.exceptions import VCSError, NodeDoesNotExistError
36 36 from rhodecode.lib.diff_match_patch import diff_match_patch
37 37 from rhodecode.lib.diffs import LimitedDiffContainer, DEL_FILENODE, BIN_FILENODE
38 38
39 39
40 40 plain_text_lexer = get_lexer_by_name(
41 41 'text', stripall=False, stripnl=False, ensurenl=False)
42 42
43 43
44 44 log = logging.getLogger(__name__)
45 45
46 46
47 47 def filenode_as_lines_tokens(filenode, lexer=None):
48 48 org_lexer = lexer
49 49 lexer = lexer or get_lexer_for_filenode(filenode)
50 50 log.debug('Generating file node pygment tokens for %s, %s, org_lexer:%s',
51 51 lexer, filenode, org_lexer)
52 52 content = filenode.content
53 53 tokens = tokenize_string(content, lexer)
54 54 lines = split_token_stream(tokens, content)
55 55 rv = list(lines)
56 56 return rv
57 57
58 58
59 59 def tokenize_string(content, lexer):
60 60 """
61 61 Use pygments to tokenize some content based on a lexer
62 62 ensuring all original new lines and whitespace is preserved
63 63 """
64 64
65 65 lexer.stripall = False
66 66 lexer.stripnl = False
67 67 lexer.ensurenl = False
68 68
69 69 if isinstance(lexer, TextLexer):
70 70 lexed = [(Token.Text, content)]
71 71 else:
72 72 lexed = lex(content, lexer)
73 73
74 74 for token_type, token_text in lexed:
75 75 yield pygment_token_class(token_type), token_text
76 76
77 77
78 78 def split_token_stream(tokens, content):
79 79 """
80 80 Take a list of (TokenType, text) tuples and split them by a string
81 81
82 82 split_token_stream([(TEXT, 'some\ntext'), (TEXT, 'more\n')])
83 83 [(TEXT, 'some'), (TEXT, 'text'),
84 84 (TEXT, 'more'), (TEXT, 'text')]
85 85 """
86 86
87 87 token_buffer = []
88 88 for token_class, token_text in tokens:
89 89 parts = token_text.split('\n')
90 90 for part in parts[:-1]:
91 91 token_buffer.append((token_class, part))
92 92 yield token_buffer
93 93 token_buffer = []
94 94
95 95 token_buffer.append((token_class, parts[-1]))
96 96
97 97 if token_buffer:
98 98 yield token_buffer
99 99 elif content:
100 100 # this is a special case, we have the content, but tokenization didn't produce
101 101 # any results. THis can happen if know file extensions like .css have some bogus
102 102 # unicode content without any newline characters
103 103 yield [(pygment_token_class(Token.Text), content)]
104 104
105 105
106 106 def filenode_as_annotated_lines_tokens(filenode):
107 107 """
108 108 Take a file node and return a list of annotations => lines, if no annotation
109 109 is found, it will be None.
110 110
111 111 eg:
112 112
113 113 [
114 114 (annotation1, [
115 115 (1, line1_tokens_list),
116 116 (2, line2_tokens_list),
117 117 ]),
118 118 (annotation2, [
119 119 (3, line1_tokens_list),
120 120 ]),
121 121 (None, [
122 122 (4, line1_tokens_list),
123 123 ]),
124 124 (annotation1, [
125 125 (5, line1_tokens_list),
126 126 (6, line2_tokens_list),
127 127 ])
128 128 ]
129 129 """
130 130
131 131 commit_cache = {} # cache commit_getter lookups
132 132
133 133 def _get_annotation(commit_id, commit_getter):
134 134 if commit_id not in commit_cache:
135 135 commit_cache[commit_id] = commit_getter()
136 136 return commit_cache[commit_id]
137 137
138 138 annotation_lookup = {
139 139 line_no: _get_annotation(commit_id, commit_getter)
140 140 for line_no, commit_id, commit_getter, line_content
141 141 in filenode.annotate
142 142 }
143 143
144 144 annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens)
145 145 for line_no, tokens
146 146 in enumerate(filenode_as_lines_tokens(filenode), 1))
147 147
148 148 grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0])
149 149
150 150 for annotation, group in grouped_annotations_lines:
151 151 yield (
152 152 annotation, [(line_no, tokens)
153 153 for (_, line_no, tokens) in group]
154 154 )
155 155
156 156
157 157 def render_tokenstream(tokenstream):
158 158 result = []
159 159 for token_class, token_ops_texts in rollup_tokenstream(tokenstream):
160 160
161 161 if token_class:
162 162 result.append(u'<span class="%s">' % token_class)
163 163 else:
164 164 result.append(u'<span>')
165 165
166 166 for op_tag, token_text in token_ops_texts:
167 167
168 168 if op_tag:
169 169 result.append(u'<%s>' % op_tag)
170 170
171 171 # NOTE(marcink): in some cases of mixed encodings, we might run into
172 172 # troubles in the html_escape, in this case we say unicode force on token_text
173 173 # that would ensure "correct" data even with the cost of rendered
174 174 try:
175 175 escaped_text = html_escape(token_text)
176 176 except TypeError:
177 177 escaped_text = html_escape(safe_unicode(token_text))
178 178
179 179 # TODO: dan: investigate showing hidden characters like space/nl/tab
180 180 # escaped_text = escaped_text.replace(' ', '<sp> </sp>')
181 181 # escaped_text = escaped_text.replace('\n', '<nl>\n</nl>')
182 182 # escaped_text = escaped_text.replace('\t', '<tab>\t</tab>')
183 183
184 184 result.append(escaped_text)
185 185
186 186 if op_tag:
187 187 result.append(u'</%s>' % op_tag)
188 188
189 189 result.append(u'</span>')
190 190
191 191 html = ''.join(result)
192 192 return html
193 193
194 194
195 195 def rollup_tokenstream(tokenstream):
196 196 """
197 197 Group a token stream of the format:
198 198
199 199 ('class', 'op', 'text')
200 200 or
201 201 ('class', 'text')
202 202
203 203 into
204 204
205 205 [('class1',
206 206 [('op1', 'text'),
207 207 ('op2', 'text')]),
208 208 ('class2',
209 209 [('op3', 'text')])]
210 210
211 211 This is used to get the minimal tags necessary when
212 212 rendering to html eg for a token stream ie.
213 213
214 214 <span class="A"><ins>he</ins>llo</span>
215 215 vs
216 216 <span class="A"><ins>he</ins></span><span class="A">llo</span>
217 217
218 218 If a 2 tuple is passed in, the output op will be an empty string.
219 219
220 220 eg:
221 221
222 222 >>> rollup_tokenstream([('classA', '', 'h'),
223 223 ('classA', 'del', 'ell'),
224 224 ('classA', '', 'o'),
225 225 ('classB', '', ' '),
226 226 ('classA', '', 'the'),
227 227 ('classA', '', 're'),
228 228 ])
229 229
230 230 [('classA', [('', 'h'), ('del', 'ell'), ('', 'o')],
231 231 ('classB', [('', ' ')],
232 232 ('classA', [('', 'there')]]
233 233
234 234 """
235 235 if tokenstream and len(tokenstream[0]) == 2:
236 236 tokenstream = ((t[0], '', t[1]) for t in tokenstream)
237 237
238 238 result = []
239 239 for token_class, op_list in groupby(tokenstream, lambda t: t[0]):
240 240 ops = []
241 241 for token_op, token_text_list in groupby(op_list, lambda o: o[1]):
242 242 text_buffer = []
243 243 for t_class, t_op, t_text in token_text_list:
244 244 text_buffer.append(t_text)
245 245 ops.append((token_op, ''.join(text_buffer)))
246 246 result.append((token_class, ops))
247 247 return result
248 248
249 249
250 250 def tokens_diff(old_tokens, new_tokens, use_diff_match_patch=True):
251 251 """
252 252 Converts a list of (token_class, token_text) tuples to a list of
253 253 (token_class, token_op, token_text) tuples where token_op is one of
254 254 ('ins', 'del', '')
255 255
256 256 :param old_tokens: list of (token_class, token_text) tuples of old line
257 257 :param new_tokens: list of (token_class, token_text) tuples of new line
258 258 :param use_diff_match_patch: boolean, will use google's diff match patch
259 259 library which has options to 'smooth' out the character by character
260 260 differences making nicer ins/del blocks
261 261 """
262 262
263 263 old_tokens_result = []
264 264 new_tokens_result = []
265 265
266 266 similarity = difflib.SequenceMatcher(None,
267 267 ''.join(token_text for token_class, token_text in old_tokens),
268 268 ''.join(token_text for token_class, token_text in new_tokens)
269 269 ).ratio()
270 270
271 271 if similarity < 0.6: # return, the blocks are too different
272 272 for token_class, token_text in old_tokens:
273 273 old_tokens_result.append((token_class, '', token_text))
274 274 for token_class, token_text in new_tokens:
275 275 new_tokens_result.append((token_class, '', token_text))
276 276 return old_tokens_result, new_tokens_result, similarity
277 277
278 278 token_sequence_matcher = difflib.SequenceMatcher(None,
279 279 [x[1] for x in old_tokens],
280 280 [x[1] for x in new_tokens])
281 281
282 282 for tag, o1, o2, n1, n2 in token_sequence_matcher.get_opcodes():
283 283 # check the differences by token block types first to give a more
284 284 # nicer "block" level replacement vs character diffs
285 285
286 286 if tag == 'equal':
287 287 for token_class, token_text in old_tokens[o1:o2]:
288 288 old_tokens_result.append((token_class, '', token_text))
289 289 for token_class, token_text in new_tokens[n1:n2]:
290 290 new_tokens_result.append((token_class, '', token_text))
291 291 elif tag == 'delete':
292 292 for token_class, token_text in old_tokens[o1:o2]:
293 293 old_tokens_result.append((token_class, 'del', token_text))
294 294 elif tag == 'insert':
295 295 for token_class, token_text in new_tokens[n1:n2]:
296 296 new_tokens_result.append((token_class, 'ins', token_text))
297 297 elif tag == 'replace':
298 298 # if same type token blocks must be replaced, do a diff on the
299 299 # characters in the token blocks to show individual changes
300 300
301 301 old_char_tokens = []
302 302 new_char_tokens = []
303 303 for token_class, token_text in old_tokens[o1:o2]:
304 304 for char in token_text:
305 305 old_char_tokens.append((token_class, char))
306 306
307 307 for token_class, token_text in new_tokens[n1:n2]:
308 308 for char in token_text:
309 309 new_char_tokens.append((token_class, char))
310 310
311 311 old_string = ''.join([token_text for
312 312 token_class, token_text in old_char_tokens])
313 313 new_string = ''.join([token_text for
314 314 token_class, token_text in new_char_tokens])
315 315
316 316 char_sequence = difflib.SequenceMatcher(
317 317 None, old_string, new_string)
318 318 copcodes = char_sequence.get_opcodes()
319 319 obuffer, nbuffer = [], []
320 320
321 321 if use_diff_match_patch:
322 322 dmp = diff_match_patch()
323 323 dmp.Diff_EditCost = 11 # TODO: dan: extract this to a setting
324 324 reps = dmp.diff_main(old_string, new_string)
325 325 dmp.diff_cleanupEfficiency(reps)
326 326
327 327 a, b = 0, 0
328 328 for op, rep in reps:
329 329 l = len(rep)
330 330 if op == 0:
331 331 for i, c in enumerate(rep):
332 332 obuffer.append((old_char_tokens[a+i][0], '', c))
333 333 nbuffer.append((new_char_tokens[b+i][0], '', c))
334 334 a += l
335 335 b += l
336 336 elif op == -1:
337 337 for i, c in enumerate(rep):
338 338 obuffer.append((old_char_tokens[a+i][0], 'del', c))
339 339 a += l
340 340 elif op == 1:
341 341 for i, c in enumerate(rep):
342 342 nbuffer.append((new_char_tokens[b+i][0], 'ins', c))
343 343 b += l
344 344 else:
345 345 for ctag, co1, co2, cn1, cn2 in copcodes:
346 346 if ctag == 'equal':
347 347 for token_class, token_text in old_char_tokens[co1:co2]:
348 348 obuffer.append((token_class, '', token_text))
349 349 for token_class, token_text in new_char_tokens[cn1:cn2]:
350 350 nbuffer.append((token_class, '', token_text))
351 351 elif ctag == 'delete':
352 352 for token_class, token_text in old_char_tokens[co1:co2]:
353 353 obuffer.append((token_class, 'del', token_text))
354 354 elif ctag == 'insert':
355 355 for token_class, token_text in new_char_tokens[cn1:cn2]:
356 356 nbuffer.append((token_class, 'ins', token_text))
357 357 elif ctag == 'replace':
358 358 for token_class, token_text in old_char_tokens[co1:co2]:
359 359 obuffer.append((token_class, 'del', token_text))
360 360 for token_class, token_text in new_char_tokens[cn1:cn2]:
361 361 nbuffer.append((token_class, 'ins', token_text))
362 362
363 363 old_tokens_result.extend(obuffer)
364 364 new_tokens_result.extend(nbuffer)
365 365
366 366 return old_tokens_result, new_tokens_result, similarity
367 367
368 368
369 369 def diffset_node_getter(commit):
370 370 def get_node(fname):
371 371 try:
372 372 return commit.get_node(fname)
373 373 except NodeDoesNotExistError:
374 374 return None
375 375
376 376 return get_node
377 377
378 378
379 379 class DiffSet(object):
380 380 """
381 381 An object for parsing the diff result from diffs.DiffProcessor and
382 382 adding highlighting, side by side/unified renderings and line diffs
383 383 """
384 384
385 385 HL_REAL = 'REAL' # highlights using original file, slow
386 386 HL_FAST = 'FAST' # highlights using just the line, fast but not correct
387 387 # in the case of multiline code
388 388 HL_NONE = 'NONE' # no highlighting, fastest
389 389
390 390 def __init__(self, highlight_mode=HL_REAL, repo_name=None,
391 391 source_repo_name=None,
392 392 source_node_getter=lambda filename: None,
393 393 target_repo_name=None,
394 394 target_node_getter=lambda filename: None,
395 395 source_nodes=None, target_nodes=None,
396 396 # files over this size will use fast highlighting
397 397 max_file_size_limit=150 * 1024,
398 398 ):
399 399
400 400 self.highlight_mode = highlight_mode
401 401 self.highlighted_filenodes = {}
402 402 self.source_node_getter = source_node_getter
403 403 self.target_node_getter = target_node_getter
404 404 self.source_nodes = source_nodes or {}
405 405 self.target_nodes = target_nodes or {}
406 406 self.repo_name = repo_name
407 407 self.target_repo_name = target_repo_name or repo_name
408 408 self.source_repo_name = source_repo_name or repo_name
409 409 self.max_file_size_limit = max_file_size_limit
410 410
411 411 def render_patchset(self, patchset, source_ref=None, target_ref=None):
412 412 diffset = AttributeDict(dict(
413 413 lines_added=0,
414 414 lines_deleted=0,
415 415 changed_files=0,
416 416 files=[],
417 417 file_stats={},
418 418 limited_diff=isinstance(patchset, LimitedDiffContainer),
419 419 repo_name=self.repo_name,
420 420 target_repo_name=self.target_repo_name,
421 421 source_repo_name=self.source_repo_name,
422 422 source_ref=source_ref,
423 423 target_ref=target_ref,
424 424 ))
425 425 for patch in patchset:
426 426 diffset.file_stats[patch['filename']] = patch['stats']
427 427 filediff = self.render_patch(patch)
428 428 filediff.diffset = StrictAttributeDict(dict(
429 429 source_ref=diffset.source_ref,
430 430 target_ref=diffset.target_ref,
431 431 repo_name=diffset.repo_name,
432 432 source_repo_name=diffset.source_repo_name,
433 433 target_repo_name=diffset.target_repo_name,
434 434 ))
435 435 diffset.files.append(filediff)
436 436 diffset.changed_files += 1
437 437 if not patch['stats']['binary']:
438 438 diffset.lines_added += patch['stats']['added']
439 439 diffset.lines_deleted += patch['stats']['deleted']
440 440
441 441 return diffset
442 442
443 443 _lexer_cache = {}
444 444
445 445 def _get_lexer_for_filename(self, filename, filenode=None):
446 446 # cached because we might need to call it twice for source/target
447 447 if filename not in self._lexer_cache:
448 448 if filenode:
449 449 lexer = filenode.lexer
450 450 extension = filenode.extension
451 451 else:
452 452 lexer = FileNode.get_lexer(filename=filename)
453 453 extension = filename.split('.')[-1]
454 454
455 455 lexer = get_custom_lexer(extension) or lexer
456 456 self._lexer_cache[filename] = lexer
457 457 return self._lexer_cache[filename]
458 458
459 459 def render_patch(self, patch):
460 460 log.debug('rendering diff for %r', patch['filename'])
461 461
462 462 source_filename = patch['original_filename']
463 463 target_filename = patch['filename']
464 464
465 465 source_lexer = plain_text_lexer
466 466 target_lexer = plain_text_lexer
467 467
468 468 if not patch['stats']['binary']:
469 469 node_hl_mode = self.HL_NONE if patch['chunks'] == [] else None
470 470 hl_mode = node_hl_mode or self.highlight_mode
471 471
472 472 if hl_mode == self.HL_REAL:
473 473 if (source_filename and patch['operation'] in ('D', 'M')
474 474 and source_filename not in self.source_nodes):
475 475 self.source_nodes[source_filename] = (
476 476 self.source_node_getter(source_filename))
477 477
478 478 if (target_filename and patch['operation'] in ('A', 'M')
479 479 and target_filename not in self.target_nodes):
480 480 self.target_nodes[target_filename] = (
481 481 self.target_node_getter(target_filename))
482 482
483 483 elif hl_mode == self.HL_FAST:
484 484 source_lexer = self._get_lexer_for_filename(source_filename)
485 485 target_lexer = self._get_lexer_for_filename(target_filename)
486 486
487 487 source_file = self.source_nodes.get(source_filename, source_filename)
488 488 target_file = self.target_nodes.get(target_filename, target_filename)
489 489 raw_id_uid = ''
490 490 if self.source_nodes.get(source_filename):
491 491 raw_id_uid = self.source_nodes[source_filename].commit.raw_id
492 492
493 493 if not raw_id_uid and self.target_nodes.get(target_filename):
494 494 # in case this is a new file we only have it in target
495 495 raw_id_uid = self.target_nodes[target_filename].commit.raw_id
496 496
497 497 source_filenode, target_filenode = None, None
498 498
499 499 # TODO: dan: FileNode.lexer works on the content of the file - which
500 500 # can be slow - issue #4289 explains a lexer clean up - which once
501 501 # done can allow caching a lexer for a filenode to avoid the file lookup
502 502 if isinstance(source_file, FileNode):
503 503 source_filenode = source_file
504 504 #source_lexer = source_file.lexer
505 505 source_lexer = self._get_lexer_for_filename(source_filename)
506 506 source_file.lexer = source_lexer
507 507
508 508 if isinstance(target_file, FileNode):
509 509 target_filenode = target_file
510 510 #target_lexer = target_file.lexer
511 511 target_lexer = self._get_lexer_for_filename(target_filename)
512 512 target_file.lexer = target_lexer
513 513
514 514 source_file_path, target_file_path = None, None
515 515
516 516 if source_filename != '/dev/null':
517 517 source_file_path = source_filename
518 518 if target_filename != '/dev/null':
519 519 target_file_path = target_filename
520 520
521 521 source_file_type = source_lexer.name
522 522 target_file_type = target_lexer.name
523 523
524 524 filediff = AttributeDict({
525 525 'source_file_path': source_file_path,
526 526 'target_file_path': target_file_path,
527 527 'source_filenode': source_filenode,
528 528 'target_filenode': target_filenode,
529 529 'source_file_type': target_file_type,
530 530 'target_file_type': source_file_type,
531 531 'patch': {'filename': patch['filename'], 'stats': patch['stats']},
532 532 'operation': patch['operation'],
533 533 'source_mode': patch['stats']['old_mode'],
534 534 'target_mode': patch['stats']['new_mode'],
535 535 'limited_diff': patch['is_limited_diff'],
536 536 'hunks': [],
537 537 'hunk_ops': None,
538 538 'diffset': self,
539 539 'raw_id': raw_id_uid,
540 540 })
541 541
542 542 file_chunks = patch['chunks'][1:]
543 for hunk in file_chunks:
543 for i, hunk in enumerate(file_chunks, 1):
544 544 hunkbit = self.parse_hunk(hunk, source_file, target_file)
545 545 hunkbit.source_file_path = source_file_path
546 546 hunkbit.target_file_path = target_file_path
547 hunkbit.index = i
547 548 filediff.hunks.append(hunkbit)
548 549
549 550 # Simulate hunk on OPS type line which doesn't really contain any diff
550 551 # this allows commenting on those
551 552 if not file_chunks:
552 553 actions = []
553 554 for op_id, op_text in filediff.patch['stats']['ops'].items():
554 555 if op_id == DEL_FILENODE:
555 556 actions.append(u'file was removed')
556 557 elif op_id == BIN_FILENODE:
557 558 actions.append(u'binary diff hidden')
558 559 else:
559 560 actions.append(safe_unicode(op_text))
560 561 action_line = u'NO CONTENT: ' + \
561 562 u', '.join(actions) or u'UNDEFINED_ACTION'
562 563
563 564 hunk_ops = {'source_length': 0, 'source_start': 0,
564 565 'lines': [
565 566 {'new_lineno': 0, 'old_lineno': 1,
566 567 'action': 'unmod-no-hl', 'line': action_line}
567 568 ],
568 569 'section_header': u'', 'target_start': 1, 'target_length': 1}
569 570
570 571 hunkbit = self.parse_hunk(hunk_ops, source_file, target_file)
571 572 hunkbit.source_file_path = source_file_path
572 573 hunkbit.target_file_path = target_file_path
573 574 filediff.hunk_ops = hunkbit
574 575 return filediff
575 576
576 577 def parse_hunk(self, hunk, source_file, target_file):
577 578 result = AttributeDict(dict(
578 579 source_start=hunk['source_start'],
579 580 source_length=hunk['source_length'],
580 581 target_start=hunk['target_start'],
581 582 target_length=hunk['target_length'],
582 583 section_header=hunk['section_header'],
583 584 lines=[],
584 585 ))
585 586 before, after = [], []
586 587
587 588 for line in hunk['lines']:
588 589 if line['action'] in ['unmod', 'unmod-no-hl']:
589 590 no_hl = line['action'] == 'unmod-no-hl'
590 591 result.lines.extend(
591 592 self.parse_lines(before, after, source_file, target_file, no_hl=no_hl))
592 593 after.append(line)
593 594 before.append(line)
594 595 elif line['action'] == 'add':
595 596 after.append(line)
596 597 elif line['action'] == 'del':
597 598 before.append(line)
598 599 elif line['action'] == 'old-no-nl':
599 600 before.append(line)
600 601 elif line['action'] == 'new-no-nl':
601 602 after.append(line)
602 603
603 604 all_actions = [x['action'] for x in after] + [x['action'] for x in before]
604 605 no_hl = {x for x in all_actions} == {'unmod-no-hl'}
605 606 result.lines.extend(
606 607 self.parse_lines(before, after, source_file, target_file, no_hl=no_hl))
607 608 # NOTE(marcink): we must keep list() call here so we can cache the result...
608 609 result.unified = list(self.as_unified(result.lines))
609 610 result.sideside = result.lines
610 611
611 612 return result
612 613
613 614 def parse_lines(self, before_lines, after_lines, source_file, target_file,
614 615 no_hl=False):
615 616 # TODO: dan: investigate doing the diff comparison and fast highlighting
616 617 # on the entire before and after buffered block lines rather than by
617 618 # line, this means we can get better 'fast' highlighting if the context
618 619 # allows it - eg.
619 620 # line 4: """
620 621 # line 5: this gets highlighted as a string
621 622 # line 6: """
622 623
623 624 lines = []
624 625
625 626 before_newline = AttributeDict()
626 627 after_newline = AttributeDict()
627 628 if before_lines and before_lines[-1]['action'] == 'old-no-nl':
628 629 before_newline_line = before_lines.pop(-1)
629 630 before_newline.content = '\n {}'.format(
630 631 render_tokenstream(
631 632 [(x[0], '', x[1])
632 633 for x in [('nonl', before_newline_line['line'])]]))
633 634
634 635 if after_lines and after_lines[-1]['action'] == 'new-no-nl':
635 636 after_newline_line = after_lines.pop(-1)
636 637 after_newline.content = '\n {}'.format(
637 638 render_tokenstream(
638 639 [(x[0], '', x[1])
639 640 for x in [('nonl', after_newline_line['line'])]]))
640 641
641 642 while before_lines or after_lines:
642 643 before, after = None, None
643 644 before_tokens, after_tokens = None, None
644 645
645 646 if before_lines:
646 647 before = before_lines.pop(0)
647 648 if after_lines:
648 649 after = after_lines.pop(0)
649 650
650 651 original = AttributeDict()
651 652 modified = AttributeDict()
652 653
653 654 if before:
654 655 if before['action'] == 'old-no-nl':
655 656 before_tokens = [('nonl', before['line'])]
656 657 else:
657 658 before_tokens = self.get_line_tokens(
658 659 line_text=before['line'], line_number=before['old_lineno'],
659 660 input_file=source_file, no_hl=no_hl)
660 661 original.lineno = before['old_lineno']
661 662 original.content = before['line']
662 663 original.action = self.action_to_op(before['action'])
663 664
664 665 original.get_comment_args = (
665 666 source_file, 'o', before['old_lineno'])
666 667
667 668 if after:
668 669 if after['action'] == 'new-no-nl':
669 670 after_tokens = [('nonl', after['line'])]
670 671 else:
671 672 after_tokens = self.get_line_tokens(
672 673 line_text=after['line'], line_number=after['new_lineno'],
673 674 input_file=target_file, no_hl=no_hl)
674 675 modified.lineno = after['new_lineno']
675 676 modified.content = after['line']
676 677 modified.action = self.action_to_op(after['action'])
677 678
678 679 modified.get_comment_args = (target_file, 'n', after['new_lineno'])
679 680
680 681 # diff the lines
681 682 if before_tokens and after_tokens:
682 683 o_tokens, m_tokens, similarity = tokens_diff(
683 684 before_tokens, after_tokens)
684 685 original.content = render_tokenstream(o_tokens)
685 686 modified.content = render_tokenstream(m_tokens)
686 687 elif before_tokens:
687 688 original.content = render_tokenstream(
688 689 [(x[0], '', x[1]) for x in before_tokens])
689 690 elif after_tokens:
690 691 modified.content = render_tokenstream(
691 692 [(x[0], '', x[1]) for x in after_tokens])
692 693
693 694 if not before_lines and before_newline:
694 695 original.content += before_newline.content
695 696 before_newline = None
696 697 if not after_lines and after_newline:
697 698 modified.content += after_newline.content
698 699 after_newline = None
699 700
700 701 lines.append(AttributeDict({
701 702 'original': original,
702 703 'modified': modified,
703 704 }))
704 705
705 706 return lines
706 707
707 708 def get_line_tokens(self, line_text, line_number, input_file=None, no_hl=False):
708 709 filenode = None
709 710 filename = None
710 711
711 712 if isinstance(input_file, compat.string_types):
712 713 filename = input_file
713 714 elif isinstance(input_file, FileNode):
714 715 filenode = input_file
715 716 filename = input_file.unicode_path
716 717
717 718 hl_mode = self.HL_NONE if no_hl else self.highlight_mode
718 719 if hl_mode == self.HL_REAL and filenode:
719 720 lexer = self._get_lexer_for_filename(filename)
720 721 file_size_allowed = input_file.size < self.max_file_size_limit
721 722 if line_number and file_size_allowed:
722 723 return self.get_tokenized_filenode_line(
723 724 input_file, line_number, lexer)
724 725
725 726 if hl_mode in (self.HL_REAL, self.HL_FAST) and filename:
726 727 lexer = self._get_lexer_for_filename(filename)
727 728 return list(tokenize_string(line_text, lexer))
728 729
729 730 return list(tokenize_string(line_text, plain_text_lexer))
730 731
731 732 def get_tokenized_filenode_line(self, filenode, line_number, lexer=None):
732 733
733 734 if filenode not in self.highlighted_filenodes:
734 735 tokenized_lines = filenode_as_lines_tokens(filenode, lexer)
735 736 self.highlighted_filenodes[filenode] = tokenized_lines
736 737
737 738 try:
738 739 return self.highlighted_filenodes[filenode][line_number - 1]
739 740 except Exception:
740 741 return [('', u'rhodecode diff rendering error')]
741 742
742 743 def action_to_op(self, action):
743 744 return {
744 745 'add': '+',
745 746 'del': '-',
746 747 'unmod': ' ',
747 748 'unmod-no-hl': ' ',
748 749 'old-no-nl': ' ',
749 750 'new-no-nl': ' ',
750 751 }.get(action, action)
751 752
752 753 def as_unified(self, lines):
753 754 """
754 755 Return a generator that yields the lines of a diff in unified order
755 756 """
756 757 def generator():
757 758 buf = []
758 759 for line in lines:
759 760
760 761 if buf and not line.original or line.original.action == ' ':
761 762 for b in buf:
762 763 yield b
763 764 buf = []
764 765
765 766 if line.original:
766 767 if line.original.action == ' ':
767 768 yield (line.original.lineno, line.modified.lineno,
768 769 line.original.action, line.original.content,
769 770 line.original.get_comment_args)
770 771 continue
771 772
772 773 if line.original.action == '-':
773 774 yield (line.original.lineno, None,
774 775 line.original.action, line.original.content,
775 776 line.original.get_comment_args)
776 777
777 778 if line.modified.action == '+':
778 779 buf.append((
779 780 None, line.modified.lineno,
780 781 line.modified.action, line.modified.content,
781 782 line.modified.get_comment_args))
782 783 continue
783 784
784 785 if line.modified:
785 786 yield (None, line.modified.lineno,
786 787 line.modified.action, line.modified.content,
787 788 line.modified.get_comment_args)
788 789
789 790 for b in buf:
790 791 yield b
791 792
792 793 return generator()
@@ -1,2041 +1,2041 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Helper functions
23 23
24 24 Consists of functions to typically be used within templates, but also
25 25 available to Controllers. This module is available to both as 'h'.
26 26 """
27 27 import base64
28 28
29 29 import os
30 30 import random
31 31 import hashlib
32 32 import StringIO
33 33 import textwrap
34 34 import urllib
35 35 import math
36 36 import logging
37 37 import re
38 38 import time
39 39 import string
40 40 import hashlib
41 41 from collections import OrderedDict
42 42
43 43 import pygments
44 44 import itertools
45 45 import fnmatch
46 46 import bleach
47 47
48 48 from pyramid import compat
49 49 from datetime import datetime
50 50 from functools import partial
51 51 from pygments.formatters.html import HtmlFormatter
52 52 from pygments.lexers import (
53 53 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
54 54
55 55 from pyramid.threadlocal import get_current_request
56
56 from tempita import looper
57 57 from webhelpers2.html import literal, HTML, escape
58 58 from webhelpers2.html._autolink import _auto_link_urls
59 59 from webhelpers2.html.tools import (
60 60 button_to, highlight, js_obfuscate, strip_links, strip_tags)
61 61
62 62 from webhelpers2.text import (
63 63 chop_at, collapse, convert_accented_entities,
64 64 convert_misc_entities, lchop, plural, rchop, remove_formatting,
65 65 replace_whitespace, urlify, truncate, wrap_paragraphs)
66 66 from webhelpers2.date import time_ago_in_words
67 67
68 68 from webhelpers2.html.tags import (
69 69 _input, NotGiven, _make_safe_id_component as safeid,
70 70 form as insecure_form,
71 71 auto_discovery_link, checkbox, end_form, file,
72 72 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
73 73 select as raw_select, stylesheet_link, submit, text, password, textarea,
74 74 ul, radio, Options)
75 75
76 76 from webhelpers2.number import format_byte_size
77 77
78 78 from rhodecode.lib.action_parser import action_parser
79 79 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
80 80 from rhodecode.lib.ext_json import json
81 81 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
82 82 from rhodecode.lib.utils2 import (
83 83 str2bool, safe_unicode, safe_str,
84 84 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
85 85 AttributeDict, safe_int, md5, md5_safe, get_host_info)
86 86 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
87 87 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
88 88 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
89 89 from rhodecode.lib.index.search_utils import get_matching_line_offsets
90 90 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
91 91 from rhodecode.model.changeset_status import ChangesetStatusModel
92 92 from rhodecode.model.db import Permission, User, Repository, UserApiKeys
93 93 from rhodecode.model.repo_group import RepoGroupModel
94 94 from rhodecode.model.settings import IssueTrackerSettingsModel
95 95
96 96
97 97 log = logging.getLogger(__name__)
98 98
99 99
100 100 DEFAULT_USER = User.DEFAULT_USER
101 101 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
102 102
103 103
104 104 def asset(path, ver=None, **kwargs):
105 105 """
106 106 Helper to generate a static asset file path for rhodecode assets
107 107
108 108 eg. h.asset('images/image.png', ver='3923')
109 109
110 110 :param path: path of asset
111 111 :param ver: optional version query param to append as ?ver=
112 112 """
113 113 request = get_current_request()
114 114 query = {}
115 115 query.update(kwargs)
116 116 if ver:
117 117 query = {'ver': ver}
118 118 return request.static_path(
119 119 'rhodecode:public/{}'.format(path), _query=query)
120 120
121 121
122 122 default_html_escape_table = {
123 123 ord('&'): u'&amp;',
124 124 ord('<'): u'&lt;',
125 125 ord('>'): u'&gt;',
126 126 ord('"'): u'&quot;',
127 127 ord("'"): u'&#39;',
128 128 }
129 129
130 130
131 131 def html_escape(text, html_escape_table=default_html_escape_table):
132 132 """Produce entities within text."""
133 133 return text.translate(html_escape_table)
134 134
135 135
136 136 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
137 137 """
138 138 Truncate string ``s`` at the first occurrence of ``sub``.
139 139
140 140 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
141 141 """
142 142 suffix_if_chopped = suffix_if_chopped or ''
143 143 pos = s.find(sub)
144 144 if pos == -1:
145 145 return s
146 146
147 147 if inclusive:
148 148 pos += len(sub)
149 149
150 150 chopped = s[:pos]
151 151 left = s[pos:].strip()
152 152
153 153 if left and suffix_if_chopped:
154 154 chopped += suffix_if_chopped
155 155
156 156 return chopped
157 157
158 158
159 159 def shorter(text, size=20, prefix=False):
160 160 postfix = '...'
161 161 if len(text) > size:
162 162 if prefix:
163 163 # shorten in front
164 164 return postfix + text[-(size - len(postfix)):]
165 165 else:
166 166 return text[:size - len(postfix)] + postfix
167 167 return text
168 168
169 169
170 170 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
171 171 """
172 172 Reset button
173 173 """
174 174 return _input(type, name, value, id, attrs)
175 175
176 176
177 177 def select(name, selected_values, options, id=NotGiven, **attrs):
178 178
179 179 if isinstance(options, (list, tuple)):
180 180 options_iter = options
181 181 # Handle old value,label lists ... where value also can be value,label lists
182 182 options = Options()
183 183 for opt in options_iter:
184 184 if isinstance(opt, tuple) and len(opt) == 2:
185 185 value, label = opt
186 186 elif isinstance(opt, basestring):
187 187 value = label = opt
188 188 else:
189 189 raise ValueError('invalid select option type %r' % type(opt))
190 190
191 191 if isinstance(value, (list, tuple)):
192 192 option_group = options.add_optgroup(label)
193 193 for opt2 in value:
194 194 if isinstance(opt2, tuple) and len(opt2) == 2:
195 195 group_value, group_label = opt2
196 196 elif isinstance(opt2, basestring):
197 197 group_value = group_label = opt2
198 198 else:
199 199 raise ValueError('invalid select option type %r' % type(opt2))
200 200
201 201 option_group.add_option(group_label, group_value)
202 202 else:
203 203 options.add_option(label, value)
204 204
205 205 return raw_select(name, selected_values, options, id=id, **attrs)
206 206
207 207
208 208 def branding(name, length=40):
209 209 return truncate(name, length, indicator="")
210 210
211 211
212 212 def FID(raw_id, path):
213 213 """
214 214 Creates a unique ID for filenode based on it's hash of path and commit
215 215 it's safe to use in urls
216 216
217 217 :param raw_id:
218 218 :param path:
219 219 """
220 220
221 221 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
222 222
223 223
224 224 class _GetError(object):
225 225 """Get error from form_errors, and represent it as span wrapped error
226 226 message
227 227
228 228 :param field_name: field to fetch errors for
229 229 :param form_errors: form errors dict
230 230 """
231 231
232 232 def __call__(self, field_name, form_errors):
233 233 tmpl = """<span class="error_msg">%s</span>"""
234 234 if form_errors and field_name in form_errors:
235 235 return literal(tmpl % form_errors.get(field_name))
236 236
237 237
238 238 get_error = _GetError()
239 239
240 240
241 241 class _ToolTip(object):
242 242
243 243 def __call__(self, tooltip_title, trim_at=50):
244 244 """
245 245 Special function just to wrap our text into nice formatted
246 246 autowrapped text
247 247
248 248 :param tooltip_title:
249 249 """
250 250 tooltip_title = escape(tooltip_title)
251 251 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
252 252 return tooltip_title
253 253
254 254
255 255 tooltip = _ToolTip()
256 256
257 257 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy file path"></i>'
258 258
259 259
260 260 def files_breadcrumbs(repo_name, repo_type, commit_id, file_path, landing_ref_name=None, at_ref=None,
261 261 limit_items=False, linkify_last_item=False, hide_last_item=False,
262 262 copy_path_icon=True):
263 263 if isinstance(file_path, str):
264 264 file_path = safe_unicode(file_path)
265 265
266 266 if at_ref:
267 267 route_qry = {'at': at_ref}
268 268 default_landing_ref = at_ref or landing_ref_name or commit_id
269 269 else:
270 270 route_qry = None
271 271 default_landing_ref = commit_id
272 272
273 273 # first segment is a `HOME` link to repo files root location
274 274 root_name = literal(u'<i class="icon-home"></i>')
275 275
276 276 url_segments = [
277 277 link_to(
278 278 root_name,
279 279 repo_files_by_ref_url(
280 280 repo_name,
281 281 repo_type,
282 282 f_path=None, # None here is a special case for SVN repos,
283 283 # that won't prefix with a ref
284 284 ref_name=default_landing_ref,
285 285 commit_id=commit_id,
286 286 query=route_qry
287 287 )
288 288 )]
289 289
290 290 path_segments = file_path.split('/')
291 291 last_cnt = len(path_segments) - 1
292 292 for cnt, segment in enumerate(path_segments):
293 293 if not segment:
294 294 continue
295 295 segment_html = escape(segment)
296 296
297 297 last_item = cnt == last_cnt
298 298
299 299 if last_item and hide_last_item:
300 300 # iterate over and hide last element
301 301 continue
302 302
303 303 if last_item and linkify_last_item is False:
304 304 # plain version
305 305 url_segments.append(segment_html)
306 306 else:
307 307 url_segments.append(
308 308 link_to(
309 309 segment_html,
310 310 repo_files_by_ref_url(
311 311 repo_name,
312 312 repo_type,
313 313 f_path='/'.join(path_segments[:cnt + 1]),
314 314 ref_name=default_landing_ref,
315 315 commit_id=commit_id,
316 316 query=route_qry
317 317 ),
318 318 ))
319 319
320 320 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
321 321 if limit_items and len(limited_url_segments) < len(url_segments):
322 322 url_segments = limited_url_segments
323 323
324 324 full_path = file_path
325 325 if copy_path_icon:
326 326 icon = files_icon.format(escape(full_path))
327 327 else:
328 328 icon = ''
329 329
330 330 if file_path == '':
331 331 return root_name
332 332 else:
333 333 return literal(' / '.join(url_segments) + icon)
334 334
335 335
336 336 def files_url_data(request):
337 337 matchdict = request.matchdict
338 338
339 339 if 'f_path' not in matchdict:
340 340 matchdict['f_path'] = ''
341 341
342 342 if 'commit_id' not in matchdict:
343 343 matchdict['commit_id'] = 'tip'
344 344
345 345 return json.dumps(matchdict)
346 346
347 347
348 348 def repo_files_by_ref_url(db_repo_name, db_repo_type, f_path, ref_name, commit_id, query=None, ):
349 349 _is_svn = is_svn(db_repo_type)
350 350 final_f_path = f_path
351 351
352 352 if _is_svn:
353 353 """
354 354 For SVN the ref_name cannot be used as a commit_id, it needs to be prefixed with
355 355 actually commit_id followed by the ref_name. This should be done only in case
356 356 This is a initial landing url, without additional paths.
357 357
358 358 like: /1000/tags/1.0.0/?at=tags/1.0.0
359 359 """
360 360
361 361 if ref_name and ref_name != 'tip':
362 362 # NOTE(marcink): for svn the ref_name is actually the stored path, so we prefix it
363 363 # for SVN we only do this magic prefix if it's root, .eg landing revision
364 364 # of files link. If we are in the tree we don't need this since we traverse the url
365 365 # that has everything stored
366 366 if f_path in ['', '/']:
367 367 final_f_path = '/'.join([ref_name, f_path])
368 368
369 369 # SVN always needs a commit_id explicitly, without a named REF
370 370 default_commit_id = commit_id
371 371 else:
372 372 """
373 373 For git and mercurial we construct a new URL using the names instead of commit_id
374 374 like: /master/some_path?at=master
375 375 """
376 376 # We currently do not support branches with slashes
377 377 if '/' in ref_name:
378 378 default_commit_id = commit_id
379 379 else:
380 380 default_commit_id = ref_name
381 381
382 382 # sometimes we pass f_path as None, to indicate explicit no prefix,
383 383 # we translate it to string to not have None
384 384 final_f_path = final_f_path or ''
385 385
386 386 files_url = route_path(
387 387 'repo_files',
388 388 repo_name=db_repo_name,
389 389 commit_id=default_commit_id,
390 390 f_path=final_f_path,
391 391 _query=query
392 392 )
393 393 return files_url
394 394
395 395
396 396 def code_highlight(code, lexer, formatter, use_hl_filter=False):
397 397 """
398 398 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
399 399
400 400 If ``outfile`` is given and a valid file object (an object
401 401 with a ``write`` method), the result will be written to it, otherwise
402 402 it is returned as a string.
403 403 """
404 404 if use_hl_filter:
405 405 # add HL filter
406 406 from rhodecode.lib.index import search_utils
407 407 lexer.add_filter(search_utils.ElasticSearchHLFilter())
408 408 return pygments.format(pygments.lex(code, lexer), formatter)
409 409
410 410
411 411 class CodeHtmlFormatter(HtmlFormatter):
412 412 """
413 413 My code Html Formatter for source codes
414 414 """
415 415
416 416 def wrap(self, source, outfile):
417 417 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
418 418
419 419 def _wrap_code(self, source):
420 420 for cnt, it in enumerate(source):
421 421 i, t = it
422 422 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
423 423 yield i, t
424 424
425 425 def _wrap_tablelinenos(self, inner):
426 426 dummyoutfile = StringIO.StringIO()
427 427 lncount = 0
428 428 for t, line in inner:
429 429 if t:
430 430 lncount += 1
431 431 dummyoutfile.write(line)
432 432
433 433 fl = self.linenostart
434 434 mw = len(str(lncount + fl - 1))
435 435 sp = self.linenospecial
436 436 st = self.linenostep
437 437 la = self.lineanchors
438 438 aln = self.anchorlinenos
439 439 nocls = self.noclasses
440 440 if sp:
441 441 lines = []
442 442
443 443 for i in range(fl, fl + lncount):
444 444 if i % st == 0:
445 445 if i % sp == 0:
446 446 if aln:
447 447 lines.append('<a href="#%s%d" class="special">%*d</a>' %
448 448 (la, i, mw, i))
449 449 else:
450 450 lines.append('<span class="special">%*d</span>' % (mw, i))
451 451 else:
452 452 if aln:
453 453 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
454 454 else:
455 455 lines.append('%*d' % (mw, i))
456 456 else:
457 457 lines.append('')
458 458 ls = '\n'.join(lines)
459 459 else:
460 460 lines = []
461 461 for i in range(fl, fl + lncount):
462 462 if i % st == 0:
463 463 if aln:
464 464 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
465 465 else:
466 466 lines.append('%*d' % (mw, i))
467 467 else:
468 468 lines.append('')
469 469 ls = '\n'.join(lines)
470 470
471 471 # in case you wonder about the seemingly redundant <div> here: since the
472 472 # content in the other cell also is wrapped in a div, some browsers in
473 473 # some configurations seem to mess up the formatting...
474 474 if nocls:
475 475 yield 0, ('<table class="%stable">' % self.cssclass +
476 476 '<tr><td><div class="linenodiv" '
477 477 'style="background-color: #f0f0f0; padding-right: 10px">'
478 478 '<pre style="line-height: 125%">' +
479 479 ls + '</pre></div></td><td id="hlcode" class="code">')
480 480 else:
481 481 yield 0, ('<table class="%stable">' % self.cssclass +
482 482 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
483 483 ls + '</pre></div></td><td id="hlcode" class="code">')
484 484 yield 0, dummyoutfile.getvalue()
485 485 yield 0, '</td></tr></table>'
486 486
487 487
488 488 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
489 489 def __init__(self, **kw):
490 490 # only show these line numbers if set
491 491 self.only_lines = kw.pop('only_line_numbers', [])
492 492 self.query_terms = kw.pop('query_terms', [])
493 493 self.max_lines = kw.pop('max_lines', 5)
494 494 self.line_context = kw.pop('line_context', 3)
495 495 self.url = kw.pop('url', None)
496 496
497 497 super(CodeHtmlFormatter, self).__init__(**kw)
498 498
499 499 def _wrap_code(self, source):
500 500 for cnt, it in enumerate(source):
501 501 i, t = it
502 502 t = '<pre>%s</pre>' % t
503 503 yield i, t
504 504
505 505 def _wrap_tablelinenos(self, inner):
506 506 yield 0, '<table class="code-highlight %stable">' % self.cssclass
507 507
508 508 last_shown_line_number = 0
509 509 current_line_number = 1
510 510
511 511 for t, line in inner:
512 512 if not t:
513 513 yield t, line
514 514 continue
515 515
516 516 if current_line_number in self.only_lines:
517 517 if last_shown_line_number + 1 != current_line_number:
518 518 yield 0, '<tr>'
519 519 yield 0, '<td class="line">...</td>'
520 520 yield 0, '<td id="hlcode" class="code"></td>'
521 521 yield 0, '</tr>'
522 522
523 523 yield 0, '<tr>'
524 524 if self.url:
525 525 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
526 526 self.url, current_line_number, current_line_number)
527 527 else:
528 528 yield 0, '<td class="line"><a href="">%i</a></td>' % (
529 529 current_line_number)
530 530 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
531 531 yield 0, '</tr>'
532 532
533 533 last_shown_line_number = current_line_number
534 534
535 535 current_line_number += 1
536 536
537 537 yield 0, '</table>'
538 538
539 539
540 540 def hsv_to_rgb(h, s, v):
541 541 """ Convert hsv color values to rgb """
542 542
543 543 if s == 0.0:
544 544 return v, v, v
545 545 i = int(h * 6.0) # XXX assume int() truncates!
546 546 f = (h * 6.0) - i
547 547 p = v * (1.0 - s)
548 548 q = v * (1.0 - s * f)
549 549 t = v * (1.0 - s * (1.0 - f))
550 550 i = i % 6
551 551 if i == 0:
552 552 return v, t, p
553 553 if i == 1:
554 554 return q, v, p
555 555 if i == 2:
556 556 return p, v, t
557 557 if i == 3:
558 558 return p, q, v
559 559 if i == 4:
560 560 return t, p, v
561 561 if i == 5:
562 562 return v, p, q
563 563
564 564
565 565 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
566 566 """
567 567 Generator for getting n of evenly distributed colors using
568 568 hsv color and golden ratio. It always return same order of colors
569 569
570 570 :param n: number of colors to generate
571 571 :param saturation: saturation of returned colors
572 572 :param lightness: lightness of returned colors
573 573 :returns: RGB tuple
574 574 """
575 575
576 576 golden_ratio = 0.618033988749895
577 577 h = 0.22717784590367374
578 578
579 579 for _ in xrange(n):
580 580 h += golden_ratio
581 581 h %= 1
582 582 HSV_tuple = [h, saturation, lightness]
583 583 RGB_tuple = hsv_to_rgb(*HSV_tuple)
584 584 yield map(lambda x: str(int(x * 256)), RGB_tuple)
585 585
586 586
587 587 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
588 588 """
589 589 Returns a function which when called with an argument returns a unique
590 590 color for that argument, eg.
591 591
592 592 :param n: number of colors to generate
593 593 :param saturation: saturation of returned colors
594 594 :param lightness: lightness of returned colors
595 595 :returns: css RGB string
596 596
597 597 >>> color_hash = color_hasher()
598 598 >>> color_hash('hello')
599 599 'rgb(34, 12, 59)'
600 600 >>> color_hash('hello')
601 601 'rgb(34, 12, 59)'
602 602 >>> color_hash('other')
603 603 'rgb(90, 224, 159)'
604 604 """
605 605
606 606 color_dict = {}
607 607 cgenerator = unique_color_generator(
608 608 saturation=saturation, lightness=lightness)
609 609
610 610 def get_color_string(thing):
611 611 if thing in color_dict:
612 612 col = color_dict[thing]
613 613 else:
614 614 col = color_dict[thing] = cgenerator.next()
615 615 return "rgb(%s)" % (', '.join(col))
616 616
617 617 return get_color_string
618 618
619 619
620 620 def get_lexer_safe(mimetype=None, filepath=None):
621 621 """
622 622 Tries to return a relevant pygments lexer using mimetype/filepath name,
623 623 defaulting to plain text if none could be found
624 624 """
625 625 lexer = None
626 626 try:
627 627 if mimetype:
628 628 lexer = get_lexer_for_mimetype(mimetype)
629 629 if not lexer:
630 630 lexer = get_lexer_for_filename(filepath)
631 631 except pygments.util.ClassNotFound:
632 632 pass
633 633
634 634 if not lexer:
635 635 lexer = get_lexer_by_name('text')
636 636
637 637 return lexer
638 638
639 639
640 640 def get_lexer_for_filenode(filenode):
641 641 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
642 642 return lexer
643 643
644 644
645 645 def pygmentize(filenode, **kwargs):
646 646 """
647 647 pygmentize function using pygments
648 648
649 649 :param filenode:
650 650 """
651 651 lexer = get_lexer_for_filenode(filenode)
652 652 return literal(code_highlight(filenode.content, lexer,
653 653 CodeHtmlFormatter(**kwargs)))
654 654
655 655
656 656 def is_following_repo(repo_name, user_id):
657 657 from rhodecode.model.scm import ScmModel
658 658 return ScmModel().is_following_repo(repo_name, user_id)
659 659
660 660
661 661 class _Message(object):
662 662 """A message returned by ``Flash.pop_messages()``.
663 663
664 664 Converting the message to a string returns the message text. Instances
665 665 also have the following attributes:
666 666
667 667 * ``message``: the message text.
668 668 * ``category``: the category specified when the message was created.
669 669 """
670 670
671 671 def __init__(self, category, message, sub_data=None):
672 672 self.category = category
673 673 self.message = message
674 674 self.sub_data = sub_data or {}
675 675
676 676 def __str__(self):
677 677 return self.message
678 678
679 679 __unicode__ = __str__
680 680
681 681 def __html__(self):
682 682 return escape(safe_unicode(self.message))
683 683
684 684
685 685 class Flash(object):
686 686 # List of allowed categories. If None, allow any category.
687 687 categories = ["warning", "notice", "error", "success"]
688 688
689 689 # Default category if none is specified.
690 690 default_category = "notice"
691 691
692 692 def __init__(self, session_key="flash", categories=None,
693 693 default_category=None):
694 694 """
695 695 Instantiate a ``Flash`` object.
696 696
697 697 ``session_key`` is the key to save the messages under in the user's
698 698 session.
699 699
700 700 ``categories`` is an optional list which overrides the default list
701 701 of categories.
702 702
703 703 ``default_category`` overrides the default category used for messages
704 704 when none is specified.
705 705 """
706 706 self.session_key = session_key
707 707 if categories is not None:
708 708 self.categories = categories
709 709 if default_category is not None:
710 710 self.default_category = default_category
711 711 if self.categories and self.default_category not in self.categories:
712 712 raise ValueError(
713 713 "unrecognized default category %r" % (self.default_category,))
714 714
715 715 def pop_messages(self, session=None, request=None):
716 716 """
717 717 Return all accumulated messages and delete them from the session.
718 718
719 719 The return value is a list of ``Message`` objects.
720 720 """
721 721 messages = []
722 722
723 723 if not session:
724 724 if not request:
725 725 request = get_current_request()
726 726 session = request.session
727 727
728 728 # Pop the 'old' pylons flash messages. They are tuples of the form
729 729 # (category, message)
730 730 for cat, msg in session.pop(self.session_key, []):
731 731 messages.append(_Message(cat, msg))
732 732
733 733 # Pop the 'new' pyramid flash messages for each category as list
734 734 # of strings.
735 735 for cat in self.categories:
736 736 for msg in session.pop_flash(queue=cat):
737 737 sub_data = {}
738 738 if hasattr(msg, 'rsplit'):
739 739 flash_data = msg.rsplit('|DELIM|', 1)
740 740 org_message = flash_data[0]
741 741 if len(flash_data) > 1:
742 742 sub_data = json.loads(flash_data[1])
743 743 else:
744 744 org_message = msg
745 745
746 746 messages.append(_Message(cat, org_message, sub_data=sub_data))
747 747
748 748 # Map messages from the default queue to the 'notice' category.
749 749 for msg in session.pop_flash():
750 750 messages.append(_Message('notice', msg))
751 751
752 752 session.save()
753 753 return messages
754 754
755 755 def json_alerts(self, session=None, request=None):
756 756 payloads = []
757 757 messages = flash.pop_messages(session=session, request=request) or []
758 758 for message in messages:
759 759 payloads.append({
760 760 'message': {
761 761 'message': u'{}'.format(message.message),
762 762 'level': message.category,
763 763 'force': True,
764 764 'subdata': message.sub_data
765 765 }
766 766 })
767 767 return json.dumps(payloads)
768 768
769 769 def __call__(self, message, category=None, ignore_duplicate=True,
770 770 session=None, request=None):
771 771
772 772 if not session:
773 773 if not request:
774 774 request = get_current_request()
775 775 session = request.session
776 776
777 777 session.flash(
778 778 message, queue=category, allow_duplicate=not ignore_duplicate)
779 779
780 780
781 781 flash = Flash()
782 782
783 783 #==============================================================================
784 784 # SCM FILTERS available via h.
785 785 #==============================================================================
786 786 from rhodecode.lib.vcs.utils import author_name, author_email
787 787 from rhodecode.lib.utils2 import age, age_from_seconds
788 788 from rhodecode.model.db import User, ChangesetStatus
789 789
790 790
791 791 email = author_email
792 792
793 793
794 794 def capitalize(raw_text):
795 795 return raw_text.capitalize()
796 796
797 797
798 798 def short_id(long_id):
799 799 return long_id[:12]
800 800
801 801
802 802 def hide_credentials(url):
803 803 from rhodecode.lib.utils2 import credentials_filter
804 804 return credentials_filter(url)
805 805
806 806
807 807 import pytz
808 808 import tzlocal
809 809 local_timezone = tzlocal.get_localzone()
810 810
811 811
812 812 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
813 813 title = value or format_date(datetime_iso)
814 814 tzinfo = '+00:00'
815 815
816 816 # detect if we have a timezone info, otherwise, add it
817 817 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
818 818 force_timezone = os.environ.get('RC_TIMEZONE', '')
819 819 if force_timezone:
820 820 force_timezone = pytz.timezone(force_timezone)
821 821 timezone = force_timezone or local_timezone
822 822 offset = timezone.localize(datetime_iso).strftime('%z')
823 823 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
824 824
825 825 return literal(
826 826 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
827 827 cls='tooltip' if tooltip else '',
828 828 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
829 829 title=title, dt=datetime_iso, tzinfo=tzinfo
830 830 ))
831 831
832 832
833 833 def _shorten_commit_id(commit_id, commit_len=None):
834 834 if commit_len is None:
835 835 request = get_current_request()
836 836 commit_len = request.call_context.visual.show_sha_length
837 837 return commit_id[:commit_len]
838 838
839 839
840 840 def show_id(commit, show_idx=None, commit_len=None):
841 841 """
842 842 Configurable function that shows ID
843 843 by default it's r123:fffeeefffeee
844 844
845 845 :param commit: commit instance
846 846 """
847 847 if show_idx is None:
848 848 request = get_current_request()
849 849 show_idx = request.call_context.visual.show_revision_number
850 850
851 851 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
852 852 if show_idx:
853 853 return 'r%s:%s' % (commit.idx, raw_id)
854 854 else:
855 855 return '%s' % (raw_id, )
856 856
857 857
858 858 def format_date(date):
859 859 """
860 860 use a standardized formatting for dates used in RhodeCode
861 861
862 862 :param date: date/datetime object
863 863 :return: formatted date
864 864 """
865 865
866 866 if date:
867 867 _fmt = "%a, %d %b %Y %H:%M:%S"
868 868 return safe_unicode(date.strftime(_fmt))
869 869
870 870 return u""
871 871
872 872
873 873 class _RepoChecker(object):
874 874
875 875 def __init__(self, backend_alias):
876 876 self._backend_alias = backend_alias
877 877
878 878 def __call__(self, repository):
879 879 if hasattr(repository, 'alias'):
880 880 _type = repository.alias
881 881 elif hasattr(repository, 'repo_type'):
882 882 _type = repository.repo_type
883 883 else:
884 884 _type = repository
885 885 return _type == self._backend_alias
886 886
887 887
888 888 is_git = _RepoChecker('git')
889 889 is_hg = _RepoChecker('hg')
890 890 is_svn = _RepoChecker('svn')
891 891
892 892
893 893 def get_repo_type_by_name(repo_name):
894 894 repo = Repository.get_by_repo_name(repo_name)
895 895 if repo:
896 896 return repo.repo_type
897 897
898 898
899 899 def is_svn_without_proxy(repository):
900 900 if is_svn(repository):
901 901 from rhodecode.model.settings import VcsSettingsModel
902 902 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
903 903 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
904 904 return False
905 905
906 906
907 907 def discover_user(author):
908 908 """
909 909 Tries to discover RhodeCode User based on the author string. Author string
910 910 is typically `FirstName LastName <email@address.com>`
911 911 """
912 912
913 913 # if author is already an instance use it for extraction
914 914 if isinstance(author, User):
915 915 return author
916 916
917 917 # Valid email in the attribute passed, see if they're in the system
918 918 _email = author_email(author)
919 919 if _email != '':
920 920 user = User.get_by_email(_email, case_insensitive=True, cache=True)
921 921 if user is not None:
922 922 return user
923 923
924 924 # Maybe it's a username, we try to extract it and fetch by username ?
925 925 _author = author_name(author)
926 926 user = User.get_by_username(_author, case_insensitive=True, cache=True)
927 927 if user is not None:
928 928 return user
929 929
930 930 return None
931 931
932 932
933 933 def email_or_none(author):
934 934 # extract email from the commit string
935 935 _email = author_email(author)
936 936
937 937 # If we have an email, use it, otherwise
938 938 # see if it contains a username we can get an email from
939 939 if _email != '':
940 940 return _email
941 941 else:
942 942 user = User.get_by_username(
943 943 author_name(author), case_insensitive=True, cache=True)
944 944
945 945 if user is not None:
946 946 return user.email
947 947
948 948 # No valid email, not a valid user in the system, none!
949 949 return None
950 950
951 951
952 952 def link_to_user(author, length=0, **kwargs):
953 953 user = discover_user(author)
954 954 # user can be None, but if we have it already it means we can re-use it
955 955 # in the person() function, so we save 1 intensive-query
956 956 if user:
957 957 author = user
958 958
959 959 display_person = person(author, 'username_or_name_or_email')
960 960 if length:
961 961 display_person = shorter(display_person, length)
962 962
963 963 if user and user.username != user.DEFAULT_USER:
964 964 return link_to(
965 965 escape(display_person),
966 966 route_path('user_profile', username=user.username),
967 967 **kwargs)
968 968 else:
969 969 return escape(display_person)
970 970
971 971
972 972 def link_to_group(users_group_name, **kwargs):
973 973 return link_to(
974 974 escape(users_group_name),
975 975 route_path('user_group_profile', user_group_name=users_group_name),
976 976 **kwargs)
977 977
978 978
979 979 def person(author, show_attr="username_and_name"):
980 980 user = discover_user(author)
981 981 if user:
982 982 return getattr(user, show_attr)
983 983 else:
984 984 _author = author_name(author)
985 985 _email = email(author)
986 986 return _author or _email
987 987
988 988
989 989 def author_string(email):
990 990 if email:
991 991 user = User.get_by_email(email, case_insensitive=True, cache=True)
992 992 if user:
993 993 if user.first_name or user.last_name:
994 994 return '%s %s &lt;%s&gt;' % (
995 995 user.first_name, user.last_name, email)
996 996 else:
997 997 return email
998 998 else:
999 999 return email
1000 1000 else:
1001 1001 return None
1002 1002
1003 1003
1004 1004 def person_by_id(id_, show_attr="username_and_name"):
1005 1005 # attr to return from fetched user
1006 1006 person_getter = lambda usr: getattr(usr, show_attr)
1007 1007
1008 1008 #maybe it's an ID ?
1009 1009 if str(id_).isdigit() or isinstance(id_, int):
1010 1010 id_ = int(id_)
1011 1011 user = User.get(id_)
1012 1012 if user is not None:
1013 1013 return person_getter(user)
1014 1014 return id_
1015 1015
1016 1016
1017 1017 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
1018 1018 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
1019 1019 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
1020 1020
1021 1021
1022 1022 tags_paterns = OrderedDict((
1023 1023 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
1024 1024 '<div class="metatag" tag="lang">\\2</div>')),
1025 1025
1026 1026 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1027 1027 '<div class="metatag" tag="see">see: \\1 </div>')),
1028 1028
1029 1029 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
1030 1030 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
1031 1031
1032 1032 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1033 1033 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
1034 1034
1035 1035 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
1036 1036 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
1037 1037
1038 1038 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
1039 1039 '<div class="metatag" tag="state \\1">\\1</div>')),
1040 1040
1041 1041 # label in grey
1042 1042 ('label', (re.compile(r'\[([a-z]+)\]'),
1043 1043 '<div class="metatag" tag="label">\\1</div>')),
1044 1044
1045 1045 # generic catch all in grey
1046 1046 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
1047 1047 '<div class="metatag" tag="generic">\\1</div>')),
1048 1048 ))
1049 1049
1050 1050
1051 1051 def extract_metatags(value):
1052 1052 """
1053 1053 Extract supported meta-tags from given text value
1054 1054 """
1055 1055 tags = []
1056 1056 if not value:
1057 1057 return tags, ''
1058 1058
1059 1059 for key, val in tags_paterns.items():
1060 1060 pat, replace_html = val
1061 1061 tags.extend([(key, x.group()) for x in pat.finditer(value)])
1062 1062 value = pat.sub('', value)
1063 1063
1064 1064 return tags, value
1065 1065
1066 1066
1067 1067 def style_metatag(tag_type, value):
1068 1068 """
1069 1069 converts tags from value into html equivalent
1070 1070 """
1071 1071 if not value:
1072 1072 return ''
1073 1073
1074 1074 html_value = value
1075 1075 tag_data = tags_paterns.get(tag_type)
1076 1076 if tag_data:
1077 1077 pat, replace_html = tag_data
1078 1078 # convert to plain `unicode` instead of a markup tag to be used in
1079 1079 # regex expressions. safe_unicode doesn't work here
1080 1080 html_value = pat.sub(replace_html, unicode(value))
1081 1081
1082 1082 return html_value
1083 1083
1084 1084
1085 1085 def bool2icon(value, show_at_false=True):
1086 1086 """
1087 1087 Returns boolean value of a given value, represented as html element with
1088 1088 classes that will represent icons
1089 1089
1090 1090 :param value: given value to convert to html node
1091 1091 """
1092 1092
1093 1093 if value: # does bool conversion
1094 1094 return HTML.tag('i', class_="icon-true", title='True')
1095 1095 else: # not true as bool
1096 1096 if show_at_false:
1097 1097 return HTML.tag('i', class_="icon-false", title='False')
1098 1098 return HTML.tag('i')
1099 1099
1100 1100 #==============================================================================
1101 1101 # PERMS
1102 1102 #==============================================================================
1103 1103 from rhodecode.lib.auth import (
1104 1104 HasPermissionAny, HasPermissionAll,
1105 1105 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1106 1106 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1107 1107 csrf_token_key, AuthUser)
1108 1108
1109 1109
1110 1110 #==============================================================================
1111 1111 # GRAVATAR URL
1112 1112 #==============================================================================
1113 1113 class InitialsGravatar(object):
1114 1114 def __init__(self, email_address, first_name, last_name, size=30,
1115 1115 background=None, text_color='#fff'):
1116 1116 self.size = size
1117 1117 self.first_name = first_name
1118 1118 self.last_name = last_name
1119 1119 self.email_address = email_address
1120 1120 self.background = background or self.str2color(email_address)
1121 1121 self.text_color = text_color
1122 1122
1123 1123 def get_color_bank(self):
1124 1124 """
1125 1125 returns a predefined list of colors that gravatars can use.
1126 1126 Those are randomized distinct colors that guarantee readability and
1127 1127 uniqueness.
1128 1128
1129 1129 generated with: http://phrogz.net/css/distinct-colors.html
1130 1130 """
1131 1131 return [
1132 1132 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1133 1133 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1134 1134 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1135 1135 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1136 1136 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1137 1137 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1138 1138 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1139 1139 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1140 1140 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1141 1141 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1142 1142 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1143 1143 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1144 1144 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1145 1145 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1146 1146 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1147 1147 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1148 1148 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1149 1149 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1150 1150 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1151 1151 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1152 1152 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1153 1153 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1154 1154 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1155 1155 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1156 1156 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1157 1157 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1158 1158 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1159 1159 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1160 1160 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1161 1161 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1162 1162 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1163 1163 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1164 1164 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1165 1165 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1166 1166 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1167 1167 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1168 1168 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1169 1169 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1170 1170 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1171 1171 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1172 1172 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1173 1173 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1174 1174 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1175 1175 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1176 1176 '#4f8c46', '#368dd9', '#5c0073'
1177 1177 ]
1178 1178
1179 1179 def rgb_to_hex_color(self, rgb_tuple):
1180 1180 """
1181 1181 Converts an rgb_tuple passed to an hex color.
1182 1182
1183 1183 :param rgb_tuple: tuple with 3 ints represents rgb color space
1184 1184 """
1185 1185 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1186 1186
1187 1187 def email_to_int_list(self, email_str):
1188 1188 """
1189 1189 Get every byte of the hex digest value of email and turn it to integer.
1190 1190 It's going to be always between 0-255
1191 1191 """
1192 1192 digest = md5_safe(email_str.lower())
1193 1193 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1194 1194
1195 1195 def pick_color_bank_index(self, email_str, color_bank):
1196 1196 return self.email_to_int_list(email_str)[0] % len(color_bank)
1197 1197
1198 1198 def str2color(self, email_str):
1199 1199 """
1200 1200 Tries to map in a stable algorithm an email to color
1201 1201
1202 1202 :param email_str:
1203 1203 """
1204 1204 color_bank = self.get_color_bank()
1205 1205 # pick position (module it's length so we always find it in the
1206 1206 # bank even if it's smaller than 256 values
1207 1207 pos = self.pick_color_bank_index(email_str, color_bank)
1208 1208 return color_bank[pos]
1209 1209
1210 1210 def normalize_email(self, email_address):
1211 1211 import unicodedata
1212 1212 # default host used to fill in the fake/missing email
1213 1213 default_host = u'localhost'
1214 1214
1215 1215 if not email_address:
1216 1216 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1217 1217
1218 1218 email_address = safe_unicode(email_address)
1219 1219
1220 1220 if u'@' not in email_address:
1221 1221 email_address = u'%s@%s' % (email_address, default_host)
1222 1222
1223 1223 if email_address.endswith(u'@'):
1224 1224 email_address = u'%s%s' % (email_address, default_host)
1225 1225
1226 1226 email_address = unicodedata.normalize('NFKD', email_address)\
1227 1227 .encode('ascii', 'ignore')
1228 1228 return email_address
1229 1229
1230 1230 def get_initials(self):
1231 1231 """
1232 1232 Returns 2 letter initials calculated based on the input.
1233 1233 The algorithm picks first given email address, and takes first letter
1234 1234 of part before @, and then the first letter of server name. In case
1235 1235 the part before @ is in a format of `somestring.somestring2` it replaces
1236 1236 the server letter with first letter of somestring2
1237 1237
1238 1238 In case function was initialized with both first and lastname, this
1239 1239 overrides the extraction from email by first letter of the first and
1240 1240 last name. We add special logic to that functionality, In case Full name
1241 1241 is compound, like Guido Von Rossum, we use last part of the last name
1242 1242 (Von Rossum) picking `R`.
1243 1243
1244 1244 Function also normalizes the non-ascii characters to they ascii
1245 1245 representation, eg Δ„ => A
1246 1246 """
1247 1247 import unicodedata
1248 1248 # replace non-ascii to ascii
1249 1249 first_name = unicodedata.normalize(
1250 1250 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1251 1251 last_name = unicodedata.normalize(
1252 1252 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1253 1253
1254 1254 # do NFKD encoding, and also make sure email has proper format
1255 1255 email_address = self.normalize_email(self.email_address)
1256 1256
1257 1257 # first push the email initials
1258 1258 prefix, server = email_address.split('@', 1)
1259 1259
1260 1260 # check if prefix is maybe a 'first_name.last_name' syntax
1261 1261 _dot_split = prefix.rsplit('.', 1)
1262 1262 if len(_dot_split) == 2 and _dot_split[1]:
1263 1263 initials = [_dot_split[0][0], _dot_split[1][0]]
1264 1264 else:
1265 1265 initials = [prefix[0], server[0]]
1266 1266
1267 1267 # then try to replace either first_name or last_name
1268 1268 fn_letter = (first_name or " ")[0].strip()
1269 1269 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1270 1270
1271 1271 if fn_letter:
1272 1272 initials[0] = fn_letter
1273 1273
1274 1274 if ln_letter:
1275 1275 initials[1] = ln_letter
1276 1276
1277 1277 return ''.join(initials).upper()
1278 1278
1279 1279 def get_img_data_by_type(self, font_family, img_type):
1280 1280 default_user = """
1281 1281 <svg xmlns="http://www.w3.org/2000/svg"
1282 1282 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1283 1283 viewBox="-15 -10 439.165 429.164"
1284 1284
1285 1285 xml:space="preserve"
1286 1286 style="background:{background};" >
1287 1287
1288 1288 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1289 1289 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1290 1290 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1291 1291 168.596,153.916,216.671,
1292 1292 204.583,216.671z" fill="{text_color}"/>
1293 1293 <path d="M407.164,374.717L360.88,
1294 1294 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1295 1295 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1296 1296 15.366-44.203,23.488-69.076,23.488c-24.877,
1297 1297 0-48.762-8.122-69.078-23.488
1298 1298 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1299 1299 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1300 1300 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1301 1301 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1302 1302 19.402-10.527 C409.699,390.129,
1303 1303 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1304 1304 </svg>""".format(
1305 1305 size=self.size,
1306 1306 background='#979797', # @grey4
1307 1307 text_color=self.text_color,
1308 1308 font_family=font_family)
1309 1309
1310 1310 return {
1311 1311 "default_user": default_user
1312 1312 }[img_type]
1313 1313
1314 1314 def get_img_data(self, svg_type=None):
1315 1315 """
1316 1316 generates the svg metadata for image
1317 1317 """
1318 1318 fonts = [
1319 1319 '-apple-system',
1320 1320 'BlinkMacSystemFont',
1321 1321 'Segoe UI',
1322 1322 'Roboto',
1323 1323 'Oxygen-Sans',
1324 1324 'Ubuntu',
1325 1325 'Cantarell',
1326 1326 'Helvetica Neue',
1327 1327 'sans-serif'
1328 1328 ]
1329 1329 font_family = ','.join(fonts)
1330 1330 if svg_type:
1331 1331 return self.get_img_data_by_type(font_family, svg_type)
1332 1332
1333 1333 initials = self.get_initials()
1334 1334 img_data = """
1335 1335 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1336 1336 width="{size}" height="{size}"
1337 1337 style="width: 100%; height: 100%; background-color: {background}"
1338 1338 viewBox="0 0 {size} {size}">
1339 1339 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1340 1340 pointer-events="auto" fill="{text_color}"
1341 1341 font-family="{font_family}"
1342 1342 style="font-weight: 400; font-size: {f_size}px;">{text}
1343 1343 </text>
1344 1344 </svg>""".format(
1345 1345 size=self.size,
1346 1346 f_size=self.size/2.05, # scale the text inside the box nicely
1347 1347 background=self.background,
1348 1348 text_color=self.text_color,
1349 1349 text=initials.upper(),
1350 1350 font_family=font_family)
1351 1351
1352 1352 return img_data
1353 1353
1354 1354 def generate_svg(self, svg_type=None):
1355 1355 img_data = self.get_img_data(svg_type)
1356 1356 return "data:image/svg+xml;base64,%s" % base64.b64encode(img_data)
1357 1357
1358 1358
1359 1359 def initials_gravatar(email_address, first_name, last_name, size=30):
1360 1360 svg_type = None
1361 1361 if email_address == User.DEFAULT_USER_EMAIL:
1362 1362 svg_type = 'default_user'
1363 1363 klass = InitialsGravatar(email_address, first_name, last_name, size)
1364 1364 return klass.generate_svg(svg_type=svg_type)
1365 1365
1366 1366
1367 1367 def gravatar_url(email_address, size=30, request=None):
1368 1368 request = get_current_request()
1369 1369 _use_gravatar = request.call_context.visual.use_gravatar
1370 1370 _gravatar_url = request.call_context.visual.gravatar_url
1371 1371
1372 1372 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1373 1373
1374 1374 email_address = email_address or User.DEFAULT_USER_EMAIL
1375 1375 if isinstance(email_address, unicode):
1376 1376 # hashlib crashes on unicode items
1377 1377 email_address = safe_str(email_address)
1378 1378
1379 1379 # empty email or default user
1380 1380 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1381 1381 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1382 1382
1383 1383 if _use_gravatar:
1384 1384 # TODO: Disuse pyramid thread locals. Think about another solution to
1385 1385 # get the host and schema here.
1386 1386 request = get_current_request()
1387 1387 tmpl = safe_str(_gravatar_url)
1388 1388 tmpl = tmpl.replace('{email}', email_address)\
1389 1389 .replace('{md5email}', md5_safe(email_address.lower())) \
1390 1390 .replace('{netloc}', request.host)\
1391 1391 .replace('{scheme}', request.scheme)\
1392 1392 .replace('{size}', safe_str(size))
1393 1393 return tmpl
1394 1394 else:
1395 1395 return initials_gravatar(email_address, '', '', size=size)
1396 1396
1397 1397
1398 1398 def breadcrumb_repo_link(repo):
1399 1399 """
1400 1400 Makes a breadcrumbs path link to repo
1401 1401
1402 1402 ex::
1403 1403 group >> subgroup >> repo
1404 1404
1405 1405 :param repo: a Repository instance
1406 1406 """
1407 1407
1408 1408 path = [
1409 1409 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1410 1410 title='last change:{}'.format(format_date(group.last_commit_change)))
1411 1411 for group in repo.groups_with_parents
1412 1412 ] + [
1413 1413 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1414 1414 title='last change:{}'.format(format_date(repo.last_commit_change)))
1415 1415 ]
1416 1416
1417 1417 return literal(' &raquo; '.join(path))
1418 1418
1419 1419
1420 1420 def breadcrumb_repo_group_link(repo_group):
1421 1421 """
1422 1422 Makes a breadcrumbs path link to repo
1423 1423
1424 1424 ex::
1425 1425 group >> subgroup
1426 1426
1427 1427 :param repo_group: a Repository Group instance
1428 1428 """
1429 1429
1430 1430 path = [
1431 1431 link_to(group.name,
1432 1432 route_path('repo_group_home', repo_group_name=group.group_name),
1433 1433 title='last change:{}'.format(format_date(group.last_commit_change)))
1434 1434 for group in repo_group.parents
1435 1435 ] + [
1436 1436 link_to(repo_group.name,
1437 1437 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1438 1438 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1439 1439 ]
1440 1440
1441 1441 return literal(' &raquo; '.join(path))
1442 1442
1443 1443
1444 1444 def format_byte_size_binary(file_size):
1445 1445 """
1446 1446 Formats file/folder sizes to standard.
1447 1447 """
1448 1448 if file_size is None:
1449 1449 file_size = 0
1450 1450
1451 1451 formatted_size = format_byte_size(file_size, binary=True)
1452 1452 return formatted_size
1453 1453
1454 1454
1455 1455 def urlify_text(text_, safe=True, **href_attrs):
1456 1456 """
1457 1457 Extract urls from text and make html links out of them
1458 1458 """
1459 1459
1460 1460 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1461 1461 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1462 1462
1463 1463 def url_func(match_obj):
1464 1464 url_full = match_obj.groups()[0]
1465 1465 a_options = dict(href_attrs)
1466 1466 a_options['href'] = url_full
1467 1467 a_text = url_full
1468 1468 return HTML.tag("a", a_text, **a_options)
1469 1469
1470 1470 _new_text = url_pat.sub(url_func, text_)
1471 1471
1472 1472 if safe:
1473 1473 return literal(_new_text)
1474 1474 return _new_text
1475 1475
1476 1476
1477 1477 def urlify_commits(text_, repo_name):
1478 1478 """
1479 1479 Extract commit ids from text and make link from them
1480 1480
1481 1481 :param text_:
1482 1482 :param repo_name: repo name to build the URL with
1483 1483 """
1484 1484
1485 1485 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1486 1486
1487 1487 def url_func(match_obj):
1488 1488 commit_id = match_obj.groups()[1]
1489 1489 pref = match_obj.groups()[0]
1490 1490 suf = match_obj.groups()[2]
1491 1491
1492 1492 tmpl = (
1493 1493 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1494 1494 '%(commit_id)s</a>%(suf)s'
1495 1495 )
1496 1496 return tmpl % {
1497 1497 'pref': pref,
1498 1498 'cls': 'revision-link',
1499 1499 'url': route_url(
1500 1500 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1501 1501 'commit_id': commit_id,
1502 1502 'suf': suf,
1503 1503 'hovercard_alt': 'Commit: {}'.format(commit_id),
1504 1504 'hovercard_url': route_url(
1505 1505 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1506 1506 }
1507 1507
1508 1508 new_text = url_pat.sub(url_func, text_)
1509 1509
1510 1510 return new_text
1511 1511
1512 1512
1513 1513 def _process_url_func(match_obj, repo_name, uid, entry,
1514 1514 return_raw_data=False, link_format='html'):
1515 1515 pref = ''
1516 1516 if match_obj.group().startswith(' '):
1517 1517 pref = ' '
1518 1518
1519 1519 issue_id = ''.join(match_obj.groups())
1520 1520
1521 1521 if link_format == 'html':
1522 1522 tmpl = (
1523 1523 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1524 1524 '%(issue-prefix)s%(id-repr)s'
1525 1525 '</a>')
1526 1526 elif link_format == 'html+hovercard':
1527 1527 tmpl = (
1528 1528 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1529 1529 '%(issue-prefix)s%(id-repr)s'
1530 1530 '</a>')
1531 1531 elif link_format in ['rst', 'rst+hovercard']:
1532 1532 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1533 1533 elif link_format in ['markdown', 'markdown+hovercard']:
1534 1534 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1535 1535 else:
1536 1536 raise ValueError('Bad link_format:{}'.format(link_format))
1537 1537
1538 1538 (repo_name_cleaned,
1539 1539 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1540 1540
1541 1541 # variables replacement
1542 1542 named_vars = {
1543 1543 'id': issue_id,
1544 1544 'repo': repo_name,
1545 1545 'repo_name': repo_name_cleaned,
1546 1546 'group_name': parent_group_name,
1547 1547 # set dummy keys so we always have them
1548 1548 'hostname': '',
1549 1549 'netloc': '',
1550 1550 'scheme': ''
1551 1551 }
1552 1552
1553 1553 request = get_current_request()
1554 1554 if request:
1555 1555 # exposes, hostname, netloc, scheme
1556 1556 host_data = get_host_info(request)
1557 1557 named_vars.update(host_data)
1558 1558
1559 1559 # named regex variables
1560 1560 named_vars.update(match_obj.groupdict())
1561 1561 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1562 1562 desc = string.Template(entry['desc']).safe_substitute(**named_vars)
1563 1563 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1564 1564
1565 1565 def quote_cleaner(input_str):
1566 1566 """Remove quotes as it's HTML"""
1567 1567 return input_str.replace('"', '')
1568 1568
1569 1569 data = {
1570 1570 'pref': pref,
1571 1571 'cls': quote_cleaner('issue-tracker-link'),
1572 1572 'url': quote_cleaner(_url),
1573 1573 'id-repr': issue_id,
1574 1574 'issue-prefix': entry['pref'],
1575 1575 'serv': entry['url'],
1576 1576 'title': bleach.clean(desc, strip=True),
1577 1577 'hovercard_url': hovercard_url
1578 1578 }
1579 1579
1580 1580 if return_raw_data:
1581 1581 return {
1582 1582 'id': issue_id,
1583 1583 'url': _url
1584 1584 }
1585 1585 return tmpl % data
1586 1586
1587 1587
1588 1588 def get_active_pattern_entries(repo_name):
1589 1589 repo = None
1590 1590 if repo_name:
1591 1591 # Retrieving repo_name to avoid invalid repo_name to explode on
1592 1592 # IssueTrackerSettingsModel but still passing invalid name further down
1593 1593 repo = Repository.get_by_repo_name(repo_name, cache=True)
1594 1594
1595 1595 settings_model = IssueTrackerSettingsModel(repo=repo)
1596 1596 active_entries = settings_model.get_settings(cache=True)
1597 1597 return active_entries
1598 1598
1599 1599
1600 1600 pr_pattern_re = re.compile(r'(?:(?:^!)|(?: !))(\d+)')
1601 1601
1602 1602
1603 1603 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1604 1604
1605 1605 allowed_formats = ['html', 'rst', 'markdown',
1606 1606 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1607 1607 if link_format not in allowed_formats:
1608 1608 raise ValueError('Link format can be only one of:{} got {}'.format(
1609 1609 allowed_formats, link_format))
1610 1610
1611 1611 if active_entries is None:
1612 1612 log.debug('Fetch active patterns for repo: %s', repo_name)
1613 1613 active_entries = get_active_pattern_entries(repo_name)
1614 1614
1615 1615 issues_data = []
1616 1616 new_text = text_string
1617 1617
1618 1618 log.debug('Got %s entries to process', len(active_entries))
1619 1619 for uid, entry in active_entries.items():
1620 1620 log.debug('found issue tracker entry with uid %s', uid)
1621 1621
1622 1622 if not (entry['pat'] and entry['url']):
1623 1623 log.debug('skipping due to missing data')
1624 1624 continue
1625 1625
1626 1626 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1627 1627 uid, entry['pat'], entry['url'], entry['pref'])
1628 1628
1629 1629 if entry.get('pat_compiled'):
1630 1630 pattern = entry['pat_compiled']
1631 1631 else:
1632 1632 try:
1633 1633 pattern = re.compile(r'%s' % entry['pat'])
1634 1634 except re.error:
1635 1635 log.exception('issue tracker pattern: `%s` failed to compile', entry['pat'])
1636 1636 continue
1637 1637
1638 1638 data_func = partial(
1639 1639 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1640 1640 return_raw_data=True)
1641 1641
1642 1642 for match_obj in pattern.finditer(text_string):
1643 1643 issues_data.append(data_func(match_obj))
1644 1644
1645 1645 url_func = partial(
1646 1646 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1647 1647 link_format=link_format)
1648 1648
1649 1649 new_text = pattern.sub(url_func, new_text)
1650 1650 log.debug('processed prefix:uid `%s`', uid)
1651 1651
1652 1652 # finally use global replace, eg !123 -> pr-link, those will not catch
1653 1653 # if already similar pattern exists
1654 1654 server_url = '${scheme}://${netloc}'
1655 1655 pr_entry = {
1656 1656 'pref': '!',
1657 1657 'url': server_url + '/_admin/pull-requests/${id}',
1658 1658 'desc': 'Pull Request !${id}',
1659 1659 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1660 1660 }
1661 1661 pr_url_func = partial(
1662 1662 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1663 1663 link_format=link_format+'+hovercard')
1664 1664 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1665 1665 log.debug('processed !pr pattern')
1666 1666
1667 1667 return new_text, issues_data
1668 1668
1669 1669
1670 1670 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1671 1671 """
1672 1672 Parses given text message and makes proper links.
1673 1673 issues are linked to given issue-server, and rest is a commit link
1674 1674 """
1675 1675
1676 1676 def escaper(_text):
1677 1677 return _text.replace('<', '&lt;').replace('>', '&gt;')
1678 1678
1679 1679 new_text = escaper(commit_text)
1680 1680
1681 1681 # extract http/https links and make them real urls
1682 1682 new_text = urlify_text(new_text, safe=False)
1683 1683
1684 1684 # urlify commits - extract commit ids and make link out of them, if we have
1685 1685 # the scope of repository present.
1686 1686 if repository:
1687 1687 new_text = urlify_commits(new_text, repository)
1688 1688
1689 1689 # process issue tracker patterns
1690 1690 new_text, issues = process_patterns(new_text, repository or '',
1691 1691 active_entries=active_pattern_entries)
1692 1692
1693 1693 return literal(new_text)
1694 1694
1695 1695
1696 1696 def render_binary(repo_name, file_obj):
1697 1697 """
1698 1698 Choose how to render a binary file
1699 1699 """
1700 1700
1701 1701 # unicode
1702 1702 filename = file_obj.name
1703 1703
1704 1704 # images
1705 1705 for ext in ['*.png', '*.jpeg', '*.jpg', '*.ico', '*.gif']:
1706 1706 if fnmatch.fnmatch(filename, pat=ext):
1707 1707 src = route_path(
1708 1708 'repo_file_raw', repo_name=repo_name,
1709 1709 commit_id=file_obj.commit.raw_id,
1710 1710 f_path=file_obj.path)
1711 1711
1712 1712 return literal(
1713 1713 '<img class="rendered-binary" alt="rendered-image" src="{}">'.format(src))
1714 1714
1715 1715
1716 1716 def renderer_from_filename(filename, exclude=None):
1717 1717 """
1718 1718 choose a renderer based on filename, this works only for text based files
1719 1719 """
1720 1720
1721 1721 # ipython
1722 1722 for ext in ['*.ipynb']:
1723 1723 if fnmatch.fnmatch(filename, pat=ext):
1724 1724 return 'jupyter'
1725 1725
1726 1726 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1727 1727 if is_markup:
1728 1728 return is_markup
1729 1729 return None
1730 1730
1731 1731
1732 1732 def render(source, renderer='rst', mentions=False, relative_urls=None,
1733 1733 repo_name=None, active_pattern_entries=None):
1734 1734
1735 1735 def maybe_convert_relative_links(html_source):
1736 1736 if relative_urls:
1737 1737 return relative_links(html_source, relative_urls)
1738 1738 return html_source
1739 1739
1740 1740 if renderer == 'plain':
1741 1741 return literal(
1742 1742 MarkupRenderer.plain(source, leading_newline=False))
1743 1743
1744 1744 elif renderer == 'rst':
1745 1745 if repo_name:
1746 1746 # process patterns on comments if we pass in repo name
1747 1747 source, issues = process_patterns(
1748 1748 source, repo_name, link_format='rst',
1749 1749 active_entries=active_pattern_entries)
1750 1750
1751 1751 return literal(
1752 1752 '<div class="rst-block">%s</div>' %
1753 1753 maybe_convert_relative_links(
1754 1754 MarkupRenderer.rst(source, mentions=mentions)))
1755 1755
1756 1756 elif renderer == 'markdown':
1757 1757 if repo_name:
1758 1758 # process patterns on comments if we pass in repo name
1759 1759 source, issues = process_patterns(
1760 1760 source, repo_name, link_format='markdown',
1761 1761 active_entries=active_pattern_entries)
1762 1762
1763 1763 return literal(
1764 1764 '<div class="markdown-block">%s</div>' %
1765 1765 maybe_convert_relative_links(
1766 1766 MarkupRenderer.markdown(source, flavored=True,
1767 1767 mentions=mentions)))
1768 1768
1769 1769 elif renderer == 'jupyter':
1770 1770 return literal(
1771 1771 '<div class="ipynb">%s</div>' %
1772 1772 maybe_convert_relative_links(
1773 1773 MarkupRenderer.jupyter(source)))
1774 1774
1775 1775 # None means just show the file-source
1776 1776 return None
1777 1777
1778 1778
1779 1779 def commit_status(repo, commit_id):
1780 1780 return ChangesetStatusModel().get_status(repo, commit_id)
1781 1781
1782 1782
1783 1783 def commit_status_lbl(commit_status):
1784 1784 return dict(ChangesetStatus.STATUSES).get(commit_status)
1785 1785
1786 1786
1787 1787 def commit_time(repo_name, commit_id):
1788 1788 repo = Repository.get_by_repo_name(repo_name)
1789 1789 commit = repo.get_commit(commit_id=commit_id)
1790 1790 return commit.date
1791 1791
1792 1792
1793 1793 def get_permission_name(key):
1794 1794 return dict(Permission.PERMS).get(key)
1795 1795
1796 1796
1797 1797 def journal_filter_help(request):
1798 1798 _ = request.translate
1799 1799 from rhodecode.lib.audit_logger import ACTIONS
1800 1800 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1801 1801
1802 1802 return _(
1803 1803 'Example filter terms:\n' +
1804 1804 ' repository:vcs\n' +
1805 1805 ' username:marcin\n' +
1806 1806 ' username:(NOT marcin)\n' +
1807 1807 ' action:*push*\n' +
1808 1808 ' ip:127.0.0.1\n' +
1809 1809 ' date:20120101\n' +
1810 1810 ' date:[20120101100000 TO 20120102]\n' +
1811 1811 '\n' +
1812 1812 'Actions: {actions}\n' +
1813 1813 '\n' +
1814 1814 'Generate wildcards using \'*\' character:\n' +
1815 1815 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1816 1816 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1817 1817 '\n' +
1818 1818 'Optional AND / OR operators in queries\n' +
1819 1819 ' "repository:vcs OR repository:test"\n' +
1820 1820 ' "username:test AND repository:test*"\n'
1821 1821 ).format(actions=actions)
1822 1822
1823 1823
1824 1824 def not_mapped_error(repo_name):
1825 1825 from rhodecode.translation import _
1826 1826 flash(_('%s repository is not mapped to db perhaps'
1827 1827 ' it was created or renamed from the filesystem'
1828 1828 ' please run the application again'
1829 1829 ' in order to rescan repositories') % repo_name, category='error')
1830 1830
1831 1831
1832 1832 def ip_range(ip_addr):
1833 1833 from rhodecode.model.db import UserIpMap
1834 1834 s, e = UserIpMap._get_ip_range(ip_addr)
1835 1835 return '%s - %s' % (s, e)
1836 1836
1837 1837
1838 1838 def form(url, method='post', needs_csrf_token=True, **attrs):
1839 1839 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1840 1840 if method.lower() != 'get' and needs_csrf_token:
1841 1841 raise Exception(
1842 1842 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1843 1843 'CSRF token. If the endpoint does not require such token you can ' +
1844 1844 'explicitly set the parameter needs_csrf_token to false.')
1845 1845
1846 1846 return insecure_form(url, method=method, **attrs)
1847 1847
1848 1848
1849 1849 def secure_form(form_url, method="POST", multipart=False, **attrs):
1850 1850 """Start a form tag that points the action to an url. This
1851 1851 form tag will also include the hidden field containing
1852 1852 the auth token.
1853 1853
1854 1854 The url options should be given either as a string, or as a
1855 1855 ``url()`` function. The method for the form defaults to POST.
1856 1856
1857 1857 Options:
1858 1858
1859 1859 ``multipart``
1860 1860 If set to True, the enctype is set to "multipart/form-data".
1861 1861 ``method``
1862 1862 The method to use when submitting the form, usually either
1863 1863 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1864 1864 hidden input with name _method is added to simulate the verb
1865 1865 over POST.
1866 1866
1867 1867 """
1868 1868
1869 1869 if 'request' in attrs:
1870 1870 session = attrs['request'].session
1871 1871 del attrs['request']
1872 1872 else:
1873 1873 raise ValueError(
1874 1874 'Calling this form requires request= to be passed as argument')
1875 1875
1876 1876 _form = insecure_form(form_url, method, multipart, **attrs)
1877 1877 token = literal(
1878 1878 '<input type="hidden" name="{}" value="{}">'.format(
1879 1879 csrf_token_key, get_csrf_token(session)))
1880 1880
1881 1881 return literal("%s\n%s" % (_form, token))
1882 1882
1883 1883
1884 1884 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1885 1885 select_html = select(name, selected, options, **attrs)
1886 1886
1887 1887 select2 = """
1888 1888 <script>
1889 1889 $(document).ready(function() {
1890 1890 $('#%s').select2({
1891 1891 containerCssClass: 'drop-menu %s',
1892 1892 dropdownCssClass: 'drop-menu-dropdown',
1893 1893 dropdownAutoWidth: true%s
1894 1894 });
1895 1895 });
1896 1896 </script>
1897 1897 """
1898 1898
1899 1899 filter_option = """,
1900 1900 minimumResultsForSearch: -1
1901 1901 """
1902 1902 input_id = attrs.get('id') or name
1903 1903 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1904 1904 filter_enabled = "" if enable_filter else filter_option
1905 1905 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1906 1906
1907 1907 return literal(select_html+select_script)
1908 1908
1909 1909
1910 1910 def get_visual_attr(tmpl_context_var, attr_name):
1911 1911 """
1912 1912 A safe way to get a variable from visual variable of template context
1913 1913
1914 1914 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1915 1915 :param attr_name: name of the attribute we fetch from the c.visual
1916 1916 """
1917 1917 visual = getattr(tmpl_context_var, 'visual', None)
1918 1918 if not visual:
1919 1919 return
1920 1920 else:
1921 1921 return getattr(visual, attr_name, None)
1922 1922
1923 1923
1924 1924 def get_last_path_part(file_node):
1925 1925 if not file_node.path:
1926 1926 return u'/'
1927 1927
1928 1928 path = safe_unicode(file_node.path.split('/')[-1])
1929 1929 return u'../' + path
1930 1930
1931 1931
1932 1932 def route_url(*args, **kwargs):
1933 1933 """
1934 1934 Wrapper around pyramids `route_url` (fully qualified url) function.
1935 1935 """
1936 1936 req = get_current_request()
1937 1937 return req.route_url(*args, **kwargs)
1938 1938
1939 1939
1940 1940 def route_path(*args, **kwargs):
1941 1941 """
1942 1942 Wrapper around pyramids `route_path` function.
1943 1943 """
1944 1944 req = get_current_request()
1945 1945 return req.route_path(*args, **kwargs)
1946 1946
1947 1947
1948 1948 def route_path_or_none(*args, **kwargs):
1949 1949 try:
1950 1950 return route_path(*args, **kwargs)
1951 1951 except KeyError:
1952 1952 return None
1953 1953
1954 1954
1955 1955 def current_route_path(request, **kw):
1956 1956 new_args = request.GET.mixed()
1957 1957 new_args.update(kw)
1958 1958 return request.current_route_path(_query=new_args)
1959 1959
1960 1960
1961 1961 def curl_api_example(method, args):
1962 1962 args_json = json.dumps(OrderedDict([
1963 1963 ('id', 1),
1964 1964 ('auth_token', 'SECRET'),
1965 1965 ('method', method),
1966 1966 ('args', args)
1967 1967 ]))
1968 1968
1969 1969 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
1970 1970 api_url=route_url('apiv2'),
1971 1971 args_json=args_json
1972 1972 )
1973 1973
1974 1974
1975 1975 def api_call_example(method, args):
1976 1976 """
1977 1977 Generates an API call example via CURL
1978 1978 """
1979 1979 curl_call = curl_api_example(method, args)
1980 1980
1981 1981 return literal(
1982 1982 curl_call +
1983 1983 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
1984 1984 "and needs to be of `api calls` role."
1985 1985 .format(token_url=route_url('my_account_auth_tokens')))
1986 1986
1987 1987
1988 1988 def notification_description(notification, request):
1989 1989 """
1990 1990 Generate notification human readable description based on notification type
1991 1991 """
1992 1992 from rhodecode.model.notification import NotificationModel
1993 1993 return NotificationModel().make_description(
1994 1994 notification, translate=request.translate)
1995 1995
1996 1996
1997 1997 def go_import_header(request, db_repo=None):
1998 1998 """
1999 1999 Creates a header for go-import functionality in Go Lang
2000 2000 """
2001 2001
2002 2002 if not db_repo:
2003 2003 return
2004 2004 if 'go-get' not in request.GET:
2005 2005 return
2006 2006
2007 2007 clone_url = db_repo.clone_url()
2008 2008 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2009 2009 # we have a repo and go-get flag,
2010 2010 return literal('<meta name="go-import" content="{} {} {}">'.format(
2011 2011 prefix, db_repo.repo_type, clone_url))
2012 2012
2013 2013
2014 2014 def reviewer_as_json(*args, **kwargs):
2015 2015 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2016 2016 return _reviewer_as_json(*args, **kwargs)
2017 2017
2018 2018
2019 2019 def get_repo_view_type(request):
2020 2020 route_name = request.matched_route.name
2021 2021 route_to_view_type = {
2022 2022 'repo_changelog': 'commits',
2023 2023 'repo_commits': 'commits',
2024 2024 'repo_files': 'files',
2025 2025 'repo_summary': 'summary',
2026 2026 'repo_commit': 'commit'
2027 2027 }
2028 2028
2029 2029 return route_to_view_type.get(route_name)
2030 2030
2031 2031
2032 2032 def is_active(menu_entry, selected):
2033 2033 """
2034 2034 Returns active class for selecting menus in templates
2035 2035 <li class=${h.is_active('settings', current_active)}></li>
2036 2036 """
2037 2037 if not isinstance(menu_entry, list):
2038 2038 menu_entry = [menu_entry]
2039 2039
2040 2040 if selected in menu_entry:
2041 2041 return "active"
@@ -1,1326 +1,1341 b''
1 1 // Default styles
2 2
3 3 .diff-collapse {
4 4 margin: @padding 0;
5 5 text-align: right;
6 6 }
7 7
8 8 .diff-container {
9 9 margin-bottom: @space;
10 10
11 11 .diffblock {
12 12 margin-bottom: @space;
13 13 }
14 14
15 15 &.hidden {
16 16 display: none;
17 17 overflow: hidden;
18 18 }
19 19 }
20 20
21 21
22 22 div.diffblock .sidebyside {
23 23 background: #ffffff;
24 24 }
25 25
26 26 div.diffblock {
27 27 overflow-x: auto;
28 28 overflow-y: hidden;
29 29 clear: both;
30 30 padding: 0px;
31 31 background: @grey6;
32 32 border: @border-thickness solid @grey5;
33 33 -webkit-border-radius: @border-radius @border-radius 0px 0px;
34 34 border-radius: @border-radius @border-radius 0px 0px;
35 35
36 36
37 37 .comments-number {
38 38 float: right;
39 39 }
40 40
41 41 // BEGIN CODE-HEADER STYLES
42 42
43 43 .code-header {
44 44 background: @grey6;
45 45 padding: 10px 0 10px 0;
46 46 height: auto;
47 47 width: 100%;
48 48
49 49 .hash {
50 50 float: left;
51 51 padding: 2px 0 0 2px;
52 52 }
53 53
54 54 .date {
55 55 float: left;
56 56 text-transform: uppercase;
57 57 padding: 4px 0px 0px 2px;
58 58 }
59 59
60 60 div {
61 61 margin-left: 4px;
62 62 }
63 63
64 64 div.compare_header {
65 65 min-height: 40px;
66 66 margin: 0;
67 67 padding: 0 @padding;
68 68
69 69 .drop-menu {
70 70 float:left;
71 71 display: block;
72 72 margin:0 0 @padding 0;
73 73 }
74 74
75 75 .compare-label {
76 76 float: left;
77 77 clear: both;
78 78 display: inline-block;
79 79 min-width: 5em;
80 80 margin: 0;
81 81 padding: @button-padding @button-padding @button-padding 0;
82 82 font-weight: @text-semibold-weight;
83 83 font-family: @text-semibold;
84 84 }
85 85
86 86 .compare-buttons {
87 87 float: left;
88 88 margin: 0;
89 89 padding: 0 0 @padding;
90 90
91 91 .btn {
92 92 margin: 0 @padding 0 0;
93 93 }
94 94 }
95 95 }
96 96
97 97 }
98 98
99 99 .parents {
100 100 float: left;
101 101 width: 100px;
102 102 font-weight: 400;
103 103 vertical-align: middle;
104 104 padding: 0px 2px 0px 2px;
105 105 background-color: @grey6;
106 106
107 107 #parent_link {
108 108 margin: 00px 2px;
109 109
110 110 &.double {
111 111 margin: 0px 2px;
112 112 }
113 113
114 114 &.disabled{
115 115 margin-right: @padding;
116 116 }
117 117 }
118 118 }
119 119
120 120 .children {
121 121 float: right;
122 122 width: 100px;
123 123 font-weight: 400;
124 124 vertical-align: middle;
125 125 text-align: right;
126 126 padding: 0px 2px 0px 2px;
127 127 background-color: @grey6;
128 128
129 129 #child_link {
130 130 margin: 0px 2px;
131 131
132 132 &.double {
133 133 margin: 0px 2px;
134 134 }
135 135
136 136 &.disabled{
137 137 margin-right: @padding;
138 138 }
139 139 }
140 140 }
141 141
142 142 .changeset_header {
143 143 height: 16px;
144 144
145 145 & > div{
146 146 margin-right: @padding;
147 147 }
148 148 }
149 149
150 150 .changeset_file {
151 151 text-align: left;
152 152 float: left;
153 153 padding: 0;
154 154
155 155 a{
156 156 display: inline-block;
157 157 margin-right: 0.5em;
158 158 }
159 159
160 160 #selected_mode{
161 161 margin-left: 0;
162 162 }
163 163 }
164 164
165 165 .diff-menu-wrapper {
166 166 float: left;
167 167 }
168 168
169 169 .diff-menu {
170 170 position: absolute;
171 171 background: none repeat scroll 0 0 #FFFFFF;
172 172 border-color: #003367 @grey3 @grey3;
173 173 border-right: 1px solid @grey3;
174 174 border-style: solid solid solid;
175 175 border-width: @border-thickness;
176 176 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
177 177 margin-top: 5px;
178 178 margin-left: 1px;
179 179 }
180 180
181 181 .diff-actions, .editor-actions {
182 182 float: left;
183 183
184 184 input{
185 185 margin: 0 0.5em 0 0;
186 186 }
187 187 }
188 188
189 189 // END CODE-HEADER STYLES
190 190
191 191 // BEGIN CODE-BODY STYLES
192 192
193 193 .code-body {
194 194 padding: 0;
195 195 background-color: #ffffff;
196 196 position: relative;
197 197 max-width: none;
198 198 box-sizing: border-box;
199 199 // TODO: johbo: Parent has overflow: auto, this forces the child here
200 200 // to have the intended size and to scroll. Should be simplified.
201 201 width: 100%;
202 202 overflow-x: auto;
203 203 }
204 204
205 205 pre.raw {
206 206 background: white;
207 207 color: @grey1;
208 208 }
209 209 // END CODE-BODY STYLES
210 210
211 211 }
212 212
213 213
214 214 table.code-difftable {
215 215 border-collapse: collapse;
216 216 width: 99%;
217 217 border-radius: 0px !important;
218 218
219 219 td {
220 220 padding: 0 !important;
221 221 background: none !important;
222 222 border: 0 !important;
223 223 }
224 224
225 225 .context {
226 226 background: none repeat scroll 0 0 #DDE7EF;
227 227 }
228 228
229 229 .add {
230 230 background: none repeat scroll 0 0 #DDFFDD;
231 231
232 232 ins {
233 233 background: none repeat scroll 0 0 #AAFFAA;
234 234 text-decoration: none;
235 235 }
236 236 }
237 237
238 238 .del {
239 239 background: none repeat scroll 0 0 #FFDDDD;
240 240
241 241 del {
242 242 background: none repeat scroll 0 0 #FFAAAA;
243 243 text-decoration: none;
244 244 }
245 245 }
246 246
247 247 /** LINE NUMBERS **/
248 248 .lineno {
249 249 padding-left: 2px !important;
250 250 padding-right: 2px;
251 251 text-align: right;
252 252 width: 32px;
253 253 -moz-user-select: none;
254 254 -webkit-user-select: none;
255 255 border-right: @border-thickness solid @grey5 !important;
256 256 border-left: 0px solid #CCC !important;
257 257 border-top: 0px solid #CCC !important;
258 258 border-bottom: none !important;
259 259
260 260 a {
261 261 &:extend(pre);
262 262 text-align: right;
263 263 padding-right: 2px;
264 264 cursor: pointer;
265 265 display: block;
266 266 width: 32px;
267 267 }
268 268 }
269 269
270 270 .context {
271 271 cursor: auto;
272 272 &:extend(pre);
273 273 }
274 274
275 275 .lineno-inline {
276 276 background: none repeat scroll 0 0 #FFF !important;
277 277 padding-left: 2px;
278 278 padding-right: 2px;
279 279 text-align: right;
280 280 width: 30px;
281 281 -moz-user-select: none;
282 282 -webkit-user-select: none;
283 283 }
284 284
285 285 /** CODE **/
286 286 .code {
287 287 display: block;
288 288 width: 100%;
289 289
290 290 td {
291 291 margin: 0;
292 292 padding: 0;
293 293 }
294 294
295 295 pre {
296 296 margin: 0;
297 297 padding: 0;
298 298 margin-left: .5em;
299 299 }
300 300 }
301 301 }
302 302
303 303
304 304 // Comments
305 305 .comment-selected-hl {
306 306 border-left: 6px solid @comment-highlight-color !important;
307 307 padding-left: 3px !important;
308 308 margin-left: -7px !important;
309 309 }
310 310
311 311 div.comment:target,
312 312 div.comment-outdated:target {
313 313 .comment-selected-hl;
314 314 }
315 315
316 316 //TODO: anderson: can't get an absolute number out of anything, so had to put the
317 317 //current values that might change. But to make it clear I put as a calculation
318 318 @comment-max-width: 1065px;
319 319 @pr-extra-margin: 34px;
320 320 @pr-border-spacing: 4px;
321 321 @pr-comment-width: @comment-max-width - @pr-extra-margin - @pr-border-spacing;
322 322
323 323 // Pull Request
324 324 .cs_files .code-difftable {
325 325 border: @border-thickness solid @grey5; //borders only on PRs
326 326
327 327 .comment-inline-form,
328 328 div.comment {
329 329 width: @pr-comment-width;
330 330 }
331 331 }
332 332
333 333 // Changeset
334 334 .code-difftable {
335 335 .comment-inline-form,
336 336 div.comment {
337 337 width: @comment-max-width;
338 338 }
339 339 }
340 340
341 341 //Style page
342 342 @style-extra-margin: @sidebar-width + (@sidebarpadding * 3) + @padding;
343 343 #style-page .code-difftable{
344 344 .comment-inline-form,
345 345 div.comment {
346 346 width: @comment-max-width - @style-extra-margin;
347 347 }
348 348 }
349 349
350 350 #context-bar > h2 {
351 351 font-size: 20px;
352 352 }
353 353
354 354 #context-bar > h2> a {
355 355 font-size: 20px;
356 356 }
357 357 // end of defaults
358 358
359 359 .file_diff_buttons {
360 360 padding: 0 0 @padding;
361 361
362 362 .drop-menu {
363 363 float: left;
364 364 margin: 0 @padding 0 0;
365 365 }
366 366 .btn {
367 367 margin: 0 @padding 0 0;
368 368 }
369 369 }
370 370
371 371 .code-body.textarea.editor {
372 372 max-width: none;
373 373 padding: 15px;
374 374 }
375 375
376 376 td.injected_diff{
377 377 max-width: 1178px;
378 378 overflow-x: auto;
379 379 overflow-y: hidden;
380 380
381 381 div.diff-container,
382 382 div.diffblock{
383 383 max-width: 100%;
384 384 }
385 385
386 386 div.code-body {
387 387 max-width: 1124px;
388 388 overflow-x: auto;
389 389 overflow-y: hidden;
390 390 padding: 0;
391 391 }
392 392 div.diffblock {
393 393 border: none;
394 394 }
395 395
396 396 &.inline-form {
397 397 width: 99%
398 398 }
399 399 }
400 400
401 401
402 402 table.code-difftable {
403 403 width: 100%;
404 404 }
405 405
406 406 /** PYGMENTS COLORING **/
407 407 div.codeblock {
408 408
409 409 // TODO: johbo: Added interim to get rid of the margin around
410 410 // Select2 widgets. This needs further cleanup.
411 411 overflow: auto;
412 412 padding: 0px;
413 413 border: @border-thickness solid @grey6;
414 414 .border-radius(@border-radius);
415 415
416 416 #remove_gist {
417 417 float: right;
418 418 }
419 419
420 420 .gist_url {
421 421 padding: 0px 0px 35px 0px;
422 422 }
423 423
424 424 .gist-desc {
425 425 clear: both;
426 426 margin: 0 0 10px 0;
427 427 code {
428 428 white-space: pre-line;
429 429 line-height: inherit
430 430 }
431 431 }
432 432
433 433 .author {
434 434 clear: both;
435 435 vertical-align: middle;
436 436 font-weight: @text-bold-weight;
437 437 font-family: @text-bold;
438 438 }
439 439
440 440 .btn-mini {
441 441 float: left;
442 442 margin: 0 5px 0 0;
443 443 }
444 444
445 445 .code-header {
446 446 padding: @padding;
447 447 border-bottom: @border-thickness solid @grey5;
448 448
449 449 .rc-user {
450 450 min-width: 0;
451 451 margin-right: .5em;
452 452 }
453 453
454 454 .stats {
455 455 clear: both;
456 456 margin: 0 0 @padding 0;
457 457 padding: 0;
458 458 .left {
459 459 float: left;
460 460 clear: left;
461 461 max-width: 75%;
462 462 margin: 0 0 @padding 0;
463 463
464 464 &.item {
465 465 margin-right: @padding;
466 466 &.last { border-right: none; }
467 467 }
468 468 }
469 469 .buttons { float: right; }
470 470 .author {
471 471 height: 25px; margin-left: 15px; font-weight: bold;
472 472 }
473 473 }
474 474
475 475 .commit {
476 476 margin: 5px 0 0 26px;
477 477 font-weight: normal;
478 478 white-space: pre-wrap;
479 479 }
480 480 }
481 481
482 482 .message {
483 483 position: relative;
484 484 margin: @padding;
485 485
486 486 .codeblock-label {
487 487 margin: 0 0 1em 0;
488 488 }
489 489 }
490 490
491 491 .code-body {
492 492 padding: 0.8em 1em;
493 493 background-color: #ffffff;
494 494 min-width: 100%;
495 495 box-sizing: border-box;
496 496 // TODO: johbo: Parent has overflow: auto, this forces the child here
497 497 // to have the intended size and to scroll. Should be simplified.
498 498 width: 100%;
499 499 overflow-x: auto;
500 500
501 501 img.rendered-binary {
502 502 height: auto;
503 503 width: auto;
504 504 }
505 505
506 506 .markdown-block {
507 507 padding: 1em 0;
508 508 }
509 509 }
510 510
511 511 .codeblock-header {
512 512 background: @grey7;
513 513 height: 36px;
514 514 }
515 515
516 516 .path {
517 517 border-bottom: 1px solid @grey6;
518 518 padding: .65em 1em;
519 519 height: 18px;
520 520 }
521 521 }
522 522
523 523 .code-highlighttable,
524 524 div.codeblock {
525 525
526 526 &.readme {
527 527 background-color: white;
528 528 }
529 529
530 530 .markdown-block table {
531 531 border-collapse: collapse;
532 532
533 533 th,
534 534 td {
535 535 padding: .5em;
536 536 border: @border-thickness solid @border-default-color;
537 537 }
538 538 }
539 539
540 540 table {
541 541 border: 0px;
542 542 margin: 0;
543 543 letter-spacing: normal;
544 544
545 545
546 546 td {
547 547 border: 0px;
548 548 vertical-align: top;
549 549 }
550 550 }
551 551 }
552 552
553 553 div.codeblock .code-header .search-path { padding: 0 0 0 10px; }
554 554 div.search-code-body {
555 555 background-color: #ffffff; padding: 5px 0 5px 10px;
556 556 pre {
557 557 .match { background-color: #faffa6;}
558 558 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
559 559 }
560 560 .code-highlighttable {
561 561 border-collapse: collapse;
562 562
563 563 tr:hover {
564 564 background: #fafafa;
565 565 }
566 566 td.code {
567 567 padding-left: 10px;
568 568 }
569 569 td.line {
570 570 border-right: 1px solid #ccc !important;
571 571 padding-right: 10px;
572 572 text-align: right;
573 573 font-family: @text-monospace;
574 574 span {
575 575 white-space: pre-wrap;
576 576 color: #666666;
577 577 }
578 578 }
579 579 }
580 580 }
581 581
582 582 div.annotatediv { margin-left: 2px; margin-right: 4px; }
583 583 .code-highlight {
584 584 margin: 0; padding: 0; border-left: @border-thickness solid @grey5;
585 585 pre, .linenodiv pre { padding: 0 5px; margin: 0; }
586 586 pre div:target {background-color: @comment-highlight-color !important;}
587 587 }
588 588
589 589 .linenos a { text-decoration: none; }
590 590
591 591 .CodeMirror-selected { background: @rchighlightblue; }
592 592 .CodeMirror-focused .CodeMirror-selected { background: @rchighlightblue; }
593 593 .CodeMirror ::selection { background: @rchighlightblue; }
594 594 .CodeMirror ::-moz-selection { background: @rchighlightblue; }
595 595
596 596 .code { display: block; border:0px !important; }
597 597
598 598 .code-highlight, /* TODO: dan: merge codehilite into code-highlight */
599 599 .codehilite {
600 600 /*ElasticMatch is custom RhodeCode TAG*/
601 601
602 602 .c-ElasticMatch {
603 603 background-color: #faffa6;
604 604 padding: 0.2em;
605 605 }
606 606 }
607 607
608 608 /* This can be generated with `pygmentize -S default -f html` */
609 609 .code-highlight,
610 610 .codehilite {
611 611 /*ElasticMatch is custom RhodeCode TAG*/
612 612 .c-ElasticMatch { background-color: #faffa6; padding: 0.2em;}
613 613 .hll { background-color: #ffffcc }
614 614 .c { color: #408080; font-style: italic } /* Comment */
615 615 .err, .codehilite .err { border: none } /* Error */
616 616 .k { color: #008000; font-weight: bold } /* Keyword */
617 617 .o { color: #666666 } /* Operator */
618 618 .ch { color: #408080; font-style: italic } /* Comment.Hashbang */
619 619 .cm { color: #408080; font-style: italic } /* Comment.Multiline */
620 620 .cp { color: #BC7A00 } /* Comment.Preproc */
621 621 .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */
622 622 .c1 { color: #408080; font-style: italic } /* Comment.Single */
623 623 .cs { color: #408080; font-style: italic } /* Comment.Special */
624 624 .gd { color: #A00000 } /* Generic.Deleted */
625 625 .ge { font-style: italic } /* Generic.Emph */
626 626 .gr { color: #FF0000 } /* Generic.Error */
627 627 .gh { color: #000080; font-weight: bold } /* Generic.Heading */
628 628 .gi { color: #00A000 } /* Generic.Inserted */
629 629 .go { color: #888888 } /* Generic.Output */
630 630 .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
631 631 .gs { font-weight: bold } /* Generic.Strong */
632 632 .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
633 633 .gt { color: #0044DD } /* Generic.Traceback */
634 634 .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
635 635 .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
636 636 .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
637 637 .kp { color: #008000 } /* Keyword.Pseudo */
638 638 .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
639 639 .kt { color: #B00040 } /* Keyword.Type */
640 640 .m { color: #666666 } /* Literal.Number */
641 641 .s { color: #BA2121 } /* Literal.String */
642 642 .na { color: #7D9029 } /* Name.Attribute */
643 643 .nb { color: #008000 } /* Name.Builtin */
644 644 .nc { color: #0000FF; font-weight: bold } /* Name.Class */
645 645 .no { color: #880000 } /* Name.Constant */
646 646 .nd { color: #AA22FF } /* Name.Decorator */
647 647 .ni { color: #999999; font-weight: bold } /* Name.Entity */
648 648 .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
649 649 .nf { color: #0000FF } /* Name.Function */
650 650 .nl { color: #A0A000 } /* Name.Label */
651 651 .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
652 652 .nt { color: #008000; font-weight: bold } /* Name.Tag */
653 653 .nv { color: #19177C } /* Name.Variable */
654 654 .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
655 655 .w { color: #bbbbbb } /* Text.Whitespace */
656 656 .mb { color: #666666 } /* Literal.Number.Bin */
657 657 .mf { color: #666666 } /* Literal.Number.Float */
658 658 .mh { color: #666666 } /* Literal.Number.Hex */
659 659 .mi { color: #666666 } /* Literal.Number.Integer */
660 660 .mo { color: #666666 } /* Literal.Number.Oct */
661 661 .sa { color: #BA2121 } /* Literal.String.Affix */
662 662 .sb { color: #BA2121 } /* Literal.String.Backtick */
663 663 .sc { color: #BA2121 } /* Literal.String.Char */
664 664 .dl { color: #BA2121 } /* Literal.String.Delimiter */
665 665 .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
666 666 .s2 { color: #BA2121 } /* Literal.String.Double */
667 667 .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
668 668 .sh { color: #BA2121 } /* Literal.String.Heredoc */
669 669 .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
670 670 .sx { color: #008000 } /* Literal.String.Other */
671 671 .sr { color: #BB6688 } /* Literal.String.Regex */
672 672 .s1 { color: #BA2121 } /* Literal.String.Single */
673 673 .ss { color: #19177C } /* Literal.String.Symbol */
674 674 .bp { color: #008000 } /* Name.Builtin.Pseudo */
675 675 .fm { color: #0000FF } /* Name.Function.Magic */
676 676 .vc { color: #19177C } /* Name.Variable.Class */
677 677 .vg { color: #19177C } /* Name.Variable.Global */
678 678 .vi { color: #19177C } /* Name.Variable.Instance */
679 679 .vm { color: #19177C } /* Name.Variable.Magic */
680 680 .il { color: #666666 } /* Literal.Number.Integer.Long */
681 681
682 682 }
683 683
684 684 /* customized pre blocks for markdown/rst */
685 685 pre.literal-block, .codehilite pre{
686 686 padding: @padding;
687 687 border: 1px solid @grey6;
688 688 .border-radius(@border-radius);
689 689 background-color: @grey7;
690 690 }
691 691
692 692
693 693 /* START NEW CODE BLOCK CSS */
694 694
695 695 @cb-line-height: 18px;
696 696 @cb-line-code-padding: 10px;
697 697 @cb-text-padding: 5px;
698 698
699 699 @pill-padding: 2px 7px;
700 700 @pill-padding-small: 2px 2px 1px 2px;
701 701
702 702 input.filediff-collapse-state {
703 703 display: none;
704 704
705 705 &:checked + .filediff { /* file diff is collapsed */
706 706 .cb {
707 707 display: none
708 708 }
709 709 .filediff-collapse-indicator {
710 710 float: left;
711 711 cursor: pointer;
712 712 margin: 1px -5px;
713 713 }
714 714 .filediff-collapse-indicator:before {
715 715 content: '\f105';
716 716 }
717 717
718 718 .filediff-menu {
719 719 display: none;
720 720 }
721 721
722 722 }
723 723
724 724 &+ .filediff { /* file diff is expanded */
725 725
726 726 .filediff-collapse-indicator {
727 727 float: left;
728 728 cursor: pointer;
729 729 margin: 1px -5px;
730 730 }
731 731 .filediff-collapse-indicator:before {
732 732 content: '\f107';
733 733 }
734 734
735 735 .filediff-menu {
736 736 display: block;
737 737 }
738 738
739 739 margin: 10px 0;
740 740 &:nth-child(2) {
741 741 margin: 0;
742 742 }
743 743 }
744 744 }
745 745
746 746 .filediffs .anchor {
747 747 display: block;
748 748 height: 40px;
749 749 margin-top: -40px;
750 750 visibility: hidden;
751 751 }
752 752
753 753 .filediffs .anchor:nth-of-type(1) {
754 754 display: block;
755 755 height: 80px;
756 756 margin-top: -80px;
757 757 visibility: hidden;
758 758 }
759 759
760 760 .cs_files {
761 761 clear: both;
762 762 }
763 763
764 764 #diff-file-sticky{
765 765 will-change: min-height;
766 766 height: 80px;
767 767 }
768 768
769 769 .sidebar__inner{
770 770 transform: translate(0, 0); /* For browsers don't support translate3d. */
771 771 transform: translate3d(0, 0, 0);
772 772 will-change: position, transform;
773 773 height: 65px;
774 774 background-color: #fff;
775 775 padding: 5px 0px;
776 776 }
777 777
778 778 .sidebar__bar {
779 779 padding: 5px 0px 0px 0px
780 780 }
781 781
782 782 .fpath-placeholder {
783 783 clear: both;
784 784 visibility: hidden
785 785 }
786 786
787 787 .is-affixed {
788 788
789 789 .sidebar__inner {
790 790 z-index: 30;
791 791 }
792 792
793 793 .sidebar_inner_shadow {
794 794 position: fixed;
795 795 top: 75px;
796 796 right: -100%;
797 797 left: -100%;
798 798 z-index: 30;
799 799 display: block;
800 800 height: 5px;
801 801 content: "";
802 802 background: linear-gradient(rgba(0, 0, 0, 0.075), rgba(0, 0, 0, 0.001)) repeat-x 0 0;
803 803 border-top: 1px solid rgba(0, 0, 0, 0.15);
804 804 }
805 805
806 806 .fpath-placeholder {
807 807 visibility: visible !important;
808 808 }
809 809 }
810 810
811 811 .diffset-menu {
812 812
813 813 }
814 814
815 815 #todo-box {
816 816 clear:both;
817 817 display: none;
818 818 text-align: right
819 819 }
820 820
821 821 .diffset {
822 822 margin: 0px auto;
823 823 .diffset-heading {
824 824 border: 1px solid @grey5;
825 825 margin-bottom: -1px;
826 826 // margin-top: 20px;
827 827 h2 {
828 828 margin: 0;
829 829 line-height: 38px;
830 830 padding-left: 10px;
831 831 }
832 832 .btn {
833 833 margin: 0;
834 834 }
835 835 background: @grey6;
836 836 display: block;
837 837 padding: 5px;
838 838 }
839 839 .diffset-heading-warning {
840 840 background: @alert3-inner;
841 841 border: 1px solid @alert3;
842 842 }
843 843 &.diffset-comments-disabled {
844 844 .cb-comment-box-opener, .comment-inline-form, .cb-comment-add-button {
845 845 display: none !important;
846 846 }
847 847 }
848 848 }
849 849
850 850 .filelist {
851 851 .pill {
852 852 display: block;
853 853 float: left;
854 854 padding: @pill-padding-small;
855 855 }
856 856 }
857 857
858 858 .pill {
859 859 display: block;
860 860 float: left;
861 861 padding: @pill-padding;
862 862 }
863 863
864 864 .pill-group {
865 865 .pill {
866 866 opacity: .8;
867 867 margin-right: 3px;
868 868 font-size: 12px;
869 869 font-weight: normal;
870 870 min-width: 30px;
871 871 text-align: center;
872 872
873 873 &:first-child {
874 874 border-radius: @border-radius 0 0 @border-radius;
875 875 }
876 876 &:last-child {
877 877 border-radius: 0 @border-radius @border-radius 0;
878 878 }
879 879 &:only-child {
880 880 border-radius: @border-radius;
881 881 margin-right: 0;
882 882 }
883 883 }
884 884 }
885 885
886 886 /* Main comments*/
887 887 #comments {
888 888 .comment-selected {
889 889 border-left: 6px solid @comment-highlight-color;
890 890 padding-left: 3px;
891 891 margin-left: -9px;
892 892 }
893 893 }
894 894
895 895 .filediff {
896 896 border: 1px solid @grey5;
897 897
898 898 /* START OVERRIDES */
899 899 .code-highlight {
900 900 border: none; // TODO: remove this border from the global
901 901 // .code-highlight, it doesn't belong there
902 902 }
903 903 label {
904 904 margin: 0; // TODO: remove this margin definition from global label
905 905 // it doesn't belong there - if margin on labels
906 906 // are needed for a form they should be defined
907 907 // in the form's class
908 908 }
909 909 /* END OVERRIDES */
910 910
911 911 * {
912 912 box-sizing: border-box;
913 913 }
914 914
915 915 .on-hover-icon {
916 916 visibility: hidden;
917 917 }
918 918
919 919 .filediff-anchor {
920 920 visibility: hidden;
921 921 }
922 922 &:hover {
923 923 .filediff-anchor {
924 924 visibility: visible;
925 925 }
926 926 .on-hover-icon {
927 927 visibility: visible;
928 928 }
929 929 }
930 930
931 931 .filediff-heading {
932 932 cursor: pointer;
933 933 display: block;
934 934 padding: 10px 10px;
935 935 }
936 936 .filediff-heading:after {
937 937 content: "";
938 938 display: table;
939 939 clear: both;
940 940 }
941 941 .filediff-heading:hover {
942 942 background: #e1e9f4 !important;
943 943 }
944 944
945 945 .filediff-menu {
946 946 text-align: right;
947 947 padding: 5px 5px 5px 0px;
948 948 background: @grey7;
949 949
950 950 &> a,
951 951 &> span {
952 952 padding: 1px;
953 953 }
954 954 }
955 955
956 956 .filediff-collapse-button, .filediff-expand-button {
957 957 cursor: pointer;
958 958 }
959 959 .filediff-collapse-button {
960 960 display: inline;
961 961 }
962 962 .filediff-expand-button {
963 963 display: none;
964 964 }
965 965 .filediff-collapsed .filediff-collapse-button {
966 966 display: none;
967 967 }
968 968 .filediff-collapsed .filediff-expand-button {
969 969 display: inline;
970 970 }
971 971
972 972 /**** COMMENTS ****/
973 973
974 974 .filediff-menu {
975 975 .show-comment-button {
976 976 display: none;
977 977 }
978 978 }
979 979 &.hide-comments {
980 980 .inline-comments {
981 981 display: none;
982 982 }
983 983 .filediff-menu {
984 984 .show-comment-button {
985 985 display: inline;
986 986 }
987 987 .hide-comment-button {
988 988 display: none;
989 989 }
990 990 }
991 991 }
992 992
993 993 .hide-line-comments {
994 994 .inline-comments {
995 995 display: none;
996 996 }
997 997 }
998 998
999 999 /**** END COMMENTS ****/
1000 1000
1001
1002 .nav-chunk {
1003 position: absolute;
1004 right: 20px;
1005 margin-top: -17px;
1006 }
1007
1008 .nav-chunk.selected {
1009 visibility: visible !important;
1010 }
1011
1012 #diff_nav {
1013 color: @grey3;
1014 }
1015
1001 1016 }
1002 1017
1003 1018
1004 1019 .op-added {
1005 1020 color: @alert1;
1006 1021 }
1007 1022
1008 1023 .op-deleted {
1009 1024 color: @alert2;
1010 1025 }
1011 1026
1012 1027 .filediff, .filelist {
1013 1028
1014 1029 .pill {
1015 1030 &[op="name"] {
1016 1031 background: none;
1017 1032 opacity: 1;
1018 1033 color: white;
1019 1034 }
1020 1035 &[op="limited"] {
1021 1036 background: @grey2;
1022 1037 color: white;
1023 1038 }
1024 1039 &[op="binary"] {
1025 1040 background: @color7;
1026 1041 color: white;
1027 1042 }
1028 1043 &[op="modified"] {
1029 1044 background: @alert1;
1030 1045 color: white;
1031 1046 }
1032 1047 &[op="renamed"] {
1033 1048 background: @color4;
1034 1049 color: white;
1035 1050 }
1036 1051 &[op="copied"] {
1037 1052 background: @color4;
1038 1053 color: white;
1039 1054 }
1040 1055 &[op="mode"] {
1041 1056 background: @grey3;
1042 1057 color: white;
1043 1058 }
1044 1059 &[op="symlink"] {
1045 1060 background: @color8;
1046 1061 color: white;
1047 1062 }
1048 1063
1049 1064 &[op="added"] { /* added lines */
1050 1065 background: @alert1;
1051 1066 color: white;
1052 1067 }
1053 1068 &[op="deleted"] { /* deleted lines */
1054 1069 background: @alert2;
1055 1070 color: white;
1056 1071 }
1057 1072
1058 1073 &[op="created"] { /* created file */
1059 1074 background: @alert1;
1060 1075 color: white;
1061 1076 }
1062 1077 &[op="removed"] { /* deleted file */
1063 1078 background: @color5;
1064 1079 color: white;
1065 1080 }
1066 1081 &[op="comments"] { /* comments on file */
1067 1082 background: @grey4;
1068 1083 color: white;
1069 1084 }
1070 1085 }
1071 1086 }
1072 1087
1073 1088
1074 1089 .filediff-outdated {
1075 1090 padding: 8px 0;
1076 1091
1077 1092 .filediff-heading {
1078 1093 opacity: .5;
1079 1094 }
1080 1095 }
1081 1096
1082 1097 table.cb {
1083 1098 width: 100%;
1084 1099 border-collapse: collapse;
1085 1100
1086 1101 .cb-text {
1087 1102 padding: @cb-text-padding;
1088 1103 }
1089 1104 .cb-hunk {
1090 1105 padding: @cb-text-padding;
1091 1106 }
1092 1107 .cb-expand {
1093 1108 display: none;
1094 1109 }
1095 1110 .cb-collapse {
1096 1111 display: inline;
1097 1112 }
1098 1113 &.cb-collapsed {
1099 1114 .cb-line {
1100 1115 display: none;
1101 1116 }
1102 1117 .cb-expand {
1103 1118 display: inline;
1104 1119 }
1105 1120 .cb-collapse {
1106 1121 display: none;
1107 1122 }
1108 1123 .cb-hunk {
1109 1124 display: none;
1110 1125 }
1111 1126 }
1112 1127
1113 1128 /* intentionally general selector since .cb-line-selected must override it
1114 1129 and they both use !important since the td itself may have a random color
1115 1130 generated by annotation blocks. TLDR: if you change it, make sure
1116 1131 annotated block selection and line selection in file view still work */
1117 1132 .cb-line-fresh .cb-content {
1118 1133 background: white !important;
1119 1134 }
1120 1135 .cb-warning {
1121 1136 background: #fff4dd;
1122 1137 }
1123 1138
1124 1139 &.cb-diff-sideside {
1125 1140 td {
1126 1141 &.cb-content {
1127 1142 width: 50%;
1128 1143 }
1129 1144 }
1130 1145 }
1131 1146
1132 1147 tr {
1133 1148 &.cb-annotate {
1134 1149 border-top: 1px solid #eee;
1135 1150 }
1136 1151
1137 1152 &.cb-comment-info {
1138 1153 border-top: 1px solid #eee;
1139 1154 color: rgba(0, 0, 0, 0.3);
1140 1155 background: #edf2f9;
1141 1156
1142 1157 td {
1143 1158
1144 1159 }
1145 1160 }
1146 1161
1147 1162 &.cb-hunk {
1148 1163 font-family: @text-monospace;
1149 1164 color: rgba(0, 0, 0, 0.3);
1150 1165
1151 1166 td {
1152 1167 &:first-child {
1153 1168 background: #edf2f9;
1154 1169 }
1155 1170 &:last-child {
1156 1171 background: #f4f7fb;
1157 1172 }
1158 1173 }
1159 1174 }
1160 1175 }
1161 1176
1162 1177
1163 1178 td {
1164 1179 vertical-align: top;
1165 1180 padding: 0;
1166 1181
1167 1182 &.cb-content {
1168 1183 font-size: 12.35px;
1169 1184
1170 1185 &.cb-line-selected .cb-code {
1171 1186 background: @comment-highlight-color !important;
1172 1187 }
1173 1188
1174 1189 span.cb-code {
1175 1190 line-height: @cb-line-height;
1176 1191 padding-left: @cb-line-code-padding;
1177 1192 padding-right: @cb-line-code-padding;
1178 1193 display: block;
1179 1194 white-space: pre-wrap;
1180 1195 font-family: @text-monospace;
1181 1196 word-break: break-all;
1182 1197 .nonl {
1183 1198 color: @color5;
1184 1199 }
1185 1200 .cb-action {
1186 1201 &:before {
1187 1202 content: " ";
1188 1203 }
1189 1204 &.cb-deletion:before {
1190 1205 content: "- ";
1191 1206 }
1192 1207 &.cb-addition:before {
1193 1208 content: "+ ";
1194 1209 }
1195 1210 }
1196 1211 }
1197 1212
1198 1213 &> button.cb-comment-box-opener {
1199 1214
1200 1215 padding: 2px 2px 1px 3px;
1201 1216 margin-left: -6px;
1202 1217 margin-top: -1px;
1203 1218
1204 1219 border-radius: @border-radius;
1205 1220 position: absolute;
1206 1221 display: none;
1207 1222 }
1208 1223 .cb-comment {
1209 1224 margin-top: 10px;
1210 1225 white-space: normal;
1211 1226 }
1212 1227 }
1213 1228 &:hover {
1214 1229 button.cb-comment-box-opener {
1215 1230 display: block;
1216 1231 }
1217 1232 &+ td button.cb-comment-box-opener {
1218 1233 display: block
1219 1234 }
1220 1235 }
1221 1236
1222 1237 &.cb-data {
1223 1238 text-align: right;
1224 1239 width: 30px;
1225 1240 font-family: @text-monospace;
1226 1241
1227 1242 .icon-comment {
1228 1243 cursor: pointer;
1229 1244 }
1230 1245 &.cb-line-selected {
1231 1246 background: @comment-highlight-color !important;
1232 1247 }
1233 1248 &.cb-line-selected > div {
1234 1249 display: block;
1235 1250 background: @comment-highlight-color !important;
1236 1251 line-height: @cb-line-height;
1237 1252 color: rgba(0, 0, 0, 0.3);
1238 1253 }
1239 1254 }
1240 1255
1241 1256 &.cb-lineno {
1242 1257 padding: 0;
1243 1258 width: 50px;
1244 1259 color: rgba(0, 0, 0, 0.3);
1245 1260 text-align: right;
1246 1261 border-right: 1px solid #eee;
1247 1262 font-family: @text-monospace;
1248 1263 -webkit-user-select: none;
1249 1264 -moz-user-select: none;
1250 1265 user-select: none;
1251 1266
1252 1267 a::before {
1253 1268 content: attr(data-line-no);
1254 1269 }
1255 1270 &.cb-line-selected {
1256 1271 background: @comment-highlight-color !important;
1257 1272 }
1258 1273
1259 1274 a {
1260 1275 display: block;
1261 1276 padding-right: @cb-line-code-padding;
1262 1277 padding-left: @cb-line-code-padding;
1263 1278 line-height: @cb-line-height;
1264 1279 color: rgba(0, 0, 0, 0.3);
1265 1280 }
1266 1281 }
1267 1282
1268 1283 &.cb-empty {
1269 1284 background: @grey7;
1270 1285 }
1271 1286
1272 1287 ins {
1273 1288 color: black;
1274 1289 background: #a6f3a6;
1275 1290 text-decoration: none;
1276 1291 }
1277 1292 del {
1278 1293 color: black;
1279 1294 background: #f8cbcb;
1280 1295 text-decoration: none;
1281 1296 }
1282 1297 &.cb-addition {
1283 1298 background: #ecffec;
1284 1299
1285 1300 &.blob-lineno {
1286 1301 background: #ddffdd;
1287 1302 }
1288 1303 }
1289 1304 &.cb-deletion {
1290 1305 background: #ffecec;
1291 1306
1292 1307 &.blob-lineno {
1293 1308 background: #ffdddd;
1294 1309 }
1295 1310 }
1296 1311 &.cb-annotate-message-spacer {
1297 1312 width:8px;
1298 1313 padding: 1px 0px 0px 3px;
1299 1314 }
1300 1315 &.cb-annotate-info {
1301 1316 width: 320px;
1302 1317 min-width: 320px;
1303 1318 max-width: 320px;
1304 1319 padding: 5px 2px;
1305 1320 font-size: 13px;
1306 1321
1307 1322 .cb-annotate-message {
1308 1323 padding: 2px 0px 0px 0px;
1309 1324 white-space: pre-line;
1310 1325 overflow: hidden;
1311 1326 }
1312 1327 .rc-user {
1313 1328 float: none;
1314 1329 padding: 0 6px 0 17px;
1315 1330 min-width: unset;
1316 1331 min-height: unset;
1317 1332 }
1318 1333 }
1319 1334
1320 1335 &.cb-annotate-revision {
1321 1336 cursor: pointer;
1322 1337 text-align: right;
1323 1338 padding: 1px 3px 0px 3px;
1324 1339 }
1325 1340 }
1326 1341 }
@@ -1,1219 +1,1355 b''
1 1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2 2
3 3 <%def name="diff_line_anchor(commit, filename, line, type)"><%
4 4 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
5 5 %></%def>
6 6
7 7 <%def name="action_class(action)">
8 8 <%
9 9 return {
10 10 '-': 'cb-deletion',
11 11 '+': 'cb-addition',
12 12 ' ': 'cb-context',
13 13 }.get(action, 'cb-empty')
14 14 %>
15 15 </%def>
16 16
17 17 <%def name="op_class(op_id)">
18 18 <%
19 19 return {
20 20 DEL_FILENODE: 'deletion', # file deleted
21 21 BIN_FILENODE: 'warning' # binary diff hidden
22 22 }.get(op_id, 'addition')
23 23 %>
24 24 </%def>
25 25
26 26
27 27
28 28 <%def name="render_diffset(diffset, commit=None,
29 29
30 30 # collapse all file diff entries when there are more than this amount of files in the diff
31 31 collapse_when_files_over=20,
32 32
33 33 # collapse lines in the diff when more than this amount of lines changed in the file diff
34 34 lines_changed_limit=500,
35 35
36 36 # add a ruler at to the output
37 37 ruler_at_chars=0,
38 38
39 39 # show inline comments
40 40 use_comments=False,
41 41
42 42 # disable new comments
43 43 disable_new_comments=False,
44 44
45 45 # special file-comments that were deleted in previous versions
46 46 # it's used for showing outdated comments for deleted files in a PR
47 47 deleted_files_comments=None,
48 48
49 49 # for cache purpose
50 50 inline_comments=None,
51 51
52 52 # additional menu for PRs
53 53 pull_request_menu=None,
54 54
55 55 # show/hide todo next to comments
56 56 show_todos=True,
57 57
58 58 )">
59 59
60 60 <%
61 61 diffset_container_id = h.md5(diffset.target_ref)
62 62 collapse_all = len(diffset.files) > collapse_when_files_over
63 63 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
64 64 %>
65 65
66 66 %if use_comments:
67 67
68 68 ## Template for injecting comments
69 69 <div id="cb-comments-inline-container-template" class="js-template">
70 70 ${inline_comments_container([])}
71 71 </div>
72 72
73 73 <div class="js-template" id="cb-comment-inline-form-template">
74 74 <div class="comment-inline-form ac">
75 75
76 76 %if c.rhodecode_user.username != h.DEFAULT_USER:
77 77 ## render template for inline comments
78 78 ${commentblock.comment_form(form_type='inline')}
79 79 %else:
80 80 ${h.form('', class_='inline-form comment-form-login', method='get')}
81 81 <div class="pull-left">
82 82 <div class="comment-help pull-right">
83 83 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
84 84 </div>
85 85 </div>
86 86 <div class="comment-button pull-right">
87 87 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
88 88 ${_('Cancel')}
89 89 </button>
90 90 </div>
91 91 <div class="clearfix"></div>
92 92 ${h.end_form()}
93 93 %endif
94 94 </div>
95 95 </div>
96 96
97 97 %endif
98 98
99 99 %if c.user_session_attrs["diffmode"] == 'sideside':
100 100 <style>
101 101 .wrapper {
102 102 max-width: 1600px !important;
103 103 }
104 104 </style>
105 105 %endif
106 106
107 107 %if ruler_at_chars:
108 108 <style>
109 109 .diff table.cb .cb-content:after {
110 110 content: "";
111 111 border-left: 1px solid blue;
112 112 position: absolute;
113 113 top: 0;
114 114 height: 18px;
115 115 opacity: .2;
116 116 z-index: 10;
117 117 //## +5 to account for diff action (+/-)
118 118 left: ${ruler_at_chars + 5}ch;
119 119 </style>
120 120 %endif
121 121
122 122 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
123 123
124 124 <div style="height: 20px; line-height: 20px">
125 125 ## expand/collapse action
126 126 <div class="pull-left">
127 127 <a class="${'collapsed' if collapse_all else ''}" href="#expand-files" onclick="toggleExpand(this, '${diffset_container_id}'); return false">
128 128 % if collapse_all:
129 129 <i class="icon-plus-squared-alt icon-no-margin"></i>${_('Expand all files')}
130 130 % else:
131 131 <i class="icon-minus-squared-alt icon-no-margin"></i>${_('Collapse all files')}
132 132 % endif
133 133 </a>
134 134
135 135 </div>
136 136
137 137 ## todos
138 138 % if show_todos and getattr(c, 'at_version', None):
139 139 <div class="pull-right">
140 140 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
141 141 ${_('not available in this view')}
142 142 </div>
143 143 % elif show_todos:
144 144 <div class="pull-right">
145 145 <div class="comments-number" style="padding-left: 10px">
146 146 % if hasattr(c, 'unresolved_comments') and hasattr(c, 'resolved_comments'):
147 147 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
148 148 % if c.unresolved_comments:
149 149 <a href="#show-todos" onclick="$('#todo-box').toggle(); return false">
150 150 ${_('{} unresolved').format(len(c.unresolved_comments))}
151 151 </a>
152 152 % else:
153 153 ${_('0 unresolved')}
154 154 % endif
155 155
156 156 ${_('{} Resolved').format(len(c.resolved_comments))}
157 157 % endif
158 158 </div>
159 159 </div>
160 160 % endif
161 161
162 162 ## comments
163 163 <div class="pull-right">
164 164 <div class="comments-number" style="padding-left: 10px">
165 165 % if hasattr(c, 'comments') and hasattr(c, 'inline_cnt'):
166 166 <i class="icon-comment" style="color: #949494">COMMENTS:</i>
167 167 % if c.comments:
168 168 <a href="#comments">${_ungettext("{} General", "{} General", len(c.comments)).format(len(c.comments))}</a>,
169 169 % else:
170 170 ${_('0 General')}
171 171 % endif
172 172
173 173 % if c.inline_cnt:
174 174 <a href="#" onclick="return Rhodecode.comments.nextComment();"
175 175 id="inline-comments-counter">${_ungettext("{} Inline", "{} Inline", c.inline_cnt).format(c.inline_cnt)}
176 176 </a>
177 177 % else:
178 178 ${_('0 Inline')}
179 179 % endif
180 180 % endif
181 181
182 182 % if pull_request_menu:
183 183 <%
184 184 outdated_comm_count_ver = pull_request_menu['outdated_comm_count_ver']
185 185 %>
186 186
187 187 % if outdated_comm_count_ver:
188 188 <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
189 189 (${_("{} Outdated").format(outdated_comm_count_ver)})
190 190 </a>
191 191 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
192 192 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
193 193 % else:
194 194 (${_("{} Outdated").format(outdated_comm_count_ver)})
195 195 % endif
196 196
197 197 % endif
198 198
199 199 </div>
200 200 </div>
201 201
202 202 </div>
203 203
204 204 % if diffset.limited_diff:
205 205 <div class="diffset-heading ${(diffset.limited_diff and 'diffset-heading-warning' or '')}">
206 206 <h2 class="clearinner">
207 207 ${_('The requested changes are too big and content was truncated.')}
208 208 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
209 209 </h2>
210 210 </div>
211 211 ## commit range header for each individual diff
212 212 % elif commit and hasattr(c, 'commit_ranges') and len(c.commit_ranges) > 1:
213 213 <div class="diffset-heading ${(diffset.limited_diff and 'diffset-heading-warning' or '')}">
214 214 <div class="clearinner">
215 215 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=diffset.repo_name,commit_id=commit.raw_id)}">${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}</a>
216 216 </div>
217 217 </div>
218 218 % endif
219 219
220 220 <div id="todo-box">
221 221 % if hasattr(c, 'unresolved_comments') and c.unresolved_comments:
222 222 % for co in c.unresolved_comments:
223 223 <a class="permalink" href="#comment-${co.comment_id}"
224 224 onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'))">
225 225 <i class="icon-flag-filled-red"></i>
226 226 ${co.comment_id}</a>${('' if loop.last else ',')}
227 227 % endfor
228 228 % endif
229 229 </div>
230 230 %if diffset.has_hidden_changes:
231 231 <p class="empty_data">${_('Some changes may be hidden')}</p>
232 232 %elif not diffset.files:
233 233 <p class="empty_data">${_('No files')}</p>
234 234 %endif
235 235
236 236 <div class="filediffs">
237 237
238 238 ## initial value could be marked as False later on
239 239 <% over_lines_changed_limit = False %>
240 240 %for i, filediff in enumerate(diffset.files):
241 241
242 242 <%
243 243 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
244 244 over_lines_changed_limit = lines_changed > lines_changed_limit
245 245 %>
246 246 ## anchor with support of sticky header
247 247 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
248 248
249 249 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
250 250 <div
251 251 class="filediff"
252 252 data-f-path="${filediff.patch['filename']}"
253 253 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
254 254 >
255 255 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
256 256 <%
257 257 file_comments = (get_inline_comments(inline_comments, filediff.patch['filename']) or {}).values()
258 258 total_file_comments = [_c for _c in h.itertools.chain.from_iterable(file_comments) if not _c.outdated]
259 259 %>
260 260 <div class="filediff-collapse-indicator icon-"></div>
261 261 <span class="pill-group pull-right" >
262 262 <span class="pill" op="comments">
263 263
264 264 <i class="icon-comment"></i> ${len(total_file_comments)}
265 265 </span>
266 266 </span>
267 267 ${diff_ops(filediff)}
268 268
269 269 </label>
270 270
271 271 ${diff_menu(filediff, use_comments=use_comments)}
272 272 <table data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
273 273
274 274 ## new/deleted/empty content case
275 275 % if not filediff.hunks:
276 276 ## Comment container, on "fakes" hunk that contains all data to render comments
277 277 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
278 278 % endif
279 279
280 280 %if filediff.limited_diff:
281 281 <tr class="cb-warning cb-collapser">
282 282 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
283 283 ${_('The requested commit or file is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
284 284 </td>
285 285 </tr>
286 286 %else:
287 287 %if over_lines_changed_limit:
288 288 <tr class="cb-warning cb-collapser">
289 289 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
290 290 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
291 291 <a href="#" class="cb-expand"
292 292 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
293 293 </a>
294 294 <a href="#" class="cb-collapse"
295 295 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
296 296 </a>
297 297 </td>
298 298 </tr>
299 299 %endif
300 300 %endif
301 301
302 302 % for hunk in filediff.hunks:
303 303 <tr class="cb-hunk">
304 304 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
305 305 ## TODO: dan: add ajax loading of more context here
306 306 ## <a href="#">
307 307 <i class="icon-more"></i>
308 308 ## </a>
309 309 </td>
310 310 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
311 311 @@
312 312 -${hunk.source_start},${hunk.source_length}
313 313 +${hunk.target_start},${hunk.target_length}
314 314 ${hunk.section_header}
315 315 </td>
316 316 </tr>
317
317 318 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
318 319 % endfor
319 320
320 321 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
321 322
322 323 ## outdated comments that do not fit into currently displayed lines
323 324 % for lineno, comments in unmatched_comments.items():
324 325
325 326 %if c.user_session_attrs["diffmode"] == 'unified':
326 327 % if loop.index == 0:
327 328 <tr class="cb-hunk">
328 329 <td colspan="3"></td>
329 330 <td>
330 331 <div>
331 332 ${_('Unmatched/outdated inline comments below')}
332 333 </div>
333 334 </td>
334 335 </tr>
335 336 % endif
336 337 <tr class="cb-line">
337 338 <td class="cb-data cb-context"></td>
338 339 <td class="cb-lineno cb-context"></td>
339 340 <td class="cb-lineno cb-context"></td>
340 341 <td class="cb-content cb-context">
341 342 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
342 343 </td>
343 344 </tr>
344 345 %elif c.user_session_attrs["diffmode"] == 'sideside':
345 346 % if loop.index == 0:
346 347 <tr class="cb-comment-info">
347 348 <td colspan="2"></td>
348 349 <td class="cb-line">
349 350 <div>
350 351 ${_('Unmatched/outdated inline comments below')}
351 352 </div>
352 353 </td>
353 354 <td colspan="2"></td>
354 355 <td class="cb-line">
355 356 <div>
356 357 ${_('Unmatched/outdated comments below')}
357 358 </div>
358 359 </td>
359 360 </tr>
360 361 % endif
361 362 <tr class="cb-line">
362 363 <td class="cb-data cb-context"></td>
363 364 <td class="cb-lineno cb-context"></td>
364 365 <td class="cb-content cb-context">
365 366 % if lineno.startswith('o'):
366 367 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
367 368 % endif
368 369 </td>
369 370
370 371 <td class="cb-data cb-context"></td>
371 372 <td class="cb-lineno cb-context"></td>
372 373 <td class="cb-content cb-context">
373 374 % if lineno.startswith('n'):
374 375 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
375 376 % endif
376 377 </td>
377 378 </tr>
378 379 %endif
379 380
380 381 % endfor
381 382
382 383 </table>
383 384 </div>
384 385 %endfor
385 386
386 387 ## outdated comments that are made for a file that has been deleted
387 388 % for filename, comments_dict in (deleted_files_comments or {}).items():
388 389
389 390 <%
390 391 display_state = 'display: none'
391 392 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
392 393 if open_comments_in_file:
393 394 display_state = ''
394 395 fid = str(id(filename))
395 396 %>
396 397 <div class="filediffs filediff-outdated" style="${display_state}">
397 398 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
398 399 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(fid, filename)}">
399 400 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
400 401 <div class="filediff-collapse-indicator icon-"></div>
401 402
402 403 <span class="pill">
403 404 ## file was deleted
404 405 ${filename}
405 406 </span>
406 407 <span class="pill-group pull-left" >
407 408 ## file op, doesn't need translation
408 409 <span class="pill" op="removed">unresolved comments</span>
409 410 </span>
410 411 <a class="pill filediff-anchor" href="#a_${h.FID(fid, filename)}">ΒΆ</a>
411 412 <span class="pill-group pull-right">
412 413 <span class="pill" op="deleted">
413 414 % if comments_dict['stats'] >0:
414 415 -${comments_dict['stats']}
415 416 % else:
416 417 ${comments_dict['stats']}
417 418 % endif
418 419 </span>
419 420 </span>
420 421 </label>
421 422
422 423 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
423 424 <tr>
424 425 % if c.user_session_attrs["diffmode"] == 'unified':
425 426 <td></td>
426 427 %endif
427 428
428 429 <td></td>
429 430 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
430 431 <strong>${_('This file was removed from diff during updates to this pull-request.')}</strong><br/>
431 432 ${_('There are still outdated/unresolved comments attached to it.')}
432 433 </td>
433 434 </tr>
434 435 %if c.user_session_attrs["diffmode"] == 'unified':
435 436 <tr class="cb-line">
436 437 <td class="cb-data cb-context"></td>
437 438 <td class="cb-lineno cb-context"></td>
438 439 <td class="cb-lineno cb-context"></td>
439 440 <td class="cb-content cb-context">
440 441 ${inline_comments_container(comments_dict['comments'], active_pattern_entries=active_pattern_entries)}
441 442 </td>
442 443 </tr>
443 444 %elif c.user_session_attrs["diffmode"] == 'sideside':
444 445 <tr class="cb-line">
445 446 <td class="cb-data cb-context"></td>
446 447 <td class="cb-lineno cb-context"></td>
447 448 <td class="cb-content cb-context"></td>
448 449
449 450 <td class="cb-data cb-context"></td>
450 451 <td class="cb-lineno cb-context"></td>
451 452 <td class="cb-content cb-context">
452 453 ${inline_comments_container(comments_dict['comments'], active_pattern_entries=active_pattern_entries)}
453 454 </td>
454 455 </tr>
455 456 %endif
456 457 </table>
457 458 </div>
458 459 </div>
459 460 % endfor
460 461
461 462 </div>
462 463 </div>
463 464 </%def>
464 465
465 466 <%def name="diff_ops(filediff)">
466 467 <%
467 468 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
468 469 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
469 470 %>
470 471 <span class="pill">
471 472 <i class="icon-file-text"></i>
472 473 %if filediff.source_file_path and filediff.target_file_path:
473 474 %if filediff.source_file_path != filediff.target_file_path:
474 475 ## file was renamed, or copied
475 476 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
476 477 ${filediff.target_file_path} β¬… <del>${filediff.source_file_path}</del>
477 478 <% final_path = filediff.target_file_path %>
478 479 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
479 480 ${filediff.target_file_path} β¬… ${filediff.source_file_path}
480 481 <% final_path = filediff.target_file_path %>
481 482 %endif
482 483 %else:
483 484 ## file was modified
484 485 ${filediff.source_file_path}
485 486 <% final_path = filediff.source_file_path %>
486 487 %endif
487 488 %else:
488 489 %if filediff.source_file_path:
489 490 ## file was deleted
490 491 ${filediff.source_file_path}
491 492 <% final_path = filediff.source_file_path %>
492 493 %else:
493 494 ## file was added
494 495 ${filediff.target_file_path}
495 496 <% final_path = filediff.target_file_path %>
496 497 %endif
497 498 %endif
498 499 <i style="color: #aaa" class="on-hover-icon icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy file path')}" onclick="return false;"></i>
499 500 </span>
500 501 ## anchor link
501 502 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filediff.patch['filename'])}">ΒΆ</a>
502 503
503 504 <span class="pill-group pull-right">
504 505
505 506 ## ops pills
506 507 %if filediff.limited_diff:
507 508 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
508 509 %endif
509 510
510 511 %if NEW_FILENODE in filediff.patch['stats']['ops']:
511 512 <span class="pill" op="created">created</span>
512 513 %if filediff['target_mode'].startswith('120'):
513 514 <span class="pill" op="symlink">symlink</span>
514 515 %else:
515 516 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
516 517 %endif
517 518 %endif
518 519
519 520 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
520 521 <span class="pill" op="renamed">renamed</span>
521 522 %endif
522 523
523 524 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
524 525 <span class="pill" op="copied">copied</span>
525 526 %endif
526 527
527 528 %if DEL_FILENODE in filediff.patch['stats']['ops']:
528 529 <span class="pill" op="removed">removed</span>
529 530 %endif
530 531
531 532 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
532 533 <span class="pill" op="mode">
533 534 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
534 535 </span>
535 536 %endif
536 537
537 538 %if BIN_FILENODE in filediff.patch['stats']['ops']:
538 539 <span class="pill" op="binary">binary</span>
539 540 %if MOD_FILENODE in filediff.patch['stats']['ops']:
540 541 <span class="pill" op="modified">modified</span>
541 542 %endif
542 543 %endif
543 544
544 545 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
545 546 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
546 547
547 548 </span>
548 549
549 550 </%def>
550 551
551 552 <%def name="nice_mode(filemode)">
552 553 ${(filemode.startswith('100') and filemode[3:] or filemode)}
553 554 </%def>
554 555
555 556 <%def name="diff_menu(filediff, use_comments=False)">
556 557 <div class="filediff-menu">
557 558
558 559 %if filediff.diffset.source_ref:
559 560
560 561 ## FILE BEFORE CHANGES
561 562 %if filediff.operation in ['D', 'M']:
562 563 <a
563 564 class="tooltip"
564 565 href="${h.route_path('repo_files',repo_name=filediff.diffset.target_repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
565 566 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
566 567 >
567 568 ${_('Show file before')}
568 569 </a> |
569 570 %else:
570 571 <span
571 572 class="tooltip"
572 573 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
573 574 >
574 575 ${_('Show file before')}
575 576 </span> |
576 577 %endif
577 578
578 579 ## FILE AFTER CHANGES
579 580 %if filediff.operation in ['A', 'M']:
580 581 <a
581 582 class="tooltip"
582 583 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
583 584 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
584 585 >
585 586 ${_('Show file after')}
586 587 </a>
587 588 %else:
588 589 <span
589 590 class="tooltip"
590 591 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
591 592 >
592 593 ${_('Show file after')}
593 594 </span>
594 595 %endif
595 596
596 597 % if use_comments:
597 598 |
598 599 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
599 600 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
600 601 </a>
601 602 % endif
602 603
603 604 %endif
604 605
605 606 </div>
606 607 </%def>
607 608
608 609
609 610 <%def name="inline_comments_container(comments, active_pattern_entries=None)">
610 611
611 612 <div class="inline-comments">
612 613 %for comment in comments:
613 614 ${commentblock.comment_block(comment, inline=True, active_pattern_entries=active_pattern_entries)}
614 615 %endfor
615 616 % if comments and comments[-1].outdated:
616 617 <span class="btn btn-secondary cb-comment-add-button comment-outdated}" style="display: none;}">
617 618 ${_('Add another comment')}
618 619 </span>
619 620 % else:
620 621 <span onclick="return Rhodecode.comments.createComment(this)" class="btn btn-secondary cb-comment-add-button">
621 622 ${_('Add another comment')}
622 623 </span>
623 624 % endif
624 625
625 626 </div>
626 627 </%def>
627 628
628 629 <%!
629 630
630 631 def get_inline_comments(comments, filename):
631 632 if hasattr(filename, 'unicode_path'):
632 633 filename = filename.unicode_path
633 634
634 635 if not isinstance(filename, (unicode, str)):
635 636 return None
636 637
637 638 if comments and filename in comments:
638 639 return comments[filename]
639 640
640 641 return None
641 642
642 643 def get_comments_for(diff_type, comments, filename, line_version, line_number):
643 644 if hasattr(filename, 'unicode_path'):
644 645 filename = filename.unicode_path
645 646
646 647 if not isinstance(filename, (unicode, str)):
647 648 return None
648 649
649 650 file_comments = get_inline_comments(comments, filename)
650 651 if file_comments is None:
651 652 return None
652 653
653 654 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
654 655 if line_key in file_comments:
655 656 data = file_comments.pop(line_key)
656 657 return data
657 658 %>
658 659
659 660 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
660 %for i, line in enumerate(hunk.sideside):
661
662 <% chunk_count = 1 %>
663 %for loop_obj, item in h.looper(hunk.sideside):
661 664 <%
665 line = item
666 i = loop_obj.index
667 prev_line = loop_obj.previous
662 668 old_line_anchor, new_line_anchor = None, None
663 669
664 670 if line.original.lineno:
665 671 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
666 672 if line.modified.lineno:
667 673 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
674
675 line_action = line.modified.action or line.original.action
676 prev_line_action = prev_line and (prev_line.modified.action or prev_line.original.action)
668 677 %>
669 678
670 679 <tr class="cb-line">
671 680 <td class="cb-data ${action_class(line.original.action)}"
672 681 data-line-no="${line.original.lineno}"
673 682 >
674 <div>
675 683
676 684 <% line_old_comments = None %>
677 685 %if line.original.get_comment_args:
678 686 <% line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args) %>
679 687 %endif
680 688 %if line_old_comments:
681 689 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
682 690 % if has_outdated:
683 691 <i class="tooltip icon-comment-toggle" title="${_('comments including outdated: {}. Click here to display them.').format(len(line_old_comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
684 692 % else:
685 693 <i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(line_old_comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
686 694 % endif
687 695 %endif
688 </div>
689 696 </td>
690 697 <td class="cb-lineno ${action_class(line.original.action)}"
691 698 data-line-no="${line.original.lineno}"
692 699 %if old_line_anchor:
693 700 id="${old_line_anchor}"
694 701 %endif
695 702 >
696 703 %if line.original.lineno:
697 704 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
698 705 %endif
699 706 </td>
700 707 <td class="cb-content ${action_class(line.original.action)}"
701 708 data-line-no="o${line.original.lineno}"
702 709 >
703 710 %if use_comments and line.original.lineno:
704 711 ${render_add_comment_button()}
705 712 %endif
706 713 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
707 714
708 715 %if use_comments and line.original.lineno and line_old_comments:
709 716 ${inline_comments_container(line_old_comments, active_pattern_entries=active_pattern_entries)}
710 717 %endif
711 718
712 719 </td>
713 720 <td class="cb-data ${action_class(line.modified.action)}"
714 721 data-line-no="${line.modified.lineno}"
715 722 >
716 723 <div>
717 724
718 725 %if line.modified.get_comment_args:
719 726 <% line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args) %>
720 727 %else:
721 728 <% line_new_comments = None%>
722 729 %endif
723 730 %if line_new_comments:
724 731
725 732 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
726 733 % if has_outdated:
727 734 <i class="tooltip icon-comment-toggle" title="${_('comments including outdated: {}. Click here to display them.').format(len(line_new_comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
728 735 % else:
729 736 <i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(line_new_comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
730 737 % endif
731 738 %endif
732 739 </div>
733 740 </td>
734 741 <td class="cb-lineno ${action_class(line.modified.action)}"
735 742 data-line-no="${line.modified.lineno}"
736 743 %if new_line_anchor:
737 744 id="${new_line_anchor}"
738 745 %endif
739 746 >
740 747 %if line.modified.lineno:
741 748 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
742 749 %endif
743 750 </td>
744 751 <td class="cb-content ${action_class(line.modified.action)}"
745 752 data-line-no="n${line.modified.lineno}"
746 753 >
747 754 %if use_comments and line.modified.lineno:
748 755 ${render_add_comment_button()}
749 756 %endif
750 757 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
751 758 %if use_comments and line.modified.lineno and line_new_comments:
752 759 ${inline_comments_container(line_new_comments, active_pattern_entries=active_pattern_entries)}
753 760 %endif
761 % if line_action in ['+', '-'] and prev_line_action not in ['+', '-']:
762 <div class="nav-chunk" style="visibility: hidden">
763 <i class="icon-eye" title="viewing diff hunk-${hunk.index}-${chunk_count}"></i>
764 </div>
765 <% chunk_count +=1 %>
766 % endif
754 767 </td>
755 768 </tr>
756 769 %endfor
757 770 </%def>
758 771
759 772
760 773 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
761 774 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
762 775
763 776 <%
764 777 old_line_anchor, new_line_anchor = None, None
765 778 if old_line_no:
766 779 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
767 780 if new_line_no:
768 781 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
769 782 %>
770 783 <tr class="cb-line">
771 784 <td class="cb-data ${action_class(action)}">
772 785 <div>
773 786
774 787 %if comments_args:
775 788 <% comments = get_comments_for('unified', inline_comments, *comments_args) %>
776 789 %else:
777 790 <% comments = None %>
778 791 %endif
779 792
780 793 % if comments:
781 794 <% has_outdated = any([x.outdated for x in comments]) %>
782 795 % if has_outdated:
783 796 <i class="tooltip icon-comment-toggle" title="${_('comments including outdated: {}. Click here to display them.').format(len(comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
784 797 % else:
785 798 <i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
786 799 % endif
787 800 % endif
788 801 </div>
789 802 </td>
790 803 <td class="cb-lineno ${action_class(action)}"
791 804 data-line-no="${old_line_no}"
792 805 %if old_line_anchor:
793 806 id="${old_line_anchor}"
794 807 %endif
795 808 >
796 809 %if old_line_anchor:
797 810 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
798 811 %endif
799 812 </td>
800 813 <td class="cb-lineno ${action_class(action)}"
801 814 data-line-no="${new_line_no}"
802 815 %if new_line_anchor:
803 816 id="${new_line_anchor}"
804 817 %endif
805 818 >
806 819 %if new_line_anchor:
807 820 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
808 821 %endif
809 822 </td>
810 823 <td class="cb-content ${action_class(action)}"
811 824 data-line-no="${(new_line_no and 'n' or 'o')}${(new_line_no or old_line_no)}"
812 825 >
813 826 %if use_comments:
814 827 ${render_add_comment_button()}
815 828 %endif
816 829 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
817 830 %if use_comments and comments:
818 831 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
819 832 %endif
820 833 </td>
821 834 </tr>
822 835 %endfor
823 836 </%def>
824 837
825 838
826 839 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments, active_pattern_entries)">
827 840 % if diff_mode == 'unified':
828 841 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
829 842 % elif diff_mode == 'sideside':
830 843 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
831 844 % else:
832 845 <tr class="cb-line">
833 846 <td>unknown diff mode</td>
834 847 </tr>
835 848 % endif
836 849 </%def>file changes
837 850
838 851
839 852 <%def name="render_add_comment_button()">
840 853 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
841 854 <span><i class="icon-comment"></i></span>
842 855 </button>
843 856 </%def>
844 857
845 858 <%def name="render_diffset_menu(diffset, range_diff_on=None)">
846 859 <% diffset_container_id = h.md5(diffset.target_ref) %>
847 860
848 861 <div id="diff-file-sticky" class="diffset-menu clearinner">
849 862 ## auto adjustable
850 863 <div class="sidebar__inner">
851 864 <div class="sidebar__bar">
852 865 <div class="pull-right">
853 866 <div class="btn-group">
854 867 <a class="btn tooltip toggle-wide-diff" href="#toggle-wide-diff" onclick="toggleWideDiff(this); return false" title="${h.tooltip(_('Toggle wide diff'))}">
855 868 <i class="icon-wide-mode"></i>
856 869 </a>
857 870 </div>
858 871 <div class="btn-group">
859 872
860 873 <a
861 874 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-active')} tooltip"
862 875 title="${h.tooltip(_('View diff as side by side'))}"
863 876 href="${h.current_route_path(request, diffmode='sideside')}">
864 877 <span>${_('Side by Side')}</span>
865 878 </a>
866 879
867 880 <a
868 881 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-active')} tooltip"
869 882 title="${h.tooltip(_('View diff as unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
870 883 <span>${_('Unified')}</span>
871 884 </a>
872 885
873 886 % if range_diff_on is True:
874 887 <a
875 888 title="${_('Turn off: Show the diff as commit range')}"
876 889 class="btn btn-primary"
877 890 href="${h.current_route_path(request, **{"range-diff":"0"})}">
878 891 <span>${_('Range Diff')}</span>
879 892 </a>
880 893 % elif range_diff_on is False:
881 894 <a
882 895 title="${_('Show the diff as commit range')}"
883 896 class="btn"
884 897 href="${h.current_route_path(request, **{"range-diff":"1"})}">
885 898 <span>${_('Range Diff')}</span>
886 899 </a>
887 900 % endif
888 901 </div>
889 902 <div class="btn-group">
890 903
891 904 <div class="pull-left">
892 905 ${h.hidden('diff_menu_{}'.format(diffset_container_id))}
893 906 </div>
894 907
895 908 </div>
896 909 </div>
897 910 <div class="pull-left">
898 911 <div class="btn-group">
899 912 <div class="pull-left">
900 913 ${h.hidden('file_filter_{}'.format(diffset_container_id))}
901 914 </div>
902 915
903 916 </div>
904 917 </div>
905 918 </div>
906 <div class="fpath-placeholder">
919 <div class="fpath-placeholder pull-left">
907 920 <i class="icon-file-text"></i>
908 921 <strong class="fpath-placeholder-text">
909 922 Context file:
910 923 </strong>
911 924 </div>
925 <div class="pull-right noselect">
926 <span id="diff_nav">Loading diff...:</span>
927 <span class="cursor-pointer" onclick="scrollToPrevChunk(); return false">
928 <i class="icon-angle-up"></i>
929 </span>
930 <span class="cursor-pointer" onclick="scrollToNextChunk(); return false">
931 <i class="icon-angle-down"></i>
932 </span>
933 </div>
912 934 <div class="sidebar_inner_shadow"></div>
913 935 </div>
914 936 </div>
915 937
916 938 % if diffset:
917 939 %if diffset.limited_diff:
918 940 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
919 941 %else:
920 942 <% file_placeholder = h.literal(_ungettext('%(num)s file changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>', '%(num)s files changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>',
921 943 diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}) %>
922 944
923 945 %endif
924 946 ## case on range-diff placeholder needs to be updated
925 947 % if range_diff_on is True:
926 948 <% file_placeholder = _('Disabled on range diff') %>
927 949 % endif
928 950
929 951 <script type="text/javascript">
930 952 var feedFilesOptions = function (query, initialData) {
931 953 var data = {results: []};
932 954 var isQuery = typeof query.term !== 'undefined';
933 955
934 956 var section = _gettext('Changed files');
935 957 var filteredData = [];
936 958
937 959 //filter results
938 960 $.each(initialData.results, function (idx, value) {
939 961
940 962 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
941 963 filteredData.push({
942 964 'id': this.id,
943 965 'text': this.text,
944 966 "ops": this.ops,
945 967 })
946 968 }
947 969
948 970 });
949 971
950 972 data.results = filteredData;
951 973
952 974 query.callback(data);
953 975 };
954 976
955 977 var selectionFormatter = function(data, escapeMarkup) {
956 978 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
957 979 var tmpl = '<div><strong>{0}</strong></div>'.format(escapeMarkup(data['text']));
958 980 var pill = '<div class="pill-group" style="position: absolute; top:7px; right: 0">' +
959 981 '<span class="pill" op="added">{0}</span>' +
960 982 '<span class="pill" op="deleted">{1}</span>' +
961 983 '</div>'
962 984 ;
963 985 var added = data['ops']['added'];
964 986 if (added === 0) {
965 987 // don't show +0
966 988 added = 0;
967 989 } else {
968 990 added = '+' + added;
969 991 }
970 992
971 993 var deleted = -1*data['ops']['deleted'];
972 994
973 995 tmpl += pill.format(added, deleted);
974 996 return container.format(tmpl);
975 997 };
976 998 var formatFileResult = function(result, container, query, escapeMarkup) {
977 999 return selectionFormatter(result, escapeMarkup);
978 1000 };
979 1001
980 1002 var formatSelection = function (data, container) {
981 1003 return '${file_placeholder}'
982 1004 };
983 1005
984 1006 if (window.preloadFileFilterData === undefined) {
985 1007 window.preloadFileFilterData = {}
986 1008 }
987 1009
988 1010 preloadFileFilterData["${diffset_container_id}"] = {
989 1011 results: [
990 1012 % for filediff in diffset.files:
991 1013 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
992 1014 text:"${filediff.patch['filename']}",
993 1015 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
994 1016 % endfor
995 1017 ]
996 1018 };
997 1019
998 1020 var diffFileFilterId = "#file_filter_" + "${diffset_container_id}";
999 1021 var diffFileFilter = $(diffFileFilterId).select2({
1000 1022 'dropdownAutoWidth': true,
1001 1023 'width': 'auto',
1002 1024
1003 1025 containerCssClass: "drop-menu",
1004 1026 dropdownCssClass: "drop-menu-dropdown",
1005 1027 data: preloadFileFilterData["${diffset_container_id}"],
1006 1028 query: function(query) {
1007 1029 feedFilesOptions(query, preloadFileFilterData["${diffset_container_id}"]);
1008 1030 },
1009 1031 initSelection: function(element, callback) {
1010 1032 callback({'init': true});
1011 1033 },
1012 1034 formatResult: formatFileResult,
1013 1035 formatSelection: formatSelection
1014 1036 });
1015 1037
1016 1038 % if range_diff_on is True:
1017 1039 diffFileFilter.select2("enable", false);
1018 1040 % endif
1019 1041
1020 1042 $(diffFileFilterId).on('select2-selecting', function (e) {
1021 1043 var idSelector = e.choice.id;
1022 1044
1023 1045 // expand the container if we quick-select the field
1024 1046 $('#'+idSelector).next().prop('checked', false);
1025 1047 // hide the mast as we later do preventDefault()
1026 1048 $("#select2-drop-mask").click();
1027 1049
1028 1050 window.location.hash = '#'+idSelector;
1029 1051 updateSticky();
1030 1052
1031 1053 e.preventDefault();
1032 1054 });
1033 1055
1056 getCurrentChunk = function () {
1057
1058 var chunksAll = $('.nav-chunk').filter(function () {
1059 return $(this).parents('.filediff').prev().get(0).checked !== true
1060 })
1061 var chunkSelected = $('.nav-chunk.selected');
1062 var initial = false;
1063
1064 if (chunkSelected.length === 0) {
1065 // no initial chunk selected, we pick first
1066 chunkSelected = $(chunksAll.get(0));
1067 var initial = true;
1068 }
1069
1070 return {
1071 'all': chunksAll,
1072 'selected': chunkSelected,
1073 'initial': initial,
1074 }
1075 }
1076
1077 animateDiffNavText = function () {
1078 var $diffNav = $('#diff_nav')
1079
1080 var callback = function () {
1081 $diffNav.animate({'opacity': 1.00}, 200)
1082 };
1083 $diffNav.animate({'opacity': 0.15}, 200, callback);
1084 }
1085
1086 scrollToChunk = function (moveBy) {
1087 var chunk = getCurrentChunk();
1088 var all = chunk.all
1089 var selected = chunk.selected
1090
1091 var curPos = all.index(selected);
1092 var newPos = curPos;
1093 if (!chunk.initial) {
1094 var newPos = curPos + moveBy;
1095 }
1096
1097 var curElem = all.get(newPos);
1098
1099 if (curElem === undefined) {
1100 // end or back
1101 $('#diff_nav').html('No next diff element.')
1102 animateDiffNavText()
1103 return
1104 } else if (newPos < 0) {
1105 $('#diff_nav').html('No previous diff element.')
1106 animateDiffNavText()
1107 return
1108 } else {
1109 $('#diff_nav').html('Diff navigation:')
1110 }
1111
1112 curElem = $(curElem)
1113 var offset = 100;
1114 $(window).scrollTop(curElem.position().top - offset);
1115
1116 //clear selection
1117 all.removeClass('selected')
1118 curElem.addClass('selected')
1119 }
1120
1121 scrollToPrevChunk = function () {
1122 scrollToChunk(-1)
1123 }
1124 scrollToNextChunk = function () {
1125 scrollToChunk(1)
1126 }
1127
1034 1128 </script>
1035 1129 % endif
1036 1130
1037 1131 <script type="text/javascript">
1132 $('#diff_nav').html('loading diff...') // wait until whole page is loaded
1133
1038 1134 $(document).ready(function () {
1039 1135
1040 1136 var contextPrefix = _gettext('Context file: ');
1041 1137 ## sticky sidebar
1042 1138 var sidebarElement = document.getElementById('diff-file-sticky');
1043 1139 sidebar = new StickySidebar(sidebarElement, {
1044 1140 topSpacing: 0,
1045 1141 bottomSpacing: 0,
1046 1142 innerWrapperSelector: '.sidebar__inner'
1047 1143 });
1048 1144 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
1049 1145 // reset our file so it's not holding new value
1050 1146 $('.fpath-placeholder-text').html(contextPrefix + ' - ')
1051 1147 });
1052 1148
1053 1149 updateSticky = function () {
1054 1150 sidebar.updateSticky();
1055 1151 Waypoint.refreshAll();
1056 1152 };
1057 1153
1058 1154 var animateText = function (fPath, anchorId) {
1059 1155 fPath = Select2.util.escapeMarkup(fPath);
1060 1156 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
1061 1157 };
1062 1158
1063 1159 ## dynamic file waypoints
1064 1160 var setFPathInfo = function(fPath, anchorId){
1065 1161 animateText(fPath, anchorId)
1066 1162 };
1067 1163
1068 1164 var codeBlock = $('.filediff');
1069 1165
1070 1166 // forward waypoint
1071 1167 codeBlock.waypoint(
1072 1168 function(direction) {
1073 1169 if (direction === "down"){
1074 1170 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1075 1171 }
1076 1172 }, {
1077 1173 offset: function () {
1078 1174 return 70;
1079 1175 },
1080 1176 context: '.fpath-placeholder'
1081 1177 }
1082 1178 );
1083 1179
1084 1180 // backward waypoint
1085 1181 codeBlock.waypoint(
1086 1182 function(direction) {
1087 1183 if (direction === "up"){
1088 1184 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1089 1185 }
1090 1186 }, {
1091 1187 offset: function () {
1092 1188 return -this.element.clientHeight + 90;
1093 1189 },
1094 1190 context: '.fpath-placeholder'
1095 1191 }
1096 1192 );
1097 1193
1098 1194 toggleWideDiff = function (el) {
1099 1195 updateSticky();
1100 1196 var wide = Rhodecode.comments.toggleWideMode(this);
1101 1197 storeUserSessionAttr('rc_user_session_attr.wide_diff_mode', wide);
1102 1198 if (wide === true) {
1103 1199 $(el).addClass('btn-active');
1104 1200 } else {
1105 1201 $(el).removeClass('btn-active');
1106 1202 }
1107 1203 return null;
1108 1204 };
1109 1205
1110 1206 var preloadDiffMenuData = {
1111 1207 results: [
1112 1208
1113 1209 ## Whitespace change
1114 1210 % if request.GET.get('ignorews', '') == '1':
1115 1211 {
1116 1212 id: 2,
1117 1213 text: _gettext('Show whitespace changes'),
1118 1214 action: function () {},
1119 1215 url: "${h.current_route_path(request, ignorews=0)|n}"
1120 1216 },
1121 1217 % else:
1122 1218 {
1123 1219 id: 2,
1124 1220 text: _gettext('Hide whitespace changes'),
1125 1221 action: function () {},
1126 1222 url: "${h.current_route_path(request, ignorews=1)|n}"
1127 1223 },
1128 1224 % endif
1129 1225
1130 1226 ## FULL CONTEXT
1131 1227 % if request.GET.get('fullcontext', '') == '1':
1132 1228 {
1133 1229 id: 3,
1134 1230 text: _gettext('Hide full context diff'),
1135 1231 action: function () {},
1136 1232 url: "${h.current_route_path(request, fullcontext=0)|n}"
1137 1233 },
1138 1234 % else:
1139 1235 {
1140 1236 id: 3,
1141 1237 text: _gettext('Show full context diff'),
1142 1238 action: function () {},
1143 1239 url: "${h.current_route_path(request, fullcontext=1)|n}"
1144 1240 },
1145 1241 % endif
1146 1242
1147 1243 ]
1148 1244 };
1149 1245
1150 1246 var diffMenuId = "#diff_menu_" + "${diffset_container_id}";
1151 1247 $(diffMenuId).select2({
1152 1248 minimumResultsForSearch: -1,
1153 1249 containerCssClass: "drop-menu-no-width",
1154 1250 dropdownCssClass: "drop-menu-dropdown",
1155 1251 dropdownAutoWidth: true,
1156 1252 data: preloadDiffMenuData,
1157 1253 placeholder: "${_('...')}",
1158 1254 });
1159 1255 $(diffMenuId).on('select2-selecting', function (e) {
1160 1256 e.choice.action();
1161 1257 if (e.choice.url !== null) {
1162 1258 window.location = e.choice.url
1163 1259 }
1164 1260 });
1165 1261 toggleExpand = function (el, diffsetEl) {
1166 1262 var el = $(el);
1167 1263 if (el.hasClass('collapsed')) {
1168 1264 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', false);
1169 1265 el.removeClass('collapsed');
1170 1266 el.html(
1171 1267 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1172 1268 _gettext('Collapse all files'));
1173 1269 }
1174 1270 else {
1175 1271 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', true);
1176 1272 el.addClass('collapsed');
1177 1273 el.html(
1178 1274 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1179 1275 _gettext('Expand all files'));
1180 1276 }
1181 1277 updateSticky()
1182 1278 };
1183 1279
1184 1280 toggleCommitExpand = function (el) {
1185 1281 var $el = $(el);
1186 1282 var commits = $el.data('toggleCommitsCnt');
1187 1283 var collapseMsg = _ngettext('Collapse {0} commit', 'Collapse {0} commits', commits).format(commits);
1188 1284 var expandMsg = _ngettext('Expand {0} commit', 'Expand {0} commits', commits).format(commits);
1189 1285
1190 1286 if ($el.hasClass('collapsed')) {
1191 1287 $('.compare_select').show();
1192 1288 $('.compare_select_hidden').hide();
1193 1289
1194 1290 $el.removeClass('collapsed');
1195 1291 $el.html(
1196 1292 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1197 1293 collapseMsg);
1198 1294 }
1199 1295 else {
1200 1296 $('.compare_select').hide();
1201 1297 $('.compare_select_hidden').show();
1202 1298 $el.addClass('collapsed');
1203 1299 $el.html(
1204 1300 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1205 1301 expandMsg);
1206 1302 }
1207 1303 updateSticky();
1208 1304 };
1209 1305
1210 1306 // get stored diff mode and pre-enable it
1211 1307 if (templateContext.session_attrs.wide_diff_mode === "true") {
1212 1308 Rhodecode.comments.toggleWideMode(null);
1213 1309 $('.toggle-wide-diff').addClass('btn-active');
1214 1310 updateSticky();
1215 1311 }
1312
1313 // DIFF NAV //
1314
1315 // element to detect scroll direction of
1316 var $window = $(window);
1317
1318 // initialize last scroll position
1319 var lastScrollY = $window.scrollTop();
1320
1321 $window.on('resize scrollstop', {latency: 350}, function () {
1322 var visibleChunks = $('.nav-chunk').withinviewport({top: 75});
1323
1324 // get current scroll position
1325 var currentScrollY = $window.scrollTop();
1326
1327 // determine current scroll direction
1328 if (currentScrollY > lastScrollY) {
1329 var y = 'down'
1330 } else if (currentScrollY !== lastScrollY) {
1331 var y = 'up';
1332 }
1333
1334 var pos = -1; // by default we use last element in viewport
1335 if (y === 'down') {
1336 pos = -1;
1337 } else if (y === 'up') {
1338 pos = 0;
1339 }
1340
1341 if (visibleChunks.length > 0) {
1342 $('.nav-chunk').removeClass('selected');
1343 $(visibleChunks.get(pos)).addClass('selected');
1344 }
1345
1346 // update last scroll position to current position
1347 lastScrollY = currentScrollY;
1348
1349 });
1350 $('#diff_nav').html('Diff navigation:')
1351
1216 1352 });
1217 1353 </script>
1218 1354
1219 1355 </%def>
General Comments 0
You need to be logged in to leave comments. Login now