diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index c4e41f94594..ee74734aa22 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -4.0.3 +4.1.0 diff --git a/Gemfile b/Gemfile index 17e628ceea6..5eb8c32b168 100644 --- a/Gemfile +++ b/Gemfile @@ -67,7 +67,7 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false gem 'github-linguist', '~> 4.7.0', require: 'linguist' # API -gem 'grape', '~> 0.15.0' +gem 'grape', '~> 0.18.0' gem 'grape-entity', '~> 0.6.0' gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' diff --git a/Gemfile.lock b/Gemfile.lock index 7269b528e30..23e45ddc16f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -284,15 +284,15 @@ GEM json multi_json request_store (>= 1.0) - grape (0.15.0) + grape (0.18.0) activesupport builder hashie (>= 2.1.0) multi_json (>= 1.3.2) multi_xml (>= 0.5.2) + mustermann-grape (~> 0.4.0) rack (>= 1.3.0) rack-accept - rack-mount virtus (>= 1.0.0) grape-entity (0.6.0) activesupport @@ -400,6 +400,10 @@ GEM multi_json (1.12.1) multi_xml (0.5.5) multipart-post (2.0.0) + mustermann (0.4.0) + tool (~> 0.2) + mustermann-grape (0.4.0) + mustermann (= 0.4.0) mysql2 (0.3.20) net-ldap (0.12.1) net-ssh (3.0.1) @@ -505,14 +509,12 @@ GEM pry-rails (0.3.4) pry (>= 0.9.10) pyu-ruby-sasl (0.0.3.3) - rack (1.6.4) + rack (1.6.5) rack-accept (0.4.5) rack (>= 0.4) rack-attack (4.4.1) rack rack-cors (0.4.0) - rack-mount (0.8.3) - rack (>= 1.0.0) rack-oauth2 (1.2.3) activesupport (>= 2.3) attr_required (>= 0.0.5) @@ -743,6 +745,7 @@ GEM tilt (2.0.5) timecop (0.8.1) timfel-krb5-auth (0.8.3) + tool (0.2.3) truncato (0.7.8) htmlentities (~> 4.3.1) nokogiri (~> 1.6.1) @@ -861,7 +864,7 @@ DEPENDENCIES gollum-lib (~> 4.2) gollum-rugged_adapter (~> 0.4.2) gon (~> 6.1.0) - grape (~> 0.15.0) + grape (~> 0.18.0) grape-entity (~> 0.6.0) haml_lint (~> 0.18.2) hamlit (~> 2.6.1) diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index dfaf2f7f1d3..66711aa1804 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -26,6 +26,10 @@ body { .container-limited { max-width: $fixed-layout-width; + + &.limit-container-width { + max-width: $limited-layout-width; + } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index dd01e304227..267fcd77b38 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -154,6 +154,8 @@ $row-hover-border: #b2d7ff; $progress-color: #c0392b; $header-height: 50px; $fixed-layout-width: 1280px; +$limited-layout-width: 990px; +$gl-avatar-size: 40px; $error-exclamation-point: #e62958; $border-radius-default: 2px; $settings-icon-size: 18px; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 0234f2d49e7..4fac0cfb0ba 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -1,3 +1,50 @@ +// Limit MR description for side-by-side diff view +.limit-container-width { + .detail-page-header { + max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2)); + margin-left: auto; + margin-right: auto; + } + + .issuable-details { + .detail-page-description, + .mr-source-target, + .mr-state-widget, + .merge-manually { + max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2)); + margin-left: auto; + margin-right: auto; + } + + .merge-request-tabs-holder { + &.affix { + border-bottom: 1px solid $border-color; + + .nav-links { + border: 0; + } + } + + .container-fluid { + padding-left: 0; + padding-right: 0; + max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2)); + margin-left: auto; + margin-right: auto; + } + } + } + + .diffs { + .mr-version-controls, + .files-changed { + max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2)); + margin-left: auto; + margin-right: auto; + } + } +} + .issuable-details { section { .issuable-discussion { @@ -9,7 +56,6 @@ .description img:not(.emoji) { border: 1px solid $white-normal; padding: 5px; - margin: 5px; max-height: calc(100vh - 100px); } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 1b83b40486e..106c5d4d390 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -383,10 +383,6 @@ ul.notes { .note-action-button { margin-left: 10px; } - - @media (min-width: $screen-sm-min) { - position: relative; - } } .discussion-actions { diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 33d3a800e7c..a0719b415e5 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -616,14 +616,10 @@ li { padding-top: 2px; margin: 0 5px; - } - - li:first-child { - padding-top: 6px; - } - - li:last-child { - padding-bottom: 6px; + padding-left: 0; + padding-bottom: 0; + margin-bottom: 0; + line-height: 1.2; } } } diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 9eb75bb3891..18044ca78e2 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -22,6 +22,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController end @qr_code = build_qr_code + @account_string = account_string setup_u2f_registration end @@ -78,11 +79,14 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController private def build_qr_code - issuer = "#{issuer_host} | #{current_user.email}" - uri = current_user.otp_provisioning_uri(current_user.email, issuer: issuer) + uri = current_user.otp_provisioning_uri(account_string, issuer: issuer_host) RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3) end + def account_string + "#{issuer_host}:#{current_user.email}" + end + def issuer_host Gitlab.config.gitlab.host end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index f6d4ea4659a..77dc9e7d538 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -12,11 +12,18 @@ module GroupsHelper end def group_title(group, name = nil, url = nil) - full_title = link_to(simple_sanitize(group.name), group_path(group)) + full_title = '' + + group.parents.each do |parent| + full_title += link_to(simple_sanitize(parent.name), group_path(parent)) + full_title += ' / '.html_safe + end + + full_title += link_to(simple_sanitize(group.name), group_path(group)) full_title += ' · '.html_safe + link_to(simple_sanitize(name), url) if name content_tag :span do - full_title + full_title.html_safe end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 9cda3b78761..d2177f683a1 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -52,7 +52,7 @@ module ProjectsHelper def project_title(project) namespace_link = if project.group - link_to(simple_sanitize(project.group.name), group_path(project.group)) + group_title(project.group) else owner = project.namespace.owner link_to(simple_sanitize(owner.name), user_path(owner)) @@ -390,7 +390,7 @@ module ProjectsHelper "success" end end - + def readme_cache_key sha = @project.commit.try(:sha) || 'nil' [@project.path_with_namespace, sha, "readme"].join('-') diff --git a/app/models/group.rb b/app/models/group.rb index 4248e1162d8..ac8a82c8c1e 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -83,7 +83,7 @@ class Group < Namespace end def human_name - name + full_name end def visibility_level_field diff --git a/app/models/namespace.rb b/app/models/namespace.rb index f0479d94986..fd42f2328d8 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -161,6 +161,19 @@ class Namespace < ActiveRecord::Base end end + def full_name + @full_name ||= + if parent + parent.full_name + ' / ' + name + else + name + end + end + + def parents + @parents ||= parent ? parent.parents + [parent] : [] + end + private def repository_storage_paths diff --git a/app/models/project.rb b/app/models/project.rb index 2c726cfc5df..5d092ca42c2 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -95,7 +95,8 @@ class Project < ActiveRecord::Base has_one :asana_service, dependent: :destroy has_one :gemnasium_service, dependent: :destroy has_one :mattermost_slash_commands_service, dependent: :destroy - has_one :slack_service, dependent: :destroy + has_one :mattermost_notification_service, dependent: :destroy + has_one :slack_notification_service, dependent: :destroy has_one :buildkite_service, dependent: :destroy has_one :bamboo_service, dependent: :destroy has_one :teamcity_service, dependent: :destroy diff --git a/app/models/project_services/slack_service/base_message.rb b/app/models/project_services/chat_message/base_message.rb similarity index 96% rename from app/models/project_services/slack_service/base_message.rb rename to app/models/project_services/chat_message/base_message.rb index f1182824687..a03605d01fb 100644 --- a/app/models/project_services/slack_service/base_message.rb +++ b/app/models/project_services/chat_message/base_message.rb @@ -1,6 +1,6 @@ require 'slack-notifier' -class SlackService +module ChatMessage class BaseMessage def initialize(params) raise NotImplementedError diff --git a/app/models/project_services/slack_service/build_message.rb b/app/models/project_services/chat_message/build_message.rb similarity index 98% rename from app/models/project_services/slack_service/build_message.rb rename to app/models/project_services/chat_message/build_message.rb index 0fca4267bad..53e35cb21bf 100644 --- a/app/models/project_services/slack_service/build_message.rb +++ b/app/models/project_services/chat_message/build_message.rb @@ -1,4 +1,4 @@ -class SlackService +module ChatMessage class BuildMessage < BaseMessage attr_reader :sha attr_reader :ref_type diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb similarity index 98% rename from app/models/project_services/slack_service/issue_message.rb rename to app/models/project_services/chat_message/issue_message.rb index cd87a79d0c6..14fd64e5332 100644 --- a/app/models/project_services/slack_service/issue_message.rb +++ b/app/models/project_services/chat_message/issue_message.rb @@ -1,4 +1,4 @@ -class SlackService +module ChatMessage class IssueMessage < BaseMessage attr_reader :user_name attr_reader :title diff --git a/app/models/project_services/slack_service/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb similarity index 98% rename from app/models/project_services/slack_service/merge_message.rb rename to app/models/project_services/chat_message/merge_message.rb index b7615c96068..ab5e8b24167 100644 --- a/app/models/project_services/slack_service/merge_message.rb +++ b/app/models/project_services/chat_message/merge_message.rb @@ -1,4 +1,4 @@ -class SlackService +module ChatMessage class MergeMessage < BaseMessage attr_reader :user_name attr_reader :project_name diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/chat_message/note_message.rb similarity index 99% rename from app/models/project_services/slack_service/note_message.rb rename to app/models/project_services/chat_message/note_message.rb index 797c5937f09..ca1d7207034 100644 --- a/app/models/project_services/slack_service/note_message.rb +++ b/app/models/project_services/chat_message/note_message.rb @@ -1,4 +1,4 @@ -class SlackService +module ChatMessage class NoteMessage < BaseMessage attr_reader :message attr_reader :user_name diff --git a/app/models/project_services/slack_service/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb similarity index 98% rename from app/models/project_services/slack_service/pipeline_message.rb rename to app/models/project_services/chat_message/pipeline_message.rb index b6355fc4171..210027565a8 100644 --- a/app/models/project_services/slack_service/pipeline_message.rb +++ b/app/models/project_services/chat_message/pipeline_message.rb @@ -1,4 +1,4 @@ -class SlackService +module ChatMessage class PipelineMessage < BaseMessage attr_reader :ref_type, :ref, :status, :project_name, :project_url, :user_name, :duration, :pipeline_id diff --git a/app/models/project_services/slack_service/push_message.rb b/app/models/project_services/chat_message/push_message.rb similarity index 99% rename from app/models/project_services/slack_service/push_message.rb rename to app/models/project_services/chat_message/push_message.rb index b26f3e9ddce..2d73b71ec37 100644 --- a/app/models/project_services/slack_service/push_message.rb +++ b/app/models/project_services/chat_message/push_message.rb @@ -1,4 +1,4 @@ -class SlackService +module ChatMessage class PushMessage < BaseMessage attr_reader :after attr_reader :before diff --git a/app/models/project_services/slack_service/wiki_page_message.rb b/app/models/project_services/chat_message/wiki_page_message.rb similarity index 98% rename from app/models/project_services/slack_service/wiki_page_message.rb rename to app/models/project_services/chat_message/wiki_page_message.rb index 160ca3ac115..134083e4504 100644 --- a/app/models/project_services/slack_service/wiki_page_message.rb +++ b/app/models/project_services/chat_message/wiki_page_message.rb @@ -1,4 +1,4 @@ -class SlackService +module ChatMessage class WikiPageMessage < BaseMessage attr_reader :user_name attr_reader :title diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/chat_notification_service.rb similarity index 65% rename from app/models/project_services/slack_service.rb rename to app/models/project_services/chat_notification_service.rb index e1b937817f4..b0556987721 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -1,6 +1,13 @@ -class SlackService < Service +# Base class for Chat notifications services +# This class is not meant to be used directly, but only to inherit from. +class ChatNotificationService < Service + include ChatMessage + + default_value_for :category, 'chat' + prop_accessor :webhook, :username, :channel boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines + validates :webhook, presence: true, url: true, if: :activated? def initialize_properties @@ -14,35 +21,8 @@ class SlackService < Service end end - def title - 'Slack' - end - - def description - 'A team communication tool for the 21st century' - end - - def to_param - 'slack' - end - - def help - 'This service sends notifications to your Slack channel.
- To setup this Service you need to create a new "Incoming webhook" in your Slack integration panel, - and enter the Webhook URL below.' - end - - def fields - default_fields = - [ - { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' }, - { type: 'text', name: 'username', placeholder: 'username' }, - { type: 'text', name: 'channel', placeholder: "#general" }, - { type: 'checkbox', name: 'notify_only_broken_builds' }, - { type: 'checkbox', name: 'notify_only_broken_pipelines' }, - ] - - default_fields + build_event_channels + def can_test? + valid? end def supported_events @@ -67,21 +47,16 @@ class SlackService < Service message = get_message(object_kind, data) - if message - opt = {} + return false unless message - event_channel = get_channel_field(object_kind) || channel + opt = {} - opt[:channel] = event_channel if event_channel - opt[:username] = username if username + opt[:channel] = get_channel_field(object_kind).presence || channel || default_channel + opt[:username] = username if username + notifier = Slack::Notifier.new(webhook, opt) + notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback) - notifier = Slack::Notifier.new(webhook, opt) - notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback) - - true - else - false - end + true end def event_channel_names @@ -96,6 +71,10 @@ class SlackService < Service fields.reject { |field| field[:name].end_with?('channel') } end + def default_channel + raise NotImplementedError + end + private def get_message(object_kind, data) @@ -124,7 +103,7 @@ class SlackService < Service def build_event_channels supported_events.reduce([]) do |channels, event| - channels << { type: 'text', name: event_channel_name(event), placeholder: "#general" } + channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel } end end @@ -166,11 +145,3 @@ class SlackService < Service end end end - -require "slack_service/issue_message" -require "slack_service/push_message" -require "slack_service/merge_message" -require "slack_service/note_message" -require "slack_service/build_message" -require "slack_service/pipeline_message" -require "slack_service/wiki_page_message" diff --git a/app/models/project_services/chat_service.rb b/app/models/project_services/chat_service.rb index d36beff5fa6..574788462de 100644 --- a/app/models/project_services/chat_service.rb +++ b/app/models/project_services/chat_service.rb @@ -1,5 +1,5 @@ # Base class for Chat services -# This class is not meant to be used directly, but only to inherrit from. +# This class is not meant to be used directly, but only to inherit from. class ChatService < Service default_value_for :category, 'chat' diff --git a/app/models/project_services/mattermost_notification_service.rb b/app/models/project_services/mattermost_notification_service.rb new file mode 100644 index 00000000000..de18c4b1f00 --- /dev/null +++ b/app/models/project_services/mattermost_notification_service.rb @@ -0,0 +1,41 @@ +class MattermostNotificationService < ChatNotificationService + def title + 'Mattermost notifications' + end + + def description + 'Receive event notifications in Mattermost' + end + + def to_param + 'mattermost_notification' + end + + def help + 'This service sends notifications about projects events to Mattermost channels.
+ To set up this service: +
    +
  1. Enable incoming webhooks in your Mattermost installation.
  2. +
  3. Add an incoming webhook in your Mattermost team. The default channel can be overridden for each event.
  4. +
  5. Paste the webhook URL into the field bellow.
  6. +
  7. Select events below to enable notifications. The channel and username are optional.
  8. +
