diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-17 15:09:20 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-17 15:09:20 +0300 |
commit | b84eeb256c4a780d902faee1f99ca9a711b3214a (patch) | |
tree | 32918aadbea9210eace50efbce9afbfb8cd3ba84 /app | |
parent | 53ae6b7e3f83591ad251a3f771f5bf3b8cf087ba (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
20 files changed, 250 insertions, 68 deletions
diff --git a/app/assets/javascripts/issuables_list/components/issuable.vue b/app/assets/javascripts/issuables_list/components/issuable.vue index eb924609a8a..2fd92e009eb 100644 --- a/app/assets/javascripts/issuables_list/components/issuable.vue +++ b/app/assets/javascripts/issuables_list/components/issuable.vue @@ -3,7 +3,7 @@ * This is tightly coupled to projects/issues/_issue.html.haml, * any changes done to the haml need to be reflected here. */ -import { escape, isNumber } from 'underscore'; +import { escape, isNumber } from 'lodash'; import { GlLink, GlTooltipDirective as GlTooltip } from '@gitlab/ui'; import { dateInWords, @@ -19,8 +19,6 @@ import { mergeUrlParams } from '~/lib/utils/url_utility'; import Icon from '~/vue_shared/components/icon.vue'; import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue'; -const ISSUE_TOKEN = '#'; - export default { components: { Icon, @@ -119,8 +117,7 @@ export default { ); }, referencePath() { - // TODO: The API should return the reference path (it doesn't now) https://gitlab.com/gitlab-org/gitlab/issues/31301 - return `${ISSUE_TOKEN}${this.issuable.iid}`; + return this.issuable.references.relative; }, updatedDateString() { return formatDate(new Date(this.issuable.updated_at), 'mmm d, yyyy h:MMtt'); @@ -230,7 +227,7 @@ export default { </div> <div class="issuable-info"> - <span>{{ referencePath }}</span> + <span class="js-ref-path">{{ referencePath }}</span> <span class="d-none d-sm-inline-block mr-1"> · diff --git a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue index 82601363aa4..88d174f96ed 100644 --- a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue +++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue @@ -62,9 +62,21 @@ export default { return ( report.existing_failures.length > 0 || report.new_failures.length > 0 || - report.resolved_failures.length > 0 + report.resolved_failures.length > 0 || + report.existing_errors.length > 0 || + report.new_errors.length > 0 || + report.resolved_errors.length > 0 ); }, + unresolvedIssues(report) { + return report.existing_failures.concat(report.existing_errors); + }, + newIssues(report) { + return report.new_failures.concat(report.new_errors); + }, + resolvedIssues(report) { + return report.resolved_failures.concat(report.resolved_errors); + }, }, }; </script> @@ -87,9 +99,9 @@ export default { <issues-list v-if="shouldRenderIssuesList(report)" :key="`issues-list-${i}`" - :unresolved-issues="report.existing_failures" - :new-issues="report.new_failures" - :resolved-issues="report.resolved_failures" + :unresolved-issues="unresolvedIssues(report)" + :new-issues="newIssues(report)" + :resolved-issues="resolvedIssues(report)" :component="$options.componentNames.TestIssueBody" class="report-block-group-list" /> diff --git a/app/assets/javascripts/reports/store/mutations.js b/app/assets/javascripts/reports/store/mutations.js index 2a37f5b74fa..68f6de3a7ee 100644 --- a/app/assets/javascripts/reports/store/mutations.js +++ b/app/assets/javascripts/reports/store/mutations.js @@ -16,6 +16,7 @@ export default { state.summary.total = response.summary.total; state.summary.resolved = response.summary.resolved; state.summary.failed = response.summary.failed; + state.summary.errored = response.summary.errored; state.status = response.status; state.reports = response.suites; @@ -29,6 +30,7 @@ export default { total: 0, resolved: 0, failed: 0, + errored: 0, }; state.status = null; }, diff --git a/app/assets/javascripts/reports/store/state.js b/app/assets/javascripts/reports/store/state.js index d0b2d0a37f5..4f9eb53e787 100644 --- a/app/assets/javascripts/reports/store/state.js +++ b/app/assets/javascripts/reports/store/state.js @@ -13,6 +13,7 @@ export default () => ({ total: 0, resolved: 0, failed: 0, + errored: 0, }, /** @@ -23,10 +24,14 @@ export default () => ({ * total: {Number}, * resolved: {Number}, * failed: {Number}, + * errored: {Number}, * }, * new_failures: {Array.<Object>}, * resolved_failures: {Array.<Object>}, * existing_failures: {Array.<Object>}, + * new_errors: {Array.<Object>}, + * resolved_errors: {Array.<Object>}, + * existing_errors: {Array.<Object>}, * } */ reports: [], diff --git a/app/assets/javascripts/reports/store/utils.js b/app/assets/javascripts/reports/store/utils.js index 7381f038eaf..ce3ffaae703 100644 --- a/app/assets/javascripts/reports/store/utils.js +++ b/app/assets/javascripts/reports/store/utils.js @@ -8,10 +8,11 @@ import { } from '../constants'; const textBuilder = results => { - const { failed, resolved, total } = results; + const { failed, errored, resolved, total } = results; - const failedString = failed - ? n__('%d failed/error test result', '%d failed/error test results', failed) + const failedOrErrored = (failed || 0) + (errored || 0); + const failedString = failedOrErrored + ? n__('%d failed/error test result', '%d failed/error test results', failedOrErrored) : null; const resolvedString = resolved ? n__('%d fixed test result', '%d fixed test results', resolved) @@ -20,7 +21,7 @@ const textBuilder = results => { let resultsString = s__('Reports|no changed test results'); - if (failed) { + if (failedOrErrored) { if (resolved) { resultsString = sprintf(s__('Reports|%{failedString} and %{resolvedString}'), { failedString, diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb index ed2c39e3fd3..5e14339bb07 100644 --- a/app/controllers/boards/issues_controller.rb +++ b/app/controllers/boards/issues_controller.rb @@ -86,8 +86,12 @@ module Boards head(:forbidden) unless can?(current_user, :admin_issue, board) end + def serializer_options(issues) + {} + end + def render_issues(issues, metadata) - data = { issues: serialize_as_json(issues) } + data = { issues: serialize_as_json(issues, opts: serializer_options(issues)) } data.merge!(metadata) render json: data @@ -133,8 +137,10 @@ module Boards IssueSerializer.new(current_user: current_user) end - def serialize_as_json(resource) - serializer.represent(resource, serializer: 'board', include_full_project_path: board.group_board?) + def serialize_as_json(resource, opts: {}) + opts.merge!(include_full_project_path: board.group_board?, serializer: 'board') + + serializer.represent(resource, opts) end def whitelist_query_limiting diff --git a/app/graphql/mutations/notes/update.rb b/app/graphql/mutations/notes/update.rb deleted file mode 100644 index ebf57b800c0..00000000000 --- a/app/graphql/mutations/notes/update.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -module Mutations - module Notes - class Update < Base - graphql_name 'UpdateNote' - - authorize :admin_note - - argument :id, - GraphQL::ID_TYPE, - required: true, - description: 'The global id of the note to update' - - argument :body, - GraphQL::STRING_TYPE, - required: true, - description: copy_field_description(Types::Notes::NoteType, :body) - - def resolve(args) - note = authorized_find!(id: args[:id]) - - check_object_is_note!(note) - - note = ::Notes::UpdateService.new( - note.project, - current_user, - { note: args[:body] } - ).execute(note) - - { - note: note.reset, - errors: errors_on_object(note) - } - end - end - end -end diff --git a/app/graphql/mutations/notes/update/base.rb b/app/graphql/mutations/notes/update/base.rb new file mode 100644 index 00000000000..9a53337f253 --- /dev/null +++ b/app/graphql/mutations/notes/update/base.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Mutations + module Notes + module Update + # This is a Base class for the Note update mutations and is not + # mounted as a GraphQL mutation itself. + class Base < Mutations::Notes::Base + authorize :admin_note + + argument :id, + GraphQL::ID_TYPE, + required: true, + description: 'The global id of the note to update' + + def resolve(args) + note = authorized_find!(id: args[:id]) + + pre_update_checks!(note, args) + + updated_note = ::Notes::UpdateService.new( + note.project, + current_user, + note_params(note, args) + ).execute(note) + + # It's possible for updated_note to be `nil`, in the situation + # where the note is deleted within `Notes::UpdateService` due to + # the body of the note only containing Quick Actions. + { + note: updated_note&.reset, + errors: updated_note ? errors_on_object(updated_note) : [] + } + end + + private + + def pre_update_checks!(_note, _args) + raise NotImplementedError + end + + def note_params(_note, args) + { note: args[:body] }.compact + end + end + end + end +end diff --git a/app/graphql/mutations/notes/update/image_diff_note.rb b/app/graphql/mutations/notes/update/image_diff_note.rb new file mode 100644 index 00000000000..7aad3af1e04 --- /dev/null +++ b/app/graphql/mutations/notes/update/image_diff_note.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Mutations + module Notes + module Update + class ImageDiffNote < Mutations::Notes::Update::Base + graphql_name 'UpdateImageDiffNote' + + argument :body, + GraphQL::STRING_TYPE, + required: false, + description: copy_field_description(Types::Notes::NoteType, :body) + + argument :position, + Types::Notes::UpdateDiffImagePositionInputType, + required: false, + description: copy_field_description(Types::Notes::NoteType, :position) + + def ready?(**args) + # As both arguments are optional, validate here that one of the + # arguments are present. + # + # This may be able to be done using InputUnions in the future + # if this RFC is merged: + # https://github.com/graphql/graphql-spec/blob/master/rfcs/InputUnion.md + if args.values_at(:body, :position).compact.blank? + raise Gitlab::Graphql::Errors::ArgumentError, + 'body or position arguments are required' + end + + super(args) + end + + private + + def pre_update_checks!(note, args) + unless note.is_a?(DiffNote) && note.position.on_image? + raise Gitlab::Graphql::Errors::ResourceNotAvailable, + 'Resource is not an ImageDiffNote' + end + end + + def note_params(note, args) + super(note, args).merge( + position: position_params(note, args) + ).compact + end + + def position_params(note, args) + new_position = args[:position]&.to_h&.compact + return unless new_position + + original_position = note.position.to_h + + Gitlab::Diff::Position.new(original_position.merge(new_position)) + end + end + end + end +end diff --git a/app/graphql/mutations/notes/update/note.rb b/app/graphql/mutations/notes/update/note.rb new file mode 100644 index 00000000000..03a174fc8d9 --- /dev/null +++ b/app/graphql/mutations/notes/update/note.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Mutations + module Notes + module Update + class Note < Mutations::Notes::Update::Base + graphql_name 'UpdateNote' + + argument :body, + GraphQL::STRING_TYPE, + required: true, + description: copy_field_description(Types::Notes::NoteType, :body) + + private + + def pre_update_checks!(note, _args) + check_object_is_note!(note) + end + end + end + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index fc0a2a099df..ee0f4dbb05f 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -4,7 +4,7 @@ module Types class MutationType < BaseObject include Gitlab::Graphql::MountMutation - graphql_name "Mutation" + graphql_name 'Mutation' mount_mutation Mutations::AwardEmojis::Add mount_mutation Mutations::AwardEmojis::Remove @@ -20,7 +20,14 @@ module Types mount_mutation Mutations::Notes::Create::Note, calls_gitaly: true mount_mutation Mutations::Notes::Create::DiffNote, calls_gitaly: true mount_mutation Mutations::Notes::Create::ImageDiffNote, calls_gitaly: true - mount_mutation Mutations::Notes::Update + mount_mutation Mutations::Notes::Update::Note, + description: 'Updates a Note. If the body of the Note contains only quick actions, ' \ + 'the Note will be destroyed during the update, and no Note will be ' \ + 'returned' + mount_mutation Mutations::Notes::Update::ImageDiffNote, + description: 'Updates a DiffNote on an image (a `Note` where the `position.positionType` is `"image"`). ' \ + 'If the body of the Note contains only quick actions, the Note will be ' \ + 'destroyed during the update, and no Note will be returned' mount_mutation Mutations::Notes::Destroy mount_mutation Mutations::Todos::MarkDone mount_mutation Mutations::Todos::Restore diff --git a/app/graphql/types/notes/diff_position_type.rb b/app/graphql/types/notes/diff_position_type.rb index 654562da0a7..cc00feba2e6 100644 --- a/app/graphql/types/notes/diff_position_type.rb +++ b/app/graphql/types/notes/diff_position_type.rb @@ -29,10 +29,10 @@ module Types # Fields for image positions field :x, GraphQL::INT_TYPE, null: true, - description: 'X position on which the comment was made', + description: 'X position of the note', resolve: -> (position, _args, _ctx) { position.x if position.on_image? } field :y, GraphQL::INT_TYPE, null: true, - description: 'Y position on which the comment was made', + description: 'Y position of the note', resolve: -> (position, _args, _ctx) { position.y if position.on_image? } field :width, GraphQL::INT_TYPE, null: true, description: 'Total width of the image', diff --git a/app/graphql/types/notes/update_diff_image_position_input_type.rb b/app/graphql/types/notes/update_diff_image_position_input_type.rb new file mode 100644 index 00000000000..af99764f9f2 --- /dev/null +++ b/app/graphql/types/notes/update_diff_image_position_input_type.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Types + module Notes + # InputType used for updateImageDiffNote mutation. + # + # rubocop: disable Graphql/AuthorizeTypes + class UpdateDiffImagePositionInputType < BaseInputObject + graphql_name 'UpdateDiffImagePositionInput' + + argument :x, GraphQL::INT_TYPE, + required: false, + description: copy_field_description(Types::Notes::DiffPositionType, :x) + + argument :y, GraphQL::INT_TYPE, + required: false, + description: copy_field_description(Types::Notes::DiffPositionType, :y) + + argument :width, GraphQL::INT_TYPE, + required: false, + description: copy_field_description(Types::Notes::DiffPositionType, :width) + + argument :height, GraphQL::INT_TYPE, + required: false, + description: copy_field_description(Types::Notes::DiffPositionType, :height) + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 4ed99b229b5..023790f7d87 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -720,8 +720,7 @@ module ProjectsHelper end def settings_container_registry_expiration_policy_available?(project) - Feature.enabled?(:registry_retention_policies_settings, project) && - Gitlab.config.registry.enabled && + Gitlab.config.registry.enabled && can?(current_user, :destroy_container_image, project) end end diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb index 95bb52d8f97..773b9fead3a 100644 --- a/app/mailers/emails/pipelines.rb +++ b/app/mailers/emails/pipelines.rb @@ -15,7 +15,13 @@ module Emails def pipeline_mail(pipeline, recipients, status) @project = pipeline.project @pipeline = pipeline - @merge_request = pipeline.all_merge_requests.first + + @merge_request = if pipeline.merge_request? + pipeline.merge_request + else + pipeline.merge_requests_as_head_pipeline.first + end + add_headers # We use bcc here because we don't want to generate these emails for a diff --git a/app/models/concerns/analytics/cycle_analytics/stage.rb b/app/models/concerns/analytics/cycle_analytics/stage.rb index dde73b567db..39e8408f794 100644 --- a/app/models/concerns/analytics/cycle_analytics/stage.rb +++ b/app/models/concerns/analytics/cycle_analytics/stage.rb @@ -15,8 +15,8 @@ module Analytics validates :name, exclusion: { in: Gitlab::Analytics::CycleAnalytics::DefaultStages.names }, if: :custom? validates :start_event_identifier, presence: true validates :end_event_identifier, presence: true - validates :start_event_label, presence: true, if: :start_event_label_based? - validates :end_event_label, presence: true, if: :end_event_label_based? + validates :start_event_label_id, presence: true, if: :start_event_label_based? + validates :end_event_label_id, presence: true, if: :end_event_label_based? validate :validate_stage_event_pairs validate :validate_labels @@ -109,8 +109,8 @@ module Analytics end def validate_labels - validate_label_within_group(:start_event_label, start_event_label_id) if start_event_label_id_changed? - validate_label_within_group(:end_event_label, end_event_label_id) if end_event_label_id_changed? + validate_label_within_group(:start_event_label_id, start_event_label_id) if start_event_label_id_changed? + validate_label_within_group(:end_event_label_id, end_event_label_id) if end_event_label_id_changed? end def validate_label_within_group(association_name, label_id) diff --git a/app/serializers/test_reports_comparer_entity.rb b/app/serializers/test_reports_comparer_entity.rb index d7a3dd34fdc..5f8a68338cc 100644 --- a/app/serializers/test_reports_comparer_entity.rb +++ b/app/serializers/test_reports_comparer_entity.rb @@ -7,6 +7,7 @@ class TestReportsComparerEntity < Grape::Entity expose :total_count, as: :total expose :resolved_count, as: :resolved expose :failed_count, as: :failed + expose :error_count, as: :errored end expose :suite_comparers, as: :suites, using: TestSuiteComparerEntity diff --git a/app/serializers/test_suite_comparer_entity.rb b/app/serializers/test_suite_comparer_entity.rb index d402a4d5718..78c243f75b8 100644 --- a/app/serializers/test_suite_comparer_entity.rb +++ b/app/serializers/test_suite_comparer_entity.rb @@ -11,6 +11,7 @@ class TestSuiteComparerEntity < Grape::Entity expose :total_count, as: :total expose :resolved_count, as: :resolved expose :failed_count, as: :failed + expose :error_count, as: :errored end # rubocop: disable CodeReuse/ActiveRecord @@ -28,6 +29,20 @@ class TestSuiteComparerEntity < Grape::Entity max_tests(suite.new_failures, suite.existing_failures)) end + expose :new_errors, using: TestCaseEntity do |suite| + suite.new_errors.take(max_tests) + end + + expose :existing_errors, using: TestCaseEntity do |suite| + suite.existing_errors.take( + max_tests(suite.new_errors)) + end + + expose :resolved_errors, using: TestCaseEntity do |suite| + suite.resolved_errors.take( + max_tests(suite.new_errors, suite.existing_errors)) + end + private def max_tests(*used) diff --git a/app/services/snippets/create_service.rb b/app/services/snippets/create_service.rb index 51860adca77..9e87bebbe4e 100644 --- a/app/services/snippets/create_service.rb +++ b/app/services/snippets/create_service.rb @@ -24,7 +24,9 @@ module Snippets spam_check(snippet, current_user) snippet_saved = snippet.with_transaction_returning_status do - snippet.save + if snippet.save && snippet.store_mentions! + create_repository_for(snippet, current_user) + end end if snippet_saved @@ -36,5 +38,11 @@ module Snippets snippet_error_response(snippet, 400) end end + + private + + def create_repository_for(snippet, user) + snippet.create_repository if Feature.enabled?(:version_snippets, user) + end end end diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 0affd9f0e6f..438d390389c 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -27,7 +27,9 @@ .form-group.row .col-sm-12= f.label :format, class: 'control-label-full-width' .col-sm-12 - = f.select :format, options_for_select(ProjectWiki::MARKUPS, {selected: @page.format}), {}, class: 'form-control' + .select-wrapper + = f.select :format, options_for_select(ProjectWiki::MARKUPS, {selected: @page.format}), {}, class: 'form-control select-control' + = icon('chevron-down') .form-group.row .col-sm-12= f.label :content, class: 'control-label-full-width' |