diff options
54 files changed, 623 insertions, 158 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index 0df519e1d75..78aefcd0c7a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -327,7 +327,6 @@ RSpec/LeakyConstantDeclaration: Enabled: true Exclude: - 'spec/**/*.rb' - - 'qa/spec/**/*.rb' RSpec/EmptyLineAfterHook: Enabled: false diff --git a/app/assets/javascripts/static_site_editor/components/static_site_editor.vue b/app/assets/javascripts/static_site_editor/components/static_site_editor.vue index 82917319fc3..d45c3d08ef4 100644 --- a/app/assets/javascripts/static_site_editor/components/static_site_editor.vue +++ b/app/assets/javascripts/static_site_editor/components/static_site_editor.vue @@ -5,7 +5,7 @@ import { GlSkeletonLoader } from '@gitlab/ui'; import EditArea from './edit_area.vue'; import EditHeader from './edit_header.vue'; import SavedChangesMessage from './saved_changes_message.vue'; -import Toolbar from './publish_toolbar.vue'; +import PublishToolbar from './publish_toolbar.vue'; import InvalidContentMessage from './invalid_content_message.vue'; import SubmitChangesError from './submit_changes_error.vue'; @@ -16,7 +16,7 @@ export default { InvalidContentMessage, GlSkeletonLoader, SavedChangesMessage, - Toolbar, + PublishToolbar, SubmitChangesError, }, computed: { @@ -80,7 +80,7 @@ export default { :value="content" @input="setContent" /> - <toolbar + <publish-toolbar :return-url="returnUrl" :saveable="contentChanged" :saving-changes="isSavingChanges" diff --git a/app/controllers/projects/alert_management_controller.rb b/app/controllers/projects/alert_management_controller.rb index 46db87dba94..5480e19072d 100644 --- a/app/controllers/projects/alert_management_controller.rb +++ b/app/controllers/projects/alert_management_controller.rb @@ -1,9 +1,14 @@ # frozen_string_literal: true class Projects::AlertManagementController < Projects::ApplicationController + before_action :ensure_feature_enabled + def index - respond_to do |format| - format.html - end + end + + private + + def ensure_feature_enabled + render_404 unless Feature.enabled?(:alert_management_minimal, project) end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 3aae8990f07..3d53ad1a29f 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -193,7 +193,8 @@ class Projects::IssuesController < Projects::ApplicationController ExportCsvWorker.perform_async(current_user.id, project.id, finder_options.to_h) # rubocop:disable CodeReuse/Worker index_path = project_issues_path(project) - redirect_to(index_path, notice: "Your CSV export has started. It will be emailed to #{current_user.notification_email} when complete.") + message = _('Your CSV export has started. It will be emailed to %{email} when complete.') % { email: current_user.notification_email } + redirect_to(index_path, notice: message) end def import_csv diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb index 04da54a6bb6..4d33a874a81 100644 --- a/app/graphql/resolvers/issues_resolver.rb +++ b/app/graphql/resolvers/issues_resolver.rb @@ -52,6 +52,8 @@ module Resolvers type Types::IssueType, null: true + NON_STABLE_CURSOR_SORTS = %i[priority_asc priority_desc].freeze + def resolve(**args) # The project could have been loaded in batch by `BatchLoader`. # At this point we need the `id` of the project to query for issues, so @@ -70,7 +72,15 @@ module Resolvers args[:iids] ||= [args[:iid]].compact args[:attempt_project_search_optimizations] = args[:search].present? - IssuesFinder.new(context[:current_user], args).execute + issues = IssuesFinder.new(context[:current_user], args).execute + + if non_stable_cursor_sort?(args[:sort]) + # Certain complex sorts are not supported by the stable cursor pagination yet. + # In these cases, we use offset pagination, so we return the correct connection. + Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection.new(issues) + else + issues + end end def self.resolver_complexity(args, child_complexity:) @@ -79,5 +89,9 @@ module Resolvers complexity end + + def non_stable_cursor_sort?(sort) + NON_STABLE_CURSOR_SORTS.include?(sort) + end end end diff --git a/app/graphql/types/issuable_sort_enum.rb b/app/graphql/types/issuable_sort_enum.rb index 9fb1249d582..bade05b2744 100644 --- a/app/graphql/types/issuable_sort_enum.rb +++ b/app/graphql/types/issuable_sort_enum.rb @@ -4,5 +4,8 @@ module Types class IssuableSortEnum < SortEnum graphql_name 'IssuableSort' description 'Values for sorting issuables' + + value 'PRIORITY_ASC', 'Priority by ascending order', value: :priority_asc + value 'PRIORITY_DESC', 'Priority by descending order', value: :priority_desc end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 046f131b041..a2a30c61bfb 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -267,8 +267,16 @@ class CommitStatus < ApplicationRecord end end + def recoverable? + failed? && !unrecoverable_failure? + end + private + def unrecoverable_failure? + script_failure? || missing_dependency_failure? || archived_failure? || scheduler_failure? || data_integrity_failure? + end + def schedule_stage_and_pipeline_update if Feature.enabled?(:ci_atomic_processing, project) # Atomic Processing requires only single Worker diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb index 23e688e562e..52811e152a6 100644 --- a/app/presenters/commit_status_presenter.rb +++ b/app/presenters/commit_status_presenter.rb @@ -33,14 +33,6 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated def callout_failure_message self.class.callout_failure_messages.fetch(failure_reason.to_sym) end - - def recoverable? - failed? && !unrecoverable? - end - - def unrecoverable? - script_failure? || missing_dependency_failure? || archived_failure? || scheduler_failure? || data_integrity_failure? - end end CommitStatusPresenter.prepend_if_ee('::EE::CommitStatusPresenter') diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 3aa056fad7b..9df6a7dfc07 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -222,12 +222,14 @@ %span = _('Metrics') - - if project_nav_tab?(:alert_management) - = nav_link(controller: :alert_management) do - = link_to project_alert_management_index_path(@project), title: _('Alerts'), class: 'shortcuts-tracking qa-operations-tracking-link' do - %span - = _('Alerts') + - if Feature.enabled?(:alert_management_minimal, @project) + - if project_nav_tab?(:alert_management) + = nav_link(controller: :alert_management) do + = link_to project_alert_management_index_path(@project), title: _('Alerts'), class: 'shortcuts-tracking qa-operations-tracking-link' do + %span + = _('Alerts') + - if project_nav_tab? :environments = render_if_exists "layouts/nav/sidebar/tracing_link" = nav_link(controller: :environments, action: [:index, :folder, :show, :new, :edit, :create, :update, :stop, :terminal]) do diff --git a/app/views/notify/issues_csv_email.html.haml b/app/views/notify/issues_csv_email.html.haml index b777ca1e57d..77502a45f02 100644 --- a/app/views/notify/issues_csv_email.html.haml +++ b/app/views/notify/issues_csv_email.html.haml @@ -1,9 +1,6 @@ --# haml-lint:disable NoPlainNodes %p{ style: 'font-size:18px; text-align:center; line-height:30px;' } - Your CSV export of #{ pluralize(@written_count, 'issue') } from project - %a{ href: project_url(@project), style: "color:#3777b0; text-decoration:none; display:block;" } - = @project.full_name - has been added to this email as an attachment. + - project_link = link_to(@project.full_name, project_url(@project), style: "color:#3777b0; text-decoration:none; display:block;") + = _('Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment.').html_safe % { issues_count: pluralize(@written_count, 'issue'), project_link: project_link } - if @truncated %p - This attachment has been truncated to avoid exceeding a maximum allowed attachment size of 15MB. #{ @written_count } of #{ @issues_count } issues have been included. Consider re-exporting with a narrower selection of issues. + = _('This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues.') % { written_count: @written_count, issues_count: @issues_count } diff --git a/app/views/notify/issues_csv_email.text.erb b/app/views/notify/issues_csv_email.text.erb index 5d4128e3ae9..a1d2a4691bc 100644 --- a/app/views/notify/issues_csv_email.text.erb +++ b/app/views/notify/issues_csv_email.text.erb @@ -1,5 +1,5 @@ -Your CSV export of <%= pluralize(@written_count, 'issue') %> from project <%= @project.full_name %> (<%= project_url(@project) %>) has been added to this email as an attachment. +<%= _('Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment.') % { written_count: pluralize(@written_count, 'issue'), project_name: @project.full_name, project_url: project_url(@project) } %> <% if @truncated %> -This attachment has been truncated to avoid exceeding a maximum allowed attachment size of 15MB. <%= @written_count %> of <%= @issues_count %> issues have been included. Consider re-exporting with a narrower selection of issues. + <%= _('This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues.') % { written_count: @written_count, issues_count: @issues_count} %> <% end %> diff --git a/app/views/projects/issues/export_csv/_modal.html.haml b/app/views/projects/issues/export_csv/_modal.html.haml index af3a087ca59..9fdeb901b56 100644 --- a/app/views/projects/issues/export_csv/_modal.html.haml +++ b/app/views/projects/issues/export_csv/_modal.html.haml @@ -1,4 +1,3 @@ --# haml-lint:disable NoPlainNodes - if current_user .issues-export-modal.modal .modal-dialog diff --git a/app/views/shared/members/_badge.html.haml b/app/views/shared/members/_badge.html.haml new file mode 100644 index 00000000000..e304207f3e9 --- /dev/null +++ b/app/views/shared/members/_badge.html.haml @@ -0,0 +1,4 @@ +- type ||= 'info' + +%span.px-1.py-1 + %span{ class: "badge badge-#{type}" }= yield diff --git a/app/views/shared/members/_blocked_badge.html.haml b/app/views/shared/members/_blocked_badge.html.haml new file mode 100644 index 00000000000..95335ebe74d --- /dev/null +++ b/app/views/shared/members/_blocked_badge.html.haml @@ -0,0 +1,3 @@ +- if user.blocked? + = render 'shared/members/badge', type: 'danger' do + = _("Blocked") diff --git a/app/views/shared/members/_its_you_badge.html.haml b/app/views/shared/members/_its_you_badge.html.haml new file mode 100644 index 00000000000..b53ffd8032d --- /dev/null +++ b/app/views/shared/members/_its_you_badge.html.haml @@ -0,0 +1,3 @@ +- if user == current_user + = render 'shared/members/badge', type: 'success' do + = _("It's you") diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index d74030c566f..31eba9d71ee 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -13,24 +13,23 @@ - if user = image_tag avatar_icon_for_user(user, 40), class: "avatar s40 flex-shrink-0 flex-grow-0", alt: '' .user-info - = link_to user.name, user_path(user), class: 'member js-user-link', data: { user_id: user.id } - = user_status(user) - %span.cgray= user.to_reference + %span.mr-1 + = link_to user.name, user_path(user), class: 'member js-user-link', data: { user_id: user.id } + = user_status(user) + %span.cgray= user.to_reference - = render_if_exists 'shared/members/ee/sso_badge', member: member + .mx-n1.d-inline-flex.flex-wrap + = render_if_exists 'shared/members/ee/sso_badge', member: member - - if user == current_user - %span.badge.badge-success.prepend-left-5= _("It's you") + = render_if_exists 'shared/members/ee/gma_badge', member: member - = render_if_exists 'shared/members/ee/license_badge', user: user, group: @group + = render 'shared/members/its_you_badge', user: user, current_user: current_user - - if user.blocked? - %label.badge.badge-danger - %strong= _("Blocked") + = render_if_exists 'shared/members/ee/license_badge', user: user, group: @group - - if user.two_factor_enabled? - %label.badge.badge-info - = _("2FA") + = render 'shared/members/blocked_badge', user: user + + = render 'shared/members/two_factor_auth_badge', user: user - if source.instance_of?(Group) && source != @group · diff --git a/app/views/shared/members/_two_factor_auth_badge.html.haml b/app/views/shared/members/_two_factor_auth_badge.html.haml new file mode 100644 index 00000000000..34850c135d6 --- /dev/null +++ b/app/views/shared/members/_two_factor_auth_badge.html.haml @@ -0,0 +1,3 @@ +- if user.two_factor_enabled? + = render 'shared/members/badge', type: 'info' do + = _("2FA") diff --git a/changelogs/unreleased/29713-graphql-add-issue-priority-sort.yml b/changelogs/unreleased/29713-graphql-add-issue-priority-sort.yml new file mode 100644 index 00000000000..12661ae69ad --- /dev/null +++ b/changelogs/unreleased/29713-graphql-add-issue-priority-sort.yml @@ -0,0 +1,5 @@ +--- +title: Graphql query for issues can now be sorted by priority +merge_request: 18901 +author: +type: added diff --git a/changelogs/unreleased/38096-resource-state-events-pd.yml b/changelogs/unreleased/38096-resource-state-events-pd.yml new file mode 100644 index 00000000000..a761945d994 --- /dev/null +++ b/changelogs/unreleased/38096-resource-state-events-pd.yml @@ -0,0 +1,5 @@ +--- +title: Add resource_state_events table +merge_request: 28926 +author: +type: added diff --git a/changelogs/unreleased/tr-alert-management-feature-flag.yml b/changelogs/unreleased/tr-alert-management-feature-flag.yml new file mode 100644 index 00000000000..6ae9b766d13 --- /dev/null +++ b/changelogs/unreleased/tr-alert-management-feature-flag.yml @@ -0,0 +1,5 @@ +--- +title: Move alert management behind a feature flag +merge_request: 30133 +author: +type: fixed diff --git a/config/routes/project.rb b/config/routes/project.rb index 67afbe5e7f5..bad1c57a6d4 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -316,7 +316,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do # All new routes should go under /-/ scope. # Look for scope '-' at the top of the file. - # rubocop: disable Cop/PutProjectRoutesUnderScope # # Templates @@ -332,8 +331,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do defaults: { format: 'json' }, constraints: { template_type: %r{issue|merge_request}, format: 'json' } - resource :pages, only: [:show, :update, :destroy] do - resources :domains, except: :index, controller: 'pages_domains', constraints: { id: %r{[^/]+} } do + resource :pages, only: [:show, :update, :destroy] do # rubocop: disable Cop/PutProjectRoutesUnderScope + resources :domains, except: :index, controller: 'pages_domains', constraints: { id: %r{[^/]+} } do # rubocop: disable Cop/PutProjectRoutesUnderScope member do post :verify post :retry_auto_ssl @@ -342,7 +341,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end end - resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do + resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do # rubocop: disable Cop/PutProjectRoutesUnderScope member do get :raw post :mark_as_spam @@ -350,14 +349,14 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end namespace :prometheus do - resources :alerts, constraints: { id: /\d+/ }, only: [:index, :create, :show, :update, :destroy] do + resources :alerts, constraints: { id: /\d+/ }, only: [:index, :create, :show, :update, :destroy] do # rubocop: disable Cop/PutProjectRoutesUnderScope post :notify, on: :collection member do get :metrics_dashboard end end - resources :metrics, constraints: { id: %r{[^\/]+} }, only: [:index, :new, :create, :edit, :update, :destroy] do + resources :metrics, constraints: { id: %r{[^\/]+} }, only: [:index, :new, :create, :edit, :update, :destroy] do # rubocop: disable Cop/PutProjectRoutesUnderScope get :active_common, on: :collection post :validate_query, on: :collection end @@ -378,28 +377,28 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do draw :legacy_builds - resources :hooks, only: [:index, :create, :edit, :update, :destroy], constraints: { id: /\d+/ } do + resources :hooks, only: [:index, :create, :edit, :update, :destroy], constraints: { id: /\d+/ } do # rubocop: disable Cop/PutProjectRoutesUnderScope member do post :test end - resources :hook_logs, only: [:show] do + resources :hook_logs, only: [:show] do # rubocop: disable Cop/PutProjectRoutesUnderScope member do post :retry end end end - resources :container_registry, only: [:index, :destroy, :show], + resources :container_registry, only: [:index, :destroy, :show], # rubocop: disable Cop/PutProjectRoutesUnderScope controller: 'registry/repositories' namespace :registry do - resources :repository, only: [] do + resources :repository, only: [] do # rubocop: disable Cop/PutProjectRoutesUnderScope # We default to JSON format in the controller to avoid ambiguity. # `latest.json` could either be a request for a tag named `latest` # in JSON format, or a request for tag named `latest.json`. scope format: false do - resources :tags, only: [:index, :destroy], + resources :tags, only: [:index, :destroy], # rubocop: disable Cop/PutProjectRoutesUnderScope constraints: { id: Gitlab::Regex.container_registry_tag_regex } do collection do delete :bulk_destroy @@ -415,7 +414,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do draw :issues end - resources :notes, only: [:create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do + resources :notes, only: [:create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do # rubocop: disable Cop/PutProjectRoutesUnderScope member do delete :delete_attachment post :resolve @@ -425,16 +424,16 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do get 'noteable/:target_type/:target_id/notes' => 'notes#index', as: 'noteable_notes' - resources :todos, only: [:create] + resources :todos, only: [:create] # rubocop: disable Cop/PutProjectRoutesUnderScope - resources :uploads, only: [:create] do + resources :uploads, only: [:create] do # rubocop: disable Cop/PutProjectRoutesUnderScope collection do get ":secret/:filename", action: :show, as: :show, constraints: { filename: %r{[^/]+} }, format: false, defaults: { format: nil } post :authorize end end - resources :runners, only: [:index, :edit, :update, :destroy, :show] do + resources :runners, only: [:index, :edit, :update, :destroy, :show] do # rubocop: disable Cop/PutProjectRoutesUnderScope member do post :resume post :pause @@ -446,8 +445,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end end - resources :runner_projects, only: [:create, :destroy] - resources :badges, only: [:index] do + resources :runner_projects, only: [:create, :destroy] # rubocop: disable Cop/PutProjectRoutesUnderScope + resources :badges, only: [:index] do # rubocop: disable Cop/PutProjectRoutesUnderScope collection do scope '*ref', constraints: { ref: Gitlab::PathRegex.git_reference_regex } do constraints format: /svg/ do @@ -470,7 +469,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do # All new routes should go under /-/ scope. # Look for scope '-' at the top of the file. - # rubocop: enable Cop/PutProjectRoutesUnderScope # Legacy routes. # Introduced in 12.0. diff --git a/db/migrate/20200406132529_add_resource_state_events_table.rb b/db/migrate/20200406132529_add_resource_state_events_table.rb new file mode 100644 index 00000000000..ce241dff4dd --- /dev/null +++ b/db/migrate/20200406132529_add_resource_state_events_table.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class AddResourceStateEventsTable < ActiveRecord::Migration[6.0] + DOWNTIME = false + + def change + create_table :resource_state_events, id: :bigserial do |t| + t.bigint :user_id, null: false + t.bigint :issue_id, null: true + t.bigint :merge_request_id, null: true + + t.datetime_with_timezone :created_at, null: false + t.integer :state, limit: 2, null: false + + t.index [:issue_id, :created_at], name: 'index_resource_state_events_on_issue_id_and_created_at' + t.index [:user_id], name: 'index_resource_state_events_on_user_id' + t.index [:merge_request_id], name: 'index_resource_state_events_on_merge_request_id' + end + end +end diff --git a/db/migrate/20200423075720_add_user_id_foreign_key_to_resource_state_events.rb b/db/migrate/20200423075720_add_user_id_foreign_key_to_resource_state_events.rb new file mode 100644 index 00000000000..702347e5d43 --- /dev/null +++ b/db/migrate/20200423075720_add_user_id_foreign_key_to_resource_state_events.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddUserIdForeignKeyToResourceStateEvents < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + with_lock_retries do + add_foreign_key :resource_state_events, :users, column: :user_id, on_delete: :nullify # rubocop:disable Migration/AddConcurrentForeignKey + end + end + + def down + with_lock_retries do + remove_foreign_key :resource_state_events, column: :user_id + end + end +end diff --git a/db/migrate/20200423080334_add_issue_id_foreign_key_to_resource_state_events.rb b/db/migrate/20200423080334_add_issue_id_foreign_key_to_resource_state_events.rb new file mode 100644 index 00000000000..660c51eb3a6 --- /dev/null +++ b/db/migrate/20200423080334_add_issue_id_foreign_key_to_resource_state_events.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddIssueIdForeignKeyToResourceStateEvents < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + with_lock_retries do + add_foreign_key :resource_state_events, :issues, column: :issue_id, on_delete: :cascade # rubocop:disable Migration/AddConcurrentForeignKey + end + end + + def down + with_lock_retries do + remove_foreign_key :resource_state_events, column: :issue_id + end + end +end diff --git a/db/migrate/20200423080607_add_merge_request_id_foreign_key_to_resource_state_events.rb b/db/migrate/20200423080607_add_merge_request_id_foreign_key_to_resource_state_events.rb new file mode 100644 index 00000000000..4f0a689a992 --- /dev/null +++ b/db/migrate/20200423080607_add_merge_request_id_foreign_key_to_resource_state_events.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddMergeRequestIdForeignKeyToResourceStateEvents < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + with_lock_retries do + add_foreign_key :resource_state_events, :merge_requests, column: :merge_request_id, on_delete: :cascade # rubocop:disable Migration/AddConcurrentForeignKey + end + end + + def down + with_lock_retries do + remove_foreign_key :resource_state_events, column: :merge_request_id + end + end +end diff --git a/db/migrate/20200423081409_add_constraint_to_resource_state_events_must_belong_to_issue_or_merge_request.rb b/db/migrate/20200423081409_add_constraint_to_resource_state_events_must_belong_to_issue_or_merge_request.rb new file mode 100644 index 00000000000..57df1045e2c --- /dev/null +++ b/db/migrate/20200423081409_add_constraint_to_resource_state_events_must_belong_to_issue_or_merge_request.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddConstraintToResourceStateEventsMustBelongToIssueOrMergeRequest < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + CONSTRAINT_NAME = 'resource_state_events_must_belong_to_issue_or_merge_request' + + def up + add_check_constraint :resource_state_events, '(issue_id != NULL AND merge_request_id IS NULL) OR (merge_request_id != NULL AND issue_id IS NULL)', CONSTRAINT_NAME + end + + def down + remove_check_constraint :resource_state_events, CONSTRAINT_NAME + end +end diff --git a/db/structure.sql b/db/structure.sql index 759465c6845..b3e605b97cf 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -5643,6 +5643,25 @@ CREATE SEQUENCE public.resource_milestone_events_id_seq ALTER SEQUENCE public.resource_milestone_events_id_seq OWNED BY public.resource_milestone_events.id; +CREATE TABLE public.resource_state_events ( + id bigint NOT NULL, + user_id bigint NOT NULL, + issue_id bigint, + merge_request_id bigint, + created_at timestamp with time zone NOT NULL, + state smallint NOT NULL, + CONSTRAINT resource_state_events_must_belong_to_issue_or_merge_request CHECK ((((issue_id <> NULL::bigint) AND (merge_request_id IS NULL)) OR ((merge_request_id <> NULL::bigint) AND (issue_id IS NULL)))) +); + +CREATE SEQUENCE public.resource_state_events_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE public.resource_state_events_id_seq OWNED BY public.resource_state_events.id; + CREATE TABLE public.resource_weight_events ( id bigint NOT NULL, user_id bigint NOT NULL, @@ -7530,6 +7549,8 @@ ALTER TABLE ONLY public.resource_label_events ALTER COLUMN id SET DEFAULT nextva ALTER TABLE ONLY public.resource_milestone_events ALTER COLUMN id SET DEFAULT nextval('public.resource_milestone_events_id_seq'::regclass); +ALTER TABLE ONLY public.resource_state_events ALTER COLUMN id SET DEFAULT nextval('public.resource_state_events_id_seq'::regclass); + ALTER TABLE ONLY public.resource_weight_events ALTER COLUMN id SET DEFAULT nextval('public.resource_weight_events_id_seq'::regclass); ALTER TABLE ONLY public.reviews ALTER COLUMN id SET DEFAULT nextval('public.reviews_id_seq'::regclass); @@ -8422,6 +8443,9 @@ ALTER TABLE ONLY public.resource_label_events ALTER TABLE ONLY public.resource_milestone_events ADD CONSTRAINT resource_milestone_events_pkey PRIMARY KEY (id); +ALTER TABLE ONLY public.resource_state_events + ADD CONSTRAINT resource_state_events_pkey PRIMARY KEY (id); + ALTER TABLE ONLY public.resource_weight_events ADD CONSTRAINT resource_weight_events_pkey PRIMARY KEY (id); @@ -10197,6 +10221,12 @@ CREATE INDEX index_resource_milestone_events_on_milestone_id ON public.resource_ CREATE INDEX index_resource_milestone_events_on_user_id ON public.resource_milestone_events USING btree (user_id); +CREATE INDEX index_resource_state_events_on_issue_id_and_created_at ON public.resource_state_events USING btree (issue_id, created_at); + +CREATE INDEX index_resource_state_events_on_merge_request_id ON public.resource_state_events USING btree (merge_request_id); + +CREATE INDEX index_resource_state_events_on_user_id ON public.resource_state_events USING btree (user_id); + CREATE INDEX index_resource_weight_events_on_issue_id_and_created_at ON public.resource_weight_events USING btree (issue_id, created_at); CREATE INDEX index_resource_weight_events_on_issue_id_and_weight ON public.resource_weight_events USING btree (issue_id, weight); @@ -11329,6 +11359,9 @@ ALTER TABLE ONLY public.lfs_file_locks ALTER TABLE ONLY public.project_alerting_settings ADD CONSTRAINT fk_rails_27a84b407d FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.resource_state_events + ADD CONSTRAINT fk_rails_29af06892a FOREIGN KEY (issue_id) REFERENCES public.issues(id) ON DELETE CASCADE; + ALTER TABLE ONLY public.reviews ADD CONSTRAINT fk_rails_29e6f859c4 FOREIGN KEY (author_id) REFERENCES public.users(id) ON DELETE SET NULL; @@ -11347,6 +11380,9 @@ ALTER TABLE ONLY public.protected_branch_unprotect_access_levels ALTER TABLE ONLY public.saml_providers ADD CONSTRAINT fk_rails_306d459be7 FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.resource_state_events + ADD CONSTRAINT fk_rails_3112bba7dc FOREIGN KEY (merge_request_id) REFERENCES public.merge_requests(id) ON DELETE CASCADE; + ALTER TABLE ONLY public.merge_request_diff_commits ADD CONSTRAINT fk_rails_316aaceda3 FOREIGN KEY (merge_request_diff_id) REFERENCES public.merge_request_diffs(id) ON DELETE CASCADE; @@ -12148,6 +12184,9 @@ ALTER TABLE ONLY public.insights ALTER TABLE ONLY public.board_group_recent_visits ADD CONSTRAINT fk_rails_f410736518 FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.resource_state_events + ADD CONSTRAINT fk_rails_f5827a7ccd FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE SET NULL; + ALTER TABLE ONLY public.design_user_mentions ADD CONSTRAINT fk_rails_f7075a53c1 FOREIGN KEY (design_id) REFERENCES public.design_management_designs(id) ON DELETE CASCADE; @@ -13281,6 +13320,7 @@ COPY "schema_migrations" (version) FROM STDIN; 20200406100909 20200406102111 20200406102120 +20200406132529 20200406135648 20200406141452 20200406192059 @@ -13326,5 +13366,9 @@ COPY "schema_migrations" (version) FROM STDIN; 20200416120354 20200417044453 20200421233150 +20200423075720 +20200423080334 +20200423080607 +20200423081409 \. diff --git a/doc/administration/geo/replication/index.md b/doc/administration/geo/replication/index.md index 7c661abef9a..5a9befc2232 100644 --- a/doc/administration/geo/replication/index.md +++ b/doc/administration/geo/replication/index.md @@ -245,7 +245,7 @@ This list of limitations only reflects the latest version of GitLab. If you are - Pushing directly to a **secondary** node redirects (for HTTP) or proxies (for SSH) the request to the **primary** node instead of [handling it directly](https://gitlab.com/gitlab-org/gitlab/issues/1381), except when using Git over HTTP with credentials embedded within the URI. For example, `https://user:password@secondary.tld`. - Cloning, pulling, or pushing repositories that exist on the **primary** node but not on the **secondary** nodes where [selective synchronization](configuration.md#selective-synchronization) does not include the project is not supported over SSH [but support is planned](https://gitlab.com/groups/gitlab-org/-/epics/2562). HTTP(S) is supported. -- The **primary** node has to be online for OAuth login to happen. Existing sessions and Git are not affected. +- The **primary** node has to be online for OAuth login to happen. Existing sessions and Git are not affected. Support for the **secondary** node to use an OAuth provider independent from the primary is [being planned](https://gitlab.com/gitlab-org/gitlab/issues/208465). - The installation takes multiple manual steps that together can take about an hour depending on circumstances. We are working on improving this experience. See [Omnibus GitLab issue #2978](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2978) for details. - Real-time updates of issues/merge requests (for example, via long polling) doesn't work on the **secondary** node. - [Selective synchronization](configuration.md#selective-synchronization) applies only to files and repositories. Other datasets are replicated to the **secondary** node in full, making it inappropriate for use as an access control mechanism. diff --git a/doc/administration/high_availability/consul.md b/doc/administration/high_availability/consul.md index 6762a81f671..acd1906eb7b 100644 --- a/doc/administration/high_availability/consul.md +++ b/doc/administration/high_availability/consul.md @@ -4,7 +4,10 @@ type: reference # Working with the bundled Consul service **(PREMIUM ONLY)** -As part of its High Availability stack, GitLab Premium includes a bundled version of [Consul](https://www.consul.io/) that can be managed through `/etc/gitlab/gitlab.rb`. +As part of its High Availability stack, GitLab Premium includes a bundled version of [Consul](https://www.consul.io/) that can be managed through `/etc/gitlab/gitlab.rb`. Consul is a service networking solution. When it comes to [GitLab Architecture](../../development/architecture.md), Consul utilization is supported for configuring: + +1. [Monitoring in Scaled and Highly Available environments](monitoring_node.md) +1. [PostgreSQL High Availability with Omnibus](database.md#high-availability-with-gitlab-omnibus-premium-only) A Consul cluster consists of multiple server agents, as well as client agents that run on other nodes which need to talk to the Consul cluster. diff --git a/doc/api/epics.md b/doc/api/epics.md index bf6a18fcedc..7cdf6c2729b 100644 --- a/doc/api/epics.md +++ b/doc/api/epics.md @@ -1,5 +1,8 @@ # Epics API **(PREMIUM)** +> - Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.2. +> - Single-level Epics [were moved](https://gitlab.com/gitlab-org/gitlab/issues/37081) to [GitLab Premium](https://about.gitlab.com/pricing/) in 12.8. + Every API call to epic must be authenticated. If a user is not a member of a group and the group is private, a `GET` request on that group will result to a `404` status code. diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 749461b5e9e..3522a5a50eb 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -4399,6 +4399,16 @@ enum IssueSort { DUE_DATE_DESC """ + Priority by ascending order + """ + PRIORITY_ASC + + """ + Priority by descending order + """ + PRIORITY_DESC + + """ Relative position by ascending order """ RELATIVE_POSITION_ASC diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 70f9a53244e..4c8fb099faa 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -12547,6 +12547,18 @@ "deprecationReason": null }, { + "name": "PRIORITY_ASC", + "description": "Priority by ascending order", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRIORITY_DESC", + "description": "Priority by descending order", + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "DUE_DATE_ASC", "description": "Due date by ascending order", "isDeprecated": false, diff --git a/doc/development/go_guide/index.md b/doc/development/go_guide/index.md index c1dfb220df8..48135c14b9b 100644 --- a/doc/development/go_guide/index.md +++ b/doc/development/go_guide/index.md @@ -70,8 +70,14 @@ projects: - Avoid global variables, even in packages. By doing so you will introduce side effects if the package is included multiple times. -- Use `go fmt` before committing ([Gofmt](https://golang.org/cmd/gofmt/) is a - tool that automatically formats Go source code). +- Use `goimports` before committing. + [goimports](https://godoc.org/golang.org/x/tools/cmd/goimports) + is a tool that automatically formats Go source code using + [Gofmt](https://golang.org/cmd/gofmt/), in addition to formatting import lines, + adding missing ones and removing unreferenced ones. + + Most editors/IDEs will allow you to run commands before/after saving a file, you can set it + up to run `goimports` so that it's applied to every file when saving. - Place private methods below the first caller method in the source file. ### Automatic linting diff --git a/doc/development/permissions.md b/doc/development/permissions.md index bca137337fc..0772389bf9e 100644 --- a/doc/development/permissions.md +++ b/doc/development/permissions.md @@ -9,9 +9,9 @@ anything that deals with permissions, all of them should be considered. Groups and projects can have the following visibility levels: -- public (20) - an entity is visible to everyone -- internal (10) - an entity is visible to logged in users -- private (0) - an entity is visible only to the approved members of the entity +- public (`20`) - an entity is visible to everyone +- internal (`10`) - an entity is visible to logged in users +- private (`0`) - an entity is visible only to the approved members of the entity The visibility level of a group can be changed only if all subgroups and subprojects have the same or lower visibility level. (e.g., a group can be set diff --git a/doc/user/group/epics/index.md b/doc/user/group/epics/index.md index 024f346ad47..d0962dbc65f 100644 --- a/doc/user/group/epics/index.md +++ b/doc/user/group/epics/index.md @@ -5,7 +5,7 @@ type: reference, howto # Epics **(PREMIUM)** > - Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.2. -> - In [GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/issues/37081), single-level Epics were moved to the Premium tier. +> - Single-level Epics [were moved](https://gitlab.com/gitlab-org/gitlab/issues/37081) to [GitLab Premium](https://about.gitlab.com/pricing/) in 12.8. Epics let you manage your portfolio of projects more efficiently and with less effort by tracking groups of issues that share a theme, across projects and diff --git a/doc/user/project/code_owners.md b/doc/user/project/code_owners.md index 49083be77dc..c9129ef3269 100644 --- a/doc/user/project/code_owners.md +++ b/doc/user/project/code_owners.md @@ -9,6 +9,33 @@ in [GitLab Starter](https://about.gitlab.com/pricing/) 11.3. > - [Support for group namespaces](https://gitlab.com/gitlab-org/gitlab-foss/issues/53182) added in GitLab Starter 12.1. > - Code Owners for Merge Request approvals was [introduced](https://gitlab.com/gitlab-org/gitlab/issues/4418) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.9. +## Introduction + +When contributing to a project, it can often be difficult +to find out who should review or approve merge requests. +Additionally, if you have a question over a specific file or +code block, it may be difficult to know who to find the answer from. + +GitLab Code Owners is a feature to define who owns specific +files or paths in a repository, allowing other users to understand +who is responsible for each file or path. + +## Why is this useful? + +Code Owners allows for a version controlled single source of +truth file outlining the exact GitLab users or groups that +own certain files or paths in a repository. Code Owners can be +utilized in the merge request approval process which can streamline +the process of finding the right reviewers and approvers for a given +merge request. + +In larger organizations or popular open source projects, Code Owners +can also be useful to understand who to contact if you have +a question that may not be related to code review or a merge request +approval. + +## How to set up Code Owners + You can use a `CODEOWNERS` file to specify users or [shared groups](members/share_project_with_groups.md) that are responsible for certain files in a repository. @@ -41,7 +68,7 @@ The user that would show for `README.md` would be `@user2`. ## Approvals by Code Owners Once you've set Code Owners to a project, you can configure it to -receive approvals: +be used for merge request approvals: - As [merge request eligible approvers](merge_requests/merge_request_approvals.md#code-owners-as-eligible-approvers). - As required approvers for [protected branches](protected_branches.md#protected-branches-approval-by-code-owners-premium). **(PREMIUM)** diff --git a/doc/user/project/merge_requests/code_quality.md b/doc/user/project/merge_requests/code_quality.md index a7e712a4c0a..887ed3e0cb9 100644 --- a/doc/user/project/merge_requests/code_quality.md +++ b/doc/user/project/merge_requests/code_quality.md @@ -27,7 +27,19 @@ in the merge request widget area:  -For more information, see the Code Climate list of [Supported Languages for Maintainability](https://docs.codeclimate.com/docs/supported-languages-for-maintainability). +Watch a quick walkthrough of Code Quality in action: + +<div class="video-fallback"> + See the video: <a href="https://www.youtube.com/watch?v=B32LxtJKo9M">Video title</a>. +</div> +<figure class="video-container"> + <iframe src="https://www.youtube.com/embed/B32LxtJKo9M" frameborder="0" allowfullscreen="true"> </iframe> +</figure> + +NOTE: **Note:** +For one customer, the auditor found that having Code Quality, SAST, and Container Scanning all automated in GitLab CI/CD was almost better than a manual review! [Read more](https://about.gitlab.com/customers/bi_worldwide/). + +See also the Code Climate list of [Supported Languages for Maintainability](https://docs.codeclimate.com/docs/supported-languages-for-maintainability). ## Use cases diff --git a/doc/user/project/merge_requests/img/code_quality.png b/doc/user/project/merge_requests/img/code_quality.png Binary files differindex a20f6476fb8..3c6c92baad2 100644 --- a/doc/user/project/merge_requests/img/code_quality.png +++ b/doc/user/project/merge_requests/img/code_quality.png diff --git a/doc/user/project/repository/file_finder.md b/doc/user/project/repository/file_finder.md index 1361053dac3..3a923ff32a8 100644 --- a/doc/user/project/repository/file_finder.md +++ b/doc/user/project/repository/file_finder.md @@ -12,7 +12,7 @@ GitLab UI. You can find the **Find File** button when in the **Files** section of a project. - + For those who prefer to keep their fingers on the keyboard, there is a [shortcut button](../../shortcuts.md) as well, which you can invoke from _anywhere_ @@ -32,11 +32,11 @@ The File finder feature is powered by the [Fuzzy filter](https://github.com/jean It implements a fuzzy search with highlight, and tries to provide intuitive results by recognizing patterns that people use while searching. -For example, consider the [GitLab CE repository](https://gitlab.com/gitlab-org/gitlab-foss/tree/master) and that we want to open +For example, consider the [GitLab FOSS repository](https://gitlab.com/gitlab-org/gitlab-foss/tree/master) and that we want to open the `app/controllers/admin/deploy_keys_controller.rb` file. Using fuzzy search, we start by typing letters that get us closer to the file. **Tip:** To narrow down your search, include `/` in your search terms. - + diff --git a/doc/user/project/repository/img/file_finder_find_button.png b/doc/user/project/repository/img/file_finder_find_button.png Binary files differdeleted file mode 100644 index 0c2d7d7bc73..00000000000 --- a/doc/user/project/repository/img/file_finder_find_button.png +++ /dev/null diff --git a/doc/user/project/repository/img/file_finder_find_button_v12_10.png b/doc/user/project/repository/img/file_finder_find_button_v12_10.png Binary files differnew file mode 100644 index 00000000000..e93db946005 --- /dev/null +++ b/doc/user/project/repository/img/file_finder_find_button_v12_10.png diff --git a/doc/user/project/repository/img/file_finder_find_file.png b/doc/user/project/repository/img/file_finder_find_file.png Binary files differdeleted file mode 100644 index c2212c7cd9e..00000000000 --- a/doc/user/project/repository/img/file_finder_find_file.png +++ /dev/null diff --git a/doc/user/project/repository/img/file_finder_find_file_v12_10.png b/doc/user/project/repository/img/file_finder_find_file_v12_10.png Binary files differnew file mode 100644 index 00000000000..1404ccc6d0b --- /dev/null +++ b/doc/user/project/repository/img/file_finder_find_file_v12_10.png diff --git a/doc/user/project/requirements/index.md b/doc/user/project/requirements/index.md index 8f4ec7bbbed..50343e52a68 100644 --- a/doc/user/project/requirements/index.md +++ b/doc/user/project/requirements/index.md @@ -10,6 +10,9 @@ Requirements allow you to create criteria to check your products against. They can be based on users, stakeholders, system, software, or anything else you find important to capture. +<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> +For an overview, see [GitLab 12.10 Introduces Requirements Management](https://www.youtube.com/watch?v=uSS7oUNSEoU). +  ## Create a requirement diff --git a/locale/gitlab.pot b/locale/gitlab.pot index b235caaa6a2..610c504bfee 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -12521,6 +12521,9 @@ msgstr "" msgid "Manage your license" msgstr "" +msgid "Managed Account" +msgstr "" + msgid "Manifest" msgstr "" @@ -20981,6 +20984,9 @@ msgstr "" msgid "This application will be able to:" msgstr "" +msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues." +msgstr "" + msgid "This block is self-referential" msgstr "" @@ -24138,6 +24144,15 @@ msgstr "" msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features." msgstr "" +msgid "Your CSV export has started. It will be emailed to %{email} when complete." +msgstr "" + +msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment." +msgstr "" + +msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment." +msgstr "" + msgid "Your Commit Email will be used for web based operations, such as edits and merges." msgstr "" diff --git a/qa/spec/factory/resource/user_spec.rb b/qa/spec/factory/resource/user_spec.rb index 820c506b715..d59ee24c758 100644 --- a/qa/spec/factory/resource/user_spec.rb +++ b/qa/spec/factory/resource/user_spec.rb @@ -2,7 +2,7 @@ describe QA::Resource::User do describe "#fabricate_via_api!" do - Response = Struct.new(:code, :body) + response = Struct.new(:code, :body) it 'fetches an existing user' do existing_users = [ @@ -13,8 +13,8 @@ describe QA::Resource::User do web_url: '' } ] - users_response = Response.new('200', JSON.dump(existing_users)) - single_user_response = Response.new('200', JSON.dump(existing_users.first)) + users_response = response.new('200', JSON.dump(existing_users)) + single_user_response = response.new('200', JSON.dump(existing_users.first)) expect(subject).to receive(:api_get_from).with("/users?username=name").and_return(users_response) expect(subject).to receive(:api_get_from).with("/users/0").and_return(single_user_response) @@ -26,7 +26,7 @@ describe QA::Resource::User do end it 'tries to create a user if it does not exist' do - expect(subject).to receive(:api_get_from).with("/users?username=foo").and_return(Response.new('200', '[]')) + expect(subject).to receive(:api_get_from).with("/users?username=foo").and_return(response.new('200', '[]')) expect(subject).to receive(:api_post).and_return({ web_url: '' }) subject.username = 'foo' diff --git a/qa/spec/git/repository_spec.rb b/qa/spec/git/repository_spec.rb index 6cca9f55e11..0559fabbfd5 100644 --- a/qa/spec/git/repository_spec.rb +++ b/qa/spec/git/repository_spec.rb @@ -69,20 +69,20 @@ describe QA::Git::Repository do end describe '#fetch_supported_git_protocol' do - Result = Struct.new(:response) + result = Struct.new(:response) it "reports the detected version" do - expect(repository).to receive(:run).and_return(Result.new("packet: git< version 2")) + expect(repository).to receive(:run).and_return(result.new("packet: git< version 2")) expect(repository.fetch_supported_git_protocol).to eq('2') end it 'reports unknown if version is unknown' do - expect(repository).to receive(:run).and_return(Result.new("packet: git< version -1")) + expect(repository).to receive(:run).and_return(result.new("packet: git< version -1")) expect(repository.fetch_supported_git_protocol).to eq('unknown') end it 'reports unknown if content does not identify a version' do - expect(repository).to receive(:run).and_return(Result.new("foo")) + expect(repository).to receive(:run).and_return(result.new("foo")) expect(repository.fetch_supported_git_protocol).to eq('unknown') end end diff --git a/spec/controllers/projects/alert_management_controller_spec.rb b/spec/controllers/projects/alert_management_controller_spec.rb new file mode 100644 index 00000000000..5321632875e --- /dev/null +++ b/spec/controllers/projects/alert_management_controller_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::AlertManagementController do + let_it_be(:project) { create(:project) } + let_it_be(:role) { :reporter } + let_it_be(:user) { create(:user) } + + before do + project.add_role(user, role) + sign_in(user) + end + + describe 'GET #index' do + context 'when alert_management_minimal is enabled' do + before do + stub_feature_flags(alert_management_minimal: true) + end + + it 'shows the page' do + get :index, params: { namespace_id: project.namespace, project_id: project } + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when alert_management_minimal is disabled' do + before do + stub_feature_flags(alert_management_minimal: false) + end + + it 'shows 404' do + get :index, params: { namespace_id: project.namespace, project_id: project } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end +end diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb index 53e0a9e3724..3dd4bb31e94 100644 --- a/spec/graphql/resolvers/issues_resolver_spec.rb +++ b/spec/graphql/resolvers/issues_resolver_spec.rb @@ -125,12 +125,12 @@ describe Resolvers::IssuesResolver do end context 'when sorting by due date' do - let(:project) { create(:project) } + let_it_be(:project) { create(:project) } - let!(:due_issue1) { create(:issue, project: project, due_date: 3.days.from_now) } - let!(:due_issue2) { create(:issue, project: project, due_date: nil) } - let!(:due_issue3) { create(:issue, project: project, due_date: 2.days.ago) } - let!(:due_issue4) { create(:issue, project: project, due_date: nil) } + let_it_be(:due_issue1) { create(:issue, project: project, due_date: 3.days.from_now) } + let_it_be(:due_issue2) { create(:issue, project: project, due_date: nil) } + let_it_be(:due_issue3) { create(:issue, project: project, due_date: 2.days.ago) } + let_it_be(:due_issue4) { create(:issue, project: project, due_date: nil) } it 'sorts issues ascending' do expect(resolve_issues(sort: :due_date_asc)).to eq [due_issue3, due_issue1, due_issue4, due_issue2] @@ -142,17 +142,38 @@ describe Resolvers::IssuesResolver do end context 'when sorting by relative position' do - let(:project) { create(:project) } + let_it_be(:project) { create(:project) } - let!(:relative_issue1) { create(:issue, project: project, relative_position: 2000) } - let!(:relative_issue2) { create(:issue, project: project, relative_position: nil) } - let!(:relative_issue3) { create(:issue, project: project, relative_position: 1000) } - let!(:relative_issue4) { create(:issue, project: project, relative_position: nil) } + let_it_be(:relative_issue1) { create(:issue, project: project, relative_position: 2000) } + let_it_be(:relative_issue2) { create(:issue, project: project, relative_position: nil) } + let_it_be(:relative_issue3) { create(:issue, project: project, relative_position: 1000) } + let_it_be(:relative_issue4) { create(:issue, project: project, relative_position: nil) } it 'sorts issues ascending' do expect(resolve_issues(sort: :relative_position_asc)).to eq [relative_issue3, relative_issue1, relative_issue4, relative_issue2] end end + + context 'when sorting by priority' do + let_it_be(:project) { create(:project) } + + let_it_be(:early_milestone) { create(:milestone, project: project, due_date: 10.days.from_now) } + let_it_be(:late_milestone) { create(:milestone, project: project, due_date: 30.days.from_now) } + let_it_be(:label_1) { create(:label, project: project, priority: 1) } + let_it_be(:label_2) { create(:label, project: project, priority: 5) } + let_it_be(:issue1) { create(:issue, project: project, labels: [label_1], milestone: late_milestone) } + let_it_be(:issue2) { create(:issue, project: project, labels: [label_2]) } + let_it_be(:issue3) { create(:issue, project: project, milestone: early_milestone) } + let_it_be(:issue4) { create(:issue, project: project) } + + it 'sorts issues ascending' do + expect(resolve_issues(sort: :priority_asc).items).to eq([issue3, issue1, issue2, issue4]) + end + + it 'sorts issues descending' do + expect(resolve_issues(sort: :priority_desc).items).to eq([issue1, issue3, issue2, issue4]) + end + end end it 'returns issues user can see' do diff --git a/spec/graphql/types/issuable_sort_enum_spec.rb b/spec/graphql/types/issuable_sort_enum_spec.rb new file mode 100644 index 00000000000..feb318df66d --- /dev/null +++ b/spec/graphql/types/issuable_sort_enum_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Types::IssuableSortEnum do + it { expect(described_class.graphql_name).to eq('IssuableSort') } + + it 'exposes all the existing issuable sort values' do + expect(described_class.values.keys).to include(*%w[PRIORITY_ASC PRIORITY_DESC]) + end +end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 73b81b2225a..05d3329215a 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -751,4 +751,48 @@ describe CommitStatus do it { is_expected.to be_a(CommitStatusPresenter) } end + + describe '#recoverable?' do + using RSpec::Parameterized::TableSyntax + + let(:commit_status) { create(:commit_status, :pending) } + + subject(:recoverable?) { commit_status.recoverable? } + + context 'when commit status is failed' do + before do + commit_status.drop! + end + + where(:failure_reason, :recoverable) do + :script_failure | false + :missing_dependency_failure | false + :archived_failure | false + :scheduler_failure | false + :data_integrity_failure | false + :unknown_failure | true + :api_failure | true + :stuck_or_timeout_failure | true + :runner_system_failure | true + end + + with_them do + context "when failure reason is #{params[:failure_reason]}" do + before do + commit_status.update_attribute(:failure_reason, failure_reason) + end + + it { is_expected.to eq(recoverable) } + end + end + end + + context 'when commit status is not failed' do + before do + commit_status.success! + end + + it { is_expected.to eq(false) } + end + end end diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb index b6c47f40ceb..9cf6eb45c63 100644 --- a/spec/presenters/ci/build_presenter_spec.rb +++ b/spec/presenters/ci/build_presenter_spec.rb @@ -264,30 +264,4 @@ describe Ci::BuildPresenter do expect(description).to eq('There has been an API failure, please try again') end end - - describe '#recoverable?' do - let(:build) { create(:ci_build, :failed, :script_failure) } - - context 'when is a script or missing dependency failure' do - let(:failure_reasons) { %w(script_failure missing_dependency_failure archived_failure scheduler_failure data_integrity_failure) } - - it 'returns false' do - failure_reasons.each do |failure_reason| - build.update_attribute(:failure_reason, failure_reason) - expect(presenter.recoverable?).to be_falsy - end - end - end - - context 'when is any other failure type' do - let(:failure_reasons) { %w(unknown_failure api_failure stuck_or_timeout_failure runner_system_failure) } - - it 'returns true' do - failure_reasons.each do |failure_reason| - build.update_attribute(:failure_reason, failure_reason) - expect(presenter.recoverable?).to be_truthy - end - end - end - end end diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index 4ce7a3912a3..ae21951008c 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -45,8 +45,8 @@ describe 'getting an issue list for a project' do it 'includes discussion locked' do post_graphql(query, current_user: current_user) - expect(issues_data[0]['node']['discussionLocked']).to eq false - expect(issues_data[1]['node']['discussionLocked']).to eq true + expect(issues_data[0]['node']['discussionLocked']).to eq(false) + expect(issues_data[1]['node']['discussionLocked']).to eq(true) end context 'when limiting the number of results' do @@ -79,7 +79,7 @@ describe 'getting an issue list for a project' do post_graphql(query) - expect(issues_data).to eq [] + expect(issues_data).to eq([]) end end @@ -122,15 +122,15 @@ describe 'getting an issue list for a project' do let(:end_cursor) { graphql_data['project']['issues']['pageInfo']['endCursor'] } context 'when sorting by due date' do - let(:sort_project) { create(:project, :public) } + let_it_be(:sort_project) { create(:project, :public) } - let!(:due_issue1) { create(:issue, project: sort_project, due_date: 3.days.from_now) } - let!(:due_issue2) { create(:issue, project: sort_project, due_date: nil) } - let!(:due_issue3) { create(:issue, project: sort_project, due_date: 2.days.ago) } - let!(:due_issue4) { create(:issue, project: sort_project, due_date: nil) } - let!(:due_issue5) { create(:issue, project: sort_project, due_date: 1.day.ago) } + let_it_be(:due_issue1) { create(:issue, project: sort_project, due_date: 3.days.from_now) } + let_it_be(:due_issue2) { create(:issue, project: sort_project, due_date: nil) } + let_it_be(:due_issue3) { create(:issue, project: sort_project, due_date: 2.days.ago) } + let_it_be(:due_issue4) { create(:issue, project: sort_project, due_date: nil) } + let_it_be(:due_issue5) { create(:issue, project: sort_project, due_date: 1.day.ago) } - let(:params) { 'sort: DUE_DATE_ASC' } + let_it_be(:params) { 'sort: DUE_DATE_ASC' } def query(issue_params = params) graphql_query_for( @@ -160,20 +160,20 @@ describe 'getting an issue list for a project' do context 'when ascending' do it 'sorts issues' do - expect(grab_iids).to eq [due_issue3.iid, due_issue5.iid, due_issue1.iid, due_issue4.iid, due_issue2.iid] + expect(grab_iids).to eq([due_issue3.iid, due_issue5.iid, due_issue1.iid, due_issue4.iid, due_issue2.iid]) end context 'when paginating' do let(:params) { 'sort: DUE_DATE_ASC, first: 2' } it 'sorts issues' do - expect(grab_iids).to eq [due_issue3.iid, due_issue5.iid] + expect(grab_iids).to eq([due_issue3.iid, due_issue5.iid]) cursored_query = query("sort: DUE_DATE_ASC, after: \"#{end_cursor}\"") post_graphql(cursored_query, current_user: current_user) response_data = JSON.parse(response.body)['data']['project']['issues']['edges'] - expect(grab_iids(response_data)).to eq [due_issue1.iid, due_issue4.iid, due_issue2.iid] + expect(grab_iids(response_data)).to eq([due_issue1.iid, due_issue4.iid, due_issue2.iid]) end end end @@ -182,35 +182,35 @@ describe 'getting an issue list for a project' do let(:params) { 'sort: DUE_DATE_DESC' } it 'sorts issues' do - expect(grab_iids).to eq [due_issue1.iid, due_issue5.iid, due_issue3.iid, due_issue4.iid, due_issue2.iid] + expect(grab_iids).to eq([due_issue1.iid, due_issue5.iid, due_issue3.iid, due_issue4.iid, due_issue2.iid]) end context 'when paginating' do let(:params) { 'sort: DUE_DATE_DESC, first: 2' } it 'sorts issues' do - expect(grab_iids).to eq [due_issue1.iid, due_issue5.iid] + expect(grab_iids).to eq([due_issue1.iid, due_issue5.iid]) cursored_query = query("sort: DUE_DATE_DESC, after: \"#{end_cursor}\"") post_graphql(cursored_query, current_user: current_user) response_data = JSON.parse(response.body)['data']['project']['issues']['edges'] - expect(grab_iids(response_data)).to eq [due_issue3.iid, due_issue4.iid, due_issue2.iid] + expect(grab_iids(response_data)).to eq([due_issue3.iid, due_issue4.iid, due_issue2.iid]) end end end end context 'when sorting by relative position' do - let(:sort_project) { create(:project, :public) } + let_it_be(:sort_project) { create(:project, :public) } - let!(:relative_issue1) { create(:issue, project: sort_project, relative_position: 2000) } - let!(:relative_issue2) { create(:issue, project: sort_project, relative_position: nil) } - let!(:relative_issue3) { create(:issue, project: sort_project, relative_position: 1000) } - let!(:relative_issue4) { create(:issue, project: sort_project, relative_position: nil) } - let!(:relative_issue5) { create(:issue, project: sort_project, relative_position: 500) } + let_it_be(:relative_issue1) { create(:issue, project: sort_project, relative_position: 2000) } + let_it_be(:relative_issue2) { create(:issue, project: sort_project, relative_position: nil) } + let_it_be(:relative_issue3) { create(:issue, project: sort_project, relative_position: 1000) } + let_it_be(:relative_issue4) { create(:issue, project: sort_project, relative_position: nil) } + let_it_be(:relative_issue5) { create(:issue, project: sort_project, relative_position: 500) } - let(:params) { 'sort: RELATIVE_POSITION_ASC' } + let_it_be(:params) { 'sort: RELATIVE_POSITION_ASC' } def query(issue_params = params) graphql_query_for( @@ -228,20 +228,91 @@ describe 'getting an issue list for a project' do context 'when ascending' do it 'sorts issues' do - expect(grab_iids).to eq [relative_issue5.iid, relative_issue3.iid, relative_issue1.iid, relative_issue4.iid, relative_issue2.iid] + expect(grab_iids).to eq([relative_issue5.iid, relative_issue3.iid, relative_issue1.iid, relative_issue4.iid, relative_issue2.iid]) end context 'when paginating' do let(:params) { 'sort: RELATIVE_POSITION_ASC, first: 2' } it 'sorts issues' do - expect(grab_iids).to eq [relative_issue5.iid, relative_issue3.iid] + expect(grab_iids).to eq([relative_issue5.iid, relative_issue3.iid]) cursored_query = query("sort: RELATIVE_POSITION_ASC, after: \"#{end_cursor}\"") post_graphql(cursored_query, current_user: current_user) response_data = JSON.parse(response.body)['data']['project']['issues']['edges'] - expect(grab_iids(response_data)).to eq [relative_issue1.iid, relative_issue4.iid, relative_issue2.iid] + expect(grab_iids(response_data)).to eq([relative_issue1.iid, relative_issue4.iid, relative_issue2.iid]) + end + end + end + end + + context 'when sorting by priority' do + let_it_be(:sort_project) { create(:project, :public) } + + let_it_be(:early_milestone) { create(:milestone, project: sort_project, due_date: 10.days.from_now) } + let_it_be(:late_milestone) { create(:milestone, project: sort_project, due_date: 30.days.from_now) } + let_it_be(:label_1) { create(:label, project: sort_project, priority: 1) } + let_it_be(:label_2) { create(:label, project: sort_project, priority: 5) } + let_it_be(:issue1) { create(:issue, project: sort_project, labels: [label_1], milestone: late_milestone) } + let_it_be(:issue2) { create(:issue, project: sort_project, labels: [label_2]) } + let_it_be(:issue3) { create(:issue, project: sort_project, milestone: early_milestone) } + let_it_be(:issue4) { create(:issue, project: sort_project) } + + let_it_be(:params) { 'sort: PRIORITY_ASC' } + + def query(issue_params = params) + graphql_query_for( + 'project', + { 'fullPath' => sort_project.full_path }, + "issues(#{issue_params}) { pageInfo { endCursor} edges { node { iid dueDate } } }" + ) + end + + before do + post_graphql(query, current_user: current_user) + end + + it_behaves_like 'a working graphql query' + + context 'when ascending' do + it 'sorts issues' do + expect(grab_iids).to eq([issue3.iid, issue1.iid, issue2.iid, issue4.iid]) + end + + context 'when paginating' do + let(:params) { 'sort: PRIORITY_ASC, first: 2' } + + it 'sorts issues' do + expect(grab_iids).to eq([issue3.iid, issue1.iid]) + + cursored_query = query("sort: PRIORITY_ASC, after: \"#{end_cursor}\"") + post_graphql(cursored_query, current_user: current_user) + response_data = JSON.parse(response.body)['data']['project']['issues']['edges'] + + expect(grab_iids(response_data)).to eq([issue2.iid, issue4.iid]) + end + end + end + + context 'when descending' do + let(:params) { 'sort: PRIORITY_DESC' } + + it 'sorts issues' do + expect(grab_iids).to eq([issue1.iid, issue3.iid, issue2.iid, issue4.iid]) + end + + context 'when paginating' do + let(:params) { 'sort: PRIORITY_DESC, first: 2' } + + it 'sorts issues' do + expect(grab_iids).to eq([issue1.iid, issue3.iid]) + + cursored_query = query("sort: PRIORITY_DESC, after: \"#{end_cursor}\"") + post_graphql(cursored_query, current_user: current_user) + response_data = JSON.parse(response.body)['data']['project']['issues']['edges'] + + expect(grab_iids(response_data)).to eq([issue2.iid, issue4.iid]) end end end diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb index cd622807c09..41bb5f55453 100644 --- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb +++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb @@ -136,27 +136,55 @@ describe 'layouts/nav/sidebar/_project' do end describe 'operations settings tab' do - before do - project.update!(archived: project_archived) - end + describe 'archive projects' do + before do + project.update!(archived: project_archived) + end - context 'when project is archived' do - let(:project_archived) { true } + context 'when project is archived' do + let(:project_archived) { true } - it 'does not show the operations settings tab' do - render + it 'does not show the operations settings tab' do + render + + expect(rendered).not_to have_link('Operations', href: project_settings_operations_path(project)) + end + end - expect(rendered).not_to have_link('Operations', href: project_settings_operations_path(project)) + context 'when project is active' do + let(:project_archived) { false } + + it 'shows the operations settings tab' do + render + + expect(rendered).to have_link('Operations', href: project_settings_operations_path(project)) + end end end - context 'when project is active' do - let(:project_archived) { false } + describe 'Alert Management' do + context 'when alert_management_minimal is enabled' do + before do + stub_feature_flags(alert_management_minimal: true) + end - it 'shows the operations settings tab' do - render + it 'shows the Alerts sidebar entry' do + render + + expect(rendered).to have_css('a[title="Alerts"]') + end + end + + context 'when alert_management_minimal is disabled' do + before do + stub_feature_flags(alert_management_minimal: false) + end + + it 'does not show the Alerts sidebar entry' do + render - expect(rendered).to have_link('Operations', href: project_settings_operations_path(project)) + expect(rendered).to have_no_css('a[title="Alerts"]') + end end end end |