' + end + + def fields + default_fields + build_event_channels + end + + def default_fields + [ + { type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' }, + { type: 'text', name: 'username', placeholder: 'username' }, + { type: 'checkbox', name: 'notify_only_broken_builds' }, + { type: 'checkbox', name: 'notify_only_broken_pipelines' }, + ] + end + + def default_channel + "#town-square" + end +end diff --git a/app/models/project_services/slack_notification_service.rb b/app/models/project_services/slack_notification_service.rb new file mode 100644 index 00000000000..3cbf89efba4 --- /dev/null +++ b/app/models/project_services/slack_notification_service.rb @@ -0,0 +1,40 @@ +class SlackNotificationService < ChatNotificationService + def title + 'Slack notifications' + end + + def description + 'Receive event notifications in Slack' + end + + def to_param + 'slack_notification' + end + + def help + 'This service sends notifications about projects events to Slack channels.
+ To setup this service: +
    +
  1. Add an incoming webhook in your Slack team. The default channel can be overridden for each event.
  2. +
  3. Paste the Webhook URL into the field below.
  4. +
  5. Select events below to enable notifications. The channel and username are optional.
  6. +
' + end + + def fields + default_fields + build_event_channels + end + + def default_fields + [ + { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' }, + { type: 'text', name: 'username', placeholder: 'username' }, + { type: 'checkbox', name: 'notify_only_broken_builds' }, + { type: 'checkbox', name: 'notify_only_broken_pipelines' }, + ] + end + + def default_channel + "#general" + end +end diff --git a/app/models/service.rb b/app/models/service.rb index e49a8fa2904..0bbab078cf6 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -220,7 +220,8 @@ class Service < ActiveRecord::Base pivotaltracker pushover redmine - slack + mattermost_notification + slack_notification teamcity ] end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index b5f63cc5a1a..ab3d2a9a0cd 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -184,7 +184,8 @@ class IssuableBaseService < BaseService old_labels = issuable.labels.to_a old_mentioned_users = issuable.mentioned_users.to_a - params[:label_ids] = process_label_ids(params, existing_label_ids: issuable.label_ids) + label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids) + params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids) if params.present? && update_issuable(issuable, params) # We do not touch as it will affect a update on updated_at field @@ -201,6 +202,10 @@ class IssuableBaseService < BaseService issuable end + def labels_changing?(old_label_ids, new_label_ids) + old_label_ids.sort != new_label_ids.sort + end + def change_state(issuable) case params.delete(:state_event) when 'reopen' diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml index 4efeec0ea4e..cf28f92853e 100644 --- a/app/views/admin/groups/_group.html.haml +++ b/app/views/admin/groups/_group.html.haml @@ -20,7 +20,7 @@ = image_tag group_icon(group), class: "avatar s40 hidden-xs" .title = link_to [:admin, group], class: 'group-name' do - = group.name + = group.full_name - if group.description.present? .description diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 71a605f33b1..7b0175af214 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -1,6 +1,6 @@ - page_title @group.name, "Groups" %h3.page-title - Group: #{@group.name} + Group: #{@group.full_name} = link_to admin_group_edit_path(@group), class: "btn pull-right" do %i.fa.fa-pencil-square-o diff --git a/app/views/ci/status/_graph_badge.html.haml b/app/views/ci/status/_graph_badge.html.haml index df1b763e67c..c7d04ab61e9 100644 --- a/app/views/ci/status/_graph_badge.html.haml +++ b/app/views/ci/status/_graph_badge.html.haml @@ -3,10 +3,9 @@ - subject = local_assigns.fetch(:subject) - status = subject.detailed_status(current_user) - klass = "ci-status-icon ci-status-icon-#{status}" -- tooltip_title = "#{subject.name} - #{status.label}" - if status.has_details? - = link_to status.details_path, data: { toggle: 'tooltip', title: tooltip_title } do + = link_to status.details_path, data: { toggle: 'tooltip', title: "#{subject.name} - #{status.label}" } do %span{ class: klass }= custom_icon(status.icon) .ci-status-text= subject.name - else @@ -15,6 +14,6 @@ - if status.has_action? = link_to status.action_path, method: status.action_method, - title: tooltip_title, class: 'ci-action-icon-container' do + title: status.action_title, class: 'ci-action-icon-container' do %i.ci-action-icon-wrapper = icon(status.action_icon, class: status.action_class) diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index 03ac739ade5..558a1d56151 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -30,7 +30,7 @@ To add the entry manually, provide the following details to the application on your phone. %p.prepend-top-0.append-bottom-0 Account: - = current_user.email + = @account_string %p.prepend-top-0.append-bottom-0 Key: = current_user.otp_secret.scan(/.{4}/).join(' ') diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml deleted file mode 100644 index ad1a7360a8b..00000000000 --- a/app/views/projects/ci/builds/_build_pipeline.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -- is_playable = subject.playable? && can?(current_user, :update_build, @project) -- if is_playable - = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.js-pipeline-graph', placement: 'bottom' } do - = ci_icon_for_status('play') - .ci-status-text= subject.name -- elsif can?(current_user, :read_build, @project) - = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } do - %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} - = ci_icon_for_status(subject.status) - .ci-status-text= subject.name -- else - %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} - = ci_icon_for_status(subject.status) diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml deleted file mode 100644 index 1bba0443154..00000000000 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } } - - if subject.target_url - = link_to subject.target_url do - %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} - = ci_icon_for_status(subject.status) - %span.ci-status-text= subject.name - - else - %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} - = ci_icon_for_status(subject.status) - %span.ci-status-text= subject.name diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index bd629b5c519..981bf640a6b 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -1,3 +1,4 @@ +- @content_class = "limit-container-width" - page_title "#{@issue.title} (#{@issue.to_reference})", "Issues" - page_description @issue.description - page_card_attributes @issue.card_attributes diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 5febe806bdd..7725558518f 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -1,3 +1,4 @@ +- @content_class = "limit-container-width" - page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes @@ -41,7 +42,7 @@ = render "projects/merge_requests/widget/show.html.haml" - if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user) - .light.prepend-top-default.append-bottom-default + .merge-manually.light.prepend-top-default.append-bottom-default You can also accept this merge request manually using the = succeed '.' do = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index 19221e3391f..8164f61797c 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -28,7 +28,7 @@ = image_tag group_icon(group), class: "avatar s40 hidden-xs" .title = link_to group, class: 'group-name' do - = group.name + = group.full_name - if group_member as diff --git a/changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml b/changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml new file mode 100644 index 00000000000..b12eab26b67 --- /dev/null +++ b/changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml @@ -0,0 +1,4 @@ +--- +title: Ensure issuable state changes only fire webhooks once +merge_request: +author: diff --git a/changelogs/unreleased/abuse_report-fixture.yml b/changelogs/unreleased/abuse_report-fixture.yml new file mode 100644 index 00000000000..47478a2048b --- /dev/null +++ b/changelogs/unreleased/abuse_report-fixture.yml @@ -0,0 +1,4 @@ +--- +title: Replace static fixture for abuse_reports_spec +merge_request: 7644 +author: winniehell diff --git a/changelogs/unreleased/add_info_to_qr.yml b/changelogs/unreleased/add_info_to_qr.yml new file mode 100644 index 00000000000..a4b0354a9c9 --- /dev/null +++ b/changelogs/unreleased/add_info_to_qr.yml @@ -0,0 +1,4 @@ +--- +title: Add GitLab host to 2FA QR code and manual info +merge_request: 6941 +author: diff --git a/changelogs/unreleased/change_development_build_fixtures.yml b/changelogs/unreleased/change_development_build_fixtures.yml new file mode 100644 index 00000000000..b5dc3792745 --- /dev/null +++ b/changelogs/unreleased/change_development_build_fixtures.yml @@ -0,0 +1,4 @@ +--- +title: Ci::Builds have same ref as Ci::Pipeline in dev fixtures +merge_request: +author: twonegatives diff --git a/changelogs/unreleased/gem-update-grape.yml b/changelogs/unreleased/gem-update-grape.yml new file mode 100644 index 00000000000..46b6702d9fd --- /dev/null +++ b/changelogs/unreleased/gem-update-grape.yml @@ -0,0 +1,4 @@ +--- +title: 'Gem update: Update grape to 0.18.0' +merge_request: +author: Robert Schilling diff --git a/changelogs/unreleased/issue_22269.yml b/changelogs/unreleased/issue_22269.yml new file mode 100644 index 00000000000..6b7164aff77 --- /dev/null +++ b/changelogs/unreleased/issue_22269.yml @@ -0,0 +1,4 @@ +--- +title: Create mattermost service +merge_request: +author: diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb index 08ad3097d34..19e001854d2 100644 --- a/db/fixtures/development/14_pipelines.rb +++ b/db/fixtures/development/14_pipelines.rb @@ -115,7 +115,7 @@ class Gitlab::Seeder::Pipelines def job_attributes(pipeline, opts) { name: 'test build', stage: 'test', stage_idx: stage_index(opts[:stage]), - ref: 'master', tag: false, user: build_user, project: @project, pipeline: pipeline, + ref: pipeline.ref, tag: false, user: build_user, project: @project, pipeline: pipeline, created_at: Time.now, updated_at: Time.now }.merge(opts) end diff --git a/db/migrate/20141006143943_move_slack_service_to_webhook.rb b/db/migrate/20141006143943_move_slack_service_to_webhook.rb index 8cb120f7007..42e88d6d6e3 100644 --- a/db/migrate/20141006143943_move_slack_service_to_webhook.rb +++ b/db/migrate/20141006143943_move_slack_service_to_webhook.rb @@ -1,7 +1,11 @@ # rubocop:disable all class MoveSlackServiceToWebhook < ActiveRecord::Migration + + DOWNTIME = true + DOWNTIME_REASON = 'Move old fields "token" and "subdomain" to one single field "webhook"' + def change - SlackService.all.each do |slack_service| + SlackNotificationService.all.each do |slack_service| if ["token", "subdomain"].all? { |property| slack_service.properties.key? property } token = slack_service.properties['token'] subdomain = slack_service.properties['subdomain'] diff --git a/db/migrate/20161130095245_fill_routes_table.rb b/db/migrate/20161130095245_fill_routes_table.rb index 6754e583000..c3536d6d911 100644 --- a/db/migrate/20161130095245_fill_routes_table.rb +++ b/db/migrate/20161130095245_fill_routes_table.rb @@ -16,6 +16,6 @@ class FillRoutesTable < ActiveRecord::Migration end def down - Route.delete_all(source_type: 'Namespace') + execute("DELETE FROM routes WHERE source_type = 'Namespace'") end end diff --git a/db/migrate/20161130101252_fill_projects_routes_table.rb b/db/migrate/20161130101252_fill_projects_routes_table.rb index 14700583be5..56ba6fcdbe3 100644 --- a/db/migrate/20161130101252_fill_projects_routes_table.rb +++ b/db/migrate/20161130101252_fill_projects_routes_table.rb @@ -8,15 +8,23 @@ class FillProjectsRoutesTable < ActiveRecord::Migration DOWNTIME_REASON = 'No new projects should be created during data copy' def up - execute <<-EOF - INSERT INTO routes - (source_id, source_type, path) - (SELECT projects.id, 'Project', concat(namespaces.path, '/', projects.path) FROM projects - INNER JOIN namespaces ON projects.namespace_id = namespaces.id) - EOF + if Gitlab::Database.postgresql? + execute <<-EOF + INSERT INTO routes (source_id, source_type, path) + (SELECT DISTINCT ON (namespaces.path, projects.path) projects.id, 'Project', concat(namespaces.path, '/', projects.path) + FROM projects INNER JOIN namespaces ON projects.namespace_id = namespaces.id + ORDER BY namespaces.path, projects.path, projects.id DESC) + EOF + else + execute <<-EOF + INSERT INTO routes (source_id, source_type, path) + (SELECT projects.id, 'Project', concat(namespaces.path, '/', projects.path) + FROM projects INNER JOIN namespaces ON projects.namespace_id = namespaces.id) + EOF + end end def down - Route.delete_all(source_type: 'Project') + execute("DELETE FROM routes WHERE source_type = 'Project'") end end diff --git a/db/migrate/20161202152031_remove_duplicates_from_routes.rb b/db/migrate/20161202152031_remove_duplicates_from_routes.rb index 510796e05f2..d73b0847506 100644 --- a/db/migrate/20161202152031_remove_duplicates_from_routes.rb +++ b/db/migrate/20161202152031_remove_duplicates_from_routes.rb @@ -7,20 +7,21 @@ class RemoveDuplicatesFromRoutes < ActiveRecord::Migration DOWNTIME = false def up - select_all("SELECT path FROM #{quote_table_name(:routes)} GROUP BY path HAVING COUNT(*) > 1").each do |row| - path = connection.quote(row['path']) - execute(%Q{ - DELETE FROM #{quote_table_name(:routes)} - WHERE path = #{path} - AND id != ( - SELECT id FROM ( - SELECT max(id) AS id - FROM #{quote_table_name(:routes)} - WHERE path = #{path} - ) max_ids - ) - }) - end + # We can skip this migration when running a PostgreSQL database because + # we use an optimized query in the "FillProjectsRoutesTable" migration + # to fill these values that avoid duplicate entries in the routes table. + return unless Gitlab::Database.mysql? + + execute <<-EOF + DELETE duplicated_rows.* + FROM routes AS duplicated_rows + INNER JOIN ( + SELECT path, MAX(id) as max_id + FROM routes + GROUP BY path + HAVING COUNT(*) > 1 + ) AS good_rows ON good_rows.path = duplicated_rows.path AND good_rows.max_id <> duplicated_rows.id; + EOF end def down diff --git a/db/migrate/20161206153754_add_name_index_to_namespace.rb b/db/migrate/20161206153754_add_name_index_to_namespace.rb index aaa35ed6f0a..b3f3cb68a99 100644 --- a/db/migrate/20161206153754_add_name_index_to_namespace.rb +++ b/db/migrate/20161206153754_add_name_index_to_namespace.rb @@ -13,7 +13,7 @@ class AddNameIndexToNamespace < ActiveRecord::Migration end def down - if index_exists?(:namespaces, :name) + if index_exists?(:namespaces, [:name, :parent_id]) remove_index :namespaces, [:name, :parent_id] end end diff --git a/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb b/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb new file mode 100644 index 00000000000..a7278d7b5a6 --- /dev/null +++ b/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb @@ -0,0 +1,14 @@ +class ChangeSlackServiceToSlackNotificationService < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + DOWNTIME_REASON = 'Rename SlackService to SlackNotificationService' + + def up + execute("UPDATE services SET type = 'SlackNotificationService' WHERE type = 'SlackService'") + end + + def down + execute("UPDATE services SET type = 'SlackService' WHERE type = 'SlackNotificationService'") + end +end diff --git a/db/schema.rb b/db/schema.rb index 67ff83d96d9..78e3a356f74 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161212142807) do +ActiveRecord::Schema.define(version: 20161213172958) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -98,14 +98,14 @@ ActiveRecord::Schema.define(version: 20161212142807) do t.text "help_page_text_html" t.text "shared_runners_text_html" t.text "after_sign_up_text_html" - t.boolean "sidekiq_throttling_enabled", default: false - t.string "sidekiq_throttling_queues" - t.decimal "sidekiq_throttling_factor" t.boolean "housekeeping_enabled", default: true, null: false t.boolean "housekeeping_bitmaps_enabled", default: true, null: false t.integer "housekeeping_incremental_repack_period", default: 10, null: false t.integer "housekeeping_full_repack_period", default: 50, null: false t.integer "housekeeping_gc_period", default: 200, null: false + t.boolean "sidekiq_throttling_enabled", default: false + t.string "sidekiq_throttling_queues" + t.decimal "sidekiq_throttling_factor" t.boolean "html_emails_enabled", default: true end @@ -527,6 +527,7 @@ ActiveRecord::Schema.define(version: 20161212142807) do t.string "type" t.string "fingerprint" t.boolean "public", default: false, null: false + t.boolean "can_push", default: false, null: false end add_index "keys", ["fingerprint"], name: "index_keys_on_fingerprint", unique: true, using: :btree @@ -739,8 +740,8 @@ ActiveRecord::Schema.define(version: 20161212142807) do t.integer "visibility_level", default: 20, null: false t.boolean "request_access_enabled", default: false, null: false t.datetime "deleted_at" - t.boolean "lfs_enabled" t.text "description_html" + t.boolean "lfs_enabled" t.integer "parent_id" end @@ -1221,8 +1222,8 @@ ActiveRecord::Schema.define(version: 20161212142807) do t.datetime "otp_grace_period_started_at" t.boolean "ldap_email", default: false, null: false t.boolean "external", default: false - t.string "organization" t.string "incoming_email_token" + t.string "organization" t.boolean "authorized_projects_populated" end diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md index 06291705702..80e5d80aa41 100644 --- a/doc/administration/custom_hooks.md +++ b/doc/administration/custom_hooks.md @@ -44,22 +44,30 @@ as appropriate. ## Chained hooks support -> [Introduced][93] in GitLab Shell 4.1.0. +> [Introduced][93] in GitLab Shell 4.1.0 and GitLab 8.15. -The hooks could be also placed in `hooks/.d` (global) or `custom_hooks/.d` (per project) -directories supporting chained execution of the hooks. +Hooks can be also placed in `hooks/.d` (global) or +`custom_hooks/.d` (per project) directories supporting chained +execution of the hooks. + +To look in a different directory for the global custom hooks (those in +`hooks/`), set `custom_hooks_dir` in gitlab-shell config. For +Omnibus installations, this can be set in `gitlab.rb`; and in source +installations, this can be set in `gitlab-shell/config.yml`. The hooks are searched and executed in this order: + 1. `.git/hooks/` - symlink to `gitlab-shell/hooks` global dir 1. `.git/hooks/` - executed by `git` itself, this is `gitlab-shell/hooks/` 1. `.git/custom_hooks/` - per project hook (this is already existing behavior) 1. `.git/custom_hooks/.d/*` - per project hooks -1. `.git/hooks/.d/*` - global hooks: all executable files (minus editor backup files) +1. `.git/hooks/.d/*` OR `//*` - global hooks: all executable files (minus editor backup files) -Files in `.d` directories need to be executable and not match the backup file pattern (`*~`). +Files in `.d` directories need to be executable and not match the backup file +pattern (`*~`). -The hooks of the same type are executed in order and execution stops on the first -script exiting with non-zero value. +The hooks of the same type are executed in order and execution stops on the +first script exiting with a non-zero value. ## Custom error messages diff --git a/doc/api/services.md b/doc/api/services.md index 3dad953cd1e..1466b8189b0 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -703,9 +703,9 @@ Get Redmine service settings for a project. GET /projects/:id/services/redmine ``` -## Slack +## Slack notifications -A team communication tool for the 21st century +Receive event notifications in Slack ### Create/Edit Slack service @@ -737,6 +737,40 @@ Get Slack service settings for a project. GET /projects/:id/services/slack ``` +## Mattermost notifications + +Receive event notifications in Mattermost + +### Create/Edit Mattermost notifications service + +Set Mattermost service for a project. + +``` +PUT /projects/:id/services/mattermost +``` + +Parameters: + +- `webhook` (**required**) - https://mattermost.example/hooks/1298aff... +- `username` (optional) - username +- `channel` (optional) - #channel + +### Delete Mattermost notifications service + +Delete Mattermost Notifications service for a project. + +``` +DELETE /projects/:id/services/mattermost +``` + +### Get Mattermost notifications service settings + +Get Mattermost notifications service settings for a project. + +``` +GET /projects/:id/services/mattermost +``` + ## JetBrains TeamCity CI A continuous integration and build server diff --git a/doc/development/ux_guide/basics.md b/doc/development/ux_guide/basics.md index 76b739386a5..e81729556d8 100644 --- a/doc/development/ux_guide/basics.md +++ b/doc/development/ux_guide/basics.md @@ -5,6 +5,7 @@ * [Typography](#typography) * [Icons](#icons) * [Color](#color) +* [Cursors](#cursors) --- @@ -59,3 +60,18 @@ GitLab uses Font Awesome icons throughout our interface. > TODO: Establish a perspective for color in terms of our personality and rationalize with Marketing usage. +--- + +## Cursors +The mouse cursor is key in helping users understand how to interact with elements on the screen. + +| | | +| :------: | :------- | +| ![Default cursor](img/cursors-default.png) | Default cursor | +| ![Pointer cursor](img/cursors-pointer.png) | Pointer cursor: used to indicate that you can click on an element to invoke a command or navigate, such as links and buttons | +| ![Move cursor](img/cursors-move.png) | Move cursor: used to indicate that you can move an element around on the screen | +| ![Pan opened cursor](img/cursors-panopened.png) | Pan cursor (opened): indicates that you can grab and move the entire canvas, affecting what is seen in the view port. | +| ![Pan closed cursor](img/cursors-panclosed.png) | Pan cursor (closed): indicates that you are actively panning the canvas. | +| ![I-beam cursor](img/cursors-ibeam.png) | I-beam cursor: indicates that this is either text that you can select and copy, or a text field that you can click into to enter text. | + + diff --git a/doc/development/ux_guide/img/cursors-default.png b/doc/development/ux_guide/img/cursors-default.png new file mode 100644 index 00000000000..c188ec4e351 Binary files /dev/null and b/doc/development/ux_guide/img/cursors-default.png differ diff --git a/doc/development/ux_guide/img/cursors-ibeam.png b/doc/development/ux_guide/img/cursors-ibeam.png new file mode 100644 index 00000000000..86f28639982 Binary files /dev/null and b/doc/development/ux_guide/img/cursors-ibeam.png differ diff --git a/doc/development/ux_guide/img/cursors-move.png b/doc/development/ux_guide/img/cursors-move.png new file mode 100644 index 00000000000..a9c610eaa88 Binary files /dev/null and b/doc/development/ux_guide/img/cursors-move.png differ diff --git a/doc/development/ux_guide/img/cursors-panclosed.png b/doc/development/ux_guide/img/cursors-panclosed.png new file mode 100644 index 00000000000..6d247a765ac Binary files /dev/null and b/doc/development/ux_guide/img/cursors-panclosed.png differ diff --git a/doc/development/ux_guide/img/cursors-panopened.png b/doc/development/ux_guide/img/cursors-panopened.png new file mode 100644 index 00000000000..76f2eeda831 Binary files /dev/null and b/doc/development/ux_guide/img/cursors-panopened.png differ diff --git a/doc/development/ux_guide/img/cursors-pointer.png b/doc/development/ux_guide/img/cursors-pointer.png new file mode 100644 index 00000000000..d86dd955fa7 Binary files /dev/null and b/doc/development/ux_guide/img/cursors-pointer.png differ diff --git a/doc/project_services/img/mattermost_configuration.png b/doc/project_services/img/mattermost_configuration.png new file mode 100644 index 00000000000..3c5ff5ee317 Binary files /dev/null and b/doc/project_services/img/mattermost_configuration.png differ diff --git a/doc/project_services/mattermost.md b/doc/project_services/mattermost.md new file mode 100644 index 00000000000..fbc7dfeee6d --- /dev/null +++ b/doc/project_services/mattermost.md @@ -0,0 +1,45 @@ +# Mattermost Notifications Service + +## On Mattermost + +To enable Mattermost integration you must create an incoming webhook integration: + +1. Sign in to your Mattermost instance +1. Visit incoming webhooks, that will be something like: https://mattermost.example/your_team_name/integrations/incoming_webhooks/add +1. Choose a display name, description and channel, those can be overridden on GitLab +1. Save it, copy the **Webhook URL**, we'll need this later for GitLab. + +There might be some cases that Incoming Webhooks are blocked by admin, ask your mattermost admin to enable +it on https://mattermost.example/admin_console/integrations/custom. + +Display name override is not enabled by default, you need to ask your admin to enable it on that same section. + +## On GitLab + +After you set up Mattermost, it's time to set up GitLab. + +Go to your project's **Settings > Services > Mattermost Notifications** and you will see a +checkbox with the following events that can be triggered: + +- Push +- Issue +- Merge request +- Note +- Tag push +- Build +- Wiki page + +Bellow each of these event checkboxes, you will have an input field to insert +which Mattermost channel you want to send that event message, with `#town-square` +being the default. The hash sign is optional. + +At the end, fill in your Mattermost details: + +| Field | Description | +| ----- | ----------- | +| **Webhook** | The incoming webhooks which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo... | +| **Username** | Optional username which can be on messages sent to Mattermost. Fill this in if you want to change the username of the bot. | +| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. | + + +![Mattermost configuration](img/mattermost_configuration.png) diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index a7bcd186a8c..0f398874b8f 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -44,10 +44,11 @@ further configuration instructions and details. Contributions are welcome. | JetBrains TeamCity CI | A continuous integration and build server | | [Kubernetes](kubernetes.md) | A containerized deployment service | | [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands | +| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost | +| [Slack Notifications](slack.md) | Receive event notifications in Slack | | PivotalTracker | Project Management Software (Source Commits Endpoint) | | Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop | | [Redmine](redmine.md) | Redmine issue tracker | -| [Slack](slack.md) | A team communication tool for the 21st century | ## Services Templates diff --git a/doc/project_services/slack.md b/doc/project_services/slack.md index 3cfe77c9f85..0b682b43810 100644 --- a/doc/project_services/slack.md +++ b/doc/project_services/slack.md @@ -1,4 +1,4 @@ -# Slack Service +# Slack Notifications Service ## On Slack @@ -15,7 +15,7 @@ Slack: After you set up Slack, it's time to set up GitLab. -Go to your project's **Settings > Services > Slack** and you will see a +Go to your project's **Settings > Services > Slack Notifications** and you will see a checkbox with the following events that can be triggered: - Push diff --git a/doc/update/8.14-to-8.15.md b/doc/update/8.14-to-8.15.md index 5556dae2551..4eacab0c890 100644 --- a/doc/update/8.14-to-8.15.md +++ b/doc/update/8.14-to-8.15.md @@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-15-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch --all --tags -sudo -u git -H git checkout v4.0.3 +sudo -u git -H git checkout v4.1.0 ``` ### 6. Update gitlab-workhorse diff --git a/lib/api/services.rb b/lib/api/services.rb index b1e072b4f47..59232c84c24 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -473,7 +473,7 @@ module API desc: 'The description of the tracker' } ], - 'slack' => [ + 'slack-notification' => [ { required: true, name: :webhook, @@ -493,6 +493,14 @@ module API desc: 'The channel name' } ], + 'mattermost-notification' => [ + { + required: true, + name: :webhook, + type: String, + desc: 'The Mattermost webhook. e.g. http://mattermost_host/hooks/...' + } + ], 'teamcity' => [ { required: true, diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 23a504ff965..8f561c8f90b 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -107,7 +107,7 @@ describe 'Commits' do describe 'Cancel build' do it 'cancels build' do visit ci_status_path(pipeline) - click_on 'Cancel' + find('a.btn[title="Cancel"]').click expect(page).to have_content 'canceled' end end diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz index bfe59bdb90e..d3165d07d7b 100644 Binary files a/spec/features/projects/import_export/test_project_export.tar.gz and b/spec/features/projects/import_export/test_project_export.tar.gz differ diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 9d43d264bdf..0a77eaa123c 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -38,8 +38,8 @@ describe "Pipelines", feature: true, js: true do expect(page).to have_css('#js-tab-pipeline.active') end - context 'pipeline graph' do - context 'running build' do + describe 'pipeline graph' do + context 'when pipeline has running builds' do it 'shows a running icon and a cancel action for the running build' do page.within('a[data-title="deploy - running"]') do expect(page).to have_selector('.ci-status-icon-running') @@ -58,7 +58,7 @@ describe "Pipelines", feature: true, js: true do end end - context 'success build' do + context 'when pipeline has successful builds' do it 'shows the success icon and a retry action for the successfull build' do page.within('a[data-title="build - passed"]') do expect(page).to have_selector('.ci-status-icon-success') @@ -77,7 +77,7 @@ describe "Pipelines", feature: true, js: true do end end - context 'failed build' do + context 'when pipeline has failed builds' do it 'shows the failed icon and a retry action for the failed build' do page.within('a[data-title="test - failed"]') do expect(page).to have_selector('.ci-status-icon-failed') @@ -96,7 +96,7 @@ describe "Pipelines", feature: true, js: true do end end - context 'manual build' do + context 'when pipeline has manual builds' do it 'shows the skipped icon and a play action for the manual build' do page.within('a[data-title="manual build - manual play action"]') do expect(page).to have_selector('.ci-status-icon-skipped') @@ -115,7 +115,7 @@ describe "Pipelines", feature: true, js: true do end end - context 'external build' do + context 'when pipeline has external build' do it 'shows the success icon and the generic comit status build' do expect(page).to have_selector('.ci-status-icon-success') expect(page).to have_content('jenkins') diff --git a/spec/features/projects/services/slack_service_spec.rb b/spec/features/projects/services/slack_service_spec.rb index 16541f51d98..320ed13a01d 100644 --- a/spec/features/projects/services/slack_service_spec.rb +++ b/spec/features/projects/services/slack_service_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' feature 'Projects > Slack service > Setup events', feature: true do let(:user) { create(:user) } - let(:service) { SlackService.new } - let(:project) { create(:project, slack_service: service) } + let(:service) { SlackNotificationService.new } + let(:project) { create(:project, slack_notification_service: service) } background do service.fields diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6 index 9e94c9d1d74..49e56249565 100644 --- a/spec/javascripts/abuse_reports_spec.js.es6 +++ b/spec/javascripts/abuse_reports_spec.js.es6 @@ -1,42 +1,44 @@ -/* eslint-disable space-before-function-paren, no-new, padded-blocks */ - +/*= require lib/utils/text_utility */ /*= require abuse_reports */ -/*= require jquery */ ((global) => { - const FIXTURE = 'abuse_reports.html'; - const MAX_MESSAGE_LENGTH = 500; + describe('Abuse Reports', () => { + const FIXTURE = 'abuse_reports/abuse_reports_list.html.raw'; + const MAX_MESSAGE_LENGTH = 500; - function assertMaxLength($message) { - expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH); - } + let messages; + + const assertMaxLength = $message => expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH); + const findMessage = searchText => messages.filter( + (index, element) => element.innerText.indexOf(searchText) > -1, + ).first(); - describe('Abuse Reports', function() { fixture.preload(FIXTURE); - beforeEach(function() { + beforeEach(function () { fixture.load(FIXTURE); - new global.AbuseReports(); + this.abuseReports = new global.AbuseReports(); + messages = $('.abuse-reports .message'); }); - it('should truncate long messages', function() { - const $longMessage = $('#long'); + + it('should truncate long messages', () => { + const $longMessage = findMessage('LONG MESSAGE'); expect($longMessage.data('original-message')).toEqual(jasmine.anything()); assertMaxLength($longMessage); }); - it('should not truncate short messages', function() { - const $shortMessage = $('#short'); + it('should not truncate short messages', () => { + const $shortMessage = findMessage('SHORT MESSAGE'); expect($shortMessage.data('original-message')).not.toEqual(jasmine.anything()); }); - it('should allow clicking a truncated message to expand and collapse the full message', function() { - const $longMessage = $('#long'); + it('should allow clicking a truncated message to expand and collapse the full message', () => { + const $longMessage = findMessage('LONG MESSAGE'); $longMessage.click(); expect($longMessage.data('original-message').length).toEqual($longMessage.text().length); $longMessage.click(); assertMaxLength($longMessage); }); }); - })(window.gl); diff --git a/spec/javascripts/fixtures/abuse_reports.html.haml b/spec/javascripts/fixtures/abuse_reports.html.haml deleted file mode 100644 index 2ec302abcb7..00000000000 --- a/spec/javascripts/fixtures/abuse_reports.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -.abuse-reports - .message#long - Cat ipsum dolor sit amet, hide head under blanket so no one can see. - Gate keepers of hell eat and than sleep on your face but hunt by meowing - loudly at 5am next to human slave food dispenser cats go for world - domination or chase laser, yet poop on grasses chirp at birds. Cat is love, - cat is life chase after silly colored fish toys around the house climb a - tree, wait for a fireman jump to fireman then scratch his face fall asleep - on the washing machine lies down always hungry so caticus cuteicus. Sit on - human. Spot something, big eyes, big eyes, crouch, shake butt, prepare to - pounce sleep in the bathroom sink hiss at vacuum cleaner hide head under - blanket so no one can see throwup on your pillow. - .message#short - Cat ipsum dolor sit amet, groom yourself 4 hours - checked, have your - beauty sleep 18 hours - checked, be fabulous for the rest of the day - - checked! for shake treat bag. diff --git a/spec/javascripts/fixtures/abuse_reports.rb b/spec/javascripts/fixtures/abuse_reports.rb new file mode 100644 index 00000000000..de673f94d72 --- /dev/null +++ b/spec/javascripts/fixtures/abuse_reports.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Admin::AbuseReportsController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let!(:abuse_report) { create(:abuse_report) } + let!(:abuse_report_with_short_message) { create(:abuse_report, message: 'SHORT MESSAGE') } + let!(:abuse_report_with_long_message) { create(:abuse_report, message: "LONG MESSAGE\n" * 50) } + + render_views + + before(:all) do + clean_frontend_fixtures('abuse_reports/') + end + + before(:each) do + sign_in(admin) + end + + it 'abuse_reports/abuse_reports_list.html.raw' do |example| + get :index + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index c4ee838b7c9..9b49d6837c3 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -136,7 +136,8 @@ project: - assembla_service - asana_service - gemnasium_service -- slack_service +- slack_notification_service +- mattermost_notification_service - buildkite_service - bamboo_service - teamcity_service diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb index c79c6494576..ab1ab22795c 100644 --- a/spec/lib/gitlab/middleware/multipart_spec.rb +++ b/spec/lib/gitlab/middleware/multipart_spec.rb @@ -7,7 +7,7 @@ describe Gitlab::Middleware::Multipart do let(:middleware) { described_class.new(app) } it 'opens top-level files' do - Tempfile.open do |tempfile| + Tempfile.open('top-level') do |tempfile| env = post_env({ 'file' => tempfile.path }, { 'file.name' => 'filename' }, Gitlab::Workhorse.secret, 'gitlab-workhorse') expect(app).to receive(:call) do |env| @@ -33,7 +33,7 @@ describe Gitlab::Middleware::Multipart do end it 'opens files one level deep' do - Tempfile.open do |tempfile| + Tempfile.open('one-level') do |tempfile| in_params = { 'user' => { 'avatar' => { '.name' => 'filename' } } } env = post_env({ 'user[avatar]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse') @@ -48,7 +48,7 @@ describe Gitlab::Middleware::Multipart do end it 'opens files two levels deep' do - Tempfile.open do |tempfile| + Tempfile.open('two-levels') do |tempfile| in_params = { 'project' => { 'milestone' => { 'themesong' => { '.name' => 'filename' } } } } env = post_env({ 'project[milestone][themesong]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse') diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 069c59fb5ca..9fd06bb6b23 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -128,4 +128,26 @@ describe Namespace, models: true do it { expect(group.full_path).to eq(group.path) } it { expect(nested_group.full_path).to eq("#{group.path}/#{nested_group.path}") } end + + describe '#full_name' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + + it { expect(group.full_name).to eq(group.name) } + it { expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") } + end + + describe '#parents' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + let(:deep_nested_group) { create(:group, parent: nested_group) } + let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } + + it 'returns the correct parents' do + expect(very_deep_nested_group.parents).to eq([group, nested_group, deep_nested_group]) + expect(deep_nested_group.parents).to eq([group, nested_group]) + expect(nested_group.parents).to eq([group]) + expect(group.parents).to eq([]) + end + end end diff --git a/spec/models/project_services/slack_service/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb similarity index 94% rename from spec/models/project_services/slack_service/build_message_spec.rb rename to spec/models/project_services/chat_message/build_message_spec.rb index 452f4e2782c..b71d153f814 100644 --- a/spec/models/project_services/slack_service/build_message_spec.rb +++ b/spec/models/project_services/chat_message/build_message_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe SlackService::BuildMessage do - subject { SlackService::BuildMessage.new(args) } +describe ChatMessage::BuildMessage do + subject { described_class.new(args) } let(:args) do { diff --git a/spec/models/project_services/slack_service/issue_message_spec.rb b/spec/models/project_services/chat_message/issue_message_spec.rb similarity index 93% rename from spec/models/project_services/slack_service/issue_message_spec.rb rename to spec/models/project_services/chat_message/issue_message_spec.rb index 98c36ec088d..ebe0ead4408 100644 --- a/spec/models/project_services/slack_service/issue_message_spec.rb +++ b/spec/models/project_services/chat_message/issue_message_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe SlackService::IssueMessage, models: true do - subject { SlackService::IssueMessage.new(args) } +describe ChatMessage::IssueMessage, models: true do + subject { described_class.new(args) } let(:args) do { diff --git a/spec/models/project_services/slack_service/merge_message_spec.rb b/spec/models/project_services/chat_message/merge_message_spec.rb similarity index 92% rename from spec/models/project_services/slack_service/merge_message_spec.rb rename to spec/models/project_services/chat_message/merge_message_spec.rb index c5c052d9af1..07c414c6ca4 100644 --- a/spec/models/project_services/slack_service/merge_message_spec.rb +++ b/spec/models/project_services/chat_message/merge_message_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe SlackService::MergeMessage, models: true do - subject { SlackService::MergeMessage.new(args) } +describe ChatMessage::MergeMessage, models: true do + subject { described_class.new(args) } let(:args) do { diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/chat_message/note_message_spec.rb similarity index 92% rename from spec/models/project_services/slack_service/note_message_spec.rb rename to spec/models/project_services/chat_message/note_message_spec.rb index 97f818125d3..31936da40a2 100644 --- a/spec/models/project_services/slack_service/note_message_spec.rb +++ b/spec/models/project_services/chat_message/note_message_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe SlackService::NoteMessage, models: true do +describe ChatMessage::NoteMessage, models: true do let(:color) { '#345' } before do @@ -36,7 +36,7 @@ describe SlackService::NoteMessage, models: true do end it 'returns a message regarding notes on commits' do - message = SlackService::NoteMessage.new(@args) + message = described_class.new(@args) expect(message.pretext).to eq("test.user in : " \ "*Added a commit message*") @@ -62,7 +62,7 @@ describe SlackService::NoteMessage, models: true do end it 'returns a message regarding notes on a merge request' do - message = SlackService::NoteMessage.new(@args) + message = described_class.new(@args) expect(message.pretext).to eq("test.user in : " \ "*merge request title*") @@ -88,7 +88,7 @@ describe SlackService::NoteMessage, models: true do end it 'returns a message regarding notes on an issue' do - message = SlackService::NoteMessage.new(@args) + message = described_class.new(@args) expect(message.pretext).to eq( "test.user in : " \ @@ -114,7 +114,7 @@ describe SlackService::NoteMessage, models: true do end it 'returns a message regarding notes on a project snippet' do - message = SlackService::NoteMessage.new(@args) + message = described_class.new(@args) expect(message.pretext).to eq("test.user in : " \ "*snippet title*") diff --git a/spec/models/project_services/slack_service/pipeline_message_spec.rb b/spec/models/project_services/chat_message/pipeline_message_spec.rb similarity index 94% rename from spec/models/project_services/slack_service/pipeline_message_spec.rb rename to spec/models/project_services/chat_message/pipeline_message_spec.rb index 4098500122f..eca71db07b6 100644 --- a/spec/models/project_services/slack_service/pipeline_message_spec.rb +++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe SlackService::PipelineMessage do - subject { SlackService::PipelineMessage.new(args) } +describe ChatMessage::PipelineMessage do + subject { described_class.new(args) } let(:user) { { name: 'hacker' } } let(:args) do diff --git a/spec/models/project_services/slack_service/push_message_spec.rb b/spec/models/project_services/chat_message/push_message_spec.rb similarity index 95% rename from spec/models/project_services/slack_service/push_message_spec.rb rename to spec/models/project_services/chat_message/push_message_spec.rb index 17cd05e24f1..b781c4505db 100644 --- a/spec/models/project_services/slack_service/push_message_spec.rb +++ b/spec/models/project_services/chat_message/push_message_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe SlackService::PushMessage, models: true do - subject { SlackService::PushMessage.new(args) } +describe ChatMessage::PushMessage, models: true do + subject { described_class.new(args) } let(:args) do { diff --git a/spec/models/project_services/slack_service/wiki_page_message_spec.rb b/spec/models/project_services/chat_message/wiki_page_message_spec.rb similarity index 97% rename from spec/models/project_services/slack_service/wiki_page_message_spec.rb rename to spec/models/project_services/chat_message/wiki_page_message_spec.rb index 093911598b0..94c04dc0865 100644 --- a/spec/models/project_services/slack_service/wiki_page_message_spec.rb +++ b/spec/models/project_services/chat_message/wiki_page_message_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe SlackService::WikiPageMessage, models: true do +describe ChatMessage::WikiPageMessage, models: true do subject { described_class.new(args) } let(:args) do diff --git a/spec/models/project_services/chat_notification_service_spec.rb b/spec/models/project_services/chat_notification_service_spec.rb new file mode 100644 index 00000000000..c98e7ee14fd --- /dev/null +++ b/spec/models/project_services/chat_notification_service_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe ChatNotificationService, models: true do + describe "Associations" do + before do + allow(subject).to receive(:activated?).and_return(true) + end + + it { is_expected.to validate_presence_of :webhook } + end +end diff --git a/spec/models/project_services/mattermost_notification_service_spec.rb b/spec/models/project_services/mattermost_notification_service_spec.rb new file mode 100644 index 00000000000..c01e64b4c8e --- /dev/null +++ b/spec/models/project_services/mattermost_notification_service_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe MattermostNotificationService, models: true do + it_behaves_like "slack or mattermost" +end diff --git a/spec/models/project_services/slack_notification_service_spec.rb b/spec/models/project_services/slack_notification_service_spec.rb new file mode 100644 index 00000000000..59ddddf7454 --- /dev/null +++ b/spec/models/project_services/slack_notification_service_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe SlackNotificationService, models: true do + it_behaves_like "slack or mattermost" +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 21ff238841e..bab3c3dbb02 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -22,7 +22,8 @@ describe Project, models: true do it { is_expected.to have_many(:protected_branches).dependent(:destroy) } it { is_expected.to have_many(:chat_services) } it { is_expected.to have_one(:forked_project_link).dependent(:destroy) } - it { is_expected.to have_one(:slack_service).dependent(:destroy) } + it { is_expected.to have_one(:slack_notification_service).dependent(:destroy) } + it { is_expected.to have_one(:mattermost_notification_service).dependent(:destroy) } it { is_expected.to have_one(:pushover_service).dependent(:destroy) } it { is_expected.to have_one(:asana_service).dependent(:destroy) } it { is_expected.to have_many(:boards).dependent(:destroy) } diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb new file mode 100644 index 00000000000..a20ac303a53 --- /dev/null +++ b/spec/policies/group_policy_spec.rb @@ -0,0 +1,108 @@ +require 'spec_helper' + +describe GroupPolicy, models: true do + let(:guest) { create(:user) } + let(:reporter) { create(:user) } + let(:developer) { create(:user) } + let(:master) { create(:user) } + let(:owner) { create(:user) } + let(:admin) { create(:admin) } + let(:group) { create(:group) } + + let(:master_permissions) do + [ + :create_projects, + :admin_milestones, + :admin_label + ] + end + + let(:owner_permissions) do + [ + :admin_group, + :admin_namespace, + :admin_group_member, + :change_visibility_level + ] + end + + before do + group.add_guest(guest) + group.add_reporter(reporter) + group.add_developer(developer) + group.add_master(master) + group.add_owner(owner) + end + + subject { described_class.abilities(current_user, group).to_set } + + context 'with no user' do + let(:current_user) { nil } + + it do + is_expected.to include(:read_group) + is_expected.not_to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'guests' do + let(:current_user) { guest } + + it do + is_expected.to include(:read_group) + is_expected.not_to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'reporter' do + let(:current_user) { reporter } + + it do + is_expected.to include(:read_group) + is_expected.not_to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'developer' do + let(:current_user) { developer } + + it do + is_expected.to include(:read_group) + is_expected.not_to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'master' do + let(:current_user) { master } + + it do + is_expected.to include(:read_group) + is_expected.to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'owner' do + let(:current_user) { owner } + + it do + is_expected.to include(:read_group) + is_expected.to include(*master_permissions) + is_expected.to include(*owner_permissions) + end + end + + context 'admin' do + let(:current_user) { admin } + + it do + is_expected.to include(:read_group) + is_expected.to include(*master_permissions) + is_expected.to include(*owner_permissions) + end + end +end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 500d224ff98..eafbea46905 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -376,5 +376,10 @@ describe Issues::UpdateService, services: true do let(:mentionable) { issue } include_examples 'updating mentions', Issues::UpdateService end + + include_examples 'issuable update service' do + let(:open_issuable) { issue } + let(:closed_issuable) { create(:closed_issue, project: project) } + end end end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 790ef765f3a..88c786947d3 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -320,5 +320,10 @@ describe MergeRequests::UpdateService, services: true do expect(issue_ids).to be_empty end end + + include_examples 'issuable update service' do + let(:open_issuable) { merge_request } + let(:closed_issuable) { create(:closed_merge_request, source_project: project) } + end end end diff --git a/spec/support/services/issuable_update_service_shared_examples.rb b/spec/support/services/issuable_update_service_shared_examples.rb new file mode 100644 index 00000000000..a3336755773 --- /dev/null +++ b/spec/support/services/issuable_update_service_shared_examples.rb @@ -0,0 +1,17 @@ +shared_examples 'issuable update service' do + context 'changing state' do + before { expect(project).to receive(:execute_hooks).once } + + context 'to reopened' do + it 'executes hooks only once' do + described_class.new(project, user, state_event: 'reopen').execute(closed_issuable) + end + end + + context 'to closed' do + it 'executes hooks only once' do + described_class.new(project, user, state_event: 'close').execute(open_issuable) + end + end + end +end diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/support/slack_mattermost_shared_examples.rb similarity index 76% rename from spec/models/project_services/slack_service_spec.rb rename to spec/support/slack_mattermost_shared_examples.rb index c07a70a8069..56d4965f74d 100644 --- a/spec/models/project_services/slack_service_spec.rb +++ b/spec/support/slack_mattermost_shared_examples.rb @@ -1,7 +1,7 @@ -require 'spec_helper' +Dir[Rails.root.join("app/models/project_services/chat_message/*.rb")].each { |f| require f } -describe SlackService, models: true do - let(:slack) { SlackService.new } +RSpec.shared_examples 'slack or mattermost' do + let(:chat_service) { described_class.new } let(:webhook_url) { 'https://example.gitlab.com/' } describe "Associations" do @@ -24,7 +24,7 @@ describe SlackService, models: true do end end - describe "Execute" do + describe "#execute" do let(:user) { create(:user) } let(:project) { create(:project) } let(:username) { 'slack_username' } @@ -35,7 +35,7 @@ describe SlackService, models: true do end before do - allow(slack).to receive_messages( + allow(chat_service).to receive_messages( project: project, project_id: project.id, service_hook: true, @@ -77,54 +77,55 @@ describe SlackService, models: true do @wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create') end - it "calls Slack API for push events" do - slack.execute(push_sample_data) + it "calls Slack/Mattermost API for push events" do + chat_service.execute(push_sample_data) expect(WebMock).to have_requested(:post, webhook_url).once end - it "calls Slack API for issue events" do - slack.execute(@issues_sample_data) + it "calls Slack/Mattermost API for issue events" do + chat_service.execute(@issues_sample_data) expect(WebMock).to have_requested(:post, webhook_url).once end - it "calls Slack API for merge requests events" do - slack.execute(@merge_sample_data) + it "calls Slack/Mattermost API for merge requests events" do + chat_service.execute(@merge_sample_data) expect(WebMock).to have_requested(:post, webhook_url).once end - it "calls Slack API for wiki page events" do - slack.execute(@wiki_page_sample_data) + it "calls Slack/Mattermost API for wiki page events" do + chat_service.execute(@wiki_page_sample_data) expect(WebMock).to have_requested(:post, webhook_url).once end it 'uses the username as an option for slack when configured' do - allow(slack).to receive(:username).and_return(username) + allow(chat_service).to receive(:username).and_return(username) + expect(Slack::Notifier).to receive(:new). - with(webhook_url, username: username). + with(webhook_url, username: username, channel: chat_service.default_channel). and_return( double(:slack_service).as_null_object ) - slack.execute(push_sample_data) + chat_service.execute(push_sample_data) end it 'uses the channel as an option when it is configured' do - allow(slack).to receive(:channel).and_return(channel) + allow(chat_service).to receive(:channel).and_return(channel) expect(Slack::Notifier).to receive(:new). with(webhook_url, channel: channel). and_return( double(:slack_service).as_null_object ) - slack.execute(push_sample_data) + chat_service.execute(push_sample_data) end context "event channels" do it "uses the right channel for push event" do - slack.update_attributes(push_channel: "random") + chat_service.update_attributes(push_channel: "random") expect(Slack::Notifier).to receive(:new). with(webhook_url, channel: "random"). @@ -132,11 +133,11 @@ describe SlackService, models: true do double(:slack_service).as_null_object ) - slack.execute(push_sample_data) + chat_service.execute(push_sample_data) end it "uses the right channel for merge request event" do - slack.update_attributes(merge_request_channel: "random") + chat_service.update_attributes(merge_request_channel: "random") expect(Slack::Notifier).to receive(:new). with(webhook_url, channel: "random"). @@ -144,11 +145,11 @@ describe SlackService, models: true do double(:slack_service).as_null_object ) - slack.execute(@merge_sample_data) + chat_service.execute(@merge_sample_data) end it "uses the right channel for issue event" do - slack.update_attributes(issue_channel: "random") + chat_service.update_attributes(issue_channel: "random") expect(Slack::Notifier).to receive(:new). with(webhook_url, channel: "random"). @@ -156,11 +157,11 @@ describe SlackService, models: true do double(:slack_service).as_null_object ) - slack.execute(@issues_sample_data) + chat_service.execute(@issues_sample_data) end it "uses the right channel for wiki event" do - slack.update_attributes(wiki_page_channel: "random") + chat_service.update_attributes(wiki_page_channel: "random") expect(Slack::Notifier).to receive(:new). with(webhook_url, channel: "random"). @@ -168,7 +169,7 @@ describe SlackService, models: true do double(:slack_service).as_null_object ) - slack.execute(@wiki_page_sample_data) + chat_service.execute(@wiki_page_sample_data) end context "note event" do @@ -177,7 +178,7 @@ describe SlackService, models: true do end it "uses the right channel" do - slack.update_attributes(note_channel: "random") + chat_service.update_attributes(note_channel: "random") note_data = Gitlab::DataBuilder::Note.build(issue_note, user) @@ -187,7 +188,7 @@ describe SlackService, models: true do double(:slack_service).as_null_object ) - slack.execute(note_data) + chat_service.execute(note_data) end end end @@ -198,7 +199,7 @@ describe SlackService, models: true do let(:project) { create(:project, creator_id: user.id) } before do - allow(slack).to receive_messages( + allow(chat_service).to receive_messages( project: project, project_id: project.id, service_hook: true, @@ -216,9 +217,9 @@ describe SlackService, models: true do note: 'a comment on a commit') end - it "calls Slack API for commit comment events" do + it "calls Slack/Mattermost API for commit comment events" do data = Gitlab::DataBuilder::Note.build(commit_note, user) - slack.execute(data) + chat_service.execute(data) expect(WebMock).to have_requested(:post, webhook_url).once end @@ -232,7 +233,7 @@ describe SlackService, models: true do it "calls Slack API for merge request comment events" do data = Gitlab::DataBuilder::Note.build(merge_request_note, user) - slack.execute(data) + chat_service.execute(data) expect(WebMock).to have_requested(:post, webhook_url).once end @@ -245,7 +246,7 @@ describe SlackService, models: true do it "calls Slack API for issue comment events" do data = Gitlab::DataBuilder::Note.build(issue_note, user) - slack.execute(data) + chat_service.execute(data) expect(WebMock).to have_requested(:post, webhook_url).once end @@ -259,7 +260,7 @@ describe SlackService, models: true do it "calls Slack API for snippet comment events" do data = Gitlab::DataBuilder::Note.build(snippet_note, user) - slack.execute(data) + chat_service.execute(data) expect(WebMock).to have_requested(:post, webhook_url).once end @@ -277,21 +278,21 @@ describe SlackService, models: true do end before do - allow(slack).to receive_messages( + allow(chat_service).to receive_messages( project: project, service_hook: true, webhook: webhook_url ) end - shared_examples 'call Slack API' do + shared_examples 'call Slack/Mattermost API' do before do WebMock.stub_request(:post, webhook_url) end - it 'calls Slack API for pipeline events' do + it 'calls Slack/Mattermost API for pipeline events' do data = Gitlab::DataBuilder::Pipeline.build(pipeline) - slack.execute(data) + chat_service.execute(data) expect(WebMock).to have_requested(:post, webhook_url).once end @@ -300,16 +301,16 @@ describe SlackService, models: true do context 'with failed pipeline' do let(:status) { 'failed' } - it_behaves_like 'call Slack API' + it_behaves_like 'call Slack/Mattermost API' end context 'with succeeded pipeline' do let(:status) { 'success' } context 'with default to notify_only_broken_pipelines' do - it 'does not call Slack API for pipeline events' do + it 'does not call Slack/Mattermost API for pipeline events' do data = Gitlab::DataBuilder::Pipeline.build(pipeline) - result = slack.execute(data) + result = chat_service.execute(data) expect(result).to be_falsy end @@ -317,10 +318,10 @@ describe SlackService, models: true do context 'with setting notify_only_broken_pipelines to false' do before do - slack.notify_only_broken_pipelines = false + chat_service.notify_only_broken_pipelines = false end - it_behaves_like 'call Slack API' + it_behaves_like 'call Slack/Mattermost API' end end end diff --git a/vendor/assets/javascripts/katex.js b/vendor/assets/javascripts/katex.js index 9596b839832..beb31ca6a7e 100644 --- a/vendor/assets/javascripts/katex.js +++ b/vendor/assets/javascripts/katex.js @@ -1,3 +1,44 @@ +/* + The MIT License (MIT) + + Copyright (c) 2015 Khan Academy + + This software also uses portions of the underscore.js project, which is + MIT licensed with the following copyright: + + Copyright (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative + Reporters & Editors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +/* + Here is how to build a version of KaTeX that works with gitlab. + + The problem is that the standard procedure for changing font location doesn't work for the empty string. + + 1. Clone KaTeX. Anything later than 4fb9445a9 (is merged into master) will do. + 2. make (requires node) + 3. sed -i 's,fonts/,,' build/katex.css + 4. Copy build/katex.js, build/katex.css and fonts/* to gitlab. +*/ + (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.katex = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o