Commit 75f3f6e1 authored by Robert Speicher's avatar Robert Speicher

Merge branch 'master' into rs-sign_in

parents 7d03012f 69e17c22
...@@ -362,6 +362,7 @@ db:migrate:reset-mysql: ...@@ -362,6 +362,7 @@ db:migrate:reset-mysql:
- git fetch origin v8.14.10 - git fetch origin v8.14.10
- git checkout -f FETCH_HEAD - git checkout -f FETCH_HEAD
- bundle install $BUNDLE_INSTALL_FLAGS - bundle install $BUNDLE_INSTALL_FLAGS
- cp config/gitlab.yml.example config/gitlab.yml
- bundle exec rake db:drop db:create db:schema:load db:seed_fu - bundle exec rake db:drop db:create db:schema:load db:seed_fu
- git checkout $CI_COMMIT_SHA - git checkout $CI_COMMIT_SHA
- bundle install $BUNDLE_INSTALL_FLAGS - bundle install $BUNDLE_INSTALL_FLAGS
......
...@@ -386,7 +386,7 @@ gem 'vmstat', '~> 2.3.0' ...@@ -386,7 +386,7 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6' gem 'sys-filesystem', '~> 1.1.6'
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly', '~> 0.13.0' gem 'gitaly', '~> 0.14.0'
gem 'toml-rb', '~> 0.3.15', require: false gem 'toml-rb', '~> 0.3.15', require: false
......
...@@ -278,7 +278,7 @@ GEM ...@@ -278,7 +278,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly (0.13.0) gitaly (0.14.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -980,7 +980,7 @@ DEPENDENCIES ...@@ -980,7 +980,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly (~> 0.13.0) gitaly (~> 0.14.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1) gitlab-markup (~> 1.5.1)
......
import DropLab from './droplab/drop_lab';
import ISetter from './droplab/plugins/input_setter';
// Todo: Remove this when fixing issue in input_setter plugin
const InputSetter = Object.assign({}, ISetter);
class CloseReopenReportToggle {
constructor(opts = {}) {
this.dropdownTrigger = opts.dropdownTrigger;
this.dropdownList = opts.dropdownList;
this.button = opts.button;
}
initDroplab() {
this.reopenItem = this.dropdownList.querySelector('.reopen-item');
this.closeItem = this.dropdownList.querySelector('.close-item');
this.droplab = new DropLab();
const config = this.setConfig();
this.droplab.init(this.dropdownTrigger, this.dropdownList, [InputSetter], config);
}
updateButton(isClosed) {
this.toggleButtonType(isClosed);
this.button.blur();
}
toggleButtonType(isClosed) {
const [showItem, hideItem] = this.getButtonTypes(isClosed);
showItem.classList.remove('hidden');
showItem.classList.add('droplab-item-selected');
hideItem.classList.add('hidden');
hideItem.classList.remove('droplab-item-selected');
showItem.click();
}
getButtonTypes(isClosed) {
return isClosed ? [this.reopenItem, this.closeItem] : [this.closeItem, this.reopenItem];
}
setDisable(shouldDisable = true) {
if (shouldDisable) {
this.button.setAttribute('disabled', 'true');
this.dropdownTrigger.setAttribute('disabled', 'true');
} else {
this.button.removeAttribute('disabled');
this.dropdownTrigger.removeAttribute('disabled');
}
}
setConfig() {
const config = {
InputSetter: [
{
input: this.button,
valueAttribute: 'data-text',
inputAttribute: 'data-value',
},
{
input: this.button,
valueAttribute: 'data-text',
inputAttribute: 'title',
},
{
input: this.button,
valueAttribute: 'data-button-class',
inputAttribute: 'class',
},
{
input: this.dropdownTrigger,
valueAttribute: 'data-toggle-class',
inputAttribute: 'class',
},
{
input: this.button,
valueAttribute: 'data-url',
inputAttribute: 'href',
},
{
input: this.button,
valueAttribute: 'data-method',
inputAttribute: 'data-method',
},
],
};
return config;
}
}
export default CloseReopenReportToggle;
import DropLab from './droplab/drop_lab'; import DropLab from './droplab/drop_lab';
import InputSetter from './droplab/plugins/input_setter'; import ISetter from './droplab/plugins/input_setter';
// Todo: Remove this when fixing issue in input_setter plugin
const InputSetter = Object.assign({}, ISetter);
class CommentTypeToggle { class CommentTypeToggle {
constructor(opts = {}) { constructor(opts = {}) {
......
...@@ -30,6 +30,7 @@ class GfmAutoComplete { ...@@ -30,6 +30,7 @@ class GfmAutoComplete {
this.input.each((i, input) => { this.input.each((i, input) => {
const $input = $(input); const $input = $(input);
$input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
$input.on('change.atwho', () => input.dispatchEvent(new Event('input')));
// This triggers at.js again // This triggers at.js again
// Needed for quick actions with suffixes (ex: /label ~) // Needed for quick actions with suffixes (ex: /label ~)
$input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup')); $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
......
import CloseReopenReportToggle from '../close_reopen_report_toggle';
function initCloseReopenReport() {
const container = document.querySelector('.js-issuable-close-dropdown');
if (!container) return undefined;
const dropdownTrigger = container.querySelector('.js-issuable-close-toggle');
const dropdownList = container.querySelector('.js-issuable-close-menu');
const button = container.querySelector('.js-issuable-close-button');
const closeReopenReportToggle = new CloseReopenReportToggle({
dropdownTrigger,
dropdownList,
button,
});
closeReopenReportToggle.initDroplab();
return closeReopenReportToggle;
}
const IssuablesHelper = {
initCloseReopenReport,
};
export default IssuablesHelper;
...@@ -6,6 +6,7 @@ import '~/lib/utils/text_utility'; ...@@ -6,6 +6,7 @@ import '~/lib/utils/text_utility';
import './flash'; import './flash';
import TaskList from './task_list'; import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown'; import CreateMergeRequestDropdown from './create_merge_request_dropdown';
import IssuablesHelper from './helpers/issuables_helper';
class Issue { class Issue {
constructor() { constructor() {
...@@ -28,6 +29,11 @@ class Issue { ...@@ -28,6 +29,11 @@ class Issue {
Issue.initMergeRequests(); Issue.initMergeRequests();
Issue.initRelatedBranches(); Issue.initRelatedBranches();
this.closeButtons = $('a.btn-close');
this.reopenButtons = $('a.btn-reopen');
this.initCloseReopenReport();
if (Issue.createMrDropdownWrap) { if (Issue.createMrDropdownWrap) {
this.createMergeRequestDropdown = new CreateMergeRequestDropdown(Issue.createMrDropdownWrap); this.createMergeRequestDropdown = new CreateMergeRequestDropdown(Issue.createMrDropdownWrap);
} }
...@@ -35,13 +41,8 @@ class Issue { ...@@ -35,13 +41,8 @@ class Issue {
initIssueBtnEventListeners() { initIssueBtnEventListeners() {
const issueFailMessage = 'Unable to update this issue at this time.'; const issueFailMessage = 'Unable to update this issue at this time.';
const closeButtons = $('a.btn-close');
const isClosedBadge = $('div.status-box-closed');
const isOpenBadge = $('div.status-box-open');
const projectIssuesCounter = $('.issue_counter');
const reopenButtons = $('a.btn-reopen');
return closeButtons.add(reopenButtons).on('click', (e) => { return $(document).on('click', 'a.btn-close, a.btn-reopen', (e) => {
var $button, shouldSubmit, url; var $button, shouldSubmit, url;
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
...@@ -50,7 +51,9 @@ class Issue { ...@@ -50,7 +51,9 @@ class Issue {
if (shouldSubmit) { if (shouldSubmit) {
Issue.submitNoteForm($button.closest('form')); Issue.submitNoteForm($button.closest('form'));
} }
$button.prop('disabled', true);
this.disableCloseReopenButton($button);
url = $button.attr('href'); url = $button.attr('href');
return $.ajax({ return $.ajax({
type: 'PUT', type: 'PUT',
...@@ -58,15 +61,19 @@ class Issue { ...@@ -58,15 +61,19 @@ class Issue {
}) })
.fail(() => new Flash(issueFailMessage)) .fail(() => new Flash(issueFailMessage))
.done((data) => { .done((data) => {
const isClosedBadge = $('div.status-box-closed');
const isOpenBadge = $('div.status-box-open');
const projectIssuesCounter = $('.issue_counter');
if ('id' in data) { if ('id' in data) {
$(document).trigger('issuable:change'); $(document).trigger('issuable:change');
const isClosed = $button.hasClass('btn-close'); const isClosed = $button.hasClass('btn-close');
closeButtons.toggleClass('hidden', isClosed);
reopenButtons.toggleClass('hidden', !isClosed);
isClosedBadge.toggleClass('hidden', !isClosed); isClosedBadge.toggleClass('hidden', !isClosed);
isOpenBadge.toggleClass('hidden', isClosed); isOpenBadge.toggleClass('hidden', isClosed);
this.toggleCloseReopenButton(isClosed);
let numProjectIssues = Number(projectIssuesCounter.text().replace(/[^\d]/, '')); let numProjectIssues = Number(projectIssuesCounter.text().replace(/[^\d]/, ''));
numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1; numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues)); projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues));
...@@ -83,12 +90,34 @@ class Issue { ...@@ -83,12 +90,34 @@ class Issue {
} else { } else {
new Flash(issueFailMessage); new Flash(issueFailMessage);
} }
})
$button.prop('disabled', false); .then(() => {
this.disableCloseReopenButton($button, false);
}); });
}); });
} }
initCloseReopenReport() {
this.closeReopenReportToggle = IssuablesHelper.initCloseReopenReport();
if (this.closeButtons) this.closeButtons = this.closeButtons.not('.issuable-close-button');
if (this.reopenButtons) this.reopenButtons = this.reopenButtons.not('.issuable-close-button');
}
disableCloseReopenButton($button, shouldDisable) {
if (this.closeReopenReportToggle) {
this.closeReopenReportToggle.setDisable(shouldDisable);
} else {
$button.prop('disabled', shouldDisable);
}
}
toggleCloseReopenButton(isClosed) {
if (this.closeReopenReportToggle) this.closeReopenReportToggle.updateButton(isClosed);
this.closeButtons.toggleClass('hidden', isClosed);
this.reopenButtons.toggleClass('hidden', !isClosed);
}
static submitNoteForm(form) { static submitNoteForm(form) {
var noteText; var noteText;
noteText = form.find("textarea.js-note-text").val(); noteText = form.find("textarea.js-note-text").val();
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'vendor/jquery.waitforimages'; import 'vendor/jquery.waitforimages';
import TaskList from './task_list'; import TaskList from './task_list';
import './merge_request_tabs'; import './merge_request_tabs';
import IssuablesHelper from './helpers/issuables_helper';
(function() { (function() {
this.MergeRequest = (function() { this.MergeRequest = (function() {
...@@ -21,9 +22,12 @@ import './merge_request_tabs'; ...@@ -21,9 +22,12 @@ import './merge_request_tabs';
return _this.showAllCommits(); return _this.showAllCommits();
}; };
})(this)); })(this));
this.initTabs(); this.initTabs();
this.initMRBtnListeners(); this.initMRBtnListeners();
this.initCommitMessageListeners(); this.initCommitMessageListeners();
this.closeReopenReportToggle = IssuablesHelper.initCloseReopenReport();
if ($("a.btn-close").length) { if ($("a.btn-close").length) {
this.taskList = new TaskList({ this.taskList = new TaskList({
dataType: 'merge_request', dataType: 'merge_request',
...@@ -64,11 +68,15 @@ import './merge_request_tabs'; ...@@ -64,11 +68,15 @@ import './merge_request_tabs';
if (shouldSubmit && $this.data('submitted')) { if (shouldSubmit && $this.data('submitted')) {
return; return;
} }
if (this.closeReopenReportToggle) this.closeReopenReportToggle.setDisable();
if (shouldSubmit) { if (shouldSubmit) {
if ($this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')) { if ($this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')) {
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
return _this.submitNoteForm($this.closest('form'), $this);
_this.submitNoteForm($this.closest('form'), $this);
} }
} }
}); });
......
...@@ -62,7 +62,7 @@ import findAndFollowLink from './shortcuts_dashboard_navigation'; ...@@ -62,7 +62,7 @@ import findAndFollowLink from './shortcuts_dashboard_navigation';
if (Cookies.get(performanceBarCookieName) === 'true') { if (Cookies.get(performanceBarCookieName) === 'true') {
Cookies.remove(performanceBarCookieName, { path: '/' }); Cookies.remove(performanceBarCookieName, { path: '/' });
} else { } else {
Cookies.set(performanceBarCookieName, true, { path: '/' }); Cookies.set(performanceBarCookieName, 'true', { path: '/' });
} }
gl.utils.refreshCurrentPage(); gl.utils.refreshCurrentPage();
}; };
......
...@@ -20,17 +20,29 @@ ...@@ -20,17 +20,29 @@
color: $text; color: $text;
border-color: $border; border-color: $border;
> .icon {
color: $text;
}
&:hover, &:hover,
&:focus { &:focus {
background-color: $hover-background; background-color: $hover-background;
border-color: $hover-border; border-color: $hover-border;
color: $hover-text; color: $hover-text;
> .icon {
color: $hover-text;
}
} }
&:active { &:active {
background-color: $active-background; background-color: $active-background;
border-color: $active-border; border-color: $active-border;
color: $hover-text; color: $hover-text;
> .icon {
color: $hover-text;
}
} }
} }
...@@ -163,7 +175,8 @@ ...@@ -163,7 +175,8 @@
@include btn-orange; @include btn-orange;
} }
&.btn-close { &.btn-close,
&.btn-close-color {
@include btn-outline($white-light, $orange-600, $orange-500, $orange-500, $white-light, $orange-600, $orange-600, $orange-700); @include btn-outline($white-light, $orange-600, $orange-500, $orange-500, $white-light, $orange-600, $orange-600, $orange-700);
} }
...@@ -181,7 +194,8 @@ ...@@ -181,7 +194,8 @@
float: right; float: right;
} }
&.btn-reopen { &.btn-reopen,
.btn-reopen-color {
/* should be same as parent class for now */ /* should be same as parent class for now */
} }
......
...@@ -295,9 +295,74 @@ ...@@ -295,9 +295,74 @@
} }
} }
.filtered-search-box-input-container .dropdown-menu, .droplab-dropdown {
.filtered-search-box-input-container .dropdown-menu-nav, .description {
.comment-type-dropdown .dropdown-menu { display: inline-block;
white-space: normal;
margin-left: 5px;
}
.dropdown-toggle > i {
pointer-events: none;
}
li {
padding: $gl-btn-padding $gl-btn-padding 2px;
cursor: pointer;
> a,
> button {
display: flex;
margin: 0;
padding: 0;
border-radius: 0;
text-overflow: inherit;
background-color: inherit;
color: inherit;
border: inherit;
text-align: left;
&:hover,
&:focus {
background-color: inherit;
color: inherit;
}
&.btn .fa:not(:last-child) {
margin-left: 5px;
}
}
&:hover,
&:focus {
background-color: $dropdown-hover-color;
color: $white-light;
}
&.droplab-item-selected i {
visibility: visible;
}
.icon {
visibility: hidden;
}
}
.icon {
display: inline-block;
vertical-align: top;
padding-top: 2px;
}
.divider {
margin: 0 8px;
padding: 0;
border-top: $gray-darkest;
}
}
.droplab-dropdown .dropdown-menu,
.droplab-dropdown .dropdown-menu-nav {
display: none; display: none;
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
......
...@@ -70,6 +70,13 @@ ...@@ -70,6 +70,13 @@
.input-token { .input-token {
max-width: 200px; max-width: 200px;
padding: 0;
&:hover,
&:focus {
background-color: inherit;
color: inherit;
}
} }
.input-token:only-child, .input-token:only-child,
...@@ -156,6 +163,16 @@ ...@@ -156,6 +163,16 @@
} }
} }
.droplab-dropdown li.filtered-search-token {
padding: 0;
&:hover,
&:focus {
background-color: inherit;
color: inherit;
}
}
.filtered-search-term { .filtered-search-term {
.name { .name {
background-color: inherit; background-color: inherit;
......
...@@ -265,7 +265,7 @@ $diff-view-modes-border: #c1c1c1; ...@@ -265,7 +265,7 @@ $diff-view-modes-border: #c1c1c1;
/* /*
* Fonts * Fonts
*/ */
$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; $monospace_font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
$regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; $regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
/* /*
......
...@@ -31,6 +31,12 @@ $new-sidebar-width: 220px; ...@@ -31,6 +31,12 @@ $new-sidebar-width: 220px;
&:hover { &:hover {
background-color: $border-color; background-color: $border-color;
} }
.project-title,
.group-title {
overflow: hidden;
text-overflow: ellipsis;
}
} }
.settings-avatar { .settings-avatar {
......
...@@ -799,3 +799,28 @@ ...@@ -799,3 +799,28 @@
} }
} }
} }
.issuable-close-button,
.issuable-close-toggle {
@include transition(border-color, color);
}
.issuable-close-dropdown {
.dropdown-menu {
min-width: 270px;
left: auto;
right: 0;
}
.description {
margin-bottom: 10px;
.text {
margin: 0;
}
}
.dropdown-toggle > .icon {
margin: 0 3px;
}
}
...@@ -356,7 +356,6 @@ ...@@ -356,7 +356,6 @@
color: $white-light; color: $white-light;
padding-right: 2px; padding-right: 2px;
margin-top: 2px; margin-top: 2px;
pointer-events: none;
} }
} }
...@@ -366,56 +365,6 @@ ...@@ -366,56 +365,6 @@
width: 298px; width: 298px;
} }
.description {
display: inline-block;
white-space: normal;
margin-left: 8px;
padding-right: 33px;
}
li {
padding-top: 6px;
& > a {
margin: 0;
padding: 0;
color: inherit;
border-radius: 0;
text-overflow: inherit;
&:hover,
&:focus {
background-color: inherit;
color: inherit;
}
}
&:hover,
&:focus {
background-color: $dropdown-hover-color;
color: $white-light;
}
&.droplab-item-selected i {
visibility: visible;
}
i {
visibility: hidden;
}
}
i {
display: inline-block;
vertical-align: top;
padding-top: 2px;
}
.divider {
margin: 0 8px;
padding: 0;
border-top: $gray-darkest;
}
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
display: flex; display: flex;
......
...@@ -126,6 +126,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -126,6 +126,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:metrics_port, :metrics_port,
:metrics_sample_interval, :metrics_sample_interval,
:metrics_timeout, :metrics_timeout,
:performance_bar_allowed_group_id,
:performance_bar_enabled,
:recaptcha_enabled, :recaptcha_enabled,
:recaptcha_private_key, :recaptcha_private_key,
:recaptcha_site_key, :recaptcha_site_key,
......
...@@ -9,7 +9,7 @@ class ApplicationController < ActionController::Base ...@@ -9,7 +9,7 @@ class ApplicationController < ActionController::Base
include SentryHelper include SentryHelper
include WorkhorseHelper include WorkhorseHelper
include EnforcesTwoFactorAuthentication include EnforcesTwoFactorAuthentication
include Peek::Rblineprof::CustomControllerHelpers include WithPerformanceBar
before_action :authenticate_user_from_private_token! before_action :authenticate_user_from_private_token!
before_action :authenticate_user_from_rss_token! before_action :authenticate_user_from_rss_token!
...@@ -68,21 +68,6 @@ class ApplicationController < ActionController::Base ...@@ -68,21 +68,6 @@ class ApplicationController < ActionController::Base
end end
end end
def peek_enabled?
return false unless Gitlab::PerformanceBar.enabled?
return false unless current_user
if RequestStore.active?
if RequestStore.store.key?(:peek_enabled)
RequestStore.store[:peek_enabled]
else
RequestStore.store[:peek_enabled] = cookies[:perf_bar_enabled].present?
end
else
cookies[:perf_bar_enabled].present?
end
end
protected protected
# This filter handles both private tokens and personal access tokens # This filter handles both private tokens and personal access tokens
......
module WithPerformanceBar
extend ActiveSupport::Concern
included do
include Peek::Rblineprof::CustomControllerHelpers
end
def peek_enabled?
return false unless Gitlab::PerformanceBar.enabled?(current_user)
if RequestStore.active?
RequestStore.fetch(:peek_enabled) { cookies[:perf_bar_enabled].present? }
else
cookies[:perf_bar_enabled].present?
end
end
end
...@@ -17,8 +17,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont ...@@ -17,8 +17,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
end end
def merge_request_params def merge_request_params
params.require(:merge_request) params.require(:merge_request).permit(merge_request_params_attributes)
.permit(merge_request_params_attributes)
end end
def merge_request_params_attributes def merge_request_params_attributes
......
...@@ -245,6 +245,53 @@ module IssuablesHelper ...@@ -245,6 +245,53 @@ module IssuablesHelper
@counts[cache_key][state] @counts[cache_key][state]
end end
def close_issuable_url(issuable)
issuable_url(issuable, close_reopen_params(issuable, :close))
end
def reopen_issuable_url(issuable)
issuable_url(issuable, close_reopen_params(issuable, :reopen))
end
def close_reopen_issuable_url(issuable, should_inverse = false)
issuable.closed? ^ should_inverse ? reopen_issuable_url(issuable) : close_issuable_url(issuable)
end
def issuable_url(issuable, *options)
case issuable
when Issue
issue_url(issuable, *options)
when MergeRequest
merge_request_url(issuable, *options)
end
end
def issuable_button_visibility(issuable, closed)
case issuable
when Issue
issue_button_visibility(issuable, closed)
when MergeRequest
merge_request_button_visibility(issuable, closed)
end
end
def issuable_close_reopen_button_method(issuable)
case issuable
when Issue
''
when MergeRequest
'put'
end
end
def issuable_author_is_current_user(issuable)
issuable.author == current_user
end
def issuable_display_type(issuable)
issuable.model_name.human.downcase
end
private private
def sidebar_gutter_collapsed? def sidebar_gutter_collapsed?
...@@ -270,8 +317,6 @@ module IssuablesHelper ...@@ -270,8 +317,6 @@ module IssuablesHelper
issue_template_names issue_template_names
when MergeRequest when MergeRequest
merge_request_template_names merge_request_template_names
else
raise 'Unknown issuable type!'
end end
end end
...@@ -301,4 +346,12 @@ module IssuablesHelper ...@@ -301,4 +346,12 @@ module IssuablesHelper
container: (is_collapsed ? 'body' : nil) container: (is_collapsed ? 'body' : nil)
} }
end end
def close_reopen_params(issuable, action)
{
issuable.model_name.to_s.underscore => { state_event: action }
}.tap do |params|
params[:format] = :json if issuable.is_a?(Issue)
end
end
end end
...@@ -234,6 +234,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -234,6 +234,7 @@ class ApplicationSetting < ActiveRecord::Base
koding_url: nil, koding_url: nil,
max_artifacts_size: Settings.artifacts['max_size'], max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_size'], max_attachment_size: Settings.gitlab['max_attachment_size'],
performance_bar_allowed_group_id: nil,
plantuml_enabled: false, plantuml_enabled: false,
plantuml_url: nil, plantuml_url: nil,
recaptcha_enabled: false, recaptcha_enabled: false,
...@@ -336,6 +337,48 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -336,6 +337,48 @@ class ApplicationSetting < ActiveRecord::Base
super(levels.map { |level| Gitlab::VisibilityLevel.level_value(level) }) super(levels.map { |level| Gitlab::VisibilityLevel.level_value(level) })
end end
def performance_bar_allowed_group_id=(group_full_path)
group_full_path = nil if group_full_path.blank?
if group_full_path.nil?
if group_full_path != performance_bar_allowed_group_id
super(group_full_path)
Gitlab::PerformanceBar.expire_allowed_user_ids_cache
end
return
end
group = Group.find_by_full_path(group_full_path)
if group
if group.id != performance_bar_allowed_group_id
super(group.id)
Gitlab::PerformanceBar.expire_allowed_user_ids_cache
end
else
super(nil)
Gitlab::PerformanceBar.expire_allowed_user_ids_cache
end
end
def performance_bar_allowed_group
Group.find_by_id(performance_bar_allowed_group_id)
end
# Return true if the Performance Bar is enabled for a given group
def performance_bar_enabled
performance_bar_allowed_group_id.present?
end
# - If `enable` is true, we early return since the actual attribute that holds
# the enabling/disabling is `performance_bar_allowed_group_id`
# - If `enable` is false, we set `performance_bar_allowed_group_id` to `nil`
def performance_bar_enabled=(enable)
return if enable
self.performance_bar_allowed_group_id = nil
end
# Choose one of the available repository storage options. Currently all have # Choose one of the available repository storage options. Currently all have
# equal weighting. # equal weighting.
def pick_repository_storage def pick_repository_storage
......
module EachBatch
extend ActiveSupport::Concern
module ClassMethods
# Iterates over the rows in a relation in batches, similar to Rails'
# `in_batches` but in a more efficient way.
#
# Unlike `in_batches` provided by Rails this method does not support a
# custom start/end range, nor does it provide support for the `load:`
# keyword argument.
#
# This method will yield an ActiveRecord::Relation to the supplied block, or
# return an Enumerator if no block is given.
#
# Example:
#
# User.each_batch do |relation|
# relation.update_all(updated_at: Time.now)
# end
#
# The supplied block is also passed an optional batch index:
#
# User.each_batch do |relation, index|
# puts index # => 1, 2, 3, ...
# end
#
# You can also specify an alternative column to use for ordering the rows:
#
# User.each_batch(column: :created_at) do |relation|
# ...
# end
#
# This will produce SQL queries along the lines of:
#
# User Load (0.7ms) SELECT "users"."id" FROM "users" WHERE ("users"."id" >= 41654) ORDER BY "users"."id" ASC LIMIT 1 OFFSET 1000
# (0.7ms) SELECT COUNT(*) FROM "users" WHERE ("users"."id" >= 41654) AND ("users"."id" < 42687)
#
# of - The number of rows to retrieve per batch.
# column - The column to use for ordering the batches.
def each_batch(of: 1000, column: primary_key)
unless column
raise ArgumentError,
'the column: argument must be set to a column name to use for ordering rows'
end
start = except(:select)
.select(column)
.reorder(column => :asc)
.take
return unless start
start_id = start[column]
arel_table = self.arel_table
1.step do |index|
stop = except(:select)
.select(column)
.where(arel_table[column].gteq(start_id))
.reorder(column => :asc)
.offset(of)
.limit(1)
.take
relation = where(arel_table[column].gteq(start_id))
if stop
stop_id = stop[column]
start_id = stop_id
relation = relation.where(arel_table[column].lt(stop_id))
end
# Any ORDER BYs are useless for this relation and can lead to less
# efficient UPDATE queries, hence we get rid of it.
yield relation.except(:order), index
break unless stop
end
end
end
end
class GitlabIssueTrackerService < IssueTrackerService class GitlabIssueTrackerService < IssueTrackerService
include Gitlab::Routing.url_helpers include Gitlab::Routing
validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated? validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated?
......
class JiraService < IssueTrackerService class JiraService < IssueTrackerService
include Gitlab::Routing.url_helpers include Gitlab::Routing
validates :url, url: true, presence: true, if: :activated? validates :url, url: true, presence: true, if: :activated?
validates :api_url, url: true, allow_blank: true validates :api_url, url: true, allow_blank: true
......
module ChatNames module ChatNames
class AuthorizeUserService class AuthorizeUserService
include Gitlab::Routing.url_helpers include Gitlab::Routing
def initialize(service, params) def initialize(service, params)
@service = service @service = service
......
...@@ -35,11 +35,12 @@ module MergeRequests ...@@ -35,11 +35,12 @@ module MergeRequests
# target branch manually # target branch manually
def close_merge_requests def close_merge_requests
commit_ids = @commits.map(&:id) commit_ids = @commits.map(&:id)
merge_requests = @project.merge_requests.opened.where(target_branch: @branch_name).to_a merge_requests = @project.merge_requests.preload(:merge_request_diff).opened.where(target_branch: @branch_name).to_a
merge_requests = merge_requests.select(&:diff_head_commit) merge_requests = merge_requests.select(&:diff_head_commit)
merge_requests = merge_requests.select do |merge_request| merge_requests = merge_requests.select do |merge_request|
commit_ids.include?(merge_request.diff_head_sha) commit_ids.include?(merge_request.diff_head_sha) &&
merge_request.merge_request_diff.state != 'empty'
end end
filter_merge_requests(merge_requests).each do |merge_request| filter_merge_requests(merge_requests).each do |merge_request|
......
...@@ -332,6 +332,22 @@ ...@@ -332,6 +332,22 @@
%strong.cred WARNING: %strong.cred WARNING:
Environment variable `prometheus_multiproc_dir` does not exist or is not pointing to a valid directory. Environment variable `prometheus_multiproc_dir` does not exist or is not pointing to a valid directory.
%fieldset
%legend Profiling - Performance Bar
%p
Enable the Performance Bar for a given group.
= link_to icon('question-circle'), help_page_path('administration/monitoring/performance/performance_bar')
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :performance_bar_enabled do
= f.check_box :performance_bar_enabled
Enable the Performance Bar
.form-group
= f.label :performance_bar_allowed_group_id, 'Allowed group', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :performance_bar_allowed_group_id, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
%fieldset %fieldset
%legend Background Jobs %legend Background Jobs
%p %p
......
...@@ -12,10 +12,12 @@ ...@@ -12,10 +12,12 @@
.content-wrapper{ class: "#{(layout_nav_class unless show_new_nav?)}" } .content-wrapper{ class: "#{(layout_nav_class unless show_new_nav?)}" }
.alert-wrapper .alert-wrapper
= render "layouts/broadcast" = render "layouts/broadcast"
= render "layouts/flash"
= yield :flash_message
- if show_new_nav? - if show_new_nav?
- if content_for?(:new_global_flash)
= yield :new_global_flash
= render "layouts/nav/breadcrumbs" = render "layouts/nav/breadcrumbs"
= render "layouts/flash"
= yield :flash_message
%div{ class: "#{(container_class unless @no_container)} #{@content_class}" } %div{ class: "#{(container_class unless @no_container)} #{@content_class}" }
.content{ id: "content-body" } .content{ id: "content-body" }
= yield = yield
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
= icon('wrench') = icon('wrench')
.project-title Admin Area .project-title Admin Area
%ul.sidebar-top-level-items %ul.sidebar-top-level-items
= nav_link(controller: %w(dashboard admin projects users groups builds runners cohorts), html_options: {class: 'home'}) do = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: {class: 'home'}) do
= link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
%span %span
Overview Overview
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
= link_to admin_groups_path, title: 'Groups' do = link_to admin_groups_path, title: 'Groups' do
%span %span
Groups Groups
= nav_link path: 'builds#index' do = nav_link path: 'jobs#index' do
= link_to admin_jobs_path, title: 'Jobs' do = link_to admin_jobs_path, title: 'Jobs' do
%span %span
Jobs Jobs
......
.nav-sidebar .nav-sidebar
= link_to group_path(@group), title: 'Group', class: 'context-header' do = link_to group_path(@group), title: @group.name, class: 'context-header' do
.avatar-container.s40.group-avatar .avatar-container.s40.group-avatar
= image_tag group_icon(@group), class: "avatar s40 avatar-tile" = image_tag group_icon(@group), class: "avatar s40 avatar-tile"
.group-title .group-title
......
.nav-sidebar .nav-sidebar
- can_edit = can?(current_user, :admin_project, @project) - can_edit = can?(current_user, :admin_project, @project)
= link_to project_path(@project), title: 'Project', class: 'context-header' do = link_to project_path(@project), title: @project.name, class: 'context-header' do
.avatar-container.s40.project-avatar .avatar-container.s40.project-avatar
= project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile') = project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile')
.project-title .project-title
......
- @no_container = true - @no_container = true
- flash_message_container = show_new_nav? ? :new_global_flash : :flash_message
= content_for :flash_message do = content_for flash_message_container do
- if current_user && can?(current_user, :download_code, @project) - if current_user && can?(current_user, :download_code, @project)
= render 'shared/no_ssh' = render 'shared/no_ssh'
= render 'shared/no_password' = render 'shared/no_password'
......
...@@ -30,24 +30,23 @@ ...@@ -30,24 +30,23 @@
.dropdown-menu.dropdown-menu-align-right.hidden-lg .dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul %ul
- if can_update_issue - if can_update_issue
%li %li= link_to 'Edit', edit_project_issue_path(@project, @issue)
= link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'issuable-edit' - unless current_user == @issue.author
%li %li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' - if can_update_issue
%li %li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' %li= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- if can_report_spam - if can_report_spam
%li %li= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
- if can_update_issue || can_report_spam - if can_update_issue || can_report_spam
%li.divider %li.divider
%li %li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link'
= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link'
- if can_update_issue - if can_update_issue
= link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' = link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' = render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue
- if can_report_spam - if can_report_spam
= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam' = link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
= link_to new_project_issue_path(@project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do = link_to new_project_issue_path(@project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
......
- can_update_merge_request = can?(current_user, :update_merge_request, @merge_request)
- if @merge_request.closed_without_fork? - if @merge_request.closed_without_fork?
.alert.alert-danger .alert.alert-danger
%p The source project of this merge request has been removed. %p The source project of this merge request has been removed.
...@@ -15,7 +17,6 @@ ...@@ -15,7 +17,6 @@
.issuable-meta .issuable-meta
= issuable_meta(@merge_request, @project, "Merge request") = issuable_meta(@merge_request, @project, "Merge request")
- if can?(current_user, :update_merge_request, @merge_request)
.issuable-actions .issuable-actions
.clearfix.issue-btn-group.dropdown .clearfix.issue-btn-group.dropdown
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } } %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
...@@ -23,13 +24,17 @@ ...@@ -23,13 +24,17 @@
= icon('caret-down') = icon('caret-down')
.dropdown-menu.dropdown-menu-align-right.hidden-lg .dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul %ul
- if can_update_merge_request
%li= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit'
- unless current_user == @merge_request.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request))
- if can_update_merge_request
%li{ class: merge_request_button_visibility(@merge_request, true) } %li{ class: merge_request_button_visibility(@merge_request, true) }
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request' = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
%li{ class: merge_request_button_visibility(@merge_request, false) } %li{ class: merge_request_button_visibility(@merge_request, false) }
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request' = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
%li
= link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: 'issuable-edit' - if can_update_merge_request
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{merge_request_button_visibility(@merge_request, true)}", title: 'Close merge request' = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit"
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen reopen-mr-link #{merge_request_button_visibility(@merge_request, false)}", title: 'Reopen merge request'
= link_to edit_project_merge_request_path(@project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit" do = render 'shared/issuable/close_reopen_button', issuable: @merge_request, can_update: can_update_merge_request
Edit
- @no_container = true - @no_container = true
- flash_message_container = show_new_nav? ? :new_global_flash : :flash_message
= content_for :meta_tags do = content_for :meta_tags do
= auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity") = auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity")
= content_for :flash_message do = content_for flash_message_container do
- if current_user && can?(current_user, :download_code, @project) - if current_user && can?(current_user, :download_code, @project)
= render 'shared/no_ssh' = render 'shared/no_ssh'
= render 'shared/no_password' = render 'shared/no_password'
......
- is_current_user = issuable_author_is_current_user(issuable)
- display_issuable_type = issuable_display_type(issuable)
- button_method = issuable_close_reopen_button_method(issuable)
- if can_update && is_current_user
= link_to "Close #{display_issuable_type}", close_issuable_url(issuable), method: button_method,
class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}"
= link_to "Reopen #{display_issuable_type}", reopen_issuable_url(issuable), method: button_method,
class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}"
- elsif can_update && !is_current_user
= render 'shared/issuable/close_reopen_report_toggle', issuable: issuable
- else
= link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
class: 'hidden-xs hidden-sm btn btn-grouped btn-close-color', title: 'Report abuse'
- display_issuable_type = issuable_display_type(issuable)
- button_action = issuable.closed? ? 'reopen' : 'close'
- display_button_action = button_action.capitalize
- button_responsive_class = 'hidden-xs hidden-sm'
- button_class = "#{button_responsive_class} btn btn-grouped js-issuable-close-button issuable-close-button"
- toggle_class = "#{button_responsive_class} btn btn-nr dropdown-toggle js-issuable-close-toggle"
- button_method = issuable_close_reopen_button_method(issuable)
.pull-left.btn-group.prepend-left-10.issuable-close-dropdown.droplab-dropdown.js-issuable-close-dropdown
= link_to "#{display_button_action} #{display_issuable_type}", close_reopen_issuable_url(issuable),
method: button_method, class: "#{button_class} btn-#{button_action}", title: "#{display_button_action} #{display_issuable_type}"
= button_tag type: 'button', class: "#{toggle_class} btn-#{button_action}-color",
data: { 'dropdown-trigger' => '#issuable-close-menu' }, 'aria-label' => 'Toggle dropdown' do
= icon('caret-down', class: 'toggle-icon icon')
%ul#issuable-close-menu.js-issuable-close-menu.dropdown-menu{ class: button_responsive_class, data: { dropdown: true } }
%li.close-item{ class: "#{issuable_button_visibility(issuable, true) || 'droplab-item-selected'}",
data: { text: "Close #{display_issuable_type}", url: close_issuable_url(issuable),
button_class: "#{button_class} btn-close", toggle_class: "#{toggle_class} btn-close-color", method: button_method } }
%button.btn.btn-transparent
= icon('check', class: 'icon')
.description
%strong.title
Close
= display_issuable_type
%li.reopen-item{ class: "#{issuable_button_visibility(issuable, false) || 'droplab-item-selected'}",
data: { text: "Reopen #{display_issuable_type}", url: reopen_issuable_url(issuable),
button_class: "#{button_class} btn-reopen", toggle_class: "#{toggle_class} btn-reopen-color", method: button_method } }
%button.btn.btn-transparent
= icon('check', class: 'icon')
.description
%strong.title
Reopen
= display_issuable_type
%li.divider.droplab-item-ignore
%li.report-item{ data: { text: 'Report abuse', url: new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
button_class: "#{button_class} btn-close-color", toggle_class: "#{toggle_class} btn-close-color", method: '' } }
%button.btn.btn-transparent
= icon('check', class: 'icon')
.description
%strong.title Report abuse
%p.text
Report
= display_issuable_type.pluralize
that are abusive, inappropriate or spam.
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
content_class: "filtered-search-history-dropdown-content", content_class: "filtered-search-history-dropdown-content",
title: "Recent searches" }) do title: "Recent searches" }) do
.js-filtered-search-history-dropdown{ data: { project_full_path: @project.full_path } } .js-filtered-search-history-dropdown{ data: { project_full_path: @project.full_path } }
.filtered-search-box-input-container .filtered-search-box-input-container.droplab-dropdown
.scroll-container .scroll-container
%ul.tokens-container.list-unstyled %ul.tokens-container.list-unstyled
%li.input-token %li.input-token
......
- noteable_name = @note.noteable.human_class_name - noteable_name = @note.noteable.human_class_name
.pull-left.btn-group.append-right-10.comment-type-dropdown.js-comment-type-dropdown .pull-left.btn-group.append-right-10.droplab-dropdown.comment-type-dropdown.js-comment-type-dropdown
%input.btn.btn-nr.btn-create.comment-btn.js-comment-button.js-comment-submit-button{ type: 'submit', value: 'Comment' } %input.btn.btn-nr.btn-create.comment-btn.js-comment-button.js-comment-submit-button{ type: 'submit', value: 'Comment' }
- if @note.can_be_discussion_note? - if @note.can_be_discussion_note?
...@@ -9,8 +9,8 @@ ...@@ -9,8 +9,8 @@
%ul#resolvable-comment-menu.dropdown-menu{ data: { dropdown: true } } %ul#resolvable-comment-menu.dropdown-menu{ data: { dropdown: true } }
%li#comment.droplab-item-selected{ data: { value: '', 'submit-text' => 'Comment', 'close-text' => "Comment & close #{noteable_name}", 'reopen-text' => "Comment & reopen #{noteable_name}" } } %li#comment.droplab-item-selected{ data: { value: '', 'submit-text' => 'Comment', 'close-text' => "Comment & close #{noteable_name}", 'reopen-text' => "Comment & reopen #{noteable_name}" } }
%a{ href: '#' } %button.btn.btn-transparent
= icon('check') = icon('check', class: 'icon')
.description .description
%strong Comment %strong Comment
%p %p
...@@ -19,8 +19,8 @@ ...@@ -19,8 +19,8 @@
%li.divider.droplab-item-ignore %li.divider.droplab-item-ignore
%li#discussion{ data: { value: 'DiscussionNote', 'submit-text' => 'Start discussion', 'close-text' => "Start discussion & close #{noteable_name}", 'reopen-text' => "Start discussion & reopen #{noteable_name}" } } %li#discussion{ data: { value: 'DiscussionNote', 'submit-text' => 'Start discussion', 'close-text' => "Start discussion & close #{noteable_name}", 'reopen-text' => "Start discussion & reopen #{noteable_name}" } }
%a{ href: '#' } %button.btn.btn-transparent
= icon('check') = icon('check', class: 'icon')
.description .description
%strong Start discussion %strong Start discussion
%p %p
......
---
title: "#20628 Enable implicit grant in GitLab as OAuth Provider"
merge_request: 12384
author: Mateusz Pytel
---
title: Change order of monospace fonts to fix bug on some linux distros
merge_request:
author:
---
title: Allow to enable the performance bar per user or Feature group
merge_request: 12362
author:
---
title: Allow admins to retrieve user agent details for an issue or snippet
merge_request: 12655
author:
---
title: Don't mark empty MRs as merged on push to the target branch
merge_request:
author:
---
title: Fixed GFM references not being included when updating issues inline
merge_request:
author:
...@@ -166,9 +166,10 @@ module Gitlab ...@@ -166,9 +166,10 @@ module Gitlab
config.after_initialize do config.after_initialize do
Rails.application.reload_routes! Rails.application.reload_routes!
named_routes_set = Gitlab::Application.routes.named_routes
project_url_helpers = Module.new do project_url_helpers = Module.new do
named_routes_set.helper_names.each do |name| extend ActiveSupport::Concern
Gitlab::Application.routes.named_routes.helper_names.each do |name|
next unless name.include?('namespace_project') next unless name.include?('namespace_project')
define_method(name.sub('namespace_project', 'project')) do |project, *args| define_method(name.sub('namespace_project', 'project')) do |project, *args|
...@@ -177,14 +178,7 @@ module Gitlab ...@@ -177,14 +178,7 @@ module Gitlab
end end
end end
named_routes_set.url_helpers_module.include project_url_helpers Gitlab::Routing.add_helpers(project_url_helpers)
named_routes_set.url_helpers_module.extend project_url_helpers
Gitlab::Routing.url_helpers.include project_url_helpers
Gitlab::Routing.url_helpers.extend project_url_helpers
GitlabRoutingHelper.include project_url_helpers
GitlabRoutingHelper.extend project_url_helpers
end end
end end
end end
...@@ -657,7 +657,10 @@ test: ...@@ -657,7 +657,10 @@ test:
client_id: 'YOUR_AUTH0_CLIENT_ID', client_id: 'YOUR_AUTH0_CLIENT_ID',
client_secret: 'YOUR_AUTH0_CLIENT_SECRET', client_secret: 'YOUR_AUTH0_CLIENT_SECRET',
namespace: 'YOUR_AUTH0_DOMAIN' } } namespace: 'YOUR_AUTH0_DOMAIN' } }
- { name: 'authentiq',
app_id: 'YOUR_CLIENT_ID',
app_secret: 'YOUR_CLIENT_SECRET',
args: { scope: 'aq:name email~rs address aq:push' } }
ldap: ldap:
enabled: false enabled: false
servers: servers:
......
...@@ -87,9 +87,7 @@ Doorkeeper.configure do ...@@ -87,9 +87,7 @@ Doorkeeper.configure do
# "password" => Resource Owner Password Credentials Grant Flow # "password" => Resource Owner Password Credentials Grant Flow
# "client_credentials" => Client Credentials Grant Flow # "client_credentials" => Client Credentials Grant Flow
# #
# If not specified, Doorkeeper enables all the four grant flows. grant_flows %w(authorization_code implicit password client_credentials)
#
grant_flows %w(authorization_code password client_credentials)
# Under some circumstances you might want to have applications auto-approved, # Under some circumstances you might want to have applications auto-approved,
# so that the user skips the authorization step. # so that the user skips the authorization step.
......
class AddPerformanceBarAllowedGroupIdToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :application_settings, :performance_bar_allowed_group_id, :integer
end
end
...@@ -126,6 +126,7 @@ ActiveRecord::Schema.define(version: 20170724184243) do ...@@ -126,6 +126,7 @@ ActiveRecord::Schema.define(version: 20170724184243) do
t.boolean "prometheus_metrics_enabled", default: false, null: false t.boolean "prometheus_metrics_enabled", default: false, null: false
t.boolean "help_page_hide_commercial_content", default: false t.boolean "help_page_hide_commercial_content", default: false
t.string "help_page_support_url" t.string "help_page_support_url"
t.integer "performance_bar_allowed_group_id"
end end
create_table "audit_events", force: :cascade do |t| create_table "audit_events", force: :cascade do |t|
......
...@@ -167,6 +167,7 @@ have access to GitLab administration tools and settings. ...@@ -167,6 +167,7 @@ have access to GitLab administration tools and settings.
- [Operations](administration/operations.md): Keeping GitLab up and running. - [Operations](administration/operations.md): Keeping GitLab up and running.
- [Polling](administration/polling.md): Configure how often the GitLab UI polls for updates. - [Polling](administration/polling.md): Configure how often the GitLab UI polls for updates.
- [Request Profiling](administration/monitoring/performance/request_profiling.md): Get a detailed profile on slow requests. - [Request Profiling](administration/monitoring/performance/request_profiling.md): Get a detailed profile on slow requests.
- [Performance Bar](administration/monitoring/performance/performance_bar.md): Get performance information for the current page.
### Customization ### Customization
......
# Performance Bar
A Performance Bar can be displayed, to dig into the performance of a page. When
activated, it looks as follows:
![Performance Bar](img/performance_bar.png)
It allows you to:
- see the current host serving the page
- see the timing of the page (backend, frontend)
- the number of DB queries, the time it took, and the detail of these queries
![SQL profiling using the Performance Bar](img/performance_bar_sql_queries.png)
- the number of calls to Redis, and the time it took
- the number of background jobs created by Sidekiq, and the time it took
- the number of Ruby GC calls, and the time it took
- profile the code used to generate the page, line by line
![Line profiling using the Performance Bar](img/performance_bar_line_profiling.png)
## Enable the Performance Bar via the Admin panel
GitLab Performance Bar is disabled by default. To enable it for a given group,
navigate to the Admin area in **Settings > Profiling - Performance Bar**
(`/admin/application_settings`).
The only required setting you need to set is the full path of the group that
will be allowed to display the Performance Bar.
Make sure _Enable the Performance Bar_ is checked and hit
**Save** to save the changes.
---
![GitLab Performance Bar Admin Settings](img/performance_bar_configuration_settings.png)
---
...@@ -17,6 +17,7 @@ following locations: ...@@ -17,6 +17,7 @@ following locations:
- [Deploy Keys](deploy_keys.md) - [Deploy Keys](deploy_keys.md)
- [Environments](environments.md) - [Environments](environments.md)
- [Events](events.md) - [Events](events.md)
- [Feature flags](features.md)
- [Gitignores templates](templates/gitignores.md) - [Gitignores templates](templates/gitignores.md)
- [GitLab CI Config templates](templates/gitlab_ci_ymls.md) - [GitLab CI Config templates](templates/gitlab_ci_ymls.md)
- [Groups](groups.md) - [Groups](groups.md)
......
# Features API # Features flags API
All methods require administrator authorization. All methods require administrator authorization.
...@@ -61,7 +61,8 @@ POST /features/:name ...@@ -61,7 +61,8 @@ POST /features/:name
| `feature_group` | string | no | A Feature group name | | `feature_group` | string | no | A Feature group name |
| `user` | string | no | A GitLab username | | `user` | string | no | A GitLab username |
Note that `feature_group` and `user` are mutually exclusive. Note that you can enable or disable a feature for both a `feature_group` and a
`user` with a single API call.
```bash ```bash
curl --data "value=30" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/features/new_library curl --data "value=30" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/features/new_library
......
...@@ -964,3 +964,30 @@ Example response: ...@@ -964,3 +964,30 @@ Example response:
## Comments on issues ## Comments on issues
Comments are done via the [notes](notes.md) resource. Comments are done via the [notes](notes.md) resource.
## Get user agent details
Available only for admins.
```
GET /projects/:id/issues/:issue_iid/user_agent_detail
```
| Attribute | Type | Required | Description |
|-------------|---------|----------|--------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
```bash
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/user_agent_detail
```
Example response:
```json
{
"user_agent": "AppleWebKit/537.36",
"ip_address": "127.0.0.1",
"akismet_submitted": false
}
```
# GitLab as an OAuth2 provider # GitLab as an OAuth2 provider
This document covers using the OAuth2 protocol to access GitLab. This document covers using the [OAuth2](https://oauth.net/2/) protocol to allow other services access Gitlab resources on user's behalf.
If you want GitLab to be an OAuth authentication service provider to sign into other services please see the [Oauth2 provider documentation](../integration/oauth_provider.md). If you want GitLab to be an OAuth authentication service provider to sign into other services please see the [OAuth2 provider](../integration/oauth_provider.md)
documentation.
OAuth2 is a protocol that enables us to authenticate a user without requiring them to give their password to a third-party. This functionality is based on [doorkeeper gem](https://github.com/doorkeeper-gem/doorkeeper).
This functionality is based on [doorkeeper gem](https://github.com/doorkeeper-gem/doorkeeper) ## Supported OAuth2 Flows
## Web Application Flow Gitlab currently supports following authorization flows:
This is the most common type of flow and is used by server-side clients that wish to access GitLab on a user's behalf. * *Web Application Flow* - Most secure and common type of flow, designed for the applications with secure server-side.
* *Implicit Flow* - This flow is designed for user-agent only apps (e.g. single page web application running on GitLab Pages).
* *Resource Owner Password Credentials Flow* - To be used **only** for securely hosted, first-party services.
>**Note:** Please refer to [OAuth RFC](https://tools.ietf.org/html/rfc6749) to find out in details how all those flows work and pick the right one for your use case.
This flow **should not** be used for client-side clients as you would leak your `client_secret`. Client-side clients should use the Implicit Grant (which is currently unsupported).
For more detailed information, check out the [RFC spec](http://tools.ietf.org/html/rfc6749#section-4.1) Both *web application* and *implicit* flows require `application` to be registered first via `/profile/applications` page
in your user's account. During registration, by enabling proper scopes you can limit the range of resources which the `application` can access. Upon creation
you'll obtain `application` credentials: _Application ID_ and _Client Secret_ - **keep them secure**.
In the following sections you will be introduced to the three steps needed for this flow. >**Important:** OAuth specification advises sending `state` parameter with each request to `/oauth/authorize`. We highly recommended to send a unique
value with each request and validate it against the one in redirect request. This is important to prevent [CSRF attacks]. The `state` param really should
have been a requirement in the standard!
### 1. Registering the client In the following sections you will find detailed instructions on how to obtain authorization with each flow.
First, you should create an application (`/profile/applications`) in your user's account. ### Web Application Flow
Each application gets a unique App ID and App Secret parameters.
>**Note:** Check [RFC spec](http://tools.ietf.org/html/rfc6749#section-4.1) for a detailed flow description
**You should not share/leak your App ID or App Secret.**
### 2. Requesting authorization #### 1. Requesting authorization code
To request the authorization code, you should redirect the user to the `/oauth/authorize` endpoint: To request the authorization code, you should redirect the user to the `/oauth/authorize` endpoint with following GET parameters:
``` ```
https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code&state=your_unique_state_hash https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code&state=YOUR_UNIQUE_STATE_HASH
``` ```
This will ask the user to approve the applications access to their account and then redirect back to the `REDIRECT_URI` you provided. This will ask the user to approve the applications access to their account and then redirect back to the `REDIRECT_URI` you provided. The redirect will
include the GET `code` parameter, for example:
The redirect will include the GET `code` parameter, for example: `http://myapp.com/oauth/redirect?code=1234567890&state=YOUR_UNIQUE_STATE_HASH`
```
http://myapp.com/oauth/redirect?code=1234567890&state=your_unique_state_hash
```
You should then use the `code` to request an access token. You should then use the `code` to request an access token.
>**Important:** #### 2. Requesting access token
It is highly recommended that you send a `state` value with the request to `/oauth/authorize` and
validate that value is returned and matches in the redirect request.
This is important to prevent [CSRF attacks](http://www.oauthsecurity.com/#user-content-authorization-code-flow),
`state` really should have been a requirement in the standard!
### 3. Requesting the access token
Once you have the authorization code you can request an `access_token` using the code, to do that you can use any HTTP client. In the following example, we are using Ruby's `rest-client`: Once you have the authorization code you can request an `access_token` using the code, to do that you can use any HTTP client. In the following example,
we are using Ruby's `rest-client`:
``` ```
parameters = 'client_id=APP_ID&client_secret=APP_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=REDIRECT_URI' parameters = 'client_id=APP_ID&client_secret=APP_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=REDIRECT_URI'
...@@ -72,28 +68,40 @@ The `redirect_uri` must match the `redirect_uri` used in the original authorizat ...@@ -72,28 +68,40 @@ The `redirect_uri` must match the `redirect_uri` used in the original authorizat
You can now make requests to the API with the access token returned. You can now make requests to the API with the access token returned.
### Use the access token to access the API
The access token allows you to make requests to the API on a behalf of a user. ### Implicit Grant
Check [RFC spec](http://tools.ietf.org/html/rfc6749#section-4.2) for a detailed flow description.
Unlike the web flow, the client receives an `access token` immediately as a result of the authorization request. The flow does not use client secret
or authorization code because all of the application code and storage is easily accessible, therefore __secrets__ can leak easily.
>**Important:** Avoid using this flow for applications that store data outside of the Gitlab instance. If you do, make sure to verify `application id`
associated with access token before granting access to the data
(see [/oauth/token/info](https://github.com/doorkeeper-gem/doorkeeper/wiki/API-endpoint-descriptions-and-examples#get----oauthtokeninfo)).
#### 1. Requesting access token
To request the access token, you should redirect the user to the `/oauth/authorize` endpoint using `token` response type:
``` ```
GET https://gitlab.example.com/api/v4/user?access_token=OAUTH-TOKEN https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=token&state=YOUR_UNIQUE_STATE_HASH
``` ```
Or you can put the token to the Authorization header: This will ask the user to approve the applications access to their account and then redirect back to the `REDIRECT_URI` you provided. The redirect
will include a fragment with `access_token` as well as token details in GET parameters, for example:
``` ```
curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v4/user http://myapp.com/oauth/redirect#access_token=ABCDExyz123&state=YOUR_UNIQUE_STATE_HASH&token_type=bearer&expires_in=3600
``` ```
## Resource Owner Password Credentials ### Resource Owner Password Credentials
## Deprecation Notice
1. Starting in GitLab 8.11, the Resource Owner Password Credentials has been *disabled* for users with two-factor authentication turned on. Check [RFC spec](http://tools.ietf.org/html/rfc6749#section-4.3) for a detailed flow description.
2. These users can access the API using [personal access tokens] instead.
--- > **Deprecation notice:** Starting in GitLab 8.11, the Resource Owner Password Credentials has been *disabled* for users with two-factor authentication
turned on. These users can access the API using [personal access tokens] instead.
In this flow, a token is requested in exchange for the resource owner credentials (username and password). In this flow, a token is requested in exchange for the resource owner credentials (username and password).
The credentials should only be used when there is a high degree of trust between the resource owner and the client (e.g. the The credentials should only be used when there is a high degree of trust between the resource owner and the client (e.g. the
...@@ -101,12 +109,16 @@ client is part of the device operating system or a highly privileged application ...@@ -101,12 +109,16 @@ client is part of the device operating system or a highly privileged application
available (such as an authorization code). available (such as an authorization code).
>**Important:** >**Important:**
Never store the users credentials and only use this grant type when your client is deployed to a trusted environment, in 99% of cases [personal access tokens] are a better choice. Never store the users credentials and only use this grant type when your client is deployed to a trusted environment, in 99% of cases [personal access tokens]
are a better choice.
Even though this grant type requires direct client access to the resource owner credentials, the resource owner credentials are used Even though this grant type requires direct client access to the resource owner credentials, the resource owner credentials are used
for a single request and are exchanged for an access token. This grant type can eliminate the need for the client to store the for a single request and are exchanged for an access token. This grant type can eliminate the need for the client to store the
resource owner credentials for future use, by exchanging the credentials with a long-lived access token or refresh token. resource owner credentials for future use, by exchanging the credentials with a long-lived access token or refresh token.
You can do POST request to `/oauth/token` with parameters:
#### 1. Requesting access token
POST request to `/oauth/token` with parameters:
``` ```
{ {
...@@ -134,4 +146,18 @@ access_token = client.password.get_token('user@example.com', 'secret') ...@@ -134,4 +146,18 @@ access_token = client.password.get_token('user@example.com', 'secret')
puts access_token.token puts access_token.token
``` ```
## Access Gitlab API with `access token`
The `access token` allows you to make requests to the API on a behalf of a user. You can pass the token either as GET parameter
```
GET https://gitlab.example.com/api/v4/user?access_token=OAUTH-TOKEN
```
or you can put the token to the Authorization header:
```
curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v4/user
```
[personal access tokens]: ../user/profile/personal_access_tokens.md [personal access tokens]: ../user/profile/personal_access_tokens.md
[CSRF attacks]: http://www.oauthsecurity.com/#user-content-authorization-code-flow
\ No newline at end of file
...@@ -119,3 +119,35 @@ Parameters: ...@@ -119,3 +119,35 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `snippet_id` (required) - The ID of a project's snippet - `snippet_id` (required) - The ID of a project's snippet
## Get user agent details
> **Notes:**
> [Introduced][ce-29508] in GitLab 9.4.
Available only for admins.
```
GET /projects/:id/snippets/:snippet_id/user_agent_detail
```
| Attribute | Type | Required | Description |
|-------------|---------|----------|--------------------------------------|
| `id` | Integer | yes | The ID of a snippet |
```bash
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/snippets/1/user_agent_detail
```
Example response:
```json
{
"user_agent": "AppleWebKit/537.36",
"ip_address": "127.0.0.1",
"akismet_submitted": false
}
```
[ce-[ce-29508]: https://gitlab.com/gitlab-org/gitlab-ce/issues/29508]: https://gitlab.com/gitlab-org/gitlab-ce/issues/29508
...@@ -234,3 +234,35 @@ Example response: ...@@ -234,3 +234,35 @@ Example response:
} }
] ]
``` ```
## Get user agent details
> **Notes:**
> [Introduced][ce-29508] in GitLab 9.4.
Available only for admins.
```
GET /snippets/:id/user_agent_detail
```
| Attribute | Type | Required | Description |
|-------------|---------|----------|--------------------------------------|
| `id` | Integer | yes | The ID of a snippet |
```bash
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/snippets/1/user_agent_detail
```
Example response:
```json
{
"user_agent": "AppleWebKit/537.36",
"ip_address": "127.0.0.1",
"akismet_submitted": false
}
```
[ce-[ce-29508]: https://gitlab.com/gitlab-org/gitlab-ce/issues/29508]: https://gitlab.com/gitlab-org/gitlab-ce/issues/29508
...@@ -55,6 +55,7 @@ ...@@ -55,6 +55,7 @@
- [Single Table Inheritance](single_table_inheritance.md) - [Single Table Inheritance](single_table_inheritance.md)
- [Background Migrations](background_migrations.md) - [Background Migrations](background_migrations.md)
- [Storing SHA1 Hashes As Binary](sha1_as_binary.md) - [Storing SHA1 Hashes As Binary](sha1_as_binary.md)
- [Iterating Tables In Batches](iterating_tables_in_batches.md)
## i18n ## i18n
......
...@@ -3,5 +3,19 @@ ...@@ -3,5 +3,19 @@
Starting from GitLab 9.3 we support feature flags via Starting from GitLab 9.3 we support feature flags via
[Flipper](https://github.com/jnunemaker/flipper/). You should use the `Feature` [Flipper](https://github.com/jnunemaker/flipper/). You should use the `Feature`
class (defined in `lib/feature.rb`) in your code to get, set and list feature class (defined in `lib/feature.rb`) in your code to get, set and list feature
flags. During runtime you can set the values for the gates via the flags.
[admin API](../api/features.md).
During runtime you can set the values for the gates via the
[features API](../api/features.md) (accessible to admins only).
## Feature groups
Starting from GitLab 9.4 we support feature groups via
[Flipper groups](https://github.com/jnunemaker/flipper/blob/v0.10.2/docs/Gates.md#2-group).
Feature groups must be defined statically in `lib/feature.rb` (in the
`.register_feature_groups` method), but their implementation can obviously be
dynamic (querying the DB etc.).
Once defined in `lib/feature.rb`, you will be able to activate a
feature for a given feature group via the [`feature_group` param of the features API](../api/features.md#set-or-create-a-feature)
# Iterating Tables In Batches
Rails provides a method called `in_batches` that can be used to iterate over
rows in batches. For example:
```ruby
User.in_batches(of: 10) do |relation|
relation.update_all(updated_at: Time.now)
end
```
Unfortunately this method is implemented in a way that is not very efficient,
both query and memory usage wise.
To work around this you can include the `EachBatch` module into your models,
then use the `each_batch` class method. For example:
```ruby
class User < ActiveRecord::Base
include EachBatch
end
User.each_batch(of: 10) do |relation|
relation.update_all(updated_at: Time.now)
end
```
This will end up producing queries such as:
```
User Load (0.7ms) SELECT "users"."id" FROM "users" WHERE ("users"."id" >= 41654) ORDER BY "users"."id" ASC LIMIT 1 OFFSET 1000
(0.7ms) SELECT COUNT(*) FROM "users" WHERE ("users"."id" >= 41654) AND ("users"."id" < 42687)
```
The API of this method is similar to `in_batches`, though it doesn't support
all of the arguments that `in_batches` supports. You should always use
`each_batch` _unless_ you have a specific need for `in_batches`.
...@@ -39,6 +39,9 @@ mysql> SET storage_engine=INNODB; ...@@ -39,6 +39,9 @@ mysql> SET storage_engine=INNODB;
# If you have MySQL < 5.7.7 and want to enable utf8mb4 character set support with your GitLab install, you must set the following NOW: # If you have MySQL < 5.7.7 and want to enable utf8mb4 character set support with your GitLab install, you must set the following NOW:
mysql> SET GLOBAL innodb_file_per_table=1, innodb_file_format=Barracuda, innodb_large_prefix=1; mysql> SET GLOBAL innodb_file_per_table=1, innodb_file_format=Barracuda, innodb_large_prefix=1;
# If you use MySQL with replication, or just have MySQL configured with binary logging, you need to run the following to allow the use of `TRIGGER`:
mysql> SET GLOBAL log_bin_trust_function_creators = 1;
# Create the GitLab production database # Create the GitLab production database
mysql> CREATE DATABASE IF NOT EXISTS `gitlabhq_production` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_general_ci`; mysql> CREATE DATABASE IF NOT EXISTS `gitlabhq_production` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_general_ci`;
...@@ -60,7 +63,15 @@ mysql> \q ...@@ -60,7 +63,15 @@ mysql> \q
``` ```
You are done installing the database for now and can go back to the rest of the installation. You are done installing the database for now and can go back to the rest of the installation.
Please proceed to the rest of the installation before running through the utf8mb4 support section. Please proceed to the rest of the installation **before** running through the steps below.
### `log_bin_trust_function_creators`
If you use MySQL with replication, or just have MySQL configured with binary logging, all of your MySQL servers will need to have `log_bin_trust_function_creators` enabled to allow the use of `TRIGGER` in migrations. You have already set this global variable in the steps above, but to make it persistent, add the following to your `my.cnf` file:
```
log_bin_trust_function_creators=1
```
### MySQL utf8mb4 support ### MySQL utf8mb4 support
......
...@@ -165,6 +165,19 @@ permissions on the database: ...@@ -165,6 +165,19 @@ permissions on the database:
mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';" mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';"
``` ```
If you use MySQL with replication, or just have MySQL configured with binary logging,
you will need to also run the following on all of your MySQL servers:
```bash
mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;"
```
You can make this setting permanent by adding it to your `my.cnf`:
```
log_bin_trust_function_creators=1
```
### 11. Update configuration files ### 11. Update configuration files
#### New configuration options for `gitlab.yml` #### New configuration options for `gitlab.yml`
......
...@@ -888,5 +888,11 @@ module API ...@@ -888,5 +888,11 @@ module API
expose :dependencies, using: Dependency expose :dependencies, using: Dependency
end end
end end
class UserAgentDetail < Grape::Entity
expose :user_agent
expose :ip_address
expose :submitted, as: :akismet_submitted
end
end end
end end
...@@ -14,14 +14,12 @@ module API ...@@ -14,14 +14,12 @@ module API
end end
end end
def gate_target(params) def gate_targets(params)
if params[:feature_group] targets = []
Feature.group(params[:feature_group]) targets << Feature.group(params[:feature_group]) if params[:feature_group]
elsif params[:user] targets << User.find_by_username(params[:user]) if params[:user]
User.find_by_username(params[:user])
else targets
gate_value(params)
end
end end
end end
...@@ -42,18 +40,25 @@ module API ...@@ -42,18 +40,25 @@ module API
requires :value, type: String, desc: '`true` or `false` to enable/disable, an integer for percentage of time' requires :value, type: String, desc: '`true` or `false` to enable/disable, an integer for percentage of time'
optional :feature_group, type: String, desc: 'A Feature group name' optional :feature_group, type: String, desc: 'A Feature group name'
optional :user, type: String, desc: 'A GitLab username' optional :user, type: String, desc: 'A GitLab username'
mutually_exclusive :feature_group, :user
end end
post ':name' do post ':name' do
feature = Feature.get(params[:name]) feature = Feature.get(params[:name])
target = gate_target(params) targets = gate_targets(params)
value = gate_value(params) value = gate_value(params)
case value case value
when true when true
feature.enable(target) if targets.present?
targets.each { |target| feature.enable(target) }
else
feature.enable
end
when false when false
feature.disable(target) if targets.present?
targets.each { |target| feature.disable(target) }
else
feature.disable
end
else else
feature.enable_percentage_of_time(value) feature.enable_percentage_of_time(value)
end end
......
...@@ -241,6 +241,22 @@ module API ...@@ -241,6 +241,22 @@ module API
present paginate(merge_requests), with: Entities::MergeRequestBasic, current_user: current_user, project: user_project present paginate(merge_requests), with: Entities::MergeRequestBasic, current_user: current_user, project: user_project
end end
desc 'Get the user agent details for an issue' do
success Entities::UserAgentDetail
end
params do
requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
end
get ":id/issues/:issue_iid/user_agent_detail" do
authenticated_as_admin!
issue = find_project_issue(params[:issue_iid])
return not_found!('UserAgentDetail') unless issue.user_agent_detail
present issue.user_agent_detail, with: Entities::UserAgentDetail
end
end end
end end
end end
...@@ -131,6 +131,22 @@ module API ...@@ -131,6 +131,22 @@ module API
content_type 'text/plain' content_type 'text/plain'
present snippet.content present snippet.content
end end
desc 'Get the user agent details for a project snippet' do
success Entities::UserAgentDetail
end
params do
requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
end
get ":id/snippets/:snippet_id/user_agent_detail" do
authenticated_as_admin!
snippet = Snippet.find_by!(id: params[:id])
return not_found!('UserAgentDetail') unless snippet.user_agent_detail
present snippet.user_agent_detail, with: Entities::UserAgentDetail
end
end end
end end
end end
...@@ -140,6 +140,22 @@ module API ...@@ -140,6 +140,22 @@ module API
content_type 'text/plain' content_type 'text/plain'
present snippet.content present snippet.content
end end
desc 'Get the user agent details for a snippet' do
success Entities::UserAgentDetail
end
params do
requires :id, type: Integer, desc: 'The ID of a snippet'
end
get ":id/user_agent_detail" do
authenticated_as_admin!
snippet = Snippet.find_by!(id: params[:id])
return not_found!('UserAgentDetail') unless snippet.user_agent_detail
present snippet.user_agent_detail, with: Entities::UserAgentDetail
end
end end
end end
end end
...@@ -4,7 +4,7 @@ module Gitlab ...@@ -4,7 +4,7 @@ module Gitlab
# Abstract class for badge metadata # Abstract class for badge metadata
# #
class Metadata class Metadata
include Gitlab::Routing.url_helpers include Gitlab::Routing
include ActionView::Helpers::AssetTagHelper include ActionView::Helpers::AssetTagHelper
include ActionView::Helpers::UrlHelper include ActionView::Helpers::UrlHelper
......
module Gitlab module Gitlab
module Conflict module Conflict
class File class File
include Gitlab::Routing.url_helpers include Gitlab::Routing
include IconsHelper include IconsHelper
MissingResolution = Class.new(ResolutionError) MissingResolution = Class.new(ResolutionError)
......
...@@ -4,7 +4,7 @@ module Gitlab ...@@ -4,7 +4,7 @@ module Gitlab
class RepositoryPush class RepositoryPush
attr_reader :author_id, :ref, :action attr_reader :author_id, :ref, :action
include Gitlab::Routing.url_helpers include Gitlab::Routing
include DiffHelper include DiffHelper
delegate :namespace, :name_with_namespace, to: :project, prefix: :project delegate :namespace, :name_with_namespace, to: :project, prefix: :project
......
...@@ -41,10 +41,6 @@ module Gitlab ...@@ -41,10 +41,6 @@ module Gitlab
commit_id: sha commit_id: sha
) )
when :BLOB when :BLOB
# EncodingDetector checks the first 1024 * 1024 bytes for NUL byte, libgit2 checks
# only the first 8000 (https://github.com/libgit2/libgit2/blob/2ed855a9e8f9af211e7274021c2264e600c0f86b/src/filter.h#L15),
# which is what we use below to keep a consistent behavior.
detect = CharlockHolmes::EncodingDetector.new(8000).detect(entry.data)
new( new(
id: entry.oid, id: entry.oid,
name: name, name: name,
...@@ -53,7 +49,7 @@ module Gitlab ...@@ -53,7 +49,7 @@ module Gitlab
mode: entry.mode.to_s(8), mode: entry.mode.to_s(8),
path: path, path: path,
commit_id: sha, commit_id: sha,
binary: detect && detect[:type] == :binary binary: binary?(entry.data)
) )
end end
end end
...@@ -87,6 +83,10 @@ module Gitlab ...@@ -87,6 +83,10 @@ module Gitlab
end end
def raw(repository, sha) def raw(repository, sha)
Gitlab::GitalyClient.migrate(:git_blob_raw) do |is_enabled|
if is_enabled
Gitlab::GitalyClient::Blob.new(repository).get_blob(oid: sha, limit: MAX_DATA_DISPLAY_SIZE)
else
blob = repository.lookup(sha) blob = repository.lookup(sha)
new( new(
...@@ -96,6 +96,16 @@ module Gitlab ...@@ -96,6 +96,16 @@ module Gitlab
binary: blob.binary? binary: blob.binary?
) )
end end
end
end
def binary?(data)
# EncodingDetector checks the first 1024 * 1024 bytes for NUL byte, libgit2 checks
# only the first 8000 (https://github.com/libgit2/libgit2/blob/2ed855a9e8f9af211e7274021c2264e600c0f86b/src/filter.h#L15),
# which is what we use below to keep a consistent behavior.
detect = CharlockHolmes::EncodingDetector.new(8000).detect(data)
detect && detect[:type] == :binary
end
# Recursive search of blob id by path # Recursive search of blob id by path
# #
...@@ -165,8 +175,17 @@ module Gitlab ...@@ -165,8 +175,17 @@ module Gitlab
return if @data == '' # don't mess with submodule blobs return if @data == '' # don't mess with submodule blobs
return @data if @loaded_all_data return @data if @loaded_all_data
Gitlab::GitalyClient.migrate(:git_blob_load_all_data) do |is_enabled|
@data = begin
if is_enabled
Gitlab::GitalyClient::Blob.new(repository).get_blob(oid: id, limit: -1).data
else
repository.lookup(id).content
end
end
end
@loaded_all_data = true @loaded_all_data = true
@data = repository.lookup(id).content
@loaded_size = @data.bytesize @loaded_size = @data.bytesize
@binary = nil @binary = nil
end end
......
module Gitlab
module GitalyClient
class Blob
def initialize(repository)
@gitaly_repo = repository.gitaly_repository
end
def get_blob(oid:, limit:)
request = Gitaly::GetBlobRequest.new(
repository: @gitaly_repo,
oid: oid,
limit: limit
)
response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_blob, request)
blob = response.first
return unless blob.oid.present?
data = response.reduce(blob.data.dup) { |memo, msg| memo << msg.data.dup }
Gitlab::Git::Blob.new(
id: blob.oid,
size: blob.size,
data: data,
binary: Gitlab::Git::Blob.binary?(data)
)
end
end
end
end
...@@ -34,7 +34,7 @@ module Gitlab ...@@ -34,7 +34,7 @@ module Gitlab
write_csv do |csv| write_csv do |csv|
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
User.with_two_factor.in_batches do |relation| User.with_two_factor.in_batches do |relation| # rubocop: disable Cop/InBatches
rows = relation.pluck(:id, :encrypted_otp_secret, :encrypted_otp_secret_iv, :encrypted_otp_secret_salt) rows = relation.pluck(:id, :encrypted_otp_secret, :encrypted_otp_secret_iv, :encrypted_otp_secret_salt)
rows.each do |row| rows.each do |row|
user = %i[id ciphertext iv salt].zip(row).to_h user = %i[id ciphertext iv salt].zip(row).to_h
......
module Gitlab module Gitlab
module PerformanceBar module PerformanceBar
def self.enabled? include Gitlab::CurrentSettings
Rails.env.development? || Feature.enabled?('gitlab_performance_bar')
ALLOWED_USER_IDS_KEY = 'performance_bar_allowed_user_ids'.freeze
def self.enabled?(user = nil)
return false unless user && allowed_group_id
allowed_user_ids.include?(user.id)
end
def self.allowed_group_id
current_application_settings.performance_bar_allowed_group_id
end
def self.allowed_user_ids
Rails.cache.fetch(ALLOWED_USER_IDS_KEY) do
group = Group.find_by_id(allowed_group_id)
if group
GroupMembersFinder.new(group).execute.pluck(:user_id)
else
[]
end
end
end
def self.expire_allowed_user_ids_cache
Rails.cache.delete(ALLOWED_USER_IDS_KEY)
end end
end end
end end
...@@ -2,10 +2,30 @@ module Gitlab ...@@ -2,10 +2,30 @@ module Gitlab
module Routing module Routing
extend ActiveSupport::Concern extend ActiveSupport::Concern
mattr_accessor :_includers
self._includers = []
included do included do
Gitlab::Routing._includers << self
include Gitlab::Routing.url_helpers include Gitlab::Routing.url_helpers
end end
def self.add_helpers(mod)
url_helpers.include mod
url_helpers.extend mod
app_url_helpers = Gitlab::Application.routes.named_routes.url_helpers_module
app_url_helpers.include mod
app_url_helpers.extend mod
GitlabRoutingHelper.include mod
GitlabRoutingHelper.extend mod
_includers.each do |klass|
klass.include mod
end
end
# Returns the URL helpers Module. # Returns the URL helpers Module.
# #
# This method caches the output as Rails' "url_helpers" method creates an # This method caches the output as Rails' "url_helpers" method creates an
......
...@@ -2,7 +2,7 @@ module Gitlab ...@@ -2,7 +2,7 @@ module Gitlab
module SlashCommands module SlashCommands
module Presenters module Presenters
class Base class Base
include Gitlab::Routing.url_helpers include Gitlab::Routing
def initialize(resource = nil) def initialize(resource = nil)
@resource = resource @resource = resource
......
module Gitlab module Gitlab
class UrlBuilder class UrlBuilder
include Gitlab::Routing.url_helpers include Gitlab::Routing
include GitlabRoutingHelper include GitlabRoutingHelper
include ActionView::RecordIdentifier include ActionView::RecordIdentifier
......
require_relative '../model_helpers'
module RuboCop
module Cop
# Cop that prevents the use of `in_batches`
class InBatches < RuboCop::Cop::Cop
MSG = 'Do not use `in_batches`, use `each_batch` from the EachBatch module instead'.freeze
def on_send(node)
return unless node.children[1] == :in_batches
add_offense(node, :selector)
end
end
end
end
...@@ -5,6 +5,7 @@ require_relative 'cop/redirect_with_status' ...@@ -5,6 +5,7 @@ require_relative 'cop/redirect_with_status'
require_relative 'cop/polymorphic_associations' require_relative 'cop/polymorphic_associations'
require_relative 'cop/project_path_helper' require_relative 'cop/project_path_helper'
require_relative 'cop/active_record_dependent' require_relative 'cop/active_record_dependent'
require_relative 'cop/in_batches'
require_relative 'cop/migration/add_column' require_relative 'cop/migration/add_column'
require_relative 'cop/migration/add_column_with_default_to_large_table' require_relative 'cop/migration/add_column_with_default_to_large_table'
require_relative 'cop/migration/add_concurrent_foreign_key' require_relative 'cop/migration/add_concurrent_foreign_key'
......
require 'spec_helper'
describe 'Issuables Close/Reopen/Report toggle', :feature do
let(:user) { create(:user) }
shared_examples 'an issuable close/reopen/report toggle' do
let(:container) { find('.issuable-close-dropdown') }
let(:human_model_name) { issuable.model_name.human.downcase }
it 'shows toggle' do
expect(page).to have_link("Close #{human_model_name}")
expect(page).to have_selector('.issuable-close-dropdown')
end
it 'opens a dropdown when toggle is clicked' do
container.find('.dropdown-toggle').click
expect(container).to have_selector('.dropdown-menu')
expect(container).to have_content("Close #{human_model_name}")
expect(container).to have_content('Report abuse')
expect(container).to have_content("Report #{human_model_name.pluralize} that are abusive, inappropriate or spam.")
expect(container).to have_selector('.close-item.droplab-item-selected')
expect(container).to have_selector('.report-item')
expect(container).not_to have_selector('.report-item.droplab-item-selected')
expect(container).not_to have_selector('.reopen-item')
end
it 'changes the button when an item is selected' do
button = container.find('.issuable-close-button')
container.find('.dropdown-toggle').click
container.find('.report-item').click
expect(container).not_to have_selector('.dropdown-menu')
expect(button).to have_content('Report abuse')
container.find('.dropdown-toggle').click
container.find('.close-item').click
expect(button).to have_content("Close #{human_model_name}")
end
end
context 'on an issue' do
let(:project) { create(:empty_project) }
let(:issuable) { create(:issue, project: project) }
before do
project.add_master(user)
login_as user
end
context 'when user has permission to update', :js do
before do
visit project_issue_path(project, issuable)
end
it_behaves_like 'an issuable close/reopen/report toggle'
end
context 'when user doesnt have permission to update' do
let(:cant_project) { create(:empty_project) }
let(:cant_issuable) { create(:issue, project: cant_project) }
before do
cant_project.add_guest(user)
visit project_issue_path(cant_project, cant_issuable)
end
it 'only shows the `Report abuse` and `New issue` buttons' do
expect(page).to have_link('Report abuse')
expect(page).to have_link('New issue')
expect(page).not_to have_link('Close issue')
expect(page).not_to have_link('Reopen issue')
expect(page).not_to have_link('Edit')
end
end
end
context 'on a merge request' do
let(:project) { create(:project) }
let(:issuable) { create(:merge_request, source_project: project) }
before do
project.add_master(user)
login_as user
end
context 'when user has permission to update', :js do
before do
visit project_merge_request_path(project, issuable)
end
it_behaves_like 'an issuable close/reopen/report toggle'
end
context 'when user doesnt have permission to update' do
let(:cant_project) { create(:project) }
let(:cant_issuable) { create(:merge_request, source_project: cant_project) }
before do
cant_project.add_reporter(user)
visit project_merge_request_path(cant_project, cant_issuable)
end
it 'only shows a `Report abuse` button' do
expect(page).to have_link('Report abuse')
expect(page).not_to have_link('Close merge request')
expect(page).not_to have_link('Reopen merge request')
expect(page).not_to have_link('Edit')
end
end
end
end
...@@ -133,7 +133,7 @@ describe 'Visual tokens', js: true, feature: true do ...@@ -133,7 +133,7 @@ describe 'Visual tokens', js: true, feature: true do
describe 'editing milestone token' do describe 'editing milestone token' do
before do before do
input_filtered_search('milestone:%10.0 author:none', submit: false) input_filtered_search('milestone:%10.0 author:none', submit: false)
first('.tokens-container .filtered-search-token').double_click first('.tokens-container .filtered-search-token').click
first('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item') first('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item')
end end
......
...@@ -14,6 +14,18 @@ feature 'GFM autocomplete', feature: true, js: true do ...@@ -14,6 +14,18 @@ feature 'GFM autocomplete', feature: true, js: true do
wait_for_requests wait_for_requests
end end
it 'updates issue descripton with GFM reference' do
find('.issuable-edit').click
find('#issue-description').native.send_keys("@#{user.name[0...3]}")
find('.atwho-view .cur').trigger('click')
click_button 'Save changes'
expect(find('.description')).to have_content(user.to_reference)
end
it 'opens autocomplete menu when field starts with text' do it 'opens autocomplete menu when field starts with text' do
page.within '.timeline-content-form' do page.within '.timeline-content-form' do
find('#note_note').native.send_keys('') find('#note_note').native.send_keys('')
......
...@@ -13,7 +13,7 @@ feature 'OAuth Login', js: true do ...@@ -13,7 +13,7 @@ feature 'OAuth Login', js: true do
end end
providers = [:github, :twitter, :bitbucket, :gitlab, :google_oauth2, providers = [:github, :twitter, :bitbucket, :gitlab, :google_oauth2,
:facebook, :cas3, :auth0] :facebook, :cas3, :auth0, :authentiq]
before(:all) do before(:all) do
# The OmniAuth `full_host` parameter doesn't get set correctly (it gets set to something like `http://localhost` # The OmniAuth `full_host` parameter doesn't get set correctly (it gets set to something like `http://localhost`
......
...@@ -33,22 +33,24 @@ describe 'User can display performance bar', :js do ...@@ -33,22 +33,24 @@ describe 'User can display performance bar', :js do
end end
end end
let(:group) { create(:group) }
context 'when user is logged-out' do context 'when user is logged-out' do
before do before do
visit root_path visit root_path
end end
context 'when the gitlab_performance_bar feature is disabled' do context 'when the performance_bar feature is disabled' do
before do before do
Feature.disable('gitlab_performance_bar') stub_application_setting(performance_bar_allowed_group_id: nil)
end end
it_behaves_like 'performance bar is disabled' it_behaves_like 'performance bar is disabled'
end end
context 'when the gitlab_performance_bar feature is enabled' do context 'when the performance_bar feature is enabled' do
before do before do
Feature.enable('gitlab_performance_bar') stub_application_setting(performance_bar_allowed_group_id: group.id)
end end
it_behaves_like 'performance bar is disabled' it_behaves_like 'performance bar is disabled'
...@@ -57,22 +59,25 @@ describe 'User can display performance bar', :js do ...@@ -57,22 +59,25 @@ describe 'User can display performance bar', :js do
context 'when user is logged-in' do context 'when user is logged-in' do
before do before do
sign_in(create(:user)) user = create(:user)
sign_in(user)
group.add_guest(user)
visit root_path visit root_path
end end
context 'when the gitlab_performance_bar feature is disabled' do context 'when the performance_bar feature is disabled' do
before do before do
Feature.disable('gitlab_performance_bar') stub_application_setting(performance_bar_allowed_group_id: nil)
end end
it_behaves_like 'performance bar is disabled' it_behaves_like 'performance bar is disabled'
end end
context 'when the gitlab_performance_bar feature is enabled' do context 'when the performance_bar feature is enabled' do
before do before do
Feature.enable('gitlab_performance_bar') stub_application_setting(performance_bar_allowed_group_id: group.id)
end end
it_behaves_like 'performance bar is enabled' it_behaves_like 'performance bar is enabled'
......
import CloseReopenReportToggle from '~/close_reopen_report_toggle';
import DropLab from '~/droplab/drop_lab';
describe('CloseReopenReportToggle', () => {
describe('class constructor', () => {
const dropdownTrigger = {};
const dropdownList = {};
const button = {};
let commentTypeToggle;
beforeEach(function () {
commentTypeToggle = new CloseReopenReportToggle({
dropdownTrigger,
dropdownList,
button,
});
});
it('sets .dropdownTrigger', function () {
expect(commentTypeToggle.dropdownTrigger).toBe(dropdownTrigger);
});
it('sets .dropdownList', function () {
expect(commentTypeToggle.dropdownList).toBe(dropdownList);
});
it('sets .button', function () {
expect(commentTypeToggle.button).toBe(button);
});
});
describe('initDroplab', () => {
let closeReopenReportToggle;
const dropdownList = jasmine.createSpyObj('dropdownList', ['querySelector']);
const dropdownTrigger = {};
const button = {};
const reopenItem = {};
const closeItem = {};
const config = {};
beforeEach(() => {
spyOn(DropLab.prototype, 'init');
dropdownList.querySelector.and.returnValues(reopenItem, closeItem);
closeReopenReportToggle = new CloseReopenReportToggle({
dropdownTrigger,
dropdownList,
button,
});
spyOn(closeReopenReportToggle, 'setConfig').and.returnValue(config);
closeReopenReportToggle.initDroplab();
});
it('sets .reopenItem and .closeItem', () => {
expect(dropdownList.querySelector).toHaveBeenCalledWith('.reopen-item');
expect(dropdownList.querySelector).toHaveBeenCalledWith('.close-item');
expect(closeReopenReportToggle.reopenItem).toBe(reopenItem);
expect(closeReopenReportToggle.closeItem).toBe(closeItem);
});
it('sets .droplab', () => {
expect(closeReopenReportToggle.droplab).toEqual(jasmine.any(Object));
});
it('calls .setConfig', () => {
expect(closeReopenReportToggle.setConfig).toHaveBeenCalled();
});
it('calls droplab.init', () => {
expect(DropLab.prototype.init).toHaveBeenCalledWith(
dropdownTrigger,
dropdownList,
jasmine.any(Array),
config,
);
});
});
describe('updateButton', () => {
let closeReopenReportToggle;
const dropdownList = {};
const dropdownTrigger = {};
const button = jasmine.createSpyObj('button', ['blur']);
const isClosed = true;
beforeEach(() => {
closeReopenReportToggle = new CloseReopenReportToggle({
dropdownTrigger,
dropdownList,
button,
});
spyOn(closeReopenReportToggle, 'toggleButtonType');
closeReopenReportToggle.updateButton(isClosed);
});
it('calls .toggleButtonType', () => {
expect(closeReopenReportToggle.toggleButtonType).toHaveBeenCalledWith(isClosed);
});
it('calls .button.blur', () => {
expect(closeReopenReportToggle.button.blur).toHaveBeenCalled();
});
});
describe('toggleButtonType', () => {
let closeReopenReportToggle;
const dropdownList = {};
const dropdownTrigger = {};
const button = {};
const isClosed = true;
const showItem = jasmine.createSpyObj('showItem', ['click']);
const hideItem = {};
showItem.classList = jasmine.createSpyObj('classList', ['add', 'remove']);
hideItem.classList = jasmine.createSpyObj('classList', ['add', 'remove']);
beforeEach(() => {
closeReopenReportToggle = new CloseReopenReportToggle({
dropdownTrigger,
dropdownList,
button,
});
spyOn(closeReopenReportToggle, 'getButtonTypes').and.returnValue([showItem, hideItem]);
closeReopenReportToggle.toggleButtonType(isClosed);
});
it('calls .getButtonTypes', () => {
expect(closeReopenReportToggle.getButtonTypes).toHaveBeenCalledWith(isClosed);
});
it('removes hide class and add selected class to showItem, opposite for hideItem', () => {
expect(showItem.classList.remove).toHaveBeenCalledWith('hidden');
expect(showItem.classList.add).toHaveBeenCalledWith('droplab-item-selected');
expect(hideItem.classList.add).toHaveBeenCalledWith('hidden');
expect(hideItem.classList.remove).toHaveBeenCalledWith('droplab-item-selected');
});
it('clicks the showItem', () => {
expect(showItem.click).toHaveBeenCalled();
});
});
describe('getButtonTypes', () => {
let closeReopenReportToggle;
const dropdownList = {};
const dropdownTrigger = {};
const button = {};
const reopenItem = {};
const closeItem = {};
beforeEach(() => {
closeReopenReportToggle = new CloseReopenReportToggle({
dropdownTrigger,
dropdownList,
button,
});
closeReopenReportToggle.reopenItem = reopenItem;
closeReopenReportToggle.closeItem = closeItem;
});
it('returns reopenItem, closeItem if isClosed is true', () => {
const buttonTypes = closeReopenReportToggle.getButtonTypes(true);
expect(buttonTypes).toEqual([reopenItem, closeItem]);
});
it('returns closeItem, reopenItem if isClosed is false', () => {
const buttonTypes = closeReopenReportToggle.getButtonTypes(false);
expect(buttonTypes).toEqual([closeItem, reopenItem]);
});
});
describe('setDisable', () => {
let closeReopenReportToggle;
const dropdownList = {};
const dropdownTrigger = jasmine.createSpyObj('button', ['setAttribute', 'removeAttribute']);
const button = jasmine.createSpyObj('button', ['setAttribute', 'removeAttribute']);
beforeEach(() => {
closeReopenReportToggle = new CloseReopenReportToggle({
dropdownTrigger,
dropdownList,
button,
});
});
it('disable .button and .dropdownTrigger if shouldDisable is true', () => {
closeReopenReportToggle.setDisable(true);
expect(button.setAttribute).toHaveBeenCalledWith('disabled', 'true');
expect(dropdownTrigger.setAttribute).toHaveBeenCalledWith('disabled', 'true');
});
it('disable .button and .dropdownTrigger if shouldDisable is undefined', () => {
closeReopenReportToggle.setDisable();
expect(button.setAttribute).toHaveBeenCalledWith('disabled', 'true');
expect(dropdownTrigger.setAttribute).toHaveBeenCalledWith('disabled', 'true');
});
it('enable .button and .dropdownTrigger if shouldDisable is false', () => {
closeReopenReportToggle.setDisable(false);
expect(button.removeAttribute).toHaveBeenCalledWith('disabled');
expect(dropdownTrigger.removeAttribute).toHaveBeenCalledWith('disabled');
});
});
describe('setConfig', () => {
let closeReopenReportToggle;
const dropdownList = {};
const dropdownTrigger = {};
const button = {};
let config;
beforeEach(() => {
closeReopenReportToggle = new CloseReopenReportToggle({
dropdownTrigger,
dropdownList,
button,
});
config = closeReopenReportToggle.setConfig();
});
it('returns a config object', () => {
expect(config).toEqual({
InputSetter: [
{
input: button,
valueAttribute: 'data-text',
inputAttribute: 'data-value',
},
{
input: button,
valueAttribute: 'data-text',
inputAttribute: 'title',
},
{
input: button,
valueAttribute: 'data-button-class',
inputAttribute: 'class',
},
{
input: dropdownTrigger,
valueAttribute: 'data-toggle-class',
inputAttribute: 'class',
},
{
input: button,
valueAttribute: 'data-url',
inputAttribute: 'href',
},
{
input: button,
valueAttribute: 'data-method',
inputAttribute: 'data-method',
},
],
});
});
});
});
/* eslint-disable space-before-function-paren, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */ /* eslint-disable space-before-function-paren, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */
import Issue from '~/issue'; import Issue from '~/issue';
import CloseReopenReportToggle from '~/close_reopen_report_toggle';
import '~/lib/utils/text_utility'; import '~/lib/utils/text_utility';
describe('Issue', function() { describe('Issue', function() {
let $boxClosed, $boxOpen, $btnClose, $btnReopen; let $boxClosed, $boxOpen, $btn;
preloadFixtures('issues/closed-issue.html.raw'); preloadFixtures('issues/closed-issue.html.raw');
preloadFixtures('issues/issue-with-task-list.html.raw'); preloadFixtures('issues/issue-with-task-list.html.raw');
...@@ -20,9 +20,7 @@ describe('Issue', function() { ...@@ -20,9 +20,7 @@ describe('Issue', function() {
function expectIssueState(isIssueOpen) { function expectIssueState(isIssueOpen) {
expectVisibility($boxClosed, !isIssueOpen); expectVisibility($boxClosed, !isIssueOpen);
expectVisibility($boxOpen, isIssueOpen); expectVisibility($boxOpen, isIssueOpen);
expect($btn).toHaveText(isIssueOpen ? 'Close issue' : 'Reopen issue');
expectVisibility($btnClose, isIssueOpen);
expectVisibility($btnReopen, !isIssueOpen);
} }
function expectNewBranchButtonState(isPending, canCreate) { function expectNewBranchButtonState(isPending, canCreate) {
...@@ -57,7 +55,7 @@ describe('Issue', function() { ...@@ -57,7 +55,7 @@ describe('Issue', function() {
} }
} }
function findElements() { function findElements(isIssueInitiallyOpen) {
$boxClosed = $('div.status-box-closed'); $boxClosed = $('div.status-box-closed');
expect($boxClosed).toExist(); expect($boxClosed).toExist();
expect($boxClosed).toHaveText('Closed'); expect($boxClosed).toHaveText('Closed');
...@@ -66,13 +64,9 @@ describe('Issue', function() { ...@@ -66,13 +64,9 @@ describe('Issue', function() {
expect($boxOpen).toExist(); expect($boxOpen).toExist();
expect($boxOpen).toHaveText('Open'); expect($boxOpen).toHaveText('Open');
$btnClose = $('.btn-close.btn-grouped'); $btn = $('.js-issuable-close-button');
expect($btnClose).toExist(); expect($btn).toExist();
expect($btnClose).toHaveText('Close issue'); expect($btn).toHaveText(isIssueInitiallyOpen ? 'Close issue' : 'Reopen issue');
$btnReopen = $('.btn-reopen.btn-grouped');
expect($btnReopen).toExist();
expect($btnReopen).toHaveText('Reopen issue');
} }
describe('task lists', function() { describe('task lists', function() {
...@@ -99,7 +93,6 @@ describe('Issue', function() { ...@@ -99,7 +93,6 @@ describe('Issue', function() {
function ajaxSpy(req) { function ajaxSpy(req) {
if (req.url === this.$triggeredButton.attr('href')) { if (req.url === this.$triggeredButton.attr('href')) {
expect(req.type).toBe('PUT'); expect(req.type).toBe('PUT');
expect(this.$triggeredButton).toHaveProp('disabled', true);
expectNewBranchButtonState(true, false); expectNewBranchButtonState(true, false);
return this.issueStateDeferred; return this.issueStateDeferred;
} else if (req.url === Issue.createMrDropdownWrap.dataset.canCreatePath) { } else if (req.url === Issue.createMrDropdownWrap.dataset.canCreatePath) {
...@@ -119,10 +112,11 @@ describe('Issue', function() { ...@@ -119,10 +112,11 @@ describe('Issue', function() {
loadFixtures('issues/closed-issue.html.raw'); loadFixtures('issues/closed-issue.html.raw');
} }
findElements(); findElements(isIssueInitiallyOpen);
this.issue = new Issue(); this.issue = new Issue();
expectIssueState(isIssueInitiallyOpen); expectIssueState(isIssueInitiallyOpen);
this.$triggeredButton = isIssueInitiallyOpen ? $btnClose : $btnReopen;
this.$triggeredButton = $btn;
this.$projectIssuesCounter = $('.issue_counter'); this.$projectIssuesCounter = $('.issue_counter');
this.$projectIssuesCounter.text('1,001'); this.$projectIssuesCounter.text('1,001');
...@@ -143,7 +137,7 @@ describe('Issue', function() { ...@@ -143,7 +137,7 @@ describe('Issue', function() {
}); });
expectIssueState(!isIssueInitiallyOpen); expectIssueState(!isIssueInitiallyOpen);
expect(this.$triggeredButton).toHaveProp('disabled', false); expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
expect(this.$projectIssuesCounter.text()).toBe(isIssueInitiallyOpen ? '1,000' : '1,002'); expect(this.$projectIssuesCounter.text()).toBe(isIssueInitiallyOpen ? '1,000' : '1,002');
expectNewBranchButtonState(false, !isIssueInitiallyOpen); expectNewBranchButtonState(false, !isIssueInitiallyOpen);
}); });
...@@ -158,7 +152,7 @@ describe('Issue', function() { ...@@ -158,7 +152,7 @@ describe('Issue', function() {
}); });
expectIssueState(isIssueInitiallyOpen); expectIssueState(isIssueInitiallyOpen);
expect(this.$triggeredButton).toHaveProp('disabled', false); expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
expectErrorMessage(); expectErrorMessage();
expect(this.$projectIssuesCounter.text()).toBe('1,001'); expect(this.$projectIssuesCounter.text()).toBe('1,001');
expectNewBranchButtonState(false, isIssueInitiallyOpen); expectNewBranchButtonState(false, isIssueInitiallyOpen);
...@@ -172,7 +166,7 @@ describe('Issue', function() { ...@@ -172,7 +166,7 @@ describe('Issue', function() {
}); });
expectIssueState(isIssueInitiallyOpen); expectIssueState(isIssueInitiallyOpen);
expect(this.$triggeredButton).toHaveProp('disabled', true); expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
expectErrorMessage(); expectErrorMessage();
expect(this.$projectIssuesCounter.text()).toBe('1,001'); expect(this.$projectIssuesCounter.text()).toBe('1,001');
expectNewBranchButtonState(false, isIssueInitiallyOpen); expectNewBranchButtonState(false, isIssueInitiallyOpen);
...@@ -195,4 +189,37 @@ describe('Issue', function() { ...@@ -195,4 +189,37 @@ describe('Issue', function() {
}); });
}); });
}); });
describe('units', () => {
describe('class constructor', () => {
it('calls .initCloseReopenReport', () => {
spyOn(Issue.prototype, 'initCloseReopenReport');
new Issue(); // eslint-disable-line no-new
expect(Issue.prototype.initCloseReopenReport).toHaveBeenCalled();
});
});
describe('initCloseReopenReport', () => {
it('calls .initDroplab', () => {
const container = jasmine.createSpyObj('container', ['querySelector']);
const dropdownTrigger = {};
const dropdownList = {};
const button = {};
spyOn(document, 'querySelector').and.returnValue(container);
spyOn(CloseReopenReportToggle.prototype, 'initDroplab');
container.querySelector.and.returnValues(dropdownTrigger, dropdownList, button);
Issue.prototype.initCloseReopenReport();
expect(document.querySelector).toHaveBeenCalledWith('.js-issuable-close-dropdown');
expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-toggle');
expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-menu');
expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-button');
expect(CloseReopenReportToggle.prototype.initDroplab).toHaveBeenCalled();
});
});
});
}); });
...@@ -2,10 +2,12 @@ ...@@ -2,10 +2,12 @@
/* global MergeRequest */ /* global MergeRequest */
import '~/merge_request'; import '~/merge_request';
import CloseReopenReportToggle from '~/close_reopen_report_toggle';
import IssuablesHelper from '~/helpers/issuables_helper';
(function() { (function() {
describe('MergeRequest', function() { describe('MergeRequest', function() {
return describe('task lists', function() { describe('task lists', function() {
preloadFixtures('merge_requests/merge_request_with_task_list.html.raw'); preloadFixtures('merge_requests/merge_request_with_task_list.html.raw');
beforeEach(function() { beforeEach(function() {
loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
...@@ -27,5 +29,34 @@ import '~/merge_request'; ...@@ -27,5 +29,34 @@ import '~/merge_request';
return $('.js-task-list-field').trigger('tasklist:changed'); return $('.js-task-list-field').trigger('tasklist:changed');
}); });
}); });
describe('class constructor', () => {
it('calls .initCloseReopenReport', () => {
spyOn(IssuablesHelper, 'initCloseReopenReport');
new MergeRequest(); // eslint-disable-line no-new
expect(IssuablesHelper.initCloseReopenReport).toHaveBeenCalled();
});
it('calls .initDroplab', () => {
const container = jasmine.createSpyObj('container', ['querySelector']);
const dropdownTrigger = {};
const dropdownList = {};
const button = {};
spyOn(CloseReopenReportToggle.prototype, 'initDroplab');
spyOn(document, 'querySelector').and.returnValue(container);
container.querySelector.and.returnValues(dropdownTrigger, dropdownList, button);
new MergeRequest(); // eslint-disable-line no-new
expect(document.querySelector).toHaveBeenCalledWith('.js-issuable-close-dropdown');
expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-toggle');
expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-menu');
expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-button');
expect(CloseReopenReportToggle.prototype.initDroplab).toHaveBeenCalled();
});
});
}); });
}).call(window); }).call(window);
...@@ -3,7 +3,7 @@ require 'spec_helper' ...@@ -3,7 +3,7 @@ require 'spec_helper'
describe ExtractsPath, lib: true do describe ExtractsPath, lib: true do
include ExtractsPath include ExtractsPath
include RepoHelpers include RepoHelpers
include Gitlab::Routing.url_helpers include Gitlab::Routing
let(:project) { double('project') } let(:project) { double('project') }
let(:request) { double('request') } let(:request) { double('request') }
......
...@@ -111,7 +111,7 @@ describe Gitlab::Git::Blob, seed_helper: true do ...@@ -111,7 +111,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
end end
end end
describe '.raw' do shared_examples 'finding blobs by ID' do
let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) } let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) }
it { expect(raw_blob.id).to eq(SeedRepo::RubyBlob::ID) } it { expect(raw_blob.id).to eq(SeedRepo::RubyBlob::ID) }
it { expect(raw_blob.data[0..10]).to eq("require \'fi") } it { expect(raw_blob.data[0..10]).to eq("require \'fi") }
...@@ -136,6 +136,16 @@ describe Gitlab::Git::Blob, seed_helper: true do ...@@ -136,6 +136,16 @@ describe Gitlab::Git::Blob, seed_helper: true do
end end
end end
describe '.raw' do
context 'when the blob_raw Gitaly feature is enabled' do
it_behaves_like 'finding blobs by ID'
end
context 'when the blob_raw Gitaly feature is disabled', skip_gitaly_mock: true do
it_behaves_like 'finding blobs by ID'
end
end
describe 'encoding' do describe 'encoding' do
context 'file with russian text' do context 'file with russian text' do
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/russian.rb") } let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/russian.rb") }
......
require 'spec_helper'
describe Gitlab::PerformanceBar do
shared_examples 'allowed user IDs are cached' do
before do
# Warm the Redis cache
described_class.enabled?(user)
end
it 'caches the allowed user IDs in cache', :caching do
expect do
expect(described_class.enabled?(user)).to be_truthy
end.not_to exceed_query_limit(0)
end
end
describe '.enabled?' do
let(:user) { create(:user) }
before do
stub_application_setting(performance_bar_allowed_group_id: -1)
end
it 'returns false when given user is nil' do
expect(described_class.enabled?(nil)).to be_falsy
end
it 'returns false when allowed_group_id is nil' do
expect(described_class).to receive(:allowed_group_id).and_return(nil)
expect(described_class.enabled?(user)).to be_falsy
end
context 'when allowed group ID does not exist' do
it 'returns false' do
expect(described_class.enabled?(user)).to be_falsy
end
end
context 'when allowed group exists' do
let!(:my_group) { create(:group, path: 'my-group') }
before do
stub_application_setting(performance_bar_allowed_group_id: my_group.id)
end
context 'when user is not a member of the allowed group' do
it 'returns false' do
expect(described_class.enabled?(user)).to be_falsy
end
it_behaves_like 'allowed user IDs are cached'
end
context 'when user is a member of the allowed group' do
before do
my_group.add_developer(user)
end
it 'returns true' do
expect(described_class.enabled?(user)).to be_truthy
end
it_behaves_like 'allowed user IDs are cached'
end
end
context 'when allowed group is nested', :nested_groups do
let!(:nested_my_group) { create(:group, parent: create(:group, path: 'my-org'), path: 'my-group') }
before do
create(:group, path: 'my-group')
nested_my_group.add_developer(user)
stub_application_setting(performance_bar_allowed_group_id: nested_my_group.id)
end
it 'returns the nested group' do
expect(described_class.enabled?(user)).to be_truthy
end
end
context 'when a nested group has the same path', :nested_groups do
before do
create(:group, :nested, path: 'my-group').add_developer(user)
end
it 'returns false' do
expect(described_class.enabled?(user)).to be_falsy
end
end
end
end
...@@ -214,6 +214,160 @@ describe ApplicationSetting, models: true do ...@@ -214,6 +214,160 @@ describe ApplicationSetting, models: true do
end end
end end
describe 'performance bar settings' do
describe 'performance_bar_allowed_group_id=' do
context 'with a blank path' do
before do
setting.performance_bar_allowed_group_id = create(:group).full_path
end
it 'persists nil for a "" path and clears allowed user IDs cache' do
expect(Gitlab::PerformanceBar).to receive(:expire_allowed_user_ids_cache)
setting.performance_bar_allowed_group_id = ''
expect(setting.performance_bar_allowed_group_id).to be_nil
end
end
context 'with an invalid path' do
it 'does not persist an invalid group path' do
setting.performance_bar_allowed_group_id = 'foo'
expect(setting.performance_bar_allowed_group_id).to be_nil
end
end
context 'with a path to an existing group' do
let(:group) { create(:group) }
it 'persists a valid group path and clears allowed user IDs cache' do
expect(Gitlab::PerformanceBar).to receive(:expire_allowed_user_ids_cache)
setting.performance_bar_allowed_group_id = group.full_path
expect(setting.performance_bar_allowed_group_id).to eq(group.id)
end
context 'when the given path is the same' do
context 'with a blank path' do
before do
setting.performance_bar_allowed_group_id = nil
end
it 'clears the cached allowed user IDs' do
expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache)
setting.performance_bar_allowed_group_id = ''
end
end
context 'with a valid path' do
before do
setting.performance_bar_allowed_group_id = group.full_path
end
it 'clears the cached allowed user IDs' do
expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache)
setting.performance_bar_allowed_group_id = group.full_path
end
end
end
end
end
describe 'performance_bar_allowed_group' do
context 'with no performance_bar_allowed_group_id saved' do
it 'returns nil' do
expect(setting.performance_bar_allowed_group).to be_nil
end
end
context 'with a performance_bar_allowed_group_id saved' do
let(:group) { create(:group) }
before do
setting.performance_bar_allowed_group_id = group.full_path
end
it 'returns the group' do
expect(setting.performance_bar_allowed_group).to eq(group)
end
end
end
describe 'performance_bar_enabled' do
context 'with the Performance Bar is enabled' do
let(:group) { create(:group) }
before do
setting.performance_bar_allowed_group_id = group.full_path
end
it 'returns true' do
expect(setting.performance_bar_enabled).to be_truthy
end
end
end
describe 'performance_bar_enabled=' do
context 'when the performance bar is enabled' do
let(:group) { create(:group) }
before do
setting.performance_bar_allowed_group_id = group.full_path
end
context 'when passing true' do
it 'does not clear allowed user IDs cache' do
expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache)
setting.performance_bar_enabled = true
expect(setting.performance_bar_allowed_group_id).to eq(group.id)
expect(setting.performance_bar_enabled).to be_truthy
end
end
context 'when passing false' do
it 'disables the performance bar and clears allowed user IDs cache' do
expect(Gitlab::PerformanceBar).to receive(:expire_allowed_user_ids_cache)
setting.performance_bar_enabled = false
expect(setting.performance_bar_allowed_group_id).to be_nil
expect(setting.performance_bar_enabled).to be_falsey
end
end
end
context 'when the performance bar is disabled' do
context 'when passing true' do
it 'does nothing and does not clear allowed user IDs cache' do
expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache)
setting.performance_bar_enabled = true
expect(setting.performance_bar_allowed_group_id).to be_nil
expect(setting.performance_bar_enabled).to be_falsey
end
end
context 'when passing false' do
it 'does nothing and does not clear allowed user IDs cache' do
expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache)
setting.performance_bar_enabled = false
expect(setting.performance_bar_allowed_group_id).to be_nil
expect(setting.performance_bar_enabled).to be_falsey
end
end
end
end
end
describe 'usage ping settings' do describe 'usage ping settings' do
context 'when the usage ping is disabled in gitlab.yml' do context 'when the usage ping is disabled in gitlab.yml' do
before do before do
......
require 'spec_helper'
describe EachBatch do
describe '.each_batch' do
let(:model) do
Class.new(ActiveRecord::Base) do
include EachBatch
self.table_name = 'users'
end
end
before do
5.times { create(:user, updated_at: 1.day.ago) }
end
it 'yields an ActiveRecord::Relation when a block is given' do
model.each_batch do |relation|
expect(relation).to be_a_kind_of(ActiveRecord::Relation)
end
end
it 'yields a batch index as the second argument' do
model.each_batch do |_, index|
expect(index).to eq(1)
end
end
it 'accepts a custom batch size' do
amount = 0
model.each_batch(of: 1) { amount += 1 }
expect(amount).to eq(5)
end
it 'does not include ORDER BYs in the yielded relations' do
model.each_batch do |relation|
expect(relation.to_sql).not_to include('ORDER BY')
end
end
it 'allows updating of the yielded relations' do
time = Time.now
model.each_batch do |relation|
relation.update_all(updated_at: time)
end
expect(model.where(updated_at: time).count).to eq(5)
end
end
end
...@@ -13,7 +13,7 @@ describe ShaAttribute do ...@@ -13,7 +13,7 @@ describe ShaAttribute do
end end
describe '#sha_attribute' do describe '#sha_attribute' do
context' when the table exists' do context 'when the table exists' do
before do before do
allow(model).to receive(:table_exists?).and_return(true) allow(model).to receive(:table_exists?).and_return(true)
end end
...@@ -30,7 +30,7 @@ describe ShaAttribute do ...@@ -30,7 +30,7 @@ describe ShaAttribute do
end end
end end
context' when the table does not exist' do context 'when the table does not exist' do
before do before do
allow(model).to receive(:table_exists?).and_return(false) allow(model).to receive(:table_exists?).and_return(false)
end end
......
...@@ -466,13 +466,13 @@ describe Group, models: true do ...@@ -466,13 +466,13 @@ describe Group, models: true do
it_behaves_like 'ref is protected' it_behaves_like 'ref is protected'
end end
context 'when group has children' do context 'when group has children', :postgresql do
let!(:group_child) { create(:group, parent: group) } let(:group_child) { create(:group, parent: group) }
let!(:variable_child) { create(:ci_group_variable, group: group_child) } let(:group_child_2) { create(:group, parent: group_child) }
let!(:group_child_3) { create(:group, parent: group_child_2) } let(:group_child_3) { create(:group, parent: group_child_2) }
let!(:variable_child_3) { create(:ci_group_variable, group: group_child_3) } let(:variable_child) { create(:ci_group_variable, group: group_child) }
let!(:group_child_2) { create(:group, parent: group_child) } let(:variable_child_2) { create(:ci_group_variable, group: group_child_2) }
let!(:variable_child_2) { create(:ci_group_variable, group: group_child_2) } let(:variable_child_3) { create(:ci_group_variable, group: group_child_3) }
it 'returns all variables belong to the group and parent groups' do it 'returns all variables belong to the group and parent groups' do
expected_array1 = [protected_variable, secret_variable] expected_array1 = [protected_variable, secret_variable]
......
require 'spec_helper' require 'spec_helper'
describe JiraService, models: true do describe JiraService, models: true do
include Gitlab::Routing.url_helpers include Gitlab::Routing
describe "Associations" do describe "Associations" do
it { is_expected.to belong_to :project } it { is_expected.to belong_to :project }
......
...@@ -113,6 +113,20 @@ describe API::Features do ...@@ -113,6 +113,20 @@ describe API::Features do
{ 'key' => 'actors', 'value' => ["User:#{user.id}"] } { 'key' => 'actors', 'value' => ["User:#{user.id}"] }
]) ])
end end
it 'creates an enabled feature for the given user and feature group when passed user=username and feature_group=perf_team' do
post api("/features/#{feature_name}", admin), value: 'true', user: user.username, feature_group: 'perf_team'
expect(response).to have_http_status(201)
expect(json_response).to eq(
'name' => 'my_feature',
'state' => 'conditional',
'gates' => [
{ 'key' => 'boolean', 'value' => false },
{ 'key' => 'groups', 'value' => ['perf_team'] },
{ 'key' => 'actors', 'value' => ["User:#{user.id}"] }
])
end
end end
it 'creates a feature with the given percentage if passed an integer' do it 'creates a feature with the given percentage if passed an integer' do
......
...@@ -1462,6 +1462,25 @@ describe API::Issues do ...@@ -1462,6 +1462,25 @@ describe API::Issues do
end end
end end
describe "GET /projects/:id/issues/:issue_iid/user_agent_detail" do
let!(:user_agent_detail) { create(:user_agent_detail, subject: issue) }
it 'exposes known attributes' do
get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", admin)
expect(response).to have_http_status(200)
expect(json_response['user_agent']).to eq(user_agent_detail.user_agent)
expect(json_response['ip_address']).to eq(user_agent_detail.ip_address)
expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted)
end
it "returns unautorized for non-admin users" do
get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", user)
expect(response).to have_http_status(403)
end
end
def expect_paginated_array_response(size: nil) def expect_paginated_array_response(size: nil)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response).to include_pagination_headers expect(response).to include_pagination_headers
......
...@@ -5,6 +5,26 @@ describe API::ProjectSnippets do ...@@ -5,6 +5,26 @@ describe API::ProjectSnippets do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
describe "GET /projects/:project_id/snippets/:id/user_agent_detail" do
let(:snippet) { create(:project_snippet, :public, project: project) }
let!(:user_agent_detail) { create(:user_agent_detail, subject: snippet) }
it 'exposes known attributes' do
get api("/projects/#{project.id}/snippets/#{snippet.id}/user_agent_detail", admin)
expect(response).to have_http_status(200)
expect(json_response['user_agent']).to eq(user_agent_detail.user_agent)
expect(json_response['ip_address']).to eq(user_agent_detail.ip_address)
expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted)
end
it "returns unautorized for non-admin users" do
get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/user_agent_detail", user)
expect(response).to have_http_status(403)
end
end
describe 'GET /projects/:project_id/snippets/' do describe 'GET /projects/:project_id/snippets/' do
let(:user) { create(:user) } let(:user) { create(:user) }
...@@ -20,7 +40,7 @@ describe API::ProjectSnippets do ...@@ -20,7 +40,7 @@ describe API::ProjectSnippets do
expect(response).to include_pagination_headers expect(response).to include_pagination_headers
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(json_response.size).to eq(3) expect(json_response.size).to eq(3)
expect(json_response.map{ |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id) expect(json_response.map { |snippet| snippet['id'] }).to include(public_snippet.id, internal_snippet.id, private_snippet.id)
expect(json_response.last).to have_key('web_url') expect(json_response.last).to have_key('web_url')
end end
......
...@@ -271,4 +271,25 @@ describe API::Snippets do ...@@ -271,4 +271,25 @@ describe API::Snippets do
expect(json_response['message']).to eq('404 Snippet Not Found') expect(json_response['message']).to eq('404 Snippet Not Found')
end end
end end
describe "GET /snippets/:id/user_agent_detail" do
let(:admin) { create(:admin) }
let(:snippet) { create(:personal_snippet, :public, author: user) }
let!(:user_agent_detail) { create(:user_agent_detail, subject: snippet) }
it 'exposes known attributes' do
get api("/snippets/#{snippet.id}/user_agent_detail", admin)
expect(response).to have_http_status(200)
expect(json_response['user_agent']).to eq(user_agent_detail.user_agent)
expect(json_response['ip_address']).to eq(user_agent_detail.ip_address)
expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted)
end
it "returns unautorized for non-admin users" do
get api("/snippets/#{snippet.id}/user_agent_detail", user)
expect(response).to have_http_status(403)
end
end
end end
require 'spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
require_relative '../../../rubocop/cop/in_batches'
describe RuboCop::Cop::InBatches do
include CopHelper
subject(:cop) { described_class.new }
it 'registers an offense when in_batches is used' do
inspect_source(cop, 'foo.in_batches do; end')
aggregate_failures do
expect(cop.offenses.size).to eq(1)
expect(cop.offenses.map(&:line)).to eq([1])
end
end
end
...@@ -98,6 +98,7 @@ describe MergeRequests::RefreshService, services: true do ...@@ -98,6 +98,7 @@ describe MergeRequests::RefreshService, services: true do
end end
context 'push to origin repo target branch' do context 'push to origin repo target branch' do
context 'when all MRs to the target branch had diffs' do
before do before do
service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature') service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature')
reload_mrs reload_mrs
...@@ -113,6 +114,39 @@ describe MergeRequests::RefreshService, services: true do ...@@ -113,6 +114,39 @@ describe MergeRequests::RefreshService, services: true do
end end
end end
context 'when an MR to be closed was empty already' do
let!(:empty_fork_merge_request) do
create(:merge_request,
source_project: @fork_project,
source_branch: 'master',
target_branch: 'master',
target_project: @project)
end
before do
# This spec already has a fake push, so pretend that we were targeting
# feature all along.
empty_fork_merge_request.update_columns(target_branch: 'feature')
service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature')
reload_mrs
empty_fork_merge_request.reload
end
it 'only updates the non-empty MRs' do
expect(@merge_request).to be_merged
expect(@merge_request.notes.last.note).to include('merged')
expect(@fork_merge_request).to be_merged
expect(@fork_merge_request.notes.last.note).to include('merged')
expect(empty_fork_merge_request).to be_open
expect(empty_fork_merge_request.merge_request_diff.state).to eq('empty')
expect(empty_fork_merge_request.notes).to be_empty
end
end
end
context 'manual merge of source branch' do context 'manual merge of source branch' do
before do before do
# Merge master -> feature branch # Merge master -> feature branch
......
require 'spec_helper' require 'spec_helper'
describe SystemNoteService, services: true do describe SystemNoteService, services: true do
include Gitlab::Routing.url_helpers include Gitlab::Routing
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
let(:author) { create(:user) } let(:author) { create(:user) }
......
...@@ -57,7 +57,7 @@ RSpec.configure do |config| ...@@ -57,7 +57,7 @@ RSpec.configure do |config|
config.include StubGitlabCalls config.include StubGitlabCalls
config.include StubGitlabData config.include StubGitlabData
config.include ApiHelpers, :api config.include ApiHelpers, :api
config.include Gitlab::Routing.url_helpers, type: :routing config.include Gitlab::Routing, type: :routing
config.include MigrationsHelpers, :migration config.include MigrationsHelpers, :migration
config.infer_spec_type_from_file_location! config.infer_spec_type_from_file_location!
......
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