summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/flash.js34
-rw-r--r--app/assets/javascripts/gl_field_errors.js10
-rw-r--r--app/assets/javascripts/integrations/index.js7
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js123
-rw-r--r--app/assets/stylesheets/framework/files.scss2
-rw-r--r--app/assets/stylesheets/framework/flash.scss16
-rw-r--r--app/controllers/projects/services_controller.rb39
-rw-r--r--app/models/project_services/asana_service.rb3
-rw-r--r--app/models/project_services/assembla_service.rb2
-rw-r--r--app/models/project_services/bamboo_service.rb4
-rw-r--r--app/models/project_services/buildkite_service.rb4
-rw-r--r--app/models/project_services/campfire_service.rb4
-rw-r--r--app/models/project_services/chat_notification_service.rb6
-rw-r--r--app/models/project_services/custom_issue_tracker_service.rb6
-rw-r--r--app/models/project_services/deployment_service.rb4
-rw-r--r--app/models/project_services/drone_ci_service.rb4
-rw-r--r--app/models/project_services/external_wiki_service.rb2
-rw-r--r--app/models/project_services/flowdock_service.rb2
-rw-r--r--app/models/project_services/gemnasium_service.rb4
-rw-r--r--app/models/project_services/hipchat_service.rb2
-rw-r--r--app/models/project_services/irker_service.rb2
-rw-r--r--app/models/project_services/issue_tracker_service.rb6
-rw-r--r--app/models/project_services/jira_service.rb12
-rw-r--r--app/models/project_services/mock_ci_service.rb7
-rw-r--r--app/models/project_services/mock_monitoring_service.rb4
-rw-r--r--app/models/project_services/pipelines_email_service.rb3
-rw-r--r--app/models/project_services/pivotaltracker_service.rb3
-rw-r--r--app/models/project_services/prometheus_service.rb3
-rw-r--r--app/models/project_services/pushover_service.rb6
-rw-r--r--app/models/project_services/teamcity_service.rb4
-rw-r--r--app/models/user.rb6
-rw-r--r--app/views/projects/blame/show.html.haml2
-rw-r--r--app/views/projects/blob/_breadcrumb.html.haml2
-rw-r--r--app/views/projects/services/_form.html.haml13
-rw-r--r--app/views/shared/_field.html.haml7
-rw-r--r--changelogs/unreleased/10378-promote-blameless-culture.yml4
-rw-r--r--changelogs/unreleased/28080-system-checks.yml4
-rw-r--r--changelogs/unreleased/31511-jira-settings.yml4
-rw-r--r--changelogs/unreleased/33215-fix-hard-delete-of-users.yml4
-rw-r--r--changelogs/unreleased/33242-create-project-for-user-api-ignores-path-parameter.yml4
-rw-r--r--config/karma.config.js2
-rw-r--r--config/routes/project.rb2
-rw-r--r--config/webpack.config.js3
-rw-r--r--doc/install/installation.md4
-rw-r--r--doc/workflow/gitlab_flow.md12
-rw-r--r--features/project/service.feature26
-rw-r--r--features/steps/project/services.rb70
-rw-r--r--features/steps/project/source/browse_files.rb1
-rw-r--r--lib/api/projects.rb1
-rw-r--r--lib/system_check.rb21
-rw-r--r--lib/system_check/app/active_users_check.rb17
-rw-r--r--lib/system_check/app/database_config_exists_check.rb25
-rw-r--r--lib/system_check/app/git_config_check.rb42
-rw-r--r--lib/system_check/app/git_version_check.rb29
-rw-r--r--lib/system_check/app/gitlab_config_exists_check.rb24
-rw-r--r--lib/system_check/app/gitlab_config_up_to_date_check.rb30
-rw-r--r--lib/system_check/app/init_script_exists_check.rb27
-rw-r--r--lib/system_check/app/init_script_up_to_date_check.rb43
-rw-r--r--lib/system_check/app/log_writable_check.rb28
-rw-r--r--lib/system_check/app/migrations_are_up_check.rb20
-rw-r--r--lib/system_check/app/orphaned_group_members_check.rb20
-rw-r--r--lib/system_check/app/projects_have_namespace_check.rb37
-rw-r--r--lib/system_check/app/redis_version_check.rb25
-rw-r--r--lib/system_check/app/ruby_version_check.rb27
-rw-r--r--lib/system_check/app/tmp_writable_check.rb28
-rw-r--r--lib/system_check/app/uploads_directory_exists_check.rb21
-rw-r--r--lib/system_check/app/uploads_path_permission_check.rb36
-rw-r--r--lib/system_check/app/uploads_path_tmp_permission_check.rb40
-rw-r--r--lib/system_check/base_check.rb129
-rw-r--r--lib/system_check/helpers.rb75
-rw-r--r--lib/system_check/simple_executor.rb99
-rw-r--r--lib/tasks/gitlab/check.rake494
-rw-r--r--lib/tasks/gitlab/task_helpers.rb44
-rw-r--r--spec/controllers/projects/services_controller_spec.rb112
-rw-r--r--spec/db/production/settings.rb1
-rw-r--r--spec/factories/services.rb6
-rw-r--r--spec/features/projects/blobs/blob_line_permalink_updater_spec.rb2
-rw-r--r--spec/features/projects/files/browse_files_spec.rb2
-rw-r--r--spec/features/projects/services/jira_service_spec.rb92
-rw-r--r--spec/features/projects/services/mattermost_slash_command_spec.rb18
-rw-r--r--spec/features/projects/services/slack_slash_command_spec.rb14
-rw-r--r--spec/javascripts/fixtures/services.rb31
-rw-r--r--spec/javascripts/integrations/integration_settings_form_spec.js199
-rw-r--r--spec/lib/system_check/simple_executor_spec.rb223
-rw-r--r--spec/lib/system_check_spec.rb36
-rw-r--r--spec/models/project_services/jira_service_spec.rb35
-rw-r--r--spec/models/user_spec.rb2
-rw-r--r--spec/requests/api/projects_spec.rb26
-rw-r--r--spec/services/users/destroy_service_spec.rb8
-rw-r--r--spec/spec_helper.rb3
-rw-r--r--spec/support/matchers/execute_check.rb23
-rw-r--r--spec/support/rake_helpers.rb5
92 files changed, 1984 insertions, 734 deletions
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index eec30624ff2..ccff8f0ace7 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -7,8 +7,21 @@ window.Flash = (function() {
return $(this).fadeOut();
};
- function Flash(message, type, parent) {
- var flash, textDiv;
+ /**
+ * Flash banner supports different types of Flash configurations
+ * along with ability to provide actionConfig which can be used to show
+ * additional action or link on banner next to message
+ *
+ * @param {String} message Flash message
+ * @param {String} type Type of Flash, it can be `notice` or `alert` (default)
+ * @param {Object} parent Reference to Parent element under which Flash needs to appear
+ * @param {Object} actionConfig Map of config to show action on banner
+ * @param {String} href URL to which action link should point (default '#')
+ * @param {String} title Title of action
+ * @param {Function} clickHandler Method to call when action is clicked on
+ */
+ function Flash(message, type, parent, actionConfig) {
+ var flash, textDiv, actionLink;
if (type == null) {
type = 'alert';
}
@@ -30,6 +43,23 @@ window.Flash = (function() {
text: message
});
textDiv.appendTo(flash);
+
+ if (actionConfig) {
+ const actionLinkConfig = {
+ class: 'flash-action',
+ href: actionConfig.href || '#',
+ text: actionConfig.title
+ };
+
+ if (!actionConfig.href) {
+ actionLinkConfig.role = 'button';
+ }
+
+ actionLink = $('<a/>', actionLinkConfig);
+
+ actionLink.appendTo(flash);
+ this.flashContainer.on('click', '.flash-action', actionConfig.clickHandler);
+ }
if (this.flashContainer.parent().hasClass('content-wrapper')) {
textDiv.addClass('container-fluid container-limited');
}
diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js
index 4f226ff96ea..4bef60264bb 100644
--- a/app/assets/javascripts/gl_field_errors.js
+++ b/app/assets/javascripts/gl_field_errors.js
@@ -31,9 +31,13 @@ class GlFieldErrors {
* and prevents disabling of invalid submit button by application.js */
catchInvalidFormSubmit (event) {
- if (!event.currentTarget.checkValidity()) {
- event.preventDefault();
- event.stopPropagation();
+ const $form = $(event.currentTarget);
+
+ if (!$form.attr('novalidate')) {
+ if (!event.currentTarget.checkValidity()) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
}
}
diff --git a/app/assets/javascripts/integrations/index.js b/app/assets/javascripts/integrations/index.js
new file mode 100644
index 00000000000..10fe6bac0e8
--- /dev/null
+++ b/app/assets/javascripts/integrations/index.js
@@ -0,0 +1,7 @@
+/* eslint-disable no-new */
+import IntegrationSettingsForm from './integration_settings_form';
+
+$(() => {
+ const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ integrationSettingsForm.init();
+});
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
new file mode 100644
index 00000000000..ddd3a6aab99
--- /dev/null
+++ b/app/assets/javascripts/integrations/integration_settings_form.js
@@ -0,0 +1,123 @@
+/* global Flash */
+
+export default class IntegrationSettingsForm {
+ constructor(formSelector) {
+ this.$form = $(formSelector);
+
+ // Form Metadata
+ this.canTestService = this.$form.data('can-test');
+ this.testEndPoint = this.$form.data('test-url');
+
+ // Form Child Elements
+ this.$serviceToggle = this.$form.find('#service_active');
+ this.$submitBtn = this.$form.find('button[type="submit"]');
+ this.$submitBtnLoader = this.$submitBtn.find('.js-btn-spinner');
+ this.$submitBtnLabel = this.$submitBtn.find('.js-btn-label');
+ }
+
+ init() {
+ // Initialize View
+ this.toggleServiceState(this.$serviceToggle.is(':checked'));
+
+ // Bind Event Listeners
+ this.$serviceToggle.on('change', e => this.handleServiceToggle(e));
+ this.$submitBtn.on('click', e => this.handleSettingsSave(e));
+ }
+
+ handleSettingsSave(e) {
+ // Check if Service is marked active, as if not marked active,
+ // We can skip testing it and directly go ahead to allow form to
+ // be submitted
+ if (!this.$serviceToggle.is(':checked')) {
+ return;
+ }
+
+ // Service was marked active so now we check;
+ // 1) If form contents are valid
+ // 2) If this service can be tested
+ // If both conditions are true, we override form submission
+ // and test the service using provided configuration.
+ if (this.$form.get(0).checkValidity() && this.canTestService) {
+ e.preventDefault();
+ this.testSettings(this.$form.serialize());
+ }
+ }
+
+ handleServiceToggle(e) {
+ this.toggleServiceState($(e.currentTarget).is(':checked'));
+ }
+
+ /**
+ * Change Form's validation enforcement based on service status (active/inactive)
+ */
+ toggleServiceState(serviceActive) {
+ this.toggleSubmitBtnLabel(serviceActive);
+ if (serviceActive) {
+ this.$form.removeAttr('novalidate');
+ } else if (!this.$form.attr('novalidate')) {
+ this.$form.attr('novalidate', 'novalidate');
+ }
+ }
+
+ /**
+ * Toggle Submit button label based on Integration status and ability to test service
+ */
+ toggleSubmitBtnLabel(serviceActive) {
+ let btnLabel = 'Save changes';
+
+ if (serviceActive && this.canTestService) {
+ btnLabel = 'Test settings and save changes';
+ }
+
+ this.$submitBtnLabel.text(btnLabel);
+ }
+
+ /**
+ * Toggle Submit button state based on provided boolean value of `saveTestActive`
+ * When enabled, it does two things, and reverts back when disabled
+ *
+ * 1. It shows load spinner on submit button
+ * 2. Makes submit button disabled
+ */
+ toggleSubmitBtnState(saveTestActive) {
+ if (saveTestActive) {
+ this.$submitBtn.disable();
+ this.$submitBtnLoader.removeClass('hidden');
+ } else {
+ this.$submitBtn.enable();
+ this.$submitBtnLoader.addClass('hidden');
+ }
+ }
+
+ /* eslint-disable promise/catch-or-return, no-new */
+ /**
+ * Test Integration config
+ */
+ testSettings(formData) {
+ this.toggleSubmitBtnState(true);
+ $.ajax({
+ type: 'PUT',
+ url: this.testEndPoint,
+ data: formData,
+ })
+ .done((res) => {
+ if (res.error) {
+ new Flash(res.message, null, null, {
+ title: 'Save anyway',
+ clickHandler: (e) => {
+ e.preventDefault();
+ this.$form.submit();
+ },
+ });
+ } else {
+ this.$form.submit();
+ }
+ })
+ .fail(() => {
+ new Flash('Something went wrong on our end.');
+ })
+ .always(() => {
+ this.toggleSubmitBtnState(false);
+ });
+ }
+}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 78f425057eb..d08df05fd6c 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -85,7 +85,7 @@
}
/**
- * Blame file
+ * Annotate file
*/
&.blame {
table {
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index 25b4feca3c3..38d884bc7eb 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -16,6 +16,22 @@
@extend .alert;
@extend .alert-danger;
margin: 0;
+
+ .flash-text,
+ .flash-action {
+ display: inline-block;
+ }
+
+ a.flash-action {
+ margin-left: 5px;
+ text-decoration: none;
+ font-weight: normal;
+ border-bottom: 1px solid;
+
+ &:hover {
+ border-color: transparent;
+ }
+ }
}
.flash-notice,
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index f9d798d0455..704f8cc8a79 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -4,6 +4,7 @@ class Projects::ServicesController < Projects::ApplicationController
# Authorize
before_action :authorize_admin_project!
before_action :service, only: [:edit, :update, :test]
+ before_action :update_service, only: [:update, :test]
respond_to :html
@@ -13,36 +14,46 @@ class Projects::ServicesController < Projects::ApplicationController
end
def update
- @service.assign_attributes(service_params[:service])
if @service.save(context: :manual_change)
- redirect_to(
- edit_namespace_project_service_path(@project.namespace, @project, @service.to_param),
- notice: 'Successfully updated.'
- )
+ redirect_to(namespace_project_settings_integrations_path(@project.namespace, @project), notice: success_message)
else
render 'edit'
end
end
def test
- return render_404 unless @service.can_test?
+ message = {}
+
+ if @service.can_test?
+ data = @service.test_data(project, current_user)
+ outcome = @service.test(data)
- data = @service.test_data(project, current_user)
- outcome = @service.test(data)
+ unless outcome[:success]
+ message = { error: true, message: 'Test failed.', service_response: outcome[:result].to_s }
+ end
- if outcome[:success]
- message = { notice: 'We sent a request to the provided URL' }
+ status = :ok
else
- error_message = "We tried to send a request to the provided URL but an error occurred"
- error_message << ": #{outcome[:result]}" if outcome[:result].present?
- message = { alert: error_message }
+ status = :not_found
end
- redirect_back_or_default(options: message)
+ render json: message, status: status
end
private
+ def success_message
+ if @service.active?
+ "#{@service.title} activated."
+ else
+ "#{@service.title} settings saved, but not activated."
+ end
+ end
+
+ def update_service
+ @service.assign_attributes(service_params[:service])
+ end
+
def service
@service ||= @project.find_or_initialize_service(params[:id])
end
diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb
index 3728f5642e4..9ce2d1153a7 100644
--- a/app/models/project_services/asana_service.rb
+++ b/app/models/project_services/asana_service.rb
@@ -34,7 +34,8 @@ http://app.asana.com/-/account_api'
{
type: 'text',
name: 'api_key',
- placeholder: 'User Personal Access Token. User must have access to task, all comments will be attributed to this user.'
+ placeholder: 'User Personal Access Token. User must have access to task, all comments will be attributed to this user.',
+ required: true
},
{
type: 'text',
diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb
index aeeff8917bf..ae6af732ed4 100644
--- a/app/models/project_services/assembla_service.rb
+++ b/app/models/project_services/assembla_service.rb
@@ -18,7 +18,7 @@ class AssemblaService < Service
def fields
[
- { type: 'text', name: 'token', placeholder: '' },
+ { type: 'text', name: 'token', placeholder: '', required: true },
{ type: 'text', name: 'subdomain', placeholder: '' }
]
end
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index 3f5b3eb159b..42939ea0ec8 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -47,9 +47,9 @@ class BambooService < CiService
def fields
[
{ type: 'text', name: 'bamboo_url',
- placeholder: 'Bamboo root URL like https://bamboo.example.com' },
+ placeholder: 'Bamboo root URL like https://bamboo.example.com', required: true },
{ type: 'text', name: 'build_key',
- placeholder: 'Bamboo build plan key like KEY' },
+ placeholder: 'Bamboo build plan key like KEY', required: true },
{ type: 'text', name: 'username',
placeholder: 'A user with API access, if applicable' },
{ type: 'password', name: 'password' }
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
index 5fb95050b83..fc30f6e3365 100644
--- a/app/models/project_services/buildkite_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -58,11 +58,11 @@ class BuildkiteService < CiService
[
{ type: 'text',
name: 'token',
- placeholder: 'Buildkite project GitLab token' },
+ placeholder: 'Buildkite project GitLab token', required: true },
{ type: 'text',
name: 'project_url',
- placeholder: "#{ENDPOINT}/example/project" },
+ placeholder: "#{ENDPOINT}/example/project", required: true },
{ type: 'checkbox',
name: 'enable_ssl_verification',
diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb
index 0de59af5652..c3f5b310619 100644
--- a/app/models/project_services/campfire_service.rb
+++ b/app/models/project_services/campfire_service.rb
@@ -18,7 +18,7 @@ class CampfireService < Service
def fields
[
- { type: 'text', name: 'token', placeholder: '' },
+ { type: 'text', name: 'token', placeholder: '', required: true },
{ type: 'text', name: 'subdomain', placeholder: '' },
{ type: 'text', name: 'room', placeholder: '' }
]
@@ -76,7 +76,7 @@ class CampfireService < Service
# Returns a list of rooms, or [].
# https://github.com/basecamp/campfire-api/blob/master/sections/rooms.md#get-rooms
def rooms(auth)
- res = self.class.get("/rooms.json", auth)
+ res = self.class.get("/rooms.json", auth)
res.code == 200 ? res["rooms"] : []
end
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index 779ef54cfcb..6d1a321f651 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -21,10 +21,6 @@ class ChatNotificationService < Service
end
end
- def can_test?
- valid?
- end
-
def self.supported_events
%w[push issue confidential_issue merge_request note tag_push
pipeline wiki_page]
@@ -36,7 +32,7 @@ class ChatNotificationService < Service
def default_fields
[
- { type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" },
+ { type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}", required: true },
{ type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{ type: 'checkbox', name: 'notify_only_default_branch' }
diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb
index dea915a4d05..b9e3e982b64 100644
--- a/app/models/project_services/custom_issue_tracker_service.rb
+++ b/app/models/project_services/custom_issue_tracker_service.rb
@@ -31,9 +31,9 @@ class CustomIssueTrackerService < IssueTrackerService
[
{ type: 'text', name: 'title', placeholder: title },
{ type: 'text', name: 'description', placeholder: description },
- { type: 'text', name: 'project_url', placeholder: 'Project url' },
- { type: 'text', name: 'issues_url', placeholder: 'Issue url' },
- { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url' }
+ { type: 'text', name: 'project_url', placeholder: 'Project url', required: true },
+ { type: 'text', name: 'issues_url', placeholder: 'Issue url', required: true },
+ { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url', required: true }
]
end
end
diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb
index 91a55514a9a..5b8320158fc 100644
--- a/app/models/project_services/deployment_service.rb
+++ b/app/models/project_services/deployment_service.rb
@@ -30,4 +30,8 @@ class DeploymentService < Service
def terminals(environment)
raise NotImplementedError
end
+
+ def can_test?
+ false
+ end
end
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index 2717c240f05..f6cade9c290 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -93,8 +93,8 @@ class DroneCiService < CiService
def fields
[
- { type: 'text', name: 'token', placeholder: 'Drone CI project specific token' },
- { type: 'text', name: 'drone_url', placeholder: 'http://drone.example.com' },
+ { type: 'text', name: 'token', placeholder: 'Drone CI project specific token', required: true },
+ { type: 'text', name: 'drone_url', placeholder: 'http://drone.example.com', required: true },
{ type: 'checkbox', name: 'enable_ssl_verification', title: "Enable SSL verification" }
]
end
diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb
index b4d7c977ce4..720ad61162e 100644
--- a/app/models/project_services/external_wiki_service.rb
+++ b/app/models/project_services/external_wiki_service.rb
@@ -19,7 +19,7 @@ class ExternalWikiService < Service
def fields
[
- { type: 'text', name: 'external_wiki_url', placeholder: 'The URL of the external Wiki' }
+ { type: 'text', name: 'external_wiki_url', placeholder: 'The URL of the external Wiki', required: true }
]
end
diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb
index 2a05d757eb4..2db95b9aaa3 100644
--- a/app/models/project_services/flowdock_service.rb
+++ b/app/models/project_services/flowdock_service.rb
@@ -18,7 +18,7 @@ class FlowdockService < Service
def fields
[
- { type: 'text', name: 'token', placeholder: 'Flowdock Git source token' }
+ { type: 'text', name: 'token', placeholder: 'Flowdock Git source token', required: true }
]
end
diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb
index f271e1f1739..017a9b2df6e 100644
--- a/app/models/project_services/gemnasium_service.rb
+++ b/app/models/project_services/gemnasium_service.rb
@@ -18,8 +18,8 @@ class GemnasiumService < Service
def fields
[
- { type: 'text', name: 'api_key', placeholder: 'Your personal API KEY on gemnasium.com ' },
- { type: 'text', name: 'token', placeholder: 'The project\'s slug on gemnasium.com' }
+ { type: 'text', name: 'api_key', placeholder: 'Your personal API KEY on gemnasium.com ', required: true },
+ { type: 'text', name: 'token', placeholder: 'The project\'s slug on gemnasium.com', required: true }
]
end
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index c19fed339ba..e3906943ecd 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -33,7 +33,7 @@ class HipchatService < Service
def fields
[
- { type: 'text', name: 'token', placeholder: 'Room token' },
+ { type: 'text', name: 'token', placeholder: 'Room token', required: true },
{ type: 'text', name: 'room', placeholder: 'Room name or ID' },
{ type: 'checkbox', name: 'notify' },
{ type: 'select', name: 'color', choices: %w(yellow red green purple gray random) },
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index a51d43adcb9..19357f90810 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -49,7 +49,7 @@ class IrkerService < Service
help: 'A default IRC URI to prepend before each recipient (optional)',
placeholder: 'irc://irc.network.net:6697/' },
{ type: 'textarea', name: 'recipients',
- placeholder: 'Recipients/channels separated by whitespaces',
+ placeholder: 'Recipients/channels separated by whitespaces', required: true,
help: 'Recipients have to be specified with a full URI: '\
'irc[s]://irc.network.net[:port]/#channel. Special cases: if '\
'you want the channel to be a nickname instead, append ",isnick" to ' \
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index eddf308eae3..ff138b9066d 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -32,9 +32,9 @@ class IssueTrackerService < Service
def fields
[
{ type: 'text', name: 'description', placeholder: description },
- { type: 'text', name: 'project_url', placeholder: 'Project url' },
- { type: 'text', name: 'issues_url', placeholder: 'Issue url' },
- { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url' }
+ { type: 'text', name: 'project_url', placeholder: 'Project url', required: true },
+ { type: 'text', name: 'issues_url', placeholder: 'Issue url', required: true },
+ { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url', required: true }
]
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 25d098b63c0..2450fb43212 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -86,11 +86,11 @@ class JiraService < IssueTrackerService
def fields
[
- { type: 'text', name: 'url', title: 'Web URL', placeholder: 'https://jira.example.com' },
+ { type: 'text', name: 'url', title: 'Web URL', placeholder: 'https://jira.example.com', required: true },
{ type: 'text', name: 'api_url', title: 'JIRA API URL', placeholder: 'If different from Web URL' },
- { type: 'text', name: 'project_key', placeholder: 'Project Key' },
- { type: 'text', name: 'username', placeholder: '' },
- { type: 'password', name: 'password', placeholder: '' },
+ { type: 'text', name: 'project_key', placeholder: 'Project Key', required: true },
+ { type: 'text', name: 'username', placeholder: '', required: true },
+ { type: 'password', name: 'password', placeholder: '', required: true },
{ type: 'text', name: 'jira_issue_transition_id', placeholder: '' }
]
end
@@ -175,10 +175,6 @@ class JiraService < IssueTrackerService
{ success: result.present?, result: result }
end
- def can_test?
- username.present? && password.present?
- end
-
# JIRA does not need test data.
# We are requesting the project that belongs to the project key.
def test_data(user = nil, project = nil)
diff --git a/app/models/project_services/mock_ci_service.rb b/app/models/project_services/mock_ci_service.rb
index 546b6e0a498..72ddf9a4be3 100644
--- a/app/models/project_services/mock_ci_service.rb
+++ b/app/models/project_services/mock_ci_service.rb
@@ -21,7 +21,8 @@ class MockCiService < CiService
[
{ type: 'text',
name: 'mock_service_url',
- placeholder: 'http://localhost:4004' }
+ placeholder: 'http://localhost:4004',
+ required: true }
]
end
@@ -79,4 +80,8 @@ class MockCiService < CiService
:error
end
end
+
+ def can_test?
+ false
+ end
end
diff --git a/app/models/project_services/mock_monitoring_service.rb b/app/models/project_services/mock_monitoring_service.rb
index dd04e04e198..ed0318c6b27 100644
--- a/app/models/project_services/mock_monitoring_service.rb
+++ b/app/models/project_services/mock_monitoring_service.rb
@@ -14,4 +14,8 @@ class MockMonitoringService < MonitoringService
def metrics(environment)
JSON.parse(File.read(Rails.root + 'spec/fixtures/metrics.json'))
end
+
+ def can_test?
+ false
+ end
end
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
index f824171ad09..9d37184be2c 100644
--- a/app/models/project_services/pipelines_email_service.rb
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -53,7 +53,8 @@ class PipelinesEmailService < Service
[
{ type: 'textarea',
name: 'recipients',
- placeholder: 'Emails separated by comma' },
+ placeholder: 'Emails separated by comma',
+ required: true },
{ type: 'checkbox',
name: 'notify_only_broken_pipelines' }
]
diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb
index d86f4f6f448..f9dfa2e91c3 100644
--- a/app/models/project_services/pivotaltracker_service.rb
+++ b/app/models/project_services/pivotaltracker_service.rb
@@ -23,7 +23,8 @@ class PivotaltrackerService < Service
{
type: 'text',
name: 'token',
- placeholder: 'Pivotal Tracker API token.'
+ placeholder: 'Pivotal Tracker API token.',
+ required: true
},
{
type: 'text',
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
index ec72cb6856d..110b8bc209b 100644
--- a/app/models/project_services/prometheus_service.rb
+++ b/app/models/project_services/prometheus_service.rb
@@ -49,7 +49,8 @@ class PrometheusService < MonitoringService
type: 'text',
name: 'api_url',
title: 'API URL',
- placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/'
+ placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/',
+ required: true
}
]
end
diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb
index fc29a5277bb..aa7bd4c3c84 100644
--- a/app/models/project_services/pushover_service.rb
+++ b/app/models/project_services/pushover_service.rb
@@ -19,10 +19,10 @@ class PushoverService < Service
def fields
[
- { type: 'text', name: 'api_key', placeholder: 'Your application key' },
- { type: 'text', name: 'user_key', placeholder: 'Your user key' },
+ { type: 'text', name: 'api_key', placeholder: 'Your application key', required: true },
+ { type: 'text', name: 'user_key', placeholder: 'Your user key', required: true },
{ type: 'text', name: 'device', placeholder: 'Leave blank for all active devices' },
- { type: 'select', name: 'priority', choices:
+ { type: 'select', name: 'priority', required: true, choices:
[
['Lowest Priority', -2],
['Low Priority', -1],
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index b16beb406b9..cbe137452bd 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -50,9 +50,9 @@ class TeamcityService < CiService
def fields
[
{ type: 'text', name: 'teamcity_url',
- placeholder: 'TeamCity root URL like https://teamcity.example.com' },
+ placeholder: 'TeamCity root URL like https://teamcity.example.com', required: true },
{ type: 'text', name: 'build_type',
- placeholder: 'Build configuration ID' },
+ placeholder: 'Build configuration ID', required: true },
{ type: 'text', name: 'username',
placeholder: 'A user with permissions to trigger a manual build' },
{ type: 'password', name: 'password' }
diff --git a/app/models/user.rb b/app/models/user.rb
index 32048da6c6f..e6eb9d09656 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -101,6 +101,7 @@ class User < ActiveRecord::Base
has_many :snippets, dependent: :destroy, foreign_key: :author_id
has_many :notes, dependent: :destroy, foreign_key: :author_id
+ has_many :issues, dependent: :destroy, foreign_key: :author_id
has_many :merge_requests, dependent: :destroy, foreign_key: :author_id
has_many :events, dependent: :destroy, foreign_key: :author_id
has_many :subscriptions, dependent: :destroy
@@ -120,11 +121,6 @@ class User < ActiveRecord::Base
has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest"
- # Issues that a user owns are expected to be moved to the "ghost" user before
- # the user is destroyed. If the user owns any issues during deletion, this
- # should be treated as an exceptional condition.
- has_many :issues, dependent: :restrict_with_exception, foreign_key: :author_id
-
#
# Validations
#
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index a2ec3d44185..a6ee2b2f7b8 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- page_title "Blame", @blob.path, @ref
+- page_title "Annotate", @blob.path, @ref
= render "projects/commits/head"
%div{ class: container_class }
diff --git a/app/views/projects/blob/_breadcrumb.html.haml b/app/views/projects/blob/_breadcrumb.html.haml
index 3f58e8d232f..0ad9f258e48 100644
--- a/app/views/projects/blob/_breadcrumb.html.haml
+++ b/app/views/projects/blob/_breadcrumb.html.haml
@@ -10,7 +10,7 @@
= link_to 'Normal view', namespace_project_blob_path(@project.namespace, @project, @id),
class: 'btn'
- else
- = link_to 'Blame', namespace_project_blame_path(@project.namespace, @project, @id),
+ = link_to 'Annotate', namespace_project_blame_path(@project.namespace, @project, @id),
class: 'btn js-blob-blame-link' unless blob.empty?
= link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id),
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index f1a80f1d5e1..9167789a69d 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -1,3 +1,6 @@
+- content_for :page_specific_javascripts do
+ = webpack_bundle_tag('integrations')
+
.row.prepend-top-default.append-bottom-default
.col-lg-3
%h4.prepend-top-0
@@ -6,15 +9,17 @@
%p= @service.description
.col-lg-9
- = form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form|
+ = form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'gl-show-field-errors form-horizontal js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_namespace_project_service_path } }) do |form|
= render 'shared/service_settings', form: form, subject: @service
.footer-block.row-content-block
- = form.submit 'Save changes', class: 'btn btn-save'
+ %button.btn.btn-save{ type: 'submit' }
+ = icon('spinner spin', class: 'hidden js-btn-spinner')
+ %span.js-btn-label
+ Save changes
&nbsp;
- if @service.valid? && @service.activated?
- unless @service.can_test?
- disabled_class = 'disabled'
- disabled_title = @service.disabled_title
- = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service), class: "btn #{disabled_class}", title: disabled_title
- = link_to "Cancel", namespace_project_settings_integrations_path(@project.namespace, @project), class: "btn btn-cancel"
+ = link_to 'Cancel', namespace_project_settings_integrations_path(@project.namespace, @project), class: 'btn btn-cancel'
diff --git a/app/views/shared/_field.html.haml b/app/views/shared/_field.html.haml
index d74b0043949..795447a9ca6 100644
--- a/app/views/shared/_field.html.haml
+++ b/app/views/shared/_field.html.haml
@@ -3,6 +3,7 @@
- value = @service.send(name)
- type = field[:type]
- placeholder = field[:placeholder]
+- required = field[:required]
- choices = field[:choices]
- default_choice = field[:default_choice]
- help = field[:help]
@@ -14,14 +15,14 @@
= form.label name, title, class: "control-label"
.col-sm-10
- if type == 'text'
- = form.text_field name, class: "form-control", placeholder: placeholder
+ = form.text_field name, class: "form-control", placeholder: placeholder, required: required
- elsif type == 'textarea'
- = form.text_area name, rows: 5, class: "form-control", placeholder: placeholder
+ = form.text_area name, rows: 5, class: "form-control", placeholder: placeholder, required: required
- elsif type == 'checkbox'
= form.check_box name
- elsif type == 'select'
= form.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" }
- elsif type == 'password'
- = form.password_field name, autocomplete: "new-password", class: "form-control"
+ = form.password_field name, autocomplete: "new-password", class: "form-control", required: value.blank? && :required
- if help
%span.help-block= help
diff --git a/changelogs/unreleased/10378-promote-blameless-culture.yml b/changelogs/unreleased/10378-promote-blameless-culture.yml
new file mode 100644
index 00000000000..8cf64dfd793
--- /dev/null
+++ b/changelogs/unreleased/10378-promote-blameless-culture.yml
@@ -0,0 +1,4 @@
+---
+title: Changed Blame to Annotate in the UI to promote blameless culture
+merge_request: 10378
+author: Ilya Vassilevsky
diff --git a/changelogs/unreleased/28080-system-checks.yml b/changelogs/unreleased/28080-system-checks.yml
new file mode 100644
index 00000000000..7d83014279a
--- /dev/null
+++ b/changelogs/unreleased/28080-system-checks.yml
@@ -0,0 +1,4 @@
+---
+title: Refactored gitlab:app:check into SystemCheck liberary and improve some checks
+merge_request: 9173
+author:
diff --git a/changelogs/unreleased/31511-jira-settings.yml b/changelogs/unreleased/31511-jira-settings.yml
new file mode 100644
index 00000000000..4f9ddb13ef6
--- /dev/null
+++ b/changelogs/unreleased/31511-jira-settings.yml
@@ -0,0 +1,4 @@
+---
+title: Simplify testing and saving service integrations
+merge_request: 11599
+author:
diff --git a/changelogs/unreleased/33215-fix-hard-delete-of-users.yml b/changelogs/unreleased/33215-fix-hard-delete-of-users.yml
new file mode 100644
index 00000000000..29699ff745a
--- /dev/null
+++ b/changelogs/unreleased/33215-fix-hard-delete-of-users.yml
@@ -0,0 +1,4 @@
+---
+title: Fix hard-deleting users when they have authored issues
+merge_request: 11855
+author:
diff --git a/changelogs/unreleased/33242-create-project-for-user-api-ignores-path-parameter.yml b/changelogs/unreleased/33242-create-project-for-user-api-ignores-path-parameter.yml
new file mode 100644
index 00000000000..c33278998ee
--- /dev/null
+++ b/changelogs/unreleased/33242-create-project-for-user-api-ignores-path-parameter.yml
@@ -0,0 +1,4 @@
+---
+title: Fix missing optional path parameter in "Create project for user" API
+merge_request: 11868
+author:
diff --git a/config/karma.config.js b/config/karma.config.js
index eb082dd28bf..40c58e7771d 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -13,6 +13,8 @@ if (webpackConfig.plugins) {
});
}
+webpackConfig.devtool = 'cheap-inline-source-map';
+
// Karma configuration
module.exports = function(config) {
var progressReporter = process.env.CI ? 'mocha' : 'progress';
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 5aac44fce10..14718e2f3c4 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -67,7 +67,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do
member do
- get :test
+ put :test
end
end
diff --git a/config/webpack.config.js b/config/webpack.config.js
index c77b1d6334c..c99298239b2 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -41,6 +41,7 @@ var config = {
group: './group.js',
groups_list: './groups_list.js',
issue_show: './issue_show/index.js',
+ integrations: './integrations',
locale: './locale/index.js',
main: './main.js',
merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js',
@@ -74,8 +75,6 @@ var config = {
chunkFilename: IS_PRODUCTION ? '[name].[chunkhash].chunk.js' : '[name].chunk.js',
},
- devtool: 'cheap-module-source-map',
-
module: {
rules: [
{
diff --git a/doc/install/installation.md b/doc/install/installation.md
index af21d99d024..c911b297f8d 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -505,6 +505,10 @@ Check if GitLab and its environment are configured correctly:
sudo -u git -H yarn install --production --pure-lockfile
sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
+### Compile GetText PO files
+
+ sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production
+
### Start Your GitLab Instance
sudo service gitlab start
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index 1b172b21f3d..e10ccc4fc46 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -67,7 +67,7 @@ With GitLab flow we offer additional guidance for these questions.
![Master branch and production branch with arrow that indicate deployments](production_branch.png)
GitHub flow does assume you are able to deploy to production every time you merge a feature branch.
-This is possible for SaaS applications but there are many cases where this is not possible.
+This is possible for e.g. SaaS applications, but there are many cases where this is not possible.
One would be a situation where you are not in control of the exact release moment, for example an iOS application that needs to pass App Store validation.
Another example is when you have deployment windows (workdays from 10am to 4pm when the operations team is at full capacity) but you also merge code at other times.
In these cases you can make a production branch that reflects the deployed code.
@@ -134,7 +134,7 @@ If the assigned person does not feel comfortable they can close the merge reques
In GitLab it is common to protect the long-lived branches (e.g. the master branch) so that normal developers [can't modify these protected branches](http://docs.gitlab.com/ce/permissions/permissions.html).
So if you want to merge it into a protected branch you assign it to someone with master authorizations.
-## Issues with GitLab flow
+## Issue tracking with GitLab flow
![Merge request with the branch name 15-require-a-password-to-change-it and assignee field shown](merge_request.png)
@@ -173,9 +173,9 @@ It is possible that one feature branch solves more than one issue.
![Merge request showing the linked issues that will be closed](close_issue_mr.png)
-Linking to the issue can happen by mentioning them from commit messages (fixes #14, closes #67, etc.) or from the merge request description.
-In GitLab this creates a comment in the issue that the merge requests mentions the issue.
-And the merge request shows the linked issues.
+Linking to issues can happen by mentioning them in commit messages (fixes #14, closes #67, etc.) or in the merge request description.
+GitLab then creates links to the mentioned issues and creates comments in the corresponding issues linking back to the merge request.
+
These issues are closed once code is merged into the default branch.
If you only want to make the reference without closing the issue you can also just mention it: "Duck typing is preferred. #12".
@@ -300,7 +300,7 @@ If there are no merge conflicts and the feature branches are short lived the ris
If there are merge conflicts you merge the master branch into the feature branch and the CI server will rerun the tests.
If you have long lived feature branches that last for more than a few days you should make your issues smaller.
-## Merging in other code
+## Working wih feature branches
![Shell output showing git pull output](git_pull.png)
diff --git a/features/project/service.feature b/features/project/service.feature
index cce5f58adec..54f07ebca92 100644
--- a/features/project/service.feature
+++ b/features/project/service.feature
@@ -11,77 +11,77 @@ Feature: Project Services
When I visit project "Shop" services page
And I click hipchat service link
And I fill hipchat settings
- Then I should see hipchat service settings saved
+ Then I should see the Hipchat success message
Scenario: Activate hipchat service with custom server
When I visit project "Shop" services page
And I click hipchat service link
And I fill hipchat settings with custom server
- Then I should see hipchat service settings with custom server saved
+ Then I should see the Hipchat success message
Scenario: Activate pivotaltracker service
When I visit project "Shop" services page
And I click pivotaltracker service link
And I fill pivotaltracker settings
- Then I should see pivotaltracker service settings saved
+ Then I should see the Pivotaltracker success message
Scenario: Activate Flowdock service
When I visit project "Shop" services page
And I click Flowdock service link
And I fill Flowdock settings
- Then I should see Flowdock service settings saved
+ Then I should see the Flowdock success message
Scenario: Activate Assembla service
When I visit project "Shop" services page
And I click Assembla service link
And I fill Assembla settings
- Then I should see Assembla service settings saved
+ Then I should see the Assembla success message
Scenario: Activate Slack notifications service
When I visit project "Shop" services page
And I click Slack notifications service link
And I fill Slack notifications settings
- Then I should see Slack Notifications service settings saved
+ Then I should see the Slack notifications success message
Scenario: Activate Pushover service
When I visit project "Shop" services page
And I click Pushover service link
And I fill Pushover settings
- Then I should see Pushover service settings saved
+ Then I should see the Pushover success message
Scenario: Activate email on push service
When I visit project "Shop" services page
And I click email on push service link
And I fill email on push settings
- Then I should see email on push service settings saved
+ Then I should see the Emails on push success message
Scenario: Activate JIRA service
When I visit project "Shop" services page
And I click jira service link
And I fill jira settings
- Then I should see jira service settings saved
+ Then I should see the JIRA success message
Scenario: Activate Irker (IRC Gateway) service
When I visit project "Shop" services page
And I click Irker service link
And I fill Irker settings
- Then I should see Irker service settings saved
+ Then I should see the Irker success message
Scenario: Activate Atlassian Bamboo CI service
When I visit project "Shop" services page
And I click Atlassian Bamboo CI service link
And I fill Atlassian Bamboo CI settings
- Then I should see Atlassian Bamboo CI service settings saved
+ Then I should see the Bamboo success message
And I should see empty field Change Password
Scenario: Activate jetBrains TeamCity CI service
When I visit project "Shop" services page
And I click jetBrains TeamCity CI service link
And I fill jetBrains TeamCity CI settings
- Then I should see jetBrains TeamCity CI service settings saved
+ Then I should see the JetBrains success message
Scenario: Activate Asana service
When I visit project "Shop" services page
And I click Asana service link
And I fill Asana settings
- Then I should see Asana service settings saved
+ Then I should see the Asana success message
diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb
index 66368a159ec..6bac4df16f8 100644
--- a/features/steps/project/services.rb
+++ b/features/steps/project/services.rb
@@ -34,8 +34,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see hipchat service settings saved' do
- expect(find_field('Room').value).to eq 'gitlab'
+ step 'I should see the Hipchat success message' do
+ expect(page).to have_content 'HipChat activated.'
end
step 'I fill hipchat settings with custom server' do
@@ -46,10 +46,6 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see hipchat service settings with custom server saved' do
- expect(find_field('Server').value).to eq 'https://chat.example.com'
- end
-
step 'I click pivotaltracker service link' do
click_link 'PivotalTracker'
end
@@ -60,8 +56,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see pivotaltracker service settings saved' do
- expect(find_field('Token').value).to eq 'verySecret'
+ step 'I should see the Pivotaltracker success message' do
+ expect(page).to have_content 'PivotalTracker activated.'
end
step 'I click Flowdock service link' do
@@ -74,8 +70,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see Flowdock service settings saved' do
- expect(find_field('Token').value).to eq 'verySecret'
+ step 'I should see the Flowdock success message' do
+ expect(page).to have_content 'Flowdock activated.'
end
step 'I click Assembla service link' do
@@ -88,8 +84,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see Assembla service settings saved' do
- expect(find_field('Token').value).to eq 'verySecret'
+ step 'I should see the Assembla success message' do
+ expect(page).to have_content 'Assembla activated.'
end
step 'I click Asana service link' do
@@ -103,9 +99,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see Asana service settings saved' do
- expect(find_field('Api key').value).to eq 'verySecret'
- expect(find_field('Restrict to branch').value).to eq 'master'
+ step 'I should see the Asana success message' do
+ expect(page).to have_content 'Asana activated.'
end
step 'I click email on push service link' do
@@ -113,12 +108,13 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
end
step 'I fill email on push settings' do
+ check 'Active'
fill_in 'Recipients', with: 'qa@company.name'
click_button 'Save'
end
- step 'I should see email on push service settings saved' do
- expect(find_field('Recipients').value).to eq 'qa@company.name'
+ step 'I should see the Emails on push success message' do
+ expect(page).to have_content 'Emails on push activated.'
end
step 'I click Irker service link' do
@@ -132,9 +128,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see Irker service settings saved' do
- expect(find_field('Recipients').value).to eq 'irc://chat.freenode.net/#commits'
- expect(find_field('Colorize messages').value).to eq '1'
+ step 'I should see the Irker success message' do
+ expect(page).to have_content 'Irker (IRC gateway) activated.'
end
step 'I click Slack notifications service link' do
@@ -147,8 +142,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see Slack Notifications service settings saved' do
- expect(find_field('Webhook').value).to eq 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685'
+ step 'I should see the Slack notifications success message' do
+ expect(page).to have_content 'Slack notifications activated.'
end
step 'I click Pushover service link' do
@@ -165,12 +160,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see Pushover service settings saved' do
- expect(find_field('Api key').value).to eq 'verySecret'
- expect(find_field('User key').value).to eq 'verySecret'
- expect(find_field('Device').value).to eq 'myDevice'
- expect(find_field('Priority').find('option[selected]').value).to eq '1'
- expect(find_field('Sound').find('option[selected]').value).to eq 'bike'
+ step 'I should see the Pushover success message' do
+ expect(page).to have_content 'Pushover activated.'
end
step 'I click jira service link' do
@@ -178,6 +169,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
end
step 'I fill jira settings' do
+ check 'Active'
+
fill_in 'Web URL', with: 'http://jira.example'
fill_in 'JIRA API URL', with: 'http://jira.example/api'
fill_in 'Username', with: 'gitlab'
@@ -186,11 +179,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see jira service settings saved' do
- expect(find_field('Web URL').value).to eq 'http://jira.example'
- expect(find_field('JIRA API URL').value).to eq 'http://jira.example/api'
- expect(find_field('Username').value).to eq 'gitlab'
- expect(find_field('Project Key').value).to eq 'GITLAB'
+ step 'I should see the JIRA success message' do
+ expect(page).to have_content 'JIRA activated.'
end
step 'I click Atlassian Bamboo CI service link' do
@@ -206,13 +196,13 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see Atlassian Bamboo CI service settings saved' do
- expect(find_field('Bamboo url').value).to eq 'http://bamboo.example.com'
- expect(find_field('Build key').value).to eq 'KEY'
- expect(find_field('Username').value).to eq 'user'
+ step 'I should see the Bamboo success message' do
+ expect(page).to have_content 'Atlassian Bamboo CI activated.'
end
step 'I should see empty field Change Password' do
+ click_link 'Atlassian Bamboo CI'
+
expect(find_field('Enter new password').value).to be_nil
end
@@ -229,9 +219,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see JetBrains TeamCity CI service settings saved' do
- expect(find_field('Teamcity url').value).to eq 'http://teamcity.example.com'
- expect(find_field('Build type').value).to eq 'GitlabTest_Build'
- expect(find_field('Username').value).to eq 'user'
+ step 'I should see the JetBrains success message' do
+ expect(page).to have_content 'JetBrains TeamCity CI activated.'
end
end
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 6efd4374b32..d099d7af167 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -372,6 +372,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
expect(page).to have_content 'Permalink'
expect(page).not_to have_content 'Edit'
expect(page).not_to have_content 'Blame'
+ expect(page).not_to have_content 'Annotate'
expect(page).to have_content 'Delete'
expect(page).to have_content 'Replace'
end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index d00d4fe1737..17008aa6d0f 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -129,6 +129,7 @@ module API
params do
requires :name, type: String, desc: 'The name of the project'
requires :user_id, type: Integer, desc: 'The ID of a user'
+ optional :path, type: String, desc: 'The path of the repository'
optional :default_branch, type: String, desc: 'The default branch of the project'
use :optional_params
use :create_params
diff --git a/lib/system_check.rb b/lib/system_check.rb
new file mode 100644
index 00000000000..466c39904fa
--- /dev/null
+++ b/lib/system_check.rb
@@ -0,0 +1,21 @@
+# Library to perform System Checks
+#
+# Every Check is implemented as its own class inherited from SystemCheck::BaseCheck
+# Execution coordination and boilerplate output is done by the SystemCheck::SimpleExecutor
+#
+# This structure decouples checks from Rake tasks and facilitates unit-testing
+module SystemCheck
+ # Executes a bunch of checks for specified component
+ #
+ # @param [String] component name of the component relative to the checks being executed
+ # @param [Array<BaseCheck>] checks classes of corresponding checks to be executed in the same order
+ def self.run(component, checks = [])
+ executor = SimpleExecutor.new(component)
+
+ checks.each do |check|
+ executor << check
+ end
+
+ executor.execute
+ end
+end
diff --git a/lib/system_check/app/active_users_check.rb b/lib/system_check/app/active_users_check.rb
new file mode 100644
index 00000000000..1d72c8d6903
--- /dev/null
+++ b/lib/system_check/app/active_users_check.rb
@@ -0,0 +1,17 @@
+module SystemCheck
+ module App
+ class ActiveUsersCheck < SystemCheck::BaseCheck
+ set_name 'Active users:'
+
+ def multi_check
+ active_users = User.active.count
+
+ if active_users > 0
+ $stdout.puts active_users.to_s.color(:green)
+ else
+ $stdout.puts active_users.to_s.color(:red)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/database_config_exists_check.rb b/lib/system_check/app/database_config_exists_check.rb
new file mode 100644
index 00000000000..d1fae192350
--- /dev/null
+++ b/lib/system_check/app/database_config_exists_check.rb
@@ -0,0 +1,25 @@
+module SystemCheck
+ module App
+ class DatabaseConfigExistsCheck < SystemCheck::BaseCheck
+ set_name 'Database config exists?'
+
+ def check?
+ database_config_file = Rails.root.join('config', 'database.yml')
+
+ File.exist?(database_config_file)
+ end
+
+ def show_error
+ try_fixing_it(
+ 'Copy config/database.yml.<your db> to config/database.yml',
+ 'Check that the information in config/database.yml is correct'
+ )
+ for_more_information(
+ 'doc/install/databases.md',
+ 'http://guides.rubyonrails.org/getting_started.html#configuring-a-database'
+ )
+ fix_and_rerun
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/git_config_check.rb b/lib/system_check/app/git_config_check.rb
new file mode 100644
index 00000000000..198867f7ac6
--- /dev/null
+++ b/lib/system_check/app/git_config_check.rb
@@ -0,0 +1,42 @@
+module SystemCheck
+ module App
+ class GitConfigCheck < SystemCheck::BaseCheck
+ OPTIONS = {
+ 'core.autocrlf' => 'input'
+ }.freeze
+
+ set_name 'Git configured correctly?'
+
+ def check?
+ correct_options = OPTIONS.map do |name, value|
+ run_command(%W(#{Gitlab.config.git.bin_path} config --global --get #{name})).try(:squish) == value
+ end
+
+ correct_options.all?
+ end
+
+ # Tries to configure git itself
+ #
+ # Returns true if all subcommands were successful (according to their exit code)
+ # Returns false if any or all subcommands failed.
+ def repair!
+ return false unless is_gitlab_user?
+
+ command_success = OPTIONS.map do |name, value|
+ system(*%W(#{Gitlab.config.git.bin_path} config --global #{name} #{value}))
+ end
+
+ command_success.all?
+ end
+
+ def show_error
+ try_fixing_it(
+ sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global core.autocrlf \"#{OPTIONS['core.autocrlf']}\"")
+ )
+ for_more_information(
+ see_installation_guide_section 'GitLab'
+ )
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/git_version_check.rb b/lib/system_check/app/git_version_check.rb
new file mode 100644
index 00000000000..c388682dfb4
--- /dev/null
+++ b/lib/system_check/app/git_version_check.rb
@@ -0,0 +1,29 @@
+module SystemCheck
+ module App
+ class GitVersionCheck < SystemCheck::BaseCheck
+ set_name -> { "Git version >= #{self.required_version} ?" }
+ set_check_pass -> { "yes (#{self.current_version})" }
+
+ def self.required_version
+ @required_version ||= Gitlab::VersionInfo.new(2, 7, 3)
+ end
+
+ def self.current_version
+ @current_version ||= Gitlab::VersionInfo.parse(run_command(%W(#{Gitlab.config.git.bin_path} --version)))
+ end
+
+ def check?
+ self.class.current_version.valid? && self.class.required_version <= self.class.current_version
+ end
+
+ def show_error
+ $stdout.puts "Your git bin path is \"#{Gitlab.config.git.bin_path}\""
+
+ try_fixing_it(
+ "Update your git to a version >= #{self.class.required_version} from #{self.class.current_version}"
+ )
+ fix_and_rerun
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/gitlab_config_exists_check.rb b/lib/system_check/app/gitlab_config_exists_check.rb
new file mode 100644
index 00000000000..247aa0994e4
--- /dev/null
+++ b/lib/system_check/app/gitlab_config_exists_check.rb
@@ -0,0 +1,24 @@
+module SystemCheck
+ module App
+ class GitlabConfigExistsCheck < SystemCheck::BaseCheck
+ set_name 'GitLab config exists?'
+
+ def check?
+ gitlab_config_file = Rails.root.join('config', 'gitlab.yml')
+
+ File.exist?(gitlab_config_file)
+ end
+
+ def show_error
+ try_fixing_it(
+ 'Copy config/gitlab.yml.example to config/gitlab.yml',
+ 'Update config/gitlab.yml to match your setup'
+ )
+ for_more_information(
+ see_installation_guide_section 'GitLab'
+ )
+ fix_and_rerun
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/gitlab_config_up_to_date_check.rb b/lib/system_check/app/gitlab_config_up_to_date_check.rb
new file mode 100644
index 00000000000..c609e48e133
--- /dev/null
+++ b/lib/system_check/app/gitlab_config_up_to_date_check.rb
@@ -0,0 +1,30 @@
+module SystemCheck
+ module App
+ class GitlabConfigUpToDateCheck < SystemCheck::BaseCheck
+ set_name 'GitLab config up to date?'
+ set_skip_reason "can't check because of previous errors"
+
+ def skip?
+ gitlab_config_file = Rails.root.join('config', 'gitlab.yml')
+ !File.exist?(gitlab_config_file)
+ end
+
+ def check?
+ # omniauth or ldap could have been deleted from the file
+ !Gitlab.config['git_host']
+ end
+
+ def show_error
+ try_fixing_it(
+ 'Back-up your config/gitlab.yml',
+ 'Copy config/gitlab.yml.example to config/gitlab.yml',
+ 'Update config/gitlab.yml to match your setup'
+ )
+ for_more_information(
+ see_installation_guide_section 'GitLab'
+ )
+ fix_and_rerun
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/init_script_exists_check.rb b/lib/system_check/app/init_script_exists_check.rb
new file mode 100644
index 00000000000..d246e058e86
--- /dev/null
+++ b/lib/system_check/app/init_script_exists_check.rb
@@ -0,0 +1,27 @@
+module SystemCheck
+ module App
+ class InitScriptExistsCheck < SystemCheck::BaseCheck
+ set_name 'Init script exists?'
+ set_skip_reason 'skipped (omnibus-gitlab has no init script)'
+
+ def skip?
+ omnibus_gitlab?
+ end
+
+ def check?
+ script_path = '/etc/init.d/gitlab'
+ File.exist?(script_path)
+ end
+
+ def show_error
+ try_fixing_it(
+ 'Install the init script'
+ )
+ for_more_information(
+ see_installation_guide_section 'Install Init Script'
+ )
+ fix_and_rerun
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/init_script_up_to_date_check.rb b/lib/system_check/app/init_script_up_to_date_check.rb
new file mode 100644
index 00000000000..015c7ed1731
--- /dev/null
+++ b/lib/system_check/app/init_script_up_to_date_check.rb
@@ -0,0 +1,43 @@
+module SystemCheck
+ module App
+ class InitScriptUpToDateCheck < SystemCheck::BaseCheck
+ SCRIPT_PATH = '/etc/init.d/gitlab'.freeze
+
+ set_name 'Init script up-to-date?'
+ set_skip_reason 'skipped (omnibus-gitlab has no init script)'
+
+ def skip?
+ omnibus_gitlab?
+ end
+
+ def multi_check
+ recipe_path = Rails.root.join('lib/support/init.d/', 'gitlab')
+
+ unless File.exist?(SCRIPT_PATH)
+ $stdout.puts "can't check because of previous errors".color(:magenta)
+ return
+ end
+
+ recipe_content = File.read(recipe_path)
+ script_content = File.read(SCRIPT_PATH)
+
+ if recipe_content == script_content
+ $stdout.puts 'yes'.color(:green)
+ else
+ $stdout.puts 'no'.color(:red)
+ show_error
+ end
+ end
+
+ def show_error
+ try_fixing_it(
+ 'Re-download the init script'
+ )
+ for_more_information(
+ see_installation_guide_section 'Install Init Script'
+ )
+ fix_and_rerun
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/log_writable_check.rb b/lib/system_check/app/log_writable_check.rb
new file mode 100644
index 00000000000..3e0c436d6ee
--- /dev/null
+++ b/lib/system_check/app/log_writable_check.rb
@@ -0,0 +1,28 @@
+module SystemCheck
+ module App
+ class LogWritableCheck < SystemCheck::BaseCheck
+ set_name 'Log directory writable?'
+
+ def check?
+ File.writable?(log_path)
+ end
+
+ def show_error
+ try_fixing_it(
+ "sudo chown -R gitlab #{log_path}",
+ "sudo chmod -R u+rwX #{log_path}"
+ )
+ for_more_information(
+ see_installation_guide_section 'GitLab'
+ )
+ fix_and_rerun
+ end
+
+ private
+
+ def log_path
+ Rails.root.join('log')
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/migrations_are_up_check.rb b/lib/system_check/app/migrations_are_up_check.rb
new file mode 100644
index 00000000000..5eedbacce77
--- /dev/null
+++ b/lib/system_check/app/migrations_are_up_check.rb
@@ -0,0 +1,20 @@
+module SystemCheck
+ module App
+ class MigrationsAreUpCheck < SystemCheck::BaseCheck
+ set_name 'All migrations up?'
+
+ def check?
+ migration_status, _ = Gitlab::Popen.popen(%w(bundle exec rake db:migrate:status))
+
+ migration_status !~ /down\s+\d{14}/
+ end
+
+ def show_error
+ try_fixing_it(
+ sudo_gitlab('bundle exec rake db:migrate RAILS_ENV=production')
+ )
+ fix_and_rerun
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/orphaned_group_members_check.rb b/lib/system_check/app/orphaned_group_members_check.rb
new file mode 100644
index 00000000000..2b46d36fe51
--- /dev/null
+++ b/lib/system_check/app/orphaned_group_members_check.rb
@@ -0,0 +1,20 @@
+module SystemCheck
+ module App
+ class OrphanedGroupMembersCheck < SystemCheck::BaseCheck
+ set_name 'Database contains orphaned GroupMembers?'
+ set_check_pass 'no'
+ set_check_fail 'yes'
+
+ def check?
+ !GroupMember.where('user_id not in (select id from users)').exists?
+ end
+
+ def show_error
+ try_fixing_it(
+ 'You can delete the orphaned records using something along the lines of:',
+ sudo_gitlab("bundle exec rails runner -e production 'GroupMember.where(\"user_id NOT IN (SELECT id FROM users)\").delete_all'")
+ )
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/projects_have_namespace_check.rb b/lib/system_check/app/projects_have_namespace_check.rb
new file mode 100644
index 00000000000..a6ec9f7665c
--- /dev/null
+++ b/lib/system_check/app/projects_have_namespace_check.rb
@@ -0,0 +1,37 @@
+module SystemCheck
+ module App
+ class ProjectsHaveNamespaceCheck < SystemCheck::BaseCheck
+ set_name 'Projects have namespace:'
+ set_skip_reason "can't check, you have no projects"
+
+ def skip?
+ !Project.exists?
+ end
+
+ def multi_check
+ $stdout.puts ''
+
+ Project.find_each(batch_size: 100) do |project|
+ $stdout.print sanitized_message(project)
+
+ if project.namespace
+ $stdout.puts 'yes'.color(:green)
+ else
+ $stdout.puts 'no'.color(:red)
+ show_error
+ end
+ end
+ end
+
+ def show_error
+ try_fixing_it(
+ "Migrate global projects"
+ )
+ for_more_information(
+ "doc/update/5.4-to-6.0.md in section \"#global-projects\""
+ )
+ fix_and_rerun
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/redis_version_check.rb b/lib/system_check/app/redis_version_check.rb
new file mode 100644
index 00000000000..a0610e73576
--- /dev/null
+++ b/lib/system_check/app/redis_version_check.rb
@@ -0,0 +1,25 @@
+module SystemCheck
+ module App
+ class RedisVersionCheck < SystemCheck::BaseCheck
+ MIN_REDIS_VERSION = '2.8.0'.freeze
+ set_name "Redis version >= #{MIN_REDIS_VERSION}?"
+
+ def check?
+ redis_version = run_command(%w(redis-cli --version))
+ redis_version = redis_version.try(:match, /redis-cli (\d+\.\d+\.\d+)/)
+
+ redis_version && (Gem::Version.new(redis_version[1]) > Gem::Version.new(MIN_REDIS_VERSION))
+ end
+
+ def show_error
+ try_fixing_it(
+ "Update your redis server to a version >= #{MIN_REDIS_VERSION}"
+ )
+ for_more_information(
+ 'gitlab-public-wiki/wiki/Trouble-Shooting-Guide in section sidekiq'
+ )
+ fix_and_rerun
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/ruby_version_check.rb b/lib/system_check/app/ruby_version_check.rb
new file mode 100644
index 00000000000..fd82f5f8a4a
--- /dev/null
+++ b/lib/system_check/app/ruby_version_check.rb
@@ -0,0 +1,27 @@
+module SystemCheck
+ module App
+ class RubyVersionCheck < SystemCheck::BaseCheck
+ set_name -> { "Ruby version >= #{self.required_version} ?" }
+ set_check_pass -> { "yes (#{self.current_version})" }
+
+ def self.required_version
+ @required_version ||= Gitlab::VersionInfo.new(2, 3, 3)
+ end
+
+ def self.current_version
+ @current_version ||= Gitlab::VersionInfo.parse(run_command(%w(ruby --version)))
+ end
+
+ def check?
+ self.class.current_version.valid? && self.class.required_version <= self.class.current_version
+ end
+
+ def show_error
+ try_fixing_it(
+ "Update your ruby to a version >= #{self.class.required_version} from #{self.class.current_version}"
+ )
+ fix_and_rerun
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/tmp_writable_check.rb b/lib/system_check/app/tmp_writable_check.rb
new file mode 100644
index 00000000000..99a75e57abf
--- /dev/null
+++ b/lib/system_check/app/tmp_writable_check.rb
@@ -0,0 +1,28 @@
+module SystemCheck
+ module App
+ class TmpWritableCheck < SystemCheck::BaseCheck
+ set_name 'Tmp directory writable?'
+
+ def check?
+ File.writable?(tmp_path)
+ end
+
+ def show_error
+ try_fixing_it(
+ "sudo chown -R gitlab #{tmp_path}",
+ "sudo chmod -R u+rwX #{tmp_path}"
+ )
+ for_more_information(
+ see_installation_guide_section 'GitLab'
+ )
+ fix_and_rerun
+ end
+
+ private
+
+ def tmp_path
+ Rails.root.join('tmp')
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/uploads_directory_exists_check.rb b/lib/system_check/app/uploads_directory_exists_check.rb
new file mode 100644
index 00000000000..7026d0ba075
--- /dev/null
+++ b/lib/system_check/app/uploads_directory_exists_check.rb
@@ -0,0 +1,21 @@
+module SystemCheck
+ module App
+ class UploadsDirectoryExistsCheck < SystemCheck::BaseCheck
+ set_name 'Uploads directory exists?'
+
+ def check?
+ File.directory?(Rails.root.join('public/uploads'))
+ end
+
+ def show_error
+ try_fixing_it(
+ "sudo -u #{gitlab_user} mkdir #{Rails.root}/public/uploads"
+ )
+ for_more_information(
+ see_installation_guide_section 'GitLab'
+ )
+ fix_and_rerun
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/uploads_path_permission_check.rb b/lib/system_check/app/uploads_path_permission_check.rb
new file mode 100644
index 00000000000..7df6c060254
--- /dev/null
+++ b/lib/system_check/app/uploads_path_permission_check.rb
@@ -0,0 +1,36 @@
+module SystemCheck
+ module App
+ class UploadsPathPermissionCheck < SystemCheck::BaseCheck
+ set_name 'Uploads directory has correct permissions?'
+ set_skip_reason 'skipped (no uploads folder found)'
+
+ def skip?
+ !File.directory?(rails_uploads_path)
+ end
+
+ def check?
+ File.stat(uploads_fullpath).mode == 040700
+ end
+
+ def show_error
+ try_fixing_it(
+ "sudo chmod 700 #{uploads_fullpath}"
+ )
+ for_more_information(
+ see_installation_guide_section 'GitLab'
+ )
+ fix_and_rerun
+ end
+
+ private
+
+ def rails_uploads_path
+ Rails.root.join('public/uploads')
+ end
+
+ def uploads_fullpath
+ File.realpath(rails_uploads_path)
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/uploads_path_tmp_permission_check.rb b/lib/system_check/app/uploads_path_tmp_permission_check.rb
new file mode 100644
index 00000000000..b276a81eac1
--- /dev/null
+++ b/lib/system_check/app/uploads_path_tmp_permission_check.rb
@@ -0,0 +1,40 @@
+module SystemCheck
+ module App
+ class UploadsPathTmpPermissionCheck < SystemCheck::BaseCheck
+ set_name 'Uploads directory tmp has correct permissions?'
+ set_skip_reason 'skipped (no tmp uploads folder yet)'
+
+ def skip?
+ !File.directory?(uploads_fullpath) || !Dir.exist?(upload_path_tmp)
+ end
+
+ def check?
+ # If tmp upload dir has incorrect permissions, assume others do as well
+ # Verify drwx------ permissions
+ File.stat(upload_path_tmp).mode == 040700 && File.owned?(upload_path_tmp)
+ end
+
+ def show_error
+ try_fixing_it(
+ "sudo chown -R #{gitlab_user} #{uploads_fullpath}",
+ "sudo find #{uploads_fullpath} -type f -exec chmod 0644 {} \\;",
+ "sudo find #{uploads_fullpath} -type d -not -path #{uploads_fullpath} -exec chmod 0700 {} \\;"
+ )
+ for_more_information(
+ see_installation_guide_section 'GitLab'
+ )
+ fix_and_rerun
+ end
+
+ private
+
+ def upload_path_tmp
+ File.join(uploads_fullpath, 'tmp')
+ end
+
+ def uploads_fullpath
+ File.realpath(Rails.root.join('public/uploads'))
+ end
+ end
+ end
+end
diff --git a/lib/system_check/base_check.rb b/lib/system_check/base_check.rb
new file mode 100644
index 00000000000..5dcb3f0886b
--- /dev/null
+++ b/lib/system_check/base_check.rb
@@ -0,0 +1,129 @@
+module SystemCheck
+ # Base class for Checks. You must inherit from here
+ # and implement the methods below when necessary
+ class BaseCheck
+ include ::SystemCheck::Helpers
+
+ # Define a custom term for when check passed
+ #
+ # @param [String] term used when check passed (default: 'yes')
+ def self.set_check_pass(term)
+ @check_pass = term
+ end
+
+ # Define a custom term for when check failed
+ #
+ # @param [String] term used when check failed (default: 'no')
+ def self.set_check_fail(term)
+ @check_fail = term
+ end
+
+ # Define the name of the SystemCheck that will be displayed during execution
+ #
+ # @param [String] name of the check
+ def self.set_name(name)
+ @name = name
+ end
+
+ # Define the reason why we skipped the SystemCheck
+ #
+ # This is only used if subclass implements `#skip?`
+ #
+ # @param [String] reason to be displayed
+ def self.set_skip_reason(reason)
+ @skip_reason = reason
+ end
+
+ # Term to be displayed when check passed
+ #
+ # @return [String] term when check passed ('yes' if not re-defined in a subclass)
+ def self.check_pass
+ call_or_return(@check_pass) || 'yes'
+ end
+
+ ## Term to be displayed when check failed
+ #
+ # @return [String] term when check failed ('no' if not re-defined in a subclass)
+ def self.check_fail
+ call_or_return(@check_fail) || 'no'
+ end
+
+ # Name of the SystemCheck defined by the subclass
+ #
+ # @return [String] the name
+ def self.display_name
+ call_or_return(@name) || self.name
+ end
+
+ # Skip reason defined by the subclass
+ #
+ # @return [String] the reason
+ def self.skip_reason
+ call_or_return(@skip_reason) || 'skipped'
+ end
+
+ # Does the check support automatically repair routine?
+ #
+ # @return [Boolean] whether check implemented `#repair!` method or not
+ def can_repair?
+ self.class.instance_methods(false).include?(:repair!)
+ end
+
+ def can_skip?
+ self.class.instance_methods(false).include?(:skip?)
+ end
+
+ def is_multi_check?
+ self.class.instance_methods(false).include?(:multi_check)
+ end
+
+ # Execute the check routine
+ #
+ # This is where you should implement the main logic that will return
+ # a boolean at the end
+ #
+ # You should not print any output to STDOUT here, use the specific methods instead
+ #
+ # @return [Boolean] whether check passed or failed
+ def check?
+ raise NotImplementedError
+ end
+
+ # Execute a custom check that cover multiple unities
+ #
+ # When using multi_check you have to provide the output yourself
+ def multi_check
+ raise NotImplementedError
+ end
+
+ # Prints troubleshooting instructions
+ #
+ # This is where you should print detailed information for any error found during #check?
+ #
+ # You may use helper methods to help format the output:
+ #
+ # @see #try_fixing_it
+ # @see #fix_and_rerun
+ # @see #for_more_infromation
+ def show_error
+ raise NotImplementedError
+ end
+
+ # When implemented by a subclass, will attempt to fix the issue automatically
+ def repair!
+ raise NotImplementedError
+ end
+
+ # When implemented by a subclass, will evaluate whether check should be skipped or not
+ #
+ # @return [Boolean] whether or not this check should be skipped
+ def skip?
+ raise NotImplementedError
+ end
+
+ def self.call_or_return(input)
+ input.respond_to?(:call) ? input.call : input
+ end
+ private_class_method :call_or_return
+ end
+end
diff --git a/lib/system_check/helpers.rb b/lib/system_check/helpers.rb
new file mode 100644
index 00000000000..c42ae4fe4c4
--- /dev/null
+++ b/lib/system_check/helpers.rb
@@ -0,0 +1,75 @@
+require 'tasks/gitlab/task_helpers'
+
+module SystemCheck
+ module Helpers
+ include ::Gitlab::TaskHelpers
+
+ # Display a message telling to fix and rerun the checks
+ def fix_and_rerun
+ $stdout.puts ' Please fix the error above and rerun the checks.'.color(:red)
+ end
+
+ # Display a formatted list of references (documentation or links) where to find more information
+ #
+ # @param [Array<String>] sources one or more references (documentation or links)
+ def for_more_information(*sources)
+ $stdout.puts ' For more information see:'.color(:blue)
+ sources.each do |source|
+ $stdout.puts " #{source}"
+ end
+ end
+
+ def see_installation_guide_section(section)
+ "doc/install/installation.md in section \"#{section}\""
+ end
+
+ # @deprecated This will no longer be used when all checks were executed using SystemCheck
+ def finished_checking(component)
+ $stdout.puts ''
+ $stdout.puts "Checking #{component.color(:yellow)} ... #{'Finished'.color(:green)}"
+ $stdout.puts ''
+ end
+
+ # @deprecated This will no longer be used when all checks were executed using SystemCheck
+ def start_checking(component)
+ $stdout.puts "Checking #{component.color(:yellow)} ..."
+ $stdout.puts ''
+ end
+
+ # Display a formatted list of instructions on how to fix the issue identified by the #check?
+ #
+ # @param [Array<String>] steps one or short sentences with help how to fix the issue
+ def try_fixing_it(*steps)
+ steps = steps.shift if steps.first.is_a?(Array)
+
+ $stdout.puts ' Try fixing it:'.color(:blue)
+ steps.each do |step|
+ $stdout.puts " #{step}"
+ end
+ end
+
+ def sanitized_message(project)
+ if should_sanitize?
+ "#{project.namespace_id.to_s.color(:yellow)}/#{project.id.to_s.color(:yellow)} ... "
+ else
+ "#{project.name_with_namespace.color(:yellow)} ... "
+ end
+ end
+
+ def should_sanitize?
+ if ENV['SANITIZE'] == 'true'
+ true
+ else
+ false
+ end
+ end
+
+ def omnibus_gitlab?
+ Dir.pwd == '/opt/gitlab/embedded/service/gitlab-rails'
+ end
+
+ def sudo_gitlab(command)
+ "sudo -u #{gitlab_user} -H #{command}"
+ end
+ end
+end
diff --git a/lib/system_check/simple_executor.rb b/lib/system_check/simple_executor.rb
new file mode 100644
index 00000000000..dc2d4643a01
--- /dev/null
+++ b/lib/system_check/simple_executor.rb
@@ -0,0 +1,99 @@
+module SystemCheck
+ # Simple Executor is current default executor for GitLab
+ # It is a simple port from display logic in the old check.rake
+ #
+ # There is no concurrency level and the output is progressively
+ # printed into the STDOUT
+ #
+ # @attr_reader [Array<BaseCheck>] checks classes of corresponding checks to be executed in the same order
+ # @attr_reader [String] component name of the component relative to the checks being executed
+ class SimpleExecutor
+ attr_reader :checks
+ attr_reader :component
+
+ # @param [String] component name of the component relative to the checks being executed
+ def initialize(component)
+ raise ArgumentError unless component.is_a? String
+
+ @component = component
+ @checks = Set.new
+ end
+
+ # Add a check to be executed
+ #
+ # @param [BaseCheck] check class
+ def <<(check)
+ raise ArgumentError unless check < BaseCheck
+ @checks << check
+ end
+
+ # Executes defined checks in the specified order and outputs confirmation or error information
+ def execute
+ start_checking(component)
+
+ @checks.each do |check|
+ run_check(check)
+ end
+
+ finished_checking(component)
+ end
+
+ # Executes a single check
+ #
+ # @param [SystemCheck::BaseCheck] check_klass
+ def run_check(check_klass)
+ $stdout.print "#{check_klass.display_name} ... "
+
+ check = check_klass.new
+
+ # When implements skip method, we run it first, and if true, skip the check
+ if check.can_skip? && check.skip?
+ $stdout.puts check_klass.skip_reason.color(:magenta)
+ return
+ end
+
+ # When implements a multi check, we don't control the output
+ if check.is_multi_check?
+ check.multi_check
+ return
+ end
+
+ if check.check?
+ $stdout.puts check_klass.check_pass.color(:green)
+ else
+ $stdout.puts check_klass.check_fail.color(:red)
+
+ if check.can_repair?
+ $stdout.print 'Trying to fix error automatically. ...'
+ if check.repair!
+ $stdout.puts 'Success'.color(:green)
+ return
+ else
+ $stdout.puts 'Failed'.color(:red)
+ end
+ end
+
+ check.show_error
+ end
+ end
+
+ private
+
+ # Prints header content for the series of checks to be executed for this component
+ #
+ # @param [String] component name of the component relative to the checks being executed
+ def start_checking(component)
+ $stdout.puts "Checking #{component.color(:yellow)} ..."
+ $stdout.puts ''
+ end
+
+ # Prints footer content for the series of checks executed for this component
+ #
+ # @param [String] component name of the component relative to the checks being executed
+ def finished_checking(component)
+ $stdout.puts ''
+ $stdout.puts "Checking #{component.color(:yellow)} ... #{'Finished'.color(:green)}"
+ $stdout.puts ''
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index f41c73154f5..63c5e9b9c83 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -1,5 +1,9 @@
+# Temporary hack, until we migrate all checks to SystemCheck format
+require 'system_check'
+require 'system_check/helpers'
+
namespace :gitlab do
- desc "GitLab | Check the configuration of GitLab and its environment"
+ desc 'GitLab | Check the configuration of GitLab and its environment'
task check: %w{gitlab:gitlab_shell:check
gitlab:sidekiq:check
gitlab:incoming_email:check
@@ -7,331 +11,38 @@ namespace :gitlab do
gitlab:app:check}
namespace :app do
- desc "GitLab | Check the configuration of the GitLab Rails app"
+ desc 'GitLab | Check the configuration of the GitLab Rails app'
task check: :environment do
warn_user_is_not_gitlab
- start_checking "GitLab"
-
- check_git_config
- check_database_config_exists
- check_migrations_are_up
- check_orphaned_group_members
- check_gitlab_config_exists
- check_gitlab_config_not_outdated
- check_log_writable
- check_tmp_writable
- check_uploads
- check_init_script_exists
- check_init_script_up_to_date
- check_projects_have_namespace
- check_redis_version
- check_ruby_version
- check_git_version
- check_active_users
-
- finished_checking "GitLab"
- end
-
- # Checks
- ########################
-
- def check_git_config
- print "Git configured with autocrlf=input? ... "
-
- options = {
- "core.autocrlf" => "input"
- }
-
- correct_options = options.map do |name, value|
- run_command(%W(#{Gitlab.config.git.bin_path} config --global --get #{name})).try(:squish) == value
- end
-
- if correct_options.all?
- puts "yes".color(:green)
- else
- print "Trying to fix Git error automatically. ..."
-
- if auto_fix_git_config(options)
- puts "Success".color(:green)
- else
- puts "Failed".color(:red)
- try_fixing_it(
- sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global core.autocrlf \"#{options["core.autocrlf"]}\"")
- )
- for_more_information(
- see_installation_guide_section "GitLab"
- )
- end
- end
- end
-
- def check_database_config_exists
- print "Database config exists? ... "
-
- database_config_file = Rails.root.join("config", "database.yml")
-
- if File.exist?(database_config_file)
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "Copy config/database.yml.<your db> to config/database.yml",
- "Check that the information in config/database.yml is correct"
- )
- for_more_information(
- see_database_guide,
- "http://guides.rubyonrails.org/getting_started.html#configuring-a-database"
- )
- fix_and_rerun
- end
- end
-
- def check_gitlab_config_exists
- print "GitLab config exists? ... "
-
- gitlab_config_file = Rails.root.join("config", "gitlab.yml")
-
- if File.exist?(gitlab_config_file)
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "Copy config/gitlab.yml.example to config/gitlab.yml",
- "Update config/gitlab.yml to match your setup"
- )
- for_more_information(
- see_installation_guide_section "GitLab"
- )
- fix_and_rerun
- end
- end
-
- def check_gitlab_config_not_outdated
- print "GitLab config outdated? ... "
-
- gitlab_config_file = Rails.root.join("config", "gitlab.yml")
- unless File.exist?(gitlab_config_file)
- puts "can't check because of previous errors".color(:magenta)
- end
-
- # omniauth or ldap could have been deleted from the file
- unless Gitlab.config['git_host']
- puts "no".color(:green)
- else
- puts "yes".color(:red)
- try_fixing_it(
- "Backup your config/gitlab.yml",
- "Copy config/gitlab.yml.example to config/gitlab.yml",
- "Update config/gitlab.yml to match your setup"
- )
- for_more_information(
- see_installation_guide_section "GitLab"
- )
- fix_and_rerun
- end
- end
-
- def check_init_script_exists
- print "Init script exists? ... "
-
- if omnibus_gitlab?
- puts 'skipped (omnibus-gitlab has no init script)'.color(:magenta)
- return
- end
-
- script_path = "/etc/init.d/gitlab"
-
- if File.exist?(script_path)
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "Install the init script"
- )
- for_more_information(
- see_installation_guide_section "Install Init Script"
- )
- fix_and_rerun
- end
- end
-
- def check_init_script_up_to_date
- print "Init script up-to-date? ... "
-
- if omnibus_gitlab?
- puts 'skipped (omnibus-gitlab has no init script)'.color(:magenta)
- return
- end
-
- recipe_path = Rails.root.join("lib/support/init.d/", "gitlab")
- script_path = "/etc/init.d/gitlab"
-
- unless File.exist?(script_path)
- puts "can't check because of previous errors".color(:magenta)
- return
- end
-
- recipe_content = File.read(recipe_path)
- script_content = File.read(script_path)
-
- if recipe_content == script_content
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "Redownload the init script"
- )
- for_more_information(
- see_installation_guide_section "Install Init Script"
- )
- fix_and_rerun
- end
- end
-
- def check_migrations_are_up
- print "All migrations up? ... "
-
- migration_status, _ = Gitlab::Popen.popen(%w(bundle exec rake db:migrate:status))
-
- unless migration_status =~ /down\s+\d{14}/
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- sudo_gitlab("bundle exec rake db:migrate RAILS_ENV=production")
- )
- fix_and_rerun
- end
- end
-
- def check_orphaned_group_members
- print "Database contains orphaned GroupMembers? ... "
- if GroupMember.where("user_id not in (select id from users)").count > 0
- puts "yes".color(:red)
- try_fixing_it(
- "You can delete the orphaned records using something along the lines of:",
- sudo_gitlab("bundle exec rails runner -e production 'GroupMember.where(\"user_id NOT IN (SELECT id FROM users)\").delete_all'")
- )
- else
- puts "no".color(:green)
- end
- end
-
- def check_log_writable
- print "Log directory writable? ... "
-
- log_path = Rails.root.join("log")
-
- if File.writable?(log_path)
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "sudo chown -R gitlab #{log_path}",
- "sudo chmod -R u+rwX #{log_path}"
- )
- for_more_information(
- see_installation_guide_section "GitLab"
- )
- fix_and_rerun
- end
- end
- def check_tmp_writable
- print "Tmp directory writable? ... "
-
- tmp_path = Rails.root.join("tmp")
-
- if File.writable?(tmp_path)
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "sudo chown -R gitlab #{tmp_path}",
- "sudo chmod -R u+rwX #{tmp_path}"
- )
- for_more_information(
- see_installation_guide_section "GitLab"
- )
- fix_and_rerun
- end
- end
-
- def check_uploads
- print "Uploads directory setup correctly? ... "
-
- unless File.directory?(Rails.root.join('public/uploads'))
- puts "no".color(:red)
- try_fixing_it(
- "sudo -u #{gitlab_user} mkdir #{Rails.root}/public/uploads"
- )
- for_more_information(
- see_installation_guide_section "GitLab"
- )
- fix_and_rerun
- return
- end
-
- upload_path = File.realpath(Rails.root.join('public/uploads'))
- upload_path_tmp = File.join(upload_path, 'tmp')
-
- if File.stat(upload_path).mode == 040700
- unless Dir.exist?(upload_path_tmp)
- puts 'skipped (no tmp uploads folder yet)'.color(:magenta)
- return
- end
-
- # If tmp upload dir has incorrect permissions, assume others do as well
- # Verify drwx------ permissions
- if File.stat(upload_path_tmp).mode == 040700 && File.owned?(upload_path_tmp)
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "sudo chown -R #{gitlab_user} #{upload_path}",
- "sudo find #{upload_path} -type f -exec chmod 0644 {} \\;",
- "sudo find #{upload_path} -type d -not -path #{upload_path} -exec chmod 0700 {} \\;"
- )
- for_more_information(
- see_installation_guide_section "GitLab"
- )
- fix_and_rerun
- end
- else
- puts "no".color(:red)
- try_fixing_it(
- "sudo chmod 700 #{upload_path}"
- )
- for_more_information(
- see_installation_guide_section "GitLab"
- )
- fix_and_rerun
- end
- end
-
- def check_redis_version
- min_redis_version = "2.8.0"
- print "Redis version >= #{min_redis_version}? ... "
-
- redis_version = run_command(%w(redis-cli --version))
- redis_version = redis_version.try(:match, /redis-cli (\d+\.\d+\.\d+)/)
- if redis_version &&
- (Gem::Version.new(redis_version[1]) > Gem::Version.new(min_redis_version))
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "Update your redis server to a version >= #{min_redis_version}"
- )
- for_more_information(
- "gitlab-public-wiki/wiki/Trouble-Shooting-Guide in section sidekiq"
- )
- fix_and_rerun
- end
+ checks = [
+ SystemCheck::App::GitConfigCheck,
+ SystemCheck::App::DatabaseConfigExistsCheck,
+ SystemCheck::App::MigrationsAreUpCheck,
+ SystemCheck::App::OrphanedGroupMembersCheck,
+ SystemCheck::App::GitlabConfigExistsCheck,
+ SystemCheck::App::GitlabConfigUpToDateCheck,
+ SystemCheck::App::LogWritableCheck,
+ SystemCheck::App::TmpWritableCheck,
+ SystemCheck::App::UploadsDirectoryExistsCheck,
+ SystemCheck::App::UploadsPathPermissionCheck,
+ SystemCheck::App::UploadsPathTmpPermissionCheck,
+ SystemCheck::App::InitScriptExistsCheck,
+ SystemCheck::App::InitScriptUpToDateCheck,
+ SystemCheck::App::ProjectsHaveNamespaceCheck,
+ SystemCheck::App::RedisVersionCheck,
+ SystemCheck::App::RubyVersionCheck,
+ SystemCheck::App::GitVersionCheck,
+ SystemCheck::App::ActiveUsersCheck
+ ]
+
+ SystemCheck.run('GitLab', checks)
end
end
namespace :gitlab_shell do
+ include SystemCheck::Helpers
+
desc "GitLab | Check the configuration of GitLab Shell"
task check: :environment do
warn_user_is_not_gitlab
@@ -513,33 +224,6 @@ namespace :gitlab do
end
end
- def check_projects_have_namespace
- print "projects have namespace: ... "
-
- unless Project.count > 0
- puts "can't check, you have no projects".color(:magenta)
- return
- end
- puts ""
-
- Project.find_each(batch_size: 100) do |project|
- print sanitized_message(project)
-
- if project.namespace
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "Migrate global projects"
- )
- for_more_information(
- "doc/update/5.4-to-6.0.md in section \"#global-projects\""
- )
- fix_and_rerun
- end
- end
- end
-
# Helper methods
########################
@@ -565,6 +249,8 @@ namespace :gitlab do
end
namespace :sidekiq do
+ include SystemCheck::Helpers
+
desc "GitLab | Check the configuration of Sidekiq"
task check: :environment do
warn_user_is_not_gitlab
@@ -623,6 +309,8 @@ namespace :gitlab do
end
namespace :incoming_email do
+ include SystemCheck::Helpers
+
desc "GitLab | Check the configuration of Reply by email"
task check: :environment do
warn_user_is_not_gitlab
@@ -757,6 +445,8 @@ namespace :gitlab do
end
namespace :ldap do
+ include SystemCheck::Helpers
+
task :check, [:limit] => :environment do |_, args|
# Only show up to 100 results because LDAP directories can be very big.
# This setting only affects the `rake gitlab:check` script.
@@ -812,6 +502,8 @@ namespace :gitlab do
end
namespace :repo do
+ include SystemCheck::Helpers
+
desc "GitLab | Check the integrity of the repositories managed by GitLab"
task check: :environment do
Gitlab.config.repositories.storages.each do |name, repository_storage|
@@ -826,6 +518,8 @@ namespace :gitlab do
end
namespace :user do
+ include SystemCheck::Helpers
+
desc "GitLab | Check the integrity of a specific user's repositories"
task :check_repos, [:username] => :environment do |t, args|
username = args[:username] || prompt("Check repository integrity for fsername? ".color(:blue))
@@ -848,55 +542,6 @@ namespace :gitlab do
# Helper methods
##########################
- def fix_and_rerun
- puts " Please fix the error above and rerun the checks.".color(:red)
- end
-
- def for_more_information(*sources)
- sources = sources.shift if sources.first.is_a?(Array)
-
- puts " For more information see:".color(:blue)
- sources.each do |source|
- puts " #{source}"
- end
- end
-
- def finished_checking(component)
- puts ""
- puts "Checking #{component.color(:yellow)} ... #{"Finished".color(:green)}"
- puts ""
- end
-
- def see_database_guide
- "doc/install/databases.md"
- end
-
- def see_installation_guide_section(section)
- "doc/install/installation.md in section \"#{section}\""
- end
-
- def sudo_gitlab(command)
- "sudo -u #{gitlab_user} -H #{command}"
- end
-
- def gitlab_user
- Gitlab.config.gitlab.user
- end
-
- def start_checking(component)
- puts "Checking #{component.color(:yellow)} ..."
- puts ""
- end
-
- def try_fixing_it(*steps)
- steps = steps.shift if steps.first.is_a?(Array)
-
- puts " Try fixing it:".color(:blue)
- steps.each do |step|
- puts " #{step}"
- end
- end
-
def check_gitlab_shell
required_version = Gitlab::VersionInfo.new(gitlab_shell_major_version, gitlab_shell_minor_version, gitlab_shell_patch_version)
current_version = Gitlab::VersionInfo.parse(gitlab_shell_version)
@@ -909,65 +554,6 @@ namespace :gitlab do
end
end
- def check_ruby_version
- required_version = Gitlab::VersionInfo.new(2, 1, 0)
- current_version = Gitlab::VersionInfo.parse(run_command(%w(ruby --version)))
-
- print "Ruby version >= #{required_version} ? ... "
-
- if current_version.valid? && required_version <= current_version
- puts "yes (#{current_version})".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "Update your ruby to a version >= #{required_version} from #{current_version}"
- )
- fix_and_rerun
- end
- end
-
- def check_git_version
- required_version = Gitlab::VersionInfo.new(2, 7, 3)
- current_version = Gitlab::VersionInfo.parse(run_command(%W(#{Gitlab.config.git.bin_path} --version)))
-
- puts "Your git bin path is \"#{Gitlab.config.git.bin_path}\""
- print "Git version >= #{required_version} ? ... "
-
- if current_version.valid? && required_version <= current_version
- puts "yes (#{current_version})".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "Update your git to a version >= #{required_version} from #{current_version}"
- )
- fix_and_rerun
- end
- end
-
- def check_active_users
- puts "Active users: #{User.active.count}"
- end
-
- def omnibus_gitlab?
- Dir.pwd == '/opt/gitlab/embedded/service/gitlab-rails'
- end
-
- def sanitized_message(project)
- if should_sanitize?
- "#{project.namespace_id.to_s.color(:yellow)}/#{project.id.to_s.color(:yellow)} ... "
- else
- "#{project.name_with_namespace.color(:yellow)} ... "
- end
- end
-
- def should_sanitize?
- if ENV['SANITIZE'] == "true"
- true
- else
- false
- end
- end
-
def check_repo_integrity(repo_dir)
puts "\nChecking repo at #{repo_dir.color(:yellow)}"
diff --git a/lib/tasks/gitlab/task_helpers.rb b/lib/tasks/gitlab/task_helpers.rb
index e3c9d3b491c..964aa0fe1bc 100644
--- a/lib/tasks/gitlab/task_helpers.rb
+++ b/lib/tasks/gitlab/task_helpers.rb
@@ -98,34 +98,30 @@ module Gitlab
end
end
+ def gitlab_user
+ Gitlab.config.gitlab.user
+ end
+
+ def is_gitlab_user?
+ return @is_gitlab_user unless @is_gitlab_user.nil?
+
+ current_user = run_command(%w(whoami)).chomp
+ @is_gitlab_user = current_user == gitlab_user
+ end
+
def warn_user_is_not_gitlab
- unless @warned_user_not_gitlab
- gitlab_user = Gitlab.config.gitlab.user
+ return if @warned_user_not_gitlab
+
+ unless is_gitlab_user?
current_user = run_command(%w(whoami)).chomp
- unless current_user == gitlab_user
- puts " Warning ".color(:black).background(:yellow)
- puts " You are running as user #{current_user.color(:magenta)}, we hope you know what you are doing."
- puts " Things may work\/fail for the wrong reasons."
- puts " For correct results you should run this as user #{gitlab_user.color(:magenta)}."
- puts ""
- end
- @warned_user_not_gitlab = true
- end
- end
- # Tries to configure git itself
- #
- # Returns true if all subcommands were successfull (according to their exit code)
- # Returns false if any or all subcommands failed.
- def auto_fix_git_config(options)
- if !@warned_user_not_gitlab
- command_success = options.map do |name, value|
- system(*%W(#{Gitlab.config.git.bin_path} config --global #{name} #{value}))
- end
+ puts " Warning ".color(:black).background(:yellow)
+ puts " You are running as user #{current_user.color(:magenta)}, we hope you know what you are doing."
+ puts " Things may work\/fail for the wrong reasons."
+ puts " For correct results you should run this as user #{gitlab_user.color(:magenta)}."
+ puts ""
- command_success.all?
- else
- false
+ @warned_user_not_gitlab = true
end
end
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index 2d892f4a2b7..23b463c0082 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -3,7 +3,9 @@ require 'spec_helper'
describe Projects::ServicesController do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
- let(:service) { create(:service, project: project) }
+ let(:service) { create(:hipchat_service, project: project) }
+ let(:hipchat_client) { { '#room' => double(send: true) } }
+ let(:service_params) { { token: 'hipchat_token_p', room: '#room' } }
before do
sign_in(user)
@@ -13,97 +15,81 @@ describe Projects::ServicesController do
controller.instance_variable_set(:@service, service)
end
- shared_examples_for 'services controller' do |referrer|
- before do
- request.env["HTTP_REFERER"] = referrer
- end
-
- describe "#test" do
- context 'when can_test? returns false' do
- it 'renders 404' do
- allow_any_instance_of(Service).to receive(:can_test?).and_return(false)
+ describe '#test' do
+ context 'when can_test? returns false' do
+ it 'renders 404' do
+ allow_any_instance_of(Service).to receive(:can_test?).and_return(false)
- get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
+ put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id
- expect(response).to have_http_status(404)
- end
+ expect(response).to have_http_status(404)
end
+ end
- context 'success' do
- context 'with empty project' do
- let(:project) { create(:empty_project) }
-
- context 'with chat notification service' do
- let(:service) { project.create_microsoft_teams_service(webhook: 'http://webhook.com') }
-
- it 'redirects and show success message' do
- allow_any_instance_of(MicrosoftTeams::Notifier).to receive(:ping).and_return(true)
-
- get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
+ context 'success' do
+ context 'with empty project' do
+ let(:project) { create(:empty_project) }
- expect(response).to redirect_to(root_path)
- expect(flash[:notice]).to eq('We sent a request to the provided URL')
- end
- end
+ context 'with chat notification service' do
+ let(:service) { project.create_microsoft_teams_service(webhook: 'http://webhook.com') }
- it 'redirects and show success message' do
- expect(service).to receive(:test).and_return(success: true, result: 'done')
+ it 'returns success' do
+ allow_any_instance_of(MicrosoftTeams::Notifier).to receive(:ping).and_return(true)
- get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
+ put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id
- expect(response).to redirect_to(root_path)
- expect(flash[:notice]).to eq('We sent a request to the provided URL')
+ expect(response.status).to eq(200)
end
end
- it "redirects and show success message" do
- expect(service).to receive(:test).and_return(success: true, result: 'done')
+ it 'returns success' do
+ expect(HipChat::Client).to receive(:new).with('hipchat_token_p', anything).and_return(hipchat_client)
- get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
+ put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: service_params
- expect(response).to redirect_to(root_path)
- expect(flash[:notice]).to eq('We sent a request to the provided URL')
+ expect(response.status).to eq(200)
end
end
- context 'failure' do
- it "redirects and show failure message" do
- expect(service).to receive(:test).and_return(success: false, result: 'Bad test')
+ it 'returns success' do
+ expect(HipChat::Client).to receive(:new).with('hipchat_token_p', anything).and_return(hipchat_client)
- get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
+ put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: service_params
- expect(response).to redirect_to(root_path)
- expect(flash[:alert]).to eq('We tried to send a request to the provided URL but an error occurred: Bad test')
- end
+ expect(response.status).to eq(200)
end
end
- end
- describe 'referrer defined' do
- it_should_behave_like 'services controller' do
- let!(:referrer) { "/" }
- end
- end
+ context 'failure' do
+ it 'returns success status code and the error message' do
+ expect(HipChat::Client).to receive(:new).with('hipchat_token_p', anything).and_raise('Bad test')
- describe 'referrer undefined' do
- it_should_behave_like 'services controller' do
- let!(:referrer) { nil }
+ put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: service_params
+
+ expect(response.status).to eq(200)
+ expect(JSON.parse(response.body)).
+ to eq('error' => true, 'message' => 'Test failed.', 'service_response' => 'Bad test')
+ end
end
end
describe 'PUT #update' do
- context 'on successful update' do
- it 'sets the flash' do
- expect(service).to receive(:to_param).and_return('hipchat')
- expect(service).to receive(:event_names).and_return(HipchatService.event_names)
+ context 'when param `active` is set to true' do
+ it 'activates the service and redirects to integrations paths' do
+ put :update,
+ namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: { active: true }
+
+ expect(response).to redirect_to(namespace_project_settings_integrations_path(project.namespace, project))
+ expect(flash[:notice]).to eq 'HipChat activated.'
+ end
+ end
+ context 'when param `active` is set to false' do
+ it 'does not activate the service but saves the settings' do
put :update,
- namespace_id: project.namespace.id,
- project_id: project.id,
- id: service.id,
- service: { active: false }
+ namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: { active: false }
- expect(flash[:notice]).to eq 'Successfully updated.'
+ expect(flash[:notice]).to eq 'HipChat settings saved, but not activated.'
end
end
end
diff --git a/spec/db/production/settings.rb b/spec/db/production/settings.rb
index 007b35bbb77..3cbb173c4cc 100644
--- a/spec/db/production/settings.rb
+++ b/spec/db/production/settings.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-require 'rainbow/ext/string'
describe 'seed production settings', lib: true do
include StubENV
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
index 3fad4d2d658..e7366a7fd1c 100644
--- a/spec/factories/services.rb
+++ b/spec/factories/services.rb
@@ -33,4 +33,10 @@ FactoryGirl.define do
project_key: 'jira-key'
)
end
+
+ factory :hipchat_service do
+ project factory: :empty_project
+ type 'HipchatService'
+ token 'test_token'
+ end
end
diff --git a/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
index d94204230f6..53c5a52ce3a 100644
--- a/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
+++ b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
@@ -55,7 +55,7 @@ feature 'Blob button line permalinks (BlobLinePermalinkUpdater)', feature: true,
end
end
- describe 'Click "Blame" button' do
+ describe 'Click "Annotate" button' do
it 'works with no initial line number fragment hash' do
visit_blob
diff --git a/spec/features/projects/files/browse_files_spec.rb b/spec/features/projects/files/browse_files_spec.rb
index c0a9327249c..30a1eedbb48 100644
--- a/spec/features/projects/files/browse_files_spec.rb
+++ b/spec/features/projects/files/browse_files_spec.rb
@@ -12,7 +12,7 @@ feature 'user browses project', feature: true, js: true do
scenario "can see blame of '.gitignore'" do
click_link ".gitignore"
- click_link 'Blame'
+ click_link 'Annotate'
expect(page).to have_content "*.rb"
expect(page).to have_content "Dmitriy Zaporozhets"
diff --git a/spec/features/projects/services/jira_service_spec.rb b/spec/features/projects/services/jira_service_spec.rb
new file mode 100644
index 00000000000..c96d87e5708
--- /dev/null
+++ b/spec/features/projects/services/jira_service_spec.rb
@@ -0,0 +1,92 @@
+require 'spec_helper'
+
+feature 'Setup Jira service', :feature, :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:service) { project.create_jira_service }
+
+ let(:url) { 'http://jira.example.com' }
+ let(:project_url) { 'http://username:password@jira.example.com/rest/api/2/project/GitLabProject' }
+
+ def fill_form(active = true)
+ check 'Active' if active
+
+ fill_in 'service_url', with: url
+ fill_in 'service_project_key', with: 'GitLabProject'
+ fill_in 'service_username', with: 'username'
+ fill_in 'service_password', with: 'password'
+ fill_in 'service_jira_issue_transition_id', with: '25'
+ end
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+
+ visit namespace_project_settings_integrations_path(project.namespace, project)
+ end
+
+ describe 'user sets and activates Jira Service' do
+ context 'when Jira connection test succeeds' do
+ before do
+ WebMock.stub_request(:get, project_url)
+ end
+
+ it 'activates the JIRA service' do
+ click_link('JIRA')
+ fill_form
+ click_button('Test settings and save changes')
+ wait_for_requests
+
+ expect(page).to have_content('JIRA activated.')
+ expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project))
+ end
+ end
+
+ context 'when Jira connection test fails' do
+ before do
+ WebMock.stub_request(:get, project_url).to_return(status: 401)
+ end
+
+ it 'shows errors when some required fields are not filled in' do
+ click_link('JIRA')
+
+ check 'Active'
+ fill_in 'service_password', with: 'password'
+ click_button('Test settings and save changes')
+
+ page.within('.service-settings') do
+ expect(page).to have_content('This field is required.')
+ end
+ end
+
+ it 'activates the JIRA service' do
+ click_link('JIRA')
+ fill_form
+ click_button('Test settings and save changes')
+ wait_for_requests
+
+ expect(find('.flash-container-page')).to have_content 'Test failed.'
+ expect(find('.flash-container-page')).to have_content 'Save anyway'
+
+ find('.flash-alert .flash-action').trigger('click')
+ wait_for_requests
+
+ expect(page).to have_content('JIRA activated.')
+ expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project))
+ end
+ end
+ end
+
+ describe 'user sets Jira Service but keeps it disabled' do
+ context 'when Jira connection test succeeds' do
+ it 'activates the JIRA service' do
+ click_link('JIRA')
+ fill_form(false)
+ click_button('Save changes')
+
+ expect(page).to have_content('JIRA settings saved, but not activated.')
+ expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project))
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb
index dc3854262e7..1fe82222e59 100644
--- a/spec/features/projects/services/mattermost_slash_command_spec.rb
+++ b/spec/features/projects/services/mattermost_slash_command_spec.rb
@@ -24,15 +24,25 @@ feature 'Setup Mattermost slash commands', :feature, :js do
expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
end
- it 'shows the token after saving' do
+ it 'redirects to the integrations page after saving but not activating' do
token = ('a'..'z').to_a.join
fill_in 'service_token', with: token
- click_on 'Save'
+ click_on 'Save changes'
- value = find_field('service_token').value
+ expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project))
+ expect(page).to have_content('Mattermost slash commands settings saved, but not activated.')
+ end
+
+ it 'redirects to the integrations page after activating' do
+ token = ('a'..'z').to_a.join
+
+ fill_in 'service_token', with: token
+ check 'service_active'
+ click_on 'Save changes'
- expect(value).to eq(token)
+ expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project))
+ expect(page).to have_content('Mattermost slash commands activated.')
end
it 'shows the add to mattermost button' do
diff --git a/spec/features/projects/services/slack_slash_command_spec.rb b/spec/features/projects/services/slack_slash_command_spec.rb
index db903a0c8f0..f53b820c460 100644
--- a/spec/features/projects/services/slack_slash_command_spec.rb
+++ b/spec/features/projects/services/slack_slash_command_spec.rb
@@ -21,13 +21,21 @@ feature 'Slack slash commands', feature: true do
expect(page).to have_content('This service allows users to perform common')
end
- it 'shows the token after saving' do
+ it 'redirects to the integrations page after saving but not activating' do
fill_in 'service_token', with: 'token'
click_on 'Save'
- value = find_field('service_token').value
+ expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project))
+ expect(page).to have_content('Slack slash commands settings saved, but not activated.')
+ end
+
+ it 'redirects to the integrations page after activating' do
+ fill_in 'service_token', with: 'token'
+ check 'service_active'
+ click_on 'Save'
- expect(value).to eq('token')
+ expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project))
+ expect(page).to have_content('Slack slash commands activated.')
end
it 'shows the correct trigger url' do
diff --git a/spec/javascripts/fixtures/services.rb b/spec/javascripts/fixtures/services.rb
new file mode 100644
index 00000000000..554451d1bbf
--- /dev/null
+++ b/spec/javascripts/fixtures/services.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Projects::ServicesController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') }
+ let!(:service) { create(:custom_issue_tracker_service, project: project, title: 'Custom Issue Tracker') }
+
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('services/')
+ end
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'services/edit_service.html.raw' do |example|
+ get :edit,
+ namespace_id: namespace,
+ project_id: project,
+ id: service.to_param
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/integrations/integration_settings_form_spec.js b/spec/javascripts/integrations/integration_settings_form_spec.js
new file mode 100644
index 00000000000..45909d4e70e
--- /dev/null
+++ b/spec/javascripts/integrations/integration_settings_form_spec.js
@@ -0,0 +1,199 @@
+import IntegrationSettingsForm from '~/integrations/integration_settings_form';
+
+describe('IntegrationSettingsForm', () => {
+ const FIXTURE = 'services/edit_service.html.raw';
+ preloadFixtures(FIXTURE);
+
+ beforeEach(() => {
+ loadFixtures(FIXTURE);
+ });
+
+ describe('contructor', () => {
+ let integrationSettingsForm;
+
+ beforeEach(() => {
+ integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ spyOn(integrationSettingsForm, 'init');
+ });
+
+ it('should initialize form element refs on class object', () => {
+ // Form Reference
+ expect(integrationSettingsForm.$form).toBeDefined();
+ expect(integrationSettingsForm.$form.prop('nodeName')).toEqual('FORM');
+
+ // Form Child Elements
+ expect(integrationSettingsForm.$serviceToggle).toBeDefined();
+ expect(integrationSettingsForm.$submitBtn).toBeDefined();
+ expect(integrationSettingsForm.$submitBtnLoader).toBeDefined();
+ expect(integrationSettingsForm.$submitBtnLabel).toBeDefined();
+ });
+
+ it('should initialize form metadata on class object', () => {
+ expect(integrationSettingsForm.testEndPoint).toBeDefined();
+ expect(integrationSettingsForm.canTestService).toBeDefined();
+ });
+ });
+
+ describe('toggleServiceState', () => {
+ let integrationSettingsForm;
+
+ beforeEach(() => {
+ integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ });
+
+ it('should remove `novalidate` attribute to form when called with `true`', () => {
+ integrationSettingsForm.toggleServiceState(true);
+
+ expect(integrationSettingsForm.$form.attr('novalidate')).not.toBeDefined();
+ });
+
+ it('should set `novalidate` attribute to form when called with `false`', () => {
+ integrationSettingsForm.toggleServiceState(false);
+
+ expect(integrationSettingsForm.$form.attr('novalidate')).toBeDefined();
+ });
+ });
+
+ describe('toggleSubmitBtnLabel', () => {
+ let integrationSettingsForm;
+
+ beforeEach(() => {
+ integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ });
+
+ it('should set Save button label to "Test settings and save changes" when serviceActive & canTestService are `true`', () => {
+ integrationSettingsForm.canTestService = true;
+
+ integrationSettingsForm.toggleSubmitBtnLabel(true);
+ expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Test settings and save changes');
+ });
+
+ it('should set Save button label to "Save changes" when either serviceActive or canTestService (or both) is `false`', () => {
+ integrationSettingsForm.canTestService = false;
+
+ integrationSettingsForm.toggleSubmitBtnLabel(false);
+ expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes');
+
+ integrationSettingsForm.toggleSubmitBtnLabel(true);
+ expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes');
+
+ integrationSettingsForm.canTestService = true;
+
+ integrationSettingsForm.toggleSubmitBtnLabel(false);
+ expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes');
+ });
+ });
+
+ describe('toggleSubmitBtnState', () => {
+ let integrationSettingsForm;
+
+ beforeEach(() => {
+ integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ });
+
+ it('should disable Save button and show loader animation when called with `true`', () => {
+ integrationSettingsForm.toggleSubmitBtnState(true);
+
+ expect(integrationSettingsForm.$submitBtn.is(':disabled')).toBeTruthy();
+ expect(integrationSettingsForm.$submitBtnLoader.hasClass('hidden')).toBeFalsy();
+ });
+
+ it('should enable Save button and hide loader animation when called with `false`', () => {
+ integrationSettingsForm.toggleSubmitBtnState(false);
+
+ expect(integrationSettingsForm.$submitBtn.is(':disabled')).toBeFalsy();
+ expect(integrationSettingsForm.$submitBtnLoader.hasClass('hidden')).toBeTruthy();
+ });
+ });
+
+ describe('testSettings', () => {
+ let integrationSettingsForm;
+ let formData;
+
+ beforeEach(() => {
+ integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ formData = integrationSettingsForm.$form.serialize();
+ });
+
+ it('should make an ajax request with provided `formData`', () => {
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+
+ integrationSettingsForm.testSettings(formData);
+
+ expect($.ajax).toHaveBeenCalledWith({
+ type: 'PUT',
+ url: integrationSettingsForm.testEndPoint,
+ data: formData,
+ });
+ });
+
+ it('should show error Flash with `Save anyway` action if ajax request responds with error in test', () => {
+ const errorMessage = 'Test failed.';
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+
+ integrationSettingsForm.testSettings(formData);
+
+ deferred.resolve({ error: true, message: errorMessage });
+
+ const $flashContainer = $('.flash-container');
+ expect($flashContainer.find('.flash-text').text()).toEqual(errorMessage);
+ expect($flashContainer.find('.flash-action')).toBeDefined();
+ expect($flashContainer.find('.flash-action').text()).toEqual('Save anyway');
+ });
+
+ it('should submit form if ajax request responds without any error in test', () => {
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+
+ integrationSettingsForm.testSettings(formData);
+
+ spyOn(integrationSettingsForm.$form, 'submit');
+ deferred.resolve({ error: false });
+
+ expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
+ });
+
+ it('should submit form when clicked on `Save anyway` action of error Flash', () => {
+ const errorMessage = 'Test failed.';
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+
+ integrationSettingsForm.testSettings(formData);
+
+ deferred.resolve({ error: true, message: errorMessage });
+
+ const $flashAction = $('.flash-container .flash-action');
+ expect($flashAction).toBeDefined();
+
+ spyOn(integrationSettingsForm.$form, 'submit');
+ $flashAction.trigger('click');
+ expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
+ });
+
+ it('should show error Flash if ajax request failed', () => {
+ const errorMessage = 'Something went wrong on our end.';
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+
+ integrationSettingsForm.testSettings(formData);
+
+ deferred.reject();
+
+ expect($('.flash-container .flash-text').text()).toEqual(errorMessage);
+ });
+
+ it('should always call `toggleSubmitBtnState` with `false` once request is completed', () => {
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+
+ integrationSettingsForm.testSettings(formData);
+
+ spyOn(integrationSettingsForm, 'toggleSubmitBtnState');
+ deferred.reject();
+
+ expect(integrationSettingsForm.toggleSubmitBtnState).toHaveBeenCalledWith(false);
+ });
+ });
+});
diff --git a/spec/lib/system_check/simple_executor_spec.rb b/spec/lib/system_check/simple_executor_spec.rb
new file mode 100644
index 00000000000..a5c6170cd7d
--- /dev/null
+++ b/spec/lib/system_check/simple_executor_spec.rb
@@ -0,0 +1,223 @@
+require 'spec_helper'
+require 'rake_helper'
+
+describe SystemCheck::SimpleExecutor, lib: true do
+ class SimpleCheck < SystemCheck::BaseCheck
+ set_name 'my simple check'
+
+ def check?
+ true
+ end
+ end
+
+ class OtherCheck < SystemCheck::BaseCheck
+ set_name 'other check'
+
+ def check?
+ false
+ end
+
+ def show_error
+ $stdout.puts 'this is an error text'
+ end
+ end
+
+ class SkipCheck < SystemCheck::BaseCheck
+ set_name 'skip check'
+ set_skip_reason 'this is a skip reason'
+
+ def skip?
+ true
+ end
+
+ def check?
+ raise 'should not execute this'
+ end
+ end
+
+ class MultiCheck < SystemCheck::BaseCheck
+ set_name 'multi check'
+
+ def multi_check
+ $stdout.puts 'this is a multi output check'
+ end
+
+ def check?
+ raise 'should not execute this'
+ end
+ end
+
+ class SkipMultiCheck < SystemCheck::BaseCheck
+ set_name 'skip multi check'
+
+ def skip?
+ true
+ end
+
+ def multi_check
+ raise 'should not execute this'
+ end
+ end
+
+ class RepairCheck < SystemCheck::BaseCheck
+ set_name 'repair check'
+
+ def check?
+ false
+ end
+
+ def repair!
+ true
+ end
+
+ def show_error
+ $stdout.puts 'this is an error message'
+ end
+ end
+
+ describe '#component' do
+ it 'returns stored component name' do
+ expect(subject.component).to eq('Test')
+ end
+ end
+
+ describe '#checks' do
+ before do
+ subject << SimpleCheck
+ end
+
+ it 'returns a set of classes' do
+ expect(subject.checks).to include(SimpleCheck)
+ end
+ end
+
+ describe '#<<' do
+ before do
+ subject << SimpleCheck
+ end
+
+ it 'appends a new check to the Set' do
+ subject << OtherCheck
+ stored_checks = subject.checks.to_a
+
+ expect(stored_checks.first).to eq(SimpleCheck)
+ expect(stored_checks.last).to eq(OtherCheck)
+ end
+
+ it 'inserts unique itens only' do
+ subject << SimpleCheck
+
+ expect(subject.checks.size).to eq(1)
+ end
+ end
+
+ subject { described_class.new('Test') }
+
+ describe '#execute' do
+ before do
+ silence_output
+
+ subject << SimpleCheck
+ subject << OtherCheck
+ end
+
+ it 'runs included checks' do
+ expect(subject).to receive(:run_check).with(SimpleCheck)
+ expect(subject).to receive(:run_check).with(OtherCheck)
+
+ subject.execute
+ end
+ end
+
+ describe '#run_check' do
+ it 'prints check name' do
+ expect(SimpleCheck).to receive(:display_name).and_call_original
+ expect { subject.run_check(SimpleCheck) }.to output(/my simple check/).to_stdout
+ end
+
+ context 'when check pass' do
+ it 'prints yes' do
+ expect_any_instance_of(SimpleCheck).to receive(:check?).and_call_original
+ expect { subject.run_check(SimpleCheck) }.to output(/ \.\.\. yes/).to_stdout
+ end
+ end
+
+ context 'when check fails' do
+ it 'prints no' do
+ expect_any_instance_of(OtherCheck).to receive(:check?).and_call_original
+ expect { subject.run_check(OtherCheck) }.to output(/ \.\.\. no/).to_stdout
+ end
+
+ it 'displays error message from #show_error' do
+ expect_any_instance_of(OtherCheck).to receive(:show_error).and_call_original
+ expect { subject.run_check(OtherCheck) }.to output(/this is an error text/).to_stdout
+ end
+
+ context 'when check implements #repair!' do
+ it 'executes #repair!' do
+ expect_any_instance_of(RepairCheck).to receive(:repair!)
+
+ subject.run_check(RepairCheck)
+ end
+
+ context 'when repair succeeds' do
+ it 'does not execute #show_error' do
+ expect_any_instance_of(RepairCheck).to receive(:repair!).and_call_original
+ expect_any_instance_of(RepairCheck).not_to receive(:show_error)
+
+ subject.run_check(RepairCheck)
+ end
+ end
+
+ context 'when repair fails' do
+ it 'does not execute #show_error' do
+ expect_any_instance_of(RepairCheck).to receive(:repair!) { false }
+ expect_any_instance_of(RepairCheck).to receive(:show_error)
+
+ subject.run_check(RepairCheck)
+ end
+ end
+ end
+ end
+
+ context 'when check implements skip?' do
+ it 'executes #skip? method' do
+ expect_any_instance_of(SkipCheck).to receive(:skip?).and_call_original
+
+ subject.run_check(SkipCheck)
+ end
+
+ it 'displays #skip_reason' do
+ expect { subject.run_check(SkipCheck) }.to output(/this is a skip reason/).to_stdout
+ end
+
+ it 'does not execute #check when #skip? is true' do
+ expect_any_instance_of(SkipCheck).not_to receive(:check?)
+
+ subject.run_check(SkipCheck)
+ end
+ end
+
+ context 'when implements a #multi_check' do
+ it 'executes #multi_check method' do
+ expect_any_instance_of(MultiCheck).to receive(:multi_check)
+
+ subject.run_check(MultiCheck)
+ end
+
+ it 'does not execute #check method' do
+ expect_any_instance_of(MultiCheck).not_to receive(:check)
+
+ subject.run_check(MultiCheck)
+ end
+
+ context 'when check implements #skip?' do
+ it 'executes #skip? method' do
+ expect_any_instance_of(SkipMultiCheck).to receive(:skip?).and_call_original
+
+ subject.run_check(SkipMultiCheck)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/system_check_spec.rb b/spec/lib/system_check_spec.rb
new file mode 100644
index 00000000000..23d9beddb08
--- /dev/null
+++ b/spec/lib/system_check_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+require 'rake_helper'
+
+describe SystemCheck, lib: true do
+ class SimpleCheck < SystemCheck::BaseCheck
+ def check?
+ true
+ end
+ end
+
+ class OtherCheck < SystemCheck::BaseCheck
+ def check?
+ false
+ end
+ end
+
+ before do
+ silence_output
+ end
+
+ describe '.run' do
+ subject { SystemCheck }
+
+ it 'detects execution of SimpleCheck' do
+ is_expected.to execute_check(SimpleCheck)
+
+ subject.run('Test', [SimpleCheck])
+ end
+
+ it 'detects exclusion of OtherCheck in execution' do
+ is_expected.not_to execute_check(OtherCheck)
+
+ subject.run('Test', [SimpleCheck])
+ end
+ end
+end
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 1920b5bf42b..0ee050196e4 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -69,41 +69,6 @@ describe JiraService, models: true do
end
end
- describe '#can_test?' do
- let(:jira_service) { described_class.new }
-
- it 'returns false if username is blank' do
- allow(jira_service).to receive_messages(
- url: 'http://jira.example.com',
- username: '',
- password: '12345678'
- )
-
- expect(jira_service.can_test?).to be_falsy
- end
-
- it 'returns false if password is blank' do
- allow(jira_service).to receive_messages(
- url: 'http://jira.example.com',
- username: 'tester',
- password: ''
- )
-
- expect(jira_service.can_test?).to be_falsy
- end
-
- it 'returns true if password and username are present' do
- jira_service = described_class.new
- allow(jira_service).to receive_messages(
- url: 'http://jira.example.com',
- username: 'tester',
- password: '12345678'
- )
-
- expect(jira_service.can_test?).to be_truthy
- end
- end
-
describe '#close_issue' do
let(:custom_base_url) { 'http://custom_url' }
let(:user) { create(:user) }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index fe9df3360ff..1c3541da44f 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -22,7 +22,7 @@ describe User, models: true do
it { is_expected.to have_many(:deploy_keys).dependent(:destroy) }
it { is_expected.to have_many(:events).dependent(:destroy) }
it { is_expected.to have_many(:recent_events).class_name('Event') }
- it { is_expected.to have_many(:issues).dependent(:restrict_with_exception) }
+ it { is_expected.to have_many(:issues).dependent(:destroy) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
it { is_expected.to have_many(:merge_requests).dependent(:destroy) }
it { is_expected.to have_many(:identities).dependent(:destroy) }
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 3d98551628b..40bfc0c636b 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -316,15 +316,15 @@ describe API::Projects do
expect(project.path).to eq('foo_project')
end
- it 'creates new project name and path and returns 201' do
- expect { post api('/projects', user), path: 'foo-Project', name: 'Foo Project' }.
+ it 'creates new project with name and path and returns 201' do
+ expect { post api('/projects', user), path: 'path-project-Foo', name: 'Foo Project' }.
to change { Project.count }.by(1)
expect(response).to have_http_status(201)
project = Project.first
expect(project.name).to eq('Foo Project')
- expect(project.path).to eq('foo-Project')
+ expect(project.path).to eq('path-project-Foo')
end
it 'creates last project before reaching project limit' do
@@ -470,9 +470,25 @@ describe API::Projects do
before { project }
before { admin }
- it 'creates new project without path and return 201' do
- expect { post api("/projects/user/#{user.id}", admin), name: 'foo' }.to change {Project.count}.by(1)
+ it 'creates new project without path but with name and return 201' do
+ expect { post api("/projects/user/#{user.id}", admin), name: 'Foo Project' }.to change {Project.count}.by(1)
expect(response).to have_http_status(201)
+
+ project = Project.first
+
+ expect(project.name).to eq('Foo Project')
+ expect(project.path).to eq('foo-project')
+ end
+
+ it 'creates new project with name and path and returns 201' do
+ expect { post api("/projects/user/#{user.id}", admin), path: 'path-project-Foo', name: 'Foo Project' }.
+ to change { Project.count }.by(1)
+ expect(response).to have_http_status(201)
+
+ project = Project.first
+
+ expect(project.name).to eq('Foo Project')
+ expect(project.path).to eq('path-project-Foo')
end
it 'responds with 400 on failure and not project' do
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index de37a61e388..5409f67c091 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -147,16 +147,22 @@ describe Users::DestroyService, services: true do
end
context "migrating associated records" do
+ let!(:issue) { create(:issue, author: user) }
+
it 'delegates to the `MigrateToGhostUser` service to move associated records to the ghost user' do
- expect_any_instance_of(Users::MigrateToGhostUserService).to receive(:execute).once
+ expect_any_instance_of(Users::MigrateToGhostUserService).to receive(:execute).once.and_call_original
service.execute(user)
+
+ expect(issue.reload.author).to be_ghost
end
it 'does not run `MigrateToGhostUser` if hard_delete option is given' do
expect_any_instance_of(Users::MigrateToGhostUserService).not_to receive(:execute)
service.execute(user, hard_delete: true)
+
+ expect(Issue.exists?(issue.id)).to be_falsy
end
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 4c2eba8fa46..994c7dcbb46 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -26,6 +26,9 @@ if ENV['CI'] && !ENV['NO_KNAPSACK']
Knapsack::Adapters::RSpecAdapter.bind
end
+# require rainbow gem String monkeypatch, so we can test SystemChecks
+require 'rainbow/ext/string'
+
# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
diff --git a/spec/support/matchers/execute_check.rb b/spec/support/matchers/execute_check.rb
new file mode 100644
index 00000000000..7232fad52fb
--- /dev/null
+++ b/spec/support/matchers/execute_check.rb
@@ -0,0 +1,23 @@
+RSpec::Matchers.define :execute_check do |expected|
+ match do |actual|
+ expect(actual).to eq(SystemCheck)
+ expect(actual).to receive(:run) do |*args|
+ expect(args[1]).to include(expected)
+ end
+ end
+
+ match_when_negated do |actual|
+ expect(actual).to eq(SystemCheck)
+ expect(actual).to receive(:run) do |*args|
+ expect(args[1]).not_to include(expected)
+ end
+ end
+
+ failure_message do |actual|
+ 'This matcher must be used with SystemCheck' unless actual == SystemCheck
+ end
+
+ failure_message_when_negated do |actual|
+ 'This matcher must be used with SystemCheck' unless actual == SystemCheck
+ end
+end
diff --git a/spec/support/rake_helpers.rb b/spec/support/rake_helpers.rb
index 4a8158ed79b..5cb415111d2 100644
--- a/spec/support/rake_helpers.rb
+++ b/spec/support/rake_helpers.rb
@@ -7,4 +7,9 @@ module RakeHelpers
def stub_warn_user_is_not_gitlab
allow_any_instance_of(Object).to receive(:warn_user_is_not_gitlab)
end
+
+ def silence_output
+ allow($stdout).to receive(:puts)
+ allow($stdout).to receive(:print)
+ end
end