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