Commit c6373a2c authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 6593f1f6
......@@ -14,9 +14,7 @@
- .assets-compile-cache
- .only:changes-code-backstage-qa
image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.16-graphicsmagick-1.3.33-docker-19.03.1
stage: test
dependencies: ["setup-test-env"]
needs: ["setup-test-env"]
stage: prepare
services:
- docker:19.03.0-dind
variables:
......
......@@ -148,7 +148,7 @@ gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 2.0.10'
gem 'asciidoctor-include-ext', '~> 0.3.1', require: false
gem 'asciidoctor-plantuml', '0.0.9'
gem 'asciidoctor-plantuml', '0.0.10'
gem 'rouge', '~> 3.11.0'
gem 'truncato', '~> 0.7.11'
gem 'bootstrap_form', '~> 4.2.0'
......
......@@ -71,7 +71,7 @@ GEM
asciidoctor (2.0.10)
asciidoctor-include-ext (0.3.1)
asciidoctor (>= 1.5.6, < 3.0.0)
asciidoctor-plantuml (0.0.9)
asciidoctor-plantuml (0.0.10)
asciidoctor (>= 1.5.6, < 3.0.0)
ast (2.4.0)
atlassian-jwt (0.2.0)
......@@ -1130,7 +1130,7 @@ DEPENDENCIES
asana (~> 0.9)
asciidoctor (~> 2.0.10)
asciidoctor-include-ext (~> 0.3.1)
asciidoctor-plantuml (= 0.0.9)
asciidoctor-plantuml (= 0.0.10)
atlassian-jwt (~> 0.2.0)
attr_encrypted (~> 3.1.0)
awesome_print
......
......@@ -470,7 +470,7 @@ export const pikadayToString = date => {
*/
export const parseSeconds = (
seconds,
{ daysPerWeek = 5, hoursPerDay = 8, limitToHours = false } = {},
{ daysPerWeek = 5, hoursPerDay = 8, limitToHours = false, limitToDays = false } = {},
) => {
const DAYS_PER_WEEK = daysPerWeek;
const HOURS_PER_DAY = hoursPerDay;
......@@ -486,8 +486,11 @@ export const parseSeconds = (
minutes: 1,
};
if (limitToHours) {
if (limitToDays || limitToHours) {
timePeriodConstraints.weeks = 0;
}
if (limitToHours) {
timePeriodConstraints.days = 0;
}
......@@ -612,3 +615,44 @@ export const secondsToDays = seconds => Math.round(seconds / 86400);
* @return {Date} the date following the date provided
*/
export const dayAfter = date => new Date(newDate(date).setDate(date.getDate() + 1));
/**
* Mimics the behaviour of the rails distance_of_time_in_words function
* https://api.rubyonrails.org/v6.0.1/classes/ActionView/Helpers/DateHelper.html#method-i-distance_of_time_in_words
* 0 < -> 29 secs => less than a minute
* 30 secs < -> 1 min, 29 secs => 1 minute
* 1 min, 30 secs < -> 44 mins, 29 secs => [2..44] minutes
* 44 mins, 30 secs < -> 89 mins, 29 secs => about 1 hour
* 89 mins, 30 secs < -> 23 hrs, 59 mins, 29 secs => about[2..24]hours
* 23 hrs, 59 mins, 30 secs < -> 41 hrs, 59 mins, 29 secs => 1 day
* 41 hrs, 59 mins, 30 secs => x days
*
* @param {Number} seconds
* @return {String} approximated time
*/
export const approximateDuration = (seconds = 0) => {
if (!_.isNumber(seconds) || seconds < 0) {
return '';
}
const ONE_MINUTE_LIMIT = 90; // 1 minute 30s
const MINUTES_LIMIT = 2670; // 44 minutes 30s
const ONE_HOUR_LIMIT = 5370; // 89 minutes 30s
const HOURS_LIMIT = 86370; // 23 hours 59 minutes 30s
const ONE_DAY_LIMIT = 151170; // 41 hours 59 minutes 30s
const { days = 0, hours = 0, minutes = 0 } = parseSeconds(seconds, {
daysPerWeek: 7,
hoursPerDay: 24,
limitToDays: true,
});
if (seconds < 30) {
return __('less than a minute');
} else if (seconds < MINUTES_LIMIT) {
return n__('1 minute', '%d minutes', seconds < ONE_MINUTE_LIMIT ? 1 : minutes);
} else if (seconds < HOURS_LIMIT) {
return n__('about 1 hour', 'about %d hours', seconds < ONE_HOUR_LIMIT ? 1 : hours);
}
return n__('1 day', '%d days', seconds < ONE_DAY_LIMIT ? 1 : days);
};
......@@ -136,7 +136,11 @@ export default {
>
<strong>{{ __('New! Suggest changes directly') }}</strong>
<p class="mb-2">
{{ __('Suggest code changes which are immediately applied. Try it out!') }}
{{
__(
'Suggest code changes which can be immediately applied in one click. Try it out!',
)
}}
</p>
<gl-button variant="primary" size="sm" @click="handleSuggestDismissed">
{{ __('Got it') }}
......
......@@ -20,6 +20,7 @@
}
.ci-status-icon-pending,
.ci-status-icon-waiting-for-resource,
.ci-status-icon-failed-with-warnings,
.ci-status-icon-success-with-warnings {
svg {
......
......@@ -288,7 +288,7 @@
}
.issuable-sidebar {
width: calc(100% + 100px);
width: 100%;
height: 100%;
overflow-y: scroll;
overflow-x: hidden;
......
......@@ -795,6 +795,7 @@
}
&.ci-status-icon-pending,
&.ci-status-icon-waiting-for-resource,
&.ci-status-icon-success-with-warnings {
@include mini-pipeline-graph-color($white, $orange-100, $orange-200, $orange-500, $orange-600, $orange-700);
}
......
......@@ -42,6 +42,7 @@
}
&.ci-pending,
&.ci-waiting-for-resource,
&.ci-failed-with-warnings,
&.ci-success-with-warnings {
@include status-color($orange-100, $orange-500, $orange-700);
......
......@@ -62,6 +62,7 @@ module CiStatusHelper
status.humanize
end
# rubocop:disable Metrics/CyclomaticComplexity
def ci_icon_for_status(status, size: 16)
if detailed_status?(status)
return sprite_icon(status.icon, size: size)
......@@ -77,6 +78,8 @@ module CiStatusHelper
'status_failed'
when 'pending'
'status_pending'
when 'waiting_for_resource'
'status_pending'
when 'preparing'
'status_preparing'
when 'running'
......@@ -97,6 +100,7 @@ module CiStatusHelper
sprite_icon(icon_name, size: size)
end
# rubocop:enable Metrics/CyclomaticComplexity
def ci_icon_class_for_status(status)
group = detailed_status?(status) ? status.group : status.dasherize
......
......@@ -206,9 +206,25 @@ module Ci
state_machine :status do
event :enqueue do
transition [:created, :skipped, :manual, :scheduled] => :waiting_for_resource, if: :requires_resource?
transition [:created, :skipped, :manual, :scheduled] => :preparing, if: :any_unmet_prerequisites?
end
event :enqueue_scheduled do
transition scheduled: :waiting_for_resource, if: :requires_resource?
transition scheduled: :preparing, if: :any_unmet_prerequisites?
transition scheduled: :pending
end
event :enqueue_waiting_for_resource do
transition waiting_for_resource: :preparing, if: :any_unmet_prerequisites?
transition waiting_for_resource: :pending
end
event :enqueue_preparing do
transition preparing: :pending
end
event :actionize do
transition created: :manual
end
......@@ -221,14 +237,8 @@ module Ci
transition scheduled: :manual
end
event :enqueue_scheduled do
transition scheduled: :preparing, if: ->(build) do
build.scheduled_at&.past? && build.any_unmet_prerequisites?
end
transition scheduled: :pending, if: ->(build) do
build.scheduled_at&.past? && !build.any_unmet_prerequisites?
end
before_transition on: :enqueue_scheduled do |build|
build.scheduled_at.nil? || build.scheduled_at.past? # If false is returned, it stops the transition
end
before_transition scheduled: any do |build|
......@@ -239,6 +249,27 @@ module Ci
build.scheduled_at = build.options_scheduled_at
end
before_transition any => :waiting_for_resource do |build|
build.waiting_for_resource_at = Time.now
end
before_transition on: :enqueue_waiting_for_resource do |build|
next unless build.requires_resource?
build.resource_group.assign_resource_to(build) # If false is returned, it stops the transition
end
after_transition any => :waiting_for_resource do |build|
build.run_after_commit do
Ci::ResourceGroups::AssignResourceFromResourceGroupWorker
.perform_async(build.resource_group_id)
end
end
before_transition on: :enqueue_preparing do |build|
build.any_unmet_prerequisites? # If false is returned, it stops the transition
end
after_transition created: :scheduled do |build|
build.run_after_commit do
Ci::BuildScheduleWorker.perform_at(build.scheduled_at, build.id)
......@@ -267,6 +298,16 @@ module Ci
end
end
after_transition any => ::Ci::Build.completed_statuses do |build|
next unless build.resource_group_id.present?
next unless build.resource_group.release_resource_from(build)
build.run_after_commit do
Ci::ResourceGroups::AssignResourceFromResourceGroupWorker
.perform_async(build.resource_group_id)
end
end
after_transition any => [:success, :failed, :canceled] do |build|
build.run_after_commit do
BuildFinishedWorker.perform_async(id)
......@@ -439,6 +480,11 @@ module Ci
end
end
def requires_resource?
Feature.enabled?(:ci_resource_group, project) &&
self.resource_group_id.present?
end
def has_environment?
environment.present?
end
......
......@@ -97,10 +97,14 @@ module Ci
state_machine :status, initial: :created do
event :enqueue do
transition [:created, :preparing, :skipped, :scheduled] => :pending
transition [:created, :waiting_for_resource, :preparing, :skipped, :scheduled] => :pending
transition [:success, :failed, :canceled] => :running
end
event :request_resource do
transition any - [:waiting_for_resource] => :waiting_for_resource
end
event :prepare do
transition any - [:preparing] => :preparing
end
......@@ -137,7 +141,7 @@ module Ci
# Do not add any operations to this state_machine
# Create a separate worker for each new operation
before_transition [:created, :preparing, :pending] => :running do |pipeline|
before_transition [:created, :waiting_for_resource, :preparing, :pending] => :running do |pipeline|
pipeline.started_at = Time.now
end
......@@ -160,7 +164,7 @@ module Ci
end
end
after_transition [:created, :preparing, :pending] => :running do |pipeline|
after_transition [:created, :waiting_for_resource, :preparing, :pending] => :running do |pipeline|
pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
end
......@@ -168,7 +172,7 @@ module Ci
pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
end
after_transition [:created, :preparing, :pending, :running] => :success do |pipeline|
after_transition [:created, :waiting_for_resource, :preparing, :pending, :running] => :success do |pipeline|
pipeline.run_after_commit { PipelineSuccessWorker.perform_async(pipeline.id) }
end
......@@ -319,7 +323,7 @@ module Ci
end
def self.bridgeable_statuses
::Ci::Pipeline::AVAILABLE_STATUSES - %w[created preparing pending]
::Ci::Pipeline::AVAILABLE_STATUSES - %w[created waiting_for_resource preparing pending]
end
def stages_count
......@@ -578,6 +582,7 @@ module Ci
new_status = latest_builds_status.to_s
case new_status
when 'created' then nil
when 'waiting_for_resource' then request_resource
when 'preparing' then prepare
when 'pending' then enqueue
when 'running' then run
......
......@@ -39,10 +39,14 @@ module Ci
state_machine :status, initial: :created do
event :enqueue do
transition [:created, :preparing] => :pending
transition [:created, :waiting_for_resource, :preparing] => :pending
transition [:success, :failed, :canceled, :skipped] => :running
end
event :request_resource do
transition any - [:waiting_for_resource] => :waiting_for_resource
end
event :prepare do
transition any - [:preparing] => :preparing
end
......@@ -81,6 +85,7 @@ module Ci
new_status = latest_stage_status.to_s
case new_status
when 'created' then nil
when 'waiting_for_resource' then request_resource
when 'preparing' then prepare
when 'pending' then enqueue
when 'running' then run
......
......@@ -96,7 +96,7 @@ class CommitStatus < ApplicationRecord
# A CommitStatus will never have prerequisites, but this event
# is shared by Ci::Build, which cannot progress unless prerequisites
# are satisfied.
transition [:created, :preparing, :skipped, :manual, :scheduled] => :pending, unless: :any_unmet_prerequisites?
transition [:created, :skipped, :manual, :scheduled] => :pending, if: :all_met_to_become_pending?
end
event :run do
......@@ -104,22 +104,22 @@ class CommitStatus < ApplicationRecord
end
event :skip do
transition [:created, :preparing, :pending] => :skipped
transition [:created, :waiting_for_resource, :preparing, :pending] => :skipped
end
event :drop do
transition [:created, :preparing, :pending, :running, :scheduled] => :failed
transition [:created, :waiting_for_resource, :preparing, :pending, :running, :scheduled] => :failed
end
event :success do
transition [:created, :preparing, :pending, :running] => :success
transition [:created, :waiting_for_resource, :preparing, :pending, :running] => :success
end
event :cancel do
transition [:created, :preparing, :pending, :running, :manual, :scheduled] => :canceled
transition [:created, :waiting_for_resource, :preparing, :pending, :running, :manual, :scheduled] => :canceled
end
before_transition [:created, :preparing, :skipped, :manual, :scheduled] => :pending do |commit_status|
before_transition [:created, :waiting_for_resource, :preparing, :skipped, :manual, :scheduled] => :pending do |commit_status|
commit_status.queued_at = Time.now
end
......@@ -218,10 +218,18 @@ class CommitStatus < ApplicationRecord
false
end
def all_met_to_become_pending?
!any_unmet_prerequisites? && !requires_resource?
end
def any_unmet_prerequisites?
false
end
def requires_resource?
false
end
def auto_canceled?
canceled? && auto_canceled_by_id?
end
......
......@@ -5,16 +5,16 @@ module HasStatus
DEFAULT_STATUS = 'created'
BLOCKED_STATUS = %w[manual scheduled].freeze
AVAILABLE_STATUSES = %w[created preparing pending running success failed canceled skipped manual scheduled].freeze
AVAILABLE_STATUSES = %w[created waiting_for_resource preparing pending running success failed canceled skipped manual scheduled].freeze
STARTED_STATUSES = %w[running success failed skipped manual scheduled].freeze
ACTIVE_STATUSES = %w[preparing pending running].freeze
COMPLETED_STATUSES = %w[success failed canceled skipped].freeze
ORDERED_STATUSES = %w[failed preparing pending running manual scheduled canceled success skipped created].freeze
ORDERED_STATUSES = %w[failed preparing pending running waiting_for_resource manual scheduled canceled success skipped created].freeze
PASSED_WITH_WARNINGS_STATUSES = %w[failed canceled].to_set.freeze
EXCLUDE_IGNORED_STATUSES = %w[manual failed canceled].to_set.freeze
STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3,
failed: 4, canceled: 5, skipped: 6, manual: 7,
scheduled: 8, preparing: 9 }.freeze
scheduled: 8, preparing: 9, waiting_for_resource: 10 }.freeze
UnknownStatusError = Class.new(StandardError)
......@@ -29,6 +29,7 @@ module HasStatus
manual = scope_relevant.manual.select('count(*)').to_sql
scheduled = scope_relevant.scheduled.select('count(*)').to_sql
preparing = scope_relevant.preparing.select('count(*)').to_sql
waiting_for_resource = scope_relevant.waiting_for_resource.select('count(*)').to_sql
pending = scope_relevant.pending.select('count(*)').to_sql
running = scope_relevant.running.select('count(*)').to_sql
skipped = scope_relevant.skipped.select('count(*)').to_sql
......@@ -46,6 +47,7 @@ module HasStatus
WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled'
WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending'
WHEN (#{running})+(#{pending})>0 THEN 'running'
WHEN (#{waiting_for_resource})>0 THEN 'waiting_for_resource'
WHEN (#{manual})>0 THEN 'manual'
WHEN (#{scheduled})>0 THEN 'scheduled'
WHEN (#{preparing})>0 THEN 'preparing'
......@@ -95,6 +97,7 @@ module HasStatus
state_machine :status, initial: :created do
state :created, value: 'created'
state :waiting_for_resource, value: 'waiting_for_resource'
state :preparing, value: 'preparing'
state :pending, value: 'pending'
state :running, value: 'running'
......@@ -107,6 +110,7 @@ module HasStatus
end
scope :created, -> { with_status(:created) }
scope :waiting_for_resource, -> { with_status(:waiting_for_resource) }
scope :preparing, -> { with_status(:preparing) }
scope :relevant, -> { without_status(:created) }
scope :running, -> { with_status(:running) }
......@@ -117,8 +121,8 @@ module HasStatus
scope :skipped, -> { with_status(:skipped) }
scope :manual, -> { with_status(:manual) }
scope :scheduled, -> { with_status(:scheduled) }
scope :alive, -> { with_status(:created, :preparing, :pending, :running) }
scope :alive_or_scheduled, -> { with_status(:created, :preparing, :pending, :running, :scheduled) }
scope :alive, -> { with_status(:created, :waiting_for_resource, :preparing, :pending, :running) }
scope :alive_or_scheduled, -> { with_status(:created, :waiting_for_resource, :preparing, :pending, :running, :scheduled) }
scope :created_or_pending, -> { with_status(:created, :pending) }
scope :running_or_pending, -> { with_status(:running, :pending) }
scope :finished, -> { with_status(:success, :failed, :canceled) }
......@@ -126,7 +130,7 @@ module HasStatus
scope :incomplete, -> { without_statuses(completed_statuses) }
scope :cancelable, -> do
where(status: [:running, :preparing, :pending, :created, :scheduled])
where(status: [:running, :waiting_for_resource, :preparing, :pending, :created, :scheduled])
end
scope :without_statuses, -> (names) do
......
......@@ -11,7 +11,7 @@ module Ci
def execute
prerequisites.each(&:complete!)
build.enqueue!
build.enqueue_preparing!
rescue => e
Gitlab::ErrorTracking.track_exception(e, build_id: build.id)
......
# frozen_string_literal: true
module Ci
module ResourceGroups
class AssignResourceFromResourceGroupService < ::BaseService
# rubocop: disable CodeReuse/ActiveRecord
def execute(resource_group)
free_resources = resource_group.resources.free.count
resource_group.builds.waiting_for_resource.take(free_resources).each do |build|
build.enqueue_waiting_for_resource
end
end
# rubocop: enable CodeReuse/ActiveRecord
end
end
end
......@@ -103,6 +103,7 @@
- pipeline_processing:stage_update
- pipeline_processing:update_head_pipeline_for_merge_request
- pipeline_processing:ci_build_schedule
- pipeline_processing:ci_resource_groups_assign_resource_from_resource_group
- deployment:deployments_success
- deployment:deployments_finished
......
# frozen_string_literal: true
module Ci
module ResourceGroups
class AssignResourceFromResourceGroupWorker
include ApplicationWorker
include PipelineQueue
queue_namespace :pipeline_processing
feature_category :continuous_delivery
def perform(resource_group_id)
::Ci::ResourceGroup.find_by_id(resource_group_id).try do |resource_group|
Ci::ResourceGroups::AssignResourceFromResourceGroupService.new(resource_group.project, nil)
.execute(resource_group)
end
end
end
end
end
---
title: Update to clarify slightly misleading tool tip
merge_request: 22222
author:
type: other
---
title: Sidebar getting partially hidden behind the content block
merge_request: 21978
author: allenlai18
type: fixed
......@@ -4443,7 +4443,8 @@ type Pipeline {
startedAt: Time
"""
Status of the pipeline (CREATED, PREPARING, PENDING, RUNNING, FAILED, SUCCESS, CANCELED, SKIPPED, MANUAL, SCHEDULED)
Status of the pipeline (CREATED, WAITING_FOR_RESOURCE, PREPARING, PENDING,
RUNNING, FAILED, SUCCESS, CANCELED, SKIPPED, MANUAL, SCHEDULED)
"""
status: PipelineStatusEnum!
......@@ -4521,6 +4522,7 @@ enum PipelineStatusEnum {
SCHEDULED
SKIPPED
SUCCESS
WAITING_FOR_RESOURCE
}
type Project {
......
......@@ -12203,7 +12203,7 @@
},
{
"name": "status",
"description": "Status of the pipeline (CREATED, PREPARING, PENDING, RUNNING, FAILED, SUCCESS, CANCELED, SKIPPED, MANUAL, SCHEDULED)",
"description": "Status of the pipeline (CREATED, WAITING_FOR_RESOURCE, PREPARING, PENDING, RUNNING, FAILED, SUCCESS, CANCELED, SKIPPED, MANUAL, SCHEDULED)",
"args": [
],
......@@ -12344,6 +12344,12 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "WAITING_FOR_RESOURCE",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "PREPARING",
"description": null,
......
......@@ -635,7 +635,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `iid` | String! | Internal ID of the pipeline |
| `sha` | String! | SHA of the pipeline's commit |
| `beforeSha` | String | Base SHA of the source branch |
| `status` | PipelineStatusEnum! | Status of the pipeline (CREATED, PREPARING, PENDING, RUNNING, FAILED, SUCCESS, CANCELED, SKIPPED, MANUAL, SCHEDULED) |
| `status` | PipelineStatusEnum! | Status of the pipeline (CREATED, WAITING_FOR_RESOURCE, PREPARING, PENDING, RUNNING, FAILED, SUCCESS, CANCELED, SKIPPED, MANUAL, SCHEDULED) |
| `detailedStatus` | DetailedStatus! | Detailed status of the pipeline |
| `duration` | Int | Duration of the pipeline in seconds |
| `coverage` | Float | Coverage percentage |
......
......@@ -218,6 +218,8 @@ graph RL;
subgraph "`prepare` stage"
A
B
C
F
K
J
......@@ -225,8 +227,6 @@ subgraph "`prepare` stage"
end
subgraph "`test` stage"
B --> |needs| A;
C --> |needs| A;
D --> |needs| A;
H -.-> |needs and depends on| A;
H -.-> |needs and depends on| K;
......
......@@ -253,3 +253,25 @@ questions that you know someone might ask.
Each scenario can be a third-level heading, e.g. `### Getting error message X`.
If you have none to add when creating a doc, leave this section in place
but commented out to help encourage others to add to it in the future. -->
## License list
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/13582) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.7.
The License list allows you to see your project's licenses and key
details about them.
In order for the licenses to appear under the license list, the following
requirements must be met:
1. The License Compliance CI job must be [configured](#configuration) for your project.
1. Your project must use at least one of the
[supported languages and package managers](#supported-languages-and-package-managers).
Once everything is set, navigate to **Security & Compliance > License Compliance**
in your project's sidebar, and you'll see the licenses displayed, where:
- **Name:** The name of the license.
- **Component:** The components which have this license.
![License List](img/license_list_v12_6.png)
# frozen_string_literal: true
require "nokogiri"
require "asciidoctor-plantuml/plantuml"
require "asciidoctor_plantuml/plantuml"
module Banzai
module Filter
......
......@@ -11,6 +11,7 @@ module Gitlab
Status::Build::Manual,
Status::Build::Canceled,
Status::Build::Created,
Status::Build::WaitingForResource,
Status::Build::Preparing,
Status::Build::Pending,
Status::Build::Skipped],
......
# frozen_string_literal: true
module Gitlab
module Ci
module Status
module Build
class WaitingForResource < Status::Extended
##
# TODO: image is shared with 'pending'
# until we get a dedicated one
#
def illustration
{
image: 'illustrations/pending_job_empty.svg',
size: 'svg-430',
title: _('This job is waiting for resource: ') + subject.resource_group.key
}
end
def self.matches?(build, _)
build.waiting_for_resource?
end
end
end
end
end
end
......@@ -25,6 +25,8 @@ module Gitlab
# 2. In other cases we assume that status is of that type
# based on what statuses are no longer valid based on the
# data set that we have
# rubocop: disable Metrics/CyclomaticComplexity
# rubocop: disable Metrics/PerceivedComplexity
def status
return if none?
......@@ -43,6 +45,8 @@ module Gitlab
'pending'
elsif any_of?(:running, :pending)
'running'
elsif any_of?(:waiting_for_resource)
'waiting_for_resource'
elsif any_of?(:manual)
'manual'
elsif any_of?(:scheduled)
......@@ -56,6 +60,8 @@ module Gitlab
end
end
end
# rubocop: enable Metrics/CyclomaticComplexity
# rubocop: enable Metrics/PerceivedComplexity
def warnings?
@status_set.include?(:success_with_warnings)
......
......@@ -20,7 +20,7 @@ module Gitlab
def core_status
Gitlab::Ci::Status
.const_get(@status.capitalize, false)
.const_get(@status.to_s.camelize, false)
.new(@subject, @user)
.extend(self.class.common_helpers)
end
......
# frozen_string_literal: true
module Gitlab
module Ci
module Status
class WaitingForResource < Status::Core
def text
s_('CiStatusText|waiting')
end
def label
s_('CiStatusLabel|waiting for resource')
end
def icon
'status_pending'
end
def favicon
'favicon_pending'
end
def group
'waiting-for-resource'
end
end
end
end
end
......@@ -35,16 +35,6 @@ module Gitlab
end
end
def committer_hash(email:, name:)
return if email.nil? || name.nil?
{
email: email,
name: name,
time: Time.now
}
end
def tag_name(ref)
ref = ref.to_s
if self.tag_ref?(ref)
......
......@@ -548,7 +548,9 @@ msgstr[0] ""
msgstr[1] ""
msgid "1 day"
msgstr ""
msgid_plural "%d days"
msgstr[0] ""
msgstr[1] ""
msgid "1 group"
msgid_plural "%d groups"
......@@ -560,6 +562,11 @@ msgid_plural "%{merge_requests} merged merge requests"
msgstr[0] ""
msgstr[1] ""
msgid "1 minute"
msgid_plural "%d minutes"
msgstr[0] ""
msgstr[1] ""
msgid "1 open issue"
msgid_plural "%{issues} open issues"
msgstr[0] ""
......@@ -3361,6 +3368,9 @@ msgstr ""
msgid "CiStatusLabel|waiting for manual action"
msgstr ""
msgid "CiStatusLabel|waiting for resource"
msgstr ""
msgid "CiStatusText|blocked"
msgstr ""
......@@ -3391,6 +3401,9 @@ msgstr ""
msgid "CiStatusText|skipped"
msgstr ""
msgid "CiStatusText|waiting"
msgstr ""
msgid "CiStatus|running"
msgstr ""
......@@ -17457,7 +17470,7 @@ msgstr ""
msgid "Successfully unlocked"
msgstr ""
msgid "Suggest code changes which are immediately applied. Try it out!"
msgid "Suggest code changes which can be immediately applied in one click. Try it out!"
msgstr ""
msgid "Suggested change"
......@@ -18511,6 +18524,9 @@ msgstr ""
msgid "This job is stuck because you don't have any active runners that can run this job."
msgstr ""
msgid "This job is waiting for resource: "
msgstr ""
msgid "This job requires a manual action"
msgstr ""
......@@ -21114,6 +21130,11 @@ msgstr ""
msgid "a design"
msgstr ""
msgid "about 1 hour"
msgid_plural "about %d hours"
msgstr[0] ""
msgstr[1] ""
msgid "added %{created_at_timeago}"
msgstr ""
......@@ -21704,6 +21725,9 @@ msgstr ""
msgid "leave %{group_name}"
msgstr ""
msgid "less than a minute"
msgstr ""
msgid "limit of %{project_limit} reached"
msgstr ""
......
......@@ -77,6 +77,10 @@ FactoryBot.define do
status { 'created' }
end
trait :waiting_for_resource do
status { 'waiting_for_resource' }
end
trait :preparing do
status { 'preparing' }
end
......
......@@ -35,6 +35,10 @@ FactoryBot.define do
status { 'pending' }
end
trait :waiting_for_resource do
status { 'waiting_for_resource' }
end
trait :preparing do
status { 'preparing' }
end
......
......@@ -606,6 +606,117 @@ describe 'Pipeline', :js do
end
end
context 'when build requires resource', :sidekiq_inline do
let_it_be(:project) { create(:project, :repository) }
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:resource_group) { create(:ci_resource_group, project: project) }
let!(:test_job) do
create(:ci_build, :pending, stage: 'test', name: 'test',
stage_idx: 1, pipeline: pipeline, project: project)
end
let!(:deploy_job) do
create(:ci_build, :created, stage: 'deploy', name: 'deploy',
stage_idx: 2, pipeline: pipeline, project: project, resource_group: resource_group)
end
describe 'GET /:project/pipelines/:id' do
subject { visit project_pipeline_path(project, pipeline) }
it 'shows deploy job as created' do
subject
within('.pipeline-header-container') do
expect(page).to have_content('pending')
end
within('.pipeline-graph') do
within '.stage-column:nth-child(1)' do
expect(page).to have_content('test')
expect(page).to have_css('.ci-status-icon-pending')
end
within '.stage-column:nth-child(2)' do
expect(page).to have_content('deploy')
expect(page).to have_css('.ci-status-icon-created')
end
end
end
context 'when test job succeeded' do
before do
test_job.success!
end
it 'shows deploy job as pending' do
subject
within('.pipeline-header-container') do
expect(page).to have_content('running')
end
within('.pipeline-graph') do
within '.stage-column:nth-child(1)' do
expect(page).to have_content('test')
expect(page).to have_css('.ci-status-icon-success')
end
within '.stage-column:nth-child(2)' do
expect(page).to have_content('deploy')
expect(page).to have_css('.ci-status-icon-pending')
end
end
end
end
context 'when test job succeeded but there are no available resources' do
let(:another_job) { create(:ci_build, :running, project: project, resource_group: resource_group) }
before do
resource_group.assign_resource_to(another_job)
test_job.success!
end
it 'shows deploy job as waiting for resource' do
subject
within('.pipeline-header-container') do
expect(page).to have_content('waiting')
end
within('.pipeline-graph') do
within '.stage-column:nth-child(2)' do
expect(page).to have_content('deploy')
expect(page).to have_css('.ci-status-icon-waiting-for-resource')
end
end
end
context 'when resource is released from another job' do
before do
another_job.success!
end
it 'shows deploy job as pending' do
subject
within('.pipeline-header-container') do
expect(page).to have_content('running')
end
within('.pipeline-graph') do
within '.stage-column:nth-child(2)' do
expect(page).to have_content('deploy')
expect(page).to have_css('.ci-status-icon-pending')
end
end
end
end
end
end
end
describe 'GET /:project/pipelines/:id/builds' do
include_context 'pipeline builds'
......
......@@ -341,6 +341,16 @@ describe('prettyTime methods', () => {
assertTimeUnits(twoDays, 3, 48, 0, 0);
});
it('should correctly parse values when limitedToDays is true', () => {
const sevenDays = datetimeUtility.parseSeconds(648750, {
hoursPerDay: 24,
daysPerWeek: 7,
limitToDays: true,
});
assertTimeUnits(sevenDays, 12, 12, 7, 0);
});
});
describe('stringifyTime', () => {
......@@ -507,3 +517,32 @@ describe('secondsToDays', () => {
expect(datetimeUtility.secondsToDays(270000)).toBe(3);
});
});
describe('approximateDuration', () => {
it.each`
seconds
${null}
${{}}
${[]}
${-1}
`('returns a blank string for seconds=$seconds', ({ seconds }) => {
expect(datetimeUtility.approximateDuration(seconds)).toBe('');
});
it.each`
seconds | approximation
${0} | ${'less than a minute'}
${25} | ${'less than a minute'}
${45} | ${'1 minute'}
${90} | ${'1 minute'}
${100} | ${'1 minute'}
${150} | ${'2 minutes'}
${220} | ${'3 minutes'}
${3000} | ${'about 1 hour'}
${30000} | ${'about 8 hours'}
${100000} | ${'1 day'}
${180000} | ${'2 days'}
`('converts $seconds seconds to $approximation', ({ seconds, approximation }) => {
expect(datetimeUtility.approximateDuration(seconds)).toBe(approximation);
});
});
......@@ -26,7 +26,7 @@ describe Banzai::Filter::PlantumlFilter do
it 'does not replace plantuml pre tag with img tag if url is invalid' do
stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid")
input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
output = '<div class="listingblock"><div class="content"><pre class="plantuml plantuml-error"> PlantUML Error: cannot connect to PlantUML server at "invalid"</pre></div></div>'
output = '<div class="listingblock"><div class="content"><pre class="plantuml plantuml-error"> Error: cannot connect to PlantUML server at "invalid"</pre></div></div>'
doc = filter(input)
expect(doc.to_s).to eq output
......
......@@ -22,7 +22,7 @@ describe Gitlab::Ci::Status::External::Factory do
end
let(:expected_status) do
Gitlab::Ci::Status.const_get(simple_status.capitalize, false)
Gitlab::Ci::Status.const_get(simple_status.to_s.camelize, false)
end
it "fabricates a core status #{simple_status}" do
......
......@@ -13,7 +13,7 @@ describe Gitlab::Ci::Status::Factory do
let(:resource) { double('resource', status: simple_status) }
let(:expected_status) do
Gitlab::Ci::Status.const_get(simple_status.capitalize, false)
Gitlab::Ci::Status.const_get(simple_status.to_s.camelize, false)
end
it "fabricates a core status #{simple_status}" do
......
......@@ -18,7 +18,7 @@ describe Gitlab::Ci::Status::Pipeline::Factory do
let(:pipeline) { create(:ci_pipeline, status: simple_status) }
let(:expected_status) do
Gitlab::Ci::Status.const_get(simple_status.capitalize, false)
Gitlab::Ci::Status.const_get(simple_status.camelize, false)
end
it "matches correct core status for #{simple_status}" do
......
......@@ -34,7 +34,7 @@ describe Gitlab::Ci::Status::Stage::Factory do
it "fabricates a core status #{core_status}" do
expect(status).to be_a(
Gitlab::Ci::Status.const_get(core_status.capitalize, false))
Gitlab::Ci::Status.const_get(core_status.camelize, false))
end
it 'extends core status with common stage methods' do
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Status::WaitingForResource do
subject do
described_class.new(double('subject'), double('user'))
end
describe '#text' do
it { expect(subject.text).to eq 'waiting' }
end
describe '#label' do
it { expect(subject.label).to eq 'waiting for resource' }
end
describe '#icon' do
it { expect(subject.icon).to eq 'status_pending' }
end
describe '#favicon' do
it { expect(subject.favicon).to eq 'favicon_pending' }
end
describe '#group' do
it { expect(subject.group).to eq 'waiting-for-resource' }
end
end
......@@ -71,9 +71,7 @@ describe Gitlab::Git::Branch, :seed_helper do
end
let(:user) { create(:user) }
let(:committer) do
Gitlab::Git.committer_hash(email: user.email, name: user.name)
end
let(:committer) { { email: user.email, name: user.name } }
let(:params) do
parents = [rugged.head.target]
tree = parents.first.tree
......
......@@ -6,32 +6,6 @@ describe Gitlab::Git do
let(:committer_email) { 'user@example.org' }
let(:committer_name) { 'John Doe' }
describe 'committer_hash' do
it "returns a hash containing the given email and name" do
committer_hash = described_class.committer_hash(email: committer_email, name: committer_name)
expect(committer_hash[:email]).to eq(committer_email)
expect(committer_hash[:name]).to eq(committer_name)
expect(committer_hash[:time]).to be_a(Time)
end
context 'when email is nil' do
it "returns nil" do
committer_hash = described_class.committer_hash(email: nil, name: committer_name)
expect(committer_hash).to be_nil
end
end
context 'when name is nil' do
it "returns nil" do
committer_hash = described_class.committer_hash(email: committer_email, name: nil)
expect(committer_hash).to be_nil
end
end
end
describe '.ref_name' do
it 'ensure ref is a valid UTF-8 string' do
utf8_invalid_ref = Gitlab::Git::BRANCH_REF_PREFIX + "an_invalid_ref_\xE5"
......
......@@ -1119,6 +1119,60 @@ describe Ci::Build do
end
end
describe 'state transition with resource group' do
let(:resource_group) { create(:ci_resource_group, project: project) }
context 'when build status is created' do
let(:build) { create(:ci_build, :created, project: project, resource_group: resource_group) }
it 'is waiting for resource when build is enqueued' do
expect(Ci::ResourceGroups::AssignResourceFromResourceGroupWorker).to receive(:perform_async).with(resource_group.id)
expect { build.enqueue! }.to change { build.status }.from('created').to('waiting_for_resource')
expect(build.waiting_for_resource_at).not_to be_nil
end
context 'when build is waiting for resource' do
before do
build.update_column(:status, 'waiting_for_resource')
end
it 'is enqueued when build requests resource' do
expect { build.enqueue_waiting_for_resource! }.to change { build.status }.from('waiting_for_resource').to('pending')
end
it 'releases a resource when build finished' do
expect(build.resource_group).to receive(:release_resource_from).with(build).and_call_original
expect(Ci::ResourceGroups::AssignResourceFromResourceGroupWorker).to receive(:perform_async).with(build.resource_group_id)
build.enqueue_waiting_for_resource!
build.success!
end
context 'when build has prerequisites' do
before do
allow(build).to receive(:any_unmet_prerequisites?) { true }
end
it 'is preparing when build is enqueued' do
expect { build.enqueue_waiting_for_resource! }.to change { build.status }.from('waiting_for_resource').to('preparing')
end
end
context 'when there are no available resources' do
before do
resource_group.assign_resource_to(create(:ci_build))
end
it 'stays as waiting for resource when build requests resource' do
expect { build.enqueue_waiting_for_resource }.not_to change { build.status }
end
end
end
end
end
describe '#on_stop' do
subject { build.on_stop }
......
......@@ -1750,7 +1750,7 @@ describe Ci::Pipeline, :mailer do
subject { described_class.bridgeable_statuses }
it { is_expected.to be_an(Array) }
it { is_expected.not_to include('created', 'preparing', 'pending') }
it { is_expected.not_to include('created', 'waiting_for_resource', 'preparing', 'pending') }
end
describe '#status', :sidekiq_might_not_need_inline do
......@@ -1760,6 +1760,17 @@ describe Ci::Pipeline, :mailer do
subject { pipeline.reload.status }
context 'on waiting for resource' do
before do
allow(build).to receive(:requires_resource?) { true }
allow(Ci::ResourceGroups::AssignResourceFromResourceGroupWorker).to receive(:perform_async)
build.enqueue
end
it { is_expected.to eq('waiting_for_resource') }
end
context 'on prepare' do
before do
# Prevent skipping directly to 'pending'
......
......@@ -105,6 +105,18 @@ describe Ci::Stage, :models do
end
end
context 'when build is waiting for resource' do
before do
create(:ci_build, :waiting_for_resource, stage_id: stage.id)
end
it 'updates status to waiting for resource' do
expect { stage.update_status }
.to change { stage.reload.status }
.to 'waiting_for_resource'
end
end
context 'when stage is skipped because is empty' do
it 'updates status to skipped' do
expect { stage.update_status }
......
......@@ -634,6 +634,30 @@ describe CommitStatus do
end
end
describe '#all_met_to_become_pending?' do
subject { commit_status.all_met_to_become_pending? }
let(:commit_status) { create(:commit_status) }
it { is_expected.to eq(true) }
context 'when build requires a resource' do
before do
allow(commit_status).to receive(:requires_resource?) { true }
end
it { is_expected.to eq(false) }
end
context 'when build has a prerequisite' do
before do
allow(commit_status).to receive(:any_unmet_prerequisites?) { true }
end
it { is_expected.to eq(false) }
end
end
describe '#enqueue' do
let!(:current_time) { Time.new(2018, 4, 5, 14, 0, 0) }
......@@ -654,12 +678,6 @@ describe CommitStatus do
it_behaves_like 'commit status enqueued'
end
context 'when initial state is :preparing' do
let(:commit_status) { create(:commit_status, :preparing) }
it_behaves_like 'commit status enqueued'
end
context 'when initial state is :skipped' do
let(:commit_status) { create(:commit_status, :skipped) }
......
......@@ -39,6 +39,22 @@ describe HasStatus do
it { is_expected.to eq 'running' }
end
context 'all waiting for resource' do
let!(:statuses) do
[create(type, status: :waiting_for_resource), create(type, status: :waiting_for_resource)]
end
it { is_expected.to eq 'waiting_for_resource' }
end
context 'at least one waiting for resource' do
let!(:statuses) do
[create(type, status: :success), create(type, status: :waiting_for_resource)]
end
it { is_expected.to eq 'waiting_for_resource' }
end
context 'all preparing' do
let!(:statuses) do
[create(type, status: :preparing), create(type, status: :preparing)]
......@@ -219,7 +235,7 @@ describe HasStatus do
end
end
%i[created preparing running pending success
%i[created waiting_for_resource preparing running pending success
failed canceled skipped].each do |status|
it_behaves_like 'having a job', status
end
......@@ -265,7 +281,7 @@ describe HasStatus do
describe '.alive' do
subject { CommitStatus.alive }
%i[running pending preparing created].each do |status|
%i[running pending waiting_for_resource preparing created].each do |status|
it_behaves_like 'containing the job', status
end
......@@ -277,7 +293,7 @@ describe HasStatus do
describe '.alive_or_scheduled' do
subject { CommitStatus.alive_or_scheduled }
%i[running pending preparing created scheduled].each do |status|
%i[running pending waiting_for_resource preparing created scheduled].each do |status|
it_behaves_like 'containing the job', status
end
......@@ -313,7 +329,7 @@ describe HasStatus do
describe '.cancelable' do
subject { CommitStatus.cancelable }
%i[running pending preparing created scheduled].each do |status|
%i[running pending waiting_for_resource preparing created scheduled].each do |status|
it_behaves_like 'containing the job', status
end
......
......@@ -14,7 +14,7 @@ describe Ci::PrepareBuildService do
shared_examples 'build enqueueing' do
it 'enqueues the build' do
expect(build).to receive(:enqueue).once
expect(build).to receive(:enqueue_preparing).once
subject
end
......@@ -34,7 +34,7 @@ describe Ci::PrepareBuildService do
context 'prerequisites fail to complete' do
before do
allow(build).to receive(:enqueue).and_return(false)
allow(build).to receive(:enqueue_preparing).and_return(false)
end
it 'drops the build' do
......
......@@ -261,12 +261,16 @@ describe Ci::ProcessPipelineService, '#execute' do
expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'scheduled' })
enqueue_scheduled('rollout10%')
Timecop.travel 2.minutes.from_now do
enqueue_scheduled('rollout10%')
end
succeed_pending
expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'success', 'rollout100%': 'scheduled' })
enqueue_scheduled('rollout100%')
Timecop.travel 2.minutes.from_now do
enqueue_scheduled('rollout100%')
end
succeed_pending
expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'success', 'rollout100%': 'success', 'cleanup': 'pending' })
......@@ -328,7 +332,9 @@ describe Ci::ProcessPipelineService, '#execute' do
expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'scheduled' })
enqueue_scheduled('rollout10%')
Timecop.travel 2.minutes.from_now do
enqueue_scheduled('rollout10%')
end
fail_running_or_pending
expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'failed' })
......@@ -394,7 +400,9 @@ describe Ci::ProcessPipelineService, '#execute' do
expect(process_pipeline).to be_truthy
expect(builds_names_and_statuses).to eq({ 'delayed1': 'scheduled', 'delayed2': 'scheduled' })
enqueue_scheduled('delayed1')
Timecop.travel 2.minutes.from_now do
enqueue_scheduled('delayed1')
end
expect(builds_names_and_statuses).to eq({ 'delayed1': 'pending', 'delayed2': 'scheduled' })
expect(pipeline.reload.status).to eq 'running'
......@@ -413,7 +421,9 @@ describe Ci::ProcessPipelineService, '#execute' do
expect(process_pipeline).to be_truthy
expect(builds_names_and_statuses).to eq({ 'delayed': 'scheduled' })
enqueue_scheduled('delayed')
Timecop.travel 2.minutes.from_now do
enqueue_scheduled('delayed')
end
fail_running_or_pending
expect(builds_names_and_statuses).to eq({ 'delayed': 'failed', 'job': 'pending' })
......@@ -906,7 +916,7 @@ describe Ci::ProcessPipelineService, '#execute' do
end
def enqueue_scheduled(name)
builds.scheduled.find_by(name: name).enqueue
builds.scheduled.find_by(name: name).enqueue_scheduled
end
def retry_build(name)
......
# frozen_string_literal: true
require 'spec_helper'
describe Ci::ResourceGroups::AssignResourceFromResourceGroupService do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let(:service) { described_class.new(project, user) }
describe '#execute' do
subject { service.execute(resource_group) }
let(:resource_group) { create(:ci_resource_group, project: project) }
let!(:build) { create(:ci_build, :waiting_for_resource, project: project, user: user, resource_group: resource_group) }
context 'when there is an available resource' do
it 'requests resource' do
subject
expect(build.reload).to be_pending
expect(build.resource).to be_present
end
context 'when failed to request resource' do
before do
allow_next_instance_of(Ci::Build) do |build|
allow(build).to receive(:enqueue_waiting_for_resource) { false }
end
end
it 'has a build waiting for resource' do
subject
expect(build).to be_waiting_for_resource
end
end
context 'when the build has already retained a resource' do
before do
resource_group.assign_resource_to(build)
build.update_column(:status, :pending)
end
it 'has a pending build' do
subject
expect(build).to be_pending
end
end
end
context 'when there are no available resources' do
before do
resource_group.assign_resource_to(create(:ci_build))
end
it 'does not request resource' do
expect_any_instance_of(Ci::Build).not_to receive(:enqueue_waiting_for_resource)
subject
end
end
end
end
......@@ -26,6 +26,18 @@ describe Ci::RunScheduledBuildService do
expect(build).to be_pending
end
context 'when build requires resource' do
let(:resource_group) { create(:ci_resource_group, project: project) }
before do
build.update!(resource_group: resource_group)
end
it 'transits to waiting for resource status' do
expect { subject }.to change { build.status }.from('scheduled').to('waiting_for_resource')
end
end
end
context 'when scheduled_at is not expired' do
......
# frozen_string_literal: true
require 'spec_helper'
describe Ci::ResourceGroups::AssignResourceFromResourceGroupWorker do
let(:worker) { described_class.new }
describe '#perform' do
subject { worker.perform(resource_group_id) }
context 'when resource group exists' do
let(:resource_group) { create(:ci_resource_group) }
let(:resource_group_id) { resource_group.id }
it 'executes AssignResourceFromResourceGroupService' do
expect_next_instance_of(Ci::ResourceGroups::AssignResourceFromResourceGroupService, resource_group.project, nil) do |service|
expect(service).to receive(:execute).with(resource_group)
end
subject
end
end
context 'when build does not exist' do
let(:resource_group_id) { 123 }
it 'does not execute AssignResourceFromResourceGroupService' do
expect(Ci::ResourceGroups::AssignResourceFromResourceGroupService).not_to receive(:new)
subject
end
end
end
end
......@@ -230,8 +230,8 @@ describe GitGarbageCollectWorker do
new_commit_sha = Rugged::Commit.create(
rugged,
message: "hello world #{SecureRandom.hex(6)}",
author: Gitlab::Git.committer_hash(email: 'foo@bar', name: 'baz'),
committer: Gitlab::Git.committer_hash(email: 'foo@bar', name: 'baz'),
author: { email: 'foo@bar', name: 'baz' },
committer: { email: 'foo@bar', name: 'baz' },
tree: old_commit.tree,
parents: [old_commit]
)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment