Commit 50fd7e13 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'rc/ce-to-ee-friday' into 'master'

CE Upstream - Friday

Closes gitlab-ce#26595

See merge request !1553
parents adbb6830 abde7fd7
...@@ -26,6 +26,12 @@ export default { ...@@ -26,6 +26,12 @@ export default {
}; };
}, },
computed: {
title() {
return 'Deploy to...';
},
},
methods: { methods: {
onClickAction(endpoint) { onClickAction(endpoint) {
this.isLoading = true; this.isLoading = true;
...@@ -45,8 +51,11 @@ export default { ...@@ -45,8 +51,11 @@ export default {
template: ` template: `
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button <button
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container" class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip"
data-container="body"
data-toggle="dropdown" data-toggle="dropdown"
:title="title"
:aria-label="title"
:disabled="isLoading"> :disabled="isLoading">
<span> <span>
<span v-html="playIconSvg"></span> <span v-html="playIconSvg"></span>
......
...@@ -9,13 +9,21 @@ export default { ...@@ -9,13 +9,21 @@ export default {
}, },
}, },
computed: {
title() {
return 'Open';
},
},
template: ` template: `
<a <a
class="btn external_url" class="btn external-url has-tooltip"
data-container="body"
:href="externalUrl" :href="externalUrl"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer nofollow"
title="Environment external URL"> :title="title"
:aria-label="title">
<i class="fa fa-external-link" aria-hidden="true"></i> <i class="fa fa-external-link" aria-hidden="true"></i>
</a> </a>
`, `,
......
...@@ -11,6 +11,7 @@ import ExternalUrlComponent from './environment_external_url'; ...@@ -11,6 +11,7 @@ import ExternalUrlComponent from './environment_external_url';
import StopComponent from './environment_stop'; import StopComponent from './environment_stop';
import RollbackComponent from './environment_rollback'; import RollbackComponent from './environment_rollback';
import TerminalButtonComponent from './environment_terminal_button'; import TerminalButtonComponent from './environment_terminal_button';
import MonitoringButtonComponent from './environment_monitoring';
import CommitComponent from '../../vue_shared/components/commit'; import CommitComponent from '../../vue_shared/components/commit';
const timeagoInstance = new Timeago(); const timeagoInstance = new Timeago();
...@@ -23,6 +24,7 @@ export default { ...@@ -23,6 +24,7 @@ export default {
'stop-component': StopComponent, 'stop-component': StopComponent,
'rollback-component': RollbackComponent, 'rollback-component': RollbackComponent,
'terminal-button-component': TerminalButtonComponent, 'terminal-button-component': TerminalButtonComponent,
'monitoring-button-component': MonitoringButtonComponent,
}, },
props: { props: {
...@@ -399,6 +401,14 @@ export default { ...@@ -399,6 +401,14 @@ export default {
return ''; return '';
}, },
monitoringUrl() {
if (this.model && this.model.metrics_path) {
return this.model.metrics_path;
}
return '';
},
/** /**
* Constructs folder URL based on the current location and the folder id. * Constructs folder URL based on the current location and the folder id.
* *
...@@ -518,13 +528,16 @@ export default { ...@@ -518,13 +528,16 @@ export default {
<external-url-component v-if="externalURL && canReadEnvironment" <external-url-component v-if="externalURL && canReadEnvironment"
:external-url="externalURL"/> :external-url="externalURL"/>
<stop-component v-if="hasStopAction && canCreateDeployment" <monitoring-button-component v-if="monitoringUrl && canReadEnvironment"
:stop-url="model.stop_path" :monitoring-url="monitoringUrl"/>
:service="service"/>
<terminal-button-component v-if="model && model.terminal_path" <terminal-button-component v-if="model && model.terminal_path"
:terminal-path="model.terminal_path"/> :terminal-path="model.terminal_path"/>
<stop-component v-if="hasStopAction && canCreateDeployment"
:stop-url="model.stop_path"
:service="service"/>
<rollback-component v-if="canRetry && canCreateDeployment" <rollback-component v-if="canRetry && canCreateDeployment"
:is-last-deployment="isLastDeployment" :is-last-deployment="isLastDeployment"
:retry-url="retryUrl" :retry-url="retryUrl"
......
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
export default {
props: {
monitoringUrl: {
type: String,
default: '',
required: true,
},
},
computed: {
title() {
return 'Monitoring';
},
},
template: `
<a
class="btn monitoring-url has-tooltip"
data-container="body"
:href="monitoringUrl"
target="_blank"
rel="noopener noreferrer nofollow"
:title="title"
:aria-label="title">
<i class="fa fa-area-chart" aria-hidden="true"></i>
</a>
`,
};
...@@ -26,6 +26,12 @@ export default { ...@@ -26,6 +26,12 @@ export default {
}; };
}, },
computed: {
title() {
return 'Stop';
},
},
methods: { methods: {
onClick() { onClick() {
if (confirm('Are you sure you want to stop this environment?')) { if (confirm('Are you sure you want to stop this environment?')) {
...@@ -46,10 +52,12 @@ export default { ...@@ -46,10 +52,12 @@ export default {
template: ` template: `
<button type="button" <button type="button"
class="btn stop-env-link" class="btn stop-env-link has-tooltip"
data-container="body"
@click="onClick" @click="onClick"
:disabled="isLoading" :disabled="isLoading"
title="Stop Environment"> :title="title"
:aria-label="title">
<i class="fa fa-stop stop-env-icon" aria-hidden="true"></i> <i class="fa fa-stop stop-env-icon" aria-hidden="true"></i>
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i> <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</button> </button>
......
...@@ -14,12 +14,22 @@ export default { ...@@ -14,12 +14,22 @@ export default {
}, },
data() { data() {
return { terminalIconSvg }; return {
terminalIconSvg,
};
},
computed: {
title() {
return 'Terminal';
},
}, },
template: ` template: `
<a class="btn terminal-button" <a class="btn terminal-button has-tooltip"
title="Open web terminal" data-container="body"
:title="title"
:aria-label="title"
:href="terminalPath"> :href="terminalPath">
${terminalIconSvg} ${terminalIconSvg}
</a> </a>
......
...@@ -6,23 +6,60 @@ var slice = [].slice; ...@@ -6,23 +6,60 @@ var slice = [].slice;
window.GroupsSelect = (function() { window.GroupsSelect = (function() {
function GroupsSelect() { function GroupsSelect() {
$('.ajax-groups-select').each((function(_this) { $('.ajax-groups-select').each((function(_this) {
const self = _this;
return function(i, select) { return function(i, select) {
var all_available, skip_groups; var all_available, skip_groups;
all_available = $(select).data('all-available'); const $select = $(select);
skip_groups = $(select).data('skip-groups') || []; all_available = $select.data('all-available');
return $(select).select2({ skip_groups = $select.data('skip-groups') || [];
$select.select2({
placeholder: "Search for a group", placeholder: "Search for a group",
multiple: $(select).hasClass('multiselect'), multiple: $select.hasClass('multiselect'),
minimumInputLength: 0, minimumInputLength: 0,
query: function(query) { ajax: {
var options = { all_available: all_available, skip_groups: skip_groups }; url: Api.buildUrl(Api.groupsPath),
return Api.groups(query.term, options, function(groups) { dataType: 'json',
var data; quietMillis: 250,
data = { transport: function (params) {
results: groups $.ajax(params).then((data, status, xhr) => {
const results = data || [];
const headers = gl.utils.normalizeCRLFHeaders(xhr.getAllResponseHeaders());
const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
const more = currentPage < totalPages;
return {
results,
pagination: {
more,
},
};
}).then(params.success).fail(params.error);
},
data: function (search, page) {
return {
search,
page,
per_page: GroupsSelect.PER_PAGE,
all_available,
skip_groups,
};
},
results: function (data, page) {
if (data.length) return { results: [] };
const results = data.length ? data : data.results || [];
const more = data.pagination ? data.pagination.more : false;
return {
results,
page,
more,
}; };
return query.callback(data); },
});
}, },
initSelection: function(element, callback) { initSelection: function(element, callback) {
var id; var id;
...@@ -34,19 +71,23 @@ window.GroupsSelect = (function() { ...@@ -34,19 +71,23 @@ window.GroupsSelect = (function() {
formatResult: function() { formatResult: function() {
var args; var args;
args = 1 <= arguments.length ? slice.call(arguments, 0) : []; args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
return _this.formatResult.apply(_this, args); return self.formatResult.apply(self, args);
}, },
formatSelection: function() { formatSelection: function() {
var args; var args;
args = 1 <= arguments.length ? slice.call(arguments, 0) : []; args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
return _this.formatSelection.apply(_this, args); return self.formatSelection.apply(self, args);
}, },
dropdownCssClass: "ajax-groups-dropdown", dropdownCssClass: "ajax-groups-dropdown select2-infinite",
// we do not want to escape markup since we are displaying html in results // we do not want to escape markup since we are displaying html in results
escapeMarkup: function(m) { escapeMarkup: function(m) {
return m; return m;
} }
}); });
self.dropdown = document.querySelector('.select2-infinite .select2-results');
$select.on('select2-loaded', self.forceOverflow.bind(self));
}; };
})(this)); })(this));
} }
...@@ -65,5 +106,12 @@ window.GroupsSelect = (function() { ...@@ -65,5 +106,12 @@ window.GroupsSelect = (function() {
return group.full_name; return group.full_name;
}; };
GroupsSelect.prototype.forceOverflow = function (e) {
const itemHeight = this.dropdown.querySelector('.select2-result:first-child').clientHeight;
this.dropdown.style.height = `${Math.floor(this.dropdown.scrollHeight - (itemHeight * 0.9))}px`;
};
GroupsSelect.PER_PAGE = 20;
return GroupsSelect; return GroupsSelect;
})(); })();
...@@ -231,6 +231,22 @@ ...@@ -231,6 +231,22 @@
return upperCaseHeaders; return upperCaseHeaders;
}; };
/**
this will take in the getAllResponseHeaders result and normalize them
this way we don't run into production issues when nginx gives us lowercased header keys
*/
w.gl.utils.normalizeCRLFHeaders = (headers) => {
const headersObject = {};
const headersArray = headers.split('\n');
headersArray.forEach((header) => {
const keyValue = header.split(': ');
headersObject[keyValue[0]] = keyValue[1];
});
return w.gl.utils.normalizeHeaders(headersObject);
};
/** /**
* Parses pagination object string values into numbers. * Parses pagination object string values into numbers.
* *
......
...@@ -209,7 +209,7 @@ import Cookies from 'js-cookie'; ...@@ -209,7 +209,7 @@ import Cookies from 'js-cookie';
}; };
Sidebar.prototype.setSidebarHeight = function() { Sidebar.prototype.setSidebarHeight = function() {
const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight(); const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + $('.sub-nav-scroll').outerHeight();
const $rightSidebar = $('.js-right-sidebar'); const $rightSidebar = $('.js-right-sidebar');
const diff = $navHeight - $(window).scrollTop(); const diff = $navHeight - $(window).scrollTop();
if (diff > 0) { if (diff > 0) {
......
...@@ -83,6 +83,7 @@ export default { ...@@ -83,6 +83,7 @@ export default {
:class="buttonClass" :class="buttonClass"
:title="title" :title="title"
:aria-label="title" :aria-label="title"
data-container="body"
data-placement="top" data-placement="top"
:disabled="isLoading"> :disabled="isLoading">
<i :class="iconClass" aria-hidden="true"/> <i :class="iconClass" aria-hidden="true"/>
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
.table.ci-table { .table.ci-table {
.environments-actions { .environments-actions {
min-width: 200px; min-width: 300px;
} }
.environments-commit, .environments-commit,
...@@ -358,3 +358,12 @@ ...@@ -358,3 +358,12 @@
stroke: $black; stroke: $black;
stroke-width: 1; stroke-width: 1;
} }
.environments-actions {
.external-url,
.monitoring-url,
.terminal-button,
.stop-env-link {
width: 38px;
}
}
class Admin::BackgroundJobsController < Admin::ApplicationController class Admin::BackgroundJobsController < Admin::ApplicationController
def show def show
ps_output, _ = Gitlab::Popen.popen(%W(ps ww -U #{Gitlab.config.gitlab.user} -o pid,pcpu,pmem,stat,start,command)) ps_output, _ = Gitlab::Popen.popen(%W(ps ww -U #{Gitlab.config.gitlab.user} -o pid,pcpu,pmem,stat,start,command))
@sidekiq_processes = ps_output.split("\n").grep(/sidekiq/) @sidekiq_processes = ps_output.split("\n").grep(/sidekiq \d+\.\d+\.\d+/)
@concurrency = Sidekiq.options[:concurrency] @concurrency = Sidekiq.options[:concurrency]
end end
end end
...@@ -11,7 +11,7 @@ class Import::BaseController < ApplicationController ...@@ -11,7 +11,7 @@ class Import::BaseController < ApplicationController
namespace.add_owner(current_user) namespace.add_owner(current_user)
namespace namespace
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
Namespace.find_by_path_or_name(name) Namespace.find_by_full_path(name)
end end
end end
end end
...@@ -255,7 +255,7 @@ module IssuablesHelper ...@@ -255,7 +255,7 @@ module IssuablesHelper
end end
def selected_template(issuable) def selected_template(issuable)
params[:issuable_template] if issuable_templates(issuable).include?(params[:issuable_template]) params[:issuable_template] if issuable_templates(issuable).any?{ |template| template[:name] == params[:issuable_template] }
end end
def issuable_todo_button_data(issuable, todo, is_collapsed) def issuable_todo_button_data(issuable, todo, is_collapsed)
......
...@@ -3,9 +3,9 @@ module SidekiqHelper ...@@ -3,9 +3,9 @@ module SidekiqHelper
(?<pid>\d+)\s+ (?<pid>\d+)\s+
(?<cpu>[\d\.,]+)\s+ (?<cpu>[\d\.,]+)\s+
(?<mem>[\d\.,]+)\s+ (?<mem>[\d\.,]+)\s+
(?<state>[DRSTWXZNLsl\+<]+)\s+ (?<state>[DIEKNRSTVWXZNLpsl\+<>\/\d]+)\s+
(?<start>.+)\s+ (?<start>.+?)\s+
(?<command>sidekiq.*\]) (?<command>(?:ruby\d+:\s+)?sidekiq.*\].*)
\z/x \z/x
def parse_sidekiq_ps(line) def parse_sidekiq_ps(line)
......
...@@ -47,7 +47,7 @@ class Blob < SimpleDelegator ...@@ -47,7 +47,7 @@ class Blob < SimpleDelegator
end end
def ipython_notebook? def ipython_notebook?
text? && language && language.name == 'Jupyter Notebook' text? && language&.name == 'Jupyter Notebook'
end end
def size_within_svg_limits? def size_within_svg_limits?
......
...@@ -62,7 +62,7 @@ class JiraService < IssueTrackerService ...@@ -62,7 +62,7 @@ class JiraService < IssueTrackerService
def help def help
"You need to configure JIRA before enabling this service. For more details "You need to configure JIRA before enabling this service. For more details
read the read the
[JIRA service documentation](#{help_page_url('project_services/jira')})." [JIRA service documentation](#{help_page_url('user/project/integrations/jira')})."
end end
def title def title
......
...@@ -171,6 +171,9 @@ class ProjectTeam ...@@ -171,6 +171,9 @@ class ProjectTeam
# Lookup only the IDs we need # Lookup only the IDs we need
user_ids = user_ids - access.keys user_ids = user_ids - access.keys
return access if user_ids.empty?
users_access = project.project_authorizations. users_access = project.project_authorizations.
where(user: user_ids). where(user: user_ids).
group(:user_id). group(:user_id).
......
...@@ -660,8 +660,10 @@ class User < ActiveRecord::Base ...@@ -660,8 +660,10 @@ class User < ActiveRecord::Base
end end
def fork_of(project) def fork_of(project)
links = ForkedProjectLink.where(forked_from_project_id: project, forked_to_project_id: personal_projects) links = ForkedProjectLink.where(
forked_from_project_id: project,
forked_to_project_id: personal_projects.unscope(:order)
)
if links.any? if links.any?
links.first.forked_to_project links.first.forked_to_project
else else
......
...@@ -9,6 +9,13 @@ class EnvironmentEntity < Grape::Entity ...@@ -9,6 +9,13 @@ class EnvironmentEntity < Grape::Entity
expose :last_deployment, using: DeploymentEntity expose :last_deployment, using: DeploymentEntity
expose :stop_action? expose :stop_action?
expose :metrics_path, if: -> (environment, _) { environment.has_metrics? } do |environment|
metrics_namespace_project_environment_path(
environment.project.namespace,
environment.project,
environment)
end
expose :environment_path do |environment| expose :environment_path do |environment|
namespace_project_environment_path( namespace_project_environment_path(
environment.project.namespace, environment.project.namespace,
......
...@@ -19,7 +19,7 @@ module Issues ...@@ -19,7 +19,7 @@ module Issues
if issue.previous_changes.include?('title') || if issue.previous_changes.include?('title') ||
issue.previous_changes.include?('description') issue.previous_changes.include?('description')
todo_service.update_issue(issue, current_user) todo_service.update_issue(issue, current_user, old_mentioned_users)
end end
if issue.previous_changes.include?('milestone_id') if issue.previous_changes.include?('milestone_id')
......
...@@ -38,7 +38,7 @@ module MergeRequests ...@@ -38,7 +38,7 @@ module MergeRequests
if merge_request.previous_changes.include?('title') || if merge_request.previous_changes.include?('title') ||
merge_request.previous_changes.include?('description') merge_request.previous_changes.include?('description')
todo_service.update_merge_request(merge_request, current_user) todo_service.update_merge_request(merge_request, current_user, old_mentioned_users)
end end
if merge_request.previous_changes.include?('target_branch') if merge_request.previous_changes.include?('target_branch')
......
...@@ -3,11 +3,13 @@ module Notes ...@@ -3,11 +3,13 @@ module Notes
def execute(note) def execute(note)
return note unless note.editable? return note unless note.editable?
old_mentioned_users = note.mentioned_users.to_a
note.update_attributes(params.merge(updated_by: current_user)) note.update_attributes(params.merge(updated_by: current_user))
note.create_new_cross_references!(current_user) note.create_new_cross_references!(current_user)
if note.previous_changes.include?('note') if note.previous_changes.include?('note')
TodoService.new.update_note(note, current_user) TodoService.new.update_note(note, current_user, old_mentioned_users)
end end
note note
......
...@@ -19,8 +19,8 @@ class TodoService ...@@ -19,8 +19,8 @@ class TodoService
# #
# * mark all pending todos related to the issue for the current user as done # * mark all pending todos related to the issue for the current user as done
# #
def update_issue(issue, current_user) def update_issue(issue, current_user, skip_users = [])
update_issuable(issue, current_user) update_issuable(issue, current_user, skip_users)
end end
# When close an issue we should: # When close an issue we should:
...@@ -60,8 +60,8 @@ class TodoService ...@@ -60,8 +60,8 @@ class TodoService
# #
# * create a todo for each mentioned user on merge request # * create a todo for each mentioned user on merge request
# #
def update_merge_request(merge_request, current_user) def update_merge_request(merge_request, current_user, skip_users = [])
update_issuable(merge_request, current_user) update_issuable(merge_request, current_user, skip_users)
end end
# When close a merge request we should: # When close a merge request we should:
...@@ -154,8 +154,8 @@ class TodoService ...@@ -154,8 +154,8 @@ class TodoService
# * mark all pending todos related to the noteable for the current user as done # * mark all pending todos related to the noteable for the current user as done
# * create a todo for each new user mentioned on note # * create a todo for each new user mentioned on note
# #
def update_note(note, current_user) def update_note(note, current_user, skip_users = [])
handle_note(note, current_user) handle_note(note, current_user, skip_users)
end end
# When an emoji is awarded we should: # When an emoji is awarded we should:
...@@ -236,11 +236,11 @@ class TodoService ...@@ -236,11 +236,11 @@ class TodoService
create_mention_todos(issuable.project, issuable, author) create_mention_todos(issuable.project, issuable, author)
end end
def update_issuable(issuable, author) def update_issuable(issuable, author, skip_users = [])
# Skip toggling a task list item in a description # Skip toggling a task list item in a description
return if toggling_tasks?(issuable) return if toggling_tasks?(issuable)
create_mention_todos(issuable.project, issuable, author) create_mention_todos(issuable.project, issuable, author, nil, skip_users)
end end
def destroy_issuable(issuable, user) def destroy_issuable(issuable, user)
...@@ -252,7 +252,7 @@ class TodoService ...@@ -252,7 +252,7 @@ class TodoService
issuable.tasks? && issuable.updated_tasks.any? issuable.tasks? && issuable.updated_tasks.any?
end end
def handle_note(note, author) def handle_note(note, author, skip_users = [])
# Skip system notes, and notes on project snippet # Skip system notes, and notes on project snippet
return if note.system? || note.for_snippet? return if note.system? || note.for_snippet?
...@@ -260,7 +260,7 @@ class TodoService ...@@ -260,7 +260,7 @@ class TodoService
target = note.noteable target = note.noteable
mark_pending_todos_as_done(target, author) mark_pending_todos_as_done(target, author)
create_mention_todos(project, target, author, note) create_mention_todos(project, target, author, note, skip_users)
end end
def create_assignment_todo(issuable, author) def create_assignment_todo(issuable, author)
...@@ -270,14 +270,14 @@ class TodoService ...@@ -270,14 +270,14 @@ class TodoService
end end
end end
def create_mention_todos(project, target, author, note = nil) def create_mention_todos(project, target, author, note = nil, skip_users = [])
# Create Todos for directly addressed users # Create Todos for directly addressed users
directly_addressed_users = filter_directly_addressed_users(project, note || target, author) directly_addressed_users = filter_directly_addressed_users(project, note || target, author, skip_users)
attributes = attributes_for_todo(project, target, author, Todo::DIRECTLY_ADDRESSED, note) attributes = attributes_for_todo(project, target, author, Todo::DIRECTLY_ADDRESSED, note)
create_todos(directly_addressed_users, attributes) create_todos(directly_addressed_users, attributes)
# Create Todos for mentioned users # Create Todos for mentioned users
mentioned_users = filter_mentioned_users(project, note || target, author) mentioned_users = filter_mentioned_users(project, note || target, author, skip_users)
attributes = attributes_for_todo(project, target, author, Todo::MENTIONED, note) attributes = attributes_for_todo(project, target, author, Todo::MENTIONED, note)
create_todos(mentioned_users, attributes) create_todos(mentioned_users, attributes)
end end
...@@ -325,13 +325,13 @@ class TodoService ...@@ -325,13 +325,13 @@ class TodoService
reject_users_without_access(users, project, target).uniq reject_users_without_access(users, project, target).uniq
end end
def filter_mentioned_users(project, target, author) def filter_mentioned_users(project, target, author, skip_users = [])
mentioned_users = target.mentioned_users(author) mentioned_users = target.mentioned_users(author) - skip_users
filter_todo_users(mentioned_users, project, target) filter_todo_users(mentioned_users, project, target)
end end
def filter_directly_addressed_users(project, target, author) def filter_directly_addressed_users(project, target, author, skip_users = [])
directly_addressed_users = target.directly_addressed_users(author) directly_addressed_users = target.directly_addressed_users(author) - skip_users
filter_todo_users(directly_addressed_users, project, target) filter_todo_users(directly_addressed_users, project, target)
end end
......
...@@ -94,7 +94,7 @@ module Users ...@@ -94,7 +94,7 @@ module Users
def build_user_params def build_user_params
if current_user&.is_admin? if current_user&.is_admin?
user_params = params.slice(*admin_create_params) user_params = params.slice(*admin_create_params)
user_params[:created_by_id] = current_user.id user_params[:created_by_id] = current_user&.id
if params[:reset_password] if params[:reset_password]
user_params.merge!(force_random_password: true, password_expires_at: nil) user_params.merge!(force_random_password: true, password_expires_at: nil)
......
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
Joined #{time_ago_with_tooltip(member.created_at)} Joined #{time_ago_with_tooltip(member.created_at)}
- if member.expires? - if member.expires?
· ·
%span{ class: ('text-warning' if member.expires_soon?) } %span{ class: "#{"text-warning" if member.expires_soon?} has-tooltip", title: member.expires_at.to_time.in_time_zone.to_s(:medium) }
Expires in #{distance_of_time_in_words_to_now(member.expires_at)} Expires in #{distance_of_time_in_words_to_now(member.expires_at)}
- else - else
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
.controls.member-controls .controls.member-controls
= render 'shared/members/ee/ldap_tag', can_override: can_override_member, visible: false = render 'shared/members/ee/ldap_tag', can_override: can_override_member, visible: false
- if show_controls && member.source == current_resource - if show_controls && member.source == current_resource
- if user != current_user - if user != current_user && (can_admin_member || can_override_member)
= form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f| = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f|
= f.hidden_field :access_level = f.hidden_field :access_level
.member-form-control.dropdown.append-right-5 .member-form-control.dropdown.append-right-5
......
---
title: Hide form inputs for group member without editing rights
merge_request: 7816
author:
---
title: Fix linking to new issue with selected template via url parameter
merge_request:
author:
---
title: Create todos only for new mentions
merge_request:
author:
---
title: Add metrics button to environments overview page
merge_request: 10234
author:
---
title: Fix link to Jira service documentation
merge_request:
author:
---
title: Allow users to import GitHub projects to subgroups
merge_request:
author:
---
title: Handle parsing OpenBSD ps output properly to display sidekiq infos on admin->monitoring->background
merge_request: 10303
author: Sebastian Reitenbach
---
title: Remove unnecessary ORDER BY clause from `forked_to_project_id` subquery
merge_request:
author: mhasbini
...@@ -12,10 +12,12 @@ else ...@@ -12,10 +12,12 @@ else
user_args[:password] = ENV['GITLAB_ROOT_PASSWORD'] user_args[:password] = ENV['GITLAB_ROOT_PASSWORD']
end end
user = User.new(user_args) # Only admins can create other admin users in Users::CreateService so to solve
user.skip_confirmation! # the chicken-and-egg problem, we pass a non-persisted admin user to the service.
transient_admin = User.new(admin: true)
user = Users::CreateService.new(transient_admin, user_args.merge!(skip_confirmation: true)).execute
if user.save if user.persisted?
puts "Administrator account created:".color(:green) puts "Administrator account created:".color(:green)
puts puts
puts "login: root".color(:green) puts "login: root".color(:green)
......
## Using Dpl as deployment tool # Using Dpl as deployment tool
Dpl (dee-pee-ell) is a deploy tool made for continuous deployment that's developed and used by Travis CI, but can also be used with GitLab CI.
**We recommend to use Dpl, if you're deploying to any of these of these services: https://github.com/travis-ci/dpl#supported-providers**. [Dpl](https://github.com/travis-ci/dpl) (dee-pee-ell) is a deploy tool made for
continuous deployment that's developed and used by Travis CI, but can also be
used with GitLab CI.
### Requirements >**Note:**
To use Dpl you need at least Ruby 1.8.7 with ability to install gems. We recommend to use Dpl if you're deploying to any of these of these services:
https://github.com/travis-ci/dpl#supported-providers.
## Requirements
To use Dpl you need at least Ruby 1.9.3 with ability to install gems.
## Basic usage
Dpl can be installed on any machine with:
### Basic usage
The Dpl can be installed on any machine with:
``` ```
gem install dpl gem install dpl
``` ```
This allows you to test all commands from your shell, rather than having to test it on a CI server. This allows you to test all commands from your local terminal, rather than
having to test it on a CI server.
If you don't have Ruby installed you can do it on Debian-compatible Linux with: If you don't have Ruby installed you can do it on Debian-compatible Linux with:
``` ```
apt-get update apt-get update
apt-get install ruby-dev apt-get install ruby-dev
...@@ -26,9 +36,10 @@ To use it simply define provider and any additional parameters required by the p ...@@ -26,9 +36,10 @@ To use it simply define provider and any additional parameters required by the p
For example if you want to use it to deploy your application to heroku, you need to specify `heroku` as provider, specify `api-key` and `app`. For example if you want to use it to deploy your application to heroku, you need to specify `heroku` as provider, specify `api-key` and `app`.
There's more and all possible parameters can be found here: https://github.com/travis-ci/dpl#heroku There's more and all possible parameters can be found here: https://github.com/travis-ci/dpl#heroku
``` ```yaml
staging: staging:
type: deploy stage: deploy
script:
- gem install dpl - gem install dpl
- dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY - dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY
``` ```
...@@ -37,14 +48,17 @@ In the above example we use Dpl to deploy `my-app-staging` to Heroku server with ...@@ -37,14 +48,17 @@ In the above example we use Dpl to deploy `my-app-staging` to Heroku server with
To use different provider take a look at long list of [Supported Providers](https://github.com/travis-ci/dpl#supported-providers). To use different provider take a look at long list of [Supported Providers](https://github.com/travis-ci/dpl#supported-providers).
### Using Dpl with Docker ## Using Dpl with Docker
When you use GitLab Runner you most likely configured it to use your server's shell commands. When you use GitLab Runner you most likely configured it to use your server's shell commands.
This means that all commands are run in context of local user (ie. gitlab_runner or gitlab_ci_multi_runner). This means that all commands are run in context of local user (ie. gitlab_runner or gitlab_ci_multi_runner).
It also means that most probably in your Docker container you don't have the Ruby runtime installed. It also means that most probably in your Docker container you don't have the Ruby runtime installed.
You will have to install it: You will have to install it:
```
```yaml
staging: staging:
type: deploy stage: deploy
script:
- apt-get update -yq - apt-get update -yq
- apt-get install -y ruby-dev - apt-get install -y ruby-dev
- gem install dpl - gem install dpl
...@@ -53,24 +67,31 @@ staging: ...@@ -53,24 +67,31 @@ staging:
- master - master
``` ```
The first line `apt-get update -yq` updates the list of available packages, where second `apt-get install -y ruby-dev` install `Ruby` runtime on system. The first line `apt-get update -yq` updates the list of available packages,
where second `apt-get install -y ruby-dev` installs the Ruby runtime on system.
The above example is valid for all Debian-compatible systems. The above example is valid for all Debian-compatible systems.
### Usage in staging and production ## Usage in staging and production
It's pretty common in developer workflow to have staging (development) and production environment.
If we consider above example: we would like to deploy `master` branch to `staging` and `all tags` to `production` environment. It's pretty common in the development workflow to have staging (development) and
production environments
Let's consider the following example: we would like to deploy the `master`
branch to `staging` and all tags to the `production` environment.
The final `.gitlab-ci.yml` for that setup would look like this: The final `.gitlab-ci.yml` for that setup would look like this:
``` ```yaml
staging: staging:
type: deploy stage: deploy
script:
- gem install dpl - gem install dpl
- dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY - dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY
only: only:
- master - master
production: production:
type: deploy stage: deploy
script:
- gem install dpl - gem install dpl
- dpl --provider=heroku --app=my-app-production --api-key=$HEROKU_PRODUCTION_API_KEY - dpl --provider=heroku --app=my-app-production --api-key=$HEROKU_PRODUCTION_API_KEY
only: only:
...@@ -78,21 +99,28 @@ production: ...@@ -78,21 +99,28 @@ production:
``` ```
We created two deploy jobs that are executed on different events: We created two deploy jobs that are executed on different events:
1. `staging` is executed for all commits that were pushed to `master` branch, 1. `staging` is executed for all commits that were pushed to `master` branch,
2. `production` is executed for all pushed tags. 2. `production` is executed for all pushed tags.
We also use two secure variables: We also use two secure variables:
1. `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app, 1. `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app,
2. `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app. 2. `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app.
### Storing API keys ## Storing API keys
In GitLab CI 7.12 a new feature was introduced: Secure Variables.
Secure Variables can added by going to `Project > Variables > Add Variable`. Secure Variables can added by going to your project's
**This feature requires `gitlab-runner` with version equal or greater than 0.4.0.** **Settings ➔ CI/CD Pipelines ➔ Secret variables**. The variables that are defined
The variables that are defined in the project settings are sent along with the build script to the runner. in the project settings are sent along with the build script to the Runner.
The secure variables are stored out of the repository. Never store secrets in your projects' .gitlab-ci.yml. The secure variables are stored out of the repository. Never store secrets in
It is also important that secret's value is hidden in the job log. your project's `.gitlab-ci.yml`. It is also important that the secret's value
is hidden in the job log.
You access added variable by prefixing it's name with `$` (on non-Windows runners)
or `%` (for Windows Batch runners):
You access added variable by prefixing it's name with `$` (on non-Windows runners) or `%` (for Windows Batch runners):
1. `$SECRET_VARIABLE` - use it for non-Windows runners 1. `$SECRET_VARIABLE` - use it for non-Windows runners
2. `%SECRET_VARIABLE%` - use it for Windows Batch runners 2. `%SECRET_VARIABLE%` - use it for Windows Batch runners
Read more about the [CI variables](../../variables/README.md).
...@@ -17,8 +17,7 @@ polling rate. ...@@ -17,8 +17,7 @@ polling rate.
A `Poll-Interval: -1` means you should disable polling, and this must be implemented. A `Poll-Interval: -1` means you should disable polling, and this must be implemented.
1. A response with HTTP status `4XX` or `5XX` should disable polling as well. 1. A response with HTTP status `4XX` or `5XX` should disable polling as well.
1. Use a common library for polling. 1. Use a common library for polling.
1. Poll on active tabs only. Use a common library to find out which tab currently has eyes on it. 1. Poll on active tabs only. Please use [Visibility](https://github.com/ai/visibilityjs).
Please use [Focus](https://gitlab.com/andrewn/focus). Specifically [Eyeballs Detector](https://gitlab.com/andrewn/focus/blob/master/lib/eyeballs-detector.js).
1. Use regular polling intervals, do not use backoff polling, or jitter, as the interval will be 1. Use regular polling intervals, do not use backoff polling, or jitter, as the interval will be
controlled by the server. controlled by the server.
1. The backend code will most likely be using etags. You do not and should not check for status 1. The backend code will most likely be using etags. You do not and should not check for status
......
...@@ -141,6 +141,7 @@ with GitLab 8.12. ...@@ -141,6 +141,7 @@ with GitLab 8.12.
With the new job permissions model, there is now an easy way to access all With the new job permissions model, there is now an easy way to access all
dependent source code in a project. That way, we can: dependent source code in a project. That way, we can:
1. Access a project's dependent repositories
1. Access a project's [Git submodules][gitsub] 1. Access a project's [Git submodules][gitsub]
1. Access private container images 1. Access private container images
1. Access project's and submodule LFS objects 1. Access project's and submodule LFS objects
...@@ -177,6 +178,22 @@ As a user: ...@@ -177,6 +178,22 @@ As a user:
access to. As an Administrator, you can verify that by impersonating the user access to. As an Administrator, you can verify that by impersonating the user
and retry the failing job in order to verify that everything is correct. and retry the failing job in order to verify that everything is correct.
### Dependent repositories
The [Job environment variable][jobenv] `CI_JOB_TOKEN` can be used to
authenticate any clones of dependent repositories. For example:
```
git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/myuser/mydependentrepo
```
It can also be used for system-wide authentication
(only do this in a docker container, it will overwrite ~/.netrc):
```
echo -e "machine gitlab.com\nlogin gitlab-ci-token\npassword ${CI_JOB_TOKEN}" > ~/.netrc
```
### Git submodules ### Git submodules
To properly configure submodules with GitLab CI, read the To properly configure submodules with GitLab CI, read the
...@@ -221,3 +238,4 @@ test: ...@@ -221,3 +238,4 @@ test:
[triggers]: ../../ci/triggers/README.md [triggers]: ../../ci/triggers/README.md
[update-docs]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update [update-docs]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update
[workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse [workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse
[jobenv]: ../../ci/variables/README.md#predefined-variables-environment-variables
require 'spinach/capybara'
require 'capybara/poltergeist' require 'capybara/poltergeist'
require 'capybara-screenshot/spinach' require 'capybara-screenshot/spinach'
# Give CI some extra time # Give CI some extra time
timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 40 : 10 timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30
Capybara.javascript_driver = :poltergeist Capybara.javascript_driver = :poltergeist
Capybara.register_driver :poltergeist do |app| Capybara.register_driver :poltergeist do |app|
...@@ -25,5 +24,8 @@ Capybara.ignore_hidden_elements = false ...@@ -25,5 +24,8 @@ Capybara.ignore_hidden_elements = false
Capybara::Screenshot.prune_strategy = :keep_last_run Capybara::Screenshot.prune_strategy = :keep_last_run
Spinach.hooks.before_run do Spinach.hooks.before_run do
TestEnv.warm_asset_cache unless ENV['CI'] || ENV['CI_SERVER'] require 'spinach/capybara'
require 'capybara/rails'
TestEnv.eager_load_driver_server
end end
...@@ -36,3 +36,19 @@ Spinach.hooks.before_run do ...@@ -36,3 +36,19 @@ Spinach.hooks.before_run do
include FactoryGirl::Syntax::Methods include FactoryGirl::Syntax::Methods
end end
module StdoutReporterWithScenarioLocation
# Override the standard reporter to show filename and line number next to each
# scenario for easy, focused re-runs
def before_scenario_run(scenario, step_definitions = nil)
@max_step_name_length = scenario.steps.map(&:name).map(&:length).max if scenario.steps.any?
name = scenario.name
# This number has no significance, it's just to line things up
max_length = @max_step_name_length + 19
out.puts "\n #{'Scenario:'.green} #{name.light_green.ljust(max_length)}" \
" # #{scenario.feature.filename}:#{scenario.line}"
end
end
Spinach::Reporter::Stdout.prepend(StdoutReporterWithScenarioLocation)
...@@ -13,6 +13,13 @@ describe 'Issues', feature: true do ...@@ -13,6 +13,13 @@ describe 'Issues', feature: true do
user2 = create(:user) user2 = create(:user)
project.team << [[@user, user2], :developer] project.team << [[@user, user2], :developer]
project.repository.create_file(
@user,
'.gitlab/issue_templates/bug.md',
'this is a test "bug" template',
message: 'added issue template',
branch_name: 'master')
end end
describe 'Edit issue' do describe 'Edit issue' do
...@@ -632,6 +639,16 @@ describe 'Issues', feature: true do ...@@ -632,6 +639,16 @@ describe 'Issues', feature: true do
expect(page.find_field("issue_description").value).to match /\n\n$/ expect(page.find_field("issue_description").value).to match /\n\n$/
end end
end end
context 'form filled by URL parameters' do
before do
visit new_namespace_project_issue_path(project.namespace, project, issuable_template: 'bug')
end
it 'fills in template' do
expect(find('.js-issuable-selector .dropdown-toggle-text')).to have_content('bug')
end
end
end end
describe 'new issue by email' do describe 'new issue by email' do
......
require 'spec_helper' require 'spec_helper'
feature 'Project group links', feature: true, js: true do feature 'Project group links', :feature, :js do
include Select2Helper include Select2Helper
let(:master) { create(:user) } let(:master) { create(:user) }
...@@ -51,4 +51,24 @@ feature 'Project group links', feature: true, js: true do ...@@ -51,4 +51,24 @@ feature 'Project group links', feature: true, js: true do
end end
end end
end end
describe 'the groups dropdown' do
before do
group_two = create(:group)
group.add_owner(master)
group_two.add_owner(master)
visit namespace_project_settings_members_path(project.namespace, project)
execute_script 'GroupsSelect.PER_PAGE = 1;'
open_select2 '#link_group_id'
end
it 'should infinitely scroll' do
expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 1)
scroll_select2_to_bottom('.select2-drop .select2-results:visible')
expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 2)
end
end
end end
...@@ -53,6 +53,14 @@ describe SidekiqHelper do ...@@ -53,6 +53,14 @@ describe SidekiqHelper do
expect(parts).to eq(['17725', '1.0', '12.1', 'Ssl', '19:20:15', 'sidekiq 4.2.1 gitlab-rails [0 of 25 busy]']) expect(parts).to eq(['17725', '1.0', '12.1', 'Ssl', '19:20:15', 'sidekiq 4.2.1 gitlab-rails [0 of 25 busy]'])
end end
it 'parses OpenBSD output' do
# OpenBSD 6.1
line = '49258 0.5 2.3 R/0 Fri10PM ruby23: sidekiq 4.2.7 gitlab [0 of 25 busy] (ruby23)'
parts = helper.parse_sidekiq_ps(line)
expect(parts).to eq(['49258', '0.5', '2.3', 'R/0', 'Fri10PM', 'ruby23: sidekiq 4.2.7 gitlab [0 of 25 busy] (ruby23)'])
end
it 'does fail gracefully on line not matching the format' do it 'does fail gracefully on line not matching the format' do
line = '55137 10.0 2.1 S+ 2:30pm something' line = '55137 10.0 2.1 S+ 2:30pm something'
parts = helper.parse_sidekiq_ps(line) parts = helper.parse_sidekiq_ps(line)
......
...@@ -32,7 +32,12 @@ describe('Actions Component', () => { ...@@ -32,7 +32,12 @@ describe('Actions Component', () => {
}).$mount(); }).$mount();
}); });
it('should render a dropdown with the provided actions', () => { it('should render a dropdown button with icon and title attribute', () => {
expect(component.$el.querySelector('.fa-caret-down')).toBeDefined();
expect(component.$el.querySelector('.dropdown-new').getAttribute('title')).toEqual('Deploy to...');
});
it('should render a dropdown with the provided list of actions', () => {
expect( expect(
component.$el.querySelectorAll('.dropdown-menu li').length, component.$el.querySelectorAll('.dropdown-menu li').length,
).toEqual(actionsMock.length); ).toEqual(actionsMock.length);
......
import Vue from 'vue';
import monitoringComp from '~/environments/components/environment_monitoring';
describe('Monitoring Component', () => {
let MonitoringComponent;
beforeEach(() => {
MonitoringComponent = Vue.extend(monitoringComp);
});
it('should render a link to environment monitoring page', () => {
const monitoringUrl = 'https://gitlab.com';
const component = new MonitoringComponent({
propsData: {
monitoringUrl,
},
}).$mount();
expect(component.$el.getAttribute('href')).toEqual(monitoringUrl);
expect(component.$el.querySelector('.fa-area-chart')).toBeDefined();
expect(component.$el.getAttribute('title')).toEqual('Monitoring');
});
});
...@@ -24,7 +24,7 @@ describe('Stop Component', () => { ...@@ -24,7 +24,7 @@ describe('Stop Component', () => {
it('should render a button to stop the environment', () => { it('should render a button to stop the environment', () => {
expect(component.$el.tagName).toEqual('BUTTON'); expect(component.$el.tagName).toEqual('BUTTON');
expect(component.$el.getAttribute('title')).toEqual('Stop Environment'); expect(component.$el.getAttribute('title')).toEqual('Stop');
}); });
it('should call the service when an action is clicked', () => { it('should call the service when an action is clicked', () => {
......
...@@ -18,7 +18,7 @@ describe('Stop Component', () => { ...@@ -18,7 +18,7 @@ describe('Stop Component', () => {
it('should render a link to open a web terminal with the provided path', () => { it('should render a link to open a web terminal with the provided path', () => {
expect(component.$el.tagName).toEqual('A'); expect(component.$el.tagName).toEqual('A');
expect(component.$el.getAttribute('title')).toEqual('Open web terminal'); expect(component.$el.getAttribute('title')).toEqual('Terminal');
expect(component.$el.getAttribute('href')).toEqual(terminalPath); expect(component.$el.getAttribute('href')).toEqual(terminalPath);
}); });
}); });
...@@ -108,6 +108,37 @@ require('~/lib/utils/common_utils'); ...@@ -108,6 +108,37 @@ require('~/lib/utils/common_utils');
}); });
}); });
describe('gl.utils.normalizeCRLFHeaders', () => {
beforeEach(function () {
this.CLRFHeaders = 'a-header: a-value\nAnother-Header: ANOTHER-VALUE\nLaSt-HeAdEr: last-VALUE';
spyOn(String.prototype, 'split').and.callThrough();
spyOn(gl.utils, 'normalizeHeaders').and.callThrough();
this.normalizeCRLFHeaders = gl.utils.normalizeCRLFHeaders(this.CLRFHeaders);
});
it('should split by newline', function () {
expect(String.prototype.split).toHaveBeenCalledWith('\n');
});
it('should split by colon+space for each header', function () {
expect(String.prototype.split.calls.allArgs().filter(args => args[0] === ': ').length).toBe(3);
});
it('should call gl.utils.normalizeHeaders with a parsed headers object', function () {
expect(gl.utils.normalizeHeaders).toHaveBeenCalledWith(jasmine.any(Object));
});
it('should return a normalized headers object', function () {
expect(this.normalizeCRLFHeaders).toEqual({
'A-HEADER': 'a-value',
'ANOTHER-HEADER': 'ANOTHER-VALUE',
'LAST-HEADER': 'last-VALUE',
});
});
});
describe('gl.utils.parseIntPagination', () => { describe('gl.utils.parseIntPagination', () => {
it('should parse to integers all string values and return pagination object', () => { it('should parse to integers all string values and return pagination object', () => {
const pagination = { const pagination = {
......
...@@ -16,6 +16,26 @@ describe EnvironmentEntity do ...@@ -16,6 +16,26 @@ describe EnvironmentEntity do
expect(subject).to include(:id, :name, :state, :environment_path) expect(subject).to include(:id, :name, :state, :environment_path)
end end
context 'metrics disabled' do
before do
allow(environment).to receive(:has_metrics?).and_return(false)
end
it "doesn't expose metrics path" do
expect(subject).not_to include(:metrics_path)
end
end
context 'metrics enabled' do
before do
allow(environment).to receive(:has_metrics?).and_return(true)
end
it 'exposes metrics path' do
expect(subject).to include(:metrics_path)
end
end
context 'with deployment service ready' do context 'with deployment service ready' do
before do before do
allow(environment).to receive(:deployment_service_ready?).and_return(true) allow(environment).to receive(:deployment_service_ready?).and_return(true)
......
...@@ -13,6 +13,7 @@ describe Issues::UpdateService, services: true do ...@@ -13,6 +13,7 @@ describe Issues::UpdateService, services: true do
let(:issue) do let(:issue) do
create(:issue, title: 'Old title', create(:issue, title: 'Old title',
description: "for #{user2.to_reference}",
assignee_id: user3.id, assignee_id: user3.id,
project: project) project: project)
end end
...@@ -182,16 +183,24 @@ describe Issues::UpdateService, services: true do ...@@ -182,16 +183,24 @@ describe Issues::UpdateService, services: true do
it 'marks pending todos as done' do it 'marks pending todos as done' do
expect(todo.reload.done?).to eq true expect(todo.reload.done?).to eq true
end end
it 'does not create any new todos' do
expect(Todo.count).to eq(1)
end
end end
context 'when the description change' do context 'when the description change' do
before do before do
update_issue(description: 'Also please fix') update_issue(description: "Also please fix #{user2.to_reference} #{user3.to_reference}")
end end
it 'marks todos as done' do it 'marks todos as done' do
expect(todo.reload.done?).to eq true expect(todo.reload.done?).to eq true
end end
it 'creates only 1 new todo' do
expect(Todo.count).to eq(2)
end
end end
context 'when is reassigned' do context 'when is reassigned' do
......
...@@ -12,6 +12,7 @@ describe MergeRequests::UpdateService, services: true do ...@@ -12,6 +12,7 @@ describe MergeRequests::UpdateService, services: true do
let(:merge_request) do let(:merge_request) do
create(:merge_request, :simple, title: 'Old title', create(:merge_request, :simple, title: 'Old title',
description: "FYI #{user2.to_reference}",
assignee_id: user3.id, assignee_id: user3.id,
source_project: project) source_project: project)
end end
...@@ -225,16 +226,24 @@ describe MergeRequests::UpdateService, services: true do ...@@ -225,16 +226,24 @@ describe MergeRequests::UpdateService, services: true do
it 'marks pending todos as done' do it 'marks pending todos as done' do
expect(pending_todo.reload).to be_done expect(pending_todo.reload).to be_done
end end
it 'does not create any new todos' do
expect(Todo.count).to eq(1)
end
end end
context 'when the description change' do context 'when the description change' do
before do before do
update_merge_request({ description: 'Also please fix' }) update_merge_request({ description: "Also please fix #{user2.to_reference} #{user3.to_reference}" })
end end
it 'marks pending todos as done' do it 'marks pending todos as done' do
expect(pending_todo.reload).to be_done expect(pending_todo.reload).to be_done
end end
it 'creates only 1 new todo' do
expect(Todo.count).to eq(2)
end
end end
context 'when is reassigned' do context 'when is reassigned' do
......
...@@ -4,12 +4,14 @@ describe Notes::UpdateService, services: true do ...@@ -4,12 +4,14 @@ describe Notes::UpdateService, services: true do
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:issue) { create(:issue, project: project) } let(:issue) { create(:issue, project: project) }
let(:note) { create(:note, project: project, noteable: issue, author: user, note: 'Old note') } let(:note) { create(:note, project: project, noteable: issue, author: user, note: "Old note #{user2.to_reference}") }
before do before do
project.team << [user, :master] project.team << [user, :master]
project.team << [user2, :developer] project.team << [user2, :developer]
project.team << [user3, :developer]
end end
describe '#execute' do describe '#execute' do
...@@ -23,22 +25,30 @@ describe Notes::UpdateService, services: true do ...@@ -23,22 +25,30 @@ describe Notes::UpdateService, services: true do
context 'when the note change' do context 'when the note change' do
before do before do
update_note({ note: 'New note' }) update_note({ note: "New note #{user2.to_reference} #{user3.to_reference}" })
end end
it 'marks todos as done' do it 'marks todos as done' do
expect(todo.reload).to be_done expect(todo.reload).to be_done
end end
it 'creates only 1 new todo' do
expect(Todo.count).to eq(2)
end
end end
context 'when the note does not change' do context 'when the note does not change' do
before do before do
update_note({ note: 'Old note' }) update_note({ note: "Old note #{user2.to_reference}" })
end end
it 'keep todos' do it 'keep todos' do
expect(todo.reload).to be_pending expect(todo.reload).to be_pending
end end
it 'does not create any new todos' do
expect(Todo.count).to eq(1)
end
end end
end end
end end
......
...@@ -8,10 +8,12 @@ describe TodoService, services: true do ...@@ -8,10 +8,12 @@ describe TodoService, services: true do
let(:guest) { create(:user) } let(:guest) { create(:user) }
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
let(:john_doe) { create(:user) } let(:john_doe) { create(:user) }
let(:skipped) { create(:user) }
let(:skip_users) { [skipped] }
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
let(:mentions) { 'FYI: ' + [author, assignee, john_doe, member, guest, non_member, admin].map(&:to_reference).join(' ') } let(:mentions) { 'FYI: ' + [author, assignee, john_doe, member, guest, non_member, admin, skipped].map(&:to_reference).join(' ') }
let(:directly_addressed) { [author, assignee, john_doe, member, guest, non_member, admin].map(&:to_reference).join(' ') } let(:directly_addressed) { [author, assignee, john_doe, member, guest, non_member, admin, skipped].map(&:to_reference).join(' ') }
let(:directly_addressed_and_mentioned) { member.to_reference + ", what do you think? cc: " + [guest, admin].map(&:to_reference).join(' ') } let(:directly_addressed_and_mentioned) { member.to_reference + ", what do you think? cc: " + [guest, admin, skipped].map(&:to_reference).join(' ') }
let(:service) { described_class.new } let(:service) { described_class.new }
before do before do
...@@ -19,6 +21,7 @@ describe TodoService, services: true do ...@@ -19,6 +21,7 @@ describe TodoService, services: true do
project.team << [author, :developer] project.team << [author, :developer]
project.team << [member, :developer] project.team << [member, :developer]
project.team << [john_doe, :developer] project.team << [john_doe, :developer]
project.team << [skipped, :developer]
end end
describe 'Issues' do describe 'Issues' do
...@@ -119,46 +122,61 @@ describe TodoService, services: true do ...@@ -119,46 +122,61 @@ describe TodoService, services: true do
end end
describe '#update_issue' do describe '#update_issue' do
it 'creates a todo for each valid mentioned user' do it 'creates a todo for each valid mentioned user not included in skip_users' do
service.update_issue(issue, author) service.update_issue(issue, author, skip_users)
should_create_todo(user: member, target: issue, action: Todo::MENTIONED) should_create_todo(user: member, target: issue, action: Todo::MENTIONED)
should_create_todo(user: guest, target: issue, action: Todo::MENTIONED) should_create_todo(user: guest, target: issue, action: Todo::MENTIONED)
should_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED) should_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED)
should_create_todo(user: author, target: issue, action: Todo::MENTIONED) should_create_todo(user: author, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED) should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: skipped, target: issue, action: Todo::MENTIONED)
end end
it 'creates a todo for each valid user based on the type of mention' do it 'creates a todo for each valid user not included in skip_users based on the type of mention' do
issue.update(description: directly_addressed_and_mentioned) issue.update(description: directly_addressed_and_mentioned)
service.update_issue(issue, author) service.update_issue(issue, author, skip_users)
should_create_todo(user: member, target: issue, action: Todo::DIRECTLY_ADDRESSED) should_create_todo(user: member, target: issue, action: Todo::DIRECTLY_ADDRESSED)
should_create_todo(user: guest, target: issue, action: Todo::MENTIONED) should_create_todo(user: guest, target: issue, action: Todo::MENTIONED)
should_create_todo(user: admin, target: issue, action: Todo::MENTIONED) should_create_todo(user: admin, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: skipped, target: issue)
end end
it 'creates a directly addressed todo for each valid addressed user' do it 'creates a directly addressed todo for each valid addressed user not included in skip_users' do
service.update_issue(addressed_issue, author) service.update_issue(addressed_issue, author, skip_users)
should_create_todo(user: member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) should_create_todo(user: member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
should_create_todo(user: guest, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) should_create_todo(user: guest, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
should_create_todo(user: john_doe, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) should_create_todo(user: john_doe, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
should_create_todo(user: author, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) should_create_todo(user: author, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
should_not_create_todo(user: non_member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) should_not_create_todo(user: non_member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
should_not_create_todo(user: skipped, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
end end
it 'does not create a todo if user was already mentioned' do it 'does not create a todo if user was already mentioned and todo is pending' do
create(:todo, :mentioned, user: member, project: project, target: issue, author: author) create(:todo, :mentioned, user: member, project: project, target: issue, author: author)
expect { service.update_issue(issue, author) }.not_to change(member.todos, :count) expect { service.update_issue(issue, author, skip_users) }.not_to change(member.todos, :count)
end
it 'does not create a todo if user was already mentioned and todo is done' do
create(:todo, :mentioned, :done, user: skipped, project: project, target: issue, author: author)
expect { service.update_issue(issue, author, skip_users) }.not_to change(skipped.todos, :count)
end end
it 'does not create a directly addressed todo if user was already mentioned or addressed' do it 'does not create a directly addressed todo if user was already mentioned or addressed and todo is pending' do
create(:todo, :directly_addressed, user: member, project: project, target: addressed_issue, author: author) create(:todo, :directly_addressed, user: member, project: project, target: addressed_issue, author: author)
expect { service.update_issue(addressed_issue, author) }.not_to change(member.todos, :count) expect { service.update_issue(addressed_issue, author, skip_users) }.not_to change(member.todos, :count)
end
it 'does not create a directly addressed todo if user was already mentioned or addressed and todo is done' do
create(:todo, :directly_addressed, :done, user: skipped, project: project, target: addressed_issue, author: author)
expect { service.update_issue(addressed_issue, author, skip_users) }.not_to change(skipped.todos, :count)
end end
it 'does not create todo if user can not see the issue when issue is confidential' do it 'does not create todo if user can not see the issue when issue is confidential' do
...@@ -551,47 +569,62 @@ describe TodoService, services: true do ...@@ -551,47 +569,62 @@ describe TodoService, services: true do
end end
describe '#update_merge_request' do describe '#update_merge_request' do
it 'creates a todo for each valid mentioned user' do it 'creates a todo for each valid mentioned user not included in skip_users' do
service.update_merge_request(mr_assigned, author) service.update_merge_request(mr_assigned, author, skip_users)
should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED) should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED) should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
should_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED) should_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
should_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED) should_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED) should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: skipped, target: mr_assigned, action: Todo::MENTIONED)
end end
it 'creates a todo for each valid user based on the type of mention' do it 'creates a todo for each valid user not included in skip_users based on the type of mention' do
mr_assigned.update(description: directly_addressed_and_mentioned) mr_assigned.update(description: directly_addressed_and_mentioned)
service.update_merge_request(mr_assigned, author) service.update_merge_request(mr_assigned, author, skip_users)
should_create_todo(user: member, target: mr_assigned, action: Todo::DIRECTLY_ADDRESSED) should_create_todo(user: member, target: mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
should_create_todo(user: admin, target: mr_assigned, action: Todo::MENTIONED) should_create_todo(user: admin, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: skipped, target: mr_assigned)
end end
it 'creates a directly addressed todo for each valid addressed user' do it 'creates a directly addressed todo for each valid addressed user not included in skip_users' do
service.update_merge_request(addressed_mr_assigned, author) service.update_merge_request(addressed_mr_assigned, author, skip_users)
should_create_todo(user: member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) should_create_todo(user: member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
should_create_todo(user: john_doe, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) should_create_todo(user: john_doe, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
should_create_todo(user: author, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) should_create_todo(user: author, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
should_not_create_todo(user: non_member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) should_not_create_todo(user: non_member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
should_not_create_todo(user: skipped, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
end end
it 'does not create a todo if user was already mentioned' do it 'does not create a todo if user was already mentioned and todo is pending' do
create(:todo, :mentioned, user: member, project: project, target: mr_assigned, author: author) create(:todo, :mentioned, user: member, project: project, target: mr_assigned, author: author)
expect { service.update_merge_request(mr_assigned, author) }.not_to change(member.todos, :count) expect { service.update_merge_request(mr_assigned, author) }.not_to change(member.todos, :count)
end end
it 'does not create a directly addressed todo if user was already mentioned or addressed' do it 'does not create a todo if user was already mentioned and todo is done' do
create(:todo, :mentioned, :done, user: skipped, project: project, target: mr_assigned, author: author)
expect { service.update_merge_request(mr_assigned, author, skip_users) }.not_to change(skipped.todos, :count)
end
it 'does not create a directly addressed todo if user was already mentioned or addressed and todo is pending' do
create(:todo, :directly_addressed, user: member, project: project, target: addressed_mr_assigned, author: author) create(:todo, :directly_addressed, user: member, project: project, target: addressed_mr_assigned, author: author)
expect{ service.update_merge_request(addressed_mr_assigned, author) }.not_to change(member.todos, :count) expect{ service.update_merge_request(addressed_mr_assigned, author) }.not_to change(member.todos, :count)
end end
it 'does not create a directly addressed todo if user was already mentioned or addressed and todo is done' do
create(:todo, :directly_addressed, user: skipped, project: project, target: addressed_mr_assigned, author: author)
expect{ service.update_merge_request(addressed_mr_assigned, author, skip_users) }.not_to change(skipped.todos, :count)
end
context 'with a task list' do context 'with a task list' do
it 'does not create todo when tasks are marked as completed' do it 'does not create todo when tasks are marked as completed' do
mr_assigned.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}") mr_assigned.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}")
...@@ -787,6 +820,69 @@ describe TodoService, services: true do ...@@ -787,6 +820,69 @@ describe TodoService, services: true do
end end
end end
describe '#update_note' do
let(:noteable) { create(:issue, project: project) }
let(:note) { create(:note, project: project, note: mentions, noteable: noteable) }
let(:addressed_note) { create(:note, project: project, note: "#{directly_addressed}", noteable: noteable) }
it 'creates a todo for each valid mentioned user not included in skip_users' do
service.update_note(note, author, skip_users)
should_create_todo(user: member, target: noteable, action: Todo::MENTIONED)
should_create_todo(user: guest, target: noteable, action: Todo::MENTIONED)
should_create_todo(user: john_doe, target: noteable, action: Todo::MENTIONED)
should_create_todo(user: author, target: noteable, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: noteable, action: Todo::MENTIONED)
should_not_create_todo(user: skipped, target: noteable, action: Todo::MENTIONED)
end
it 'creates a todo for each valid user not included in skip_users based on the type of mention' do
note.update(note: directly_addressed_and_mentioned)
service.update_note(note, author, skip_users)
should_create_todo(user: member, target: noteable, action: Todo::DIRECTLY_ADDRESSED)
should_create_todo(user: guest, target: noteable, action: Todo::MENTIONED)
should_create_todo(user: admin, target: noteable, action: Todo::MENTIONED)
should_not_create_todo(user: skipped, target: noteable)
end
it 'creates a directly addressed todo for each valid addressed user not included in skip_users' do
service.update_note(addressed_note, author, skip_users)
should_create_todo(user: member, target: noteable, action: Todo::DIRECTLY_ADDRESSED)
should_create_todo(user: guest, target: noteable, action: Todo::DIRECTLY_ADDRESSED)
should_create_todo(user: john_doe, target: noteable, action: Todo::DIRECTLY_ADDRESSED)
should_create_todo(user: author, target: noteable, action: Todo::DIRECTLY_ADDRESSED)
should_not_create_todo(user: non_member, target: noteable, action: Todo::DIRECTLY_ADDRESSED)
should_not_create_todo(user: skipped, target: noteable, action: Todo::DIRECTLY_ADDRESSED)
end
it 'does not create a todo if user was already mentioned and todo is pending' do
create(:todo, :mentioned, user: member, project: project, target: noteable, author: author)
expect { service.update_note(note, author, skip_users) }.not_to change(member.todos, :count)
end
it 'does not create a todo if user was already mentioned and todo is done' do
create(:todo, :mentioned, :done, user: skipped, project: project, target: noteable, author: author)
expect { service.update_note(note, author, skip_users) }.not_to change(skipped.todos, :count)
end
it 'does not create a directly addressed todo if user was already mentioned or addressed and todo is pending' do
create(:todo, :directly_addressed, user: member, project: project, target: noteable, author: author)
expect { service.update_note(addressed_note, author, skip_users) }.not_to change(member.todos, :count)
end
it 'does not create a directly addressed todo if user was already mentioned or addressed and todo is done' do
create(:todo, :directly_addressed, :done, user: skipped, project: project, target: noteable, author: author)
expect { service.update_note(addressed_note, author, skip_users) }.not_to change(skipped.todos, :count)
end
end
it 'updates cached counts when a todo is created' do it 'updates cached counts when a todo is created' do
issue = create(:issue, project: project, assignee: john_doe, author: author, description: mentions) issue = create(:issue, project: project, assignee: john_doe, author: author, description: mentions)
......
...@@ -61,6 +61,23 @@ describe Users::CreateService, services: true do ...@@ -61,6 +61,23 @@ describe Users::CreateService, services: true do
) )
end end
context 'when the current_user is not persisted' do
let(:admin_user) { build(:admin) }
it 'persists the given attributes and sets created_by_id to nil' do
user = service.execute
user.reload
expect(user).to have_attributes(
name: params[:name],
username: params[:username],
email: params[:email],
password: params[:password],
created_by_id: nil
)
end
end
it 'user is not confirmed if skip_confirmation param is not present' do it 'user is not confirmed if skip_confirmation param is not present' do
expect(service.execute).not_to be_confirmed expect(service.execute).not_to be_confirmed
end end
......
# rubocop:disable Style/GlobalVars
require 'capybara/rails' require 'capybara/rails'
require 'capybara/rspec' require 'capybara/rspec'
require 'capybara/poltergeist' require 'capybara/poltergeist'
require 'capybara-screenshot/rspec' require 'capybara-screenshot/rspec'
# Give CI some extra time # Give CI some extra time
timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 40 : 10 timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30
Capybara.javascript_driver = :poltergeist Capybara.javascript_driver = :poltergeist
Capybara.register_driver :poltergeist do |app| Capybara.register_driver :poltergeist do |app|
...@@ -26,7 +27,10 @@ Capybara.ignore_hidden_elements = true ...@@ -26,7 +27,10 @@ Capybara.ignore_hidden_elements = true
Capybara::Screenshot.prune_strategy = :keep_last_run Capybara::Screenshot.prune_strategy = :keep_last_run
RSpec.configure do |config| RSpec.configure do |config|
config.before(:suite) do config.before(:context, :js) do
TestEnv.warm_asset_cache unless ENV['CI'] || ENV['CI_SERVER'] next if $capybara_server_already_started
TestEnv.eager_load_driver_server
$capybara_server_already_started = true
end end
end end
...@@ -228,5 +228,19 @@ shared_examples 'a GitHub-ish import controller: POST create' do ...@@ -228,5 +228,19 @@ shared_examples 'a GitHub-ish import controller: POST create' do
post :create, { new_name: test_name, format: :js } post :create, { new_name: test_name, format: :js }
end end
end end
context 'user has chosen a nested namespace and name for the project' do
let(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
let(:nested_namespace) { create(:namespace, name: 'bar', parent: parent_namespace, owner: user) }
let(:test_name) { 'test_name' }
it 'takes the selected namespace and name' do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(provider_repo, test_name, nested_namespace, user, access_params, type: provider).
and_return(double(execute: true))
post :create, { target_namespace: nested_namespace.full_path, new_name: test_name, format: :js }
end
end
end end
end end
...@@ -22,4 +22,12 @@ module Select2Helper ...@@ -22,4 +22,12 @@ module Select2Helper
execute_script("$('#{selector}').select2('val', '#{value}').trigger('change');") execute_script("$('#{selector}').select2('val', '#{value}').trigger('change');")
end end
end end
def open_select2(selector)
execute_script("$('#{selector}').select2('open');")
end
def scroll_select2_to_bottom(selector)
evaluate_script "$('#{selector}').scrollTop($('#{selector}')[0].scrollHeight); $('#{selector}');"
end
end end
...@@ -171,10 +171,11 @@ module TestEnv ...@@ -171,10 +171,11 @@ module TestEnv
# #
# Otherwise they'd be created by the first test, often timing out and # Otherwise they'd be created by the first test, often timing out and
# causing a transient test failure # causing a transient test failure
def warm_asset_cache def eager_load_driver_server
return unless defined?(Capybara) return unless defined?(Capybara)
Capybara.current_session.driver.visit '/' puts "Starting the Capybara driver server..."
Capybara.current_session.visit '/'
end end
private private
......
This source diff could not be displayed because it is too large. You can view the blob instead.
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