Commit 2cce501f authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge commit '4457cf9d' into fix/gb/fix-redundant-pipeline-stages

* commit '4457cf9d': (76 commits)

Conflicts:
	spec/services/ci/retry_build_service_spec.rb
parents adb6dd06 4457cf9d
......@@ -411,6 +411,8 @@ end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.83.0', require: 'gitaly'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
gem 'google-protobuf', '= 3.5.1'
gem 'toml-rb', '~> 0.3.15', require: false
......
......@@ -340,7 +340,7 @@ GEM
mime-types (~> 3.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
google-protobuf (3.5.1.1)
google-protobuf (3.5.1)
googleapis-common-protos-types (1.0.1)
google-protobuf (~> 3.0)
googleauth (0.5.3)
......@@ -1066,6 +1066,7 @@ DEPENDENCIES
gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0)
google-api-client (~> 0.13.6)
google-protobuf (= 3.5.1)
gpgme
grape (~> 1.0)
grape-entity (~> 0.6.0)
......
......@@ -35,10 +35,11 @@ export default class Clusters {
clusterStatus,
clusterStatusReason,
helpPath,
ingressHelpPath,
} = document.querySelector('.js-edit-cluster-form').dataset;
this.store = new ClustersStore();
this.store.setHelpPath(helpPath);
this.store.setHelpPaths(helpPath, ingressHelpPath);
this.store.updateStatus(clusterStatus);
this.store.updateStatusReason(clusterStatusReason);
this.service = new ClustersService({
......@@ -93,6 +94,7 @@ export default class Clusters {
props: {
applications: this.state.applications,
helpPath: this.state.helpPath,
ingressHelpPath: this.state.ingressHelpPath,
},
});
},
......@@ -172,7 +174,7 @@ export default class Clusters {
.map(appId => newApplicationMap[appId].title);
if (appTitles.length > 0) {
const text = sprintf(s__('ClusterIntegration|%{appList} was successfully installed on your cluster'), {
const text = sprintf(s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'), {
appList: appTitles.join(', '),
});
Flash(text, 'notice', this.successApplicationContainer);
......
......@@ -18,11 +18,16 @@
required: false,
default: '',
},
ingressHelpPath: {
type: String,
required: false,
default: '',
},
},
computed: {
generalApplicationDescription() {
return sprintf(
_.escape(s__(`ClusterIntegration|Install applications on your cluster.
_.escape(s__(`ClusterIntegration|Install applications on your Kubernetes cluster.
Read more about %{helpLink}`)),
{
helpLink: `<a href="${this.helpPath}">
......@@ -34,7 +39,7 @@
},
helmTillerDescription() {
return _.escape(s__(
`ClusterIntegration|Helm streamlines installing and managing Kubernets applications.
`ClusterIntegration|Helm streamlines installing and managing Kubernetes applications.
Tiller runs inside of your Kubernetes Cluster, and manages
releases of your charts.`,
));
......@@ -49,7 +54,7 @@
_.escape(s__(
`ClusterIntegration|%{boldNotice} This will add some extra resources
like a load balancer, which may incur additional costs depending on
the hosting provider Kubernetes is installed on. If you are using GKE,
the hosting provider your Kubernetes cluster is installed on. If you are using GKE,
you can %{pricingLink}.`,
)), {
boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
......@@ -59,13 +64,28 @@
false,
);
const externalIpParagraph = sprintf(
_.escape(s__(
`ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS
at the generated external IP address in order to view your app after it is deployed. %{ingressHelpLink}`,
)), {
ingressHelpLink: `<a href="${this.ingressHelpPath}">
${_.escape(s__('ClusterIntegration|More information'))}
</a>`,
},
false,
);
return `
<p>
${descriptionParagraph}
</p>
<p class="append-bottom-0">
<p>
${extraCostParagraph}
</p>
<p class="settings-message append-bottom-0">
${externalIpParagraph}
</p>
`;
},
gitlabRunnerDescription() {
......
......@@ -4,6 +4,7 @@ export default class ClusterStore {
constructor() {
this.state = {
helpPath: null,
ingressHelpPath: null,
status: null,
statusReason: null,
applications: {
......@@ -39,8 +40,9 @@ export default class ClusterStore {
};
}
setHelpPath(helpPath) {
setHelpPaths(helpPath, ingressHelpPath) {
this.state.helpPath = helpPath;
this.state.ingressHelpPath = ingressHelpPath;
}
updateStatus(status) {
......
......@@ -8,7 +8,7 @@ import { convertPermissionToBoolean } from './lib/utils/common_utils';
```
%button.js-project-feature-toggle.project-feature-toggle{ type: "button",
class: "#{'is-checked' if enabled?}",
'aria-label': _('Toggle Cluster') }
'aria-label': _('Toggle Kubernetes Cluster') }
%input{ type: "hidden", class: 'js-project-feature-toggle-input', value: enabled? }
```
*/
......
......@@ -4,6 +4,11 @@
.page-title {
margin-top: 0;
.color-label {
font-size: $gl-font-size;
padding: $gl-vert-padding $label-padding-modal;
}
}
}
......
......@@ -558,6 +558,7 @@ $jq-ui-default-color: #777;
* Label
*/
$label-padding: 7px;
$label-padding-modal: 10px;
$label-gray-bg: #f8fafc;
$label-inverse-bg: #333;
$label-remove-border: rgba(0, 0, 0, .1);
......
......@@ -58,13 +58,13 @@
@media (min-width: $screen-sm-min) {
width: 200px;
margin-left: $gl-padding * 2;
margin-bottom: 0;
}
.label {
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
max-width: 100%;
}
}
......@@ -79,26 +79,33 @@
width: 100px;
margin-left: 10px;
margin-bottom: 0;
vertical-align: middle;
vertical-align: top;
}
}
.label-description {
display: block;
margin-bottom: 10px;
margin-left: 50px;
.description-text {
margin-bottom: $gl-padding;
}
a {
color: $blue-600;
}
@media (min-width: $screen-sm-min) {
display: inline-block;
width: 30%;
max-width: 50%;
margin-left: 10px;
margin-bottom: 0;
vertical-align: middle;
vertical-align: top;
}
}
.label {
padding: 8px 9px 9px;
padding: 8px 12px;
font-size: 14px;
}
}
......@@ -116,6 +123,12 @@
}
.manage-labels-list {
@media(min-width: $screen-md-min) {
&.content-list li {
padding: $gl-padding 0;
}
}
> li:not(.empty-message):not(.is-not-draggable) {
background-color: $white-light;
cursor: move;
......@@ -133,8 +146,6 @@
}
.btn-action {
color: $gl-text-color;
.fa {
font-size: 18px;
vertical-align: middle;
......@@ -155,10 +166,18 @@
float: right;
}
}
@media (max-width: $screen-xs-max) {
.dropdown-menu {
min-width: 100%;
}
}
}
.draggable-handler {
display: inline-block;
vertical-align: top;
margin: 5px 0;
opacity: 0;
transition: opacity .3s;
color: $gray-darkest;
......@@ -188,7 +207,7 @@
.toggle-priority {
display: inline-block;
vertical-align: middle;
vertical-align: top;
button {
border-color: transparent;
......@@ -255,6 +274,11 @@
}
.label-subscribe-button {
@media(min-width: $screen-md-min) {
min-width: 105px;
margin-left: $gl-padding;
}
.label-subscribe-button-icon {
&[disabled] {
opacity: 0.5;
......
......@@ -54,7 +54,7 @@ class Groups::LabelsController < Groups::ApplicationController
respond_to do |format|
format.html do
redirect_to group_labels_path(@group), status: 302, notice: 'Label was removed'
redirect_to group_labels_path(@group), status: 302, notice: "#{@label.name} deleted permanently"
end
format.js
end
......
......@@ -42,7 +42,7 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
when 'true'
return
when 'false'
flash[:alert] = _('Please <a href=%{link_to_billing} target="_blank" rel="noopener noreferrer">enable billing for one of your projects to be able to create a cluster</a>, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" }
flash[:alert] = _('Please <a href=%{link_to_billing} target="_blank" rel="noopener noreferrer">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" }
else
flash[:alert] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.')
end
......
......@@ -41,7 +41,7 @@ class Projects::ClustersController < Projects::ApplicationController
head :no_content
end
format.html do
flash[:notice] = "Cluster was successfully updated."
flash[:notice] = _('Kubernetes cluster was successfully updated.')
redirect_to project_cluster_path(project, cluster)
end
end
......@@ -55,10 +55,10 @@ class Projects::ClustersController < Projects::ApplicationController
def destroy
if cluster.destroy
flash[:notice] = "Cluster integration was successfully removed."
flash[:notice] = _('Kubernetes cluster integration was successfully removed.')
redirect_to project_clusters_path(project), status: 302
else
flash[:notice] = "Cluster integration was not removed."
flash[:notice] = _('Kubernetes cluster integration was not removed.')
render :show
end
end
......
......@@ -5,6 +5,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
rescue_from Gitlab::GitAccess::UnauthorizedError, with: :render_403
rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404
rescue_from Gitlab::GitAccess::ProjectCreationError, with: :render_422
# GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
......@@ -55,8 +56,15 @@ class Projects::GitHttpController < Projects::GitHttpClientController
render plain: exception.message, status: :not_found
end
def render_422(exception)
render plain: exception.message, status: :unprocessable_entity
end
def access
@access ||= access_klass.new(access_actor, project, 'http', authentication_abilities: authentication_abilities, redirected_path: redirected_path)
@access ||= access_klass.new(access_actor, project,
'http', authentication_abilities: authentication_abilities,
namespace_path: params[:namespace_id], project_path: project_path,
redirected_path: redirected_path)
end
def access_actor
......@@ -68,12 +76,17 @@ class Projects::GitHttpController < Projects::GitHttpClientController
# Use the magic string '_any' to indicate we do not know what the
# changes are. This is also what gitlab-shell does.
access.check(git_command, '_any')
@project ||= access.project
end
def access_klass
@access_klass ||= wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess
end
def project_path
@project_path ||= params[:project_id].sub(/\.git$/, '')
end
def log_user_activity
Users::ActivityService.new(user, 'pull').execute
end
......
......@@ -76,9 +76,9 @@ class Projects::WikisController < Projects::ApplicationController
@page = @project_wiki.find_page(params[:id])
if @page
@page_versions = Kaminari.paginate_array(@page.versions(page: params[:page]),
@page_versions = Kaminari.paginate_array(@page.versions(page: params[:page].to_i),
total_count: @page.count_versions)
.page(params[:page])
.page(params[:page])
else
redirect_to(
project_wiki_path(@project, :home),
......
module GroupsHelper
def group_nav_link_paths
%w[groups#projects groups#edit ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index]
end
def can_change_group_visibility_level?(group)
can?(current_user, :change_visibility_level, group)
end
......
......@@ -21,6 +21,7 @@ module Ci
has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :job_artifacts_archive, -> { where(file_type: Ci::JobArtifact.file_types[:archive]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :job_artifacts_metadata, -> { where(file_type: Ci::JobArtifact.file_types[:metadata]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
# The "environment" field for builds is a String, and is the unexpanded name
def persisted_environment
......
......@@ -9,9 +9,12 @@ module Ci
mount_uploader :file, JobArtifactUploader
delegate :open, :exists?, to: :file
enum file_type: {
archive: 1,
metadata: 2
metadata: 2,
trace: 3
}
def self.artifacts_size_for(project)
......
......@@ -180,7 +180,7 @@ module Clusters
return unless managed?
if api_url_changed? || token_changed? || ca_pem_changed?
errors.add(:base, "cannot modify managed cluster")
errors.add(:base, _('Cannot modify managed Kubernetes cluster'))
return false
end
......
......@@ -39,7 +39,6 @@ module ArtifactMigratable
end
def artifacts_size
read_attribute(:artifacts_size).to_i +
job_artifacts_archive&.size.to_i + job_artifacts_metadata&.size.to_i
read_attribute(:artifacts_size).to_i + job_artifacts.sum(:size).to_i
end
end
......@@ -7,11 +7,12 @@ module Routable
has_one :route, as: :source, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :redirect_routes, as: :source, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
validates_associated :route
validates :route, presence: true
scope :with_route, -> { includes(:route) }
after_validation :set_path_errors
before_validation do
if full_path_changed? || full_name_changed?
prepare_route
......@@ -125,6 +126,11 @@ module Routable
private
def set_path_errors
route_path_errors = self.errors.delete(:"route.path")
self.errors[:path].concat(route_path_errors) if route_path_errors
end
def uncached_full_path
if route && route.path.present?
@full_path ||= route.path # rubocop:disable Gitlab/ModuleWithInstanceVariables
......
......@@ -20,6 +20,9 @@ class Namespace < ActiveRecord::Base
has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :project_statistics
# This should _not_ be `inverse_of: :namespace`, because that would also set
# `user.namespace` when this user creates a group with themselves as `owner`.
belongs_to :owner, class_name: "User"
belongs_to :parent, class_name: "Namespace"
......@@ -29,7 +32,6 @@ class Namespace < ActiveRecord::Base
validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
validates :name,
presence: true,
uniqueness: { scope: :parent_id },
length: { maximum: 255 },
namespace_name: true
......
......@@ -245,8 +245,7 @@ class Project < ActiveRecord::Base
validates :path,
presence: true,
project_path: true,
length: { maximum: 255 },
uniqueness: { scope: :namespace_id }
length: { maximum: 255 }
validates :namespace, presence: true
validates :name, uniqueness: { scope: :namespace_id }
......@@ -511,10 +510,13 @@ class Project < ActiveRecord::Base
@repository ||= Repository.new(full_path, self, disk_path: disk_path)
end
def reload_repository!
def cleanup
@repository&.cleanup
@repository = nil
end
alias_method :reload_repository!, :cleanup
def container_registry_url
if Gitlab.config.registry.enabled
"#{Gitlab.config.registry.host_port}/#{full_path.downcase}"
......
......@@ -150,9 +150,10 @@ class KubernetesService < DeploymentService
end
def deprecation_message
content = <<-MESSAGE.strip_heredoc
Kubernetes service integration has been deprecated. #{deprecated_message_content} your clusters using the new <a href=\'#{Gitlab::Routing.url_helpers.project_clusters_path(project)}'/>Clusters</a> page
MESSAGE
content = _("Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page") % {
deprecated_message_content: deprecated_message_content,
url: Gitlab::Routing.url_helpers.project_clusters_path(project)
}
content.html_safe
end
......@@ -248,9 +249,9 @@ class KubernetesService < DeploymentService
def deprecated_message_content
if active?
"Your cluster information on this page is still editable, but you are advised to disable and reconfigure"
_("Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure")
else
"Fields on this page are now uneditable, you can configure"
_("Fields on this page are now uneditable, you can configure")
end
end
end
......@@ -93,6 +93,10 @@ class Repository
alias_method :raw, :raw_repository
def cleanup
@raw_repository&.cleanup
end
# Return absolute path to repository
def path_to_repo
@path_to_repo ||= File.expand_path(
......
......@@ -75,7 +75,7 @@ class Route < ActiveRecord::Base
def ensure_permanent_paths
return if path.nil?
errors.add(:path, "#{path} has been taken before. Please use another one") if conflicting_redirect_exists?
errors.add(:path, "has been taken before") if conflicting_redirect_exists?
end
def conflicting_redirect_exists?
......
......@@ -12,6 +12,10 @@ class Upload < ActiveRecord::Base
before_save :calculate_checksum!, if: :foreground_checksummable?
after_commit :schedule_checksum, if: :checksummable?
# as the FileUploader is not mounted, the default CarrierWave ActiveRecord
# hooks are not executed and the file will not be deleted
after_destroy :delete_file!, if: -> { uploader_class <= FileUploader }
def self.hexdigest(path)
Digest::SHA256.file(path).hexdigest
end
......@@ -49,6 +53,10 @@ class Upload < ActiveRecord::Base
private
def delete_file!
build_uploader.remove!
end
def checksummable?
checksum.nil? && local? && exist?
end
......
......@@ -77,7 +77,7 @@ class User < ActiveRecord::Base
#
# Namespace for personal projects
has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, autosave: true # rubocop:disable Cop/ActiveRecordDependent
has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, inverse_of: :owner, autosave: true # rubocop:disable Cop/ActiveRecordDependent
# Profile
has_many :keys, -> do
......@@ -151,12 +151,9 @@ class User < ActiveRecord::Base
validates :projects_limit,
presence: true,
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
validates :username,
user_path: true,
presence: true,
uniqueness: { case_sensitive: false }
validates :username, presence: true
validate :namespace_uniq, if: :username_changed?
validates :namespace, presence: true
validate :namespace_move_dir_allowed, if: :username_changed?
validate :unique_email, if: :email_changed?
......@@ -171,7 +168,8 @@ class User < ActiveRecord::Base
before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? }
before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
after_save :ensure_namespace_correct
before_validation :ensure_namespace_correct
after_validation :set_username_errors
after_update :username_changed_hook, if: :username_changed?
after_destroy :post_destroy_hook
after_destroy :remove_key_cache
......@@ -230,8 +228,8 @@ class User < ActiveRecord::Base
scope :active, -> { with_state(:active).non_internal }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'ASC')) }
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
def self.with_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id")
......@@ -505,17 +503,6 @@ class User < ActiveRecord::Base
end
end
def namespace_uniq
# Return early if username already failed the first uniqueness validation
return if errors.key?(:username) &&
errors[:username].include?('has already been taken')
existing_namespace = Namespace.by_path(username)
if existing_namespace && existing_namespace != namespace
errors.add(:username, 'has already been taken')
end
end
def namespace_move_dir_allowed
if namespace&.any_project_has_container_registry_tags?
errors.add(:username, 'cannot be changed if a personal project has container registry tags.')
......@@ -884,19 +871,18 @@ class User < ActiveRecord::Base
end
def ensure_namespace_correct
# Ensure user has namespace
create_namespace!(path: username, name: username) unless namespace
if username_changed?
unless namespace.update_attributes(path: username, name: username)
namespace.errors.each do |attribute, message|
self.errors.add(:"namespace_#{attribute}", message)
end
raise ActiveRecord::RecordInvalid.new(namespace)
end
if namespace
namespace.path = namespace.name = username if username_changed?
else
build_namespace(path: username, name: username)
end
end
def set_username_errors
namespace_path_errors = self.errors.delete(:"namespace.path")
self.errors[:username].concat(namespace_path_errors) if namespace_path_errors
end
def username_changed_hook
system_hook_service.execute_hooks_for(self, :rename)
end
......
module Ci
class CreateTraceArtifactService < BaseService
def execute(job)
return if job.job_artifacts_trace
job.trace.read do |stream|
if stream.file?
job.create_job_artifacts_trace!(
project: job.project,
file_type: :trace,
file: stream)
end
end
end
end
end
......@@ -5,7 +5,7 @@ module Clusters
def execute(access_token = nil)
@access_token = access_token
raise ArgumentError.new('Instance does not support multiple clusters') unless can_create_cluster?
raise ArgumentError.new(_('Instance does not support multiple Kubernetes clusters')) unless can_create_cluster?
create_cluster.tap do |cluster|
ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted?
......
......@@ -28,7 +28,7 @@ module Clusters
if elapsed_time_from_creation(operation) < TIMEOUT
WaitForClusterCreationWorker.perform_in(EAGER_INTERVAL, provider.cluster_id)
else
provider.make_errored!("Cluster creation time exceeds timeout; #{TIMEOUT}")
provider.make_errored!(_('Kubernetes cluster creation time exceeds timeout; %{timeout}') % { timeout: TIMEOUT })
end
end
......
module Files
class CreateService < Files::BaseService
def create_commit!
handler = Lfs::FileModificationHandler.new(project, @branch_name)
handler.new_file(@file_path, @file_content) do |content_or_lfs_pointer|
create_transformed_commit(content_or_lfs_pointer)
end
end
private
def create_transformed_commit(content_or_lfs_pointer)
repository.create_file(
current_user,
@file_path,
@file_content,
content_or_lfs_pointer,
message: @commit_message,
branch_name: @branch_name,
author_email: @author_email,
......
module Lfs
class FileModificationHandler
attr_reader :project, :branch_name
delegate :repository, to: :project
def initialize(project, branch_name)
@project = project
@branch_name = branch_name
end
def new_file(file_path, file_content)
if project.lfs_enabled? && lfs_file?(file_path)
lfs_pointer_file = Gitlab::Git::LfsPointerFile.new(file_content)
lfs_object = create_lfs_object!(lfs_pointer_file, file_content)
content = lfs_pointer_file.pointer
success = yield(content)
link_lfs_object!(lfs_object) if success
else
yield(file_content)
end
end
private
def lfs_file?(file_path)
repository.attributes_at(branch_name, file_path)['filter'] == 'lfs'
end
def create_lfs_object!(lfs_pointer_file, file_content)
LfsObject.find_or_create_by(oid: lfs_pointer_file.sha256, size: lfs_pointer_file.size) do |lfs_object|
lfs_object.file = CarrierWaveStringFile.new(file_content)
end
end
def link_lfs_object!(lfs_object)
project.lfs_objects << lfs_object
end
end
end
......@@ -15,6 +15,8 @@ class FileUploader < GitlabUploader
storage :file
after :remove, :prune_store_dir
def self.root
File.join(options.storage_path, 'uploads')
end
......@@ -140,6 +142,10 @@ class FileUploader < GitlabUploader
end
end
def prune_store_dir
storage.delete_dir!(store_dir) # only remove when empty
end
def markdown_name
(image_or_video? ? File.basename(filename, File.extname(filename)) : filename).gsub("]", "\\]")
end
......
......@@ -13,6 +13,12 @@ class JobArtifactUploader < GitlabUploader
dynamic_segment
end
def open
raise 'Only File System is supported' unless file_storage?
File.open(path, "rb") if path
end
private
def dynamic_segment
......
......@@ -13,10 +13,6 @@ class AbstractPathValidator < ActiveModel::EachValidator
raise NotImplementedError
end
def self.full_path(record, value)
value
end
def self.valid_path?(path)
encode!(path)
"#{path}/" =~ path_regex
......@@ -28,7 +24,7 @@ class AbstractPathValidator < ActiveModel::EachValidator
return
end
full_path = self.class.full_path(record, value)
full_path = record.build_full_path
return unless full_path
unless self.class.valid_path?(full_path)
......
......@@ -12,8 +12,4 @@ class NamespacePathValidator < AbstractPathValidator
def self.format_error_message
Gitlab::PathRegex.namespace_format_message
end
def self.full_path(record, value)
record.build_full_path
end
end
......@@ -12,8 +12,4 @@ class ProjectPathValidator < AbstractPathValidator
def self.format_error_message
Gitlab::PathRegex.project_path_format_message
end
def self.full_path(record, value)
record.build_full_path
end
end
class UserPathValidator < AbstractPathValidator
extend Gitlab::EncodingHelper
def self.path_regex
Gitlab::PathRegex.root_namespace_path_regex
end
def self.format_regex
Gitlab::PathRegex.namespace_format_regex
end
def self.format_error_message
Gitlab::PathRegex.namespace_format_message
end
end
......@@ -88,7 +88,7 @@
%strong.fly-out-top-item-name
#{ _('Members') }
- if current_user && can?(current_user, :admin_group, @group)
= nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do
= nav_link(path: group_nav_link_paths) do
= link_to edit_group_path(@group) do
.nav-icon-container
= sprite_icon('settings')
......
......@@ -186,9 +186,9 @@
- if project_nav_tab? :clusters
- show_cluster_hint = show_gke_cluster_integration_callout?(@project)
= nav_link(controller: [:clusters, :user, :gcp]) do
= link_to project_clusters_path(@project), title: 'Cluster', class: 'shortcuts-cluster' do
= link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-cluster' do
%span
Clusters
= _('Kubernetes')
- if show_cluster_hint
.feature-highlight.js-feature-highlight{ disabled: true,
data: { trigger: 'manual',
......@@ -206,13 +206,12 @@
%p
= _('Protip:')
= link_to 'Auto DevOps', help_page_path('topics/autodevops/index.md')
%span= _('uses clusters to deploy your code!')
%span= _('uses Kubernetes clusters to deploy your code!')
%hr
%button.btn.btn-create.btn-xs.dismiss-feature-highlight{ type: 'button' }
%span= _("Got it!")
= sprite_icon('thumb-up')
- if @project.feature_available?(:builds, current_user) && !@project.empty_repo?
= nav_link(path: 'pipelines#charts') do
= link_to charts_project_pipelines_path(@project), title: 'Charts', class: 'shortcuts-pipelines-charts' do
......
......@@ -5,11 +5,11 @@
= s_('ClusterIntegration|Google Kubernetes Engine')
%p
- link_gke = link_to(s_('ClusterIntegration|Google Kubernetes Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Manage your cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke }
= s_('ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke }
.well.form-group
%label.text-danger
= s_('ClusterIntegration|Remove cluster integration')
= s_('ClusterIntegration|Remove Kubernetes cluster integration')
%p
= s_("ClusterIntegration|Remove this cluster's configuration from this project. This will not delete your actual cluster.")
= link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: s_("ClusterIntegration|Are you sure you want to remove this cluster's integration? This will not delete your actual cluster.")})
= s_("ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster.")
= link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: s_("ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster.")})
%h4= s_('ClusterIntegration|Cluster integration')
%h4= s_('ClusterIntegration|Kubernetes cluster integration')
.settings-content
.hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' }
= s_('ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine')
= s_('ClusterIntegration|Something went wrong while creating your Kubernetes cluster on Google Kubernetes Engine')
%p.js-error-reason
.hidden.js-cluster-creating.alert.alert-info.alert-block.append-bottom-10{ role: 'alert' }
= s_('ClusterIntegration|Cluster is being created on Google Kubernetes Engine...')
= s_('ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine...')
.hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' }
= s_('ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster\'s details')
= s_("ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details")
%p= s_('ClusterIntegration|Control how your cluster integrates with GitLab')
%p= s_('ClusterIntegration|Control how your Kubernetes cluster integrates with GitLab')
.gl-responsive-table-row
.table-section.section-30
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Cluster")
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster")
.table-mobile-content
= link_to cluster.name, namespace_project_cluster_path(@project.namespace, @project, cluster)
.table-section.section-30
......@@ -14,7 +14,7 @@
.table-mobile-content
%button.js-project-feature-toggle.project-feature-toggle{ type: "button",
class: "#{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}",
"aria-label": s_("ClusterIntegration|Toggle Cluster"),
"aria-label": s_("ClusterIntegration|Toggle Kubernetes Cluster"),
disabled: !cluster.can_toggle_cluster?,
data: { endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } }
%input.js-project-feature-toggle-input{ type: "hidden", value: cluster.enabled? }
......
%h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up cluster integration')
%h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up Kubernetes cluster integration')
.dropdown.clusters-dropdown
%button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', data: { toggle: 'dropdown' }, 'aria-haspopup': true, 'aria-expanded': false }
......@@ -7,6 +7,6 @@
= icon('chevron-down')
%ul.dropdown-menu.clusters-dropdown-menu.dropdown-menu-full-width
%li
= link_to(s_('ClusterIntegration|Create cluster on Google Kubernetes Engine'), gcp_new_namespace_project_clusters_path(@project.namespace, @project))
= link_to(s_('ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine'), gcp_new_namespace_project_clusters_path(@project.namespace, @project))
%li
= link_to(s_('ClusterIntegration|Add an existing cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project))
= link_to(s_('ClusterIntegration|Add an existing Kubernetes cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project))
......@@ -3,10 +3,9 @@
.svg-content= image_tag 'illustrations/clusters_empty.svg'
.col-xs-12
.text-content
%h4.text-center= s_('ClusterIntegration|Integrate cluster automation')
- link_to_help_page = link_to(s_('ClusterIntegration|Learn more about Clusters'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
%p= s_('ClusterIntegration|Clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page}
%h4.text-center= s_('ClusterIntegration|Integrate Kubernetes cluster automation')
- link_to_help_page = link_to(s_('ClusterIntegration|Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
%p= s_('ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page}
.text-center
= link_to s_('ClusterIntegration|Add cluster'), new_project_cluster_path(@project), class: 'btn btn-success'
= link_to s_('ClusterIntegration|Add Kubernetes cluster'), new_project_cluster_path(@project), class: 'btn btn-success'
......@@ -5,15 +5,15 @@
%p
- if @cluster.enabled?
- if can?(current_user, :update_cluster, @cluster)
= s_('ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab\'s connection to it.')
= s_('ClusterIntegration|Kubernetes cluster integration is enabled for this project. Disabling this integration will not affect your Kubernetes cluster, it will only temporarily turn off GitLab\'s connection to it.')
- else
= s_('ClusterIntegration|Cluster integration is enabled for this project.')
= s_('ClusterIntegration|Kubernetes cluster integration is enabled for this project.')
- else
= s_('ClusterIntegration|Cluster integration is disabled for this project.')
= s_('ClusterIntegration|Kubernetes cluster integration is disabled for this project.')
%label.append-bottom-10.js-cluster-enable-toggle-area
%button{ type: 'button',
class: "js-project-feature-toggle project-feature-toggle #{'is-checked' if @cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}",
"aria-label": s_("ClusterIntegration|Toggle Cluster"),
"aria-label": s_("ClusterIntegration|Toggle Kubernetes cluster"),
disabled: !can?(current_user, :update_cluster, @cluster) }
= field.hidden_field :enabled, { class: 'js-project-feature-toggle-input'}
%span.toggle-icon
......@@ -23,7 +23,7 @@
.form-group
%h5= s_('ClusterIntegration|Environment scope')
%p
= s_("ClusterIntegration|Choose which of your project's environments will use this cluster.")
= s_("ClusterIntegration|Choose which of your project's environments will use this Kubernetes cluster.")
= link_to s_("ClusterIntegration|Learn more about environments"), help_page_path('ci/environments')
= field.text_field :environment_scope, class: 'form-control js-select-on-focus', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
......
%h4.prepend-top-0
= s_('ClusterIntegration|Cluster integration')
= s_('ClusterIntegration|Kubernetes cluster integration')
%p
= s_('ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way.')
= s_('ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way.')
%p
- link = link_to(s_('ClusterIntegration|cluster'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
- link = link_to(_('Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Learn more about %{link_to_documentation}').html_safe % { link_to_documentation: link }
%p
- link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Read our %{link_to_help_page} on cluster integration.').html_safe % { link_to_help_page: link_to_help_page}
= s_('ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration.').html_safe % { link_to_help_page: link_to_help_page}
= form_for @cluster, html: { class: 'prepend-top-20' }, url: gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
= form_errors(@cluster)
.form-group
= field.label :name, s_('ClusterIntegration|Cluster name')
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Cluster name')
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name')
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
.form-group
= field.label :environment_scope, s_('ClusterIntegration|Environment scope')
= field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
......@@ -32,4 +32,4 @@
= provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-4'
.form-group
= field.submit s_('ClusterIntegration|Create cluster'), class: 'btn btn-success'
= field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'btn btn-success'
%h4.prepend-top-20
= s_('ClusterIntegration|Enter the details for your cluster')
= s_('ClusterIntegration|Enter the details for your Kubernetes cluster')
%p
= s_('ClusterIntegration|Please make sure that your Google account meets the following requirements:')
%ul
......@@ -8,7 +8,7 @@
= s_('ClusterIntegration|Your account must have %{link_to_kubernetes_engine}').html_safe % { link_to_kubernetes_engine: link_to_kubernetes_engine }
%li
- link_to_requirements = link_to(s_('ClusterIntegration|meets the requirements'), 'https://cloud.google.com/kubernetes-engine/docs/quickstart?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters').html_safe % { link_to_requirements: link_to_requirements }
= s_('ClusterIntegration|Make sure your account %{link_to_requirements} to create Kubernetes clusters').html_safe % { link_to_requirements: link_to_requirements }
%li
- link_to_container_project = link_to(s_('ClusterIntegration|Google Kubernetes Engine project'), 'https://console.cloud.google.com/home/dashboard?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below').html_safe % { link_to_container_project: link_to_container_project }
= s_('ClusterIntegration|This account must have permissions to create a Kubernetes cluster in the %{link_to_container_project} specified below').html_safe % { link_to_container_project: link_to_container_project }
.form-group
%label.append-bottom-10{ for: 'cluster-name' }
= s_('ClusterIntegration|Cluster name')
= s_('ClusterIntegration|Kubernetes cluster name')
.input-group
%input.form-control.cluster-name.js-select-on-focus{ value: @cluster.name, readonly: true }
%span.input-group-btn
= clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy cluster name'), class: 'btn-default')
= clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), class: 'btn-default')
= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
= form_errors(@cluster)
......
- breadcrumb_title "Cluster"
- breadcrumb_title 'Kubernetes'
- page_title _("Login")
.row.prepend-top-default
.col-sm-4
= render 'projects/clusters/sidebar'
.col-sm-8
= render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create cluster on Google Kubernetes Engine')
= render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine')
= render 'header'
.row
.col-sm-8.col-sm-offset-4.signin-with-google
......
- breadcrumb_title "Cluster"
- page_title _("New Cluster")
- breadcrumb_title 'Kubernetes'
- page_title _("New Kubernetes Cluster")
.row.prepend-top-default
.col-sm-4
= render 'projects/clusters/sidebar'
.col-sm-8
= render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create cluster on Google Kubernetes Engine')
= render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine')
= render 'header'
= render 'form'
- breadcrumb_title "Clusters"
- page_title "Clusters"
- breadcrumb_title 'Kubernetes'
- page_title "Kubernetes Clusters"
.clusters-container
- if @clusters.empty?
......@@ -7,11 +7,11 @@
- else
.top-area.adjust
.nav-text
= s_("ClusterIntegration|Clusters can be used to deploy applications and to provide Review Apps for this project")
= s_("ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project")
.ci-table.js-clusters-list
.gl-responsive-table-row.table-row-header{ role: "row" }
.table-section.section-30{ role: "rowheader" }
= s_("ClusterIntegration|Cluster")
= s_("ClusterIntegration|Kubernetes cluster")
.table-section.section-30{ role: "rowheader" }
= s_("ClusterIntegration|Environment scope")
.table-section.section-30{ role: "rowheader" }
......
- breadcrumb_title "Cluster"
- page_title _("Cluster")
- breadcrumb_title 'Kubernetes'
- page_title _("Kubernetes Cluster")
.row.prepend-top-default
.col-sm-4
= render 'sidebar'
.col-sm-8
%h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up cluster integration')
%h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up Kubernetes cluster integration')
%p= s_('ClusterIntegration|Create a new cluster on Google Kubernetes Engine right from GitLab')
%p= s_('ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab')
= link_to s_('ClusterIntegration|Create on GKE'), gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
%p= s_('ClusterIntegration|Enter the details for an existing Kubernetes cluster')
= link_to s_('ClusterIntegration|Add an existing cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
= link_to s_('ClusterIntegration|Add an existing Kubernetes cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
- @content_class = "limit-container-width" unless fluid_layout
- add_to_breadcrumbs "Clusters", project_clusters_path(@project)
- add_to_breadcrumbs "Kubernetes Clusters", project_clusters_path(@project)
- breadcrumb_title @cluster.name
- page_title _("Cluster")
- page_title _("Kubernetes Cluster")
- expanded = Rails.env.test?
......@@ -13,7 +13,8 @@
toggle_status: @cluster.enabled? ? 'true': 'false',
cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason,
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications') } }
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'),
ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-ip-address') } }
.js-cluster-application-notice
.flash-container
......@@ -26,10 +27,10 @@
%section.settings#js-cluster-details{ class: ('expanded' if expanded) }
.settings-header
%h4= s_('ClusterIntegration|Cluster details')
%h4= s_('ClusterIntegration|Kubernetes cluster details')
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p= s_('ClusterIntegration|See and edit the details for your cluster')
%p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster')
.settings-content
- if @cluster.managed?
= render 'projects/clusters/gcp/show'
......@@ -41,6 +42,6 @@
%h4= _('Advanced settings')
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p= s_("ClusterIntegration|Advanced options on this cluster's integration")
%p= s_("ClusterIntegration|Advanced options on this Kubernetes cluster's integration")
.settings-content
= render 'advanced_settings'
= form_for @cluster, url: user_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
= form_errors(@cluster)
.form-group
= field.label :name, s_('ClusterIntegration|Cluster name')
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Cluster name')
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name')
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
.form-group
= field.label :environment_scope, s_('ClusterIntegration|Environment scope')
= field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
......@@ -25,4 +25,4 @@
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
.form-group
= field.submit s_('ClusterIntegration|Add cluster'), class: 'btn btn-success'
= field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success'
%h4.prepend-top-20
= s_('ClusterIntegration|Enter the details for your cluster')
= s_('ClusterIntegration|Enter the details for your Kubernetes cluster')
%p
- link_to_help_page = link_to(s_('ClusterIntegration|documentation'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Please enter access information for your cluster. If you need help, you can read our %{link_to_help_page} on clusters').html_safe % { link_to_help_page: link_to_help_page }
= s_('ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{link_to_help_page} on Kubernetes').html_safe % { link_to_help_page: link_to_help_page }
= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
= form_errors(@cluster)
.form-group
= field.label :name, s_('ClusterIntegration|Cluster name')
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Cluster name')
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name')
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
= field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
.form-group
......
- breadcrumb_title "Cluster"
- page_title _("New Cluster")
- breadcrumb_title 'Kubernetes'
- page_title _("New Kubernetes cluster")
.row.prepend-top-default
.col-sm-4
= render 'projects/clusters/sidebar'
.col-sm-8
= render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Add an existing cluster')
= render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Add an existing Kubernetes cluster')
= render 'header'
.prepend-top-20
= render 'form'
.modal{ id: "modal-delete-label-#{label.id}", tabindex: -1 }
.modal-dialog
.modal-content
.modal-header
%button.close{ data: {dismiss: 'modal' } } &times;
%h3.page-title Delete #{render_colored_label(label, tooltip: false)} ?
.modal-body
%p
%strong= label.name
%span will be permanently deleted from #{label.is_a?(ProjectLabel)? label.project.name : label.group.name}. This cannot be undone.
.modal-footer
%a{ href: '#', data: { dismiss: 'modal' }, class: 'btn btn-default' } Cancel
= link_to 'Delete label',
destroy_label_path(label),
title: 'Delete',
method: :delete,
class: 'btn btn-remove'
......@@ -5,10 +5,10 @@
- show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project)
- show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project)
%li{ id: label_css_id, data: { id: label.id } }
%li.label-list-item{ id: label_css_id, data: { id: label.id } }
= render "shared/label_row", label: label
.visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown
.visible-xs.visible-sm-inline-block.dropdown
%button.btn.btn-default.label-options-toggle{ type: 'button', data: { toggle: "dropdown" } }
Options
= icon('caret-down')
......@@ -46,14 +46,19 @@
data: {confirm: 'Remove this label? Are you sure?'},
class: 'text-danger'
.pull-right.hidden-xs.hidden-sm.hidden-md
- if show_label_merge_requests_link
= link_to_label(label, subject: subject, type: :merge_request, css_class: 'btn btn-transparent btn-action btn-link') do
view merge requests
- if show_label_issues_link
= link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action btn-link') do
view open issues
.pull-right.hidden-xs.hidden-sm
- if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group)
= link_to promote_project_label_path(label.project, label), title: "Promote to Group Label", class: 'btn btn-transparent btn-action', data: {confirm: "You are about to promote #{label.title} to a group level. This will make this milestone available to all projects inside #{label.project.group.name}. The existing project label will be merged into the group level. This action cannot be reversed.", toggle: "tooltip"}, method: :post do
%span.sr-only Promote to Group
= sprite_icon('level-up')
- if can?(current_user, :admin_label, label)
= link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
%span.sr-only Edit
= sprite_icon('pencil')
%span{ data: { toggle: 'modal', target: "#modal-delete-label-#{label.id}" } }
= link_to "#", title: "Delete", class: 'btn btn-transparent btn-action remove-row', data: { toggle: "tooltip" } do
%span.sr-only Delete
= sprite_icon('remove')
- if current_user
.label-subscription.inline
- if can_subscribe_to_label_in_different_levels?(label)
......@@ -76,14 +81,4 @@
%span= label_subscription_toggle_button_text(label, @project)
= icon('spinner spin', class: 'label-subscribe-button-loading')
- if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group)
= link_to promote_project_label_path(label.project, label), title: "Promote to Group Label", class: 'btn btn-transparent btn-action', data: {confirm: "You are about to promote #{label.title} to a group level. This will make this milestone available to all projects inside #{label.project.group.name}. The existing project label will be merged into the group level. This action cannot be reversed.", toggle: "tooltip"}, method: :post do
%span.sr-only Promote to Group
= icon('level-up')
- if can?(current_user, :admin_label, label)
= link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
%span.sr-only Edit
= icon('pencil-square-o')
= link_to destroy_label_path(label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, data: {confirm: label_deletion_confirm_text(label), toggle: "tooltip"} do
%span.sr-only Delete
= icon('trash-o')
= render 'shared/delete_label_modal', label: label
- subject = local_assigns[:subject]
- show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project)
- show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project)
%span.label-row
- if can?(current_user, :admin_label, @project)
.draggable-handler
......@@ -13,6 +17,14 @@
- if defined?(@project) && @project.group.present?
%span.label-type
= label.model_name.human.titleize
- if label.description
%span.label-description
= markdown_field(label, :description)
%span.label-description
- if label.description.present?
.description-text
= markdown_field(label, :description)
.hidden-xs.hidden-sm
- if show_label_issues_link
= link_to_label(label, subject: subject) { 'Issues' }
- if show_label_merge_requests_link
&middot;
= link_to_label(label, subject: subject, type: :merge_request) { 'Merge requests' }
......@@ -43,6 +43,7 @@
- pipeline_creation:run_pipeline_schedule
- pipeline_default:build_coverage
- pipeline_default:build_trace_sections
- pipeline_default:create_trace_artifact
- pipeline_default:pipeline_metrics
- pipeline_default:pipeline_notification
- pipeline_default:update_head_pipeline_for_merge_request
......
......@@ -6,9 +6,13 @@ class BuildFinishedWorker
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
BuildTraceSectionsWorker.perform_async(build.id)
# We execute that in sync as this access the files in order to access local file, and reduce IO
BuildTraceSectionsWorker.new.perform(build.id)
BuildCoverageWorker.new.perform(build.id)
BuildHooksWorker.new.perform(build.id)
# We execute that async as this are two indepentent operations that can be executed after TraceSections and Coverage
BuildHooksWorker.perform_async(build.id)
CreateTraceArtifactWorker.perform_async(build.id)
end
end
end
class CreateTraceArtifactWorker
include ApplicationWorker
include PipelineQueue
def perform(job_id)
Ci::Build.preload(:project, :user).find_by(id: job_id).try do |job|
Ci::CreateTraceArtifactService.new(job.project, job.user).execute(job)
end
end
end
......@@ -18,6 +18,8 @@ class ProjectCacheWorker
update_statistics(project, statistics.map(&:to_sym))
project.repository.refresh_method_caches(files.map(&:to_sym))
project.cleanup
end
def update_statistics(project, statistics = [])
......
---
title: Deleting an upload will correctly clean up the filesystem.
merge_request: 16799
author:
type: fixed
---
title: User can now git push to create a new project
merge_request: 16547
author:
type: added
---
title: Fix Sort by Recent Sign-in in Admin Area
merge_request: 13852
author: Poornima M
---
title: Add sorting options for /users API (admin only)
merge_request: 16945
author:
type: added
---
title: Add a link to documentation on how to get external ip in the Kubernetes cluster details page
merge_request: 16937
author:
type: added
---
title: Close low level rugged repository in project cache worker
merge_request: 16930
author: Bastian Blank
type: fixed
---
title: Upgrade GitLab Workhorse to v3.6.0
merge_request:
author:
type: other
---
title: Validate user, group and project paths consistently, and only once
merge_request:
author:
type: fixed
---
title: Validate user namespace before saving so that errors persist on model
merge_request:
author:
type: fixed
---
title: Save traces as artifacts
merge_request: 16702
author:
type: changed
---
title: Override group sidebar links
merge_request: 16942
author: George Tsiolis
type: fixed
---
title: File Upload UI can create LFS pointers based on .gitattributes
merge_request: 16412
author:
type: fixed
---
title: Replace "cluster" with "Kubernetes cluster"
merge_request: 16778
author:
type: changed
---
title: Downgrade google-protobuf gem
merge_request: 16941
author:
type: other
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class ResetEventsPrimaryKeySequence < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
class Event < ActiveRecord::Base
self.table_name = 'events'
end
def up
if Gitlab::Database.postgresql?
reset_primary_key_for_postgresql
else
reset_primary_key_for_mysql
end
end
def down
# No-op
end
def reset_primary_key_for_postgresql
reset_pk_sequence!(Event.table_name)
end
def reset_primary_key_for_mysql
amount = Event.pluck('COALESCE(MAX(id), 1)').first
execute "ALTER TABLE #{Event.table_name} AUTO_INCREMENT = #{amount}"
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180204200836) do
ActiveRecord::Schema.define(version: 20180206200543) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......
......@@ -51,6 +51,11 @@ GET /users?blocked=true
GET /users
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `order_by` | string | no | Return projects ordered by `id`, `name`, `username`, `created_at`, or `updated_at` fields. Default is `id` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
```json
[
{
......
......@@ -16,7 +16,7 @@ There are many places where file uploading is used, according to contexts:
- Project avatars
- Issues/MR/Notes Markdown attachments
- Issues/MR/Notes Legacy Markdown attachments
- CI Build Artifacts
- CI Artifacts (archive, metadata, trace)
- LFS Objects
......@@ -35,7 +35,7 @@ they are still not 100% standardized. You can see them below:
| Project avatars | yes | uploads/-/system/project/avatar/:id/:filename | `AvatarUploader` | Project |
| Issues/MR/Notes Markdown attachments | yes | uploads/:project_path_with_namespace/:random_hex/:filename | `FileUploader` | Project |
| Issues/MR/Notes Legacy Markdown attachments | no | uploads/-/system/note/attachment/:id/:filename | `AttachmentUploader` | Note |
| CI Artifacts (CE) | yes | shared/artifacts/:year_:month/:project_id/:id | `ArtifactUploader` | Ci::Build |
| CI Artifacts (CE) | yes | shared/artifacts/:disk_hash[0..1]/:disk_hash[2..3]/:disk_hash/:year_:month_:date/:job_id/:job_artifact_id (:disk_hash is SHA256 digest of project_id) | `JobArtifactUploader` | Ci::JobArtifact |
| LFS Objects (CE) | yes | shared/lfs-objects/:hex/:hex/:object_hash | `LfsObjectUploader` | LfsObject |
CI Artifacts and LFS Objects behave differently in CE and EE. In CE they inherit the `GitlabUploader`
......
......@@ -33,5 +33,40 @@
1. Click **Create project**.
## Push to create a new project
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/26388) in GitLab 10.5.
When you create a new repo locally, instead of going to GitLab to manually
create a new project and then push the repo, you can directly push it to
GitLab to create the new project, all without leaving your terminal. If you have access to that
namespace, we will automatically create a new project under that GitLab namespace with its
visibility set to private by default (you can later change it in the UI).
This can be done by using either SSH or HTTP:
```
## Git push using SSH
git push git@gitlab.example.com:namespace/nonexistent-project.git
## Git push using HTTP
git push https://gitlab.example.com/namespace/nonexistent-project.git
```
Once the push finishes successfully, a remote message will indicate
the command to set the remote and the URL to the new project:
```
remote:
remote: The private project namespace/nonexistent-project was created.
remote:
remote: To configure the remote, run:
remote: git remote add origin https://gitlab.example.com/namespace/nonexistent-project.git
remote:
remote: To view the project, visit:
remote: https://gitlab.example.com/namespace/nonexistent-project
remote:
```
[import it]: ../workflow/importing/README.md
[reserved]: ../user/reserved_names.md
# GitLab Helm Chart
> **Note**:
* This chart is deprecated, and is being replaced by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). For more information on available charts, please see our [overview](index.md#chart-overview).
* These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
> **Note:**
* This chart has been tested on Google Kubernetes Engine and Azure Container Service.
**This chart is deprecated.** For small installations on Kubernetes today, we recommend the beta [`gitlab-omnibus` Helm chart](gitlab_omnibus.md).
A new [cloud native GitLab chart](index.md#cloud-native-gitlab-chart) is in development with increased scalability and resilience, among other benefits. The cloud native chart will replace both the `gitlab` and `gitlab-omnibus` charts when available later this year.
For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview).
Due to the significant architectural changes, migrating will require backing up data out of this instance and restoring it into the new deployment. For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview).
## Introduction
The `gitlab` Helm chart deploys just GitLab into your Kubernetes cluster, and offers extensive configuration options. This chart requires advanced knowledge of Kubernetes to successfully use. We **strongly recommend** the [gitlab-omnibus](gitlab_omnibus.md) chart.
This chart is deprecated, and will be replaced by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). Due to the difficulty in supporting upgrades, migrating will require exporting data out of this instance and importing it into the new deployment.
This chart includes the following:
- Deployment using the [gitlab-ce](https://hub.docker.com/r/gitlab/gitlab-ce) or [gitlab-ee](https://hub.docker.com/r/gitlab/gitlab-ee) container image
......
# GitLab-Omnibus Helm Chart
> **Note:**
* This Helm chart is in beta, and will be deprecated by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md).
* These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
> **Note:**.
* This chart has been tested on Google Kubernetes Engine and Azure Container Service.
This work is based partially on: https://github.com/lwolf/kubernetes-gitlab/. GitLab would like to thank Sergey Nuzhdin for his work.
**[This chart is beta](#limitations), and is the best way to install GitLab on Kubernetes today.** A new [cloud native GitLab chart](index.md#cloud-native-gitlab-chart) is in development with increased scalability and resilience, among other benefits. Once available, the cloud native chart will be the recommended installation method for Kubernetes, and this chart will be deprecated.
For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview).
This work is based partially on: https://github.com/lwolf/kubernetes-gitlab/. GitLab would like to thank Sergey Nuzhdin for his work.
## Introduction
This chart provides an easy way to get started with GitLab, provisioning an installation with nearly all functionality enabled. SSL is automatically provisioned via [Let's Encrypt](https://letsencrypt.org/).
This Helm chart is in beta, and will be deprecated by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md) once available. Due to the difficulty in supporting upgrades, migrating will require exporting data out of this instance and importing it into the new deployment.
This Helm chart is in beta, and is suited for small to medium deployments. It will be deprecated by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md) once available. Due to the significant architectural changes, migrating will require backing up data out of this instance and importing it into the new deployment.
The deployment includes:
......
......@@ -154,8 +154,8 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
step 'commit has ci status' do
@project.enable_ci
pipeline = create :ci_pipeline, project: @project, sha: sample_commit.id
create :ci_build, pipeline: pipeline
@pipeline = create(:ci_pipeline, project: @project, sha: sample_commit.id)
create(:ci_build, pipeline: @pipeline)
end
step 'repository contains ".gitlab-ci.yml" file' do
......@@ -163,7 +163,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end
step 'I see commit ci info' do
expect(page).to have_content "Pipeline #1 pending"
expect(page).to have_content "Pipeline ##{@pipeline.id} pending"
end
step 'I search "submodules" commits' do
......
......@@ -15,8 +15,9 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
step 'I delete all labels' do
page.within '.labels' do
page.all('.remove-row').each do
accept_confirm { first('.remove-row').click }
page.all('.label-list-item').each do
first('.remove-row').click
first(:link, 'Delete label').click
end
end
end
......
......@@ -60,8 +60,20 @@ module API
false
end
def project_path
project&.path || project_path_match[:project_path]
end
def namespace_path
project&.namespace&.full_path || project_path_match[:namespace_path]
end
private
def project_path_match
@project_path_match ||= params[:project].match(Gitlab::PathRegex.full_project_git_path_regex) || {}
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def set_project
if params[:gl_repository]
......
......@@ -42,11 +42,14 @@ module API
end
access_checker_klass = wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess
access_checker = access_checker_klass
.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities, redirected_path: redirected_path)
access_checker = access_checker_klass.new(actor, project,
protocol, authentication_abilities: ssh_authentication_abilities,
namespace_path: namespace_path, project_path: project_path,
redirected_path: redirected_path)
begin
access_checker.check(params[:action], params[:changes])
@project ||= access_checker.project
rescue Gitlab::GitAccess::UnauthorizedError, Gitlab::GitAccess::NotFoundError => e
return { status: false, message: e.message }
end
......@@ -207,8 +210,11 @@ module API
# A user is not guaranteed to be returned; an orphaned write deploy
# key could be used
if user
redirect_message = Gitlab::Checks::ProjectMoved.fetch_redirect_message(user.id, project.id)
redirect_message = Gitlab::Checks::ProjectMoved.fetch_message(user.id, project.id)
project_created_message = Gitlab::Checks::ProjectCreated.fetch_message(user.id, project.id)
output[:redirected_message] = redirect_message if redirect_message
output[:project_created_message] = project_created_message if project_created_message
end
output
......
......@@ -18,6 +18,14 @@ module API
User.find_by(id: id) || not_found!('User')
end
def reorder_users(users)
if params[:order_by] && params[:sort]
users.reorder(params[:order_by] => params[:sort])
else
users
end
end
params :optional_attributes do
optional :skype, type: String, desc: 'The Skype username'
optional :linkedin, type: String, desc: 'The LinkedIn username'
......@@ -35,6 +43,13 @@ module API
optional :avatar, type: File, desc: 'Avatar image for user'
all_or_none_of :extern_uid, :provider
end
params :sort_params do
optional :order_by, type: String, values: %w[id name username created_at updated_at],
default: 'id', desc: 'Return users ordered by a field'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return users sorted in ascending and descending order'
end
end
desc 'Get the list of users' do
......@@ -53,16 +68,18 @@ module API
optional :created_before, type: DateTime, desc: 'Return users created before the specified time'
all_or_none_of :extern_uid, :provider
use :sort_params
use :pagination
end
get do
authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?)
unless current_user&.admin?
params.except!(:created_after, :created_before)
params.except!(:created_after, :created_before, :order_by, :sort)
end
users = UsersFinder.new(current_user, params).execute
users = reorder_users(users)
authorized = can?(current_user, :read_users_list)
......
class CarrierWaveStringFile < StringIO
def original_filename
""
end
end
......@@ -2,7 +2,7 @@ class UserUrlConstrainer
def matches?(request)
full_path = request.params[:username]
return false unless UserPathValidator.valid_path?(full_path)
return false unless NamespacePathValidator.valid_path?(full_path)
User.find_by_full_path(full_path, follow_redirects: request.get?).present?
end
......
......@@ -15,8 +15,8 @@ module Gitlab
.ancestor?(oldrev, newrev)
else
Gitlab::Git::RevList.new(
path_to_repo: project.repository.path_to_repo,
oldrev: oldrev, newrev: newrev).missed_ref.present?
project.repository.raw, oldrev: oldrev, newrev: newrev
).missed_ref.present?
end
end
end
......
module Gitlab
module Checks
class PostPushMessage
def initialize(project, user, protocol)
@project = project
@user = user
@protocol = protocol
end
def self.fetch_message(user_id, project_id)
key = message_key(user_id, project_id)
Gitlab::Redis::SharedState.with do |redis|
message = redis.get(key)
redis.del(key)
message
end
end
def add_message
return unless user.present? && project.present?
Gitlab::Redis::SharedState.with do |redis|
key = self.class.message_key(user.id, project.id)
redis.setex(key, 5.minutes, message)
end
end
def message
raise NotImplementedError
end
protected
attr_reader :project, :user, :protocol
def self.message_key(user_id, project_id)
raise NotImplementedError
end
def url_to_repo
protocol == 'ssh' ? project.ssh_url_to_repo : project.http_url_to_repo
end
end
end
end
module Gitlab
module Checks
class ProjectCreated < PostPushMessage
PROJECT_CREATED = "project_created".freeze
def message
<<~MESSAGE
The private project #{project.full_path} was successfully created.
To configure the remote, run:
git remote add origin #{url_to_repo}
To view the project, visit:
#{project_url}
MESSAGE
end
private
def self.message_key(user_id, project_id)
"#{PROJECT_CREATED}:#{user_id}:#{project_id}"
end
def project_url
Gitlab::Routing.url_helpers.project_url(project)
end
end
end
end
module Gitlab
module Checks
class ProjectMoved
class ProjectMoved < PostPushMessage
REDIRECT_NAMESPACE = "redirect_namespace".freeze
def initialize(project, user, redirected_path, protocol)
@project = project
@user = user
def initialize(project, user, protocol, redirected_path)
@redirected_path = redirected_path
@protocol = protocol
end
def self.fetch_redirect_message(user_id, project_id)
redirect_key = redirect_message_key(user_id, project_id)
Gitlab::Redis::SharedState.with do |redis|
message = redis.get(redirect_key)
redis.del(redirect_key)
message
end
end
def add_redirect_message
# Don't bother with sending a redirect message for anonymous clones
# because they never see it via the `/internal/post_receive` endpoint
return unless user.present? && project.present?
Gitlab::Redis::SharedState.with do |redis|
key = self.class.redirect_message_key(user.id, project.id)
redis.setex(key, 5.minutes, redirect_message)
end
super(project, user, protocol)
end
def redirect_message(rejected: false)
<<~MESSAGE.strip_heredoc
def message(rejected: false)
<<~MESSAGE
Project '#{redirected_path}' was moved to '#{project.full_path}'.
Please update your Git remote:
......@@ -47,17 +25,17 @@ module Gitlab
private
attr_reader :project, :redirected_path, :protocol, :user
attr_reader :redirected_path
def self.redirect_message_key(user_id, project_id)
def self.message_key(user_id, project_id)
"#{REDIRECT_NAMESPACE}:#{user_id}:#{project_id}"
end
def remote_url_message(rejected)
if rejected
"git remote set-url origin #{url} and try again."
"git remote set-url origin #{url_to_repo} and try again."
else
"git remote set-url origin #{url}"
"git remote set-url origin #{url_to_repo}"
end
end
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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