Commit d17b779c authored by Valery Sizov's avatar Valery Sizov

Merge branch 'ce_upstream' into 'master'

CE upstream

Closes omnibus-gitlab#1361

See merge request !1319
parents 1adb3ea6 f3185151
...@@ -183,6 +183,12 @@ entry. ...@@ -183,6 +183,12 @@ entry.
- Remove deprecated GitlabCiService. - Remove deprecated GitlabCiService.
- Requeue pending deletion projects. - Requeue pending deletion projects.
## 8.16.7 (2017-02-27)
- No changes.
- No changes.
- Fix MR changes tab size count when there are over 100 files in the diff.
## 8.16.6 (2017-02-17) ## 8.16.6 (2017-02-17)
- API: Fix file downloading. !0 (8267) - API: Fix file downloading. !0 (8267)
......
...@@ -7,8 +7,6 @@ ...@@ -7,8 +7,6 @@
/* global Aside */ /* global Aside */
window.$ = window.jQuery = require('jquery'); window.$ = window.jQuery = require('jquery');
require('jquery-ui/ui/draggable');
require('jquery-ui/ui/sortable');
require('jquery-ujs'); require('jquery-ujs');
require('vendor/jquery.endless-scroll'); require('vendor/jquery.endless-scroll');
require('vendor/jquery.highlight'); require('vendor/jquery.highlight');
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
var DOWN_BUILD_TRACE = '#down-build-trace'; var DOWN_BUILD_TRACE = '#down-build-trace';
this.Build = (function() { this.Build = (function() {
Build.interval = null; Build.timeout = null;
Build.state = null; Build.state = null;
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
this.$scrollBottomBtn = $('#scroll-bottom'); this.$scrollBottomBtn = $('#scroll-bottom');
this.$buildRefreshAnimation = $('.js-build-refresh'); this.$buildRefreshAnimation = $('.js-build-refresh');
clearInterval(Build.interval); clearTimeout(Build.timeout);
// Init breakpoint checker // Init breakpoint checker
this.bp = Breakpoints.get(); this.bp = Breakpoints.get();
...@@ -52,17 +52,7 @@ ...@@ -52,17 +52,7 @@
this.getInitialBuildTrace(); this.getInitialBuildTrace();
this.initScrollButtonAffix(); this.initScrollButtonAffix();
} }
if (this.buildStatus === "running" || this.buildStatus === "pending") { this.invokeBuildTrace();
Build.interval = setInterval((function(_this) {
// Check for new build output if user still watching build page
// Only valid for runnig build when output changes during time
return function() {
if (_this.location() === _this.pageUrl) {
return _this.getBuildTrace();
}
};
})(this), 4000);
}
} }
Build.prototype.initSidebar = function() { Build.prototype.initSidebar = function() {
...@@ -75,6 +65,22 @@ ...@@ -75,6 +65,22 @@
return window.location.href.split("#")[0]; return window.location.href.split("#")[0];
}; };
Build.prototype.invokeBuildTrace = function() {
var continueRefreshStatuses = ['running', 'pending'];
// Continue to update build trace when build is running or pending
if (continueRefreshStatuses.indexOf(this.buildStatus) !== -1) {
// Check for new build output if user still watching build page
// Only valid for runnig build when output changes during time
Build.timeout = setTimeout((function(_this) {
return function() {
if (_this.location() === _this.pageUrl) {
return _this.getBuildTrace();
}
};
})(this), 4000);
}
};
Build.prototype.getInitialBuildTrace = function() { Build.prototype.getInitialBuildTrace = function() {
var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']; var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'];
...@@ -86,7 +92,7 @@ ...@@ -86,7 +92,7 @@
if (window.location.hash === DOWN_BUILD_TRACE) { if (window.location.hash === DOWN_BUILD_TRACE) {
$("html,body").scrollTop(this.$buildTrace.height()); $("html,body").scrollTop(this.$buildTrace.height());
} }
if (removeRefreshStatuses.indexOf(buildData.status) >= 0) { if (removeRefreshStatuses.indexOf(buildData.status) !== -1) {
this.$buildRefreshAnimation.remove(); this.$buildRefreshAnimation.remove();
return this.initScrollMonitor(); return this.initScrollMonitor();
} }
...@@ -105,6 +111,7 @@ ...@@ -105,6 +111,7 @@
if (log.state) { if (log.state) {
_this.state = log.state; _this.state = log.state;
} }
_this.invokeBuildTrace();
if (log.status === "running") { if (log.status === "running") {
if (log.append) { if (log.append) {
$('.js-build-output').append(log.html); $('.js-build-output').append(log.html);
......
...@@ -52,6 +52,30 @@ ...@@ -52,6 +52,30 @@
return this.views[viewMode].call(this); return this.views[viewMode].call(this);
}; };
ImageFile.prototype.initDraggable = function($el, padding, callback) {
var dragging = false;
var $body = $('body');
var $offsetEl = $el.parent();
$el.off('mousedown').on('mousedown', function() {
dragging = true;
$body.css('user-select', 'none');
});
$body.off('mouseup').off('mousemove').on('mouseup', function() {
dragging = false;
$body.css('user-select', '');
})
.on('mousemove', function(e) {
var left;
if (!dragging) return;
left = e.pageX - ($offsetEl.offset().left + padding);
callback(e, left);
});
};
prepareFrames = function(view) { prepareFrames = function(view) {
var maxHeight, maxWidth; var maxHeight, maxWidth;
maxWidth = 0; maxWidth = 0;
...@@ -96,26 +120,30 @@ ...@@ -96,26 +120,30 @@
maxHeight = 0; maxHeight = 0;
return $('.swipe.view', this.file).each((function(_this) { return $('.swipe.view', this.file).each((function(_this) {
return function(index, view) { return function(index, view) {
var ref; var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref;
ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1]; ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
$('.swipe-frame', view).css({ $swipeFrame = $('.swipe-frame', view);
$swipeWrap = $('.swipe-wrap', view);
$swipeBar = $('.swipe-bar', view);
$swipeFrame.css({
width: maxWidth + 16, width: maxWidth + 16,
height: maxHeight + 28 height: maxHeight + 28
}); });
$('.swipe-wrap', view).css({ $swipeWrap.css({
width: maxWidth + 1, width: maxWidth + 1,
height: maxHeight + 2 height: maxHeight + 2
}); });
return $('.swipe-bar', view).css({ $swipeBar.css({
left: 0 left: 0
}).draggable({ });
axis: 'x',
containment: 'parent', wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10);
drag: function(event) {
return $('.swipe-wrap', view).width((maxWidth + 1) - $(this).position().left); _this.initDraggable($swipeBar, wrapPadding, function(e, left) {
}, if (left > 0 && left < $swipeFrame.width() - (wrapPadding * 2)) {
stop: function(event) { $swipeWrap.width((maxWidth + 1) - left);
return $('.swipe-wrap', view).width((maxWidth + 1) - $(this).position().left); $swipeBar.css('left', left);
} }
}); });
}; };
...@@ -128,9 +156,14 @@ ...@@ -128,9 +156,14 @@
dragTrackWidth = $('.drag-track', this.file).width() - $('.dragger', this.file).width(); dragTrackWidth = $('.drag-track', this.file).width() - $('.dragger', this.file).width();
return $('.onion-skin.view', this.file).each((function(_this) { return $('.onion-skin.view', this.file).each((function(_this) {
return function(index, view) { return function(index, view) {
var ref; var $frame, $track, $dragger, $frameAdded, framePadding, ref, dragging = false;
ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1]; ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
$('.onion-skin-frame', view).css({ $frame = $('.onion-skin-frame', view);
$frameAdded = $('.frame.added', view);
$track = $('.drag-track', view);
$dragger = $('.dragger', $track);
$frame.css({
width: maxWidth + 16, width: maxWidth + 16,
height: maxHeight + 28 height: maxHeight + 28
}); });
...@@ -138,16 +171,18 @@ ...@@ -138,16 +171,18 @@
width: maxWidth + 1, width: maxWidth + 1,
height: maxHeight + 2 height: maxHeight + 2
}); });
return $('.dragger', view).css({ $dragger.css({
left: dragTrackWidth left: dragTrackWidth
}).draggable({ });
axis: 'x',
containment: 'parent', framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10);
drag: function(event) {
return $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth); _this.initDraggable($dragger, framePadding, function(e, left) {
}, var opacity = left / dragTrackWidth;
stop: function(event) {
return $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth); if (opacity >= 0 && opacity <= 1) {
$dragger.css('left', left);
$frameAdded.css('opacity', opacity);
} }
}); });
}; };
......
...@@ -523,32 +523,30 @@ module.exports = Vue.component('environment-item', { ...@@ -523,32 +523,30 @@ module.exports = Vue.component('environment-item', {
</span> </span>
</td> </td>
<td class="hidden-xs"> <td class="hidden-xs environments-actions">
<div v-if="!model.isFolder"> <div v-if="!model.isFolder" class="btn-group pull-right" role="group">
<div class="btn-group" role="group"> <actions-component v-if="hasManualActions && canCreateDeployment"
<actions-component v-if="hasManualActions && canCreateDeployment" :play-icon-svg="playIconSvg"
:play-icon-svg="playIconSvg" :actions="manualActions">
:actions="manualActions"> </actions-component>
</actions-component>
<external-url-component v-if="externalURL && canReadEnvironment"
<external-url-component v-if="externalURL && canReadEnvironment" :external-url="externalURL">
:external-url="externalURL"> </external-url-component>
</external-url-component>
<stop-component v-if="hasStopAction && canCreateDeployment"
<stop-component v-if="hasStopAction && canCreateDeployment" :stop-url="model.stop_path">
:stop-url="model.stop_path"> </stop-component>
</stop-component>
<terminal-button-component v-if="model && model.terminal_path"
<terminal-button-component v-if="model && model.terminal_path" :terminal-icon-svg="terminalIconSvg"
:terminal-icon-svg="terminalIconSvg" :terminal-path="model.terminal_path">
:terminal-path="model.terminal_path"> </terminal-button-component>
</terminal-button-component>
<rollback-component v-if="canRetry && canCreateDeployment"
<rollback-component v-if="canRetry && canCreateDeployment" :is-last-deployment="isLastDeployment"
:is-last-deployment="isLastDeployment" :retry-url="retryUrl">
:retry-url="retryUrl"> </rollback-component>
</rollback-component>
</div>
</div> </div>
</td> </td>
</tr> </tr>
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */
/* global FilesCommentButton */ /* global FilesCommentButton */
/* global notes */
(function() { (function() {
let $commentButtonTemplate;
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.FilesCommentButton = (function() { this.FilesCommentButton = (function() {
var COMMENT_BUTTON_CLASS, COMMENT_BUTTON_TEMPLATE, DEBOUNCE_TIMEOUT_DURATION, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS; var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS;
COMMENT_BUTTON_CLASS = '.add-diff-note'; COMMENT_BUTTON_CLASS = '.add-diff-note';
COMMENT_BUTTON_TEMPLATE = _.template('<button name="button" type="submit" class="btn <%- COMMENT_BUTTON_CLASS %> js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>');
LINE_HOLDER_CLASS = '.line_holder'; LINE_HOLDER_CLASS = '.line_holder';
LINE_NUMBER_CLASS = 'diff-line-num'; LINE_NUMBER_CLASS = 'diff-line-num';
...@@ -27,26 +27,29 @@ ...@@ -27,26 +27,29 @@
TEXT_FILE_SELECTOR = '.text-file'; TEXT_FILE_SELECTOR = '.text-file';
DEBOUNCE_TIMEOUT_DURATION = 100;
function FilesCommentButton(filesContainerElement) { function FilesCommentButton(filesContainerElement) {
var debounce;
this.filesContainerElement = filesContainerElement;
this.destroy = bind(this.destroy, this);
this.render = bind(this.render, this); this.render = bind(this.render, this);
this.VIEW_TYPE = $('input#view[type=hidden]').val(); this.hideButton = bind(this.hideButton, this);
debounce = _.debounce(this.render, DEBOUNCE_TIMEOUT_DURATION); this.isParallelView = notes.isParallelView();
$(this.filesContainerElement).off('mouseover', LINE_COLUMN_CLASSES).off('mouseleave', LINE_COLUMN_CLASSES).on('mouseover', LINE_COLUMN_CLASSES, debounce).on('mouseleave', LINE_COLUMN_CLASSES, this.destroy); filesContainerElement.on('mouseover', LINE_COLUMN_CLASSES, this.render)
.on('mouseleave', LINE_COLUMN_CLASSES, this.hideButton);
} }
FilesCommentButton.prototype.render = function(e) { FilesCommentButton.prototype.render = function(e) {
var $currentTarget, buttonParentElement, lineContentElement, textFileElement; var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button;
$currentTarget = $(e.currentTarget); $currentTarget = $(e.currentTarget);
buttonParentElement = this.getButtonParent($currentTarget);
if (!this.validateButtonParent(buttonParentElement)) return;
lineContentElement = this.getLineContent($currentTarget); lineContentElement = this.getLineContent($currentTarget);
if (!this.validateLineContent(lineContentElement)) return; buttonParentElement = this.getButtonParent($currentTarget);
if (!this.validateButtonParent(buttonParentElement) || !this.validateLineContent(lineContentElement)) return;
$button = $(COMMENT_BUTTON_CLASS, buttonParentElement);
buttonParentElement.addClass('is-over')
.nextUntil(`.${LINE_CONTENT_CLASS}`).addClass('is-over');
if ($button.length) {
return;
}
textFileElement = this.getTextFileElement($currentTarget); textFileElement = this.getTextFileElement($currentTarget);
buttonParentElement.append(this.buildButton({ buttonParentElement.append(this.buildButton({
...@@ -61,19 +64,16 @@ ...@@ -61,19 +64,16 @@
})); }));
}; };
FilesCommentButton.prototype.destroy = function(e) { FilesCommentButton.prototype.hideButton = function(e) {
if (this.isMovingToSameType(e)) { var $currentTarget = $(e.currentTarget);
return; var buttonParentElement = this.getButtonParent($currentTarget);
}
$(COMMENT_BUTTON_CLASS, this.getButtonParent($(e.currentTarget))).remove(); buttonParentElement.removeClass('is-over')
.nextUntil(`.${LINE_CONTENT_CLASS}`).removeClass('is-over');
}; };
FilesCommentButton.prototype.buildButton = function(buttonAttributes) { FilesCommentButton.prototype.buildButton = function(buttonAttributes) {
var initializedButtonTemplate; return $commentButtonTemplate.clone().attr({
initializedButtonTemplate = COMMENT_BUTTON_TEMPLATE({
COMMENT_BUTTON_CLASS: COMMENT_BUTTON_CLASS.substr(1)
});
return $(initializedButtonTemplate).attr({
'data-noteable-type': buttonAttributes.noteableType, 'data-noteable-type': buttonAttributes.noteableType,
'data-noteable-id': buttonAttributes.noteableID, 'data-noteable-id': buttonAttributes.noteableID,
'data-commit-id': buttonAttributes.commitID, 'data-commit-id': buttonAttributes.commitID,
...@@ -86,14 +86,14 @@ ...@@ -86,14 +86,14 @@
}; };
FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) { FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) {
return $(hoveredElement.closest(TEXT_FILE_SELECTOR)); return hoveredElement.closest(TEXT_FILE_SELECTOR);
}; };
FilesCommentButton.prototype.getLineContent = function(hoveredElement) { FilesCommentButton.prototype.getLineContent = function(hoveredElement) {
if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) { if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) {
return hoveredElement; return hoveredElement;
} }
if (this.VIEW_TYPE === 'inline') { if (!this.isParallelView) {
return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS); return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS);
} else { } else {
return $(hoveredElement).next("." + LINE_CONTENT_CLASS); return $(hoveredElement).next("." + LINE_CONTENT_CLASS);
...@@ -101,7 +101,7 @@ ...@@ -101,7 +101,7 @@
}; };
FilesCommentButton.prototype.getButtonParent = function(hoveredElement) { FilesCommentButton.prototype.getButtonParent = function(hoveredElement) {
if (this.VIEW_TYPE === 'inline') { if (!this.isParallelView) {
if (hoveredElement.hasClass(OLD_LINE_CLASS)) { if (hoveredElement.hasClass(OLD_LINE_CLASS)) {
return hoveredElement; return hoveredElement;
} }
...@@ -114,17 +114,8 @@ ...@@ -114,17 +114,8 @@
} }
}; };
FilesCommentButton.prototype.isMovingToSameType = function(e) {
var newButtonParent;
newButtonParent = this.getButtonParent($(e.toElement));
if (!newButtonParent) {
return false;
}
return newButtonParent.is(this.getButtonParent($(e.currentTarget)));
};
FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) { FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) {
return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS) && $(COMMENT_BUTTON_CLASS, buttonParentElement).length === 0; return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS);
}; };
FilesCommentButton.prototype.validateLineContent = function(lineContentElement) { FilesCommentButton.prototype.validateLineContent = function(lineContentElement) {
...@@ -135,6 +126,8 @@ ...@@ -135,6 +126,8 @@
})(); })();
$.fn.filesCommentButton = function() { $.fn.filesCommentButton = function() {
$commentButtonTemplate = $('<button name="button" type="submit" class="add-diff-note js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>');
if (!(this && (this.parent().data('can-create-note') != null))) { if (!(this && (this.parent().data('can-create-note') != null))) {
return; return;
} }
......
...@@ -48,7 +48,11 @@ ...@@ -48,7 +48,11 @@
} }
setOffset(offset = 0) { setOffset(offset = 0) {
this.dropdown.style.left = `${offset}px`; if (window.innerWidth > 480) {
this.dropdown.style.left = `${offset}px`;
} else {
this.dropdown.style.left = '0px';
}
} }
renderContent(forceShowList = false) { renderContent(forceShowList = false) {
......
...@@ -63,7 +63,7 @@ ...@@ -63,7 +63,7 @@
} }
GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) { GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) {
return BLUR_KEYCODES.indexOf(keyCode) >= 0; return BLUR_KEYCODES.indexOf(keyCode) !== -1;
}; };
GitLabDropdownFilter.prototype.filter = function(search_text) { GitLabDropdownFilter.prototype.filter = function(search_text) {
...@@ -605,7 +605,7 @@ ...@@ -605,7 +605,7 @@
var occurrences; var occurrences;
occurrences = fuzzaldrinPlus.match(text, term); occurrences = fuzzaldrinPlus.match(text, term);
return text.split('').map(function(character, i) { return text.split('').map(function(character, i) {
if (indexOf.call(occurrences, i) >= 0) { if (indexOf.call(occurrences, i) !== -1) {
return "<b>" + character + "</b>"; return "<b>" + character + "</b>";
} else { } else {
return character; return character;
...@@ -748,7 +748,7 @@ ...@@ -748,7 +748,7 @@
return function(e) { return function(e) {
var $listItems, PREV_INDEX, currentKeyCode; var $listItems, PREV_INDEX, currentKeyCode;
currentKeyCode = e.which; currentKeyCode = e.which;
if (ARROW_KEY_CODES.indexOf(currentKeyCode) >= 0) { if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) {
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
PREV_INDEX = currentIndex; PREV_INDEX = currentIndex;
......
...@@ -116,7 +116,7 @@ ...@@ -116,7 +116,7 @@
formData = $.param(formData); formData = $.param(formData);
formAction = form.attr('action'); formAction = form.attr('action');
issuesUrl = formAction; issuesUrl = formAction;
issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&'); issuesUrl += "" + (formAction.indexOf('?') === -1 ? '?' : '&');
issuesUrl += formData; issuesUrl += formData;
return gl.utils.visitUrl(issuesUrl); return gl.utils.visitUrl(issuesUrl);
}; };
......
...@@ -329,17 +329,18 @@ ...@@ -329,17 +329,18 @@
* ``` * ```
*/ */
w.gl.utils.backOff = (fn, timeout = 60000) => { w.gl.utils.backOff = (fn, timeout = 60000) => {
const maxInterval = 32000;
let nextInterval = 2000; let nextInterval = 2000;
const startTime = (+new Date()); const startTime = Date.now();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg)); const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg));
const next = () => { const next = () => {
if (new Date().getTime() - startTime < timeout) { if (Date.now() - startTime < timeout) {
setTimeout(fn.bind(null, next, stop), nextInterval); setTimeout(fn.bind(null, next, stop), nextInterval);
nextInterval *= 2; nextInterval = Math.min(nextInterval + nextInterval, maxInterval);
} else { } else {
reject(new Error('BACKOFF_TIMEOUT')); reject(new Error('BACKOFF_TIMEOUT'));
} }
......
...@@ -82,7 +82,7 @@ require('./smart_interval'); ...@@ -82,7 +82,7 @@ require('./smart_interval');
return function() { return function() {
var page; var page;
page = $('body').data('page').split(':').last(); page = $('body').data('page').split(':').last();
if (allowedPages.indexOf(page) < 0) { if (allowedPages.indexOf(page) === -1) {
return _this.clearEventListeners(); return _this.clearEventListeners();
} }
}; };
...@@ -255,7 +255,7 @@ require('./smart_interval'); ...@@ -255,7 +255,7 @@ require('./smart_interval');
} }
$('.ci_widget').hide(); $('.ci_widget').hide();
allowed_states = ["failed", "canceled", "running", "pending", "success", "success_with_warnings", "skipped", "not_found"]; allowed_states = ["failed", "canceled", "running", "pending", "success", "success_with_warnings", "skipped", "not_found"];
if (indexOf.call(allowed_states, state) >= 0) { if (indexOf.call(allowed_states, state) !== -1) {
$('.ci_widget.ci-' + state).show(); $('.ci_widget.ci-' + state).show();
switch (state) { switch (state) {
case "failed": case "failed":
......
...@@ -81,7 +81,7 @@ ...@@ -81,7 +81,7 @@
var errorMessage, errors, formatter, unique, validator; var errorMessage, errors, formatter, unique, validator;
this.branchNameError.empty(); this.branchNameError.empty();
unique = function(values, value) { unique = function(values, value) {
if (indexOf.call(values, value) < 0) { if (indexOf.call(values, value) === -1) {
values.push(value); values.push(value);
} }
return values; return values;
......
...@@ -84,13 +84,14 @@ ...@@ -84,13 +84,14 @@
} }
$(function() { $(function() {
$(document).on('focusout.ssh_key', '#key_key', function() { $(document).on('input.ssh_key', '#key_key', function() {
const $title = $('#key_title'); const $title = $('#key_title');
const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/); const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
if (comment && comment.length > 1 && $title.val() === '') {
// Extract the SSH Key title from its comment
if (comment && comment.length > 1) {
return $title.val(comment[1]).change(); return $title.val(comment[1]).change();
} }
// Extract the SSH Key title from its comment
}); });
if (global.utils.getPagePath() === 'profiles') { if (global.utils.getPagePath() === 'profiles') {
return new Profile(); return new Profile();
......
...@@ -123,7 +123,7 @@ ...@@ -123,7 +123,7 @@
if ($('input[name="ref"]').length) { if ($('input[name="ref"]').length) {
var $form = $dropdown.closest('form'); var $form = $dropdown.closest('form');
var action = $form.attr('action'); var action = $form.attr('action');
var divider = action.indexOf('?') < 0 ? '?' : '&'; var divider = action.indexOf('?') === -1 ? '?' : '&';
gl.utils.visitUrl(action + '' + divider + '' + $form.serialize()); gl.utils.visitUrl(action + '' + divider + '' + $form.serialize());
} }
} }
......
/* global Flash */
require('vendor/task_list'); require('vendor/task_list');
class TaskList { class TaskList {
...@@ -6,6 +7,16 @@ class TaskList { ...@@ -6,6 +7,16 @@ class TaskList {
this.dataType = options.dataType; this.dataType = options.dataType;
this.fieldName = options.fieldName; this.fieldName = options.fieldName;
this.onSuccess = options.onSuccess || (() => {}); this.onSuccess = options.onSuccess || (() => {});
this.onError = function showFlash(response) {
let errorMessages = '';
if (response.responseJSON) {
errorMessages = response.responseJSON.errors.join(' ');
}
return new Flash(errorMessages || 'Update failed', 'alert');
};
this.init(); this.init();
} }
...@@ -32,6 +43,7 @@ class TaskList { ...@@ -32,6 +43,7 @@ class TaskList {
url: $target.data('update-url') || $('form.js-issuable-update').attr('action'), url: $target.data('update-url') || $('form.js-issuable-update').attr('action'),
data: patchData, data: patchData,
success: this.onSuccess, success: this.onSuccess,
error: this.onError,
}); });
} }
} }
......
...@@ -33,18 +33,16 @@ ...@@ -33,18 +33,16 @@
}, },
template: ` template: `
<td class="pipeline-actions hidden-xs"> <td class="pipeline-actions hidden-xs">
<div class="controls pull-right"> <div class="pull-right">
<div class="btn-group inline"> <div class="btn-group">
<div class="btn-group"> <div class="btn-group" v-if="actions">
<button <button
v-if='actions'
class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions" class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
data-toggle="dropdown" data-toggle="dropdown"
title="Manual job" title="Manual job"
data-placement="top" data-placement="top"
aria-label="Manual job" aria-label="Manual job">
> <span v-html="svgs.iconPlay" aria-hidden="true"></span>
<span v-html='svgs.iconPlay' aria-hidden="true"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i> <i class="fa fa-caret-down" aria-hidden="true"></i>
</button> </button>
<ul class="dropdown-menu dropdown-menu-align-right"> <ul class="dropdown-menu dropdown-menu-align-right">
...@@ -52,23 +50,21 @@ ...@@ -52,23 +50,21 @@
<a <a
rel="nofollow" rel="nofollow"
data-method="post" data-method="post"
:href='action.path' :href="action.path">
> <span v-html="svgs.iconPlay" aria-hidden="true"></span>
<span v-html='svgs.iconPlay' aria-hidden="true"></span>
<span>{{action.name}}</span> <span>{{action.name}}</span>
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
<div class="btn-group">
<div class="btn-group" v-if="artifacts">
<button <button
v-if='artifacts'
class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download" class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
title="Artifacts" title="Artifacts"
data-placement="top" data-placement="top"
data-toggle="dropdown" data-toggle="dropdown"
aria-label="Artifacts" aria-label="Artifacts">
>
<i class="fa fa-download" aria-hidden="true"></i> <i class="fa fa-download" aria-hidden="true"></i>
<i class="fa fa-caret-down" aria-hidden="true"></i> <i class="fa fa-caret-down" aria-hidden="true"></i>
</button> </button>
...@@ -76,42 +72,39 @@ ...@@ -76,42 +72,39 @@
<li v-for='artifact in pipeline.details.artifacts'> <li v-for='artifact in pipeline.details.artifacts'>
<a <a
rel="nofollow" rel="nofollow"
download :href="artifact.path">
:href='artifact.path'
>
<i class="fa fa-download" aria-hidden="true"></i> <i class="fa fa-download" aria-hidden="true"></i>
<span>{{download(artifact.name)}}</span> <span>{{download(artifact.name)}}</span>
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
</div> <div class="btn-group" v-if="pipeline.flags.retryable">
<div class="cancel-retry-btns inline"> <a
<a class="btn btn-default btn-retry has-tooltip"
v-if='pipeline.flags.retryable' title="Retry"
class="btn has-tooltip" rel="nofollow"
title="Retry" data-method="post"
rel="nofollow" data-placement="top"
data-method="post" data-toggle="dropdown"
data-placement="top" :href='pipeline.retry_path'
data-toggle="dropdown" aria-label="Retry">
:href='pipeline.retry_path' <i class="fa fa-repeat" aria-hidden="true"></i>
aria-label="Retry"> </a>
<i class="fa fa-repeat" aria-hidden="true"></i> </div>
</a> <div class="btn-group" v-if="pipeline.flags.cancelable">
<a <a
v-if='pipeline.flags.cancelable' class="btn btn-remove has-tooltip"
@click="confirmAction" title="Cancel"
class="btn btn-remove has-tooltip" rel="nofollow"
title="Cancel" data-method="post"
rel="nofollow" data-placement="top"
data-method="post" data-toggle="dropdown"
data-placement="top" :href='pipeline.cancel_path'
data-toggle="dropdown" aria-label="Cancel">
:href='pipeline.cancel_path' <i class="fa fa-remove" aria-hidden="true"></i>
aria-label="Cancel"> </a>
<i class="fa fa-remove" aria-hidden="true"></i> </div>
</a>
</div> </div>
</div> </div>
</td> </td>
......
...@@ -54,7 +54,7 @@ require('../lib/utils/datetime_utility'); ...@@ -54,7 +54,7 @@ require('../lib/utils/datetime_utility');
}, },
}, },
template: ` template: `
<td> <td class="pipelines-time-ago">
<p class="duration" v-if='duration'> <p class="duration" v-if='duration'>
<span v-html='svgs.iconTimer'></span> <span v-html='svgs.iconTimer'></span>
{{duration}} {{duration}}
...@@ -65,8 +65,7 @@ require('../lib/utils/datetime_utility'); ...@@ -65,8 +65,7 @@ require('../lib/utils/datetime_utility');
data-toggle="tooltip" data-toggle="tooltip"
data-placement="top" data-placement="top"
data-container="body" data-container="body"
:data-original-title='localTimeFinished' :data-original-title='localTimeFinished'>
>
{{timeStopped.words}} {{timeStopped.words}}
</time> </time>
</p> </p>
......
...@@ -229,7 +229,7 @@ ...@@ -229,7 +229,7 @@
.controls { .controls {
float: right; float: right;
margin-top: 8px; margin-top: 8px;
padding-bottom: 7px; padding-bottom: 8px;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
} }
} }
......
...@@ -26,6 +26,11 @@ ...@@ -26,6 +26,11 @@
.filtered-search-container { .filtered-search-container {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
@media (max-width: $screen-xs-min) {
-webkit-flex-direction: column;
flex-direction: column;
}
} }
.filtered-search-input-container { .filtered-search-input-container {
...@@ -34,6 +39,20 @@ ...@@ -34,6 +39,20 @@
position: relative; position: relative;
width: 100%; width: 100%;
@media (max-width: $screen-xs-min) {
-webkit-flex: 1 1 100%;
flex: 1 1 100%;
margin-bottom: 10px;
.dropdown-menu {
width: auto;
left: 0;
right: 0;
max-width: none;
min-width: 100%;
}
}
.form-control { .form-control {
padding-left: 25px; padding-left: 25px;
padding-right: 25px; padding-right: 25px;
...@@ -79,6 +98,31 @@ ...@@ -79,6 +98,31 @@
overflow: auto; overflow: auto;
} }
@media (max-width: $screen-xs-min) {
.issues-details-filters {
padding: 0 0 10px;
background-color: $white-light;
border-top: 0;
}
.filter-dropdown-container {
.dropdown-toggle,
.dropdown {
width: 100%;
}
.dropdown {
margin-left: 0;
}
.fa-chevron-down {
position: absolute;
right: 10px;
top: 10px;
}
}
}
%filter-dropdown-item-btn-hover { %filter-dropdown-item-btn-hover {
background-color: $dropdown-hover-color; background-color: $dropdown-hover-color;
color: $white-light; color: $white-light;
...@@ -148,4 +192,4 @@ ...@@ -148,4 +192,4 @@
.filter-dropdown-loading { .filter-dropdown-loading {
padding: 8px 16px; padding: 8px 16px;
} }
\ No newline at end of file
...@@ -20,6 +20,7 @@ $dark-highlight-bg: #ffe792; ...@@ -20,6 +20,7 @@ $dark-highlight-bg: #ffe792;
$dark-highlight-color: $black; $dark-highlight-color: $black;
$dark-pre-hll-bg: #373b41; $dark-pre-hll-bg: #373b41;
$dark-hll-bg: #373b41; $dark-hll-bg: #373b41;
$dark-over-bg: #9f9ab5;
$dark-c: #969896; $dark-c: #969896;
$dark-err: #c66; $dark-err: #c66;
$dark-k: #b294bb; $dark-k: #b294bb;
...@@ -139,6 +140,18 @@ $dark-il: #de935f; ...@@ -139,6 +140,18 @@ $dark-il: #de935f;
} }
} }
.diff-line-num {
&.is-over,
&.hll:not(.empty-cell).is-over {
background-color: $dark-over-bg;
border-color: darken($dark-over-bg, 5%);
a {
color: darken($dark-over-bg, 15%);
}
}
}
.line_content.match { .line_content.match {
@include dark-diff-match-line; @include dark-diff-match-line;
} }
......
...@@ -13,6 +13,7 @@ $monokai-line-empty-bg: #49483e; ...@@ -13,6 +13,7 @@ $monokai-line-empty-bg: #49483e;
$monokai-line-empty-border: darken($monokai-line-empty-bg, 15%); $monokai-line-empty-border: darken($monokai-line-empty-bg, 15%);
$monokai-diff-border: #808080; $monokai-diff-border: #808080;
$monokai-highlight-bg: #ffe792; $monokai-highlight-bg: #ffe792;
$monokai-over-bg: #9f9ab5;
$monokai-new-bg: rgba(166, 226, 46, 0.1); $monokai-new-bg: rgba(166, 226, 46, 0.1);
$monokai-new-idiff: rgba(166, 226, 46, 0.15); $monokai-new-idiff: rgba(166, 226, 46, 0.15);
...@@ -139,6 +140,18 @@ $monokai-gi: #a6e22e; ...@@ -139,6 +140,18 @@ $monokai-gi: #a6e22e;
} }
} }
.diff-line-num {
&.is-over,
&.hll:not(.empty-cell).is-over {
background-color: $monokai-over-bg;
border-color: darken($monokai-over-bg, 5%);
a {
color: darken($monokai-over-bg, 15%);
}
}
}
.line_content.match { .line_content.match {
@include dark-diff-match-line; @include dark-diff-match-line;
} }
......
...@@ -17,6 +17,7 @@ $solarized-dark-line-color-new: #5a766c; ...@@ -17,6 +17,7 @@ $solarized-dark-line-color-new: #5a766c;
$solarized-dark-line-color-old: #7a6c71; $solarized-dark-line-color-old: #7a6c71;
$solarized-dark-highlight: #094554; $solarized-dark-highlight: #094554;
$solarized-dark-hll-bg: #174652; $solarized-dark-hll-bg: #174652;
$solarized-dark-over-bg: #9f9ab5;
$solarized-dark-c: #586e75; $solarized-dark-c: #586e75;
$solarized-dark-err: #93a1a1; $solarized-dark-err: #93a1a1;
$solarized-dark-g: #93a1a1; $solarized-dark-g: #93a1a1;
...@@ -143,6 +144,18 @@ $solarized-dark-il: #2aa198; ...@@ -143,6 +144,18 @@ $solarized-dark-il: #2aa198;
} }
} }
.diff-line-num {
&.is-over,
&.hll:not(.empty-cell).is-over {
background-color: $solarized-dark-over-bg;
border-color: darken($solarized-dark-over-bg, 5%);
a {
color: darken($solarized-dark-over-bg, 15%);
}
}
}
.line_content.match { .line_content.match {
@include dark-diff-match-line; @include dark-diff-match-line;
} }
......
...@@ -18,6 +18,7 @@ $solarized-light-line-color-new: #a1a080; ...@@ -18,6 +18,7 @@ $solarized-light-line-color-new: #a1a080;
$solarized-light-line-color-old: #ad9186; $solarized-light-line-color-old: #ad9186;
$solarized-light-highlight: #eee8d5; $solarized-light-highlight: #eee8d5;
$solarized-light-hll-bg: #ddd8c5; $solarized-light-hll-bg: #ddd8c5;
$solarized-light-over-bg: #ded7fc;
$solarized-light-c: #93a1a1; $solarized-light-c: #93a1a1;
$solarized-light-err: #586e75; $solarized-light-err: #586e75;
$solarized-light-g: #586e75; $solarized-light-g: #586e75;
...@@ -150,6 +151,18 @@ $solarized-light-il: #2aa198; ...@@ -150,6 +151,18 @@ $solarized-light-il: #2aa198;
} }
} }
.diff-line-num {
&.is-over,
&.hll:not(.empty-cell).is-over {
background-color: $solarized-light-over-bg;
border-color: darken($solarized-light-over-bg, 5%);
a {
color: darken($solarized-light-over-bg, 15%);
}
}
}
.line_content.match { .line_content.match {
@include matchLine; @include matchLine;
} }
......
...@@ -7,6 +7,7 @@ $white-code-color: $gl-text-color; ...@@ -7,6 +7,7 @@ $white-code-color: $gl-text-color;
$white-highlight: #fafe3d; $white-highlight: #fafe3d;
$white-pre-hll-bg: #f8eec7; $white-pre-hll-bg: #f8eec7;
$white-hll-bg: #f8f8f8; $white-hll-bg: #f8f8f8;
$white-over-bg: #ded7fc;
$white-c: #998; $white-c: #998;
$white-err: #a61717; $white-err: #a61717;
$white-err-bg: #e3d2d2; $white-err-bg: #e3d2d2;
...@@ -123,6 +124,16 @@ $white-gc-bg: #eaf2f5; ...@@ -123,6 +124,16 @@ $white-gc-bg: #eaf2f5;
} }
} }
&.is-over,
&.hll:not(.empty-cell).is-over {
background-color: $white-over-bg;
border-color: darken($white-over-bg, 5%);
a {
color: darken($white-over-bg, 15%);
}
}
&.hll:not(.empty-cell) { &.hll:not(.empty-cell) {
background-color: $line-number-select; background-color: $line-number-select;
border-color: $line-select-yellow-dark; border-color: $line-select-yellow-dark;
......
...@@ -89,6 +89,10 @@ ...@@ -89,6 +89,10 @@
.diff-line-num { .diff-line-num {
width: 50px; width: 50px;
a {
transition: none;
}
} }
.line_holder td { .line_holder td {
...@@ -109,10 +113,6 @@ ...@@ -109,10 +113,6 @@
td.line_content.parallel { td.line_content.parallel {
width: 46%; width: 46%;
} }
.add-diff-note {
margin-left: -65px;
}
} }
.old_line, .old_line,
......
...@@ -452,36 +452,37 @@ ul.notes { ...@@ -452,36 +452,37 @@ ul.notes {
* Line note button on the side of diffs * Line note button on the side of diffs
*/ */
.diff-file tr.line_holder { .add-diff-note {
@mixin show-add-diff-note { display: none;
display: inline-block; margin-top: -2px;
} border-radius: 50%;
background: $white-light;
padding: 1px 5px;
font-size: 12px;
color: $gl-link-color;
margin-left: -55px;
position: absolute;
z-index: 10;
width: 23px;
height: 23px;
border: 1px solid $border-color;
transition: transform .1s ease-in-out;
.add-diff-note { &:hover {
margin-top: -8px; background: $gl-info;
border-radius: 40px; color: $white-light;
background: $white-light; transform: scale(1.15);
padding: 4px; }
font-size: 16px;
color: $gl-link-color;
margin-left: -56px;
position: absolute;
z-index: 10;
width: 32px;
// "hide" it by default
display: none;
&:hover { &:active {
background: $gl-info; outline: 0;
color: $white-light;
@include show-add-diff-note;
}
} }
}
// "show" the icon also if we just hover somewhere over the line .diff-file {
&:hover > td { .is-over {
.add-diff-note { .add-diff-note {
@include show-add-diff-note; display: inline-block;
} }
} }
} }
......
...@@ -13,21 +13,16 @@ ...@@ -13,21 +13,16 @@
white-space: nowrap; white-space: nowrap;
} }
.commit-title { .table-holder {
margin: 0; width: 100%;
} overflow: auto;
.controls {
white-space: nowrap;
} }
.btn { .commit-title {
margin: 4px; margin: 0;
} }
.table.ci-table { .table.ci-table {
min-width: 1200px;
table-layout: fixed;
.label { .label {
margin-bottom: 3px; margin-bottom: 3px;
...@@ -37,16 +32,72 @@ ...@@ -37,16 +32,72 @@
color: $black; color: $black;
} }
.pipeline-date, .stage-cell {
.pipeline-status { min-width: 130px; // Guarantees we show at least 4 stages in line
width: 10%; width: 20%;
}
.pipelines-time-ago {
text-align: right;
} }
.pipeline-info,
.pipeline-commit,
.pipeline-stages,
.pipeline-actions { .pipeline-actions {
width: 20%; padding-right: 0;
min-width: 170px; //Guarantees buttons don't break in several lines.
.btn-default {
color: $gl-text-color-secondary;
}
.btn.btn-retry:hover,
.btn.btn-retry:focus {
border-color: $gray-darkest;
background-color: $white-normal;
}
svg path {
fill: $gl-text-color-secondary;
}
.dropdown-menu {
max-height: 250px;
overflow-y: auto;
}
.dropdown-toggle,
.dropdown-menu {
color: $gl-text-color-secondary;
.fa {
color: $gl-text-color-secondary;
font-size: 14px;
}
svg,
.fa {
margin-right: 0;
}
}
.btn-group {
&.open {
.btn-default {
background-color: $white-normal;
border-color: $border-white-normal;
}
}
.btn {
.icon-play {
height: 13px;
width: 12px;
}
}
}
.tooltip {
white-space: nowrap;
}
} }
} }
} }
...@@ -61,27 +112,10 @@ ...@@ -61,27 +112,10 @@
} }
} }
.content-list.pipelines .table-holder {
min-height: 300px;
}
.pipeline-holder {
width: 100%;
overflow: auto;
}
.table.ci-table { .table.ci-table {
min-width: 900px;
&.pipeline {
min-width: 650px;
}
&.builds-page {
tr { &.builds-page tr {
height: 71px; height: 71px;
}
} }
tr { tr {
...@@ -94,6 +128,10 @@ ...@@ -94,6 +128,10 @@
padding: 10px 8px; padding: 10px 8px;
} }
td.environments-actions {
padding-right: 0;
}
td.stage-cell { td.stage-cell {
padding: 10px 0; padding: 10px 0;
} }
...@@ -103,7 +141,7 @@ ...@@ -103,7 +141,7 @@
} }
.commit-link { .commit-link {
padding: 9px 8px 10px; padding: 9px 8px 10px 2px;
} }
} }
...@@ -210,72 +248,8 @@ ...@@ -210,72 +248,8 @@
} }
} }
.pipeline-actions { .build-link a {
min-width: 140px; color: $gl-text-color;
.btn {
margin: 0;
color: $gl-text-color-secondary;
}
.cancel-retry-btns {
vertical-align: middle;
.btn:not(:first-child) {
margin-left: 8px;
}
}
.dropdown-menu {
max-height: 250px;
overflow-y: auto;
}
.dropdown-toggle,
.dropdown-menu {
color: $gl-text-color-secondary;
.fa {
color: $gl-text-color-secondary;
font-size: 14px;
}
svg,
.fa {
margin-right: 0;
}
}
.btn-remove {
color: $white-light;
}
.btn-group {
&.open {
.btn-default {
background-color: $white-normal;
border-color: $border-white-normal;
}
}
.btn {
.icon-play {
height: 13px;
width: 12px;
}
}
}
.tooltip {
white-space: nowrap;
}
}
.build-link {
a {
color: $gl-text-color;
}
} }
.btn-group.open .dropdown-toggle { .btn-group.open .dropdown-toggle {
...@@ -339,31 +313,8 @@ ...@@ -339,31 +313,8 @@
} }
.tab-pane { .tab-pane {
&.pipelines { &.builds .ci-table tr {
.ci-table { height: 71px;
min-width: 900px;
}
.content-list.pipelines {
overflow: auto;
}
.stage {
max-width: 100px;
width: 100px;
}
.pipeline-actions {
min-width: initial;
}
}
&.builds {
.ci-table {
tr {
height: 71px;
}
}
} }
} }
......
...@@ -638,14 +638,6 @@ pre.light-well { ...@@ -638,14 +638,6 @@ pre.light-well {
margin: 0; margin: 0;
} }
.activity-filter-block {
.controls {
padding-bottom: 7px;
margin-top: 8px;
border-bottom: 1px solid $border-color;
}
}
.commits-search-form { .commits-search-form {
.input-short { .input-short {
min-width: 200px; min-width: 200px;
......
...@@ -26,6 +26,23 @@ module IssuableActions ...@@ -26,6 +26,23 @@ module IssuableActions
private private
def render_conflict_response
respond_to do |format|
format.html do
@conflict = true
render :edit
end
format.json do
render json: {
errors: [
"Someone edited this #{issuable.human_class_name} at the same time you did. Please refresh your browser and make sure your changes will not unintentionally remove theirs."
]
}, status: 409
end
end
end
def labels def labels
@labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute @labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
end end
......
...@@ -139,8 +139,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -139,8 +139,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
rescue ActiveRecord::StaleObjectError rescue ActiveRecord::StaleObjectError
@conflict = true render_conflict_response
render :edit
end end
def referenced_merge_requests def referenced_merge_requests
......
...@@ -305,24 +305,23 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -305,24 +305,23 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request = MergeRequests::UpdateService.new(project, current_user, update_params).execute(@merge_request) @merge_request = MergeRequests::UpdateService.new(project, current_user, update_params).execute(@merge_request)
if @merge_request.valid? respond_to do |format|
respond_to do |format| format.html do
format.html do if @merge_request.valid?
redirect_to([@merge_request.target_project.namespace.becomes(Namespace), redirect_to([@merge_request.target_project.namespace.becomes(Namespace), @merge_request.target_project, @merge_request])
@merge_request.target_project, @merge_request]) else
end set_suggested_approvers
format.json do
render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short]) render :edit
end end
end end
else
set_suggested_approvers format.json do
render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
render "edit" end
end end
rescue ActiveRecord::StaleObjectError rescue ActiveRecord::StaleObjectError
@conflict = true render_conflict_response
render :edit
end end
def remove_wip def remove_wip
......
...@@ -15,4 +15,11 @@ module BuildsHelper ...@@ -15,4 +15,11 @@ module BuildsHelper
log_state: @build.trace_with_state[:state].to_s log_state: @build.trace_with_state[:state].to_s
} }
end end
def build_failed_issue_options
{
title: "Build Failed ##{@build.id}",
description: namespace_project_build_url(@project.namespace, @project, @build)
}
end
end end
...@@ -34,7 +34,7 @@ module ButtonHelper ...@@ -34,7 +34,7 @@ module ButtonHelper
content_tag (append_link ? :a : :span), protocol, content_tag (append_link ? :a : :span), protocol,
class: klass, class: klass,
href: (project.http_url_to_repo if append_link), href: (project.http_url_to_repo(current_user) if append_link),
data: { data: {
html: true, html: true,
placement: placement, placement: placement,
......
...@@ -253,7 +253,7 @@ module ProjectsHelper ...@@ -253,7 +253,7 @@ module ProjectsHelper
when 'ssh' when 'ssh'
project.ssh_url_to_repo project.ssh_url_to_repo
else else
project.http_url_to_repo project.http_url_to_repo(current_user)
end end
end end
......
...@@ -979,8 +979,14 @@ class Project < ActiveRecord::Base ...@@ -979,8 +979,14 @@ class Project < ActiveRecord::Base
url_to_repo url_to_repo
end end
def http_url_to_repo def http_url_to_repo(user = nil)
"#{web_url}.git" url = web_url
if user
url.sub!(%r{\Ahttps?://}) { |protocol| "#{protocol}#{user.username}@" }
end
"#{url}.git"
end end
# No need to have a Kerberos Web url. Kerberos URL will be used only to clone # No need to have a Kerberos Web url. Kerberos URL will be used only to clone
......
...@@ -18,7 +18,8 @@ module Groups ...@@ -18,7 +18,8 @@ module Groups
end end
group.children.each do |group| group.children.each do |group|
DestroyService.new(group, current_user).async_execute # This needs to be synchronous since the namespace gets destroyed below
DestroyService.new(group, current_user).execute
end end
group.really_destroy! group.really_destroy!
......
...@@ -27,10 +27,6 @@ class ArtifactUploader < GitlabUploader ...@@ -27,10 +27,6 @@ class ArtifactUploader < GitlabUploader
File.join(self.class.artifacts_cache_path, @build.artifacts_path) File.join(self.class.artifacts_cache_path, @build.artifacts_path)
end end
def file_storage?
self.class.storage == CarrierWave::Storage::File
end
def filename def filename
file.try(:filename) file.try(:filename)
end end
......
...@@ -4,6 +4,6 @@ class AttachmentUploader < GitlabUploader ...@@ -4,6 +4,6 @@ class AttachmentUploader < GitlabUploader
storage :file storage :file
def store_dir def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" "#{base_dir}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end end
end end
...@@ -4,7 +4,7 @@ class AvatarUploader < GitlabUploader ...@@ -4,7 +4,7 @@ class AvatarUploader < GitlabUploader
storage :file storage :file
def store_dir def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" "#{base_dir}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end end
def exists? def exists?
......
...@@ -4,15 +4,12 @@ class FileUploader < GitlabUploader ...@@ -4,15 +4,12 @@ class FileUploader < GitlabUploader
storage :file storage :file
attr_accessor :project, :secret attr_accessor :project
attr_reader :secret
def initialize(project, secret = nil) def initialize(project, secret = nil)
@project = project @project = project
@secret = secret || self.class.generate_secret @secret = secret || generate_secret
end
def base_dir
"uploads"
end end
def store_dir def store_dir
...@@ -23,10 +20,6 @@ class FileUploader < GitlabUploader ...@@ -23,10 +20,6 @@ class FileUploader < GitlabUploader
File.join(base_dir, 'tmp', @project.path_with_namespace, @secret) File.join(base_dir, 'tmp', @project.path_with_namespace, @secret)
end end
def secure_url
File.join("/uploads", @secret, file.filename)
end
def to_markdown def to_markdown
to_h[:markdown] to_h[:markdown]
end end
...@@ -35,17 +28,23 @@ class FileUploader < GitlabUploader ...@@ -35,17 +28,23 @@ class FileUploader < GitlabUploader
filename = image_or_video? ? self.file.basename : self.file.filename filename = image_or_video? ? self.file.basename : self.file.filename
escaped_filename = filename.gsub("]", "\\]") escaped_filename = filename.gsub("]", "\\]")
markdown = "[#{escaped_filename}](#{self.secure_url})" markdown = "[#{escaped_filename}](#{secure_url})"
markdown.prepend("!") if image_or_video? || dangerous? markdown.prepend("!") if image_or_video? || dangerous?
{ {
alt: filename, alt: filename,
url: self.secure_url, url: secure_url,
markdown: markdown markdown: markdown
} }
end end
def self.generate_secret private
def generate_secret
SecureRandom.hex SecureRandom.hex
end end
def secure_url
File.join('/uploads', @secret, file.filename)
end
end end
class GitlabUploader < CarrierWave::Uploader::Base class GitlabUploader < CarrierWave::Uploader::Base
def self.base_dir
'uploads'
end
delegate :base_dir, to: :class
def file_storage?
self.class.storage == CarrierWave::Storage::File
end
# Reduce disk IO # Reduce disk IO
def move_to_cache def move_to_cache
true true
......
...@@ -27,6 +27,8 @@ module UploaderHelper ...@@ -27,6 +27,8 @@ module UploaderHelper
extension_match?(DANGEROUS_EXT) extension_match?(DANGEROUS_EXT)
end end
private
def extension_match?(extensions) def extension_match?(extensions)
return false unless file return false unless file
...@@ -40,8 +42,4 @@ module UploaderHelper ...@@ -40,8 +42,4 @@ module UploaderHelper
extensions.include?(extension.downcase) extensions.include?(extension.downcase)
end end
def file_storage?
self.class.storage == CarrierWave::Storage::File
end
end end
...@@ -14,6 +14,8 @@ ...@@ -14,6 +14,8 @@
= runner.short_sha = runner.short_sha
%td %td
= runner.description = runner.description
%td
= runner.version
%td %td
- if runner.shared? - if runner.shared?
n/a n/a
......
...@@ -67,6 +67,7 @@ ...@@ -67,6 +67,7 @@
%th Type %th Type
%th Runner token %th Runner token
%th Description %th Description
%th Version
%th Projects %th Projects
%th Jobs %th Jobs
%th Tags %th Tags
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.nav-block .nav-block
- if current_user - if current_user
.controls .controls
= link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn has-tooltip', title: 'Subscribe' do
%i.fa.fa-rss %i.fa.fa-rss
= render 'shared/event_filter' = render 'shared/event_filter'
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.nav-block .nav-block
- if current_user - if current_user
.controls .controls
= link_to group_path(@group, format: :atom, private_token: current_user.private_token), class: 'btn rss-btn' do = link_to group_path(@group, format: :atom, private_token: current_user.private_token), class: 'btn rss-btn has-tooltip' , title: 'Subscribe' do
%i.fa.fa-rss %i.fa.fa-rss
= render 'shared/event_filter' = render 'shared/event_filter'
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.nav-block.activity-filter-block .nav-block.activity-filter-block
- if current_user - if current_user
.controls .controls
= link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Subscribe", class: 'btn rss-btn has-tooltip' do
= icon('rss') = icon('rss')
= render 'shared/event_filter' = render 'shared/event_filter'
......
.content-block.build-header .content-block.build-header.top-area
.header-content .header-content
= render 'ci/status/badge', status: @build.detailed_status(current_user), link: false = render 'ci/status/badge', status: @build.detailed_status(current_user), link: false
Job Job
...@@ -16,7 +16,10 @@ ...@@ -16,7 +16,10 @@
- if @build.user - if @build.user
= render "user" = render "user"
= time_ago_with_tooltip(@build.created_at) = time_ago_with_tooltip(@build.created_at)
- if can?(current_user, :update_build, @build) && @build.retryable? .nav-controls
= link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary pull-right', method: :post - if can?(current_user, :create_issue, @project) && @build.failed?
= link_to "New issue", new_namespace_project_issue_path(@project.namespace, @project, issue: build_failed_issue_options), class: 'btn btn-new btn-inverted'
- if can?(current_user, :update_build, @build) && @build.retryable?
= link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary', method: :post
%button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" } %button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
= icon('angle-double-left') = icon('angle-double-left')
- status = pipeline.status
- show_commit = local_assigns.fetch(:show_commit, true)
- show_branch = local_assigns.fetch(:show_branch, true)
%tr.commit
%td.commit-link
= render 'ci/status/badge', status: pipeline.detailed_status(current_user)
%td
= link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
%span.pipeline-id ##{pipeline.id}
%span by
- if pipeline.user
= user_avatar(user: pipeline.user, size: 20)
- else
%span.api.monospace API
- if pipeline.latest?
%span.label.label-success.has-tooltip{ title: 'Latest pipeline for this branch' } latest
- if pipeline.triggered?
%span.label.label-primary triggered
- if pipeline.yaml_errors.present?
%span.label.label-danger.has-tooltip{ title: "#{pipeline.yaml_errors}" } yaml invalid
- if pipeline.builds.any?(&:stuck?)
%span.label.label-warning stuck
%td.branch-commit
- if pipeline.ref && show_branch
.icon-container
= pipeline.tag? ? icon('tag') : icon('code-fork')
= link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name"
- if show_commit
.icon-container.commit-icon
= custom_icon("icon_commit")
= link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace"
%p.commit-title
- if commit = pipeline.commit
= author_avatar(commit, size: 20)
= link_to_gfm truncate(commit.title, length: 60, escape: false), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message"
- else
Cant find HEAD commit for this branch
%td
= render 'shared/mini_pipeline_graph', pipeline: pipeline, klass: 'js-mini-pipeline-graph'
%td
- if pipeline.duration
%p.duration
= custom_icon("icon_timer")
= duration_in_numbers(pipeline.duration)
- if pipeline.finished_at
%p.finished-at
= icon("calendar")
#{time_ago_with_tooltip(pipeline.finished_at, short_format: false)}
%td.pipeline-actions.hidden-xs
.controls.pull-right
- artifacts = pipeline.builds.latest.with_artifacts_not_expired
- actions = pipeline.manual_actions
- if artifacts.present? || actions.any?
.btn-group.inline
- if actions.any?
.btn-group
%button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual pipeline', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual pipeline' }
= custom_icon('icon_play')
= icon('caret-down', 'aria-hidden' => 'true')
%ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |build|
%li
= link_to play_namespace_project_build_path(pipeline.project.namespace, pipeline.project, build), method: :post, rel: 'nofollow' do
= custom_icon('icon_play')
%span= build.name
- if artifacts.present?
.btn-group
%button.dropdown-toggle.btn.btn-default.build-artifacts.has-tooltip.js-pipeline-dropdown-download{ type: 'button', title: 'Artifacts', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Artifacts' }
= icon("download")
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- artifacts.each do |build|
%li
= link_to download_namespace_project_build_artifacts_path(pipeline.project.namespace, pipeline.project, build), rel: 'nofollow', download: '' do
= icon("download")
%span Download '#{build.name}' artifacts
- if can?(current_user, :update_pipeline, pipeline.project)
.cancel-retry-btns.inline
- if pipeline.retryable?
= link_to retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn has-tooltip', title: 'Retry', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Retry' , method: :post do
= icon("repeat")
- if pipeline.cancelable?
= link_to cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-remove has-tooltip', title: 'Cancel', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Cancel' , method: :post do
= icon("remove")
...@@ -97,8 +97,8 @@ ...@@ -97,8 +97,8 @@
%li.filter-dropdown-item{ 'data-value' => "#{weight}" } %li.filter-dropdown-item{ 'data-value' => "#{weight}" }
%button.btn.btn-link= weight %button.btn.btn-link= weight
.pull-right .pull-right.filter-dropdown-container
= render 'shared/sort_dropdown', type: local_assigns[:type] = render 'shared/sort_dropdown'
- if @bulk_edit - if @bulk_edit
.issues_bulk_update.hide .issues_bulk_update.hide
......
---
title: Add runner version to /admin/runners view
merge_request: 8733
author: Jonathon Reinhart
---
title: Add the Username to the HTTP(S) clone URL of a Repository
merge_request: 9347
author: Jan Christophersen
---
title: Add button to create issue for failing build
merge_request: 9391
author: Alex Sanford
---
title: Enhanced filter issues layout for better mobile experiance
merge_request: 9280
author: Pratik Borsadiya
---
title: Replace setInterval with setTimeout to prevent highly frequent requests
merge_request: 9271
author: Takuya Noguchi
---
title: Keep consistent in handling indexOf results
merge_request: 9531
author: Takuya Noguchi
---
title: Fix MR changes tab size count when there are over 100 files in the diff
merge_request:
author:
---
title: Fix issuable stale object error handler for js when updating tasklists
merge_request:
author:
---
title: Improved diff comment button UX
merge_request:
author:
---
title: Fixed RSS button alignment on activity pages
merge_request:
author:
---
title: SSH key field updates title after pasting key
merge_request:
author:
require './spec/support/sidekiq'
def create_group_with_parents(user, full_path)
parent_path = nil
group = nil
until full_path.blank?
path, _, full_path = full_path.partition('/')
if parent_path
parent = Group.find_by_full_path(parent_path)
parent_path += '/'
parent_path += path
group = Groups::CreateService.new(user, path: path, parent_id: parent.id).execute
else
parent_path = path
group = Group.find_by_full_path(parent_path) ||
Groups::CreateService.new(user, path: path).execute
end
end
group
end
Sidekiq::Testing.inline! do
Gitlab::Seeder.quiet do
project_urls = [
'https://android.googlesource.com/platform/hardware/broadcom/libbt.git',
'https://android.googlesource.com/platform/hardware/broadcom/wlan.git',
'https://android.googlesource.com/platform/hardware/bsp/bootloader/intel/edison-u-boot.git',
'https://android.googlesource.com/platform/hardware/bsp/broadcom.git',
'https://android.googlesource.com/platform/hardware/bsp/freescale.git',
'https://android.googlesource.com/platform/hardware/bsp/imagination.git',
'https://android.googlesource.com/platform/hardware/bsp/intel.git',
'https://android.googlesource.com/platform/hardware/bsp/kernel/common/v4.1.git',
'https://android.googlesource.com/platform/hardware/bsp/kernel/common/v4.4.git'
]
user = User.admins.first
project_urls.each_with_index do |url, i|
full_path = url.sub('https://android.googlesource.com/', '')
full_path = full_path.sub(/\.git\z/, '')
full_path, _, project_path = full_path.rpartition('/')
group = Group.find_by_full_path(full_path) || create_group_with_parents(user, full_path)
params = {
import_url: url,
namespace_id: group.id,
path: project_path,
name: project_path,
description: FFaker::Lorem.sentence,
visibility_level: Gitlab::VisibilityLevel.values.sample
}
project = Projects::CreateService.new(user, params).execute
project.send(:_run_after_commit_queue)
if project.valid?
print '.'
else
print 'F'
end
end
end
end
class MigrateUsersNotificationLevel < ActiveRecord::Migration class MigrateUsersNotificationLevel < ActiveRecord::Migration
DOWNTIME = false
# Migrates only users who changed their default notification level :participating # Migrates only users who changed their default notification level :participating
# creating a new record on notification settings table # creating a new record on notification settings table
......
...@@ -466,6 +466,46 @@ If Registry is enabled in your GitLab instance, but you don't need it for your ...@@ -466,6 +466,46 @@ If Registry is enabled in your GitLab instance, but you don't need it for your
project, you can disable it from your project's settings. Read the user guide project, you can disable it from your project's settings. Read the user guide
on how to achieve that. on how to achieve that.
## Disable Container Registry but use GitLab as an auth endpoint
You can disable the embedded Container Registry to use an external one, but
still use GitLab as an auth endpoint.
**Omnibus GitLab**
1. Open `/etc/gitlab/gitlab.rb` and set necessary configurations:
```ruby
registry['enable'] = false
gitlab_rails['registry_enabled'] = true
gitlab_rails['registry_host'] = "registry.gitlab.example.com"
gitlab_rails['registry_port'] = "5005"
gitlab_rails['registry_api_url'] = "http://localhost:5000"
gitlab_rails['registry_key_path'] = "/var/opt/gitlab/gitlab-rails/certificate.key"
gitlab_rails['registry_path'] = "/var/opt/gitlab/gitlab-rails/shared/registry"
gitlab_rails['registry_issuer'] = "omnibus-gitlab-issuer"
```
1. Save the file and [reconfigure GitLab][] for the changes to take effect.
**Installations from source**
1. Open `/home/git/gitlab/config/gitlab.yml`, and edit the configuration settings under `registry`:
```
## Container Registry
registry:
enabled: true
host: "registry.gitlab.example.com"
port: "5005"
api_url: "http://localhost:5000"
path: /var/opt/gitlab/gitlab-rails/shared/registry
key: /var/opt/gitlab/gitlab-rails/certificate.key
issuer: omnibus-gitlab-issuer
```
1. Save the file and [restart GitLab][] for the changes to take effect.
## Storage limitations ## Storage limitations
Currently, there is no storage limitation, which means a user can upload an Currently, there is no storage limitation, which means a user can upload an
......
...@@ -49,7 +49,7 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps ...@@ -49,7 +49,7 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps
step 'I should see an http link to the repository' do step 'I should see an http link to the repository' do
project = Project.find_by(name: 'Community') project = Project.find_by(name: 'Community')
expect(page).to have_field('project_clone', with: project.http_url_to_repo) expect(page).to have_field('project_clone', with: project.http_url_to_repo(@user))
end end
step 'I should see an ssh link to the repository' do step 'I should see an ssh link to the repository' do
......
...@@ -8,11 +8,6 @@ module Banzai ...@@ -8,11 +8,6 @@ module Banzai
# of the anchor, and then replace the img with the link-wrapped version. # of the anchor, and then replace the img with the link-wrapped version.
def call def call
doc.xpath('descendant-or-self::img[not(ancestor::a)]').each do |img| doc.xpath('descendant-or-self::img[not(ancestor::a)]').each do |img|
div = doc.document.create_element(
'div',
class: 'image-container'
)
link = doc.document.create_element( link = doc.document.create_element(
'a', 'a',
class: 'no-attachment-icon', class: 'no-attachment-icon',
...@@ -22,9 +17,7 @@ module Banzai ...@@ -22,9 +17,7 @@ module Banzai
link.children = img.clone link.children = img.clone
div.children = link img.replace(link)
img.replace(div)
end end
doc doc
......
...@@ -8,16 +8,16 @@ module Gitlab ...@@ -8,16 +8,16 @@ module Gitlab
@proxy_host = opts.fetch(:proxy_host, 'localhost') @proxy_host = opts.fetch(:proxy_host, 'localhost')
@proxy_port = opts.fetch(:proxy_port, 3808) @proxy_port = opts.fetch(:proxy_port, 3808)
@proxy_path = opts[:proxy_path] if opts[:proxy_path] @proxy_path = opts[:proxy_path] if opts[:proxy_path]
super(app, opts)
super(app, backend: "http://#{@proxy_host}:#{@proxy_port}", **opts)
end end
def perform_request(env) def perform_request(env)
unless @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}") if @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}")
return @app.call(env) super(env)
else
@app.call(env)
end end
env['HTTP_HOST'] = "#{@proxy_host}:#{@proxy_port}"
super(env)
end end
end end
end end
......
...@@ -125,14 +125,16 @@ describe Projects::IssuesController do ...@@ -125,14 +125,16 @@ describe Projects::IssuesController do
end end
describe 'PUT #update' do describe 'PUT #update' do
before do
sign_in(user)
project.team << [user, :developer]
end
it_behaves_like 'update invalid issuable', Issue
context 'when moving issue to another private project' do context 'when moving issue to another private project' do
let(:another_project) { create(:empty_project, :private) } let(:another_project) { create(:empty_project, :private) }
before do
sign_in(user)
project.team << [user, :developer]
end
context 'when user has access to move issue' do context 'when user has access to move issue' do
before { another_project.team << [user, :reporter] } before { another_project.team << [user, :reporter] }
......
...@@ -420,6 +420,8 @@ describe Projects::MergeRequestsController do ...@@ -420,6 +420,8 @@ describe Projects::MergeRequestsController do
expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch } expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch }
end end
it_behaves_like 'update invalid issuable', MergeRequest
end end
context 'the approvals_before_merge param' do context 'the approvals_before_merge param' do
......
...@@ -32,7 +32,7 @@ feature 'Admin disables Git access protocol', feature: true do ...@@ -32,7 +32,7 @@ feature 'Admin disables Git access protocol', feature: true do
scenario 'shows only HTTP url' do scenario 'shows only HTTP url' do
visit_project visit_project
expect(page).to have_content("git clone #{project.http_url_to_repo}") expect(page).to have_content("git clone #{project.http_url_to_repo(admin)}")
expect(page).not_to have_selector('#clone-dropdown') expect(page).not_to have_selector('#clone-dropdown')
end end
end end
......
...@@ -61,7 +61,7 @@ describe "User Feed", feature: true do ...@@ -61,7 +61,7 @@ describe "User Feed", feature: true do
end end
it 'has XHTML summaries in merge request descriptions' do it 'has XHTML summaries in merge request descriptions' do
expect(body).to match /Here is the fix: <\/p><div[^>]*><a[^>]*><img[^>]*\/><\/a><\/div>/ expect(body).to match /Here is the fix: <a[^>]*><img[^>]*\/><\/a>/
end end
end end
end end
......
require 'spec_helper' require 'spec_helper'
describe 'Issues', feature: true do describe 'Issues', feature: true do
include DropzoneHelper
include IssueHelpers include IssueHelpers
include SortingHelper include SortingHelper
include WaitForAjax include WaitForAjax
...@@ -602,19 +603,13 @@ describe 'Issues', feature: true do ...@@ -602,19 +603,13 @@ describe 'Issues', feature: true do
end end
it 'uploads file when dragging into textarea' do it 'uploads file when dragging into textarea' do
drop_in_dropzone test_image_file dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
# Wait for the file to upload
sleep 1
expect(page.find_field("issue_description").value).to have_content 'banana_sample' expect(page.find_field("issue_description").value).to have_content 'banana_sample'
end end
it 'adds double newline to end of attachment markdown' do it 'adds double newline to end of attachment markdown' do
drop_in_dropzone test_image_file dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
# Wait for the file to upload
sleep 1
expect(page.find_field("issue_description").value).to match /\n\n$/ expect(page.find_field("issue_description").value).to match /\n\n$/
end end
...@@ -697,25 +692,4 @@ describe 'Issues', feature: true do ...@@ -697,25 +692,4 @@ describe 'Issues', feature: true do
end end
end end
end end
def drop_in_dropzone(file_path)
# Generate a fake input selector
page.execute_script <<-JS
var fakeFileInput = window.$('<input/>').attr(
{id: 'fakeFileInput', type: 'file'}
).appendTo('body');
JS
# Attach the file to the fake input selector with Capybara
attach_file("fakeFileInput", file_path)
# Add the file to a fileList array and trigger the fake drop event
page.execute_script <<-JS
var fileList = [$('#fakeFileInput')[0].files[0]];
var e = jQuery.Event('drop', { dataTransfer : { files : fileList } });
$('.div-dropzone')[0].dropzone.listeners[0].events.drop(e);
JS
end
def test_image_file
File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')
end
end end
...@@ -15,7 +15,7 @@ feature 'Profile > SSH Keys', feature: true do ...@@ -15,7 +15,7 @@ feature 'Profile > SSH Keys', feature: true do
scenario 'auto-populates the title', js: true do scenario 'auto-populates the title', js: true do
fill_in('Key', with: attributes_for(:key).fetch(:key)) fill_in('Key', with: attributes_for(:key).fetch(:key))
expect(find_field('Title').value).to eq 'dummy@gitlab.com' expect(page).to have_field("Title", with: "dummy@gitlab.com")
end end
scenario 'saves the new key' do scenario 'saves the new key' do
......
...@@ -68,8 +68,14 @@ feature 'Developer views empty project instructions', feature: true do ...@@ -68,8 +68,14 @@ feature 'Developer views empty project instructions', feature: true do
end end
def expect_instructions_for(protocol) def expect_instructions_for(protocol)
msg = :"#{protocol.downcase}_url_to_repo" url =
case protocol
expect(page).to have_content("git clone #{project.send(msg)}") when 'ssh'
project.ssh_url_to_repo
when 'http'
project.http_url_to_repo(developer)
end
expect(page).to have_content("git clone #{url}")
end end
end end
require 'rails_helper'
feature 'User uploads avatar to group', feature: true do
scenario 'they see the new avatar' do
user = create(:user)
group = create(:group)
group.add_owner(user)
login_as(user)
visit edit_group_path(group)
attach_file(
'group_avatar',
Rails.root.join('spec', 'fixtures', 'dk.png'),
visible: false
)
click_button 'Save group'
visit group_path(group)
expect(page).to have_selector(%Q(img[src$="/uploads/group/avatar/#{group.id}/dk.png"]))
# Cheating here to verify something that isn't user-facing, but is important
expect(group.reload.avatar.file).to exist
end
end
require 'rails_helper'
feature 'User uploads avatar to profile', feature: true do
scenario 'they see their new avatar' do
user = create(:user)
login_as(user)
visit profile_path
attach_file(
'user_avatar',
Rails.root.join('spec', 'fixtures', 'dk.png'),
visible: false
)
click_button 'Update profile settings'
visit user_path(user)
expect(page).to have_selector(%Q(img[src$="/uploads/user/avatar/#{user.id}/dk.png"]))
# Cheating here to verify something that isn't user-facing, but is important
expect(user.reload.avatar.file).to exist
end
end
require 'rails_helper'
feature 'User uploads file to note', feature: true do
include DropzoneHelper
let(:user) { create(:user) }
let(:project) { create(:empty_project, creator: user, namespace: user.namespace) }
scenario 'they see the attached file', js: true do
issue = create(:issue, project: project, author: user)
login_as(user)
visit namespace_project_issue_path(project.namespace, project, issue)
dropzone_file(Rails.root.join('spec', 'fixtures', 'dk.png'))
click_button 'Comment'
wait_for_ajax
expect(find('a.no-attachment-icon img[alt="dk"]')['src'])
.to match(%r{/#{project.full_path}/uploads/\h{32}/dk\.png$})
end
end
...@@ -13,8 +13,8 @@ describe Banzai::Filter::ImageLinkFilter, lib: true do ...@@ -13,8 +13,8 @@ describe Banzai::Filter::ImageLinkFilter, lib: true do
end end
it 'does not wrap a duplicate link' do it 'does not wrap a duplicate link' do
exp = act = %q(<a href="/whatever">#{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')}</a>) doc = filter(%Q(<a href="/whatever">#{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')}</a>))
expect(filter(act).to_html).to eq exp expect(doc.to_html).to match /^<a href="\/whatever"><img[^>]*><\/a>$/
end end
it 'works with external images' do it 'works with external images' do
...@@ -22,8 +22,8 @@ describe Banzai::Filter::ImageLinkFilter, lib: true do ...@@ -22,8 +22,8 @@ describe Banzai::Filter::ImageLinkFilter, lib: true do
expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href'] expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href']
end end
it 'wraps the image with a link and a div' do it 'works with inline images' do
doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) doc = filter(%Q(<p>test #{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')} inline</p>))
expect(doc.to_html).to include('<div class="image-container">') expect(doc.to_html).to match /^<p>test <a[^>]*><img[^>]*><\/a> inline<\/p>$/
end end
end end
...@@ -2283,4 +2283,25 @@ describe Project, models: true do ...@@ -2283,4 +2283,25 @@ describe Project, models: true do
end end
end end
end end
describe '#http_url_to_repo' do
let(:project) { create :empty_project }
context 'when no user is given' do
it 'returns the url to the repo without a username' do
url = project.http_url_to_repo
expect(url).to eq(project.http_url_to_repo)
expect(url).not_to include('@')
end
end
context 'when user is given' do
it 'returns the url to the repo with the username' do
user = build_stubbed(:user)
expect(project.http_url_to_repo(user)).to match(%r{https?:\/\/#{user.username}@})
end
end
end
end end
...@@ -5,6 +5,7 @@ describe Groups::DestroyService, services: true do ...@@ -5,6 +5,7 @@ describe Groups::DestroyService, services: true do
let!(:user) { create(:user) } let!(:user) { create(:user) }
let!(:group) { create(:group) } let!(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
let!(:project) { create(:project, namespace: group) } let!(:project) { create(:project, namespace: group) }
let!(:gitlab_shell) { Gitlab::Shell.new } let!(:gitlab_shell) { Gitlab::Shell.new }
let!(:remove_path) { group.path + "+#{group.id}+deleted" } let!(:remove_path) { group.path + "+#{group.id}+deleted" }
...@@ -20,6 +21,7 @@ describe Groups::DestroyService, services: true do ...@@ -20,6 +21,7 @@ describe Groups::DestroyService, services: true do
end end
it { expect(Group.unscoped.all).not_to include(group) } it { expect(Group.unscoped.all).not_to include(group) }
it { expect(Group.unscoped.all).not_to include(nested_group) }
it { expect(Project.unscoped.all).not_to include(project) } it { expect(Project.unscoped.all).not_to include(project) }
end end
......
module DropzoneHelper
# Provides a way to perform `attach_file` for a Dropzone-based file input
#
# This is accomplished by creating a standard HTML file input on the page,
# performing `attach_file` on that field, and then triggering the appropriate
# Dropzone events to perform the actual upload.
#
# This method waits for the upload to complete before returning.
def dropzone_file(file_path)
# Generate a fake file input that Capybara can attach to
page.execute_script <<-JS.strip_heredoc
var fakeFileInput = window.$('<input/>').attr(
{id: 'fakeFileInput', type: 'file'}
).appendTo('body');
window._dropzoneComplete = false;
JS
# Attach the file to the fake input selector with Capybara
attach_file('fakeFileInput', file_path)
# Manually trigger a Dropzone "drop" event with the fake input's file list
page.execute_script <<-JS.strip_heredoc
var fileList = [$('#fakeFileInput')[0].files[0]];
var e = jQuery.Event('drop', { dataTransfer : { files : fileList } });
var dropzone = $('.div-dropzone')[0].dropzone;
dropzone.on('queuecomplete', function() {
window._dropzoneComplete = true;
});
dropzone.listeners[0].events.drop(e);
JS
# Wait until Dropzone's fired `queuecomplete`
loop until page.evaluate_script('window._dropzoneComplete === true')
end
end
shared_examples 'update invalid issuable' do |klass|
let(:params) do
{
namespace_id: project.namespace.path,
project_id: project.path,
id: issuable.iid
}
end
let(:issuable) do
klass == Issue ? issue : merge_request
end
before do
if klass == Issue
params.merge!(issue: { title: "any" })
else
params.merge!(merge_request: { title: "any" })
end
end
context 'when updating causes conflicts' do
before do
allow_any_instance_of(issuable.class).to receive(:save).
and_raise(ActiveRecord::StaleObjectError.new(issuable, :save))
end
it 'renders edit when format is html' do
put :update, params
expect(response).to render_template(:edit)
expect(assigns[:conflict]).to be_truthy
end
it 'renders json error message when format is json' do
params[:format] = "json"
put :update, params
expect(response.status).to eq(409)
expect(JSON.parse(response.body)).to have_key('errors')
end
end
context 'when updating an invalid issuable' do
before do
key = klass == Issue ? :issue : :merge_request
params[key][:title] = ""
end
it 'renders edit when merge request is invalid' do
put :update, params
expect(response).to render_template(:edit)
end
end
end
require 'spec_helper' require 'spec_helper'
describe AttachmentUploader do describe AttachmentUploader do
let(:issue) { build(:issue) } let(:uploader) { described_class.new(build_stubbed(:user)) }
subject { described_class.new(issue) }
describe '#move_to_cache' do describe '#move_to_cache' do
it 'is true' do it 'is true' do
expect(subject.move_to_cache).to eq(true) expect(uploader.move_to_cache).to eq(true)
end end
end end
describe '#move_to_store' do describe '#move_to_store' do
it 'is true' do it 'is true' do
expect(subject.move_to_store).to eq(true) expect(uploader.move_to_store).to eq(true)
end end
end end
end end
require 'spec_helper' require 'spec_helper'
describe AvatarUploader do describe AvatarUploader do
let(:user) { build(:user) } let(:uploader) { described_class.new(build_stubbed(:user)) }
subject { described_class.new(user) }
describe '#move_to_cache' do describe '#move_to_cache' do
it 'is false' do it 'is false' do
expect(subject.move_to_cache).to eq(false) expect(uploader.move_to_cache).to eq(false)
end end
end end
describe '#move_to_store' do describe '#move_to_store' do
it 'is false' do it 'is false' do
expect(subject.move_to_store).to eq(false) expect(uploader.move_to_store).to eq(false)
end end
end end
end end
require 'spec_helper' require 'spec_helper'
describe FileUploader do describe FileUploader do
let(:project) { create(:project) } let(:uploader) { described_class.new(build_stubbed(:project)) }
before do describe 'initialize' do
@previous_enable_processing = FileUploader.enable_processing it 'generates a secret if none is provided' do
FileUploader.enable_processing = false expect(SecureRandom).to receive(:hex).and_return('secret')
@uploader = FileUploader.new(project)
end
after do
FileUploader.enable_processing = @previous_enable_processing
@uploader.remove!
end
describe '#image_or_video?' do uploader = described_class.new(double)
context 'given an image file' do
before do
@uploader.store!(fixture_file_upload(Rails.root.join('spec', 'fixtures', 'rails_sample.jpg')))
end
it 'detects an image based on file extension' do expect(uploader.secret).to eq 'secret'
expect(@uploader.image_or_video?).to be true
end
end end
context 'given an video file' do it 'accepts a secret parameter' do
before do expect(SecureRandom).not_to receive(:hex)
video_file = fixture_file_upload(Rails.root.join('spec', 'fixtures', 'video_sample.mp4'))
@uploader.store!(video_file)
end
it 'detects a video based on file extension' do
expect(@uploader.image_or_video?).to be true
end
end
it 'does not return image_or_video? for other types' do uploader = described_class.new(double, 'secret')
@uploader.store!(fixture_file_upload(Rails.root.join('spec', 'fixtures', 'doc_sample.txt')))
expect(@uploader.image_or_video?).to be false expect(uploader.secret).to eq 'secret'
end end
end end
describe '#move_to_cache' do describe '#move_to_cache' do
it 'is true' do it 'is true' do
expect(@uploader.move_to_cache).to eq(true) expect(uploader.move_to_cache).to eq(true)
end end
end end
describe '#move_to_store' do describe '#move_to_store' do
it 'is true' do it 'is true' do
expect(@uploader.move_to_store).to eq(true) expect(uploader.move_to_store).to eq(true)
end end
end end
end end
require 'rails_helper'
describe UploaderHelper do
class ExampleUploader < CarrierWave::Uploader::Base
include UploaderHelper
storage :file
end
def upload_fixture(filename)
fixture_file_upload(Rails.root.join('spec', 'fixtures', filename))
end
describe '#image_or_video?' do
let(:uploader) { ExampleUploader.new }
it 'returns true for an image file' do
uploader.store!(upload_fixture('dk.png'))
expect(uploader).to be_image_or_video
end
it 'it returns true for a video file' do
uploader.store!(upload_fixture('video_sample.mp4'))
expect(uploader).to be_image_or_video
end
it 'returns false for other extensions' do
uploader.store!(upload_fixture('doc_sample.txt'))
expect(uploader).not_to be_image_or_video
end
end
end
...@@ -209,6 +209,10 @@ describe 'projects/builds/show', :view do ...@@ -209,6 +209,10 @@ describe 'projects/builds/show', :view do
it 'does not show retry button' do it 'does not show retry button' do
expect(rendered).not_to have_link('Retry') expect(rendered).not_to have_link('Retry')
end end
it 'does not show New issue button' do
expect(rendered).not_to have_link('New issue')
end
end end
context 'when job is not running' do context 'when job is not running' do
...@@ -220,6 +224,23 @@ describe 'projects/builds/show', :view do ...@@ -220,6 +224,23 @@ describe 'projects/builds/show', :view do
it 'shows retry button' do it 'shows retry button' do
expect(rendered).to have_link('Retry') expect(rendered).to have_link('Retry')
end end
context 'if build passed' do
it 'does not show New issue button' do
expect(rendered).not_to have_link('New issue')
end
end
context 'if build failed' do
before do
build.status = 'failed'
render
end
it 'shows New issue button' do
expect(rendered).to have_link('New issue')
end
end
end end
describe 'commit title in sidebar' do describe 'commit title in sidebar' do
...@@ -248,4 +269,25 @@ describe 'projects/builds/show', :view do ...@@ -248,4 +269,25 @@ describe 'projects/builds/show', :view do
expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_2') expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_2')
end end
end end
describe 'New issue button' do
before do
build.status = 'failed'
render
end
it 'links to issues/new with the title and description filled in' do
title = "Build Failed ##{build.id}"
build_url = namespace_project_build_url(project.namespace, project, build)
href = new_namespace_project_issue_path(
project.namespace,
project,
issue: {
title: title,
description: build_url
}
)
expect(rendered).to have_link('New issue', href: href)
end
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment