Commit b2c281b8 authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-10-17

# Conflicts:
#	CHANGELOG.md
#	Gemfile.lock
#	Gemfile.rails5.lock
#	app/assets/javascripts/jobs/components/job_app.vue
#	app/assets/javascripts/jobs/store/getters.js
#	app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
#	locale/gitlab.pot
#	qa/qa/page/project/menu.rb
#	spec/javascripts/pipelines/graph/stage_column_component_spec.js

[ci skip]
parents 0bc528ae 1d7453d3
...@@ -3,6 +3,18 @@ documentation](doc/development/changelog.md) for instructions on adding your own ...@@ -3,6 +3,18 @@ documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 11.3.6 (2018-10-17) ## 11.3.6 (2018-10-17)
<<<<<<< HEAD
=======
- No changes.
## 11.3.5 (2018-10-15)
### Fixed (2 changes)
- Fix loading issue on some merge request discussion. !21982
- Fix project deletion when there is a export available. !22276
>>>>>>> upstream/master
- No changes. - No changes.
......
...@@ -302,7 +302,10 @@ GEM ...@@ -302,7 +302,10 @@ GEM
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.10) grpc (~> 1.10)
github-markup (1.7.0) github-markup (1.7.0)
<<<<<<< HEAD
gitlab-license (1.0.0) gitlab-license (1.0.0)
=======
>>>>>>> upstream/master
gitlab-markup (1.6.4) gitlab-markup (1.6.4)
gitlab-sidekiq-fetcher (0.3.0) gitlab-sidekiq-fetcher (0.3.0)
sidekiq (~> 5) sidekiq (~> 5)
...@@ -1033,7 +1036,10 @@ DEPENDENCIES ...@@ -1033,7 +1036,10 @@ DEPENDENCIES
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.118.1) gitaly-proto (~> 0.118.1)
github-markup (~> 1.7.0) github-markup (~> 1.7.0)
<<<<<<< HEAD
gitlab-license (~> 1.0) gitlab-license (~> 1.0)
=======
>>>>>>> upstream/master
gitlab-markup (~> 1.6.4) gitlab-markup (~> 1.6.4)
gitlab-sidekiq-fetcher gitlab-sidekiq-fetcher
gitlab-styles (~> 2.4) gitlab-styles (~> 2.4)
......
...@@ -305,7 +305,10 @@ GEM ...@@ -305,7 +305,10 @@ GEM
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.10) grpc (~> 1.10)
github-markup (1.7.0) github-markup (1.7.0)
<<<<<<< HEAD
gitlab-license (1.0.0) gitlab-license (1.0.0)
=======
>>>>>>> upstream/master
gitlab-markup (1.6.4) gitlab-markup (1.6.4)
gitlab-sidekiq-fetcher (0.3.0) gitlab-sidekiq-fetcher (0.3.0)
sidekiq (~> 5) sidekiq (~> 5)
...@@ -1042,7 +1045,10 @@ DEPENDENCIES ...@@ -1042,7 +1045,10 @@ DEPENDENCIES
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.118.1) gitaly-proto (~> 0.118.1)
github-markup (~> 1.7.0) github-markup (~> 1.7.0)
<<<<<<< HEAD
gitlab-license (~> 1.0) gitlab-license (~> 1.0)
=======
>>>>>>> upstream/master
gitlab-markup (~> 1.6.4) gitlab-markup (~> 1.6.4)
gitlab-sidekiq-fetcher gitlab-sidekiq-fetcher
gitlab-styles (~> 2.4) gitlab-styles (~> 2.4)
......
...@@ -18,27 +18,28 @@ export default { ...@@ -18,27 +18,28 @@ export default {
}; };
</script> </script>
<template> <template>
<div class="blank-state-row"> <div class="empty-state">
<div class="blank-state-center"> <div class="text-content">
<h2 class="blank-state-title js-blank-state-title"> <h4 class="blank-state-title js-blank-state-title">
{{ s__("Environments|You don't have any environments right now.") }} {{ s__("Environments|You don't have any environments right now") }}
</h2> </h4>
<p class="blank-state-text"> <p class="blank-state-text">
{{ s__(`Environments|Environments are places where {{ s__(`Environments|Environments are places where
code gets deployed, such as staging or production.`) }} code gets deployed, such as staging or production.`) }}
<br />
<a :href="helpPath"> <a :href="helpPath">
{{ s__("Environments|Read more about environments") }} {{ s__("Environments|Read more about environments") }}
</a> </a>
</p> </p>
<a <div class="text-center">
v-if="canCreateEnvironment" <a
:href="newPath" v-if="canCreateEnvironment"
class="btn btn-success js-new-environment-button" :href="newPath"
> class="btn btn-success js-new-environment-button"
{{ s__("Environments|New environment") }} >
</a> {{ s__("Environments|New environment") }}
</a>
</div>
</div> </div>
</div> </div>
</template> </template>
...@@ -5,9 +5,12 @@ ...@@ -5,9 +5,12 @@
import bp from '~/breakpoints'; import bp from '~/breakpoints';
import CiHeader from '~/vue_shared/components/header_ci_component.vue'; import CiHeader from '~/vue_shared/components/header_ci_component.vue';
import Callout from '~/vue_shared/components/callout.vue'; import Callout from '~/vue_shared/components/callout.vue';
<<<<<<< HEAD
// ee-only start // ee-only start
import SharedRunner from 'ee/jobs/components/shared_runner_limit_block.vue'; import SharedRunner from 'ee/jobs/components/shared_runner_limit_block.vue';
// ee-only end // ee-only end
=======
>>>>>>> upstream/master
import createStore from '../store'; import createStore from '../store';
import EmptyState from './empty_state.vue'; import EmptyState from './empty_state.vue';
import EnvironmentsBlock from './environments_block.vue'; import EnvironmentsBlock from './environments_block.vue';
...@@ -29,7 +32,10 @@ ...@@ -29,7 +32,10 @@
Log, Log,
LogTopBar, LogTopBar,
StuckBlock, StuckBlock,
<<<<<<< HEAD
SharedRunner, SharedRunner,
=======
>>>>>>> upstream/master
Sidebar, Sidebar,
}, },
props: { props: {
...@@ -207,6 +213,7 @@ ...@@ -207,6 +213,7 @@
:runners-path="runnerSettingsUrl" :runners-path="runnerSettingsUrl"
/> />
<<<<<<< HEAD
<shared-runner <shared-runner
v-if="shouldRenderSharedRunnerLimitWarning" v-if="shouldRenderSharedRunnerLimitWarning"
class="js-shared-runner-limit" class="js-shared-runner-limit"
...@@ -215,6 +222,8 @@ ...@@ -215,6 +222,8 @@
:runners-path="runnerHelpUrl" :runners-path="runnerHelpUrl"
/> />
=======
>>>>>>> upstream/master
<environments-block <environments-block
v-if="hasEnvironment" v-if="hasEnvironment"
class="js-job-environment" class="js-job-environment"
...@@ -230,8 +239,13 @@ ...@@ -230,8 +239,13 @@
/> />
<!--job log --> <!--job log -->
<<<<<<< HEAD
<div <div
v-if="hasTrace" v-if="hasTrace"
=======
<div
v-if="hasTrace"
>>>>>>> upstream/master
class="build-trace-container prepend-top-default"> class="build-trace-container prepend-top-default">
<log-top-bar <log-top-bar
:class="{ :class="{
......
...@@ -51,6 +51,7 @@ export const isJobStuck = state => ...@@ -51,6 +51,7 @@ export const isJobStuck = state =>
(!_.isEmpty(state.job.status) && state.job.status.group === 'pending') && (!_.isEmpty(state.job.status) && state.job.status.group === 'pending') &&
(!_.isEmpty(state.job.runners) && state.job.runners.available === false); (!_.isEmpty(state.job.runners) && state.job.runners.available === false);
<<<<<<< HEAD
// ee-only start // ee-only start
/** /**
* Shared runners limit is only rendered when * Shared runners limit is only rendered when
...@@ -64,6 +65,8 @@ export const shouldRenderSharedRunnerLimitWarning = state => ...@@ -64,6 +65,8 @@ export const shouldRenderSharedRunnerLimitWarning = state =>
state.job.runners.quota.used >= state.job.runners.quota.limit; state.job.runners.quota.used >= state.job.runners.quota.limit;
// ee-only end // ee-only end
=======
>>>>>>> upstream/master
export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete; export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete;
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
......
...@@ -31,12 +31,15 @@ export default { ...@@ -31,12 +31,15 @@ export default {
default: '', default: '',
}, },
<<<<<<< HEAD
hasTriggeredBy: { hasTriggeredBy: {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
}, },
=======
>>>>>>> upstream/master
methods: { methods: {
groupId(group) { groupId(group) {
return `ci-badge-${_.escape(group.name)}`; return `ci-badge-${_.escape(group.name)}`;
......
...@@ -8,7 +8,7 @@ module SendsBlob ...@@ -8,7 +8,7 @@ module SendsBlob
include SendFileUpload include SendFileUpload
end end
def send_blob(blob, params = {}) def send_blob(repository, blob, params = {})
if blob if blob
headers['X-Content-Type-Options'] = 'nosniff' headers['X-Content-Type-Options'] = 'nosniff'
......
...@@ -8,7 +8,7 @@ class Projects::AvatarsController < Projects::ApplicationController ...@@ -8,7 +8,7 @@ class Projects::AvatarsController < Projects::ApplicationController
def show def show
@blob = @repository.blob_at_branch(@repository.root_ref, @project.avatar_in_git) @blob = @repository.blob_at_branch(@repository.root_ref, @project.avatar_in_git)
send_blob(@blob) send_blob(@repository, @blob)
end end
def destroy def destroy
......
...@@ -83,7 +83,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -83,7 +83,7 @@ class Projects::BlobController < Projects::ApplicationController
def destroy def destroy
create_commit(Files::DeleteService, success_notice: "The file has been successfully deleted.", create_commit(Files::DeleteService, success_notice: "The file has been successfully deleted.",
success_path: -> { project_tree_path(@project, @branch_name) }, success_path: -> { after_delete_path },
failure_view: :show, failure_view: :show,
failure_path: project_blob_path(@project, @id)) failure_path: project_blob_path(@project, @id))
end end
...@@ -191,6 +191,15 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -191,6 +191,15 @@ class Projects::BlobController < Projects::ApplicationController
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def after_delete_path
branch = BranchesFinder.new(@repository, search: @ref).execute.first
if @repository.tree(branch.target, tree_path).entries.empty?
project_tree_path(@project, @ref)
else
project_tree_path(@project, File.join(@ref, tree_path))
end
end
def editor_variables def editor_variables
@branch_name = params[:branch_name] @branch_name = params[:branch_name]
...@@ -255,9 +264,6 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -255,9 +264,6 @@ class Projects::BlobController < Projects::ApplicationController
def show_json def show_json
set_last_commit_sha set_last_commit_sha
path_segments = @path.split('/')
path_segments.pop
tree_path = path_segments.join('/')
json = { json = {
id: @blob.id, id: @blob.id,
...@@ -283,4 +289,8 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -283,4 +289,8 @@ class Projects::BlobController < Projects::ApplicationController
render json: json render json: json
end end
def tree_path
@path.rpartition('/').first
end
end end
...@@ -12,6 +12,6 @@ class Projects::RawController < Projects::ApplicationController ...@@ -12,6 +12,6 @@ class Projects::RawController < Projects::ApplicationController
def show def show
@blob = @repository.blob_at(@commit.id, @path) @blob = @repository.blob_at(@commit.id, @path)
send_blob(@blob, inline: (params[:inline] != 'false')) send_blob(@repository, @blob, inline: (params[:inline] != 'false'))
end end
end end
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
class Projects::WikisController < Projects::ApplicationController class Projects::WikisController < Projects::ApplicationController
include PreviewMarkdown include PreviewMarkdown
include SendsBlob
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
before_action :authorize_read_wiki! before_action :authorize_read_wiki!
...@@ -26,16 +27,8 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -26,16 +27,8 @@ class Projects::WikisController < Projects::ApplicationController
set_encoding_error unless valid_encoding? set_encoding_error unless valid_encoding?
render 'show' render 'show'
elsif file = @project_wiki.find_file(params[:id], params[:version_id]) elsif file_blob
response.headers['Content-Security-Policy'] = "default-src 'none'" send_blob(@project_wiki.repository, file_blob)
response.headers['X-Content-Security-Policy'] = "default-src 'none'"
send_data(
file.raw_data,
type: file.mime_type,
disposition: 'inline',
filename: file.name
)
elsif can?(current_user, :create_wiki, @project) && view_param == 'create' elsif can?(current_user, :create_wiki, @project) && view_param == 'create'
@page = build_page(title: params[:id]) @page = build_page(title: params[:id])
...@@ -164,4 +157,14 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -164,4 +157,14 @@ class Projects::WikisController < Projects::ApplicationController
def set_encoding_error def set_encoding_error
flash.now[:notice] = "The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository." flash.now[:notice] = "The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository."
end end
def file_blob
strong_memoize(:file_blob) do
commit = @project_wiki.repository.commit(@project_wiki.default_branch)
next unless commit
@project_wiki.repository.blob_at(commit.id, params[:id])
end
end
end end
...@@ -150,7 +150,9 @@ module BlobHelper ...@@ -150,7 +150,9 @@ module BlobHelper
# example of Javascript) we tell the browser of the victim not to # example of Javascript) we tell the browser of the victim not to
# execute untrusted data. # execute untrusted data.
def safe_content_type(blob) def safe_content_type(blob)
if blob.text? if blob.extension == 'svg'
blob.mime_type
elsif blob.text?
'text/plain; charset=utf-8' 'text/plain; charset=utf-8'
elsif blob.image? elsif blob.image?
blob.content_type blob.content_type
...@@ -159,6 +161,12 @@ module BlobHelper ...@@ -159,6 +161,12 @@ module BlobHelper
end end
end end
def content_disposition(blob, inline)
return 'attachment' if blob.extension == 'svg'
inline ? 'inline' : 'attachment'
end
def ref_project def ref_project
@ref_project ||= @target_project || @project @ref_project ||= @target_project || @project
end end
......
...@@ -6,7 +6,7 @@ module WorkhorseHelper ...@@ -6,7 +6,7 @@ module WorkhorseHelper
# Send a Git blob through Workhorse # Send a Git blob through Workhorse
def send_git_blob(repository, blob, inline: true) def send_git_blob(repository, blob, inline: true)
headers.store(*Gitlab::Workhorse.send_git_blob(repository, blob)) headers.store(*Gitlab::Workhorse.send_git_blob(repository, blob))
headers['Content-Disposition'] = inline ? 'inline' : 'attachment' headers['Content-Disposition'] = content_disposition(blob, inline)
headers['Content-Type'] = safe_content_type(blob) headers['Content-Type'] = safe_content_type(blob)
render plain: "" render plain: ""
end end
......
...@@ -14,6 +14,9 @@ class JiraService < IssueTrackerService ...@@ -14,6 +14,9 @@ class JiraService < IssueTrackerService
format: { with: Gitlab::Regex.jira_transition_id_regex, message: "transition ids can have only numbers which can be split with , or ;" }, format: { with: Gitlab::Regex.jira_transition_id_regex, message: "transition ids can have only numbers which can be split with , or ;" },
allow_blank: true allow_blank: true
# JIRA cloud version is deprecating authentication via username and password.
# We should use username/password for JIRA server and email/api_token for JIRA cloud,
# for more information check: https://gitlab.com/gitlab-org/gitlab-ce/issues/49936.
prop_accessor :username, :password, :url, :api_url, :jira_issue_transition_id, :title, :description prop_accessor :username, :password, :url, :api_url, :jira_issue_transition_id, :title, :description
before_update :reset_password before_update :reset_password
...@@ -95,8 +98,8 @@ class JiraService < IssueTrackerService ...@@ -95,8 +98,8 @@ class JiraService < IssueTrackerService
[ [
{ type: 'text', name: 'url', title: 'Web URL', placeholder: 'https://jira.example.com', required: true }, { type: 'text', name: 'url', title: 'Web URL', placeholder: 'https://jira.example.com', required: true },
{ type: 'text', name: 'api_url', title: 'JIRA API URL', placeholder: 'If different from Web URL' }, { type: 'text', name: 'api_url', title: 'JIRA API URL', placeholder: 'If different from Web URL' },
{ type: 'text', name: 'username', placeholder: '', required: true }, { type: 'text', name: 'username', title: 'Username or Email', placeholder: 'Use a username for server version and an email for cloud version', required: true },
{ type: 'password', name: 'password', placeholder: '', required: true }, { type: 'password', name: 'password', title: 'Password or API token', placeholder: 'Use a password for server version and an API token for cloud version', required: true },
{ type: 'text', name: 'jira_issue_transition_id', title: 'Transition ID(s)', placeholder: 'Use , or ; to separate multiple transition IDs' } { type: 'text', name: 'jira_issue_transition_id', title: 'Transition ID(s)', placeholder: 'Use , or ; to separate multiple transition IDs' }
] ]
end end
......
...@@ -56,7 +56,6 @@ class PrometheusService < MonitoringService ...@@ -56,7 +56,6 @@ class PrometheusService < MonitoringService
name: 'api_url', name: 'api_url',
title: 'API URL', title: 'API URL',
placeholder: s_('PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/'), placeholder: s_('PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/'),
help: s_('PrometheusService|By default, Prometheus listens on ‘http://localhost:9090’. It’s not recommended to change the default address and port as this might affect or conflict with other services running on the GitLab server.'),
required: true required: true
} }
] ]
......
...@@ -492,7 +492,20 @@ class Repository ...@@ -492,7 +492,20 @@ class Repository
end end
def blob_at(sha, path) def blob_at(sha, path)
Blob.decorate(raw_repository.blob_at(sha, path), project) blob = Blob.decorate(raw_repository.blob_at(sha, path), project)
# Don't attempt to return a special result if there is no blob at all
return unless blob
# Don't attempt to return a special result unless we're looking at HEAD
return blob unless head_commit&.sha == sha
case path
when head_tree&.readme_path
ReadmeBlob.new(blob, self)
else
blob
end
rescue Gitlab::Git::Repository::NoRepository rescue Gitlab::Git::Repository::NoRepository
nil nil
end end
...@@ -574,9 +587,7 @@ class Repository ...@@ -574,9 +587,7 @@ class Repository
cache_method :merge_request_template_names, fallback: [] cache_method :merge_request_template_names, fallback: []
def readme def readme
if readme = tree(:head)&.readme head_tree&.readme
ReadmeBlob.new(readme, self)
end
end end
def rendered_readme def rendered_readme
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
class Tree class Tree
include Gitlab::MarkupHelper include Gitlab::MarkupHelper
include Gitlab::Utils::StrongMemoize
attr_accessor :repository, :sha, :path, :entries attr_accessor :repository, :sha, :path, :entries
...@@ -16,32 +17,36 @@ class Tree ...@@ -16,32 +17,36 @@ class Tree
@entries = Gitlab::Git::Tree.where(git_repo, @sha, @path, recursive) @entries = Gitlab::Git::Tree.where(git_repo, @sha, @path, recursive)
end end
def readme def readme_path
return @readme if defined?(@readme) strong_memoize(:readme_path) do
available_readmes = blobs.select do |blob|
available_readmes = blobs.select do |blob| Gitlab::FileDetector.type_of(blob.name) == :readme
Gitlab::FileDetector.type_of(blob.name) == :readme end
end
previewable_readmes = available_readmes.select do |blob|
previewable_readmes = available_readmes.select do |blob| previewable?(blob.name)
previewable?(blob.name) end
end
plain_readmes = available_readmes.select do |blob|
plain_readmes = available_readmes.select do |blob| plain?(blob.name)
plain?(blob.name) end
# Prioritize previewable over plain readmes
entry = previewable_readmes.first || plain_readmes.first
next nil unless entry
if path == '/'
entry.name
else
File.join(path, entry.name)
end
end end
end
# Prioritize previewable over plain readmes def readme
readme_tree = previewable_readmes.first || plain_readmes.first strong_memoize(:readme) do
repository.blob_at(sha, readme_path) if readme_path
# Return if we can't preview any of them
if readme_tree.nil?
return @readme = nil
end end
readme_path = path == '/' ? readme_tree.name : File.join(path, readme_tree.name)
@readme = repository.blob_at(sha, readme_path)
end end
def trees def trees
......
...@@ -160,7 +160,9 @@ class WikiPage ...@@ -160,7 +160,9 @@ class WikiPage
# Returns boolean True or False if this instance # Returns boolean True or False if this instance
# is an old version of the page. # is an old version of the page.
def historical? def historical?
@page.historical? && last_version.sha != version.sha return false unless last_commit_sha && version
@page.historical? && last_commit_sha != version.sha
end end
# Returns boolean True or False if this instance # Returns boolean True or False if this instance
......
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
- if project_nav_tab? :issues - if project_nav_tab? :issues
= nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
= link_to project_issues_path(@project), class: 'shortcuts-issues' do = link_to project_issues_path(@project), class: 'shortcuts-issues qa-issues-item' do
.nav-icon-container .nav-icon-container
= sprite_icon('issues') = sprite_icon('issues')
%span.nav-item-name %span.nav-item-name
...@@ -115,7 +115,7 @@ ...@@ -115,7 +115,7 @@
= boards_link_text = boards_link_text
= nav_link(controller: :labels) do = nav_link(controller: :labels) do
= link_to project_labels_path(@project), title: _('Labels') do = link_to project_labels_path(@project), title: _('Labels'), class: 'qa-labels-link' do
%span %span
= _('Labels') = _('Labels')
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
- if can_admin_label - if can_admin_label
- content_for(:header_content) do - content_for(:header_content) do
.nav-controls .nav-controls
= link_to _('New label'), new_project_label_path(@project), class: "btn btn-success" = link_to _('New label'), new_project_label_path(@project), class: "btn btn-success qa-label-create-new"
- if labels_or_filters - if labels_or_filters
#promote-label-modal #promote-label-modal
......
...@@ -2,4 +2,5 @@ ...@@ -2,4 +2,5 @@
= link_to wiki_page.title, project_wiki_path(@project, wiki_page) = link_to wiki_page.title, project_wiki_path(@project, wiki_page)
%small (#{wiki_page.format}) %small (#{wiki_page.format})
.float-right .float-right
%small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.last_version.authored_date) }).html_safe - if wiki_page.last_version
%small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.last_version.authored_date) }).html_safe
...@@ -11,8 +11,9 @@ ...@@ -11,8 +11,9 @@
.nav-text .nav-text
%h2.wiki-page-title= @page.title.capitalize %h2.wiki-page-title= @page.title.capitalize
%span.wiki-last-edit-by %span.wiki-last-edit-by
= (_("Last edited by %{name}") % { name: "<strong>#{@page.last_version.author_name}</strong>" }).html_safe - if @page.last_version
#{time_ago_with_tooltip(@page.last_version.authored_date)} = (_("Last edited by %{name}") % { name: "<strong>#{@page.last_version.author_name}</strong>" }).html_safe
#{time_ago_with_tooltip(@page.last_version.authored_date)}
.nav-controls .nav-controls
= render 'main_links' = render 'main_links'
......
...@@ -24,6 +24,6 @@ ...@@ -24,6 +24,6 @@
- elsif type == 'select' - elsif type == 'select'
= form.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control", disabled: disabled} = form.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control", disabled: disabled}
- elsif type == 'password' - elsif type == 'password'
= form.password_field name, autocomplete: "new-password", class: "form-control", required: value.blank? && required, disabled: disabled = form.password_field name, autocomplete: "new-password", placeholder: placeholder, class: "form-control", required: value.blank? && required, disabled: disabled
- if help - if help
%span.form-text.text-muted= help %span.form-text.text-muted= help
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
= hidden_field_tag data_options[:field_name], use_id ? label.try(:id) : label.try(:title), id: nil = hidden_field_tag data_options[:field_name], use_id ? label.try(:id) : label.try(:title), id: nil
.dropdown .dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect{ class: classes.join(' '), type: "button", data: dropdown_data } %button.dropdown-menu-toggle.js-label-select.js-multiselect.qa-issuable-label{ class: classes.join(' '), type: "button", data: dropdown_data }
- apply_is_default_styles = (selected.nil? || selected.empty?) && !no_default_styles - apply_is_default_styles = (selected.nil? || selected.empty?) && !no_default_styles
%span.dropdown-toggle-text{ class: ("is-default" if apply_is_default_styles) } %span.dropdown-toggle-text{ class: ("is-default" if apply_is_default_styles) }
= multi_label_name(selected, label_name) = multi_label_name(selected, label_name)
......
...@@ -98,7 +98,7 @@ ...@@ -98,7 +98,7 @@
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable - if can_edit_issuable
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right' = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
.value.issuable-show-labels.dont-hide.hide-collapsed{ class: ("has-labels" if selected_labels.any?) } .value.issuable-show-labels.dont-hide.hide-collapsed.qa-labels-block{ class: ("has-labels" if selected_labels.any?) }
- if selected_labels.any? - if selected_labels.any?
- selected_labels.each do |label| - selected_labels.each do |label|
= link_to_label(label, subject: issuable.project, type: issuable.to_ability_name) = link_to_label(label, subject: issuable.project, type: issuable.to_ability_name)
......
...@@ -4,18 +4,18 @@ ...@@ -4,18 +4,18 @@
.form-group.row .form-group.row
= f.label :title, class: 'col-form-label col-sm-2' = f.label :title, class: 'col-form-label col-sm-2'
.col-sm-10 .col-sm-10
= f.text_field :title, class: "form-control", required: true, autofocus: true = f.text_field :title, class: "form-control qa-label-title", required: true, autofocus: true
.form-group.row .form-group.row
= f.label :description, class: 'col-form-label col-sm-2' = f.label :description, class: 'col-form-label col-sm-2'
.col-sm-10 .col-sm-10
= f.text_field :description, class: "form-control js-quick-submit" = f.text_field :description, class: "form-control js-quick-submit qa-label-description"
.form-group.row .form-group.row
= f.label :color, "Background color", class: 'col-form-label col-sm-2' = f.label :color, "Background color", class: 'col-form-label col-sm-2'
.col-sm-10 .col-sm-10
.input-group .input-group
.input-group-prepend .input-group-prepend
.input-group-text.label-color-preview &nbsp; .input-group-text.label-color-preview &nbsp;
= f.text_field :color, class: "form-control" = f.text_field :color, class: "form-control qa-label-color"
.form-text.text-muted .form-text.text-muted
Choose any color. Choose any color.
%br %br
...@@ -30,5 +30,5 @@ ...@@ -30,5 +30,5 @@
- if @label.persisted? - if @label.persisted?
= f.submit 'Save changes', class: 'btn btn-success js-save-button' = f.submit 'Save changes', class: 'btn btn-success js-save-button'
- else - else
= f.submit 'Create label', class: 'btn btn-success js-save-button' = f.submit 'Create label', class: 'btn btn-success js-save-button qa-label-create-button'
= link_to 'Cancel', back_path, class: 'btn btn-cancel' = link_to 'Cancel', back_path, class: 'btn btn-cancel'
---
title: On deletion of a file in sub directory in web IDE redirect to the sub directory
instead of project root
merge_request: 21465
author: George Thomas @thegeorgeous
type: changed
---
title: Use cached readme contents when available
merge_request: 22325
author:
type: performance
---
title: Fix a bug displaying certain wiki pages
merge_request: 22377
author:
type: fixed
---
title: Fix bug with wiki attachments content disposition
merge_request: 22220
author:
type: fixed
---
title: Remove prometheus configuration help text
merge_request: 22413
author: George Tsiolis
type: other
---
title: Update environments empty state
merge_request: 22297
author: George Tsiolis
type: other
---
title: Update JIRA service UI to accept email and API token
merge_request:
author:
type: other
---
description: "Set and configure Git protocol v2"
---
# Configuring Git Protocol v2
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/46555) in GitLab 11.4.
---
Git protocol v2 improves the v1 wire protocol in several ways and is
enabled by default in GitLab for HTTP requests. In order to enable SSH,
further configuration is needed by the administrator.
More details about the new features and improvements are available in
the [Google Open Source Blog](https://opensource.googleblog.com/2018/05/introducing-git-protocol-version-2.html)
and the [protocol documentation](https://github.com/git/git/blob/master/Documentation/technical/protocol-v2.txt).
## Requirements
From the client side, `git` `v2.18.0` or newer must be installed.
From the server side, if we want to configure SSH we need to set the `sshd`
server to accept the `GIT_PROTOCOL` environment,
```
# /etc/ssh/sshd_config
AcceptEnv GIT_PROTOCOL
```
Once configured, restart the SSH daemon. In Ubuntu, run:
```sh
sudo service ssh restart
```
## Instructions
In order to use the new protocol, clients need to either pass the configuration
`-c protocol.version=2` to the git command, or set it globally:
```sh
git config --global protocol.version 2
```
### HTTP connections
Verify Git v2 is used by the client:
```sh
GIT_TRACE_CURL=1 git -c protocol.version=2 ls-remote https://your-gitlab-instance.com/group/repo.git 2>&1 | grep Git-Protocol
```
You should see that the `Git-Protocol` header is sent:
```
16:29:44.577888 http.c:657 => Send header: Git-Protocol: version=2
```
Verify Git v2 is used by the server:
```sh
GIT_TRACE_PACKET=1 git -c protocol.version=2 ls-remote https://your-gitlab-instance.com/group/repo.git 2>&1 | head
```
Example response using Git protocol v2:
```sh
$ GIT_TRACE_PACKET=1 git -c protocol.version=2 ls-remote https://your-gitlab-instance.com/group/repo.git 2>&1 | head
10:42:50.574485 pkt-line.c:80 packet: git< # service=git-upload-pack
10:42:50.574653 pkt-line.c:80 packet: git< 0000
10:42:50.574673 pkt-line.c:80 packet: git< version 2
10:42:50.574679 pkt-line.c:80 packet: git< agent=git/2.18.1
10:42:50.574684 pkt-line.c:80 packet: git< ls-refs
10:42:50.574688 pkt-line.c:80 packet: git< fetch=shallow
10:42:50.574693 pkt-line.c:80 packet: git< server-option
10:42:50.574697 pkt-line.c:80 packet: git< 0000
10:42:50.574817 pkt-line.c:80 packet: git< version 2
10:42:50.575308 pkt-line.c:80 packet: git< agent=git/2.18.1
```
### SSH Connections
Verify Git v2 is used by the client:
```sh
GIT_SSH_COMMAND="ssh -v" git -c protocol.version=2 ls-remote ssh://your-gitlab-instance.com:group/repo.git 2>&1 |grep GIT_PROTOCOL
```
You should see that the `GIT_PROTOCOL` environment variable is sent:
```
debug1: Sending env GIT_PROTOCOL = version=2
```
For the server side, you can use the [same examples from HTTP](#http-connections), changing the
URL to use SSH.
...@@ -37,6 +37,30 @@ options: ...@@ -37,6 +37,30 @@ options:
circumstances it could lead to data loss if a failure occurs before data has circumstances it could lead to data loss if a failure occurs before data has
synced. synced.
### Known issues
On some customer systems, we have seen NFS clients slow precipitously due to
[excessive network traffic from numerous `TEST_STATEID` NFS
messages](https://gitlab.com/gitlab-org/gitlab-ce/issues/52017). This is
likely due to a [Linux kernel
bug](https://bugzilla.redhat.com/show_bug.cgi?id=1552203) that may be fixed in
[more recent kernels with this
commit](https://github.com/torvalds/linux/commit/95da1b3a5aded124dd1bda1e3cdb876184813140).
Users encountering a similar issue may be advised to disable the NFS server
delegation feature, which is an optimization to reduce the number of network
round-trips needed to read or write files. To disable NFS server delegations
on an Linux NFS server, do the following:
1. On the NFS server, run:
```sh
echo 0 > /proc/sys/fs/leases-enable
sysctl -w fs.leases-enable=0
```
2. Restart the NFS server process. For example, on CentOS run `service nfs restart`.
## AWS Elastic File System ## AWS Elastic File System
GitLab strongly recommends against using AWS Elastic File System (EFS). GitLab strongly recommends against using AWS Elastic File System (EFS).
...@@ -63,10 +87,11 @@ GitLab.com: ...@@ -63,10 +87,11 @@ GitLab.com:
10.1.1.1:/var/opt/gitlab/git-data /var/opt/gitlab/git-data nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 10.1.1.1:/var/opt/gitlab/git-data /var/opt/gitlab/git-data nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2
``` ```
Notice several options that you should consider using: Note there are several options that you should consider using:
| Setting | Description | | Setting | Description |
| ------- | ----------- | | ------- | ----------- |
| `vers=4.1` |NFS v4.1 should be used instead of v4.0 because there is a Linux [NFS client bug in v4.0](https://gitlab.com/gitlab-org/gitaly/issues/1339) that can cause significant problems due to stale data.
| `nofail` | Don't halt boot process waiting for this mount to become available | `nofail` | Don't halt boot process waiting for this mount to become available
| `lookupcache=positive` | Tells the NFS client to honor `positive` cache results but invalidates any `negative` cache results. Negative cache results cause problems with Git. Specifically, a `git push` can fail to register uniformly across all NFS clients. The negative cache causes the clients to 'remember' that the files did not exist previously. | `lookupcache=positive` | Tells the NFS client to honor `positive` cache results but invalidates any `negative` cache results. Negative cache results cause problems with Git. Specifically, a `git push` can fail to register uniformly across all NFS clients. The negative cache causes the clients to 'remember' that the files did not exist previously.
......
...@@ -151,6 +151,7 @@ created in snippets, wikis, and repos. ...@@ -151,6 +151,7 @@ created in snippets, wikis, and repos.
- [Custom Git hooks](custom_hooks.md): Custom Git hooks (on the filesystem) for when webhooks aren't enough. - [Custom Git hooks](custom_hooks.md): Custom Git hooks (on the filesystem) for when webhooks aren't enough.
- [Git LFS configuration](../workflow/lfs/lfs_administration.md): Learn how to configure LFS for GitLab. - [Git LFS configuration](../workflow/lfs/lfs_administration.md): Learn how to configure LFS for GitLab.
- [Housekeeping](housekeeping.md): Keep your Git repositories tidy and fast. - [Housekeeping](housekeeping.md): Keep your Git repositories tidy and fast.
- [Configuring Git Protocol v2](git_protocol.md): Git protocol version 2 support.
## Monitoring GitLab ## Monitoring GitLab
......
...@@ -148,6 +148,9 @@ This label documents the planned timeline & urgency which is used to measure aga ...@@ -148,6 +148,9 @@ This label documents the planned timeline & urgency which is used to measure aga
| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter or 90 days) | | ~P3 | Medium Priority | Within the next 3 releases (approx one quarter or 90 days) |
| ~P4 | Low Priority | Anything outside the next 3 releases (more than one quarter or 120 days) | | ~P4 | Low Priority | Anything outside the next 3 releases (more than one quarter or 120 days) |
If an issue seems to fall between two priority labels, assign it to the higher-
priority label.
## Severity labels ## Severity labels
Severity labels help us clearly communicate the impact of a ~bug on users. Severity labels help us clearly communicate the impact of a ~bug on users.
...@@ -159,6 +162,10 @@ Severity labels help us clearly communicate the impact of a ~bug on users. ...@@ -159,6 +162,10 @@ Severity labels help us clearly communicate the impact of a ~bug on users.
| ~S3 | Major Severity | Broken Feature, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue. | | ~S3 | Major Severity | Broken Feature, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue. |
| ~S4 | Low Severity | Functionality inconvenience or cosmetic issue | Label colors are incorrect / not being displayed. | | ~S4 | Low Severity | Functionality inconvenience or cosmetic issue | Label colors are incorrect / not being displayed. |
If an issue seems to fall between two severity labels, even taking the
[severity impact guidance](#severity-impact-guidance) into account, assign
it to the higher-severity label.
### Severity impact guidance ### Severity impact guidance
Severity levels can be applied further depending on the facet of the impact; e.g. Affected customers, GitLab.com availability, performance and etc. The below is a guideline. Severity levels can be applied further depending on the facet of the impact; e.g. Affected customers, GitLab.com availability, performance and etc. The below is a guideline.
......
...@@ -2,20 +2,21 @@ ...@@ -2,20 +2,21 @@
> [Introduced][ce-30469] in GitLab 9.3. > [Introduced][ce-30469] in GitLab 9.3.
Conversational Development Index (ConvDev) gives you an overview of your entire The Conversational Development Index (ConvDev Index) gives you an overview of your entire
instance's feature usage, from idea to production. It looks at your usage in the instance's adoption of [Concurrent DevOps](https://about.gitlab.com/concurrent-devops/)
past 30 days, averaged over the number of active users in that time period. It also from planning to monitoring. It displays the usage of these GitLab features over
provides a lead score per feature, which is calculated based on GitLab's analysis the last 30 days, averaged over the number of active users in that time period. It also
of top performing instances, based on [usage ping data][ping] that GitLab has provides a Lead score per feature, which is calculated based on GitLab's analysis
of top-performing instances based on [usage ping data][ping] that GitLab has
collected. Your score is compared to the lead score, expressed as a percentage. collected. Your score is compared to the lead score, expressed as a percentage.
The overall index score is an average over all your feature scores. Your overall index score is an average of all your feature score percentages.
![ConvDev index](img/convdev_index.png) ![ConvDev index](img/convdev_index.png)
The page also provides helpful links to articles and GitLab docs, to help you The page also provides helpful links to articles and GitLab docs, to help you
improve your scores. improve your scores.
Your GitLab instance's usage ping must be activated in order to use this feature. Your GitLab instance's [usage ping][ping] must be activated in order to use this feature.
Usage ping data is aggregated on GitLab's servers for analysis. Your usage Usage ping data is aggregated on GitLab's servers for analysis. Your usage
information is **not sent** to any other GitLab instances. information is **not sent** to any other GitLab instances.
......
...@@ -45,56 +45,11 @@ project in Jira and then enter the correct values in GitLab. ...@@ -45,56 +45,11 @@ project in Jira and then enter the correct values in GitLab.
### Configuring Jira ### Configuring Jira
We need to create a user in Jira which will have access to all projects that When connecting to **JIRA Server**, which supports basic authentication, a **username and password** are required. Check the link below and proceed to the next step:
need to integrate with GitLab. Login to your Jira instance as admin and under * [Setting up an user in JIRA server](jira_server_configuration.md)
*Administration*, go to *User Management* and create a new user.
As an example, we'll create a user named `gitlab` and add it to the `Jira-developers` When connecting to **JIRA Cloud**, which supports authentication via API token, an **email and API token**, are required. Check the link below and proceed to the next step:
group. * [Setting up an user in JIRA cloud](jira_cloud_configuration.md)
**It is important that the user `gitlab` has 'write' access to projects in Jira**
We have split this stage in steps so it is easier to follow.
1. Log in to your Jira instance as an administrator and under **Administration**
go to **User Management** to create a new user.
![Jira user management link](img/jira_user_management_link.png)
1. The next step is to create a new user (e.g., `gitlab`) who has write access
to projects in Jira. Enter the user's name and a _valid_ e-mail address
since Jira sends a verification e-mail to set up the password.
_**Note:** Jira creates the username automatically by using the e-mail
prefix. You can change it later, if needed. Our integration does not support SSO (such as SAML). You will need to create
an HTTP basic authentication password. You can do this by visiting the user
profile, looking up the username, and setting a password._
![Jira create new user](img/jira_create_new_user.png)
1. Now, let's create a `gitlab-developers` group which will have write access
to projects in Jira. Go to the **Groups** tab and select **Create group**.
![Jira create new user](img/jira_create_new_group.png)
Give it an optional description and click **Create group**.
![Jira create new group](img/jira_create_new_group_name.png)
1. To give the newly-created group 'write' access, go to
**Application access ➔ View configuration** and add the `gitlab-developers`
group to Jira Core.
![Jira group access](img/jira_group_access.png)
1. Add the `gitlab` user to the `gitlab-developers` group by going to
**Users ➔ GitLab user ➔ Add group** and selecting the `gitlab-developers`
group from the dropdown menu. Notice that the group says _Access_, which is
intended as part of this process.
![Jira add user to group](img/jira_add_user_to_group.png)
The Jira configuration is complete. Write down the new Jira username and its
password as they will be needed when configuring GitLab in the next section.
### Configuring GitLab ### Configuring GitLab
...@@ -117,8 +72,8 @@ in the table below. ...@@ -117,8 +72,8 @@ in the table below.
| ----- | ----------- | | ----- | ----------- |
| `Web URL` | The base URL to the Jira instance web interface which is being linked to this GitLab project. E.g., `https://Jira.example.com`. | | `Web URL` | The base URL to the Jira instance web interface which is being linked to this GitLab project. E.g., `https://Jira.example.com`. |
| `Jira API URL` | The base URL to the Jira instance API. Web URL value will be used if not set. E.g., `https://jira-api.example.com`. | | `Jira API URL` | The base URL to the Jira instance API. Web URL value will be used if not set. E.g., `https://jira-api.example.com`. |
| `Username` | The user name created in [configuring Jira step](#configuring-jira). Using the email address will cause `401 unauthorized`. | | `Username/Email` | Created when [configuring Jira step](#configuring-jira). Use `username` for **JIRA server** or `email` for **JIRA cloud**. |
| `Password` |The password of the user created in [configuring Jira step](#configuring-jira). | | `Password/API token` |Created in [configuring Jira step](#configuring-jira). Use `password` for **JIRA server** or `API token` for **JIRA cloud**. |
| `Transition ID` | This is the ID of a transition that moves issues to the desired state. It is possible to insert transition ids separated by `,` or `;` which means the issue will be moved to each state after another using the given order. **Closing Jira issues via commits or Merge Requests won't work if you don't set the ID correctly.** | | `Transition ID` | This is the ID of a transition that moves issues to the desired state. It is possible to insert transition ids separated by `,` or `;` which means the issue will be moved to each state after another using the given order. **Closing Jira issues via commits or Merge Requests won't work if you don't set the ID correctly.** |
### Obtaining a transition ID ### Obtaining a transition ID
......
# Creating an API token in JIRA cloud
An API token is needed when integrating with JIRA Cloud, follow the steps
below to create one:
1. Log in to https://id.atlassian.com with your email.
2. **Click API tokens**, then **Create API token**.
![JIRA API token](img/jira_api_token_menu.png)
![JIRA API token](img/jira_api_token.png)
3. Make sure to write down your new API token as you will need it in the next [steps](jira.md#configuring-gitlab).
NOTE: **Note**
It is important that the user associated with this email has 'write' access to projects in JIRA.
The JIRA configuration is complete. You are going to need this new created token and the email you used to log in when [configuring GitLab in the next section](jira.md#configuring-gitlab).
# Creating a username and password for JIRA server
We need to create a user in Jira which will have access to all projects that
need to integrate with GitLab. Login to your Jira instance as admin and under
*Administration*, go to *User Management* and create a new user.
As an example, we'll create a user named `gitlab` and add it to the `Jira-developers`
group.
NOTE: **Note**
It is important that the user `gitlab` has 'write' access to projects in Jira.
We have split this stage in steps so it is easier to follow.
1. Log in to your Jira instance as an administrator and under **Administration**
go to **User Management** to create a new user.
![Jira user management link](img/jira_user_management_link.png)
2. The next step is to create a new user (e.g., `gitlab`) who has write access
to projects in Jira. Enter the user's name and a _valid_ e-mail address
since Jira sends a verification e-mail to set up the password.
_**Note:** Jira creates the username automatically by using the e-mail
prefix. You can change it later, if needed. Our integration does not support SSO (such as SAML). You will need to create
an HTTP basic authentication password. You can do this by visiting the user
profile, looking up the username, and setting a password._
![Jira create new user](img/jira_create_new_user.png)
3. Create a `gitlab-developers` group which will have write access
to projects in Jira. Go to the **Groups** tab and select **Create group**.
![Jira create new user](img/jira_create_new_group.png)
Give it an optional description and click **Create group**.
![Jira create new group](img/jira_create_new_group_name.png)
4. To give the newly-created group 'write' access, go to
**Application access > View configuration** and add the `gitlab-developers`
group to Jira Core.
![Jira group access](img/jira_group_access.png)
5. Add the `gitlab` user to the `gitlab-developers` group by going to
**Users > GitLab user > Add group** and selecting the `gitlab-developers`
group from the dropdown menu. Notice that the group says _Access_, which is
intended as part of this process.
![Jira add user to group](img/jira_add_user_to_group.png)
The Jira configuration is complete. Write down the new Jira username and its
password as they will be needed when [configuring GitLab in the next section](jira.md#configuring-gitlab).
...@@ -72,7 +72,7 @@ module Gitlab ...@@ -72,7 +72,7 @@ module Gitlab
# and `safe_max_bytes` # and `safe_max_bytes`
# #
# :expanded :: # :expanded ::
# If true, patch raw data will not be included in the diff after # If false, patch raw data will not be included in the diff after
# `max_files`, `max_lines` or any of the limits in `limits` are # `max_files`, `max_lines` or any of the limits in `limits` are
# exceeded # exceeded
def filter_diff_options(options, default_options = {}) def filter_diff_options(options, default_options = {})
......
...@@ -3024,7 +3024,7 @@ msgstr "" ...@@ -3024,7 +3024,7 @@ msgstr ""
msgid "Environments|Updated" msgid "Environments|Updated"
msgstr "" msgstr ""
msgid "Environments|You don't have any environments right now." msgid "Environments|You don't have any environments right now"
msgstr "" msgstr ""
msgid "Environments|protected" msgid "Environments|protected"
...@@ -6336,10 +6336,14 @@ msgstr "" ...@@ -6336,10 +6336,14 @@ msgstr ""
msgid "Promote to Group Milestone" msgid "Promote to Group Milestone"
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "Promote to group label" msgid "Promote to group label"
msgstr "" msgstr ""
msgid "Promotions|Don't show me this again" msgid "Promotions|Don't show me this again"
=======
msgid "PrometheusService|Common metrics"
>>>>>>> upstream/master
msgstr "" msgstr ""
msgid "Promotions|Epics let you manage your portfolio of projects more efficiently and with less effort by tracking groups of issues that share a theme, across projects and milestones." msgid "Promotions|Epics let you manage your portfolio of projects more efficiently and with less effort by tracking groups of issues that share a theme, across projects and milestones."
......
...@@ -46,6 +46,7 @@ module QA ...@@ -46,6 +46,7 @@ module QA
autoload :Group, 'qa/factory/resource/group' autoload :Group, 'qa/factory/resource/group'
autoload :Issue, 'qa/factory/resource/issue' autoload :Issue, 'qa/factory/resource/issue'
autoload :Project, 'qa/factory/resource/project' autoload :Project, 'qa/factory/resource/project'
autoload :Label, 'qa/factory/resource/label'
autoload :MergeRequest, 'qa/factory/resource/merge_request' autoload :MergeRequest, 'qa/factory/resource/merge_request'
autoload :ProjectImportedFromGithub, 'qa/factory/resource/project_imported_from_github' autoload :ProjectImportedFromGithub, 'qa/factory/resource/project_imported_from_github'
autoload :MergeRequestFromFork, 'qa/factory/resource/merge_request_from_fork' autoload :MergeRequestFromFork, 'qa/factory/resource/merge_request_from_fork'
...@@ -241,6 +242,11 @@ module QA ...@@ -241,6 +242,11 @@ module QA
autoload :Banner, 'qa/page/layout/banner' autoload :Banner, 'qa/page/layout/banner'
end end
module Label
autoload :New, 'qa/page/label/new'
autoload :Index, 'qa/page/label/index'
end
module MergeRequest module MergeRequest
autoload :New, 'qa/page/merge_request/new' autoload :New, 'qa/page/merge_request/new'
autoload :Show, 'qa/page/merge_request/show' autoload :Show, 'qa/page/merge_request/show'
......
require 'securerandom'
module QA
module Factory
module Resource
class Label < Factory::Base
attr_accessor :title,
:description,
:color
product(:title) { |factory| factory.title }
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-with-label'
end
def initialize
@title = "qa-test-#{SecureRandom.hex(8)}"
@description = 'This is a test label'
@color = '#0033CC'
end
def fabricate!
project.visit!
Page::Project::Menu.act { go_to_labels }
Page::Label::Index.act { go_to_new_label }
Page::Label::New.perform do |page|
page.fill_title(@title)
page.fill_description(@description)
page.fill_color(@color)
page.create_label
end
end
end
end
end
end
...@@ -52,6 +52,10 @@ module QA ...@@ -52,6 +52,10 @@ module QA
page.fill_title(@title) page.fill_title(@title)
page.fill_description(@description) page.fill_description(@description)
page.choose_milestone(@milestone) if @milestone page.choose_milestone(@milestone) if @milestone
labels.each do |label|
page.select_label(label)
end
page.create_merge_request page.create_merge_request
end end
end end
......
module QA
module Page
module Label
class Index < Page::Base
view 'app/views/projects/labels/index.html.haml' do
element :label_create_new
end
def go_to_new_label
click_element :label_create_new
end
end
end
end
end
module QA
module Page
module Label
class New < Page::Base
view 'app/views/shared/labels/_form.html.haml' do
element :label_title
element :label_description
element :label_color
element :label_create_button
end
def create_label
click_element :label_create_button
end
def fill_title(title)
fill_element :label_title, title
end
def fill_description(description)
fill_element :label_description, description
end
def fill_color(color)
fill_element :label_color, color
end
end
end
end
end
...@@ -22,6 +22,10 @@ module QA ...@@ -22,6 +22,10 @@ module QA
element :issuable_dropdown_menu_milestone element :issuable_dropdown_menu_milestone
end end
view 'app/views/shared/issuable/_label_dropdown.html.haml' do
element :issuable_label
end
def create_merge_request def create_merge_request
click_element :issuable_create_button click_element :issuable_create_button
end end
...@@ -40,6 +44,12 @@ module QA ...@@ -40,6 +44,12 @@ module QA
click_on milestone.title click_on milestone.title
end end
end end
def select_label(label)
click_element :issuable_label
click_link label.title
end
end end
end end
end end
......
...@@ -25,6 +25,10 @@ module QA ...@@ -25,6 +25,10 @@ module QA
element :squash_checkbox element :squash_checkbox
end end
view 'app/views/shared/issuable/_sidebar.html.haml' do
element :labels_block
end
def fast_forward_possible? def fast_forward_possible?
!has_text?('Fast-forward merge is not possible') !has_text?('Fast-forward merge is not possible')
end end
...@@ -66,6 +70,13 @@ module QA ...@@ -66,6 +70,13 @@ module QA
end end
end end
def has_label?(label)
page.within(element_selector_css(:labels_block)) do
element = find('span', text: label)
!element.nil?
end
end
def merge! def merge!
# The merge button is disabled on load # The merge button is disabled on load
wait do wait do
......
...@@ -22,6 +22,7 @@ module QA ...@@ -22,6 +22,7 @@ module QA
element :activity_link, "title: _('Activity')" # rubocop:disable QA/ElementWithPattern element :activity_link, "title: _('Activity')" # rubocop:disable QA/ElementWithPattern
element :wiki_link_text, "Wiki" # rubocop:disable QA/ElementWithPattern element :wiki_link_text, "Wiki" # rubocop:disable QA/ElementWithPattern
element :milestones_link element :milestones_link
element :labels_link
end end
view 'app/assets/javascripts/fly_out_nav.js' do view 'app/assets/javascripts/fly_out_nav.js' do
...@@ -104,6 +105,7 @@ module QA ...@@ -104,6 +105,7 @@ module QA
end end
end end
<<<<<<< HEAD
def click_repository def click_repository
within_sidebar do within_sidebar do
click_link('Repository') click_link('Repository')
...@@ -114,10 +116,22 @@ module QA ...@@ -114,10 +116,22 @@ module QA
within_sidebar do within_sidebar do
click_link('Epics') click_link('Epics')
end end
=======
def go_to_labels
hover_issues { click_element :labels_link }
>>>>>>> upstream/master
end end
private private
def hover_issues
within_sidebar do
find_element(:issues_item).hover
yield
end
end
def hover_settings def hover_settings
within_sidebar do within_sidebar do
find('.qa-settings-item').hover find('.qa-settings-item').hover
......
...@@ -16,16 +16,26 @@ module QA ...@@ -16,16 +16,26 @@ module QA
milestone.project = current_project milestone.project = current_project
end end
new_label = Factory::Resource::Label.fabricate! do |label|
label.project = current_project
label.title = 'qa-mr-test-label'
label.description = 'Merge Request label'
end
Factory::Resource::MergeRequest.fabricate! do |merge_request| Factory::Resource::MergeRequest.fabricate! do |merge_request|
merge_request.title = 'This is a merge request with a milestone' merge_request.title = 'This is a merge request with a milestone'
merge_request.description = 'Great feature with milestone' merge_request.description = 'Great feature with milestone'
merge_request.project = current_project merge_request.project = current_project
merge_request.milestone = current_milestone merge_request.milestone = current_milestone
merge_request.labels.push(new_label)
end end
expect(page).to have_content('This is a merge request with a milestone') Page::MergeRequest::Show.perform do |merge_request|
expect(page).to have_content('Great feature with milestone') expect(merge_request).to have_content('This is a merge request with a milestone')
expect(page).to have_content(/Opened [\w\s]+ ago/) expect(merge_request).to have_content('Great feature with milestone')
expect(merge_request).to have_content(/Opened [\w\s]+ ago/)
expect(merge_request).to have_label(new_label.title)
end
Page::Issuable::Sidebar.perform do |sidebar| Page::Issuable::Sidebar.perform do |sidebar|
expect(sidebar).to have_milestone(current_milestone.title) expect(sidebar).to have_milestone(current_milestone.title)
......
...@@ -343,4 +343,76 @@ describe Projects::BlobController do ...@@ -343,4 +343,76 @@ describe Projects::BlobController do
end end
end end
end end
describe 'DELETE destroy' do
let(:user) { create(:user) }
let(:project_root_path) { project_tree_path(project, 'master') }
before do
project.add_maintainer(user)
sign_in(user)
end
context 'for a file in a subdirectory' do
let(:default_params) do
{
namespace_id: project.namespace,
project_id: project,
id: 'master/files/whitespace',
original_branch: 'master',
branch_name: 'master',
commit_message: 'Delete whitespace'
}
end
let(:after_delete_path) { project_tree_path(project, 'master/files') }
it 'redirects to the sub directory' do
delete :destroy, default_params
expect(response).to redirect_to(after_delete_path)
end
end
context 'if deleted file is the last one in a subdirectory' do
let(:default_params) do
{
namespace_id: project.namespace,
project_id: project,
id: 'master/bar/branch-test.txt',
original_branch: 'master',
branch_name: 'master',
commit_message: 'Delete whitespace'
}
end
it 'redirects to the project root' do
delete :destroy, default_params
expect(response).to redirect_to(project_root_path)
end
context 'when deleting a file in a branch other than master' do
let(:default_params) do
{
namespace_id: project.namespace,
project_id: project,
id: 'binary-encoding/foo/bar/.gitkeep',
original_branch: 'binary-encoding',
branch_name: 'binary-encoding',
commit_message: 'Delete whitespace'
}
end
let(:after_delete_path) { project_tree_path(project, 'binary-encoding') }
it 'redirects to the project root of the branch' do
delete :destroy, default_params
expect(response).to redirect_to(after_delete_path)
end
end
end
end
end end
...@@ -22,20 +22,18 @@ describe Projects::WikisController do ...@@ -22,20 +22,18 @@ describe Projects::WikisController do
subject { get :show, namespace_id: project.namespace, project_id: project, id: wiki_title } subject { get :show, namespace_id: project.namespace, project_id: project, id: wiki_title }
context 'when page content encoding is invalid' do it 'limits the retrieved pages for the sidebar' do
it 'limits the retrieved pages for the sidebar' do expect(controller).to receive(:load_wiki).and_return(project_wiki)
expect(controller).to receive(:load_wiki).and_return(project_wiki)
# empty? call # empty? call
expect(project_wiki).to receive(:pages).with(limit: 1).and_call_original expect(project_wiki).to receive(:pages).with(limit: 1).and_call_original
# Sidebar entries # Sidebar entries
expect(project_wiki).to receive(:pages).with(limit: 15).and_call_original expect(project_wiki).to receive(:pages).with(limit: 15).and_call_original
subject subject
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(response.body).to include(wiki_title) expect(response.body).to include(wiki_title)
end
end end
context 'when page content encoding is invalid' do context 'when page content encoding is invalid' do
...@@ -48,6 +46,42 @@ describe Projects::WikisController do ...@@ -48,6 +46,42 @@ describe Projects::WikisController do
expect(flash[:notice]).to eq 'The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.' expect(flash[:notice]).to eq 'The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.'
end end
end end
context 'when page is a file' do
include WikiHelpers
let(:path) { upload_file_to_wiki(project, user, file_name) }
before do
subject
end
subject { get :show, namespace_id: project.namespace, project_id: project, id: path }
context 'when file is an image' do
let(:file_name) { 'dk.png' }
it 'renders the content inline' do
expect(response.headers['Content-Disposition']).to match(/^inline/)
end
context 'when file is a svg' do
let(:file_name) { 'unsanitized.svg' }
it 'renders the content as an attachment' do
expect(response.headers['Content-Disposition']).to match(/^attachment/)
end
end
end
context 'when file is a pdf' do
let(:file_name) { 'git-cheat-sheet.pdf' }
it 'sets the content type to application/octet-stream' do
expect(response.headers['Content-Type']).to eq 'application/octet-stream'
end
end
end
end end
describe 'POST #preview_markdown' do describe 'POST #preview_markdown' do
......
...@@ -95,7 +95,7 @@ describe 'Environments page', :js do ...@@ -95,7 +95,7 @@ describe 'Environments page', :js do
end end
it 'does not show environments and counters are set to zero' do it 'does not show environments and counters are set to zero' do
expect(page).to have_content('You don\'t have any environments right now.') expect(page).to have_content('You don\'t have any environments right now')
expect(page.find('.js-environments-tab-available .badge').text).to eq('0') expect(page.find('.js-environments-tab-available .badge').text).to eq('0')
expect(page.find('.js-environments-tab-stopped .badge').text).to eq('0') expect(page.find('.js-environments-tab-stopped .badge').text).to eq('0')
......
...@@ -31,7 +31,7 @@ describe 'Projects > Files > User deletes files', :js do ...@@ -31,7 +31,7 @@ describe 'Projects > Files > User deletes files', :js do
fill_in(:commit_message, with: 'New commit message', visible: true) fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Delete file') click_button('Delete file')
expect(current_path).to eq(project_tree_path(project, 'master')) expect(current_path).to eq(project_tree_path(project, 'master/'))
expect(page).not_to have_content('.gitignore') expect(page).not_to have_content('.gitignore')
end end
end end
......
...@@ -54,7 +54,7 @@ describe 'User views empty wiki' do ...@@ -54,7 +54,7 @@ describe 'User views empty wiki' do
it_behaves_like 'empty wiki and non-accessible issues' it_behaves_like 'empty wiki and non-accessible issues'
end end
context 'when user is logged in and a memeber' do context 'when user is logged in and a member' do
let(:project) { create(:project, :public, :wiki_repo) } let(:project) { create(:project, :public, :wiki_repo) }
before do before do
......
...@@ -2,12 +2,15 @@ require 'spec_helper' ...@@ -2,12 +2,15 @@ require 'spec_helper'
describe 'User views a wiki page' do describe 'User views a wiki page' do
shared_examples 'wiki page user view' do shared_examples 'wiki page user view' do
include WikiHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
let(:path) { 'image.png' }
let(:wiki_page) do let(:wiki_page) do
create(:wiki_page, create(:wiki_page,
wiki: project.wiki, wiki: project.wiki,
attrs: { title: 'home', content: 'Look at this [image](image.jpg)\n\n ![alt text](image.jpg)' }) attrs: { title: 'home', content: "Look at this [image](#{path})\n\n ![alt text](#{path})" })
end end
before do before do
...@@ -82,33 +85,26 @@ describe 'User views a wiki page' do ...@@ -82,33 +85,26 @@ describe 'User views a wiki page' do
expect(find('.wiki-pages')).to have_content(wiki_page.title.capitalize) expect(find('.wiki-pages')).to have_content(wiki_page.title.capitalize)
end end
it 'shows a file stored in a page' do context 'shows a file stored in a page' do
raw_file = Gitlab::GitalyClient::WikiFile.new( let(:path) { upload_file_to_wiki(project, user, 'dk.png') }
mime_type: 'image/jpeg',
name: 'images/image.jpg',
path: 'images/image.jpg',
raw_data: ''
)
wiki_file = Gitlab::Git::WikiFile.new(raw_file)
allow(wiki_file).to receive(:mime_type).and_return('image/jpeg')
allow_any_instance_of(ProjectWiki).to receive(:find_file).with('image.jpg', nil).and_return(wiki_file)
expect(page).to have_xpath("//img[@data-src='#{project.wiki.wiki_base_path}/image.jpg']") it do
expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/image.jpg") expect(page).to have_xpath("//img[@data-src='#{project.wiki.wiki_base_path}/#{path}']")
expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/#{path}")
click_on('image') click_on('image')
expect(current_path).to match('wikis/image.jpg') expect(current_path).to match("wikis/#{path}")
expect(page).not_to have_xpath('/html') # Page should render the image which means there is no html involved expect(page).not_to have_xpath('/html') # Page should render the image which means there is no html involved
end
end end
it 'shows the creation page if file does not exist' do it 'shows the creation page if file does not exist' do
expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/image.jpg") expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/#{path}")
click_on('image') click_on('image')
expect(current_path).to match('wikis/image.jpg') expect(current_path).to match("wikis/#{path}")
expect(page).to have_content('New Wiki Page') expect(page).to have_content('New Wiki Page')
expect(page).to have_content('Create page') expect(page).to have_content('Create page')
end end
......
...@@ -26,7 +26,7 @@ describe('environments empty state', () => { ...@@ -26,7 +26,7 @@ describe('environments empty state', () => {
it('renders empty state and new environment button', () => { it('renders empty state and new environment button', () => {
expect( expect(
vm.$el.querySelector('.js-blank-state-title').textContent.trim(), vm.$el.querySelector('.js-blank-state-title').textContent.trim(),
).toEqual('You don\'t have any environments right now.'); ).toEqual('You don\'t have any environments right now');
expect( expect(
vm.$el.querySelector('.js-new-environment-button').getAttribute('href'), vm.$el.querySelector('.js-new-environment-button').getAttribute('href'),
...@@ -46,7 +46,7 @@ describe('environments empty state', () => { ...@@ -46,7 +46,7 @@ describe('environments empty state', () => {
it('renders empty state without new button', () => { it('renders empty state without new button', () => {
expect( expect(
vm.$el.querySelector('.js-blank-state-title').textContent.trim(), vm.$el.querySelector('.js-blank-state-title').textContent.trim(),
).toEqual('You don\'t have any environments right now.'); ).toEqual('You don\'t have any environments right now');
expect( expect(
vm.$el.querySelector('.js-new-environment-button'), vm.$el.querySelector('.js-new-environment-button'),
......
...@@ -50,7 +50,7 @@ describe('Environment', () => { ...@@ -50,7 +50,7 @@ describe('Environment', () => {
expect( expect(
component.$el.querySelector('.js-blank-state-title').textContent, component.$el.querySelector('.js-blank-state-title').textContent,
).toContain('You don\'t have any environments right now.'); ).toContain('You don\'t have any environments right now');
}); });
}); });
...@@ -136,7 +136,7 @@ describe('Environment', () => { ...@@ -136,7 +136,7 @@ describe('Environment', () => {
it('should render empty state', () => { it('should render empty state', () => {
expect( expect(
component.$el.querySelector('.js-blank-state-title').textContent, component.$el.querySelector('.js-blank-state-title').textContent,
).toContain('You don\'t have any environments right now.'); ).toContain('You don\'t have any environments right now');
}); });
}); });
......
...@@ -35,7 +35,10 @@ describe('stage column component', () => { ...@@ -35,7 +35,10 @@ describe('stage column component', () => {
component = mountComponent(StageColumnComponent, { component = mountComponent(StageColumnComponent, {
title: 'foo', title: 'foo',
groups: mockGroups, groups: mockGroups,
<<<<<<< HEAD
hasTriggeredBy: false, hasTriggeredBy: false,
=======
>>>>>>> upstream/master
}); });
}); });
......
...@@ -570,28 +570,28 @@ describe Project do ...@@ -570,28 +570,28 @@ describe Project do
end end
describe "#readme_url" do describe "#readme_url" do
let(:project) { create(:project, :repository, path: "somewhere") }
context 'with a non-existing repository' do context 'with a non-existing repository' do
it 'returns nil' do let(:project) { create(:project) }
allow(project.repository).to receive(:tree).with(:head).and_return(nil)
it 'returns nil' do
expect(project.readme_url).to be_nil expect(project.readme_url).to be_nil
end end
end end
context 'with an existing repository' do context 'with an existing repository' do
context 'when no README exists' do context 'when no README exists' do
it 'returns nil' do let(:project) { create(:project, :empty_repo) }
allow_any_instance_of(Tree).to receive(:readme).and_return(nil)
it 'returns nil' do
expect(project.readme_url).to be_nil expect(project.readme_url).to be_nil
end end
end end
context 'when a README exists' do context 'when a README exists' do
let(:project) { create(:project, :repository) }
it 'returns the README' do it 'returns the README' do
expect(project.readme_url).to eql("#{Gitlab.config.gitlab.url}/#{project.namespace.full_path}/somewhere/blob/master/README.md") expect(project.readme_url).to eq("#{project.web_url}/blob/master/README.md")
end end
end end
end end
......
...@@ -461,6 +461,24 @@ describe Repository do ...@@ -461,6 +461,24 @@ describe Repository do
it { is_expected.to be_nil } it { is_expected.to be_nil }
end end
context 'regular blob' do
subject { repository.blob_at(repository.head_commit.sha, '.gitignore') }
it { is_expected.to be_an_instance_of(::Blob) }
end
context 'readme blob on HEAD' do
subject { repository.blob_at(repository.head_commit.sha, 'README.md') }
it { is_expected.to be_an_instance_of(::ReadmeBlob) }
end
context 'readme blob not on HEAD' do
subject { repository.blob_at(repository.find_branch('feature').target, 'README.md') }
it { is_expected.to be_an_instance_of(::Blob) }
end
end end
describe '#merged_to_root_ref?' do describe '#merged_to_root_ref?' do
...@@ -1922,23 +1940,25 @@ describe Repository do ...@@ -1922,23 +1940,25 @@ describe Repository do
describe '#readme', :use_clean_rails_memory_store_caching do describe '#readme', :use_clean_rails_memory_store_caching do
context 'with a non-existing repository' do context 'with a non-existing repository' do
it 'returns nil' do let(:project) { create(:project) }
allow(repository).to receive(:tree).with(:head).and_return(nil)
it 'returns nil' do
expect(repository.readme).to be_nil expect(repository.readme).to be_nil
end end
end end
context 'with an existing repository' do context 'with an existing repository' do
context 'when no README exists' do context 'when no README exists' do
it 'returns nil' do let(:project) { create(:project, :empty_repo) }
allow_any_instance_of(Tree).to receive(:readme).and_return(nil)
it 'returns nil' do
expect(repository.readme).to be_nil expect(repository.readme).to be_nil
end end
end end
context 'when a README exists' do context 'when a README exists' do
let(:project) { create(:project, :repository) }
it 'returns the README' do it 'returns the README' do
expect(repository.readme).to be_an_instance_of(ReadmeBlob) expect(repository.readme).to be_an_instance_of(ReadmeBlob)
end end
......
...@@ -457,6 +457,12 @@ describe WikiPage do ...@@ -457,6 +457,12 @@ describe WikiPage do
end end
describe '#historical?' do describe '#historical?' do
let(:page) { wiki.find_page('Update') }
let(:old_version) { page.versions.last.id }
let(:old_page) { wiki.find_page('Update', old_version) }
let(:latest_version) { page.versions.first.id }
let(:latest_page) { wiki.find_page('Update', latest_version) }
before do before do
create_page('Update', 'content') create_page('Update', 'content')
@page = wiki.find_page('Update') @page = wiki.find_page('Update')
...@@ -468,23 +474,27 @@ describe WikiPage do ...@@ -468,23 +474,27 @@ describe WikiPage do
end end
it 'returns true when requesting an old version' do it 'returns true when requesting an old version' do
old_version = @page.versions.last.id expect(old_page.historical?).to be_truthy
old_page = wiki.find_page('Update', old_version)
expect(old_page.historical?).to eq true
end end
it 'returns false when requesting latest version' do it 'returns false when requesting latest version' do
latest_version = @page.versions.first.id expect(latest_page.historical?).to be_falsy
latest_page = wiki.find_page('Update', latest_version)
expect(latest_page.historical?).to eq false
end end
it 'returns false when version is nil' do it 'returns false when version is nil' do
latest_page = wiki.find_page('Update', nil) expect(latest_page.historical?).to be_falsy
end
it 'returns false when the last version is nil' do
expect(old_page).to receive(:last_version) { nil }
expect(old_page.historical?).to be_falsy
end
it 'returns false when the version is nil' do
expect(old_page).to receive(:version) { nil }
expect(latest_page.historical?).to eq false expect(old_page.historical?).to be_falsy
end end
end end
......
module WikiHelpers
extend self
def upload_file_to_wiki(project, user, file_name)
opts = {
file_name: file_name,
file_content: File.read(expand_fixture_path(file_name))
}
::Wikis::CreateAttachmentService.new(project, user, opts)
.execute[:result][:file_path]
end
end
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