diff options
83 files changed, 1170 insertions, 186 deletions
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 268326f9246..e016cdeed30 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -248,7 +248,7 @@ export default { }); } - if (!file.parallel_diff_lines || !file.highlighted_diff_lines) { + if (!file.parallel_diff_lines.length || !file.highlighted_diff_lines.length) { const newDiscussions = (file.discussions || []) .filter(d => d.id !== discussion.id) .concat(discussion); diff --git a/app/assets/javascripts/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_projects/components/import_projects_table.vue index 5beb8d2edc5..407fbbb0871 100644 --- a/app/assets/javascripts/import_projects/components/import_projects_table.vue +++ b/app/assets/javascripts/import_projects/components/import_projects_table.vue @@ -1,8 +1,8 @@ <script> import { throttle } from 'lodash'; import { mapActions, mapState, mapGetters } from 'vuex'; -import { GlButton, GlLoadingIcon } from '@gitlab/ui'; -import { __, sprintf } from '~/locale'; +import { GlButton, GlLoadingIcon, GlModal } from '@gitlab/ui'; +import { n__, __, sprintf } from '~/locale'; import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; import ProviderRepoTableRow from './provider_repo_table_row.vue'; import PageQueryParamSync from './page_query_param_sync.vue'; @@ -16,6 +16,7 @@ export default { PageQueryParamSync, GlLoadingIcon, GlButton, + GlModal, PaginationLinks, }, props: { @@ -42,6 +43,7 @@ export default { 'isImportingAnyRepo', 'hasImportableRepos', 'hasIncompatibleRepos', + 'importAllCount', ]), availableNamespaces() { @@ -61,8 +63,12 @@ export default { importAllButtonText() { return this.hasIncompatibleRepos - ? __('Import all compatible repositories') - : __('Import all repositories'); + ? n__( + 'Import %d compatible repository', + 'Import %d compatible repositories', + this.importAllCount, + ) + : n__('Import %d repository', 'Import %d repositories', this.importAllCount); }, emptyStateText() { @@ -111,9 +117,8 @@ export default { <template> <div> <page-query-param-sync :page="pageInfo.page" @popstate="setPage" /> - <p class="light text-nowrap mt-2"> - {{ s__('ImportProjects|Select the projects you want to import') }} + {{ s__('ImportProjects|Select the repositories you want to import') }} </p> <template v-if="hasIncompatibleRepos"> <slot name="incompatible-repos-warning"></slot> @@ -130,9 +135,25 @@ export default { :loading="isImportingAnyRepo" :disabled="!hasImportableRepos" type="button" - @click="importAll" + @click="$refs.importAllModal.show()" >{{ importAllButtonText }}</gl-button > + <gl-modal + ref="importAllModal" + modal-id="import-all-modal" + :title="s__('ImportProjects|Import repositories')" + :ok-title="__('Import')" + @ok="importAll" + > + {{ + n__( + 'Are you sure you want to import %d repository?', + 'Are you sure you want to import %d repositories?', + importAllCount, + ) + }} + </gl-modal> + <slot name="actions"></slot> <form v-if="filterable" class="gl-ml-auto" novalidate @submit.prevent> <input @@ -140,7 +161,7 @@ export default { data-qa-selector="githubish_import_filter_field" class="form-control" name="filter" - :placeholder="__('Filter your projects by name')" + :placeholder="__('Filter your repositories by name')" autofocus size="40" @input="handleFilterInput($event)" diff --git a/app/assets/javascripts/import_projects/store/getters.js b/app/assets/javascripts/import_projects/store/getters.js index fded478bae2..b76c52beea2 100644 --- a/app/assets/javascripts/import_projects/store/getters.js +++ b/app/assets/javascripts/import_projects/store/getters.js @@ -14,6 +14,8 @@ export const hasIncompatibleRepos = state => state.repositories.some(isIncompati export const hasImportableRepos = state => state.repositories.some(isProjectImportable); +export const importAllCount = state => state.repositories.filter(isProjectImportable).length; + export const getImportTarget = state => repoId => { if (state.customImportTargets[repoId]) { return state.customImportTargets[repoId]; diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index fe975112b60..f79c66514d2 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -129,7 +129,7 @@ export default { <li :class="{ active: !previewMarkdown }" class="md-header-toolbar"> <div class="d-inline-block"> <toolbar-button tag="**" :button-title="__('Add bold text')" icon="bold" /> - <toolbar-button tag="*" :button-title="__('Add italic text')" icon="italic" /> + <toolbar-button tag="_" :button-title="__('Add italic text')" icon="italic" /> <toolbar-button :prepend="true" :tag="tag" diff --git a/app/graphql/mutations/issues/set_severity.rb b/app/graphql/mutations/issues/set_severity.rb new file mode 100644 index 00000000000..bc386e07178 --- /dev/null +++ b/app/graphql/mutations/issues/set_severity.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Mutations + module Issues + class SetSeverity < Base + graphql_name 'IssueSetSeverity' + + argument :severity, Types::IssuableSeverityEnum, required: true, + description: 'Set the incident severity level.' + + def resolve(project_path:, iid:, severity:) + issue = authorized_find!(project_path: project_path, iid: iid) + project = issue.project + + ::Issues::UpdateService.new(project, current_user, severity: severity) + .execute(issue) + + { + issue: issue, + errors: errors_on_object(issue) + } + end + end + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index aea79f4f423..d970bef8959 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -24,6 +24,7 @@ module Types mount_mutation Mutations::Issues::SetConfidential mount_mutation Mutations::Issues::SetLocked mount_mutation Mutations::Issues::SetDueDate + mount_mutation Mutations::Issues::SetSeverity mount_mutation Mutations::Issues::SetSubscription mount_mutation Mutations::Issues::Update mount_mutation Mutations::MergeRequests::Create diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 97b1dba1fc6..eccd1ea5490 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -871,13 +871,17 @@ module Ci options.dig(:release)&.any? end - def hide_secrets(trace) + def hide_secrets(data, metrics = ::Gitlab::Ci::Trace::Metrics.new) return unless trace - trace = trace.dup - Gitlab::Ci::MaskSecret.mask!(trace, project.runners_token) if project - Gitlab::Ci::MaskSecret.mask!(trace, token) if token - trace + data.dup.tap do |trace| + Gitlab::Ci::MaskSecret.mask!(trace, project.runners_token) if project + Gitlab::Ci::MaskSecret.mask!(trace, token) if token + + if trace != data + metrics.increment_trace_operation(operation: :mutated) + end + end end def serializable_hash(options = {}) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 64bb22e886e..a4090a1d61d 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -195,6 +195,15 @@ module Issuable issuable_severity&.severity || IssuableSeverity::DEFAULT end + def update_severity(severity) + return unless incident? + + severity = severity.to_s.downcase + severity = IssuableSeverity::DEFAULT unless IssuableSeverity.severities.key?(severity) + + (issuable_severity || build_issuable_severity(issue_id: id)).update(severity: severity) + end + private def description_max_length_for_new_records_is_valid diff --git a/app/models/todo.rb b/app/models/todo.rb index f973c1ff1d4..6c8e085762d 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -17,9 +17,11 @@ class Todo < ApplicationRecord UNMERGEABLE = 6 DIRECTLY_ADDRESSED = 7 MERGE_TRAIN_REMOVED = 8 # This is an EE-only feature + REVIEW_REQUESTED = 9 ACTION_NAMES = { ASSIGNED => :assigned, + REVIEW_REQUESTED => :review_requested, MENTIONED => :mentioned, BUILD_FAILED => :build_failed, MARKED => :marked, @@ -167,6 +169,10 @@ class Todo < ApplicationRecord action == ASSIGNED end + def review_requested? + action == REVIEW_REQUESTED + end + def merge_train_removed? action == MERGE_TRAIN_REMOVED end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index dea77f11baa..56bcef0c562 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -184,10 +184,7 @@ class IssuableBaseService < BaseService handle_quick_actions(issuable) filter_params(issuable) - change_state(issuable) - change_subscription(issuable) - change_todo(issuable) - toggle_award(issuable) + change_additional_attributes(issuable) old_associations = associations_before_update(issuable) label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids) @@ -305,6 +302,14 @@ class IssuableBaseService < BaseService issuable.title_changed? || issuable.description_changed? end + def change_additional_attributes(issuable) + change_state(issuable) + change_severity(issuable) + change_subscription(issuable) + change_todo(issuable) + toggle_award(issuable) + end + def change_state(issuable) case params.delete(:state_event) when 'reopen' @@ -314,6 +319,12 @@ class IssuableBaseService < BaseService end end + def change_severity(issuable) + if severity = params.delete(:severity) + issuable.update_severity(severity) + end + end + def change_subscription(issuable) case params.delete(:subscription_event) when 'subscribe' @@ -358,8 +369,8 @@ class IssuableBaseService < BaseService associations end - def has_changes?(issuable, old_labels: [], old_assignees: []) - valid_attrs = [:title, :description, :assignee_ids, :milestone_id, :target_branch] + def has_changes?(issuable, old_labels: [], old_assignees: [], old_reviewers: []) + valid_attrs = [:title, :description, :assignee_ids, :reviewer_ids, :milestone_id, :target_branch] attrs_changed = valid_attrs.any? do |attr| issuable.previous_changes.include?(attr.to_s) @@ -369,7 +380,9 @@ class IssuableBaseService < BaseService assignees_changed = issuable.assignees != old_assignees - attrs_changed || labels_changed || assignees_changed + reviewers_changed = issuable.reviewers != old_reviewers if issuable.allows_reviewers? + + attrs_changed || labels_changed || assignees_changed || reviewers_changed end def invalidate_cache_counts(issuable, users: []) diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index cf02158b629..1468bfd6bb6 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -24,8 +24,9 @@ module MergeRequests old_labels = old_associations.fetch(:labels, []) old_mentioned_users = old_associations.fetch(:mentioned_users, []) old_assignees = old_associations.fetch(:assignees, []) + old_reviewers = old_associations.fetch(:reviewers, []) - if has_changes?(merge_request, old_labels: old_labels, old_assignees: old_assignees) + if has_changes?(merge_request, old_labels: old_labels, old_assignees: old_assignees, old_reviewers: old_reviewers) todo_service.resolve_todos_for_target(merge_request, current_user) end @@ -44,6 +45,8 @@ module MergeRequests handle_assignees_change(merge_request, old_assignees) if merge_request.assignees != old_assignees + handle_reviewers_change(merge_request, old_reviewers) if merge_request.reviewers != old_reviewers + if merge_request.previous_changes.include?('target_branch') || merge_request.previous_changes.include?('source_branch') merge_request.mark_as_unchecked @@ -108,6 +111,10 @@ module MergeRequests todo_service.reassigned_assignable(merge_request, current_user, old_assignees) end + def handle_reviewers_change(merge_request, old_reviewers) + todo_service.reassigned_reviewable(merge_request, current_user, old_reviewers) + end + def create_branch_change_note(issuable, branch_type, old_branch, new_branch) SystemNoteService.change_branch( issuable, issuable.project, current_user, branch_type, diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index a3db2ae7947..1220b4cbe29 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -57,6 +57,14 @@ class TodoService create_assignment_todo(issuable, current_user, old_assignees) end + # When we reassign an reviewable object (merge request) we should: + # + # * create a pending todo for new reviewer if object is assigned + # + def reassigned_reviewable(issuable, current_user, old_reviewers = []) + create_reviewer_todo(issuable, current_user, old_reviewers) + end + # When create a merge request we should: # # * creates a pending todo for assignee if merge request is assigned @@ -217,6 +225,7 @@ class TodoService def new_issuable(issuable, author) create_assignment_todo(issuable, author) + create_reviewer_todo(issuable, author) if issuable.allows_reviewers? create_mention_todos(issuable.project, issuable, author) end @@ -250,6 +259,14 @@ class TodoService end end + def create_reviewer_todo(target, author, old_reviewers = []) + if target.reviewers.any? + reviewers = target.reviewers - old_reviewers + attributes = attributes_for_todo(target.project, target, author, Todo::REVIEW_REQUESTED) + create_todos(reviewers, attributes) + end + end + def create_mention_todos(parent, target, author, note = nil, skip_users = []) # Create Todos for directly addressed users directly_addressed_users = filter_directly_addressed_users(parent, note || target, author, skip_users) diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index fcb1c1a6f3e..ad3795445d1 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -16,7 +16,7 @@ = image_tag @appearance.header_logo_path, class: 'appearance-light-logo-preview' - if @appearance.persisted? %br - = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo" + = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn gl-button btn-danger btn-danger-secondary btn-sm" %hr = f.hidden_field :header_logo_cache = f.file_field :header_logo, class: "" @@ -35,7 +35,7 @@ = image_tag @appearance.favicon_path, class: 'appearance-light-logo-preview' - if @appearance.persisted? %br - = link_to 'Remove favicon', favicon_admin_appearances_path, data: { confirm: "Favicon will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo" + = link_to 'Remove favicon', favicon_admin_appearances_path, data: { confirm: "Favicon will be removed. Are you sure?"}, method: :delete, class: "btn gl-button btn-danger btn-danger-secondary btn-sm" %hr = f.hidden_field :favicon_cache = f.file_field :favicon, class: '' @@ -67,7 +67,7 @@ = image_tag @appearance.logo_path, class: 'appearance-logo-preview' - if @appearance.persisted? %br - = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo" + = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn gl-button btn-danger btn-danger-secondary btn-sm remove-logo" %hr = f.hidden_field :logo_cache = f.file_field :logo, class: "" @@ -101,7 +101,7 @@ = parsed_with_gfm .gl-mt-3.gl-mb-3 - = f.submit 'Update appearance settings', class: 'btn btn-success' + = f.submit 'Update appearance settings', class: 'btn gl-button btn-success' - if @appearance.persisted? || @appearance.updated_at .mt-4 - if @appearance.persisted? diff --git a/app/views/admin/appearances/_system_header_footer_form.html.haml b/app/views/admin/appearances/_system_header_footer_form.html.haml index 7f53b2baa32..b50778a1076 100644 --- a/app/views/admin/appearances/_system_header_footer_form.html.haml +++ b/app/views/admin/appearances/_system_header_footer_form.html.haml @@ -23,7 +23,7 @@ = _('Add header and footer to emails. Please note that color settings will only be applied within the application interface') .form-group.js-toggle-colors-container - %button.btn.btn-link.js-toggle-colors-link{ type: 'button' } + %button.btn.gl-button.btn-link.js-toggle-colors-link{ type: 'button' } = _('Customize colors') .form-group.js-toggle-colors-container.hide = form.label :message_background_color, _('Background Color'), class: 'col-form-label label-bold' diff --git a/app/views/admin/appearances/preview_sign_in.html.haml b/app/views/admin/appearances/preview_sign_in.html.haml index 2cd95071c73..eec4719c13c 100644 --- a/app/views/admin/appearances/preview_sign_in.html.haml +++ b/app/views/admin/appearances/preview_sign_in.html.haml @@ -8,5 +8,5 @@ = label_tag :password = password_field_tag :password, nil, class: "form-control bottom", title: 'This field is required.' .form-group - = button_tag "Sign in", class: "btn-success btn" + = button_tag "Sign in", class: "btn gl-button btn-success" diff --git a/app/views/projects/pipelines/_pipeline_warnings.html.haml b/app/views/projects/pipelines/_pipeline_warnings.html.haml deleted file mode 100644 index 0da678401f5..00000000000 --- a/app/views/projects/pipelines/_pipeline_warnings.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -- total_warnings = warnings.length -- message = warning_header(total_warnings) - -- if warnings.any? - .bs-callout.bs-callout-warning - %details - %summary.gl-mb-2= message - - warnings.map(&:content).each do |warning| - = markdown(warning) diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml index c3f14fc82d2..a9c140aee5f 100644 --- a/app/views/projects/pipelines/show.html.haml +++ b/app/views/projects/pipelines/show.html.haml @@ -19,7 +19,6 @@ - lint_link_start = '<a href="%{url}">'.html_safe % { url: lint_link_url } = s_('You can also test your %{gitlab_ci_yml} in %{lint_link_start}CI Lint%{lint_link_end}').html_safe % { gitlab_ci_yml: '.gitlab-ci.yml', lint_link_start: lint_link_start, lint_link_end: '</a>'.html_safe } - = render "projects/pipelines/pipeline_warnings", warnings: @pipeline.warning_messages(limit: Gitlab::Ci::Warnings::MAX_LIMIT) = render "projects/pipelines/with_tabs", pipeline: @pipeline, pipeline_has_errors: pipeline_has_errors .js-pipeline-details-vue{ data: { endpoint: project_pipeline_path(@project, @pipeline, format: :json) } } diff --git a/app/views/shared/blob/_markdown_buttons.html.haml b/app/views/shared/blob/_markdown_buttons.html.haml index 32fd732cda9..d0719bf21fe 100644 --- a/app/views/shared/blob/_markdown_buttons.html.haml +++ b/app/views/shared/blob/_markdown_buttons.html.haml @@ -1,6 +1,6 @@ .md-header-toolbar.active = markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: _("Add bold text") }) - = markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: _("Add italic text") }) + = markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "_" }, title: _("Add italic text") }) = markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: _("Insert a quote") }) = markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: _("Insert code") }) = markdown_toolbar_button({ icon: "link", data: { "md-tag" => "[{text}](url)", "md-select" => "url" }, title: _("Add a link") }) diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index cce0e9fd2f7..dc985f96e3f 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -167,7 +167,7 @@ = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport') .sidebar-mr-source-branch.hide-collapsed %span - = _('Source branch: %{source_branch_open}%{source_branch}%{source_branch_close}').html_safe % { source_branch_open: "<cite class='ref-name' title='#{source_branch}'>".html_safe, source_branch_close: "</cite>".html_safe, source_branch: source_branch } + = _('Source branch: %{source_branch_open}%{source_branch}%{source_branch_close}').html_safe % { source_branch_open: "<cite title='#{source_branch}'>".html_safe, source_branch_close: "</cite>".html_safe, source_branch: source_branch } = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport') - if issuable_sidebar.dig(:current_user, :can_move) diff --git a/app/views/shared/issuable/form/_type_selector.html.haml b/app/views/shared/issuable/form/_type_selector.html.haml index 1c3a720ebcc..668b4e90bef 100644 --- a/app/views/shared/issuable/form/_type_selector.html.haml +++ b/app/views/shared/issuable/form/_type_selector.html.haml @@ -1,10 +1,10 @@ .form-group.row.gl-mb-0 = form.label :type, 'Type', class: 'col-form-label col-sm-2' .col-sm-10 - .issuable-form-select-holder.selectbox.form-group + .issuable-form-select-holder.selectbox.form-group.gl-mb-0 .dropdown.js-issuable-type-filter-dropdown-wrap %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } - %span.dropdown-label + %span.dropdown-toggle-text.is-default = type.capitalize || _("Select type") = icon('chevron-down') .dropdown-menu.dropdown-menu-selectable.dropdown-select @@ -18,5 +18,10 @@ = link_to new_project_issue_path(@project), class: ("is-active" if type === 'issue') do = _("Issue") %li.js-filter-issuable-type - = link_to new_project_issue_path(@project, { 'issue[issue_type]': 'incident', issuable_template: 'incident' }), class: ("is-active" if type === 'incident') do + = link_to new_project_issue_path(@project, { issuable_template: 'incident', issue: { issue_type: 'incident' } }), class: ("is-active" if type === 'incident') do = _("Incident") + - if type === 'incident' + %p.form-text.text-muted + - incident_docs_url = help_page_path('operations/incident_management/incidents.md', anchor: 'create-and-manage-incidents-in-gitlab') + - incident_docs_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: incident_docs_url } + = _('A %{incident_docs_start}modified issue%{incident_docs_end} to guide the resolution of incidents.').html_safe % { incident_docs_start: incident_docs_start, incident_docs_end: '</a>'.html_safe } diff --git a/changelogs/unreleased/230857-incident-type-selector-help.yml b/changelogs/unreleased/230857-incident-type-selector-help.yml new file mode 100644 index 00000000000..34541958b2f --- /dev/null +++ b/changelogs/unreleased/230857-incident-type-selector-help.yml @@ -0,0 +1,4 @@ +title: Add help text to incident type select on new issue form +merge_request: 41567 +author: +type: changed diff --git a/changelogs/unreleased/238564-graphql-mutation-to-update-incident-severity.yml b/changelogs/unreleased/238564-graphql-mutation-to-update-incident-severity.yml new file mode 100644 index 00000000000..71cbd317eb6 --- /dev/null +++ b/changelogs/unreleased/238564-graphql-mutation-to-update-incident-severity.yml @@ -0,0 +1,5 @@ +--- +title: Allows to update incident severity via GraphQL +merge_request: 40869 +author: +type: added diff --git a/changelogs/unreleased/app-logger-18.yml b/changelogs/unreleased/app-logger-18.yml new file mode 100644 index 00000000000..0c0f2288e93 --- /dev/null +++ b/changelogs/unreleased/app-logger-18.yml @@ -0,0 +1,5 @@ +--- +title: Use GitLab AppLogger +merge_request: 41290 +author: Rajendra Kadam +type: other diff --git a/changelogs/unreleased/app-logger-2.yml b/changelogs/unreleased/app-logger-2.yml new file mode 100644 index 00000000000..bdc69b2714c --- /dev/null +++ b/changelogs/unreleased/app-logger-2.yml @@ -0,0 +1,5 @@ +--- +title: Use applogger in config/initializers/* +merge_request: 41047 +author: Rajendra Kadam +type: other diff --git a/changelogs/unreleased/app-logger-5.yml b/changelogs/unreleased/app-logger-5.yml new file mode 100644 index 00000000000..f0ffd786352 --- /dev/null +++ b/changelogs/unreleased/app-logger-5.yml @@ -0,0 +1,5 @@ +--- +title: Use applogger in some files of lib/gitlab/ldap/sync/* +merge_request: 41051 +author: Rajendra Kadam +type: other diff --git a/changelogs/unreleased/ph-225922-fixedImageDiscussionsNotShowingOnChangesTab.yml b/changelogs/unreleased/ph-225922-fixedImageDiscussionsNotShowingOnChangesTab.yml new file mode 100644 index 00000000000..fa55a99106f --- /dev/null +++ b/changelogs/unreleased/ph-225922-fixedImageDiscussionsNotShowingOnChangesTab.yml @@ -0,0 +1,5 @@ +--- +title: Fixed image comments not showing on the changes tab +merge_request: 41683 +author: +type: fixed diff --git a/changelogs/unreleased/remove-pipeline-warnings-from-pipeline-view.yml b/changelogs/unreleased/remove-pipeline-warnings-from-pipeline-view.yml new file mode 100644 index 00000000000..e1387518c57 --- /dev/null +++ b/changelogs/unreleased/remove-pipeline-warnings-from-pipeline-view.yml @@ -0,0 +1,5 @@ +--- +title: Remove pipeline warnings from pipeline view +merge_request: 41419 +author: +type: changed diff --git a/changelogs/unreleased/vij-snippets-missing-param-requirement.yml b/changelogs/unreleased/vij-snippets-missing-param-requirement.yml new file mode 100644 index 00000000000..85ae377b07b --- /dev/null +++ b/changelogs/unreleased/vij-snippets-missing-param-requirement.yml @@ -0,0 +1,5 @@ +--- +title: Add ability to update only Snippet descriptions via REST endpoint +merge_request: 41581 +author: +type: changed diff --git a/changelogs/unreleased/xanf-import-projects-count.yml b/changelogs/unreleased/xanf-import-projects-count.yml new file mode 100644 index 00000000000..8e506a859b5 --- /dev/null +++ b/changelogs/unreleased/xanf-import-projects-count.yml @@ -0,0 +1,5 @@ +--- +title: Add confirmation dialog when importing multiple projects +merge_request: 41306 +author: +type: changed diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb index 082f5568391..d5d8587f1c8 100644 --- a/config/initializers/7_prometheus_metrics.rb +++ b/config/initializers/7_prometheus_metrics.rb @@ -16,7 +16,7 @@ def prometheus_default_multiproc_dir end Prometheus::Client.configure do |config| - config.logger = Rails.logger # rubocop:disable Gitlab/RailsLogger + config.logger = Gitlab::AppLogger config.initial_mmap_file_size = 4 * 1024 diff --git a/config/initializers/active_record_lifecycle.rb b/config/initializers/active_record_lifecycle.rb index 493d328b93e..4d63ffaf711 100644 --- a/config/initializers/active_record_lifecycle.rb +++ b/config/initializers/active_record_lifecycle.rb @@ -7,7 +7,7 @@ if defined?(ActiveRecord::Base) && !Gitlab::Runtime.sidekiq? ActiveSupport.on_load(:active_record) do ActiveRecord::Base.establish_connection - Rails.logger.debug("ActiveRecord connection established") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.debug("ActiveRecord connection established") end end end @@ -20,6 +20,6 @@ if defined?(ActiveRecord::Base) # as there's no need for the master process to hold a connection ActiveRecord::Base.connection.disconnect! - Rails.logger.debug("ActiveRecord connection disconnected") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.debug("ActiveRecord connection disconnected") end end diff --git a/config/initializers/deprecations.rb b/config/initializers/deprecations.rb index 0d096e34eb7..2b07ca665e2 100644 --- a/config/initializers/deprecations.rb +++ b/config/initializers/deprecations.rb @@ -2,7 +2,7 @@ if Rails.env.development? || ENV['GITLAB_LEGACY_PATH_LOG_MESSAGE'] deprecator = ActiveSupport::Deprecation.new('11.0', 'GitLab') deprecator.behavior = -> (message, callstack) { - Rails.logger.warn("#{message}: #{callstack[1..20].join}") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.warn("#{message}: #{callstack[1..20].join}") } ActiveSupport::Deprecation.deprecate_methods(Gitlab::GitalyClient::StorageSettings, :legacy_disk_path, deprecator: deprecator) diff --git a/config/initializers/forbid_sidekiq_in_transactions.rb b/config/initializers/forbid_sidekiq_in_transactions.rb index 9bade443aae..6bcd4dbd52f 100644 --- a/config/initializers/forbid_sidekiq_in_transactions.rb +++ b/config/initializers/forbid_sidekiq_in_transactions.rb @@ -28,7 +28,7 @@ module Sidekiq Use an `after_commit` hook, or include `AfterCommitQueue` and use a `run_after_commit` block instead. MSG rescue Sidekiq::Worker::EnqueueFromTransactionError => e - ::Rails.logger.error(e.message) if ::Rails.env.production? + Gitlab::AppLogger.error(e.message) if ::Rails.env.production? Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e) end end diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index febcedfee82..cfb34fa9784 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -70,7 +70,7 @@ Sidekiq.configure_server do |config| cron_jobs[k]['class'] = cron_jobs[k].delete('job_class') else cron_jobs.delete(k) - Rails.logger.error("Invalid cron_jobs config key: '#{k}'. Check your gitlab config file.") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.error("Invalid cron_jobs config key: '#{k}'. Check your gitlab config file.") end end Sidekiq::Cron::Job.load_from_hash! cron_jobs diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index e23cb4b1b38..c54b04c0d42 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -2193,12 +2193,12 @@ input ConfigureSastInput { clientMutationId: String """ - Payload containing SAST variable values (https://docs.gitlab.com/ee/user/application_security/sast/#available-variables). + SAST CI configuration for the project """ - configuration: JSON! + configuration: SastCiConfigurationInput! """ - Full path of the project. + Full path of the project """ projectPath: ID! } @@ -2218,9 +2218,14 @@ type ConfigureSastPayload { errors: [String!]! """ - JSON containing the status of MR creation. + Status of creating the commit for the supplied SAST CI configuration + """ + status: String! + + """ + Redirect path to use when the response is successful """ - result: JSON + successPath: String } """ @@ -8076,6 +8081,51 @@ type IssueSetLockedPayload { } """ +Autogenerated input type of IssueSetSeverity +""" +input IssueSetSeverityInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The IID of the issue to mutate + """ + iid: String! + + """ + The project the issue to mutate is in + """ + projectPath: ID! + + """ + Set the incident severity level. + """ + severity: IssuableSeverity! +} + +""" +Autogenerated return type of IssueSetSeverity +""" +type IssueSetSeverityPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Errors encountered during execution of the mutation. + """ + errors: [String!]! + + """ + The issue after mutation + """ + issue: Issue +} + +""" Autogenerated input type of IssueSetSubscription """ input IssueSetSubscriptionInput { @@ -10279,6 +10329,7 @@ type Mutation { issueSetEpic(input: IssueSetEpicInput!): IssueSetEpicPayload issueSetIteration(input: IssueSetIterationInput!): IssueSetIterationPayload issueSetLocked(input: IssueSetLockedInput!): IssueSetLockedPayload + issueSetSeverity(input: IssueSetSeverityInput!): IssueSetSeverityPayload issueSetSubscription(input: IssueSetSubscriptionInput!): IssueSetSubscriptionPayload issueSetWeight(input: IssueSetWeightInput!): IssueSetWeightPayload jiraImportStart(input: JiraImportStartInput!): JiraImportStartPayload @@ -14697,6 +14748,41 @@ type SastCiConfigurationEntityEdge { } """ +Represents an entity in SAST CI configuration +""" +input SastCiConfigurationEntityInput { + """ + Default value that is used if value is empty + """ + defaultValue: String! + + """ + CI keyword of entity + """ + field: String! + + """ + Current value of the entity + """ + value: String! +} + +""" +Represents a CI configuration of SAST +""" +input SastCiConfigurationInput { + """ + List of global entities related to SAST configuration + """ + global: [SastCiConfigurationEntityInput!] + + """ + List of pipeline entities related to SAST configuration + """ + pipeline: [SastCiConfigurationEntityInput!] +} + +""" Represents an entity for options in SAST CI configuration """ type SastCiConfigurationOptionsEntity { diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 154085e6264..de62bf43923 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -5971,7 +5971,7 @@ "inputFields": [ { "name": "projectPath", - "description": "Full path of the project.", + "description": "Full path of the project", "type": { "kind": "NON_NULL", "name": null, @@ -5985,13 +5985,13 @@ }, { "name": "configuration", - "description": "Payload containing SAST variable values (https://docs.gitlab.com/ee/user/application_security/sast/#available-variables).", + "description": "SAST CI configuration for the project", "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "SCALAR", - "name": "JSON", + "kind": "INPUT_OBJECT", + "name": "SastCiConfigurationInput", "ofType": null } }, @@ -6058,14 +6058,32 @@ "deprecationReason": null }, { - "name": "result", - "description": "JSON containing the status of MR creation.", + "name": "status", + "description": "Status of creating the commit for the supplied SAST CI configuration", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "successPath", + "description": "Redirect path to use when the response is successful", "args": [ ], "type": { "kind": "SCALAR", - "name": "JSON", + "name": "String", "ofType": null }, "isDeprecated": false, @@ -22369,6 +22387,136 @@ }, { "kind": "INPUT_OBJECT", + "name": "IssueSetSeverityInput", + "description": "Autogenerated input type of IssueSetSeverity", + "fields": null, + "inputFields": [ + { + "name": "projectPath", + "description": "The project the issue to mutate is in", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "iid", + "description": "The IID of the issue to mutate", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "severity", + "description": "Set the incident severity level.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "IssuableSeverity", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "IssueSetSeverityPayload", + "description": "Autogenerated return type of IssueSetSeverity", + "fields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": "Errors encountered during execution of the mutation.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "issue", + "description": "The issue after mutation", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Issue", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", "name": "IssueSetSubscriptionInput", "description": "Autogenerated input type of IssueSetSubscription", "fields": null, @@ -29905,6 +30053,33 @@ "deprecationReason": null }, { + "name": "issueSetSeverity", + "description": null, + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "IssueSetSeverityInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "IssueSetSeverityPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "issueSetSubscription", "description": null, "args": [ @@ -42873,6 +43048,106 @@ "possibleTypes": null }, { + "kind": "INPUT_OBJECT", + "name": "SastCiConfigurationEntityInput", + "description": "Represents an entity in SAST CI configuration", + "fields": null, + "inputFields": [ + { + "name": "field", + "description": "CI keyword of entity", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "defaultValue", + "description": "Default value that is used if value is empty", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "value", + "description": "Current value of the entity", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SastCiConfigurationInput", + "description": "Represents a CI configuration of SAST", + "fields": null, + "inputFields": [ + { + "name": "global", + "description": "List of global entities related to SAST configuration", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "SastCiConfigurationEntityInput", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "pipeline", + "description": "List of pipeline entities related to SAST configuration", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "SastCiConfigurationEntityInput", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { "kind": "OBJECT", "name": "SastCiConfigurationOptionsEntity", "description": "Represents an entity for options in SAST CI configuration", diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 3b139efd4ea..bc61283cc33 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -369,7 +369,8 @@ Autogenerated return type of ConfigureSast | --- | ---- | ---------- | | `clientMutationId` | String | A unique identifier for the client performing the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. | -| `result` | JSON | JSON containing the status of MR creation. | +| `status` | String! | Status of creating the commit for the supplied SAST CI configuration | +| `successPath` | String | Redirect path to use when the response is successful | ## ContainerExpirationPolicy @@ -1227,6 +1228,16 @@ Autogenerated return type of IssueSetLocked | `errors` | String! => Array | Errors encountered during execution of the mutation. | | `issue` | Issue | The issue after mutation | +## IssueSetSeverityPayload + +Autogenerated return type of IssueSetSeverity + +| Name | Type | Description | +| --- | ---- | ---------- | +| `clientMutationId` | String | A unique identifier for the client performing the mutation. | +| `errors` | String! => Array | Errors encountered during execution of the mutation. | +| `issue` | Issue | The issue after mutation | + ## IssueSetSubscriptionPayload Autogenerated return type of IssueSetSubscription diff --git a/doc/ci/troubleshooting.md b/doc/ci/troubleshooting.md index 96d94a6c165..4f4311570e0 100644 --- a/doc/ci/troubleshooting.md +++ b/doc/ci/troubleshooting.md @@ -11,7 +11,6 @@ type: reference Pipeline configuration warnings are shown when you: -- [View pipeline details](pipelines/index.md#view-pipelines). - [Validate configuration with the CI Lint tool](yaml/README.md#validate-the-gitlab-ciyml). - [Manually run a pipeline](pipelines/index.md#run-a-pipeline-manually). diff --git a/doc/development/import_project.md b/doc/development/import_project.md index 1fa6ea5d405..9e2f5af6738 100644 --- a/doc/development/import_project.md +++ b/doc/development/import_project.md @@ -96,6 +96,13 @@ If you want to import it to a new group or subgroup then create it first. The specified project export file in `archive_path` is missing. +##### `Exception: Permission denied @ rb_sysopen - (filename)` + +The specified project export file can not be accessed by the `git` user. + +Setting the file owner to `git:git`, changing the file permissions to `0400`, and moving it to a +public folder (for example `/tmp/`) fixes the issue. + ##### `Name can contain only letters, digits, emojis ...` ```plaintext diff --git a/doc/operations/incident_management/img/new_incident_create_v13_4.png b/doc/operations/incident_management/img/new_incident_create_v13_4.png Binary files differnew file mode 100644 index 00000000000..458532736bd --- /dev/null +++ b/doc/operations/incident_management/img/new_incident_create_v13_4.png diff --git a/doc/operations/incident_management/incidents.md b/doc/operations/incident_management/incidents.md index 528d29f9343..2a0910a1d9f 100644 --- a/doc/operations/incident_management/incidents.md +++ b/doc/operations/incident_management/incidents.md @@ -73,15 +73,27 @@ when you receive notification that the alert is resolved. ## Create an incident manually -> [Moved](https://gitlab.com/gitlab-org/monitor/health/-/issues/24) to GitLab core in 13.3. +If you have at least Developer [permissions](../../user/permissions.md), to create an Incident, you have two options. + +### From the Incidents List -For users with at least Developer [permissions](../../user/permissions.md), to create a Incident you can take any of the following actions: +> [Moved](https://gitlab.com/gitlab-org/monitor/health/-/issues/24) to GitLab core in 13.3. - Navigate to **Operations > Incidents** and click **Create Incident**. - Create a new issue using the `incident` template available when creating it. - Create a new issue and assign the `incident` label to it. -![Incident List Create](img/incident_list_create_v13_3.png) +![Incident List Create](./img/incident_list_create_v13_3.png) + +### From the Issues List + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/230857) in GitLab 13.4. + +- Navigate to **Issues > List** and click **Create Issue**. +- Create a new issue using the `type` drop-down and select `Incident`. +- The page will refresh and you will notice there are now only fields relevant to Incidents. + +![Incident List Create](./img/new_incident_create_v13_4.png) ## Configure PagerDuty integration diff --git a/doc/user/project/index.md b/doc/user/project/index.md index a49c5fe054a..da09b9d598e 100644 --- a/doc/user/project/index.md +++ b/doc/user/project/index.md @@ -192,12 +192,16 @@ To delete a project, first navigate to the home page for that project. ### Delayed deletion **(PREMIUM)** -By default, clicking to delete a project is followed by a seven day delay. Admins can restore the project during this period of time. +By default, projects in a personal namespace are deleted after a seven day delay. + +Admins can restore the project during this period of time. This delay [may be changed by an admin](../admin_area/settings/visibility_and_access_controls.md#default-deletion-delay). Admins can view all projects pending deletion. If you're an administrator, go to the top navigation bar, click **Projects > Your projects**, and then select the **Deleted projects** tab. From this tab an admin can restore any project. +For information on delay deletion of projects within a group, please see [Enabling delayed Project removal](../group/index.md#enabling-delayed-project-removal) + ## CI/CD for external repositories **(PREMIUM)** Instead of importing a repository directly to GitLab, you can connect your repository diff --git a/lib/api/composer_packages.rb b/lib/api/composer_packages.rb index 05887e58425..31d097c4bea 100644 --- a/lib/api/composer_packages.rb +++ b/lib/api/composer_packages.rb @@ -123,7 +123,7 @@ module API bad_request! end - track_event('push_package') + package_event('push_package') ::Packages::Composer::CreatePackageService .new(authorized_user_project, current_user, declared_params) diff --git a/lib/api/conan_packages.rb b/lib/api/conan_packages.rb index cf91d8ac74a..899683de184 100644 --- a/lib/api/conan_packages.rb +++ b/lib/api/conan_packages.rb @@ -242,7 +242,7 @@ module API delete do authorize!(:destroy_package, project) - track_event('delete_package') + package_event('delete_package') package.destroy end diff --git a/lib/api/helpers/packages/conan/api_helpers.rb b/lib/api/helpers/packages/conan/api_helpers.rb index b849907bcde..5b513ce1aab 100644 --- a/lib/api/helpers/packages/conan/api_helpers.rb +++ b/lib/api/helpers/packages/conan/api_helpers.rb @@ -124,7 +124,7 @@ module API conan_package_reference: params[:conan_package_reference] ).execute! - track_event('pull_package') if params[:file_name] == ::Packages::Conan::FileMetadatum::PACKAGE_BINARY + package_event('pull_package') if params[:file_name] == ::Packages::Conan::FileMetadatum::PACKAGE_BINARY present_carrierwave_file!(package_file.file) end @@ -135,7 +135,7 @@ module API def track_push_package_event if params[:file_name] == ::Packages::Conan::FileMetadatum::PACKAGE_BINARY && params[:file].size > 0 # rubocop: disable Style/ZeroLengthPredicate - track_event('push_package') + package_event('push_package') end end diff --git a/lib/api/helpers/packages_helpers.rb b/lib/api/helpers/packages_helpers.rb index c6037d52de9..403f5ea3851 100644 --- a/lib/api/helpers/packages_helpers.rb +++ b/lib/api/helpers/packages_helpers.rb @@ -47,6 +47,10 @@ module API authorize_create_package!(subject) require_gitlab_workhorse! end + + def package_event(event_name, **args) + track_event(event_name, **args) + end end end end diff --git a/lib/api/helpers/snippets_helpers.rb b/lib/api/helpers/snippets_helpers.rb index 560c61c7e53..9224381735f 100644 --- a/lib/api/helpers/snippets_helpers.rb +++ b/lib/api/helpers/snippets_helpers.rb @@ -41,6 +41,10 @@ module API mutually_exclusive :files, :file_name end + params :minimum_update_params do + at_least_one_of :content, :description, :files, :file_name, :title, :visibility + end + def content_for(snippet) if snippet.empty_repo? env['api.format'] = :txt diff --git a/lib/api/maven_packages.rb b/lib/api/maven_packages.rb index caaeabf7061..e6d9a9a7c20 100644 --- a/lib/api/maven_packages.rb +++ b/lib/api/maven_packages.rb @@ -107,7 +107,7 @@ module API when 'sha1' package_file.file_sha1 else - track_event('pull_package') if jar_file?(format) + package_event('pull_package') if jar_file?(format) present_carrierwave_file_with_head_support!(package_file.file) end end @@ -145,7 +145,7 @@ module API when 'sha1' package_file.file_sha1 else - track_event('pull_package') if jar_file?(format) + package_event('pull_package') if jar_file?(format) present_carrierwave_file_with_head_support!(package_file.file) end @@ -181,7 +181,7 @@ module API when 'sha1' package_file.file_sha1 else - track_event('pull_package') if jar_file?(format) + package_event('pull_package') if jar_file?(format) present_carrierwave_file_with_head_support!(package_file.file) end @@ -233,7 +233,7 @@ module API when 'md5' nil else - track_event('push_package') if jar_file?(format) + package_event('push_package') if jar_file?(format) file_params = { file: params[:file], diff --git a/lib/api/npm_packages.rb b/lib/api/npm_packages.rb index 21ca57b7985..fca405b76b7 100644 --- a/lib/api/npm_packages.rb +++ b/lib/api/npm_packages.rb @@ -141,7 +141,7 @@ module API package_file = ::Packages::PackageFileFinder .new(package, params[:file_name]).execute! - track_event('pull_package') + package_event('pull_package') present_carrierwave_file!(package_file.file) end @@ -157,7 +157,7 @@ module API put ':id/packages/npm/:package_name', requirements: NPM_ENDPOINT_REQUIREMENTS do authorize_create_package!(user_project) - track_event('push_package') + package_event('push_package') created_package = ::Packages::Npm::CreatePackageService .new(user_project, current_user, params.merge(build: current_authenticated_job)).execute diff --git a/lib/api/nuget_packages.rb b/lib/api/nuget_packages.rb index 87290cd07d9..f84a3acbe6d 100644 --- a/lib/api/nuget_packages.rb +++ b/lib/api/nuget_packages.rb @@ -105,7 +105,7 @@ module API package_file = ::Packages::CreatePackageFileService.new(package, file_params) .execute - track_event('push_package') + package_event('push_package') ::Packages::Nuget::ExtractionWorker.perform_async(package_file.id) # rubocop:disable CodeReuse/Worker @@ -198,7 +198,7 @@ module API not_found!('Package') unless package_file - track_event('pull_package') + package_event('pull_package') # nuget and dotnet don't support 302 Moved status codes, supports_direct_download has to be set to false present_carrierwave_file!(package_file.file, supports_direct_download: false) @@ -233,7 +233,7 @@ module API .new(authorized_user_project, params[:q], search_options) .execute - track_event('search_package') + package_event('search_package') present ::Packages::Nuget::SearchResultsPresenter.new(search), with: ::API::Entities::Nuget::SearchResults diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index 05765d1e6ac..f6e87fece89 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -93,8 +93,7 @@ module API desc: 'The visibility of the snippet' use :update_file_params - - at_least_one_of :title, :file_name, :content, :files, :visibility + use :minimum_update_params end # rubocop: disable CodeReuse/ActiveRecord put ":id/snippets/:snippet_id" do diff --git a/lib/api/pypi_packages.rb b/lib/api/pypi_packages.rb index b2528ceae94..c07db68f8a8 100644 --- a/lib/api/pypi_packages.rb +++ b/lib/api/pypi_packages.rb @@ -72,7 +72,7 @@ module API package = packages_finder(project).by_file_name_and_sha256(filename, params[:sha256]) package_file = ::Packages::PackageFileFinder.new(package, filename, with_file_name_like: false).execute - track_event('pull_package') + package_event('pull_package') present_carrierwave_file!(package_file.file, supports_direct_download: true) end @@ -91,7 +91,7 @@ module API get 'simple/*package_name', format: :txt do authorize_read_package!(authorized_user_project) - track_event('list_package') + package_event('list_package') packages = find_package_versions presenter = ::Packages::Pypi::PackagePresenter.new(packages, authorized_user_project) @@ -122,7 +122,7 @@ module API authorize_upload!(authorized_user_project) bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?(:pypi_max_file_size, params[:content].size) - track_event('push_package') + package_event('push_package') ::Packages::Pypi::CreatePackageService .new(authorized_user_project, current_user, declared_params) diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb index b6914d571dc..c6ef35875fc 100644 --- a/lib/api/snippets.rb +++ b/lib/api/snippets.rb @@ -106,8 +106,7 @@ module API desc: 'The visibility of the snippet' use :update_file_params - - at_least_one_of :title, :file_name, :content, :files, :visibility + use :minimum_update_params end put ':id' do snippet = snippets_for_current_user.find_by_id(params.delete(:id)) diff --git a/lib/gitlab/auth/ldap/person.rb b/lib/gitlab/auth/ldap/person.rb index 88b93776bec..0042a747ff4 100644 --- a/lib/gitlab/auth/ldap/person.rb +++ b/lib/gitlab/auth/ldap/person.rb @@ -45,7 +45,7 @@ module Gitlab def self.normalize_dn(dn) ::Gitlab::Auth::Ldap::DN.new(dn).to_normalized_s rescue ::Gitlab::Auth::Ldap::DN::FormatError => e - Gitlab::AppLogger.info("Returning original DN \"#{dn}\" due to error during normalization attempt: #{e.message}") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.info("Returning original DN \"#{dn}\" due to error during normalization attempt: #{e.message}") dn end diff --git a/lib/gitlab/ci/trace/metrics.rb b/lib/gitlab/ci/trace/metrics.rb new file mode 100644 index 00000000000..7351baf5fcc --- /dev/null +++ b/lib/gitlab/ci/trace/metrics.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Trace + class Metrics + extend Gitlab::Utils::StrongMemoize + + OPERATIONS = [:mutated].freeze + + def increment_trace_operation(operation: :unknown) + unless OPERATIONS.include?(operation) + raise ArgumentError, 'unknown trace operation' + end + + self.class.trace_operations.increment(operation: operation) + end + + def self.trace_operations + strong_memoize(:trace_operations) do + name = :gitlab_ci_trace_operations_total + comment = 'Total amount of different operations on a build trace' + + Gitlab::Metrics.counter(name, comment) + end + end + end + end + end +end diff --git a/lib/gitlab/reference_counter.rb b/lib/gitlab/reference_counter.rb index 5fdfa5e75ed..c2fa2e1330a 100644 --- a/lib/gitlab/reference_counter.rb +++ b/lib/gitlab/reference_counter.rb @@ -51,10 +51,8 @@ module Gitlab redis_cmd do |redis| current_value = redis.decr(key) if current_value < 0 - # rubocop:disable Gitlab/RailsLogger - Rails.logger.warn("Reference counter for #{gl_repository} decreased" \ + Gitlab::AppLogger.warn("Reference counter for #{gl_repository} decreased" \ " when its value was less than 1. Resetting the counter.") - # rubocop:enable Gitlab/RailsLogger redis.del(key) end end @@ -87,7 +85,7 @@ module Gitlab true rescue => e - Rails.logger.warn("GitLab: An unexpected error occurred in writing to Redis: #{e}") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.warn("GitLab: An unexpected error occurred in writing to Redis: #{e}") false end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index dc0337c7ecd..6a9ec5a011b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1105,6 +1105,9 @@ msgstr "" msgid ":%{startLine} to %{endLine}" msgstr "" +msgid "A %{incident_docs_start}modified issue%{incident_docs_end} to guide the resolution of incidents." +msgstr "" + msgid "A 'Runner' is a process which runs a job. You can set up as many Runners as you need." msgstr "" @@ -3268,6 +3271,11 @@ msgstr "" msgid "Are you sure you want to erase this build?" msgstr "" +msgid "Are you sure you want to import %d repository?" +msgid_plural "Are you sure you want to import %d repositories?" +msgstr[0] "" +msgstr[1] "" + msgid "Are you sure you want to lose unsaved changes?" msgstr "" @@ -10971,7 +10979,7 @@ msgstr "" msgid "Filter results..." msgstr "" -msgid "Filter your projects by name" +msgid "Filter your repositories by name" msgstr "" msgid "Filter..." @@ -13018,6 +13026,16 @@ msgstr "" msgid "Import" msgstr "" +msgid "Import %d compatible repository" +msgid_plural "Import %d compatible repositories" +msgstr[0] "" +msgstr[1] "" + +msgid "Import %d repository" +msgid_plural "Import %d repositories" +msgstr[0] "" +msgstr[1] "" + msgid "Import CSV" msgstr "" @@ -13027,15 +13045,9 @@ msgstr "" msgid "Import all compatible projects" msgstr "" -msgid "Import all compatible repositories" -msgstr "" - msgid "Import all projects" msgstr "" -msgid "Import all repositories" -msgstr "" - msgid "Import an exported GitLab project" msgstr "" @@ -13123,6 +13135,9 @@ msgstr "" msgid "ImportProjects|Error importing repository %{project_safe_import_url} into %{project_full_path} - %{message}" msgstr "" +msgid "ImportProjects|Import repositories" +msgstr "" + msgid "ImportProjects|Importing the project failed" msgstr "" @@ -13135,7 +13150,7 @@ msgstr "" msgid "ImportProjects|Requesting your %{provider} repositories failed" msgstr "" -msgid "ImportProjects|Select the projects you want to import" +msgid "ImportProjects|Select the repositories you want to import" msgstr "" msgid "ImportProjects|The remote data could not be imported." diff --git a/package.json b/package.json index 508c6732eed..0f0b08e8d5b 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@babel/preset-env": "^7.10.1", "@gitlab/at.js": "1.5.5", "@gitlab/svgs": "1.164.0", - "@gitlab/ui": "20.18.3", + "@gitlab/ui": "20.19.0", "@gitlab/visual-review-tools": "1.6.1", "@rails/actioncable": "^6.0.3-1", "@sentry/browser": "^5.22.3", diff --git a/spec/features/import/manifest_import_spec.rb b/spec/features/import/manifest_import_spec.rb index 9c359e932d5..cfd0c7e210f 100644 --- a/spec/features/import/manifest_import_spec.rb +++ b/spec/features/import/manifest_import_spec.rb @@ -20,7 +20,7 @@ RSpec.describe 'Import multiple repositories by uploading a manifest file', :js attach_file('manifest', Rails.root.join('spec/fixtures/aosp_manifest.xml')) click_on 'List available repositories' - expect(page).to have_button('Import all repositories') + expect(page).to have_button('Import 660 repositories') expect(page).to have_content('https://android-review.googlesource.com/platform/build/blueprint') end diff --git a/spec/features/issues/markdown_toolbar_spec.rb b/spec/features/issues/markdown_toolbar_spec.rb index aab9d1026f9..6dc1cbfb2d7 100644 --- a/spec/features/issues/markdown_toolbar_spec.rb +++ b/spec/features/issues/markdown_toolbar_spec.rb @@ -36,6 +36,6 @@ RSpec.describe 'Issue markdown toolbar', :js do all('.toolbar-btn')[1].click - expect(find('#note-body')[:value]).to eq("test\n*underline*\n") + expect(find('#note-body')[:value]).to eq("test\n_underline_\n") end end diff --git a/spec/features/issues/user_creates_issue_spec.rb b/spec/features/issues/user_creates_issue_spec.rb index fe773685373..c00ba558a7b 100644 --- a/spec/features/issues/user_creates_issue_spec.rb +++ b/spec/features/issues/user_creates_issue_spec.rb @@ -196,7 +196,7 @@ RSpec.describe "User creates issue" do end it 'pre-fills the issue type dropdown with issue type' do - expect(find('.js-issuable-type-filter-dropdown-wrap .dropdown-label')).to have_content('Issue') + expect(find('.js-issuable-type-filter-dropdown-wrap .dropdown-toggle-text')).to have_content('Issue') end it 'does not hide the milestone select' do @@ -208,11 +208,11 @@ RSpec.describe "User creates issue" do let(:project) { create(:project) } before do - visit new_project_issue_path(project, { 'issue[issue_type]': 'incident', issuable_template: 'incident' }) + visit new_project_issue_path(project, { issuable_template: 'incident', issue: { issue_type: 'incident' } }) end it 'pre-fills the issue type dropdown with incident type' do - expect(find('.js-issuable-type-filter-dropdown-wrap .dropdown-label')).to have_content('Incident') + expect(find('.js-issuable-type-filter-dropdown-wrap .dropdown-toggle-text')).to have_content('Incident') end it 'hides the epic select' do @@ -226,6 +226,10 @@ RSpec.describe "User creates issue" do it 'hides the weight input' do expect(page).not_to have_selector('.qa-issuable-weight-input') end + + it 'shows the incident help text' do + expect(page).to have_text('A modified issue to guide the resolution of incidents.') + end end context 'suggestions', :js do diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js index 393f042a9f9..e1d855ae0cf 100644 --- a/spec/frontend/diffs/store/mutations_spec.js +++ b/spec/frontend/diffs/store/mutations_spec.js @@ -645,6 +645,36 @@ describe('DiffsStoreMutations', () => { expect(state.diffFiles[0].highlighted_diff_lines[0].discussions).toHaveLength(1); expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toBe(1); }); + + it('should add discussion to file', () => { + const state = { + latestDiff: true, + diffFiles: [ + { + file_hash: 'ABC', + discussions: [], + parallel_diff_lines: [], + highlighted_diff_lines: [], + }, + ], + }; + const discussion = { + id: 1, + line_code: 'ABC_1', + diff_discussion: true, + resolvable: true, + diff_file: { + file_hash: state.diffFiles[0].file_hash, + }, + }; + + mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { + discussion, + diffPositionByLineCode: null, + }); + + expect(state.diffFiles[0].discussions.length).toEqual(1); + }); }); describe('REMOVE_LINE_DISCUSSIONS', () => { diff --git a/spec/frontend/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_projects/components/import_projects_table_spec.js index 83d27635af9..bcb5a3a2231 100644 --- a/spec/frontend/import_projects/components/import_projects_table_spec.js +++ b/spec/frontend/import_projects/components/import_projects_table_spec.js @@ -16,15 +16,24 @@ describe('ImportProjectsTable', () => { wrapper.find('input[data-qa-selector="githubish_import_filter_field"]'); const providerTitle = 'THE PROVIDER'; - const providerRepo = { id: 10, sanitizedName: 'sanitizedName', fullName: 'fullName' }; + const providerRepo = { + importSource: { + id: 10, + sanitizedName: 'sanitizedName', + fullName: 'fullName', + }, + importedProject: null, + }; const findImportAllButton = () => wrapper .findAll(GlButton) .filter(w => w.props().variant === 'success') .at(0); + const findImportAllModal = () => wrapper.find({ ref: 'importAllModal' }); const importAllFn = jest.fn(); + const importAllModalShowFn = jest.fn(); const setPageFn = jest.fn(); function createComponent({ @@ -64,6 +73,9 @@ describe('ImportProjectsTable', () => { paginatable, }, slots, + stubs: { + GlModal: { template: '<div>Modal!</div>', methods: { show: importAllModalShowFn } }, + }, }); } @@ -110,18 +122,21 @@ describe('ImportProjectsTable', () => { }); it.each` - hasIncompatibleRepos | buttonText - ${false} | ${'Import all repositories'} - ${true} | ${'Import all compatible repositories'} + hasIncompatibleRepos | count | buttonText + ${false} | ${1} | ${'Import 1 repository'} + ${true} | ${1} | ${'Import 1 compatible repository'} + ${false} | ${5} | ${'Import 5 repositories'} + ${true} | ${5} | ${'Import 5 compatible repositories'} `( - 'import all button has "$buttonText" text when hasIncompatibleRepos is $hasIncompatibleRepos', - ({ hasIncompatibleRepos, buttonText }) => { + 'import all button has "$buttonText" text when hasIncompatibleRepos is $hasIncompatibleRepos and repos count is $count', + ({ hasIncompatibleRepos, buttonText, count }) => { createComponent({ state: { providerRepos: [providerRepo], }, getters: { hasIncompatibleRepos: () => hasIncompatibleRepos, + importAllCount: () => count, }, }); @@ -129,19 +144,28 @@ describe('ImportProjectsTable', () => { }, ); - it('renders an empty state if there are no projects available', () => { + it('renders an empty state if there are no repositories available', () => { createComponent({ state: { repositories: [] } }); expect(wrapper.find(ProviderRepoTableRow).exists()).toBe(false); expect(wrapper.text()).toContain(`No ${providerTitle} repositories found`); }); - it('sends importAll event when import button is clicked', async () => { - createComponent({ state: { providerRepos: [providerRepo] } }); + it('opens confirmation modal when import all button is clicked', async () => { + createComponent({ state: { repositories: [providerRepo] } }); findImportAllButton().vm.$emit('click'); await nextTick(); + expect(importAllModalShowFn).toHaveBeenCalled(); + }); + + it('triggers importAll action when modal is confirmed', async () => { + createComponent({ state: { providerRepos: [providerRepo] } }); + + findImportAllModal().vm.$emit('ok'); + await nextTick(); + expect(importAllFn).toHaveBeenCalled(); }); diff --git a/spec/frontend/import_projects/store/getters_spec.js b/spec/frontend/import_projects/store/getters_spec.js index 5b2a8ea06f0..1ce42e534ea 100644 --- a/spec/frontend/import_projects/store/getters_spec.js +++ b/spec/frontend/import_projects/store/getters_spec.js @@ -3,6 +3,7 @@ import { isImportingAnyRepo, hasIncompatibleRepos, hasImportableRepos, + importAllCount, getImportTarget, } from '~/import_projects/store/getters'; import { STATUSES } from '~/import_projects/constants'; @@ -97,6 +98,19 @@ describe('import_projects store getters', () => { }); }); + describe('importAllCount', () => { + it('returns count of available importable projects ', () => { + localState.repositories = [ + IMPORTABLE_REPO, + IMPORTABLE_REPO, + IMPORTED_REPO, + INCOMPATIBLE_REPO, + ]; + + expect(importAllCount(localState)).toBe(2); + }); + }); + describe('getImportTarget', () => { it('returns default value if no custom target available', () => { localState.defaultTargetNamespace = 'default'; diff --git a/spec/graphql/mutations/issues/set_severity_spec.rb b/spec/graphql/mutations/issues/set_severity_spec.rb new file mode 100644 index 00000000000..ed73d3b777e --- /dev/null +++ b/spec/graphql/mutations/issues/set_severity_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Issues::SetSeverity do + let_it_be(:user) { create(:user) } + let_it_be(:issue) { create(:incident) } + let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) } + + specify { expect(described_class).to require_graphql_authorizations(:update_issue) } + + describe '#resolve' do + let(:severity) { 'CRITICAL' } + let(:mutated_incident) { subject[:issue] } + + subject(:resolve) { mutation.resolve(project_path: issue.project.full_path, iid: issue.iid, severity: severity) } + + context 'when the user cannot update the issue' do + it 'raises an error' do + expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when the user can update the issue' do + before do + issue.project.add_developer(user) + end + + context 'when issue type is incident' do + context 'when severity has a correct value' do + it 'updates severity' do + expect(resolve[:issue].severity).to eq('critical') + end + + it 'returns no errors' do + expect(resolve[:errors]).to be_empty + end + end + + context 'when severity has an unsuported value' do + let(:severity) { 'unsupported-severity' } + + it 'sets severity to default' do + expect(resolve[:issue].severity).to eq(IssuableSeverity::DEFAULT) + end + + it 'returns no errorsr' do + expect(resolve[:errors]).to be_empty + end + end + end + + context 'when issue type is not incident' do + let!(:issue) { create(:issue) } + + it 'does not updates the issue' do + expect { resolve }.not_to change { issue.updated_at } + end + end + end + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index de538c6c263..3ad3f610128 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -29,6 +29,7 @@ issues: - merge_requests_closing_issues - metrics - timelogs +- issuable_severity - issue_assignees - closed_by - epic_issue @@ -542,6 +543,8 @@ timelogs: - note push_event_payload: - event +issuable_severity: +- issue issue_assignees: - issue - assignee diff --git a/spec/lib/gitlab/reference_counter_spec.rb b/spec/lib/gitlab/reference_counter_spec.rb index 0d0ac75ee22..83e4006c69b 100644 --- a/spec/lib/gitlab/reference_counter_spec.rb +++ b/spec/lib/gitlab/reference_counter_spec.rb @@ -21,7 +21,7 @@ RSpec.describe Gitlab::ReferenceCounter, :clean_gitlab_redis_shared_state do end it 'warns if attempting to decrease a counter with a value of zero or less, and resets the counter' do - expect(Rails.logger).to receive(:warn).with("Reference counter for project-1" \ + expect(Gitlab::AppLogger).to receive(:warn).with("Reference counter for project-1" \ " decreased when its value was less than 1. Resetting the counter.") expect { reference_counter.decrease }.not_to change { reference_counter.value } end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 6d775e805d9..2a422ddc9c0 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1052,18 +1052,53 @@ RSpec.describe Ci::Build do end describe '#hide_secrets' do + let(:metrics) { spy('metrics') } let(:subject) { build.hide_secrets(data) } context 'hide runners token' do let(:data) { "new #{project.runners_token} data"} it { is_expected.to match(/^new x+ data$/) } + + it 'increments trace mutation metric' do + build.hide_secrets(data, metrics) + + expect(metrics) + .to have_received(:increment_trace_operation) + .with(operation: :mutated) + end end context 'hide build token' do let(:data) { "new #{build.token} data"} it { is_expected.to match(/^new x+ data$/) } + + it 'increments trace mutation metric' do + build.hide_secrets(data, metrics) + + expect(metrics) + .to have_received(:increment_trace_operation) + .with(operation: :mutated) + end + end + + context 'when build does not include secrets' do + let(:data) { 'my build log' } + + it 'does not mutate trace' do + trace = build.hide_secrets(data) + + expect(trace).to eq data + end + + it 'does not increment trace mutation metric' do + build.hide_secrets(data, metrics) + + expect(metrics) + .not_to have_received(:increment_trace_operation) + .with(operation: :mutated) + end end end diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index 0518b2c7540..779839df670 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -20,7 +20,9 @@ RSpec.describe Ci::JobArtifact do it_behaves_like 'having unique enum values' it_behaves_like 'UpdateProjectStatistics' do - subject { build(:ci_job_artifact, :archive, size: 107464) } + let_it_be(:job, reload: true) { create(:ci_build) } + + subject { build(:ci_job_artifact, :archive, job: job, size: 107464) } end describe '.not_expired' do diff --git a/spec/models/ci/pipeline_artifact_spec.rb b/spec/models/ci/pipeline_artifact_spec.rb index 716ab4d8522..8cbace845a9 100644 --- a/spec/models/ci/pipeline_artifact_spec.rb +++ b/spec/models/ci/pipeline_artifact_spec.rb @@ -13,7 +13,9 @@ RSpec.describe Ci::PipelineArtifact, type: :model do it_behaves_like 'having unique enum values' it_behaves_like 'UpdateProjectStatistics' do - subject { build(:ci_pipeline_artifact) } + let_it_be(:pipeline, reload: true) { create(:ci_pipeline) } + + subject { build(:ci_pipeline_artifact, pipeline: pipeline) } end describe 'validations' do diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index b00395cd926..5ed9eb6252a 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -891,4 +891,58 @@ RSpec.describe Issuable do end end end + + describe '#update_severity' do + let(:severity) { 'low' } + + subject(:update_severity) { issuable.update_severity(severity) } + + context 'when issuable not an incident' do + %i(issue merge_request).each do |issuable_type| + let(:issuable) { build_stubbed(issuable_type) } + + it { is_expected.to be_nil } + + it 'does not set severity' do + expect { subject }.not_to change(IssuableSeverity, :count) + end + end + end + + context 'when issuable is an incident' do + let!(:issuable) { create(:incident) } + + context 'when issuable does not have issuable severity yet' do + it 'creates new record' do + expect { update_severity }.to change { IssuableSeverity.where(issue: issuable).count }.to(1) + end + + it 'sets severity to specified value' do + expect { update_severity }.to change { issuable.severity }.to('low') + end + end + + context 'when issuable has an issuable severity' do + let!(:issuable_severity) { create(:issuable_severity, issue: issuable, severity: 'medium') } + + it 'does not create new record' do + expect { update_severity }.not_to change(IssuableSeverity, :count) + end + + it 'updates existing issuable severity' do + expect { update_severity }.to change { issuable_severity.severity }.to(severity) + end + end + + context 'when severity value is unsupported' do + let(:severity) { 'unsupported-severity' } + + it 'sets the severity to default value' do + update_severity + + expect(issuable.issuable_severity.severity).to eq(IssuableSeverity::DEFAULT) + end + end + end + end end diff --git a/spec/models/packages/package_file_spec.rb b/spec/models/packages/package_file_spec.rb index 7cc8e13d449..6b992dbc2a5 100644 --- a/spec/models/packages/package_file_spec.rb +++ b/spec/models/packages/package_file_spec.rb @@ -33,15 +33,17 @@ RSpec.describe Packages::PackageFile, type: :model do end context 'updating project statistics' do + let_it_be(:package, reload: true) { create(:package) } + context 'when the package file has an explicit size' do it_behaves_like 'UpdateProjectStatistics' do - subject { build(:package_file, :jar, size: 42) } + subject { build(:package_file, :jar, package: package, size: 42) } end end context 'when the package file does not have a size' do it_behaves_like 'UpdateProjectStatistics' do - subject { build(:package_file, size: nil) } + subject { build(:package_file, package: package, size: nil) } end end end diff --git a/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb b/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb new file mode 100644 index 00000000000..96fd2368765 --- /dev/null +++ b/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Setting severity level of an incident' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let(:incident) { create(:incident) } + let(:project) { incident.project } + let(:input) { { severity: 'CRITICAL' } } + + let(:mutation) do + variables = { + project_path: project.full_path, + iid: incident.iid.to_s + } + + graphql_mutation(:issue_set_severity, variables.merge(input), + <<-QL.strip_heredoc + clientMutationId + errors + issue { + iid + severity + } + QL + ) + end + + def mutation_response + graphql_mutation_response(:issue_set_severity) + end + + context 'when the user is not allowed to update the incident' do + it 'returns an error' do + error = "The resource that you are attempting to access does not exist or you don't have permission to perform this action" + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(graphql_errors).to include(a_hash_including('message' => error)) + end + end + + context 'when the user is allowed to update the incident' do + before do + project.add_developer(user) + end + + it 'updates the issue' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response.dig('issue', 'severity')).to eq('CRITICAL') + end + end +end diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 0746dee3e51..08c88873078 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -306,15 +306,9 @@ RSpec.describe API::ProjectSnippets do it_behaves_like 'snippet file updates' it_behaves_like 'snippet non-file updates' + it_behaves_like 'snippet individual non-file updates' it_behaves_like 'invalid snippet updates' - it 'updates snippet with visibility parameter' do - expect { update_snippet(params: { visibility: 'private' }) } - .to change { snippet.reload.visibility } - - expect(snippet.visibility).to eq('private') - end - it_behaves_like 'update with repository actions' do let(:snippet_without_repo) { create(:project_snippet, author: admin, project: project, visibility_level: visibility_level) } end diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index 5bba308a2d3..8d77026d26c 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -393,6 +393,7 @@ RSpec.describe API::Snippets do it_behaves_like 'snippet file updates' it_behaves_like 'snippet non-file updates' + it_behaves_like 'snippet individual non-file updates' it_behaves_like 'invalid snippet updates' it "returns 404 for another user's snippet" do diff --git a/spec/serializers/test_case_entity_spec.rb b/spec/serializers/test_case_entity_spec.rb index bd2a1b0fb98..32e9562f4c1 100644 --- a/spec/serializers/test_case_entity_spec.rb +++ b/spec/serializers/test_case_entity_spec.rb @@ -5,6 +5,8 @@ require 'spec_helper' RSpec.describe TestCaseEntity do include TestReportsHelper + let_it_be(:job) { create(:ci_build) } + let(:entity) { described_class.new(test_case) } describe '#as_json' do @@ -38,7 +40,7 @@ RSpec.describe TestCaseEntity do end context 'when attachment is present' do - let(:test_case) { build(:test_case, :failed_with_attachment) } + let(:test_case) { build(:test_case, :failed_with_attachment, job: job) } it 'returns the attachment_url' do expect(subject).to include(:attachment_url) @@ -46,7 +48,7 @@ RSpec.describe TestCaseEntity do end context 'when attachment is not present' do - let(:test_case) { build(:test_case) } + let(:test_case) { build(:test_case, job: job) } it 'returns a nil attachment_url' do expect(subject[:attachment_url]).to be_nil @@ -60,7 +62,7 @@ RSpec.describe TestCaseEntity do end context 'when attachment is present' do - let(:test_case) { build(:test_case, :failed_with_attachment) } + let(:test_case) { build(:test_case, :failed_with_attachment, job: job) } it 'returns no attachment_url' do expect(subject).not_to include(:attachment_url) @@ -68,7 +70,7 @@ RSpec.describe TestCaseEntity do end context 'when attachment is not present' do - let(:test_case) { build(:test_case) } + let(:test_case) { build(:test_case, job: job) } it 'returns no attachment_url' do expect(subject).not_to include(:attachment_url) diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index b5811077acf..0c28ceb7466 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -52,7 +52,8 @@ RSpec.describe Issues::UpdateService, :mailer do state_event: 'close', label_ids: [label.id], due_date: Date.tomorrow, - discussion_locked: true + discussion_locked: true, + severity: 'low' } end @@ -71,6 +72,24 @@ RSpec.describe Issues::UpdateService, :mailer do expect(issue.discussion_locked).to be_truthy end + context 'when issue type is not incident' do + it 'returns default severity' do + update_issue(opts) + + expect(issue.severity).to eq(IssuableSeverity::DEFAULT) + end + end + + context 'when issue type is incident' do + let(:issue) { create(:incident, project: project) } + + it 'changes updates the severity' do + update_issue(opts) + + expect(issue.severity).to eq('low') + end + end + it 'refreshes the number of open issues when the issue is made confidential', :use_clean_rails_memory_store_caching do issue # make sure the issue is created first so our counts are correct. diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index 05be3c1946b..d042b318d02 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -7,7 +7,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do let(:project) { create(:project, :repository) } let(:user) { create(:user) } - let(:assignee) { create(:user) } + let(:user2) { create(:user) } describe '#execute' do context 'valid params' do @@ -26,7 +26,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do before do project.add_maintainer(user) - project.add_developer(assignee) + project.add_developer(user2) allow(service).to receive(:execute_hooks) end @@ -75,7 +75,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do description: "well this is not done yet\n/wip", source_branch: 'feature', target_branch: 'master', - assignees: [assignee] + assignees: [user2] } end @@ -91,7 +91,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do description: "well this is not done yet\n/wip", source_branch: 'feature', target_branch: 'master', - assignees: [assignee] + assignees: [user2] } end @@ -108,17 +108,17 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do description: 'please fix', source_branch: 'feature', target_branch: 'master', - assignees: [assignee] + assignees: [user2] } end - it { expect(merge_request.assignees).to eq([assignee]) } + it { expect(merge_request.assignees).to eq([user2]) } it 'creates a todo for new assignee' do attributes = { project: project, author: user, - user: assignee, + user: user2, target_id: merge_request.id, target_type: merge_request.class.name, action: Todo::ASSIGNED, @@ -129,6 +129,34 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do end end + context 'when reviewer is assigned' do + let(:opts) do + { + title: 'Awesome merge_request', + description: 'please fix', + source_branch: 'feature', + target_branch: 'master', + reviewers: [user2] + } + end + + it { expect(merge_request.reviewers).to eq([user2]) } + + it 'creates a todo for new reviewer' do + attributes = { + project: project, + author: user, + user: user2, + target_id: merge_request.id, + target_type: merge_request.class.name, + action: Todo::REVIEW_REQUESTED, + state: :pending + } + + expect(Todo.where(attributes).count).to eq 1 + end + end + context 'when head pipelines already exist for merge request source branch', :sidekiq_inline do let(:shas) { project.repository.commits(opts[:source_branch], limit: 2).map(&:id) } let!(:pipeline_1) { create(:ci_pipeline, project: project, ref: opts[:source_branch], project_id: project.id, sha: shas[1]) } @@ -213,7 +241,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do before do stub_feature_flags(ci_disallow_to_create_merge_request_pipelines_in_target_project: false) - target_project.add_developer(assignee) + target_project.add_developer(user2) target_project.add_maintainer(user) end @@ -366,7 +394,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do assignee_ids: create(:user).id, milestone_id: 1, title: 'Title', - description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}"), + description: %(/assign @#{user2.username}\n/milestone %"#{milestone.name}"), source_branch: 'feature', target_branch: 'master' } @@ -374,12 +402,12 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do before do project.add_maintainer(user) - project.add_maintainer(assignee) + project.add_maintainer(user2) end it 'assigns and sets milestone to issuable from command' do expect(merge_request).to be_persisted - expect(merge_request.assignees).to eq([assignee]) + expect(merge_request.assignees).to eq([user2]) expect(merge_request.milestone).to eq(milestone) end end @@ -387,7 +415,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do context 'merge request create service' do context 'asssignee_id' do - let(:assignee) { create(:user) } + let(:user2) { create(:user) } before do project.add_maintainer(user) @@ -410,12 +438,12 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do end it 'saves assignee when user id is valid' do - project.add_maintainer(assignee) - opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] } + project.add_maintainer(user2) + opts = { title: 'Title', description: 'Description', assignee_ids: [user2.id] } merge_request = described_class.new(project, user, opts).execute - expect(merge_request.assignees).to eq([assignee]) + expect(merge_request.assignees).to eq([user2]) end context 'when assignee is set' do @@ -423,18 +451,18 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do { title: 'Title', description: 'Description', - assignee_ids: [assignee.id], + assignee_ids: [user2.id], source_branch: 'feature', target_branch: 'master' } end it 'invalidates open merge request counter for assignees when merge request is assigned' do - project.add_maintainer(assignee) + project.add_maintainer(user2) described_class.new(project, user, opts).execute - expect(assignee.assigned_open_merge_requests_count).to eq 1 + expect(user2.assigned_open_merge_requests_count).to eq 1 end end @@ -449,7 +477,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do levels.each do |level| it "removes not authorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do project.update!(visibility_level: level) - opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] } + opts = { title: 'Title', description: 'Description', assignee_ids: [user2.id] } merge_request = described_class.new(project, user, opts).execute @@ -475,7 +503,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do before do project.add_maintainer(user) - project.add_developer(assignee) + project.add_developer(user2) end it 'creates a `MergeRequestsClosingIssues` record for each issue' do @@ -503,7 +531,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do context 'when user can not access source project' do before do - target_project.add_developer(assignee) + target_project.add_developer(user2) target_project.add_maintainer(user) end @@ -515,7 +543,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do context 'when user can not access target project' do before do - target_project.add_developer(assignee) + target_project.add_developer(user2) target_project.add_maintainer(user) end @@ -567,7 +595,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do end before do - project.add_developer(assignee) + project.add_developer(user2) project.add_maintainer(user) end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 5de1aaa65f6..6b7463d4996 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -52,6 +52,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do title: 'New title', description: 'Also please fix', assignee_ids: [user.id], + reviewer_ids: [user.id], state_event: 'close', label_ids: [label.id], target_branch: 'target', @@ -75,6 +76,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do expect(@merge_request).to be_valid expect(@merge_request.title).to eq('New title') expect(@merge_request.assignees).to match_array([user]) + expect(@merge_request.reviewers).to match_array([user]) expect(@merge_request).to be_closed expect(@merge_request.labels.count).to eq(1) expect(@merge_request.labels.first.title).to eq(label.name) @@ -402,6 +404,30 @@ RSpec.describe MergeRequests::UpdateService, :mailer do end end + context 'when reviewers gets changed' do + before do + update_merge_request({ reviewer_ids: [user2.id] }) + end + + it 'marks pending todo as done' do + expect(pending_todo.reload).to be_done + end + + it 'creates a pending todo for new review request' do + attributes = { + project: project, + author: user, + user: user2, + target_id: merge_request.id, + target_type: merge_request.class.name, + action: Todo::REVIEW_REQUESTED, + state: :pending + } + + expect(Todo.where(attributes).count).to eq 1 + end + end + context 'when the milestone is removed' do let!(:non_subscriber) { create(:user) } diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 94d4b61933d..86a428bca92 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -65,6 +65,40 @@ RSpec.describe TodoService do end end + shared_examples 'reassigned reviewable target' do + context 'with no existing reviewers' do + let(:assigned_reviewers) { [] } + + it 'creates a pending todo for new reviewer' do + target.reviewers = [john_doe] + service.send(described_method, target, author) + + should_create_todo(user: john_doe, target: target, action: Todo::REVIEW_REQUESTED) + end + end + + context 'with an existing reviewer' do + let(:assigned_reviewers) { [john_doe] } + + it 'does not create a todo if unassigned' do + target.reviewers = [] + + should_not_create_any_todo { service.send(described_method, target, author) } + end + + it 'creates a todo if new reviewer is the current user' do + target.reviewers = [john_doe] + service.send(described_method, target, john_doe) + + should_create_todo(user: john_doe, target: target, author: john_doe, action: Todo::REVIEW_REQUESTED) + end + + it 'does not create a todo if already assigned' do + should_not_create_any_todo { service.send(described_method, target, author, [john_doe]) } + end + end + end + describe 'Issues' do let(:issue) { create(:issue, project: project, assignees: [john_doe], author: author, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") } let(:addressed_issue) { create(:issue, project: project, assignees: [john_doe], author: author, description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") } @@ -605,6 +639,17 @@ RSpec.describe TodoService do end end + describe '#reassigned_reviewable' do + let(:described_method) { :reassigned_reviewable } + + context 'reviewable is a merge request' do + it_behaves_like 'reassigned reviewable target' do + let(:assigned_reviewers) { [] } + let(:target) { create(:merge_request, source_project: project, author: author, reviewers: assigned_reviewers) } + end + end + end + describe 'Merge Requests' do let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignees: [john_doe], description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") } let(:addressed_mr_assigned) { create(:merge_request, source_project: project, author: author, assignees: [john_doe], description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") } diff --git a/spec/support/shared_examples/requests/api/snippets_shared_examples.rb b/spec/support/shared_examples/requests/api/snippets_shared_examples.rb index 97f85977a20..051367fbe96 100644 --- a/spec/support/shared_examples/requests/api/snippets_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/snippets_shared_examples.rb @@ -170,6 +170,25 @@ RSpec.shared_examples 'snippet non-file updates' do end end +RSpec.shared_examples 'snippet individual non-file updates' do + using RSpec::Parameterized::TableSyntax + + where(:attribute, :updated_value) do + :description | 'new description' + :title | 'new title' + :visibility | 'private' + end + + with_them do + it 'updates the attribute' do + params = { attribute => updated_value } + + expect { update_snippet(params: params) } + .to change { snippet.reload.send(attribute) }.to(updated_value) + end + end +end + RSpec.shared_examples 'invalid snippet updates' do it 'returns 404 for invalid snippet id' do update_snippet(snippet_id: non_existing_record_id, params: { title: 'foo' }) diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb index 49add434ab5..b998023b40e 100644 --- a/spec/views/projects/pipelines/show.html.haml_spec.rb +++ b/spec/views/projects/pipelines/show.html.haml_spec.rb @@ -16,24 +16,6 @@ RSpec.describe 'projects/pipelines/show' do stub_feature_flags(new_pipeline_form: false) end - shared_examples 'pipeline with warning messages' do - let(:warning_messages) do - [double(content: 'warning 1'), double(content: 'warning 2')] - end - - before do - allow(pipeline).to receive(:warning_messages).and_return(warning_messages) - end - - it 'displays the warnings' do - render - - expect(rendered).to have_css('.bs-callout-warning') - expect(rendered).to have_content('warning 1') - expect(rendered).to have_content('warning 2') - end - end - context 'when pipeline has errors' do before do allow(pipeline).to receive(:yaml_errors).and_return('some errors') @@ -51,10 +33,6 @@ RSpec.describe 'projects/pipelines/show' do expect(rendered).not_to have_css('ul.pipelines-tabs') end - - context 'when pipeline has also warnings' do - it_behaves_like 'pipeline with warning messages' - end end context 'when pipeline is valid' do @@ -69,9 +47,5 @@ RSpec.describe 'projects/pipelines/show' do expect(rendered).to have_css('ul.pipelines-tabs') end - - context 'when pipeline has warnings' do - it_behaves_like 'pipeline with warning messages' - end end end diff --git a/yarn.lock b/yarn.lock index 916f597f40e..5e01d222b15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -848,10 +848,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.164.0.tgz#6cefad871c45f945ef92b99015d0f510b1d2de4a" integrity sha512-a9e/cYUc1QQk7azjH4x/m6/p3icavwGEi5F9ipNlDqiJtUor5tqojxvMxPOhuVbN/mTwnC6lGsSZg4tqTsdJAQ== -"@gitlab/ui@20.18.3": - version "20.18.3" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-20.18.3.tgz#14b1d6571756fbef7966d6658f56ca213ded15e8" - integrity sha512-yeXc40J8ifXm1aApuNNjEOZraDexYAyKZNRa05rLPXPppLnwZtx4Io5z9QiskN6fMiL648EgzC8JW576qtByCw== +"@gitlab/ui@20.19.0": + version "20.19.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-20.19.0.tgz#4f7f3ff5ffa59baf6390f7ab25f199ba27b9b84b" + integrity sha512-QXccwQNWfyCKqhRNIKZRnaE1JJR3g29hcHZoTQKKSlPVolHbqssszBOL8A4/H7TWuCFWRjswJPHFHfHeBHWccQ== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.3.0" |