...@@ -200,7 +200,7 @@ ...@@ -200,7 +200,7 @@
.use-pg9: .use-pg9:
services: services:
- name: postgres:9.6 - name: postgres:9.6.17
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine - name: redis:alpine
variables: variables:
...@@ -209,7 +209,7 @@ ...@@ -209,7 +209,7 @@
.use-pg10: .use-pg10:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.12-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-10-graphicsmagick-1.3.34" image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.12-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-10-graphicsmagick-1.3.34"
services: services:
- name: postgres:10.9 - name: postgres:10.12
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine - name: redis:alpine
variables: variables:
...@@ -217,7 +217,7 @@ ...@@ -217,7 +217,7 @@
.use-pg9-ee: .use-pg9-ee:
services: services:
- name: postgres:9.6 - name: postgres:9.6.17
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine - name: redis:alpine
- name: elasticsearch:6.4.2 - name: elasticsearch:6.4.2
...@@ -227,7 +227,7 @@ ...@@ -227,7 +227,7 @@
.use-pg10-ee: .use-pg10-ee:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.12-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-10-graphicsmagick-1.3.34" image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.12-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-10-graphicsmagick-1.3.34"
services: services:
- name: postgres:10.9 - name: postgres:10.12
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine - name: redis:alpine
- name: elasticsearch:6.4.2 - name: elasticsearch:6.4.2
... ...
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
* This is tightly coupled to projects/issues/_issue.html.haml, * This is tightly coupled to projects/issues/_issue.html.haml,
* any changes done to the haml need to be reflected here. * 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 { GlLink, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
import { import {
dateInWords, dateInWords,
...@@ -19,8 +19,6 @@ import { mergeUrlParams } from '~/lib/utils/url_utility'; ...@@ -19,8 +19,6 @@ import { mergeUrlParams } from '~/lib/utils/url_utility';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue'; import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
const ISSUE_TOKEN = '#';
export default { export default {
components: { components: {
Icon, Icon,
...@@ -119,8 +117,7 @@ export default { ...@@ -119,8 +117,7 @@ export default {
); );
}, },
referencePath() { referencePath() {
// TODO: The API should return the reference path (it doesn't now) https://gitlab.com/gitlab-org/gitlab/issues/31301 return this.issuable.references.relative;
return `${ISSUE_TOKEN}${this.issuable.iid}`;
}, },
updatedDateString() { updatedDateString() {
return formatDate(new Date(this.issuable.updated_at), 'mmm d, yyyy h:MMtt'); return formatDate(new Date(this.issuable.updated_at), 'mmm d, yyyy h:MMtt');
...@@ -230,7 +227,7 @@ export default { ...@@ -230,7 +227,7 @@ export default {
</div> </div>
<div class="issuable-info"> <div class="issuable-info">
<span>{{ referencePath }}</span> <span class="js-ref-path">{{ referencePath }}</span>
<span class="d-none d-sm-inline-block mr-1"> <span class="d-none d-sm-inline-block mr-1">
&middot; &middot;
... ...
......
...@@ -62,9 +62,21 @@ export default { ...@@ -62,9 +62,21 @@ export default {
return ( return (
report.existing_failures.length > 0 || report.existing_failures.length > 0 ||
report.new_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> </script>
...@@ -87,9 +99,9 @@ export default { ...@@ -87,9 +99,9 @@ export default {
<issues-list <issues-list
v-if="shouldRenderIssuesList(report)" v-if="shouldRenderIssuesList(report)"
:key="`issues-list-${i}`" :key="`issues-list-${i}`"
:unresolved-issues="report.existing_failures" :unresolved-issues="unresolvedIssues(report)"
:new-issues="report.new_failures" :new-issues="newIssues(report)"
:resolved-issues="report.resolved_failures" :resolved-issues="resolvedIssues(report)"
:component="$options.componentNames.TestIssueBody" :component="$options.componentNames.TestIssueBody"
class="report-block-group-list" class="report-block-group-list"
/> />
... ...
......
...@@ -16,6 +16,7 @@ export default { ...@@ -16,6 +16,7 @@ export default {
state.summary.total = response.summary.total; state.summary.total = response.summary.total;
state.summary.resolved = response.summary.resolved; state.summary.resolved = response.summary.resolved;
state.summary.failed = response.summary.failed; state.summary.failed = response.summary.failed;
state.summary.errored = response.summary.errored;
state.status = response.status; state.status = response.status;
state.reports = response.suites; state.reports = response.suites;
...@@ -29,6 +30,7 @@ export default { ...@@ -29,6 +30,7 @@ export default {
total: 0, total: 0,
resolved: 0, resolved: 0,
failed: 0, failed: 0,
errored: 0,
}; };
state.status = null; state.status = null;
}, },
... ...
......
...@@ -13,6 +13,7 @@ export default () => ({ ...@@ -13,6 +13,7 @@ export default () => ({
total: 0, total: 0,
resolved: 0, resolved: 0,
failed: 0, failed: 0,
errored: 0,
}, },
/** /**
...@@ -23,10 +24,14 @@ export default () => ({ ...@@ -23,10 +24,14 @@ export default () => ({
* total: {Number}, * total: {Number},
* resolved: {Number}, * resolved: {Number},
* failed: {Number}, * failed: {Number},
* errored: {Number},
* }, * },
* new_failures: {Array.<Object>}, * new_failures: {Array.<Object>},
* resolved_failures: {Array.<Object>}, * resolved_failures: {Array.<Object>},
* existing_failures: {Array.<Object>}, * existing_failures: {Array.<Object>},
* new_errors: {Array.<Object>},
* resolved_errors: {Array.<Object>},
* existing_errors: {Array.<Object>},
* } * }
*/ */
reports: [], reports: [],
... ...
......
...@@ -8,10 +8,11 @@ import { ...@@ -8,10 +8,11 @@ import {
} from '../constants'; } from '../constants';
const textBuilder = results => { const textBuilder = results => {
const { failed, resolved, total } = results; const { failed, errored, resolved, total } = results;
const failedString = failed const failedOrErrored = (failed || 0) + (errored || 0);
? n__('%d failed/error test result', '%d failed/error test results', failed) const failedString = failedOrErrored
? n__('%d failed/error test result', '%d failed/error test results', failedOrErrored)
: null; : null;
const resolvedString = resolved const resolvedString = resolved
? n__('%d fixed test result', '%d fixed test results', resolved) ? n__('%d fixed test result', '%d fixed test results', resolved)
...@@ -20,7 +21,7 @@ const textBuilder = results => { ...@@ -20,7 +21,7 @@ const textBuilder = results => {
let resultsString = s__('Reports|no changed test results'); let resultsString = s__('Reports|no changed test results');
if (failed) { if (failedOrErrored) {
if (resolved) { if (resolved) {
resultsString = sprintf(s__('Reports|%{failedString} and %{resolvedString}'), { resultsString = sprintf(s__('Reports|%{failedString} and %{resolvedString}'), {
failedString, failedString,
... ...
......
...@@ -86,8 +86,12 @@ module Boards ...@@ -86,8 +86,12 @@ module Boards
head(:forbidden) unless can?(current_user, :admin_issue, board) head(:forbidden) unless can?(current_user, :admin_issue, board)
end end
def serializer_options(issues)
{}
end
def render_issues(issues, metadata) def render_issues(issues, metadata)
data = { issues: serialize_as_json(issues) } data = { issues: serialize_as_json(issues, opts: serializer_options(issues)) }
data.merge!(metadata) data.merge!(metadata)
render json: data render json: data
...@@ -133,8 +137,10 @@ module Boards ...@@ -133,8 +137,10 @@ module Boards
IssueSerializer.new(current_user: current_user) IssueSerializer.new(current_user: current_user)
end end
def serialize_as_json(resource) def serialize_as_json(resource, opts: {})
serializer.represent(resource, serializer: 'board', include_full_project_path: board.group_board?) opts.merge!(include_full_project_path: board.group_board?, serializer: 'board')
serializer.represent(resource, opts)
end end
def whitelist_query_limiting def whitelist_query_limiting
... ...
......
# 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
# 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
# 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
# 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
...@@ -4,7 +4,7 @@ module Types ...@@ -4,7 +4,7 @@ module Types
class MutationType < BaseObject class MutationType < BaseObject
include Gitlab::Graphql::MountMutation include Gitlab::Graphql::MountMutation
graphql_name "Mutation" graphql_name 'Mutation'
mount_mutation Mutations::AwardEmojis::Add mount_mutation Mutations::AwardEmojis::Add
mount_mutation Mutations::AwardEmojis::Remove mount_mutation Mutations::AwardEmojis::Remove
...@@ -20,7 +20,14 @@ module Types ...@@ -20,7 +20,14 @@ module Types
mount_mutation Mutations::Notes::Create::Note, calls_gitaly: true mount_mutation Mutations::Notes::Create::Note, calls_gitaly: true
mount_mutation Mutations::Notes::Create::DiffNote, 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::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::Notes::Destroy
mount_mutation Mutations::Todos::MarkDone mount_mutation Mutations::Todos::MarkDone
mount_mutation Mutations::Todos::Restore mount_mutation Mutations::Todos::Restore
... ...
......
...@@ -29,10 +29,10 @@ module Types ...@@ -29,10 +29,10 @@ module Types
# Fields for image positions # Fields for image positions
field :x, GraphQL::INT_TYPE, null: true, 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? } resolve: -> (position, _args, _ctx) { position.x if position.on_image? }
field :y, GraphQL::INT_TYPE, null: true, 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? } resolve: -> (position, _args, _ctx) { position.y if position.on_image? }
field :width, GraphQL::INT_TYPE, null: true, field :width, GraphQL::INT_TYPE, null: true,
description: 'Total width of the image', description: 'Total width of the image',
... ...
......
# 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
...@@ -720,7 +720,6 @@ module ProjectsHelper ...@@ -720,7 +720,6 @@ module ProjectsHelper
end end
def settings_container_registry_expiration_policy_available?(project) 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) can?(current_user, :destroy_container_image, project)
end end
... ...
......
...@@ -15,7 +15,13 @@ module Emails ...@@ -15,7 +15,13 @@ module Emails
def pipeline_mail(pipeline, recipients, status) def pipeline_mail(pipeline, recipients, status)
@project = pipeline.project @project = pipeline.project
@pipeline = pipeline @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 add_headers
# We use bcc here because we don't want to generate these emails for a # We use bcc here because we don't want to generate these emails for a
... ...
......
...@@ -15,8 +15,8 @@ module Analytics ...@@ -15,8 +15,8 @@ module Analytics
validates :name, exclusion: { in: Gitlab::Analytics::CycleAnalytics::DefaultStages.names }, if: :custom? validates :name, exclusion: { in: Gitlab::Analytics::CycleAnalytics::DefaultStages.names }, if: :custom?
validates :start_event_identifier, presence: true validates :start_event_identifier, presence: true
validates :end_event_identifier, presence: true validates :end_event_identifier, presence: true
validates :start_event_label, presence: true, if: :start_event_label_based? validates :start_event_label_id, presence: true, if: :start_event_label_based?
validates :end_event_label, presence: true, if: :end_event_label_based? validates :end_event_label_id, presence: true, if: :end_event_label_based?
validate :validate_stage_event_pairs validate :validate_stage_event_pairs
validate :validate_labels validate :validate_labels
...@@ -109,8 +109,8 @@ module Analytics ...@@ -109,8 +109,8 @@ module Analytics
end end
def validate_labels def validate_labels
validate_label_within_group(:start_event_label, start_event_label_id) if start_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, end_event_label_id) if end_event_label_id_changed? validate_label_within_group(:end_event_label_id, end_event_label_id) if end_event_label_id_changed?
end end
def validate_label_within_group(association_name, label_id) def validate_label_within_group(association_name, label_id)
... ...
......
...@@ -7,6 +7,7 @@ class TestReportsComparerEntity < Grape::Entity ...@@ -7,6 +7,7 @@ class TestReportsComparerEntity < Grape::Entity
expose :total_count, as: :total expose :total_count, as: :total
expose :resolved_count, as: :resolved expose :resolved_count, as: :resolved
expose :failed_count, as: :failed expose :failed_count, as: :failed
expose :error_count, as: :errored
end end
expose :suite_comparers, as: :suites, using: TestSuiteComparerEntity expose :suite_comparers, as: :suites, using: TestSuiteComparerEntity
... ...
......
...@@ -11,6 +11,7 @@ class TestSuiteComparerEntity < Grape::Entity ...@@ -11,6 +11,7 @@ class TestSuiteComparerEntity < Grape::Entity
expose :total_count, as: :total expose :total_count, as: :total
expose :resolved_count, as: :resolved expose :resolved_count, as: :resolved
expose :failed_count, as: :failed expose :failed_count, as: :failed
expose :error_count, as: :errored
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
...@@ -28,6 +29,20 @@ class TestSuiteComparerEntity < Grape::Entity ...@@ -28,6 +29,20 @@ class TestSuiteComparerEntity < Grape::Entity
max_tests(suite.new_failures, suite.existing_failures)) max_tests(suite.new_failures, suite.existing_failures))
end 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 private
def max_tests(*used) def max_tests(*used)
... ...
......
...@@ -24,7 +24,9 @@ module Snippets ...@@ -24,7 +24,9 @@ module Snippets
spam_check(snippet, current_user) spam_check(snippet, current_user)
snippet_saved = snippet.with_transaction_returning_status do snippet_saved = snippet.with_transaction_returning_status do
snippet.save if snippet.save && snippet.store_mentions!
create_repository_for(snippet, current_user)
end
end end
if snippet_saved if snippet_saved
...@@ -36,5 +38,11 @@ module Snippets ...@@ -36,5 +38,11 @@ module Snippets
snippet_error_response(snippet, 400) snippet_error_response(snippet, 400)
end end
end end
private
def create_repository_for(snippet, user)
snippet.create_repository if Feature.enabled?(:version_snippets, user)
end
end end
end end