Commit d5549025 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'ce-upstream' into 'master'

CE upstream

Closes gitlab-ce#26813 and gitlab-ce#25989

See merge request !1102
parents 6fb89ee2 6c404fa9
...@@ -21,7 +21,7 @@ gem 'rugged', '~> 0.24.0' ...@@ -21,7 +21,7 @@ gem 'rugged', '~> 0.24.0'
# Authentication libraries # Authentication libraries
gem 'devise', '~> 4.2' gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0' gem 'doorkeeper', '~> 4.2.0'
gem 'omniauth', '~> 1.3.1' gem 'omniauth', '~> 1.3.2'
gem 'omniauth-auth0', '~> 1.4.1' gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6' gem 'omniauth-azure-oauth2', '~> 0.0.6'
gem 'omniauth-cas3', '~> 1.1.2' gem 'omniauth-cas3', '~> 1.1.2'
......
...@@ -473,7 +473,7 @@ GEM ...@@ -473,7 +473,7 @@ GEM
octokit (4.6.2) octokit (4.6.2)
sawyer (~> 0.8.0, >= 0.5.3) sawyer (~> 0.8.0, >= 0.5.3)
oj (2.17.4) oj (2.17.4)
omniauth (1.3.1) omniauth (1.3.2)
hashie (>= 1.2, < 4) hashie (>= 1.2, < 4)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
omniauth-auth0 (1.4.1) omniauth-auth0 (1.4.1)
...@@ -958,7 +958,7 @@ DEPENDENCIES ...@@ -958,7 +958,7 @@ DEPENDENCIES
oauth2 (~> 1.2.0) oauth2 (~> 1.2.0)
octokit (~> 4.6.2) octokit (~> 4.6.2)
oj (~> 2.17.4) oj (~> 2.17.4)
omniauth (~> 1.3.1) omniauth (~> 1.3.2)
omniauth-auth0 (~> 1.4.1) omniauth-auth0 (~> 1.4.1)
omniauth-authentiq (~> 0.2.0) omniauth-authentiq (~> 0.2.0)
omniauth-azure-oauth2 (~> 0.0.6) omniauth-azure-oauth2 (~> 0.0.6)
......
...@@ -143,4 +143,4 @@ Please see [Getting help for GitLab](https://about.gitlab.com/getting-help/) on ...@@ -143,4 +143,4 @@ Please see [Getting help for GitLab](https://about.gitlab.com/getting-help/) on
## Is it awesome? ## Is it awesome?
Thanks for [asking this question](https://twitter.com/supersloth/status/489462789384056832) Joshua. Thanks for [asking this question](https://twitter.com/supersloth/status/489462789384056832) Joshua.
[These people](https://twitter.com/gitlab/favorites) seem to like it. [These people](https://twitter.com/gitlab/likes) seem to like it.
...@@ -20,6 +20,9 @@ ...@@ -20,6 +20,9 @@
if (selected.tagName === 'LI') { if (selected.tagName === 'LI') {
if (selected.hasAttribute('data-value')) { if (selected.hasAttribute('data-value')) {
this.dismissDropdown(); this.dismissDropdown();
} else if (selected.getAttribute('data-action') === 'submit') {
this.dismissDropdown();
this.dispatchFormSubmitEvent();
} else { } else {
const token = selected.querySelector('.js-filter-hint').innerText.trim(); const token = selected.querySelector('.js-filter-hint').innerText.trim();
const tag = selected.querySelector('.js-filter-tag').innerText.trim(); const tag = selected.querySelector('.js-filter-tag').innerText.trim();
......
...@@ -39,6 +39,7 @@ ...@@ -39,6 +39,7 @@
} }
this.dismissDropdown(); this.dismissDropdown();
this.dispatchInputEvent();
} }
} }
...@@ -84,6 +85,12 @@ ...@@ -84,6 +85,12 @@
})); }));
} }
dispatchFormSubmitEvent() {
// dispatchEvent() is necessary as form.submit() does not
// trigger event handlers
this.input.form.dispatchEvent(new Event('submit'));
}
hideDropdown() { hideDropdown() {
this.getCurrentHook().list.hide(); this.getCurrentHook().list.hide();
} }
......
...@@ -66,11 +66,19 @@ ...@@ -66,11 +66,19 @@
const word = `${tokenName}:${tokenValue}`; const word = `${tokenName}:${tokenValue}`;
// Get the string to replace // Get the string to replace
const selectionStart = input.selectionStart; let newCaretPosition = input.selectionStart;
const { left, right } = gl.DropdownUtils.getInputSelectionPosition(input); const { left, right } = gl.DropdownUtils.getInputSelectionPosition(input);
input.value = `${inputValue.substr(0, left)}${word}${inputValue.substr(right)}`; input.value = `${inputValue.substr(0, left)}${word}${inputValue.substr(right)}`;
gl.FilteredSearchDropdownManager.updateInputCaretPosition(selectionStart, input);
// If we have added a tokenValue at the end of the input,
// add a space and set selection to the end
if (right >= inputValue.length && tokenValue !== '') {
input.value += ' ';
newCaretPosition = input.value.length;
}
gl.FilteredSearchDropdownManager.updateInputCaretPosition(newCaretPosition, input);
} }
static updateInputCaretPosition(selectionStart, input) { static updateInputCaretPosition(selectionStart, input) {
......
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
} }
bindEvents() { bindEvents() {
this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager); this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this); this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this);
this.checkForEnterWrapper = this.checkForEnter.bind(this); this.checkForEnterWrapper = this.checkForEnter.bind(this);
...@@ -32,6 +33,7 @@ ...@@ -32,6 +33,7 @@
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
this.tokenChange = this.tokenChange.bind(this); this.tokenChange = this.tokenChange.bind(this);
this.filteredSearchInput.form.addEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper); this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper); this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper);
...@@ -42,6 +44,7 @@ ...@@ -42,6 +44,7 @@
} }
unbindEvents() { unbindEvents() {
this.filteredSearchInput.form.removeEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper); this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper); this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper);
...@@ -88,6 +91,11 @@ ...@@ -88,6 +91,11 @@
this.dropdownManager.resetDropdowns(); this.dropdownManager.resetDropdowns();
} }
handleFormSubmit(e) {
e.preventDefault();
this.search();
}
loadSearchParamsFromURL() { loadSearchParamsFromURL() {
const params = gl.utils.getUrlParamsArray(); const params = gl.utils.getUrlParamsArray();
const usernameParams = this.getUsernameParams(); const usernameParams = this.getUsernameParams();
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
} }
.ci-status-icon-pending, .ci-status-icon-pending,
.ci-status-icon-failed_with_warnings,
.ci-status-icon-success_with_warnings { .ci-status-icon-success_with_warnings {
color: $gl-warning; color: $gl-warning;
......
...@@ -46,10 +46,6 @@ ...@@ -46,10 +46,6 @@
font-weight: bold; font-weight: bold;
} }
.fa-clipboard {
color: $dropdown-title-btn-color;
}
.commit-info { .commit-info {
&.branches { &.branches {
margin-left: 8px; margin-left: 8px;
......
...@@ -195,10 +195,10 @@ ul.notes { ...@@ -195,10 +195,10 @@ ul.notes {
} }
.note-body { .note-body {
overflow: auto; overflow-x: auto;
overflow-y: hidden;
.note-text { .note-text {
overflow: auto;
word-wrap: break-word; word-wrap: break-word;
@include md-typography; @include md-typography;
// Reset ul style types since we're nested inside a ul already // Reset ul style types since we're nested inside a ul already
......
...@@ -18,7 +18,6 @@ ...@@ -18,7 +18,6 @@
.file-finder-input:hover, .file-finder-input:hover,
.issuable-search-form:hover, .issuable-search-form:hover,
.search-text-input:hover, .search-text-input:hover,
textarea:hover,
.form-control:hover { .form-control:hover {
border-color: lighten($dropdown-input-focus-border, 20%); border-color: lighten($dropdown-input-focus-border, 20%);
box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%); box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%);
......
...@@ -19,7 +19,8 @@ ...@@ -19,7 +19,8 @@
overflow: visible; overflow: visible;
} }
&.ci-failed { &.ci-failed,
&.ci-failed_with_warnings {
color: $gl-danger; color: $gl-danger;
border-color: $gl-danger; border-color: $gl-danger;
......
module ServicesHelper module ServicesHelper
def service_event_description(event) def service_event_description(event)
case event case event
when "push" when "push", "push_events"
"Event will be triggered by a push to the repository" "Event will be triggered by a push to the repository"
when "tag_push" when "tag_push", "tag_push_events"
"Event will be triggered when a new tag is pushed to the repository" "Event will be triggered when a new tag is pushed to the repository"
when "note" when "note", "note_events"
"Event will be triggered when someone adds a comment" "Event will be triggered when someone adds a comment"
when "issue" when "issue", "issue_events"
"Event will be triggered when an issue is created/updated/closed" "Event will be triggered when an issue is created/updated/closed"
when "confidential_issue" when "confidential_issue", "confidential_issue_events"
"Event will be triggered when a confidential issue is created/updated/closed" "Event will be triggered when a confidential issue is created/updated/closed"
when "merge_request" when "merge_request", "merge_request_events"
"Event will be triggered when a merge request is created/updated/merged" "Event will be triggered when a merge request is created/updated/merged"
when "build" when "build", "build_events"
"Event will be triggered when a build status changes" "Event will be triggered when a build status changes"
when "wiki_page" when "wiki_page", "wiki_page_events"
"Event will be triggered when a wiki page is created/updated" "Event will be triggered when a wiki page is created/updated"
when "commit" when "commit", "commit_events"
"Event will be triggered when a commit is created/updated" "Event will be triggered when a commit is created/updated"
end end
end end
...@@ -26,4 +26,6 @@ module ServicesHelper ...@@ -26,4 +26,6 @@ module ServicesHelper
event = event.pluralize if %w[merge_request issue confidential_issue].include?(event) event = event.pluralize if %w[merge_request issue confidential_issue].include?(event)
"#{event}_events" "#{event}_events"
end end
extend self
end end
...@@ -108,15 +108,11 @@ class Notify < BaseMailer ...@@ -108,15 +108,11 @@ class Notify < BaseMailer
def mail_thread(model, headers = {}) def mail_thread(model, headers = {})
add_project_headers add_project_headers
add_unsubscription_headers_and_links
headers["X-GitLab-#{model.class.name}-ID"] = model.id headers["X-GitLab-#{model.class.name}-ID"] = model.id
headers['X-GitLab-Reply-Key'] = reply_key headers['X-GitLab-Reply-Key'] = reply_key
if !@labels_url && @sent_notification && @sent_notification.unsubscribable?
headers['List-Unsubscribe'] = "<#{unsubscribe_sent_notification_url(@sent_notification, force: true)}>"
@sent_notification_url = unsubscribe_sent_notification_url(@sent_notification)
end
if Gitlab::IncomingEmail.enabled? if Gitlab::IncomingEmail.enabled?
address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key)) address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
address.display_name = @project.name_with_namespace address.display_name = @project.name_with_namespace
...@@ -172,4 +168,16 @@ class Notify < BaseMailer ...@@ -172,4 +168,16 @@ class Notify < BaseMailer
headers['X-GitLab-Project-Id'] = @project.id headers['X-GitLab-Project-Id'] = @project.id
headers['X-GitLab-Project-Path'] = @project.path_with_namespace headers['X-GitLab-Project-Path'] = @project.path_with_namespace
end end
def add_unsubscription_headers_and_links
return unless !@labels_url && @sent_notification && @sent_notification.unsubscribable?
list_unsubscribe_methods = [unsubscribe_sent_notification_url(@sent_notification, force: true)]
if Gitlab::IncomingEmail.enabled? && Gitlab::IncomingEmail.supports_wildcard?
list_unsubscribe_methods << "mailto:#{Gitlab::IncomingEmail.unsubscribe_address(reply_key)}"
end
headers['List-Unsubscribe'] = list_unsubscribe_methods.map { |e| "<#{e}>" }.join(',')
@sent_notification_url = unsubscribe_sent_notification_url(@sent_notification)
end
end end
...@@ -128,16 +128,21 @@ module Ci ...@@ -128,16 +128,21 @@ module Ci
end end
def stages def stages
# TODO, this needs refactoring, see gitlab-ce#26481.
stages_query = statuses
.group('stage').select(:stage).order('max(stage_idx)')
status_sql = statuses.latest.where('stage=sg.stage').status_sql status_sql = statuses.latest.where('stage=sg.stage').status_sql
stages_query = statuses.group('stage').select(:stage) warnings_sql = statuses.latest.select('COUNT(*) > 0')
.order('max(stage_idx)') .where('stage=sg.stage').failed_but_allowed.to_sql
stages_with_statuses = CommitStatus.from(stages_query, :sg). stages_with_statuses = CommitStatus.from(stages_query, :sg)
pluck('sg.stage', status_sql) .pluck('sg.stage', status_sql, "(#{warnings_sql})")
stages_with_statuses.map do |stage| stages_with_statuses.map do |stage|
Ci::Stage.new(self, name: stage.first, status: stage.last) Ci::Stage.new(self, Hash[%i[name status warnings].zip(stage)])
end end
end end
......
...@@ -8,10 +8,11 @@ module Ci ...@@ -8,10 +8,11 @@ module Ci
delegate :project, to: :pipeline delegate :project, to: :pipeline
def initialize(pipeline, name:, status: nil) def initialize(pipeline, name:, status: nil, warnings: nil)
@pipeline = pipeline @pipeline = pipeline
@name = name @name = name
@status = status @status = status
@warnings = warnings
end end
def to_param def to_param
...@@ -39,5 +40,17 @@ module Ci ...@@ -39,5 +40,17 @@ module Ci
def builds def builds
@builds ||= pipeline.builds.where(stage: name) @builds ||= pipeline.builds.where(stage: name)
end end
def success?
status.to_s == 'success'
end
def has_warnings?
if @warnings.nil?
statuses.latest.failed_but_allowed.any?
else
@warnings
end
end
end end
end end
module HasStatus module HasStatus
extend ActiveSupport::Concern extend ActiveSupport::Concern
DEFAULT_STATUS = 'created'
AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped] AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped]
STARTED_STATUSES = %w[running success failed skipped] STARTED_STATUSES = %w[running success failed skipped]
ACTIVE_STATUSES = %w[pending running] ACTIVE_STATUSES = %w[pending running]
......
...@@ -133,6 +133,8 @@ class Namespace < ActiveRecord::Base ...@@ -133,6 +133,8 @@ class Namespace < ActiveRecord::Base
Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
Gitlab::PagesTransfer.new.rename_namespace(path_was, path) Gitlab::PagesTransfer.new.rename_namespace(path_was, path)
remove_exports!
# If repositories moved successfully we need to # If repositories moved successfully we need to
# send update instructions to users. # send update instructions to users.
# However we cannot allow rollback since we moved namespace dir # However we cannot allow rollback since we moved namespace dir
...@@ -225,6 +227,8 @@ class Namespace < ActiveRecord::Base ...@@ -225,6 +227,8 @@ class Namespace < ActiveRecord::Base
GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage_path, new_path) GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage_path, new_path)
end end
end end
remove_exports!
end end
def refresh_access_of_projects_invited_groups def refresh_access_of_projects_invited_groups
...@@ -237,4 +241,20 @@ class Namespace < ActiveRecord::Base ...@@ -237,4 +241,20 @@ class Namespace < ActiveRecord::Base
def full_path_changed? def full_path_changed?
path_changed? || parent_id_changed? path_changed? || parent_id_changed?
end end
def remove_exports!
Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete))
end
def export_path
File.join(Gitlab::ImportExport.storage_path, full_path_was)
end
def full_path_was
if parent
parent.full_path + '/' + path_was
else
path_was
end
end
end end
...@@ -25,7 +25,7 @@ You can create a Personal Access Token here: ...@@ -25,7 +25,7 @@ You can create a Personal Access Token here:
http://app.asana.com/-/account_api' http://app.asana.com/-/account_api'
end end
def to_param def self.to_param
'asana' 'asana'
end end
...@@ -44,7 +44,7 @@ http://app.asana.com/-/account_api' ...@@ -44,7 +44,7 @@ http://app.asana.com/-/account_api'
] ]
end end
def supported_events def self.supported_events
%w(push) %w(push)
end end
......
...@@ -12,7 +12,7 @@ class AssemblaService < Service ...@@ -12,7 +12,7 @@ class AssemblaService < Service
'Project Management Software (Source Commits Endpoint)' 'Project Management Software (Source Commits Endpoint)'
end end
def to_param def self.to_param
'assembla' 'assembla'
end end
...@@ -23,7 +23,7 @@ class AssemblaService < Service ...@@ -23,7 +23,7 @@ class AssemblaService < Service
] ]
end end
def supported_events def self.supported_events
%w(push) %w(push)
end end
......
...@@ -40,7 +40,7 @@ class BambooService < CiService ...@@ -40,7 +40,7 @@ class BambooService < CiService
'You must set up automatic revision labeling and a repository trigger in Bamboo.' 'You must set up automatic revision labeling and a repository trigger in Bamboo.'
end end
def to_param def self.to_param
'bamboo' 'bamboo'
end end
...@@ -56,10 +56,6 @@ class BambooService < CiService ...@@ -56,10 +56,6 @@ class BambooService < CiService
] ]
end end
def supported_events
%w(push)
end
def build_page(sha, ref) def build_page(sha, ref)
with_reactive_cache(sha, ref) {|cached| cached[:build_page] } with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
end end
......
...@@ -19,7 +19,7 @@ class BugzillaService < IssueTrackerService ...@@ -19,7 +19,7 @@ class BugzillaService < IssueTrackerService
end end
end end
def to_param def self.to_param
'bugzilla' 'bugzilla'
end end
end end
...@@ -24,10 +24,6 @@ class BuildkiteService < CiService ...@@ -24,10 +24,6 @@ class BuildkiteService < CiService
hook.save hook.save
end end
def supported_events
%w(push)
end
def execute(data) def execute(data)
return unless supported_events.include?(data[:object_kind]) return unless supported_events.include?(data[:object_kind])
...@@ -54,7 +50,7 @@ class BuildkiteService < CiService ...@@ -54,7 +50,7 @@ class BuildkiteService < CiService
'Continuous integration and deployments' 'Continuous integration and deployments'
end end
def to_param def self.to_param
'buildkite' 'buildkite'
end end
......
...@@ -19,11 +19,11 @@ class BuildsEmailService < Service ...@@ -19,11 +19,11 @@ class BuildsEmailService < Service
'Email the builds status to a list of recipients.' 'Email the builds status to a list of recipients.'
end end
def to_param def self.to_param
'builds_email' 'builds_email'
end end
def supported_events def self.supported_events
%w(build) %w(build)
end end
......
...@@ -12,7 +12,7 @@ class CampfireService < Service ...@@ -12,7 +12,7 @@ class CampfireService < Service
'Simple web-based real-time group chat' 'Simple web-based real-time group chat'
end end
def to_param def self.to_param
'campfire' 'campfire'
end end
...@@ -24,7 +24,7 @@ class CampfireService < Service ...@@ -24,7 +24,7 @@ class CampfireService < Service
] ]
end end
def supported_events def self.supported_events
%w(push) %w(push)
end end
......
...@@ -25,7 +25,7 @@ class ChatNotificationService < Service ...@@ -25,7 +25,7 @@ class ChatNotificationService < Service
valid? valid?
end end
def supported_events def self.supported_events
%w[push issue confidential_issue merge_request note tag_push %w[push issue confidential_issue merge_request note tag_push
build pipeline wiki_page] build pipeline wiki_page]
end end
...@@ -82,19 +82,19 @@ class ChatNotificationService < Service ...@@ -82,19 +82,19 @@ class ChatNotificationService < Service
def get_message(object_kind, data) def get_message(object_kind, data)
case object_kind case object_kind
when "push", "tag_push" when "push", "tag_push"
PushMessage.new(data) ChatMessage::PushMessage.new(data)
when "issue" when "issue"
IssueMessage.new(data) unless is_update?(data) ChatMessage::IssueMessage.new(data) unless is_update?(data)
when "merge_request" when "merge_request"
MergeMessage.new(data) unless is_update?(data) ChatMessage::MergeMessage.new(data) unless is_update?(data)
when "note" when "note"
NoteMessage.new(data) ChatMessage::NoteMessage.new(data)
when "build" when "build"
BuildMessage.new(data) if should_build_be_notified?(data) ChatMessage::BuildMessage.new(data) if should_build_be_notified?(data)
when "pipeline" when "pipeline"
PipelineMessage.new(data) if should_pipeline_be_notified?(data) ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
when "wiki_page" when "wiki_page"
WikiPageMessage.new(data) ChatMessage::WikiPageMessage.new(data)
end end
end end
......
...@@ -13,8 +13,8 @@ class ChatSlashCommandsService < Service ...@@ -13,8 +13,8 @@ class ChatSlashCommandsService < Service
ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end end
def supported_events def self.supported_events
[] %w()
end end
def can_test? def can_test?
......
...@@ -8,7 +8,7 @@ class CiService < Service ...@@ -8,7 +8,7 @@ class CiService < Service
self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end end
def supported_events def self.supported_events
%w(push) %w(push)
end end
......
...@@ -23,7 +23,7 @@ class CustomIssueTrackerService < IssueTrackerService ...@@ -23,7 +23,7 @@ class CustomIssueTrackerService < IssueTrackerService
end end
end end
def to_param def self.to_param
'custom_issue_tracker' 'custom_issue_tracker'
end end
......
...@@ -5,8 +5,8 @@ ...@@ -5,8 +5,8 @@
class DeploymentService < Service class DeploymentService < Service
default_value_for :category, 'deployment' default_value_for :category, 'deployment'
def supported_events def self.supported_events
[] %w()
end end
def predefined_variables def predefined_variables
......
...@@ -32,7 +32,7 @@ class DroneCiService < CiService ...@@ -32,7 +32,7 @@ class DroneCiService < CiService
true true
end end
def supported_events def self.supported_events
%w(push merge_request tag_push) %w(push merge_request tag_push)
end end
...@@ -87,7 +87,7 @@ class DroneCiService < CiService ...@@ -87,7 +87,7 @@ class DroneCiService < CiService
'Drone is a Continuous Integration platform built on Docker, written in Go' 'Drone is a Continuous Integration platform built on Docker, written in Go'
end end
def to_param def self.to_param
'drone_ci' 'drone_ci'
end end
......
...@@ -12,11 +12,11 @@ class EmailsOnPushService < Service ...@@ -12,11 +12,11 @@ class EmailsOnPushService < Service
'Email the commits and diff of each push to a list of recipients.' 'Email the commits and diff of each push to a list of recipients.'
end end
def to_param def self.to_param
'emails_on_push' 'emails_on_push'
end end
def supported_events def self.supported_events
%w(push tag_push) %w(push tag_push)
end end
......
...@@ -13,7 +13,7 @@ class ExternalWikiService < Service ...@@ -13,7 +13,7 @@ class ExternalWikiService < Service
'Replaces the link to the internal wiki with a link to an external wiki.' 'Replaces the link to the internal wiki with a link to an external wiki.'
end end
def to_param def self.to_param
'external_wiki' 'external_wiki'
end end
...@@ -29,4 +29,8 @@ class ExternalWikiService < Service ...@@ -29,4 +29,8 @@ class ExternalWikiService < Service
nil nil
end end
end end
def self.supported_events
%w()
end
end end
...@@ -12,7 +12,7 @@ class FlowdockService < Service ...@@ -12,7 +12,7 @@ class FlowdockService < Service
'Flowdock is a collaboration web app for technical teams.' 'Flowdock is a collaboration web app for technical teams.'
end end
def to_param def self.to_param
'flowdock' 'flowdock'
end end
...@@ -22,7 +22,7 @@ class FlowdockService < Service ...@@ -22,7 +22,7 @@ class FlowdockService < Service
] ]
end end
def supported_events def self.supported_events
%w(push) %w(push)
end end
......
...@@ -12,7 +12,7 @@ class GemnasiumService < Service ...@@ -12,7 +12,7 @@ class GemnasiumService < Service
'Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities.' 'Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities.'
end end
def to_param def self.to_param
'gemnasium' 'gemnasium'
end end
...@@ -23,7 +23,7 @@ class GemnasiumService < Service ...@@ -23,7 +23,7 @@ class GemnasiumService < Service
] ]
end end
def supported_events def self.supported_events
%w(push) %w(push)
end end
......
...@@ -7,7 +7,7 @@ class GitlabIssueTrackerService < IssueTrackerService ...@@ -7,7 +7,7 @@ class GitlabIssueTrackerService < IssueTrackerService
default_value_for :default, true default_value_for :default, true
def to_param def self.to_param
'gitlab' 'gitlab'
end end
......
...@@ -27,7 +27,7 @@ class HipchatService < Service ...@@ -27,7 +27,7 @@ class HipchatService < Service
'Private group chat and IM' 'Private group chat and IM'
end end
def to_param def self.to_param
'hipchat' 'hipchat'
end end
...@@ -45,7 +45,7 @@ class HipchatService < Service ...@@ -45,7 +45,7 @@ class HipchatService < Service
] ]
end end
def supported_events def self.supported_events
%w(push issue confidential_issue merge_request note tag_push build) %w(push issue confidential_issue merge_request note tag_push build)
end end
......
...@@ -17,11 +17,11 @@ class IrkerService < Service ...@@ -17,11 +17,11 @@ class IrkerService < Service
'gateway.' 'gateway.'
end end
def to_param def self.to_param
'irker' 'irker'
end end
def supported_events def self.supported_events
%w(push) %w(push)
end end
......
...@@ -61,7 +61,7 @@ class IssueTrackerService < Service ...@@ -61,7 +61,7 @@ class IssueTrackerService < Service
end end
end end
def supported_events def self.supported_events
%w(push) %w(push)
end end
......
...@@ -33,7 +33,7 @@ class JenkinsDeprecatedService < CiService ...@@ -33,7 +33,7 @@ class JenkinsDeprecatedService < CiService
'is deprecated. Use "Jenkins CI" service instead.' 'is deprecated. Use "Jenkins CI" service instead.'
end end
def to_param def self.to_param
'jenkins_deprecated' 'jenkins_deprecated'
end end
......
...@@ -52,7 +52,7 @@ class JenkinsService < CiService ...@@ -52,7 +52,7 @@ class JenkinsService < CiService
File.join(jenkins_url, "project/#{project_name}").to_s File.join(jenkins_url, "project/#{project_name}").to_s
end end
def supported_events def self.supported_events
%w(push merge_request tag_push) %w(push merge_request tag_push)
end end
...@@ -68,7 +68,7 @@ class JenkinsService < CiService ...@@ -68,7 +68,7 @@ class JenkinsService < CiService
'You must have installed the Git Plugin and GitLab Plugin in Jenkins' 'You must have installed the Git Plugin and GitLab Plugin in Jenkins'
end end
def to_param def self.to_param
'jenkins' 'jenkins'
end end
......
...@@ -12,7 +12,7 @@ class JiraService < IssueTrackerService ...@@ -12,7 +12,7 @@ class JiraService < IssueTrackerService
# This is confusing, but JiraService does not really support these events. # This is confusing, but JiraService does not really support these events.
# The values here are required to display correct options in the service # The values here are required to display correct options in the service
# configuration screen. # configuration screen.
def supported_events def self.supported_events
%w(commit merge_request) %w(commit merge_request)
end end
...@@ -81,7 +81,7 @@ class JiraService < IssueTrackerService ...@@ -81,7 +81,7 @@ class JiraService < IssueTrackerService
end end
end end
def to_param def self.to_param
'jira' 'jira'
end end
......
...@@ -52,7 +52,7 @@ class KubernetesService < DeploymentService ...@@ -52,7 +52,7 @@ class KubernetesService < DeploymentService
'deployments with `app=$CI_ENVIRONMENT_SLUG`' 'deployments with `app=$CI_ENVIRONMENT_SLUG`'
end end
def to_param def self.to_param
'kubernetes' 'kubernetes'
end end
......
...@@ -7,7 +7,7 @@ class MattermostService < ChatNotificationService ...@@ -7,7 +7,7 @@ class MattermostService < ChatNotificationService
'Receive event notifications in Mattermost' 'Receive event notifications in Mattermost'
end end
def to_param def self.to_param
'mattermost' 'mattermost'
end end
...@@ -36,6 +36,6 @@ class MattermostService < ChatNotificationService ...@@ -36,6 +36,6 @@ class MattermostService < ChatNotificationService
end end
def default_channel_placeholder def default_channel_placeholder
"#town-square" "town-square"
end end
end end
...@@ -15,7 +15,7 @@ class MattermostSlashCommandsService < ChatSlashCommandsService ...@@ -15,7 +15,7 @@ class MattermostSlashCommandsService < ChatSlashCommandsService
"Perform common operations on GitLab in Mattermost" "Perform common operations on GitLab in Mattermost"
end end
def to_param def self.to_param
'mattermost_slash_commands' 'mattermost_slash_commands'
end end
......
...@@ -15,11 +15,11 @@ class PipelinesEmailService < Service ...@@ -15,11 +15,11 @@ class PipelinesEmailService < Service
'Email the pipelines status to a list of recipients.' 'Email the pipelines status to a list of recipients.'
end end
def to_param def self.to_param
'pipelines_email' 'pipelines_email'
end end
def supported_events def self.supported_events
%w[pipeline] %w[pipeline]
end end
......
...@@ -14,7 +14,7 @@ class PivotaltrackerService < Service ...@@ -14,7 +14,7 @@ class PivotaltrackerService < Service
'Project Management Software (Source Commits Endpoint)' 'Project Management Software (Source Commits Endpoint)'
end end
def to_param def self.to_param
'pivotaltracker' 'pivotaltracker'
end end
...@@ -34,7 +34,7 @@ class PivotaltrackerService < Service ...@@ -34,7 +34,7 @@ class PivotaltrackerService < Service
] ]
end end
def supported_events def self.supported_events
%w(push) %w(push)
end end
......
...@@ -13,7 +13,7 @@ class PushoverService < Service ...@@ -13,7 +13,7 @@ class PushoverService < Service
'Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop.' 'Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop.'
end end
def to_param def self.to_param
'pushover' 'pushover'
end end
...@@ -61,7 +61,7 @@ class PushoverService < Service ...@@ -61,7 +61,7 @@ class PushoverService < Service
] ]
end end
def supported_events def self.supported_events
%w(push) %w(push)
end end
......
...@@ -19,7 +19,7 @@ class RedmineService < IssueTrackerService ...@@ -19,7 +19,7 @@ class RedmineService < IssueTrackerService
end end
end end
def to_param def self.to_param
'redmine' 'redmine'
end end
end end
...@@ -7,7 +7,7 @@ class SlackService < ChatNotificationService ...@@ -7,7 +7,7 @@ class SlackService < ChatNotificationService
'Receive event notifications in Slack' 'Receive event notifications in Slack'
end end
def to_param def self.to_param
'slack' 'slack'
end end
......
...@@ -9,7 +9,7 @@ class SlackSlashCommandsService < ChatSlashCommandsService ...@@ -9,7 +9,7 @@ class SlackSlashCommandsService < ChatSlashCommandsService
"Perform common operations on GitLab in Slack" "Perform common operations on GitLab in Slack"
end end
def to_param def self.to_param
'slack_slash_commands' 'slack_slash_commands'
end end
......
...@@ -43,14 +43,10 @@ class TeamcityService < CiService ...@@ -43,14 +43,10 @@ class TeamcityService < CiService
'requests build, that setting is in the vsc root advanced settings.' 'requests build, that setting is in the vsc root advanced settings.'
end end
def to_param def self.to_param
'teamcity' 'teamcity'
end end
def supported_events
%w(push)
end
def fields def fields
[ [
{ type: 'text', name: 'teamcity_url', { type: 'text', name: 'teamcity_url',
......
...@@ -76,6 +76,11 @@ class Service < ActiveRecord::Base ...@@ -76,6 +76,11 @@ class Service < ActiveRecord::Base
def to_param def to_param
# implement inside child # implement inside child
self.class.to_param
end
def self.to_param
raise NotImplementedError
end end
def fields def fields
...@@ -92,7 +97,11 @@ class Service < ActiveRecord::Base ...@@ -92,7 +97,11 @@ class Service < ActiveRecord::Base
end end
def event_names def event_names
supported_events.map { |event| "#{event}_events" } self.class.event_names
end
def self.event_names
self.supported_events.map { |event| "#{event}_events" }
end end
def event_field(event) def event_field(event)
...@@ -104,6 +113,10 @@ class Service < ActiveRecord::Base ...@@ -104,6 +113,10 @@ class Service < ActiveRecord::Base
end end
def supported_events def supported_events
self.class.supported_events
end
def self.supported_events
%w(push tag_push issue confidential_issue merge_request wiki_page) %w(push tag_push issue confidential_issue merge_request wiki_page)
end end
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
= icon('times') = icon('times')
#js-dropdown-hint.dropdown-menu.hint-dropdown #js-dropdown-hint.dropdown-menu.hint-dropdown
%ul{ 'data-dropdown' => true } %ul{ 'data-dropdown' => true }
%li.filter-dropdown-item{ 'data-value' => '' } %li.filter-dropdown-item{ 'data-action' => 'submit' }
%button.btn.btn-link %button.btn.btn-link
= icon('search') = icon('search')
%span %span
...@@ -139,10 +139,6 @@ ...@@ -139,10 +139,6 @@
new MilestoneSelect(); new MilestoneSelect();
new IssueStatusSelect(); new IssueStatusSelect();
new SubscriptionSelect(); new SubscriptionSelect();
$('form.filter-form').on('submit', function (event) {
event.preventDefault();
Turbolinks.visit(this.action + '&' + $(this).serialize());
});
$(document).off('page:restore').on('page:restore', function (event) { $(document).off('page:restore').on('page:restore', function (event) {
if (gl.FilteredSearchManager) { if (gl.FilteredSearchManager) {
......
---
title: Handle unsubscribe from email notifications via replying to reply+%{key}+unsubscribe@ address
merge_request: 6597
author:
---
title: Allow creating protected branches when user can merge to such branch
merge_request: 8458
author:
---
title: Adds service trigger events to api
merge_request: 8324
author:
---
title: Remove rogue scrollbars for issue comments with inline elements
merge_request:
author:
---
title: Add hover style to copy icon on commit page header
merge_request:
author: Ryan Harris
---
title: Remove blue border from comment box hover
merge_request:
author:
---
title: Ensure export files are removed after a namespace is deleted
merge_request:
author:
---
title: Use warning icon in mini-graph if stage passed conditionally
merge_request: 8503
author:
---
title: Don't allow project guests to subscribe to merge requests through the API
merge_request:
author: Robert Schilling
---
title: Prevent users from creating notes on resources they can't access
merge_request:
author:
---
title: Prevent users from deleting system deploy keys via the project deploy key API
merge_request:
author:
---
title: allow issue filter bar to be operated with mouse only
merge_request: 8681
author:
---
title: Upgrade omniauth gem to 1.3.2
merge_request:
author:
...@@ -9,7 +9,7 @@ code is effective, understandable, and maintainable. ...@@ -9,7 +9,7 @@ code is effective, understandable, and maintainable.
Any developer can, and is encouraged to, perform code review on merge requests Any developer can, and is encouraged to, perform code review on merge requests
of colleagues and contributors. However, the final decision to accept a merge of colleagues and contributors. However, the final decision to accept a merge
request is up to one of our merge request "endbosses", denoted on the request is up to one the project's maintainers, denoted on the
[team page](https://about.gitlab.com/team). [team page](https://about.gitlab.com/team).
## Everyone ## Everyone
...@@ -81,15 +81,15 @@ balance in how deep the reviewer can interfere with the code created by a ...@@ -81,15 +81,15 @@ balance in how deep the reviewer can interfere with the code created by a
reviewee. reviewee.
- Learning how to find the right balance takes time; that is why we have - Learning how to find the right balance takes time; that is why we have
minibosses that become merge request endbosses after some time spent on reviewers that become maintainers after some time spent on reviewing merge
reviewing merge requests. requests.
- Finding bugs and improving code style is important, but thinking about good - Finding bugs and improving code style is important, but thinking about good
design is important as well. Building abstractions and good design is what design is important as well. Building abstractions and good design is what
makes it possible to hide complexity and makes future changes easier. makes it possible to hide complexity and makes future changes easier.
- Asking the reviewee to change the design sometimes means the complete rewrite - Asking the reviewee to change the design sometimes means the complete rewrite
of the contributed code. It's usually a good idea to ask another merge of the contributed code. It's usually a good idea to ask another maintainer or
request endboss before doing it, but have the courage to do it when you reviewer before doing it, but have the courage to do it when you believe it is
believe it is important. important.
- There is a difference in doing things right and doing things right now. - There is a difference in doing things right and doing things right now.
Ideally, we should do the former, but in the real world we need the latter as Ideally, we should do the former, but in the real world we need the latter as
well. A good example is a security fix which should be released as soon as well. A good example is a security fix which should be released as soon as
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
To ensure a merge request does not negatively impact performance of GitLab To ensure a merge request does not negatively impact performance of GitLab
_every_ merge request **must** adhere to the guidelines outlined in this _every_ merge request **must** adhere to the guidelines outlined in this
document. There are no exceptions to this rule unless specifically discussed document. There are no exceptions to this rule unless specifically discussed
with and agreed upon by merge request endbosses and performance specialists. with and agreed upon by backend maintainers and performance specialists.
To measure the impact of a merge request you can use To measure the impact of a merge request you can use
[Sherlock](profiling.md#sherlock). It's also highly recommended that you read [Sherlock](profiling.md#sherlock). It's also highly recommended that you read
...@@ -40,9 +40,9 @@ section below for more information. ...@@ -40,9 +40,9 @@ section below for more information.
about the impact. about the impact.
Sometimes it's hard to assess the impact of a merge request. In this case you Sometimes it's hard to assess the impact of a merge request. In this case you
should ask one of the merge request (mini) endbosses to review your changes. You should ask one of the merge request reviewers to review your changes. You can
can find a list of these endbosses at <https://about.gitlab.com/team/>. An find a list of these reviewers at <https://about.gitlab.com/team/>. A reviewer
endboss in turn can request a performance specialist to review the changes. in turn can request a performance specialist to review the changes.
## Query Counts ## Query Counts
......
...@@ -105,15 +105,19 @@ module API ...@@ -105,15 +105,19 @@ module API
present key.deploy_key, with: Entities::SSHKey present key.deploy_key, with: Entities::SSHKey
end end
desc 'Delete existing deploy key of currently authenticated user' do desc 'Delete deploy key for a project' do
success Key success Key
end end
params do params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key' requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end end
delete ":id/#{path}/:key_id" do delete ":id/#{path}/:key_id" do
key = user_project.deploy_keys.find(params[:key_id]) key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
key.destroy if key
key.destroy
else
not_found!('Deploy Key')
end
end end
end end
end end
......
...@@ -90,6 +90,12 @@ module API ...@@ -90,6 +90,12 @@ module API
MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id) MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id)
end end
def find_merge_request_with_access(id, access_level = :read_merge_request)
merge_request = user_project.merge_requests.find(id)
authorize! access_level, merge_request
merge_request
end
def authenticate! def authenticate!
unauthorized! unless current_user unauthorized! unless current_user
end end
......
...@@ -15,10 +15,8 @@ module API ...@@ -15,10 +15,8 @@ module API
end end
get ":id/merge_requests/:merge_request_id/versions" do get ":id/merge_requests/:merge_request_id/versions" do
merge_request = user_project.merge_requests. merge_request = find_merge_request_with_access(params[:merge_request_id])
find(params[:merge_request_id])
authorize! :read_merge_request, merge_request
present merge_request.merge_request_diffs, with: Entities::MergeRequestDiff present merge_request.merge_request_diffs, with: Entities::MergeRequestDiff
end end
...@@ -34,10 +32,8 @@ module API ...@@ -34,10 +32,8 @@ module API
end end
get ":id/merge_requests/:merge_request_id/versions/:version_id" do get ":id/merge_requests/:merge_request_id/versions/:version_id" do
merge_request = user_project.merge_requests. merge_request = find_merge_request_with_access(params[:merge_request_id])
find(params[:merge_request_id])
authorize! :read_merge_request, merge_request
present merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull present merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull
end end
end end
......
...@@ -119,8 +119,8 @@ module API ...@@ -119,8 +119,8 @@ module API
success Entities::MergeRequest success Entities::MergeRequest
end end
get path do get path do
merge_request = find_project_merge_request(params[:merge_request_id]) merge_request = find_merge_request_with_access(params[:merge_request_id])
authorize! :read_merge_request, merge_request
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
end end
...@@ -128,8 +128,8 @@ module API ...@@ -128,8 +128,8 @@ module API
success Entities::RepoCommit success Entities::RepoCommit
end end
get "#{path}/commits" do get "#{path}/commits" do
merge_request = find_project_merge_request(params[:merge_request_id]) merge_request = find_merge_request_with_access(params[:merge_request_id])
authorize! :read_merge_request, merge_request
present merge_request.commits, with: Entities::RepoCommit present merge_request.commits, with: Entities::RepoCommit
end end
...@@ -137,8 +137,8 @@ module API ...@@ -137,8 +137,8 @@ module API
success Entities::MergeRequestChanges success Entities::MergeRequestChanges
end end
get "#{path}/changes" do get "#{path}/changes" do
merge_request = find_project_merge_request(params[:merge_request_id]) merge_request = find_merge_request_with_access(params[:merge_request_id])
authorize! :read_merge_request, merge_request
present merge_request, with: Entities::MergeRequestChanges, current_user: current_user present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
end end
...@@ -156,8 +156,7 @@ module API ...@@ -156,8 +156,7 @@ module API
:remove_source_branch :remove_source_branch
end end
put path do put path do
merge_request = find_project_merge_request(params.delete(:merge_request_id)) merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request)
authorize! :update_merge_request, merge_request
mr_params = declared_params(include_missing: false) mr_params = declared_params(include_missing: false)
mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present? mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
...@@ -236,10 +235,7 @@ module API ...@@ -236,10 +235,7 @@ module API
use :pagination use :pagination
end end
get "#{path}/comments" do get "#{path}/comments" do
merge_request = find_project_merge_request(params[:merge_request_id]) merge_request = find_merge_request_with_access(params[:merge_request_id])
authorize! :read_merge_request, merge_request
present paginate(merge_request.notes.fresh), with: Entities::MRNote present paginate(merge_request.notes.fresh), with: Entities::MRNote
end end
...@@ -251,8 +247,7 @@ module API ...@@ -251,8 +247,7 @@ module API
requires :note, type: String, desc: 'The text of the comment' requires :note, type: String, desc: 'The text of the comment'
end end
post "#{path}/comments" do post "#{path}/comments" do
merge_request = find_project_merge_request(params[:merge_request_id]) merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note)
authorize! :create_note, merge_request
opts = { opts = {
note: params[:note], note: params[:note],
...@@ -276,7 +271,7 @@ module API ...@@ -276,7 +271,7 @@ module API
use :pagination use :pagination
end end
get "#{path}/closes_issues" do get "#{path}/closes_issues" do
merge_request = find_project_merge_request(params[:merge_request_id]) merge_request = find_merge_request_with_access(params[:merge_request_id])
issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user)) issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
present paginate(issues), with: issue_entity(user_project), current_user: current_user present paginate(issues), with: issue_entity(user_project), current_user: current_user
end end
......
...@@ -70,21 +70,27 @@ module API ...@@ -70,21 +70,27 @@ module API
end end
post ":id/#{noteables_str}/:noteable_id/notes" do post ":id/#{noteables_str}/:noteable_id/notes" do
opts = { opts = {
note: params[:body], note: params[:body],
noteable_type: noteables_str.classify, noteable_type: noteables_str.classify,
noteable_id: params[:noteable_id] noteable_id: params[:noteable_id]
} }
if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user) noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
opts[:created_at] = params[:created_at]
end if can?(current_user, noteable_read_ability_name(noteable), noteable)
if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user)
opts[:created_at] = params[:created_at]
end
note = ::Notes::CreateService.new(user_project, current_user, opts).execute note = ::Notes::CreateService.new(user_project, current_user, opts).execute
if note.valid? if note.valid?
present note, with: Entities::const_get(note.class.name) present note, with: Entities::const_get(note.class.name)
else
not_found!("Note #{note.errors.messages}")
end
else else
not_found!("Note #{note.errors.messages}") not_found!("Note")
end end
end end
......
...@@ -145,7 +145,7 @@ module API ...@@ -145,7 +145,7 @@ module API
name: :room, name: :room,
type: String, type: String,
desc: 'Campfire room' desc: 'Campfire room'
}, }
], ],
'custom-issue-tracker' => [ 'custom-issue-tracker' => [
{ {
...@@ -580,7 +580,38 @@ module API ...@@ -580,7 +580,38 @@ module API
desc: 'Should unstable builds be treated as passing?' desc: 'Should unstable builds be treated as passing?'
} }
] ]
}.freeze }
service_classes = [
AsanaService,
AssemblaService,
BambooService,
BugzillaService,
BuildkiteService,
BuildsEmailService,
CampfireService,
CustomIssueTrackerService,
DroneCiService,
EmailsOnPushService,
ExternalWikiService,
FlowdockService,
GemnasiumService,
HipchatService,
IrkerService,
JiraService,
KubernetesService,
MattermostSlashCommandsService,
SlackSlashCommandsService,
PipelinesEmailService,
PivotaltrackerService,
PushoverService,
RedmineService,
SlackService,
MattermostService,
TeamcityService,
JenkinsService,
JenkinsDeprecatedService
].freeze
trigger_services = { trigger_services = {
'mattermost-slash-commands' => [ 'mattermost-slash-commands' => [
...@@ -614,6 +645,19 @@ module API ...@@ -614,6 +645,19 @@ module API
services.each do |service_slug, settings| services.each do |service_slug, settings|
desc "Set #{service_slug} service for project" desc "Set #{service_slug} service for project"
params do params do
service_classes.each do |service|
event_names = service.try(:event_names) || []
event_names.each do |event_name|
services[service.to_param.tr("_", "-")] << {
required: false,
name: event_name.to_sym,
type: String,
desc: ServicesHelper.service_event_description(event_name)
}
end
end
services.freeze
settings.each do |setting| settings.each do |setting|
if setting[:required] if setting[:required]
requires setting[:name], type: setting[:type], desc: setting[:desc] requires setting[:name], type: setting[:type], desc: setting[:desc]
...@@ -627,7 +671,7 @@ module API ...@@ -627,7 +671,7 @@ module API
service_params = declared_params(include_missing: false).merge(active: true) service_params = declared_params(include_missing: false).merge(active: true)
if service.update_attributes(service_params) if service.update_attributes(service_params)
true present service, with: Entities::ProjectService, include_passwords: current_user.is_admin?
else else
render_api_error!('400 Bad Request', 400) render_api_error!('400 Bad Request', 400)
end end
......
...@@ -3,8 +3,8 @@ module API ...@@ -3,8 +3,8 @@ module API
before { authenticate! } before { authenticate! }
subscribable_types = { subscribable_types = {
'merge_request' => proc { |id| user_project.merge_requests.find(id) }, 'merge_request' => proc { |id| find_merge_request_with_access(id, :update_merge_request) },
'merge_requests' => proc { |id| user_project.merge_requests.find(id) }, 'merge_requests' => proc { |id| find_merge_request_with_access(id, :update_merge_request) },
'issues' => proc { |id| find_project_issue(id) }, 'issues' => proc { |id| find_project_issue(id) },
'labels' => proc { |id| find_project_label(id) }, 'labels' => proc { |id| find_project_label(id) },
} }
......
...@@ -5,7 +5,7 @@ module API ...@@ -5,7 +5,7 @@ module API
before { authenticate! } before { authenticate! }
ISSUABLE_TYPES = { ISSUABLE_TYPES = {
'merge_requests' => ->(id) { user_project.merge_requests.find(id) }, 'merge_requests' => ->(id) { find_merge_request_with_access(id) },
'issues' => ->(id) { find_project_issue(id) } 'issues' => ->(id) { find_project_issue(id) }
} }
......
...@@ -4,8 +4,11 @@ module Gitlab ...@@ -4,8 +4,11 @@ module Gitlab
module Build module Build
class Factory < Status::Factory class Factory < Status::Factory
def self.extended_statuses def self.extended_statuses
[Status::Build::Stop, Status::Build::Play, [[Status::Build::Cancelable,
Status::Build::Cancelable, Status::Build::Retryable] Status::Build::Retryable],
[Status::Build::FailedAllowed,
Status::Build::Play,
Status::Build::Stop]]
end end
def self.common_helpers def self.common_helpers
......
module Gitlab module Gitlab
module Ci module Ci
module Status module Status
module Pipeline module Build
class SuccessWithWarnings < SimpleDelegator class FailedAllowed < SimpleDelegator
include Status::Extended include Status::Extended
def text
'passed'
end
def label def label
'passed with warnings' 'failed (allowed to fail)'
end end
def icon def icon
...@@ -18,11 +14,11 @@ module Gitlab ...@@ -18,11 +14,11 @@ module Gitlab
end end
def group def group
'success_with_warnings' 'failed_with_warnings'
end end
def self.matches?(pipeline, user) def self.matches?(build, user)
pipeline.success? && pipeline.has_warnings? build.failed? && build.allow_failure?
end end
end end
end end
......
...@@ -5,41 +5,46 @@ module Gitlab ...@@ -5,41 +5,46 @@ module Gitlab
def initialize(subject, user) def initialize(subject, user)
@subject = subject @subject = subject
@user = user @user = user
@status = subject.status || HasStatus::DEFAULT_STATUS
end end
def fabricate! def fabricate!
if extended_status if extended_statuses.none?
extended_status.new(core_status)
else
core_status core_status
else
compound_extended_status
end end
end end
def self.extended_statuses def core_status
[] Gitlab::Ci::Status
.const_get(@status.capitalize)
.new(@subject, @user)
.extend(self.class.common_helpers)
end end
def self.common_helpers def compound_extended_status
Module.new extended_statuses.inject(core_status) do |status, extended|
extended.new(status)
end
end end
private def extended_statuses
return @extended_statuses if defined?(@extended_statuses)
def simple_status groups = self.class.extended_statuses.map do |group|
@simple_status ||= @subject.status || :created Array(group).find { |status| status.matches?(@subject, @user) }
end
@extended_statuses = groups.flatten.compact
end end
def core_status def self.extended_statuses
Gitlab::Ci::Status []
.const_get(simple_status.capitalize)
.new(@subject, @user)
.extend(self.class.common_helpers)
end end
def extended_status def self.common_helpers
@extended ||= self.class.extended_statuses.find do |status| Module.new
status.matches?(@subject, @user)
end
end end
end end
end end
......
...@@ -4,7 +4,7 @@ module Gitlab ...@@ -4,7 +4,7 @@ module Gitlab
module Pipeline module Pipeline
class Factory < Status::Factory class Factory < Status::Factory
def self.extended_statuses def self.extended_statuses
[Pipeline::SuccessWithWarnings] [Status::SuccessWarning]
end end
def self.common_helpers def self.common_helpers
......
...@@ -3,6 +3,10 @@ module Gitlab ...@@ -3,6 +3,10 @@ module Gitlab
module Status module Status
module Stage module Stage
class Factory < Status::Factory class Factory < Status::Factory
def self.extended_statuses
[Status::SuccessWarning]
end
def self.common_helpers def self.common_helpers
Status::Stage::Common Status::Stage::Common
end end
......
module Gitlab
module Ci
module Status
##
# Extended status used when pipeline or stage passed conditionally.
# This means that failed jobs that are allowed to fail were present.
#
class SuccessWarning < SimpleDelegator
include Status::Extended
def text
'passed'
end
def label
'passed with warnings'
end
def icon
'icon_status_warning'
end
def group
'success_with_warnings'
end
def self.matches?(subject, user)
subject.success? && subject.has_warnings?
end
end
end
end
end
require 'gitlab/email/handler/create_note_handler' require 'gitlab/email/handler/create_note_handler'
require 'gitlab/email/handler/create_issue_handler' require 'gitlab/email/handler/create_issue_handler'
require 'gitlab/email/handler/unsubscribe_handler'
module Gitlab module Gitlab
module Email module Email
module Handler module Handler
HANDLERS = [CreateNoteHandler, CreateIssueHandler] HANDLERS = [UnsubscribeHandler, CreateNoteHandler, CreateIssueHandler]
def self.for(mail, mail_key) def self.for(mail, mail_key)
HANDLERS.find do |klass| HANDLERS.find do |klass|
......
...@@ -9,52 +9,13 @@ module Gitlab ...@@ -9,52 +9,13 @@ module Gitlab
@mail_key = mail_key @mail_key = mail_key
end end
def message def can_execute?
@message ||= process_message
end
def author
raise NotImplementedError raise NotImplementedError
end end
def project def execute
raise NotImplementedError raise NotImplementedError
end end
private
def validate_permission!(permission)
raise UserNotFoundError unless author
raise UserBlockedError if author.blocked?
raise ProjectNotFound unless author.can?(:read_project, project)
raise UserNotAuthorizedError unless author.can?(permission, project)
end
def process_message
message = ReplyParser.new(mail).execute.strip
add_attachments(message)
end
def add_attachments(reply)
attachments = Email::AttachmentUploader.new(mail).execute(project)
reply + attachments.map do |link|
"\n\n#{link[:markdown]}"
end.join
end
def verify_record!(record:, invalid_exception:, record_name:)
return if record.persisted?
return if record.errors.key?(:commands_only)
error_title = "The #{record_name} could not be created for the following reasons:"
msg = error_title + record.errors.full_messages.map do |error|
"\n\n- #{error}"
end.join
raise invalid_exception, msg
end
end end
end end
end end
......
...@@ -5,6 +5,7 @@ module Gitlab ...@@ -5,6 +5,7 @@ module Gitlab
module Email module Email
module Handler module Handler
class CreateIssueHandler < BaseHandler class CreateIssueHandler < BaseHandler
include ReplyProcessing
attr_reader :project_path, :incoming_email_token attr_reader :project_path, :incoming_email_token
def initialize(mail, mail_key) def initialize(mail, mail_key)
......
require 'gitlab/email/handler/base_handler' require 'gitlab/email/handler/base_handler'
require 'gitlab/email/handler/reply_processing'
module Gitlab module Gitlab
module Email module Email
module Handler module Handler
class CreateNoteHandler < BaseHandler class CreateNoteHandler < BaseHandler
include ReplyProcessing
def can_handle? def can_handle?
mail_key =~ /\A\w+\z/ mail_key =~ /\A\w+\z/
end end
...@@ -24,6 +27,8 @@ module Gitlab ...@@ -24,6 +27,8 @@ module Gitlab
record_name: 'comment') record_name: 'comment')
end end
private
def author def author
sent_notification.recipient sent_notification.recipient
end end
...@@ -36,8 +41,6 @@ module Gitlab ...@@ -36,8 +41,6 @@ module Gitlab
@sent_notification ||= SentNotification.for(mail_key) @sent_notification ||= SentNotification.for(mail_key)
end end
private
def create_note def create_note
Notes::CreateService.new( Notes::CreateService.new(
project, project,
......
module Gitlab
module Email
module Handler
module ReplyProcessing
private
def author
raise NotImplementedError
end
def project
raise NotImplementedError
end
def message
@message ||= process_message
end
def process_message
message = ReplyParser.new(mail).execute.strip
add_attachments(message)
end
def add_attachments(reply)
attachments = Email::AttachmentUploader.new(mail).execute(project)
reply + attachments.map do |link|
"\n\n#{link[:markdown]}"
end.join
end
def validate_permission!(permission)
raise UserNotFoundError unless author
raise UserBlockedError if author.blocked?
raise ProjectNotFound unless author.can?(:read_project, project)
raise UserNotAuthorizedError unless author.can?(permission, project)
end
def verify_record!(record:, invalid_exception:, record_name:)
return if record.persisted?
return if record.errors.key?(:commands_only)
error_title = "The #{record_name} could not be created for the following reasons:"
msg = error_title + record.errors.full_messages.map do |error|
"\n\n- #{error}"
end.join
raise invalid_exception, msg
end
end
end
end
end
require 'gitlab/email/handler/base_handler'
module Gitlab
module Email
module Handler
class UnsubscribeHandler < BaseHandler
def can_handle?
mail_key =~ /\A\w+#{Regexp.escape(Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX)}\z/
end
def execute
raise SentNotificationNotFoundError unless sent_notification
return unless sent_notification.unsubscribable?
noteable = sent_notification.noteable
raise NoteableNotFoundError unless noteable
noteable.unsubscribe(sent_notification.recipient)
end
private
def sent_notification
@sent_notification ||= SentNotification.for(reply_key)
end
def reply_key
mail_key.sub(Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX, '')
end
end
end
end
end
module Gitlab module Gitlab
module IncomingEmail module IncomingEmail
UNSUBSCRIBE_SUFFIX = '+unsubscribe'.freeze
WILDCARD_PLACEHOLDER = '%{key}'.freeze WILDCARD_PLACEHOLDER = '%{key}'.freeze
class << self class << self
...@@ -18,7 +19,11 @@ module Gitlab ...@@ -18,7 +19,11 @@ module Gitlab
end end
def reply_address(key) def reply_address(key)
config.address.gsub(WILDCARD_PLACEHOLDER, key) config.address.sub(WILDCARD_PLACEHOLDER, key)
end
def unsubscribe_address(key)
config.address.sub(WILDCARD_PLACEHOLDER, "#{key}#{UNSUBSCRIBE_SUFFIX}")
end end
def key_from_address(address) def key_from_address(address)
...@@ -49,7 +54,7 @@ module Gitlab ...@@ -49,7 +54,7 @@ module Gitlab
return nil unless wildcard_address return nil unless wildcard_address
regex = Regexp.escape(wildcard_address) regex = Regexp.escape(wildcard_address)
regex = regex.gsub(Regexp.escape('%{key}'), "(.+)") regex = regex.sub(Regexp.escape(WILDCARD_PLACEHOLDER), '(.+)')
Regexp.new(regex).freeze Regexp.new(regex).freeze
end end
end end
......
...@@ -35,7 +35,9 @@ module Gitlab ...@@ -35,7 +35,9 @@ module Gitlab
return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user) return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user)
access_levels = project.protected_branches.matching(ref).map(&:push_access_levels).flatten access_levels = project.protected_branches.matching(ref).map(&:push_access_levels).flatten
access_levels.any? { |access_level| access_level.check_access(user) } has_access = access_levels.any? { |access_level| access_level.check_access(user) }
has_access || !project.repository.branch_exists?(ref) && can_merge_to_branch?(ref)
else else
user.can?(:push_code, project) user.can?(:push_code, project)
end end
......
...@@ -54,6 +54,7 @@ describe Projects::ServicesController do ...@@ -54,6 +54,7 @@ describe Projects::ServicesController do
context 'on successful update' do context 'on successful update' do
it 'sets the flash' do it 'sets the flash' do
expect(service).to receive(:to_param).and_return('hipchat') expect(service).to receive(:to_param).and_return('hipchat')
expect(service).to receive(:event_names).and_return(HipchatService.event_names)
put :update, put :update,
namespace_id: project.namespace.id, namespace_id: project.namespace.id,
......
...@@ -3,11 +3,12 @@ FactoryGirl.define do ...@@ -3,11 +3,12 @@ FactoryGirl.define do
transient do transient do
name 'test' name 'test'
status nil status nil
warnings nil
pipeline factory: :ci_empty_pipeline pipeline factory: :ci_empty_pipeline
end end
initialize_with do initialize_with do
Ci::Stage.new(pipeline, name: name, status: status) Ci::Stage.new(pipeline, name: name, status: status, warnings: warnings)
end end
end end
end end
...@@ -10,7 +10,7 @@ describe "Admin::Projects", feature: true do ...@@ -10,7 +10,7 @@ describe "Admin::Projects", feature: true do
end end
describe "GET /admin/projects" do describe "GET /admin/projects" do
let!(:archived_project) { create :project, :public, archived: true } let!(:archived_project) { create :project, :public, :archived }
before do before do
visit admin_projects_path visit admin_projects_path
......
...@@ -7,7 +7,7 @@ feature 'Group merge requests page', feature: true do ...@@ -7,7 +7,7 @@ feature 'Group merge requests page', feature: true do
include_examples 'project features apply to issuables', MergeRequest include_examples 'project features apply to issuables', MergeRequest
context 'archived issuable' do context 'archived issuable' do
let(:project_archived) { create(:project, group: group, merge_requests_access_level: ProjectFeature::ENABLED, archived: true) } let(:project_archived) { create(:project, :archived, group: group, merge_requests_access_level: ProjectFeature::ENABLED) }
let(:issuable_archived) { create(:merge_request, source_project: project_archived, target_project: project_archived, title: 'issuable of an archived project') } let(:issuable_archived) { create(:merge_request, source_project: project_archived, target_project: project_archived, title: 'issuable of an archived project') }
let(:access_level) { ProjectFeature::ENABLED } let(:access_level) { ProjectFeature::ENABLED }
let(:user) { user_in_group } let(:user) { user_in_group }
......
...@@ -134,14 +134,14 @@ describe 'Dropdown assignee', js: true, feature: true do ...@@ -134,14 +134,14 @@ describe 'Dropdown assignee', js: true, feature: true do
click_button 'Assigned to me' click_button 'Assigned to me'
end end
expect(filtered_search.value).to eq("assignee:#{user.to_reference}") expect(filtered_search.value).to eq("assignee:#{user.to_reference} ")
end end
it 'fills in the assignee username when the assignee has not been filtered' do it 'fills in the assignee username when the assignee has not been filtered' do
click_assignee(user_jacob.name) click_assignee(user_jacob.name)
expect(page).to have_css(js_dropdown_assignee, visible: false) expect(page).to have_css(js_dropdown_assignee, visible: false)
expect(filtered_search.value).to eq("assignee:@#{user_jacob.username}") expect(filtered_search.value).to eq("assignee:@#{user_jacob.username} ")
end end
it 'fills in the assignee username when the assignee has been filtered' do it 'fills in the assignee username when the assignee has been filtered' do
...@@ -149,14 +149,14 @@ describe 'Dropdown assignee', js: true, feature: true do ...@@ -149,14 +149,14 @@ describe 'Dropdown assignee', js: true, feature: true do
click_assignee(user.name) click_assignee(user.name)
expect(page).to have_css(js_dropdown_assignee, visible: false) expect(page).to have_css(js_dropdown_assignee, visible: false)
expect(filtered_search.value).to eq("assignee:@#{user.username}") expect(filtered_search.value).to eq("assignee:@#{user.username} ")
end end
it 'selects `no assignee`' do it 'selects `no assignee`' do
find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click
expect(page).to have_css(js_dropdown_assignee, visible: false) expect(page).to have_css(js_dropdown_assignee, visible: false)
expect(filtered_search.value).to eq("assignee:none") expect(filtered_search.value).to eq("assignee:none ")
end end
end end
......
...@@ -121,14 +121,14 @@ describe 'Dropdown author', js: true, feature: true do ...@@ -121,14 +121,14 @@ describe 'Dropdown author', js: true, feature: true do
click_author(user_jacob.name) click_author(user_jacob.name)
expect(page).to have_css(js_dropdown_author, visible: false) expect(page).to have_css(js_dropdown_author, visible: false)
expect(filtered_search.value).to eq("author:@#{user_jacob.username}") expect(filtered_search.value).to eq("author:@#{user_jacob.username} ")
end end
it 'fills in the author username when the author has been filtered' do it 'fills in the author username when the author has been filtered' do
click_author(user.name) click_author(user.name)
expect(page).to have_css(js_dropdown_author, visible: false) expect(page).to have_css(js_dropdown_author, visible: false)
expect(filtered_search.value).to eq("author:@#{user.username}") expect(filtered_search.value).to eq("author:@#{user.username} ")
end end
end end
......
...@@ -159,7 +159,7 @@ describe 'Dropdown label', js: true, feature: true do ...@@ -159,7 +159,7 @@ describe 'Dropdown label', js: true, feature: true do
click_label(bug_label.title) click_label(bug_label.title)
expect(page).to have_css(js_dropdown_label, visible: false) expect(page).to have_css(js_dropdown_label, visible: false)
expect(filtered_search.value).to eq("label:~#{bug_label.title}") expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
end end
it 'fills in the label name when the label is partially filled' do it 'fills in the label name when the label is partially filled' do
...@@ -167,49 +167,49 @@ describe 'Dropdown label', js: true, feature: true do ...@@ -167,49 +167,49 @@ describe 'Dropdown label', js: true, feature: true do
click_label(bug_label.title) click_label(bug_label.title)
expect(page).to have_css(js_dropdown_label, visible: false) expect(page).to have_css(js_dropdown_label, visible: false)
expect(filtered_search.value).to eq("label:~#{bug_label.title}") expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
end end
it 'fills in the label name that contains multiple words' do it 'fills in the label name that contains multiple words' do
click_label(two_words_label.title) click_label(two_words_label.title)
expect(page).to have_css(js_dropdown_label, visible: false) expect(page).to have_css(js_dropdown_label, visible: false)
expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\"") expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\" ")
end end
it 'fills in the label name that contains multiple words and is very long' do it 'fills in the label name that contains multiple words and is very long' do
click_label(long_label.title) click_label(long_label.title)
expect(page).to have_css(js_dropdown_label, visible: false) expect(page).to have_css(js_dropdown_label, visible: false)
expect(filtered_search.value).to eq("label:~\"#{long_label.title}\"") expect(filtered_search.value).to eq("label:~\"#{long_label.title}\" ")
end end
it 'fills in the label name that contains double quotes' do it 'fills in the label name that contains double quotes' do
click_label(wont_fix_label.title) click_label(wont_fix_label.title)
expect(page).to have_css(js_dropdown_label, visible: false) expect(page).to have_css(js_dropdown_label, visible: false)
expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}'") expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}' ")
end end
it 'fills in the label name with the correct capitalization' do it 'fills in the label name with the correct capitalization' do
click_label(uppercase_label.title) click_label(uppercase_label.title)
expect(page).to have_css(js_dropdown_label, visible: false) expect(page).to have_css(js_dropdown_label, visible: false)
expect(filtered_search.value).to eq("label:~#{uppercase_label.title}") expect(filtered_search.value).to eq("label:~#{uppercase_label.title} ")
end end
it 'fills in the label name with special characters' do it 'fills in the label name with special characters' do
click_label(special_label.title) click_label(special_label.title)
expect(page).to have_css(js_dropdown_label, visible: false) expect(page).to have_css(js_dropdown_label, visible: false)
expect(filtered_search.value).to eq("label:~#{special_label.title}") expect(filtered_search.value).to eq("label:~#{special_label.title} ")
end end
it 'selects `no label`' do it 'selects `no label`' do
find('#js-dropdown-label .filter-dropdown-item', text: 'No Label').click find('#js-dropdown-label .filter-dropdown-item', text: 'No Label').click
expect(page).to have_css(js_dropdown_label, visible: false) expect(page).to have_css(js_dropdown_label, visible: false)
expect(filtered_search.value).to eq("label:none") expect(filtered_search.value).to eq("label:none ")
end end
end end
......
...@@ -127,7 +127,7 @@ describe 'Dropdown milestone', js: true, feature: true do ...@@ -127,7 +127,7 @@ describe 'Dropdown milestone', js: true, feature: true do
click_milestone(milestone.title) click_milestone(milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false) expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%#{milestone.title}") expect(filtered_search.value).to eq("milestone:%#{milestone.title} ")
end end
it 'fills in the milestone name when the milestone is partially filled' do it 'fills in the milestone name when the milestone is partially filled' do
...@@ -135,56 +135,56 @@ describe 'Dropdown milestone', js: true, feature: true do ...@@ -135,56 +135,56 @@ describe 'Dropdown milestone', js: true, feature: true do
click_milestone(milestone.title) click_milestone(milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false) expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%#{milestone.title}") expect(filtered_search.value).to eq("milestone:%#{milestone.title} ")
end end
it 'fills in the milestone name that contains multiple words' do it 'fills in the milestone name that contains multiple words' do
click_milestone(two_words_milestone.title) click_milestone(two_words_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false) expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%\"#{two_words_milestone.title}\"") expect(filtered_search.value).to eq("milestone:%\"#{two_words_milestone.title}\" ")
end end
it 'fills in the milestone name that contains multiple words and is very long' do it 'fills in the milestone name that contains multiple words and is very long' do
click_milestone(long_milestone.title) click_milestone(long_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false) expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%\"#{long_milestone.title}\"") expect(filtered_search.value).to eq("milestone:%\"#{long_milestone.title}\" ")
end end
it 'fills in the milestone name that contains double quotes' do it 'fills in the milestone name that contains double quotes' do
click_milestone(wont_fix_milestone.title) click_milestone(wont_fix_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false) expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%'#{wont_fix_milestone.title}'") expect(filtered_search.value).to eq("milestone:%'#{wont_fix_milestone.title}' ")
end end
it 'fills in the milestone name with the correct capitalization' do it 'fills in the milestone name with the correct capitalization' do
click_milestone(uppercase_milestone.title) click_milestone(uppercase_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false) expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%#{uppercase_milestone.title}") expect(filtered_search.value).to eq("milestone:%#{uppercase_milestone.title} ")
end end
it 'fills in the milestone name with special characters' do it 'fills in the milestone name with special characters' do
click_milestone(special_milestone.title) click_milestone(special_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false) expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%#{special_milestone.title}") expect(filtered_search.value).to eq("milestone:%#{special_milestone.title} ")
end end
it 'selects `no milestone`' do it 'selects `no milestone`' do
click_static_milestone('No Milestone') click_static_milestone('No Milestone')
expect(page).to have_css(js_dropdown_milestone, visible: false) expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:none") expect(filtered_search.value).to eq("milestone:none ")
end end
it 'selects `upcoming milestone`' do it 'selects `upcoming milestone`' do
click_static_milestone('Upcoming') click_static_milestone('Upcoming')
expect(page).to have_css(js_dropdown_milestone, visible: false) expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:upcoming") expect(filtered_search.value).to eq("milestone:upcoming ")
end end
end end
......
...@@ -62,28 +62,28 @@ describe 'Dropdown weight', js: true, feature: true do ...@@ -62,28 +62,28 @@ describe 'Dropdown weight', js: true, feature: true do
click_weight(1) click_weight(1)
expect(page).to have_css(js_dropdown_weight, visible: false) expect(page).to have_css(js_dropdown_weight, visible: false)
expect(filtered_search.value).to eq("weight:1") expect(filtered_search.value).to eq("weight:1 ")
end end
it 'fills in weight 2' do it 'fills in weight 2' do
click_weight(2) click_weight(2)
expect(page).to have_css(js_dropdown_weight, visible: false) expect(page).to have_css(js_dropdown_weight, visible: false)
expect(filtered_search.value).to eq("weight:2") expect(filtered_search.value).to eq("weight:2 ")
end end
it 'fills in weight 3' do it 'fills in weight 3' do
click_weight(3) click_weight(3)
expect(page).to have_css(js_dropdown_weight, visible: false) expect(page).to have_css(js_dropdown_weight, visible: false)
expect(filtered_search.value).to eq("weight:3") expect(filtered_search.value).to eq("weight:3 ")
end end
it 'fills in `no weight`' do it 'fills in `no weight`' do
click_static_weight('No Weight') click_static_weight('No Weight')
expect(page).to have_css(js_dropdown_weight, visible: false) expect(page).to have_css(js_dropdown_weight, visible: false)
expect(filtered_search.value).to eq("weight:none") expect(filtered_search.value).to eq("weight:none ")
end end
end end
......
...@@ -539,7 +539,7 @@ describe 'Filter issues', js: true, feature: true do ...@@ -539,7 +539,7 @@ describe 'Filter issues', js: true, feature: true do
click_button user2.username click_button user2.username
end end
expect(filtered_search.value).to eq("author:@#{user2.username}") expect(filtered_search.value).to eq("author:@#{user2.username} ")
end end
it 'changes label' do it 'changes label' do
...@@ -551,7 +551,7 @@ describe 'Filter issues', js: true, feature: true do ...@@ -551,7 +551,7 @@ describe 'Filter issues', js: true, feature: true do
click_button label.name click_button label.name
end end
expect(filtered_search.value).to eq("author:@#{user.username} label:~#{label.name}") expect(filtered_search.value).to eq("author:@#{user.username} label:~#{label.name} ")
end end
it 'changes label correctly space is in previous label' do it 'changes label correctly space is in previous label' do
...@@ -563,7 +563,7 @@ describe 'Filter issues', js: true, feature: true do ...@@ -563,7 +563,7 @@ describe 'Filter issues', js: true, feature: true do
click_button label.name click_button label.name
end end
expect(filtered_search.value).to eq("label:~#{label.name}") expect(filtered_search.value).to eq("label:~#{label.name} ")
end end
end end
......
require 'spec_helper'
feature 'Import/Export - Namespace export file cleanup', feature: true, js: true do
let(:export_path) { "#{Dir::tmpdir}/import_file_spec" }
let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
let(:project) { create(:empty_project) }
background do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
end
after do
FileUtils.rm_rf(export_path, secure: true)
end
context 'admin user' do
before do
login_as(:admin)
end
context 'moving the namespace' do
scenario 'removes the export file' do
setup_export_project
old_export_path = project.export_path.dup
expect(File).to exist(old_export_path)
project.namespace.update(path: 'new_path')
expect(File).not_to exist(old_export_path)
end
end
context 'deleting the namespace' do
scenario 'removes the export file' do
setup_export_project
old_export_path = project.export_path.dup
expect(File).to exist(old_export_path)
project.namespace.destroy
expect(File).not_to exist(old_export_path)
end
end
def setup_export_project
visit edit_namespace_project_path(project.namespace, project)
expect(page).to have_content('Export project')
click_link 'Export project'
visit edit_namespace_project_path(project.namespace, project)
expect(page).to have_content('Download export')
end
end
end
...@@ -6,7 +6,7 @@ describe MergeRequestsFinder do ...@@ -6,7 +6,7 @@ describe MergeRequestsFinder do
let(:project1) { create(:project) } let(:project1) { create(:project) }
let(:project2) { create(:project, forked_from_project: project1) } let(:project2) { create(:project, forked_from_project: project1) }
let(:project3) { create(:project, forked_from_project: project1, archived: true) } let(:project3) { create(:project, :archived, forked_from_project: project1) }
let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) } let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) }
let!(:merge_request2) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1, state: 'closed') } let!(:merge_request2) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1, state: 'closed') }
......
...@@ -36,7 +36,7 @@ describe MoveToProjectFinder do ...@@ -36,7 +36,7 @@ describe MoveToProjectFinder do
it 'does not return archived projects' do it 'does not return archived projects' do
reporter_project.team << [user, :reporter] reporter_project.team << [user, :reporter]
reporter_project.update_attributes(archived: true) reporter_project.archive!
other_reporter_project = create(:empty_project) other_reporter_project = create(:empty_project)
other_reporter_project.team << [user, :reporter] other_reporter_project.team << [user, :reporter]
......
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
it('should add tokenName and tokenValue', () => { it('should add tokenName and tokenValue', () => {
gl.FilteredSearchDropdownManager.addWordToInput('label', 'none'); gl.FilteredSearchDropdownManager.addWordToInput('label', 'none');
expect(getInputValue()).toBe('label:none'); expect(getInputValue()).toBe('label:none ');
}); });
}); });
...@@ -45,13 +45,13 @@ ...@@ -45,13 +45,13 @@
it('should replace tokenValue', () => { it('should replace tokenValue', () => {
setInputValue('author:roo'); setInputValue('author:roo');
gl.FilteredSearchDropdownManager.addWordToInput('author', '@root'); gl.FilteredSearchDropdownManager.addWordToInput('author', '@root');
expect(getInputValue()).toBe('author:@root'); expect(getInputValue()).toBe('author:@root ');
}); });
it('should add tokenValues containing spaces', () => { it('should add tokenValues containing spaces', () => {
setInputValue('label:~"test'); setInputValue('label:~"test');
gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\''); gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\'');
expect(getInputValue()).toBe('label:~\'"test me"\''); expect(getInputValue()).toBe('label:~\'"test me"\' ');
}); });
}); });
}); });
......
...@@ -3,15 +3,23 @@ require 'spec_helper' ...@@ -3,15 +3,23 @@ require 'spec_helper'
describe Gitlab::Ci::Status::Build::Factory do describe Gitlab::Ci::Status::Build::Factory do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { build.project } let(:project) { build.project }
let(:status) { factory.fabricate! }
subject { described_class.new(build, user) } let(:factory) { described_class.new(build, user) }
let(:status) { subject.fabricate! }
before { project.team << [user, :developer] } before { project.team << [user, :developer] }
context 'when build is successful' do context 'when build is successful' do
let(:build) { create(:ci_build, :success) } let(:build) { create(:ci_build, :success) }
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Success
end
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::Build::Retryable]
end
it 'fabricates a retryable build status' do it 'fabricates a retryable build status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Retryable expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
end end
...@@ -26,24 +34,72 @@ describe Gitlab::Ci::Status::Build::Factory do ...@@ -26,24 +34,72 @@ describe Gitlab::Ci::Status::Build::Factory do
end end
context 'when build is failed' do context 'when build is failed' do
let(:build) { create(:ci_build, :failed) } context 'when build is not allowed to fail' do
let(:build) { create(:ci_build, :failed) }
it 'fabricates a retryable build status' do it 'matches correct core status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Retryable expect(factory.core_status).to be_a Gitlab::Ci::Status::Failed
end
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::Build::Retryable]
end
it 'fabricates a retryable build status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
end
it 'fabricates status with correct details' do
expect(status.text).to eq 'failed'
expect(status.icon).to eq 'icon_status_failed'
expect(status.label).to eq 'failed'
expect(status).to have_details
expect(status).to have_action
end
end end
it 'fabricates status with correct details' do context 'when build is allowed to fail' do
expect(status.text).to eq 'failed' let(:build) { create(:ci_build, :failed, :allowed_to_fail) }
expect(status.icon).to eq 'icon_status_failed'
expect(status.label).to eq 'failed' it 'matches correct core status' do
expect(status).to have_details expect(factory.core_status).to be_a Gitlab::Ci::Status::Failed
expect(status).to have_action end
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::Build::Retryable,
Gitlab::Ci::Status::Build::FailedAllowed]
end
it 'fabricates a failed but allowed build status' do
expect(status).to be_a Gitlab::Ci::Status::Build::FailedAllowed
end
it 'fabricates status with correct details' do
expect(status.text).to eq 'failed'
expect(status.icon).to eq 'icon_status_warning'
expect(status.label).to eq 'failed (allowed to fail)'
expect(status).to have_details
expect(status).to have_action
expect(status.action_title).to include 'Retry'
expect(status.action_path).to include 'retry'
end
end end
end end
context 'when build is a canceled' do context 'when build is a canceled' do
let(:build) { create(:ci_build, :canceled) } let(:build) { create(:ci_build, :canceled) }
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Canceled
end
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::Build::Retryable]
end
it 'fabricates a retryable build status' do it 'fabricates a retryable build status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Retryable expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
end end
...@@ -60,6 +116,15 @@ describe Gitlab::Ci::Status::Build::Factory do ...@@ -60,6 +116,15 @@ describe Gitlab::Ci::Status::Build::Factory do
context 'when build is running' do context 'when build is running' do
let(:build) { create(:ci_build, :running) } let(:build) { create(:ci_build, :running) }
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Running
end
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::Build::Cancelable]
end
it 'fabricates a canceable build status' do it 'fabricates a canceable build status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable
end end
...@@ -76,6 +141,15 @@ describe Gitlab::Ci::Status::Build::Factory do ...@@ -76,6 +141,15 @@ describe Gitlab::Ci::Status::Build::Factory do
context 'when build is pending' do context 'when build is pending' do
let(:build) { create(:ci_build, :pending) } let(:build) { create(:ci_build, :pending) }
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Pending
end
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::Build::Cancelable]
end
it 'fabricates a cancelable build status' do it 'fabricates a cancelable build status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable
end end
...@@ -92,6 +166,14 @@ describe Gitlab::Ci::Status::Build::Factory do ...@@ -92,6 +166,14 @@ describe Gitlab::Ci::Status::Build::Factory do
context 'when build is skipped' do context 'when build is skipped' do
let(:build) { create(:ci_build, :skipped) } let(:build) { create(:ci_build, :skipped) }
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped
end
it 'does not match extended statuses' do
expect(factory.extended_statuses).to be_empty
end
it 'fabricates a core skipped status' do it 'fabricates a core skipped status' do
expect(status).to be_a Gitlab::Ci::Status::Skipped expect(status).to be_a Gitlab::Ci::Status::Skipped
end end
...@@ -109,6 +191,15 @@ describe Gitlab::Ci::Status::Build::Factory do ...@@ -109,6 +191,15 @@ describe Gitlab::Ci::Status::Build::Factory do
context 'when build is a play action' do context 'when build is a play action' do
let(:build) { create(:ci_build, :playable) } let(:build) { create(:ci_build, :playable) }
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped
end
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::Build::Play]
end
it 'fabricates a core skipped status' do it 'fabricates a core skipped status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Play expect(status).to be_a Gitlab::Ci::Status::Build::Play
end end
...@@ -119,12 +210,22 @@ describe Gitlab::Ci::Status::Build::Factory do ...@@ -119,12 +210,22 @@ describe Gitlab::Ci::Status::Build::Factory do
expect(status.label).to eq 'manual play action' expect(status.label).to eq 'manual play action'
expect(status).to have_details expect(status).to have_details
expect(status).to have_action expect(status).to have_action
expect(status.action_path).to include 'play'
end end
end end
context 'when build is an environment stop action' do context 'when build is an environment stop action' do
let(:build) { create(:ci_build, :playable, :teardown_environment) } let(:build) { create(:ci_build, :playable, :teardown_environment) }
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped
end
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::Build::Stop]
end
it 'fabricates a core skipped status' do it 'fabricates a core skipped status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Stop expect(status).to be_a Gitlab::Ci::Status::Build::Stop
end end
......
require 'spec_helper'
describe Gitlab::Ci::Status::Build::FailedAllowed do
let(:status) { double('core status') }
let(:user) { double('user') }
subject do
described_class.new(status)
end
describe '#text' do
it 'does not override status text' do
expect(status).to receive(:text)
subject.text
end
end
describe '#icon' do
it 'returns a warning icon' do
expect(subject.icon).to eq 'icon_status_warning'
end
end
describe '#label' do
it 'returns information about failed but allowed to fail status' do
expect(subject.label).to eq 'failed (allowed to fail)'
end
end
describe '#group' do
it 'returns status failed with warnings status group' do
expect(subject.group).to eq 'failed_with_warnings'
end
end
describe 'action details' do
describe '#has_action?' do
it 'does not decorate action details' do
expect(status).to receive(:has_action?)
subject.has_action?
end
end
describe '#action_path' do
it 'does not decorate action path' do
expect(status).to receive(:action_path)
subject.action_path
end
end
describe '#action_icon' do
it 'does not decorate action icon' do
expect(status).to receive(:action_icon)
subject.action_icon
end
end
describe '#action_title' do
it 'does not decorate action title' do
expect(status).to receive(:action_title)
subject.action_title
end
end
end
describe '.matches?' do
subject { described_class.matches?(build, user) }
context 'when build is failed' do
context 'when build is allowed to fail' do
let(:build) { create(:ci_build, :failed, :allowed_to_fail) }
it 'is a correct match' do
expect(subject).to be true
end
end
context 'when build is not allowed to fail' do
let(:build) { create(:ci_build, :failed) }
it 'is not a correct match' do
expect(subject).not_to be true
end
end
end
context 'when build did not fail' do
context 'when build is allowed to fail' do
let(:build) { create(:ci_build, :success, :allowed_to_fail) }
it 'is not a correct match' do
expect(subject).not_to be true
end
end
context 'when build is not allowed to fail' do
let(:build) { create(:ci_build, :success) }
it 'is not a correct match' do
expect(subject).not_to be true
end
end
end
end
end
require 'spec_helper' require 'spec_helper'
describe Gitlab::Ci::Status::Factory do describe Gitlab::Ci::Status::Factory do
subject do let(:user) { create(:user) }
described_class.new(resource, user) let(:fabricated_status) { factory.fabricate! }
let(:factory) { described_class.new(resource, user) }
context 'when object has a core status' do
HasStatus::AVAILABLE_STATUSES.each do |simple_status|
context "when simple core status is #{simple_status}" do
let(:resource) { double('resource', status: simple_status) }
let(:expected_status) do
Gitlab::Ci::Status.const_get(simple_status.capitalize)
end
it "fabricates a core status #{simple_status}" do
expect(fabricated_status).to be_a expected_status
end
it "matches a valid core status for #{simple_status}" do
expect(factory.core_status).to be_a expected_status
end
it "does not match any extended statuses for #{simple_status}" do
expect(factory.extended_statuses).to be_empty
end
end
end
end end
let(:user) { create(:user) } context 'when resource supports multiple extended statuses' do
let(:resource) { double('resource', status: :success) }
let(:status) { subject.fabricate! } let(:first_extended_status) do
Class.new(SimpleDelegator) do
def first_method
'first return value'
end
context 'when object has a core status' do def second_method
HasStatus::AVAILABLE_STATUSES.each do |core_status| 'second return value'
context "when core status is #{core_status}" do end
let(:resource) { double(status: core_status) }
def self.matches?(*)
true
end
end
end
it "fabricates a core status #{core_status}" do let(:second_extended_status) do
expect(status).to be_a( Class.new(SimpleDelegator) do
Gitlab::Ci::Status.const_get(core_status.capitalize)) def first_method
'decorated return value'
end end
def third_method
'third return value'
end
def self.matches?(*)
true
end
end
end
shared_examples 'compound decorator factory' do
it 'fabricates compound decorator' do
expect(fabricated_status.first_method).to eq 'decorated return value'
expect(fabricated_status.second_method).to eq 'second return value'
expect(fabricated_status.third_method).to eq 'third return value'
end end
it 'delegates to core status' do
expect(fabricated_status.text).to eq 'passed'
end
it 'latest matches status becomes a status name' do
expect(fabricated_status.class).to eq second_extended_status
end
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Success
end
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
.to eq [first_extended_status, second_extended_status]
end
end
context 'when exclusive statuses are matches' do
before do
allow(described_class).to receive(:extended_statuses)
.and_return([[first_extended_status, second_extended_status]])
end
it 'does not fabricate compound decorator' do
expect(fabricated_status.first_method).to eq 'first return value'
expect(fabricated_status.second_method).to eq 'second return value'
expect(fabricated_status).not_to respond_to(:third_method)
end
it 'delegates to core status' do
expect(fabricated_status.text).to eq 'passed'
end
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Success
end
it 'matches correct extended statuses' do
expect(factory.extended_statuses).to eq [first_extended_status]
end
end
context 'when exclusive statuses are not matched' do
before do
allow(described_class).to receive(:extended_statuses)
.and_return([[first_extended_status], [second_extended_status]])
end
it_behaves_like 'compound decorator factory'
end
context 'when using simplified status grouping' do
before do
allow(described_class).to receive(:extended_statuses)
.and_return([first_extended_status, second_extended_status])
end
it_behaves_like 'compound decorator factory'
end end
end end
end end
...@@ -3,29 +3,32 @@ require 'spec_helper' ...@@ -3,29 +3,32 @@ require 'spec_helper'
describe Gitlab::Ci::Status::Pipeline::Factory do describe Gitlab::Ci::Status::Pipeline::Factory do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { pipeline.project } let(:project) { pipeline.project }
let(:status) { factory.fabricate! }
subject do let(:factory) { described_class.new(pipeline, user) }
described_class.new(pipeline, user)
end
let(:status) do
subject.fabricate!
end
before do before do
project.team << [user, :developer] project.team << [user, :developer]
end end
context 'when pipeline has a core status' do context 'when pipeline has a core status' do
HasStatus::AVAILABLE_STATUSES.each do |core_status| HasStatus::AVAILABLE_STATUSES.each do |simple_status|
context "when core status is #{core_status}" do context "when core status is #{simple_status}" do
let(:pipeline) do let(:pipeline) { create(:ci_pipeline, status: simple_status) }
create(:ci_pipeline, status: core_status)
let(:expected_status) do
Gitlab::Ci::Status.const_get(simple_status.capitalize)
end
it "matches correct core status for #{simple_status}" do
expect(factory.core_status).to be_a expected_status
end end
it "fabricates a core status #{core_status}" do it 'does not matche extended statuses' do
expect(status).to be_a( expect(factory.extended_statuses).to be_empty
Gitlab::Ci::Status.const_get(core_status.capitalize)) end
it "fabricates a core status #{simple_status}" do
expect(status).to be_a expected_status
end end
it 'extends core status with common pipeline methods' do it 'extends core status with common pipeline methods' do
...@@ -47,13 +50,22 @@ describe Gitlab::Ci::Status::Pipeline::Factory do ...@@ -47,13 +50,22 @@ describe Gitlab::Ci::Status::Pipeline::Factory do
create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline) create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline)
end end
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Success
end
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::SuccessWarning]
end
it 'fabricates extended "success with warnings" status' do it 'fabricates extended "success with warnings" status' do
expect(status) expect(status).to be_a Gitlab::Ci::Status::SuccessWarning
.to be_a Gitlab::Ci::Status::Pipeline::SuccessWithWarnings
end end
it 'extends core status with common pipeline methods' do it 'extends core status with common pipeline method' do
expect(status).to have_details expect(status).to have_details
expect(status.details_path).to include "pipelines/#{pipeline.id}"
end end
end end
end end
...@@ -43,4 +43,25 @@ describe Gitlab::Ci::Status::Stage::Factory do ...@@ -43,4 +43,25 @@ describe Gitlab::Ci::Status::Stage::Factory do
end end
end end
end end
context 'when stage has warnings' do
let(:stage) do
build(:ci_stage, name: 'test', status: :success, pipeline: pipeline)
end
before do
create(:ci_build, :allowed_to_fail, :failed,
stage: 'test', pipeline: stage.pipeline)
end
it 'fabricates extended "success with warnings" status' do
expect(status)
.to be_a Gitlab::Ci::Status::SuccessWarning
end
it 'extends core status with common stage method' do
expect(status).to have_details
expect(status.details_path).to include "pipelines/#{pipeline.id}##{stage.name}"
end
end
end end
require 'spec_helper' require 'spec_helper'
describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do describe Gitlab::Ci::Status::SuccessWarning do
subject do subject do
described_class.new(double('status')) described_class.new(double('status'))
end end
...@@ -22,46 +22,52 @@ describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do ...@@ -22,46 +22,52 @@ describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do
end end
describe '.matches?' do describe '.matches?' do
context 'when pipeline is successful' do let(:matchable) { double('matchable') }
let(:pipeline) do
create(:ci_pipeline, status: :success) context 'when matchable subject is successful' do
before do
allow(matchable).to receive(:success?).and_return(true)
end end
context 'when pipeline has warnings' do context 'when matchable subject has warnings' do
before do before do
allow(pipeline).to receive(:has_warnings?).and_return(true) allow(matchable).to receive(:has_warnings?).and_return(true)
end end
it 'is a correct match' do it 'is a correct match' do
expect(described_class.matches?(pipeline, double)).to eq true expect(described_class.matches?(matchable, double)).to eq true
end end
end end
context 'when pipeline does not have warnings' do context 'when matchable subject does not have warnings' do
before do
allow(matchable).to receive(:has_warnings?).and_return(false)
end
it 'does not match' do it 'does not match' do
expect(described_class.matches?(pipeline, double)).to eq false expect(described_class.matches?(matchable, double)).to eq false
end end
end end
end end
context 'when pipeline is not successful' do context 'when matchable subject is not successful' do
let(:pipeline) do before do
create(:ci_pipeline, status: :skipped) allow(matchable).to receive(:success?).and_return(false)
end end
context 'when pipeline has warnings' do context 'when matchable subject has warnings' do
before do before do
allow(pipeline).to receive(:has_warnings?).and_return(true) allow(matchable).to receive(:has_warnings?).and_return(true)
end end
it 'does not match' do it 'does not match' do
expect(described_class.matches?(pipeline, double)).to eq false expect(described_class.matches?(matchable, double)).to eq false
end end
end end
context 'when pipeline does not have warnings' do context 'when matchable subject does not have warnings' do
it 'does not match' do it 'does not match' do
expect(described_class.matches?(pipeline, double)).to eq false expect(described_class.matches?(matchable, double)).to eq false
end end
end end
end end
......
...@@ -18,7 +18,7 @@ shared_context :email_shared_context do ...@@ -18,7 +18,7 @@ shared_context :email_shared_context do
end end
end end
shared_examples :email_shared_examples do shared_examples :reply_processing_shared_examples do
context "when the user could not be found" do context "when the user could not be found" do
before do before do
user.destroy user.destroy
......
...@@ -3,7 +3,7 @@ require_relative '../email_shared_blocks' ...@@ -3,7 +3,7 @@ require_relative '../email_shared_blocks'
describe Gitlab::Email::Handler::CreateIssueHandler, lib: true do describe Gitlab::Email::Handler::CreateIssueHandler, lib: true do
include_context :email_shared_context include_context :email_shared_context
it_behaves_like :email_shared_examples it_behaves_like :reply_processing_shared_examples
before do before do
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo") stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
......
...@@ -3,7 +3,7 @@ require_relative '../email_shared_blocks' ...@@ -3,7 +3,7 @@ require_relative '../email_shared_blocks'
describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
include_context :email_shared_context include_context :email_shared_context
it_behaves_like :email_shared_examples it_behaves_like :reply_processing_shared_examples
before do before do
stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo") stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo")
......
require 'spec_helper'
require_relative '../email_shared_blocks'
describe Gitlab::Email::Handler::UnsubscribeHandler, lib: true do
include_context :email_shared_context
before do
stub_incoming_email_setting(enabled: true, address: 'reply+%{key}@appmail.adventuretime.ooo')
stub_config_setting(host: 'localhost')
end
let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "#{mail_key}+unsubscribe") }
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
let(:noteable) { create(:issue, project: project) }
let!(:sent_notification) { SentNotification.record(noteable, user.id, mail_key) }
context 'when notification concerns a commit' do
let(:commit) { create(:commit, project: project) }
let!(:sent_notification) { SentNotification.record(commit, user.id, mail_key) }
it 'handler does not raise an error' do
expect { receiver.execute }.not_to raise_error
end
end
context 'user is unsubscribed' do
it 'leaves user unsubscribed' do
expect { receiver.execute }.not_to change { noteable.subscribed?(user) }.from(false)
end
end
context 'user is subscribed' do
before do
noteable.subscribe(user)
end
it 'unsubscribes user from notable' do
expect { receiver.execute }.to change { noteable.subscribed?(user) }.from(true).to(false)
end
end
context 'when the noteable could not be found' do
before do
noteable.destroy
end
it 'raises a NoteableNotFoundError' do
expect { receiver.execute }.to raise_error(Gitlab::Email::NoteableNotFoundError)
end
end
context 'when no sent notification for the mail key could be found' do
let(:email_raw) { fixture_file('emails/wrong_mail_key.eml') }
it 'raises a SentNotificationNotFoundError' do
expect { receiver.execute }.to raise_error(Gitlab::Email::SentNotificationNotFoundError)
end
end
end
...@@ -23,6 +23,48 @@ describe Gitlab::IncomingEmail, lib: true do ...@@ -23,6 +23,48 @@ describe Gitlab::IncomingEmail, lib: true do
end end
end end
describe 'self.supports_wildcard?' do
context 'address contains the wildard placeholder' do
before do
stub_incoming_email_setting(address: 'replies+%{key}@example.com')
end
it 'confirms that wildcard is supported' do
expect(described_class.supports_wildcard?).to be_truthy
end
end
context "address doesn't contain the wildcard placeholder" do
before do
stub_incoming_email_setting(address: 'replies@example.com')
end
it 'returns that wildcard is not supported' do
expect(described_class.supports_wildcard?).to be_falsey
end
end
context 'address is not set' do
before do
stub_incoming_email_setting(address: nil)
end
it 'returns that wildard is not supported' do
expect(described_class.supports_wildcard?).to be_falsey
end
end
end
context 'self.unsubscribe_address' do
before do
stub_incoming_email_setting(address: 'replies+%{key}@example.com')
end
it 'returns the address with interpolated reply key and unsubscribe suffix' do
expect(described_class.unsubscribe_address('key')).to eq('replies+key+unsubscribe@example.com')
end
end
context "self.reply_address" do context "self.reply_address" do
before do before do
stub_incoming_email_setting(address: "replies+%{key}@example.com") stub_incoming_email_setting(address: "replies+%{key}@example.com")
......
...@@ -66,7 +66,8 @@ describe Gitlab::UserAccess, lib: true do ...@@ -66,7 +66,8 @@ describe Gitlab::UserAccess, lib: true do
end end
describe 'push to protected branch' do describe 'push to protected branch' do
let(:branch) { create :protected_branch, project: project } let(:branch) { create :protected_branch, project: project, name: "test" }
let(:not_existing_branch) { create :protected_branch, :developers_can_merge, project: project }
it 'returns true if user is a master' do it 'returns true if user is a master' do
project.team << [user, :master] project.team << [user, :master]
...@@ -85,6 +86,12 @@ describe Gitlab::UserAccess, lib: true do ...@@ -85,6 +86,12 @@ describe Gitlab::UserAccess, lib: true do
expect(access.can_push_to_branch?(branch.name)).to be_falsey expect(access.can_push_to_branch?(branch.name)).to be_falsey
end end
it 'returns true if branch does not exist and user has permission to merge' do
project.team << [user, :developer]
expect(access.can_push_to_branch?(not_existing_branch.name)).to be_truthy
end
end end
describe 'push to protected branch if allowed for developers' do describe 'push to protected branch if allowed for developers' do
......
...@@ -122,55 +122,80 @@ describe Ci::Pipeline, models: true do ...@@ -122,55 +122,80 @@ describe Ci::Pipeline, models: true do
end end
end end
describe '#stages' do describe 'pipeline stages' do
before do before do
create(:commit_status, pipeline: pipeline, stage: 'build', name: 'linux', stage_idx: 0, status: 'success') create(:commit_status, pipeline: pipeline,
create(:commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'failed') stage: 'build',
create(:commit_status, pipeline: pipeline, stage: 'deploy', name: 'staging', stage_idx: 2, status: 'running') name: 'linux',
create(:commit_status, pipeline: pipeline, stage: 'test', name: 'rspec', stage_idx: 1, status: 'success') stage_idx: 0,
end status: 'success')
subject { pipeline.stages } create(:commit_status, pipeline: pipeline,
stage: 'build',
context 'stages list' do name: 'mac',
it 'returns ordered list of stages' do stage_idx: 0,
expect(subject.map(&:name)).to eq(%w[build test deploy]) status: 'failed')
create(:commit_status, pipeline: pipeline,
stage: 'deploy',
name: 'staging',
stage_idx: 2,
status: 'running')
create(:commit_status, pipeline: pipeline,
stage: 'test',
name: 'rspec',
stage_idx: 1,
status: 'success')
end
describe '#stages' do
subject { pipeline.stages }
context 'stages list' do
it 'returns ordered list of stages' do
expect(subject.map(&:name)).to eq(%w[build test deploy])
end
end end
end
it 'returns a valid number of stages' do context 'stages with statuses' do
expect(pipeline.stages_count).to eq(3) let(:statuses) do
end subject.map { |stage| [stage.name, stage.status] }
end
it 'returns a valid names of stages' do it 'returns list of stages with correct statuses' do
expect(pipeline.stages_name).to eq(['build', 'test', 'deploy']) expect(statuses).to eq([['build', 'failed'],
end ['test', 'success'],
['deploy', 'running']])
end
context 'stages with statuses' do context 'when commit status is retried' do
let(:statuses) do before do
subject.map do |stage| create(:commit_status, pipeline: pipeline,
[stage.name, stage.status] stage: 'build',
name: 'mac',
stage_idx: 0,
status: 'success')
end
it 'ignores the previous state' do
expect(statuses).to eq([['build', 'success'],
['test', 'success'],
['deploy', 'running']])
end
end end
end end
end
it 'returns list of stages with statuses' do describe '#stages_count' do
expect(statuses).to eq([['build', 'failed'], it 'returns a valid number of stages' do
['test', 'success'], expect(pipeline.stages_count).to eq(3)
['deploy', 'running']
])
end end
end
context 'when build is retried' do describe '#stages_name' do
before do it 'returns a valid names of stages' do
create(:commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'success') expect(pipeline.stages_name).to eq(['build', 'test', 'deploy'])
end
it 'ignores the previous state' do
expect(statuses).to eq([['build', 'success'],
['test', 'success'],
['deploy', 'running']
])
end
end end
end end
end end
......
...@@ -142,6 +142,78 @@ describe Ci::Stage, models: true do ...@@ -142,6 +142,78 @@ describe Ci::Stage, models: true do
end end
end end
describe '#success?' do
context 'when stage is successful' do
before do
create_job(:ci_build, status: :success)
create_job(:generic_commit_status, status: :success)
end
it 'is successful' do
expect(stage).to be_success
end
end
context 'when stage is not successful' do
before do
create_job(:ci_build, status: :failed)
create_job(:generic_commit_status, status: :success)
end
it 'is not successful' do
expect(stage).not_to be_success
end
end
end
describe '#has_warnings?' do
context 'when stage has warnings' do
context 'when using memoized warnings flag' do
context 'when there are warnings' do
let(:stage) { build(:ci_stage, warnings: true) }
it 'has memoized warnings' do
expect(stage).not_to receive(:statuses)
expect(stage).to have_warnings
end
end
context 'when there are no warnings' do
let(:stage) { build(:ci_stage, warnings: false) }
it 'has memoized warnings' do
expect(stage).not_to receive(:statuses)
expect(stage).not_to have_warnings
end
end
end
context 'when calculating warnings from statuses' do
before do
create(:ci_build, :failed, :allowed_to_fail,
stage: stage_name, pipeline: pipeline)
end
it 'has warnings calculated from statuses' do
expect(stage).to receive(:statuses).and_call_original
expect(stage).to have_warnings
end
end
end
context 'when stage does not have warnings' do
before do
create(:ci_build, :success, stage: stage_name,
pipeline: pipeline)
end
it 'does not have warnings calculated from statuses' do
expect(stage).to receive(:statuses).and_call_original
expect(stage).not_to have_warnings
end
end
end
def create_job(type, status: 'success', stage: stage_name) def create_job(type, status: 'success', stage: stage_name)
create(type, pipeline: pipeline, stage: stage, status: status) create(type, pipeline: pipeline, stage: stage, status: status)
end end
......
...@@ -219,4 +219,10 @@ describe HasStatus do ...@@ -219,4 +219,10 @@ describe HasStatus do
end end
end end
end end
describe '::DEFAULT_STATUS' do
it 'is a status created' do
expect(described_class::DEFAULT_STATUS).to eq 'created'
end
end
end end
...@@ -117,6 +117,7 @@ describe Namespace, models: true do ...@@ -117,6 +117,7 @@ describe Namespace, models: true do
new_path = @namespace.path + "_new" new_path = @namespace.path + "_new"
allow(@namespace).to receive(:path_was).and_return(@namespace.path) allow(@namespace).to receive(:path_was).and_return(@namespace.path)
allow(@namespace).to receive(:path).and_return(new_path) allow(@namespace).to receive(:path).and_return(new_path)
expect(@namespace).to receive(:remove_exports!)
expect(@namespace.move_dir).to be_truthy expect(@namespace.move_dir).to be_truthy
end end
...@@ -151,11 +152,17 @@ describe Namespace, models: true do ...@@ -151,11 +152,17 @@ describe Namespace, models: true do
let!(:project) { create(:project, namespace: namespace) } let!(:project) { create(:project, namespace: namespace) }
let!(:path) { File.join(Gitlab.config.repositories.storages.default, namespace.path) } let!(:path) { File.join(Gitlab.config.repositories.storages.default, namespace.path) }
before { namespace.destroy }
it "removes its dirs when deleted" do it "removes its dirs when deleted" do
namespace.destroy
expect(File.exist?(path)).to be(false) expect(File.exist?(path)).to be(false)
end end
it 'removes the exports folder' do
expect(namespace).to receive(:remove_exports!)
namespace.destroy
end
end end
describe '.find_by_path_or_name' do describe '.find_by_path_or_name' do
......
...@@ -687,6 +687,17 @@ describe API::MergeRequests, api: true do ...@@ -687,6 +687,17 @@ describe API::MergeRequests, api: true do
expect(json_response.first['title']).to eq(issue.title) expect(json_response.first['title']).to eq(issue.title)
expect(json_response.first['id']).to eq(issue.id) expect(json_response.first['id']).to eq(issue.id)
end end
it 'returns 403 if the user has no access to the merge request' do
project = create(:empty_project, :private)
merge_request = create(:merge_request, :simple, source_project: project)
guest = create(:user)
project.team << [guest, :guest]
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", guest)
expect(response).to have_http_status(403)
end
end end
describe 'POST :id/merge_requests/:merge_request_id/subscription' do describe 'POST :id/merge_requests/:merge_request_id/subscription' do
...@@ -708,6 +719,15 @@ describe API::MergeRequests, api: true do ...@@ -708,6 +719,15 @@ describe API::MergeRequests, api: true do
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
end end
it 'returns 403 if user has no access to read code' do
guest = create(:user)
project.team << [guest, :guest]
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
expect(response).to have_http_status(403)
end
end end
describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do
...@@ -729,6 +749,15 @@ describe API::MergeRequests, api: true do ...@@ -729,6 +749,15 @@ describe API::MergeRequests, api: true do
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
end end
it 'returns 403 if user has no access to read code' do
guest = create(:user)
project.team << [guest, :guest]
delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
expect(response).to have_http_status(403)
end
end end
describe 'GET :id/merge_requests/:merge_request_id/approvals' do describe 'GET :id/merge_requests/:merge_request_id/approvals' do
......
...@@ -264,6 +264,18 @@ describe API::Notes, api: true do ...@@ -264,6 +264,18 @@ describe API::Notes, api: true do
end end
end end
context 'when user does not have access to read the noteable' do
it 'responds with 404' do
project = create(:empty_project, :private) { |p| p.add_guest(user) }
issue = create(:issue, :confidential, project: project)
post api("/projects/#{project.id}/issues/#{issue.id}/notes", user),
body: 'Foo'
expect(response).to have_http_status(404)
end
end
context 'when user does not have access to create noteable' do context 'when user does not have access to create noteable' do
let(:private_issue) { create(:issue, project: create(:empty_project, :private)) } let(:private_issue) { create(:issue, project: create(:empty_project, :private)) }
......
...@@ -6,7 +6,7 @@ describe API::Services, api: true do ...@@ -6,7 +6,7 @@ describe API::Services, api: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
let(:project) {create(:empty_project, creator_id: user.id, namespace: user.namespace) } let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
Service.available_services_names.each do |service| Service.available_services_names.each do |service|
describe "PUT /projects/:id/services/#{service.dasherize}" do describe "PUT /projects/:id/services/#{service.dasherize}" do
...@@ -16,6 +16,15 @@ describe API::Services, api: true do ...@@ -16,6 +16,15 @@ describe API::Services, api: true do
put api("/projects/#{project.id}/services/#{dashed_service}", user), service_attrs put api("/projects/#{project.id}/services/#{dashed_service}", user), service_attrs
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
current_service = project.services.first
event = current_service.event_names.empty? ? "foo" : current_service.event_names.first
state = current_service[event] || false
put api("/projects/#{project.id}/services/#{dashed_service}?#{event}=#{!state}", user), service_attrs
expect(response).to have_http_status(200)
expect(project.services.first[event]).not_to eq(state) unless event == "foo"
end end
it "returns if required fields missing" do it "returns if required fields missing" do
......
...@@ -183,12 +183,25 @@ describe API::Todos, api: true do ...@@ -183,12 +183,25 @@ describe API::Todos, api: true do
expect(response.status).to eq(404) expect(response.status).to eq(404)
end end
it 'returns an error if the issuable is not accessible' do
guest = create(:user)
project_1.team << [guest, :guest]
post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", guest)
if issuable_type == 'merge_requests'
expect(response).to have_http_status(403)
else
expect(response).to have_http_status(404)
end
end
end end
describe 'POST :id/issuable_type/:issueable_id/todo' do describe 'POST :id/issuable_type/:issueable_id/todo' do
context 'for an issue' do context 'for an issue' do
it_behaves_like 'an issuable', 'issues' do it_behaves_like 'an issuable', 'issues' do
let(:issuable) { create(:issue, author: author_1, project: project_1) } let(:issuable) { create(:issue, :confidential, author: author_1, project: project_1) }
end end
end end
......
...@@ -179,9 +179,24 @@ shared_examples 'it should show Gmail Actions View Commit link' do ...@@ -179,9 +179,24 @@ shared_examples 'it should show Gmail Actions View Commit link' do
end end
shared_examples 'an unsubscribeable thread' do shared_examples 'an unsubscribeable thread' do
it_behaves_like 'an unsubscribeable thread with incoming address without %{key}'
it 'has a List-Unsubscribe header in the correct format' do
is_expected.to have_header 'List-Unsubscribe', /unsubscribe/
is_expected.to have_header 'List-Unsubscribe', /mailto/
is_expected.to have_header 'List-Unsubscribe', /^<.+,.+>$/
end
it { is_expected.to have_body_text /unsubscribe/ }
end
shared_examples 'an unsubscribeable thread with incoming address without %{key}' do
include_context 'reply-by-email is enabled with incoming address without %{key}'
it 'has a List-Unsubscribe header in the correct format' do it 'has a List-Unsubscribe header in the correct format' do
is_expected.to have_header 'List-Unsubscribe', /unsubscribe/ is_expected.to have_header 'List-Unsubscribe', /unsubscribe/
is_expected.to have_header 'List-Unsubscribe', /^<.+>$/ is_expected.not_to have_header 'List-Unsubscribe', /mailto/
is_expected.to have_header 'List-Unsubscribe', /^<[^,]+>$/
end end
it { is_expected.to have_body_text /unsubscribe/ } it { is_expected.to have_body_text /unsubscribe/ }
......
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