Commit e52c401e authored by GitLab Bot's avatar GitLab Bot

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

# Conflicts:
#	app/assets/javascripts/api.js
#	app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
#	app/finders/license_template_finder.rb
#	app/finders/template_finder.rb
#	app/models/commit_status.rb
#	app/models/issue.rb
#	app/models/note.rb
#	app/models/system_note_metadata.rb
#	app/services/ci/process_build_service.rb
#	db/schema.rb
#	doc/api/README.md
#	spec/features/projects/jobs_spec.rb
#	spec/models/issue_spec.rb

[ci skip]
parents 6dba97cf 1239629c
......@@ -16,3 +16,5 @@ db/ @abrandl @NikolayS
/ee/lib/gitlab/code_owners/ @reprazent
/ee/lib/ee/gitlab/auth/ldap/ @dblessing @mkozono
/lib/gitlab/auth/ldap/ @dblessing @mkozono
/lib/gitlab/ci/templates/ @nolith @zj
/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @DylanGriffith @mayra-cabrera @tkuah
......@@ -576,6 +576,15 @@ entry.
- Moves help_popover component to a common location.
## 11.1.8 (2018-10-05)
### Security (3 changes)
- Filter user sensitive data from discussions JSON. !2539
- Properly filter private references from system notes.
- Markdown API no longer displays confidential title references unless authorized.
## 11.1.7 (2018-09-26)
### Security (6 changes)
......
......@@ -15,7 +15,10 @@ const Api = {
mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
groupLabelsPath: '/groups/:namespace_path/-/labels',
<<<<<<< HEAD
ldapGroupsPath: '/api/:version/ldap/:provider/groups.json',
=======
>>>>>>> upstream/master
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
projectTemplatePath: '/api/:version/projects/:id/templates/:type/:key',
projectTemplatesPath: '/api/:version/projects/:id/templates/:type',
......@@ -275,6 +278,7 @@ const Api = {
});
},
<<<<<<< HEAD
approverUsers(search, options, callback = $.noop) {
const url = Api.buildUrl('/autocomplete/users.json');
return axios
......@@ -311,6 +315,8 @@ const Api = {
});
},
=======
>>>>>>> upstream/master
buildUrl(url) {
let urlRoot = '';
if (gon.relative_url_root != null) {
......
import Visibility from 'visibilityjs';
import Vue from 'vue';
import initDismissableCallout from '~/dismissable_callout';
import PersistentUserCallout from '../persistent_user_callout';
import { s__, sprintf } from '../locale';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
......@@ -62,7 +62,7 @@ export default class Clusters {
this.showTokenButton = document.querySelector('.js-show-cluster-token');
this.tokenField = document.querySelector('.js-cluster-token');
initDismissableCallout('.js-cluster-security-warning');
Clusters.initDismissableCallout();
initSettingsPanels();
setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
this.initApplications();
......@@ -105,6 +105,12 @@ export default class Clusters {
});
}
static initDismissableCallout() {
const callout = document.querySelector('.js-cluster-security-warning');
if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
}
addListeners() {
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication);
......
import createFlash from '~/flash';
import { __ } from '~/locale';
import setupToggleButtons from '~/toggle_buttons';
import initDismissableCallout from '~/dismissable_callout';
import PersistentUserCallout from '../persistent_user_callout';
import ClustersService from './services/clusters_service';
export default () => {
const clusterList = document.querySelector('.js-clusters-list');
initDismissableCallout('.gcp-signup-offer');
const callout = document.querySelector('.gcp-signup-offer');
if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
// The empty state won't have a clusterList
if (clusterList) {
......
......@@ -28,7 +28,7 @@ export default {
return diffModes[diffModeKey] || diffModes.replaced;
},
isTextFile() {
return this.diffFile.text;
return this.diffFile.viewer.name === 'text';
},
},
};
......
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import Flash from '~/flash';
export default function initDismissableCallout(alertSelector) {
const alertEl = document.querySelector(alertSelector);
if (!alertEl) {
return;
}
const closeButtonEl = alertEl.getElementsByClassName('close')[0];
const { dismissEndpoint, featureId } = closeButtonEl.dataset;
closeButtonEl.addEventListener('click', () => {
axios
.post(dismissEndpoint, {
feature_name: featureId,
})
.then(() => {
$(alertEl).alert('close');
})
.catch(() => {
Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.'));
});
});
}
import initDismissableCallout from '~/dismissable_callout';
import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
import PersistentUserCallout from '../../persistent_user_callout';
import Project from './project';
import ShortcutsNavigation from '../../behaviors/shortcuts/shortcuts_navigation';
......@@ -12,7 +12,9 @@ document.addEventListener('DOMContentLoaded', () => {
];
if (newClusterViews.indexOf(page) > -1) {
initDismissableCallout('.gcp-signup-offer');
const callout = document.querySelector('.gcp-signup-offer');
if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
initGkeDropdowns();
}
......
......@@ -57,7 +57,21 @@
required: false,
default: '',
},
<<<<<<< HEAD
packagesHelpPath: {
=======
pagesAvailable: {
type: Boolean,
required: false,
default: false,
},
pagesAccessControlEnabled: {
type: Boolean,
required: false,
default: false,
},
pagesHelpPath: {
>>>>>>> upstream/master
type: String,
required: false,
default: '',
......@@ -74,6 +88,7 @@
buildsAccessLevel: 20,
wikiAccessLevel: 20,
snippetsAccessLevel: 20,
pagesAccessLevel: 20,
containerRegistryEnabled: true,
lfsEnabled: true,
packagesEnabled: true,
......@@ -101,6 +116,13 @@
);
},
pagesFeatureAccessLevelOptions() {
if (this.visibilityLevel !== visibilityOptions.PUBLIC) {
return this.featureAccessLevelOptions.concat([[30, 'Everyone']]);
}
return this.featureAccessLevelOptions;
},
repositoryEnabled() {
return this.repositoryAccessLevel > 0;
},
......@@ -120,6 +142,10 @@
this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel);
this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel);
this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel);
if (this.pagesAccessLevel === 20) {
// When from Internal->Private narrow access for only members
this.pagesAccessLevel = 10;
}
this.highlightChanges();
} else if (oldValue === visibilityOptions.PRIVATE) {
// if changing away from private, make enabled features more permissive
......@@ -129,6 +155,7 @@
if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20;
if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20;
if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20;
if (this.pagesAccessLevel === 10) this.pagesAccessLevel = 20;
this.highlightChanges();
}
},
......@@ -348,6 +375,18 @@
name="project[project_feature_attributes][snippets_access_level]"
/>
</project-setting-row>
<project-setting-row
v-if="pagesAvailable && pagesAccessControlEnabled"
:help-path="pagesHelpPath"
label="Pages"
help-text="Static website for the project."
>
<project-feature-setting
v-model="pagesAccessLevel"
:options="pagesFeatureAccessLevelOptions"
name="project[project_feature_attributes][pages_access_level]"
/>
</project-setting-row>
</div>
</div>
</template>
// if the "projects dashboard" is a user's default dashboard, when they visit the
// instance root index, the dashboard will be served by the root controller instead
// of a dashboard controller. The root index redirects for all other default dashboards.
import '../dashboard/projects/index';
import axios from './lib/utils/axios_utils';
import { __ } from './locale';
import Flash from './flash';
export default class PersistentUserCallout {
constructor(container) {
const { dismissEndpoint, featureId } = container.dataset;
this.container = container;
this.dismissEndpoint = dismissEndpoint;
this.featureId = featureId;
this.init();
}
init() {
const closeButton = this.container.querySelector('.js-close');
closeButton.addEventListener('click', event => this.dismiss(event));
}
dismiss(event) {
event.preventDefault();
axios
.post(this.dismissEndpoint, {
feature_name: this.featureId,
})
.then(() => {
this.container.remove();
})
.catch(() => {
Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.'));
});
}
}
......@@ -1000,6 +1000,7 @@
}
.tree-list-holder {
position: -webkit-sticky;
position: sticky;
top: 100px;
max-height: calc(100vh - 100px);
......
......@@ -221,7 +221,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
metrics_url =
if can?(current_user, :read_environment, environment) && environment.has_metrics?
metrics_project_environment_deployment_path(environment.project, environment, deployment)
metrics_project_environment_deployment_path(project, environment, deployment)
end
metrics_monitoring_url =
......
......@@ -16,10 +16,10 @@ module Projects
@new_deploy_token = DeployTokens::CreateService.new(@project, current_user, deploy_token_params).execute
if @new_deploy_token.persisted?
flash[:notice] = s_('DeployTokens|Your new project deploy token has been created.')
flash.now[:notice] = s_('DeployTokens|Your new project deploy token has been created.')
end
redirect_to action: :show
render_show
end
private
......
......@@ -368,6 +368,7 @@ class ProjectsController < Projects::ApplicationController
repository_access_level
snippets_access_level
wiki_access_level
pages_access_level
]
]
end
......
......@@ -363,6 +363,7 @@ class IssuableFinder
def use_cte_for_search?
return false unless search
return false unless Gitlab::Database.postgresql?
return false unless Feature.enabled?(:use_cte_for_group_issues_search, default_enabled: true)
params[:use_cte_for_search]
end
......
......@@ -122,9 +122,13 @@ class IssuesFinder < IssuableFinder
return @user_can_see_all_confidential_issues = true if current_user.full_private_access?
@user_can_see_all_confidential_issues =
project? &&
project &&
if project? && project
project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL
elsif group
group.max_member_access_for_user(current_user) >= CONFIDENTIAL_ACCESS_LEVEL
else
false
end
end
def user_cannot_see_confidential_issues?
......
......@@ -13,8 +13,11 @@
class LicenseTemplateFinder
include Gitlab::Utils::StrongMemoize
<<<<<<< HEAD
prepend ::EE::LicenseTemplateFinder
=======
>>>>>>> upstream/master
attr_reader :project, :params
def initialize(project, params = {})
......
......@@ -3,8 +3,11 @@
class TemplateFinder
include Gitlab::Utils::StrongMemoize
<<<<<<< HEAD
prepend ::EE::TemplateFinder
=======
>>>>>>> upstream/master
VENDORED_TEMPLATES = HashWithIndifferentAccess.new(
dockerfiles: ::Gitlab::Template::DockerfileTemplate,
gitignores: ::Gitlab::Template::GitignoreTemplate,
......
......@@ -16,7 +16,7 @@ module Types
:create_deployment, :push_to_delete_protected_branch,
:admin_wiki, :admin_project, :update_pages,
:admin_remote_mirror, :create_label, :update_wiki, :destroy_wiki,
:create_pages, :destroy_pages
:create_pages, :destroy_pages, :read_pages_content
end
end
end
......@@ -21,6 +21,29 @@ module DashboardHelper
links.any? { |link| dashboard_nav_link?(link) }
end
def controller_action_to_child_dashboards(controller = controller_name, action = action_name)
case "#{controller}##{action}"
when 'projects#index', 'root#index', 'projects#starred', 'projects#trending'
%w(projects stars)
when 'dashboard#activity'
%w(starred_project_activity project_activity)
when 'groups#index'
%w(groups)
when 'todos#index'
%w(todos)
when 'dashboard#issues'
%w(issues)
when 'dashboard#merge_requests'
%w(merge_requests)
else
[]
end
end
def user_default_dashboard?(user = current_user)
controller_action_to_child_dashboards.any? {|dashboard| dashboard == user.dashboard }
end
private
def get_dashboard_nav_links
......
......@@ -456,6 +456,7 @@ module ProjectsHelper
buildsAccessLevel: feature.builds_access_level,
wikiAccessLevel: feature.wiki_access_level,
snippetsAccessLevel: feature.snippets_access_level,
pagesAccessLevel: feature.pages_access_level,
containerRegistryEnabled: !!project.container_registry_enabled,
lfsEnabled: !!project.lfs_enabled
}
......@@ -470,7 +471,10 @@ module ProjectsHelper
registryAvailable: Gitlab.config.registry.enabled,
registryHelpPath: help_page_path('user/project/container_registry'),
lfsAvailable: Gitlab.config.lfs.enabled,
lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs'),
pagesAvailable: Gitlab.config.pages.enabled,
pagesAccessControlEnabled: Gitlab.config.pages.access_control,
pagesHelpPath: help_page_path('user/project/pages/index.md')
}
end
......
......@@ -792,6 +792,9 @@ module Ci
variables.append(key: 'GITLAB_FEATURES', value: project.licensed_features.join(','))
variables.append(key: 'CI_SERVER_NAME', value: 'GitLab')
variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION)
variables.append(key: 'CI_SERVER_VERSION_MAJOR', value: gitlab_version_info.major.to_s)
variables.append(key: 'CI_SERVER_VERSION_MINOR', value: gitlab_version_info.minor.to_s)
variables.append(key: 'CI_SERVER_VERSION_PATCH', value: gitlab_version_info.patch.to_s)
variables.append(key: 'CI_SERVER_REVISION', value: Gitlab.revision)
variables.append(key: 'CI_JOB_NAME', value: name)
variables.append(key: 'CI_JOB_STAGE', value: stage)
......@@ -806,6 +809,10 @@ module Ci
end
end
def gitlab_version_info
@gitlab_version_info ||= Gitlab::VersionInfo.parse(Gitlab::VERSION)
end
def legacy_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_BUILD_REF', value: sha)
......
......@@ -53,7 +53,11 @@ class CommitStatus < ActiveRecord::Base
missing_dependency_failure: 5,
runner_unsupported: 6,
stale_schedule: 7
<<<<<<< HEAD
}.merge(EE_FAILURE_REASONS)
=======
}
>>>>>>> upstream/master
##
# We still create some CommitStatuses outside of CreatePipelineService.
......
......@@ -177,6 +177,7 @@ class Issue < ActiveRecord::Base
"#{project.to_reference(from, full: full)}#{reference}"
end
<<<<<<< HEAD
def related_issues(current_user, preload: nil)
related_issues = Issue
.select(['issues.*', 'issue_links.id AS issue_link_id'])
......@@ -194,6 +195,8 @@ class Issue < ActiveRecord::Base
)
end
=======
>>>>>>> upstream/master
def suggested_branch_name
return to_branch_name unless project.repository.branch_exists?(to_branch_name)
......
......@@ -41,11 +41,18 @@ class Note < ActiveRecord::Base
alias_attribute :last_edited_at, :updated_at
alias_attribute :last_edited_by, :updated_by
# Attribute containing rendered and redacted Markdown as generated by
# Banzai::ObjectRenderer.
# Number of user visible references as generated by Banzai::ObjectRenderer
attr_accessor :redacted_note_html
<<<<<<< HEAD
# Number of user visible references as generated by Banzai::ObjectRenderer
=======
# Total of all references as generated by Banzai::ObjectRenderer
attr_accessor :total_reference_count
# An Array containing the number of visible references as generated by
# Banzai::ObjectRenderer
>>>>>>> upstream/master
attr_accessor :user_visible_reference_count
# Total of all references as generated by Banzai::ObjectRenderer
......
......@@ -58,8 +58,8 @@ class Project < ActiveRecord::Base
cache_markdown_field :description, pipeline: :description
delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
:merge_requests_enabled?, :issues_enabled?, to: :project_feature,
allow_nil: true
:merge_requests_enabled?, :issues_enabled?, :pages_enabled?, :public_pages?,
to: :project_feature, allow_nil: true
delegate :base_dir, :disk_path, :ensure_storage_path_exists, to: :storage
......@@ -361,7 +361,7 @@ class Project < ActiveRecord::Base
# "enabled" here means "not disabled". It includes private features!
scope :with_feature_enabled, ->(feature) {
access_level_attribute = ProjectFeature.access_level_attribute(feature)
with_project_feature.where(project_features: { access_level_attribute => [nil, ProjectFeature::PRIVATE, ProjectFeature::ENABLED] })
with_project_feature.where(project_features: { access_level_attribute => [nil, ProjectFeature::PRIVATE, ProjectFeature::ENABLED, ProjectFeature::PUBLIC] })
}
# Picks a feature where the level is exactly that given.
......@@ -423,15 +423,15 @@ class Project < ActiveRecord::Base
end
end
# project features may be "disabled", "internal" or "enabled". If "internal",
# project features may be "disabled", "internal", "enabled" or "public". If "internal",
# they are only available to team members. This scope returns projects where
# the feature is either enabled, or internal with permission for the user.
# the feature is either public, enabled, or internal with permission for the user.
#
# This method uses an optimised version of `with_feature_access_level` for
# logged in users to more efficiently get private projects with the given
# feature.
def self.with_feature_available_for_user(feature, user)
visible = [nil, ProjectFeature::ENABLED]
visible = [nil, ProjectFeature::ENABLED, ProjectFeature::PUBLIC]
if user&.admin?
with_feature_enabled(feature)
......@@ -1097,26 +1097,10 @@ class Project < ActiveRecord::Base
end
def find_or_initialize_services(exceptions: [])
services_templates = Service.where(template: true)
available_services_names = Service.available_services_names - exceptions
available_services = available_services_names.map do |service_name|
service = find_service(services, service_name)
if service
service
else
# We should check if template for the service exists
template = find_service(services_templates, service_name)
if template.nil?
# If no template, we should create an instance. Ex `build_gitlab_ci_service`
public_send("build_#{service_name}_service") # rubocop:disable GitlabSecurity/PublicSend
else
Service.build_from_template(id, template)
end
end
find_or_initialize_service(service_name)
end
available_services.reject do |service|
......@@ -1129,7 +1113,18 @@ class Project < ActiveRecord::Base
end
def find_or_initialize_service(name)
find_or_initialize_services.find { |service| service.to_param == name }
service = find_service(services, name)
return service if service
# We should check if template for the service exists
template = find_service(services_templates, name)
if template
Service.build_from_template(id, template)
else
# If no template, we should create an instance. Ex `build_gitlab_ci_service`
public_send("build_#{name}_service") # rubocop:disable GitlabSecurity/PublicSend
end
end
# rubocop: disable CodeReuse/ServiceClass
......@@ -2289,4 +2284,8 @@ class Project < ActiveRecord::Base
check_access.call
end
end
def services_templates
@services_templates ||= Service.where(template: true)
end
end
......@@ -5,7 +5,8 @@ class ProjectAutoDevops < ActiveRecord::Base
enum deploy_strategy: {
continuous: 0,
manual: 1
manual: 1,
timed_incremental: 2
}
scope :enabled, -> { where(enabled: true) }
......@@ -30,10 +31,7 @@ class ProjectAutoDevops < ActiveRecord::Base
value: domain.presence || instance_domain)
end
if manual?
variables.append(key: 'STAGING_ENABLED', value: '1')
variables.append(key: 'INCREMENTAL_ROLLOUT_ENABLED', value: '1')
end
variables.concat(deployment_strategy_default_variables)
end
end
......@@ -51,4 +49,16 @@ class ProjectAutoDevops < ActiveRecord::Base
!project.public? &&
!project.deploy_tokens.find_by(name: DeployToken::GITLAB_DEPLOY_TOKEN_NAME).present?
end
def deployment_strategy_default_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
if manual?
variables.append(key: 'STAGING_ENABLED', value: '1')
variables.append(key: 'INCREMENTAL_ROLLOUT_ENABLED', value: '1') # deprecated
variables.append(key: 'INCREMENTAL_ROLLOUT_MODE', value: 'manual')
elsif timed_incremental?
variables.append(key: 'INCREMENTAL_ROLLOUT_MODE', value: 'timed')
end
end
end
end
......@@ -13,14 +13,16 @@ class ProjectFeature < ActiveRecord::Base
# Disabled: not enabled for anyone
# Private: enabled only for team members
# Enabled: enabled for everyone able to access the project
# Public: enabled for everyone (only allowed for pages)
#
# Permission levels
DISABLED = 0
PRIVATE = 10
ENABLED = 20
PUBLIC = 30
FEATURES = %i(issues merge_requests wiki snippets builds repository).freeze
FEATURES = %i(issues merge_requests wiki snippets builds repository pages).freeze
class << self
def access_level_attribute(feature)
......@@ -46,6 +48,7 @@ class ProjectFeature < ActiveRecord::Base
validates :project, presence: true
validate :repository_children_level
validate :allowed_access_levels
default_value_for :builds_access_level, value: ENABLED, allows_nil: false
default_value_for :issues_access_level, value: ENABLED, allows_nil: false
......@@ -87,6 +90,16 @@ class ProjectFeature < ActiveRecord::Base
issues_access_level > DISABLED
end
def pages_enabled?
pages_access_level > DISABLED
end
def public_pages?
return true unless Gitlab.config.pages.access_control
pages_access_level == PUBLIC || pages_access_level == ENABLED && project.public?
end
private
# Validates builds and merge requests access level
......@@ -101,6 +114,17 @@ class ProjectFeature < ActiveRecord::Base
%i(merge_requests_access_level builds_access_level).each(&validator)
end
# Validates access level for other than pages cannot be PUBLIC
def allowed_access_levels
validator = lambda do |field|
level = public_send(field) || ProjectFeature::ENABLED # rubocop:disable GitlabSecurity/PublicSend
not_allowed = level > ProjectFeature::ENABLED
self.errors.add(field, "cannot have public visibility level") if not_allowed
end
(FEATURES - %i(pages)).each {|f| validator.call("#{f}_access_level")}
end
def get_permission(user, level)
case level
when DISABLED
......@@ -109,6 +133,8 @@ class ProjectFeature < ActiveRecord::Base
user && (project.team.member?(user) || user.full_private_access?)
when ENABLED
true
when PUBLIC
true
else
true
end
......
......@@ -149,7 +149,7 @@ class HipchatService < Service
context.merge!(options)
html = Banzai.post_process(Banzai.render(text, context), context)
html = Banzai.render_and_post_process(text, context)
sanitized_html = sanitize(html, tags: HIPCHAT_ALLOWED_TAGS, attributes: %w[href title alt])
sanitized_html.truncate(200, separator: ' ', omission: '...')
......
......@@ -11,7 +11,10 @@ class SystemNoteMetadata < ActiveRecord::Base
TYPES_WITH_CROSS_REFERENCES = %w[
commit cross_reference
close duplicate
<<<<<<< HEAD
relate unrelate
=======
>>>>>>> upstream/master
moved
].freeze
......
......@@ -6,7 +6,8 @@ class UserCallout < ActiveRecord::Base
enum feature_name: {
gke_cluster_integration: 1,
gcp_signup_offer: 2,
cluster_security_warning: 3
cluster_security_warning: 3,
gold_trial: 4
}
validates :user, presence: true
......
......@@ -111,6 +111,7 @@ class ProjectPolicy < BasePolicy
snippets
wiki
builds
pages
]
features.each do |f|
......@@ -168,6 +169,7 @@ class ProjectPolicy < BasePolicy
enable :upload_file
enable :read_cycle_analytics
enable :award_emoji
enable :read_pages_content
end
# These abilities are not allowed to admins that are not members of the project,
......@@ -287,6 +289,8 @@ class ProjectPolicy < BasePolicy
prevent(*create_read_update_admin_destroy(:merge_request))
end
rule { pages_disabled }.prevent :read_pages_content
rule { issues_disabled & merge_requests_disabled }.policy do
prevent(*create_read_update_admin_destroy(:label))
prevent(*create_read_update_admin_destroy(:milestone))
......@@ -346,6 +350,7 @@ class ProjectPolicy < BasePolicy
enable :download_code
enable :download_wiki_code
enable :read_cycle_analytics
enable :read_pages_content
# NOTE: may be overridden by IssuePolicy
enable :read_issue
......
......@@ -116,6 +116,10 @@ class DiffFileEntity < Grape::Entity
project_blob_path(project, tree_join(diff_file.content_sha, diff_file.new_path))
end
expose :viewer, using: DiffViewerEntity do |diff_file|
diff_file.rich_viewer || diff_file.simple_viewer
end
expose :replaced_view_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
image_diff = diff_file.rich_viewer && diff_file.rich_viewer.partial_name == 'image'
image_replaced = diff_file.old_content_sha && diff_file.old_content_sha != diff_file.content_sha
......
# frozen_string_literal: true
class DiffViewerEntity < Grape::Entity
# Partial name refers directly to a Rails feature, let's avoid
# using this on the frontend.
expose :partial_name, as: :name
end
......@@ -27,7 +27,7 @@ class DiscussionEntity < Grape::Entity
expose :resolved?, as: :resolved
expose :resolved_by_push?, as: :resolved_by_push
expose :resolved_by
expose :resolved_by, using: NoteUserEntity
expose :resolved_at
expose :resolve_path, if: -> (d, _) { d.resolvable? } do |discussion|
resolve_project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion.id)
......
......@@ -2,8 +2,11 @@
module Ci
class ProcessBuildService < BaseService
<<<<<<< HEAD
prepend EE::Ci::ProcessBuildService
=======
>>>>>>> upstream/master
def execute(build, current_status)
if valid_statuses_for_when(build.when).include?(current_status)
if build.schedulable?
......
......@@ -21,7 +21,9 @@ module Projects
def pages_config
{
domains: pages_domains_config,
https_only: project.pages_https_only?
https_only: project.pages_https_only?,
id: project.project_id,
access_control: !project.public_pages?
}
end
......@@ -31,7 +33,9 @@ module Projects
domain: domain.domain,
certificate: domain.certificate,
key: domain.key,
https_only: project.pages_https_only? && domain.https?
https_only: project.pages_https_only? && domain.https?,
id: project.project_id,
access_control: !project.public_pages?
}
end
end
......
......@@ -74,7 +74,11 @@ module Projects
system_hook_service.execute_hooks_for(project, :update)
end
update_pages_config if changing_pages_https_only?
update_pages_config if changing_pages_related_config?
end
def changing_pages_related_config?
changing_pages_https_only? || changing_pages_access_level?
end
def update_failed!
......@@ -104,6 +108,10 @@ module Projects
params.dig(:project_feature_attributes, :wiki_access_level).to_i > ProjectFeature::DISABLED
end
def changing_pages_access_level?
params.dig(:project_feature_attributes, :pages_access_level)
end
def ensure_wiki_exists
ProjectWiki.new(project, project.owner).wiki
rescue ProjectWiki::CouldNotCreateWikiError
......
......@@ -114,6 +114,8 @@
= visibility_level_icon(@project.visibility_level)
= visibility_level_label(@project.visibility_level)
= render_if_exists 'admin/projects/geo_status_widget', locals: { project: @project }
.card
.card-header
Transfer project
......
......@@ -4,6 +4,9 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
= content_for :above_breadcrumbs_content do
= render_if_exists "shared/gold_trial_callout"
- page_title "Activity"
- header_title "Activity", activity_dashboard_path
......
......@@ -3,6 +3,9 @@
- header_title "Groups", dashboard_groups_path
= render 'dashboard/groups_head'
= content_for :above_breadcrumbs_content do
= render_if_exists "shared/gold_trial_callout"
- if params[:filter].blank? && @groups.empty?
= render 'shared/groups/empty_state'
- else
......
......@@ -4,6 +4,9 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{current_user.name} issues")
= content_for :above_breadcrumbs_content do
= render_if_exists "shared/gold_trial_callout"
.top-area
= render 'shared/issuable/nav', type: :issues, display_count: !@no_filters_set
.nav-controls
......
......@@ -2,6 +2,9 @@
- page_title _("Merge Requests")
- @breadcrumb_link = merge_requests_dashboard_path(assignee_id: current_user.id)
= content_for :above_breadcrumbs_content do
= render_if_exists "shared/gold_trial_callout"
.top-area
= render 'shared/issuable/nav', type: :merge_requests, display_count: !@no_filters_set
.nav-controls
......
......@@ -4,6 +4,9 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
= content_for :above_breadcrumbs_content do
= render_if_exists "shared/gold_trial_callout"
- page_title "Projects"
- header_title "Projects", dashboard_projects_path
......
......@@ -4,6 +4,9 @@
- page_title "Starred Projects"
- header_title "Projects", dashboard_projects_path
= content_for :above_breadcrumbs_content do
= render_if_exists "shared/gold_trial_callout"
%div{ class: container_class }
= render "projects/last_push"
= render 'dashboard/projects_head'
......
......@@ -2,6 +2,9 @@
- page_title "Todos"
- header_title "Todos", dashboard_todos_path
= content_for :above_breadcrumbs_content do
= render_if_exists "shared/gold_trial_callout"
- if current_user.todos.any?
.top-area
%ul.nav-links.mobile-separator.nav.nav-tabs
......
......@@ -2,6 +2,9 @@
- page_title _("Groups")
- header_title _("Groups"), dashboard_groups_path
= content_for :above_breadcrumbs_content do
= render_if_exists "shared/gold_trial_callout"
- if current_user
= render 'dashboard/groups_head'
- else
......
......@@ -2,6 +2,9 @@
- page_title _("Projects")
- header_title _("Projects"), dashboard_projects_path
= content_for :above_breadcrumbs_content do
= render_if_exists "shared/gold_trial_callout"
- if current_user
= render 'dashboard/projects_head'
- else
......
......@@ -2,6 +2,9 @@
- page_title _("Projects")
- header_title _("Projects"), dashboard_projects_path
= content_for :above_breadcrumbs_content do
= render_if_exists "shared/gold_trial_callout"
- if current_user
= render 'dashboard/projects_head'
- else
......
......@@ -2,6 +2,9 @@
- page_title _("Projects")
- header_title _("Projects"), dashboard_projects_path
= content_for :above_breadcrumbs_content do
= render_if_exists "shared/gold_trial_callout"
- if current_user
= render 'dashboard/projects_head'
- else
......
- container = @no_breadcrumb_container ? 'container-fluid' : container_class
- hide_top_links = @hide_top_links || false
= yield :above_breadcrumbs_content
%nav.breadcrumbs{ role: "navigation", class: [container, @content_class] }
.breadcrumbs-container
- if defined?(@left_sidebar)
......
......@@ -9,7 +9,7 @@
= s_("ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details")
- if show_cluster_security_warning?
.js-cluster-security-warning.alert.alert-block.alert-dismissable.bs-callout.bs-callout-warning
%button.close{ type: "button", data: { feature_id: UserCalloutsHelper::CLUSTER_SECURITY_WARNING, dismiss_endpoint: user_callouts_path } } &times;
.js-cluster-security-warning.alert.alert-block.alert-dismissable.bs-callout.bs-callout-warning{ data: { feature_id: UserCalloutsHelper::CLUSTER_SECURITY_WARNING, dismiss_endpoint: user_callouts_path } }
%button.close.js-close{ type: "button" } &times;
= s_("ClusterIntegration|The default cluster configuration grants access to many functionalities needed to successfully build and deploy a containerised application.")
= link_to s_("More information"), help_page_path('user/project/clusters/index.md', anchor: 'security-implications')
- link = link_to(s_('ClusterIntegration|sign up'), 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
.bs-callout.gcp-signup-offer.alert.alert-block.alert-dismissable.prepend-top-default.append-bottom-default{ role: 'alert' }
%button.close{ type: "button", data: { feature_id: UserCalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: user_callouts_path } } &times;
.bs-callout.gcp-signup-offer.alert.alert-block.alert-dismissable.prepend-top-default.append-bottom-default{ role: 'alert', data: { feature_id: UserCalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: user_callouts_path } }
%button.close.js-close{ type: "button" } &times;
.gcp-signup-offer--content
.gcp-signup-offer--icon.append-right-8
= sprite_icon("information", size: 16)
......
- page_title _("Metrics")
.row
.row.empty-state
.col-sm-12
.svg-content
= image_tag 'illustrations/operations_metrics_empty.svg'
.row.empty-environments
.col-sm-12.text-center
%h4
.col-12
.text-content
%h4.text-center
= s_('Metrics|No deployed environments')
.state-description
%p.state-description
= s_('Metrics|Check out the CI/CD documentation on deploying to an environment')
.prepend-top-10
.text-center
= link_to s_("Metrics|Learn about environments"), help_page_path('ci/environments'), class: 'btn btn-success'
......@@ -45,10 +45,17 @@
= form.label :deploy_strategy_continuous, class: 'form-check-label' do
= s_('CICD|Continuous deployment to production')
= link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-deploy'), target: '_blank'
.form-check
= form.radio_button :deploy_strategy, 'timed_incremental', class: 'form-check-input'
= form.label :deploy_strategy_timed_incremental, class: 'form-check-label' do
= s_('CICD|Continuous deployment to production using timed incremental rollout')
= link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'timed-incremental-rollout-to-production'), target: '_blank'
.form-check
= form.radio_button :deploy_strategy, 'manual', class: 'form-check-input'
= form.label :deploy_strategy_manual, class: 'form-check-label' do
= s_('CICD|Automatic deployment to staging, manual deployment to production')
= link_to icon('question-circle'), help_page_path('ci/environments.md', anchor: 'manually-deploying-to-environments'), target: '_blank'
= link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'incremental-rollout-to-production'), target: '_blank'
= f.submit _('Save changes'), class: "btn btn-success prepend-top-15"
---
title: Add GitLab version components to CI environment variables
merge_request: 21853
author:
type: added
---
title: Fix sorting by priority or popularity on group issues page, when also searching
issue content
merge_request: 21521
author:
type: fixed
---
title: Fix timeout when running the RemoveRestrictedTodos background migration
merge_request: 21893
author:
type: fixed
---
title: Add installation type to backup information file
merge_request: 22150
author:
type: changed
---
title: Add access control to GitLab pages and make it possible to enable/disable it in project settings
merge_request: 18589
author: Tuomo Ala-Vannesluoma
type: added
---
title: Add timed incremental rollout to Auto DevOps
merge_request: 22023
author:
type: added
---
title: Mitigate N+1 queries when parsing commit references in comments.
merge_request:
author:
type: performance
---
title: Fix LFS uploaded images not being rendered
merge_request: 22092
author:
type: fixed
---
title: 'Rails5: fix artifacts controller download spec Rails5 has params[:file_type]
as '''' if file_type is included as nil in the request'
merge_request: 22123
author: Jasper Maes
type: other
---
title: Markdown API no longer displays confidential title references unless authorized
merge_request:
author:
type: security
---
title: Properly filter private references from system notes
merge_request:
author:
type: security
---
title: Filter user sensitive data from discussions JSON
merge_request: 2536
author:
type: security
---
title: Update operations metrics empty state
merge_request: 21974
author: George Tsiolis
type: other
......@@ -231,6 +231,7 @@ production: &base
## GitLab Pages
pages:
enabled: false
access_control: false
# The location where pages are stored (default: shared/pages).
# path: shared/pages
......
......@@ -217,6 +217,7 @@ Settings.registry['path'] = Settings.absolute(Settings.registry['path
#
Settings['pages'] ||= Settingslogic.new({})
Settings.pages['enabled'] = false if Settings.pages['enabled'].nil?
Settings.pages['access_control'] = false if Settings.pages['access_control'].nil?
Settings.pages['path'] = Settings.absolute(Settings.pages['path'] || File.join(Settings.shared['path'], "pages"))
Settings.pages['https'] = false if Settings.pages['https'].nil?
Settings.pages['host'] ||= "example.com"
......
class AddPagesAccessLevelToProjectFeature < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
DOWNTIME = false
def up
add_column_with_default(:project_features, :pages_access_level, :integer, default: ProjectFeature::PUBLIC, allow_null: false)
change_column_default(:project_features, :pages_access_level, ProjectFeature::ENABLED)
end
def down
remove_column :project_features, :pages_access_level
end
end
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# rescheduling of the revised RemoveRestrictedTodos background migration
class RemoveRestrictedTodosWithCte < ActiveRecord::Migration
DOWNTIME = false
disable_ddl_transaction!
MIGRATION = 'RemoveRestrictedTodos'.freeze
BATCH_SIZE = 1000
DELAY_INTERVAL = 5.minutes.to_i
class Project < ActiveRecord::Base
include EachBatch
self.table_name = 'projects'
end
def up
Project.where('EXISTS (SELECT 1 FROM todos WHERE todos.project_id = projects.id)')
.each_batch(of: BATCH_SIZE) do |batch, index|
range = batch.pluck('MIN(id)', 'MAX(id)').first
BackgroundMigrationWorker.perform_in(index * DELAY_INTERVAL, MIGRATION, range)
end
end
def down
# nothing to do
end
end
......@@ -11,7 +11,11 @@
#
# It's strongly recommended that you check this file into your version control system.
<<<<<<< HEAD
ActiveRecord::Schema.define(version: 20181001172651) do
=======
ActiveRecord::Schema.define(version: 20181002172433) do
>>>>>>> upstream/master
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -2141,6 +2145,7 @@ ActiveRecord::Schema.define(version: 20181001172651) do
t.datetime "created_at"
t.datetime "updated_at"
t.integer "repository_access_level", default: 20, null: false
t.integer "pages_access_level", default: 20, null: false
end
add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", unique: true, using: :btree
......
......@@ -26,7 +26,10 @@ following locations:
- [Epic Issues](epic_issues.md) **[ULTIMATE]**
- [Events](events.md)
- [Feature flags](features.md)
<<<<<<< HEAD
- [Geo Nodes](geo_nodes.md) **[PREMIUM]**
=======
>>>>>>> upstream/master
- [Gitignore templates](templates/gitignores.md)
- [GitLab CI Config templates](templates/gitlab_ci_ymls.md)
- [Groups](groups.md)
......
......@@ -96,6 +96,9 @@ future GitLab releases.**
| **CI_SERVER_NAME** | all | all | The name of CI server that is used to coordinate jobs |
| **CI_SERVER_REVISION** | all | all | GitLab revision that is used to schedule jobs |
| **CI_SERVER_VERSION** | all | all | GitLab version that is used to schedule jobs |
| **CI_SERVER_VERSION_MAJOR** | 11.4 | all | GitLab version major component |
| **CI_SERVER_VERSION_MINOR** | 11.4 | all | GitLab version minor component |
| **CI_SERVER_VERSION_PATCH** | 11.4 | all | GitLab version patch component |
| **CI_SHARED_ENVIRONMENT** | all | 10.1 | Marks that the job is executed in a shared environment (something that is persisted across CI invocations like `shell` or `ssh` executor). If the environment is shared, it is set to true, otherwise it is not defined at all. |
| **GET_SOURCES_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to fetch sources running a job |
| **GITLAB_CI** | all | all | Mark that job is executed in GitLab CI environment |
......@@ -344,6 +347,12 @@ Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-mach
++ CI_SERVER_NAME='GitLab CI'
++ export CI_SERVER_VERSION=
++ CI_SERVER_VERSION=
++ export CI_SERVER_VERSION_MAJOR=
++ CI_SERVER_VERSION_MAJOR=
++ export CI_SERVER_VERSION_MINOR=
++ CI_SERVER_VERSION_MINOR=
++ export CI_SERVER_VERSION_PATCH=
++ CI_SERVER_VERSION_PATCH=
++ export CI_SERVER_REVISION=
++ CI_SERVER_REVISION=
++ export GITLAB_CI=true
......@@ -489,6 +498,9 @@ export CI_SERVER="yes"
export CI_SERVER_NAME="GitLab"
export CI_SERVER_REVISION="70606bf"
export CI_SERVER_VERSION="8.9.0"
export CI_SERVER_VERSION_MAJOR="8"
export CI_SERVER_VERSION_MINOR="9"
export CI_SERVER_VERSION_PATCH="0"
export GITLAB_USER_ID="42"
export GITLAB_USER_EMAIL="user@example.com"
export CI_REGISTRY_USER="gitlab-ci-token"
......
......@@ -17,8 +17,8 @@ There are two places defined variables can be used. On the:
| Definition | Can be expanded? | Expansion place | Description |
|--------------------------------------|-------------------|-----------------|--------------|
| `environment:url` | yes | GitLab | The variable expansion is made by GitLab's [internal variable expansion mechanism](#gitlab-internal-variable-expansion-mechanism).<ul><li>**Supported:** all variables defined for a job (project/group variables, variables from `.gitlab-ci.yml`, variables from triggers, variables from pipeline schedules)</li><li>**Not suported:** variables defined in Runner's `config.toml` and variables created in job's `script`</li></ul> |
| `environment:name` | yes | GitLab | Similar to `environment:url`, but the variables expansion **doesn't support**: <ul><li>variables that are based on the environment's name (`CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`)</li><li>any other variables related to environment (currently only `CI_ENVIRONMENT_URL`)</li><li>[persisted variables](#persisted-variables)</li></ul> |
| `environment:url` | yes | GitLab | The variable expansion is made by GitLab's [internal variable expansion mechanism](#gitlab-internal-variable-expansion-mechanism).<ul><li>Supported: all variables defined for a job (project/group variables, variables from `.gitlab-ci.yml`, variables from triggers, variables from pipeline schedules)</li><li>Not suported: variables defined in Runner's `config.toml` and variables created in job's `script`</li></ul> |
| `environment:name` | yes | GitLab | Similar to `environment:url`, but the variables expansion doesn't support: <ul><li>variables that are based on the environment's name (`CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`)</li><li>any other variables related to environment (currently only `CI_ENVIRONMENT_URL`)</li><li>[persisted variables](#persisted-variables)</li></ul> |
| `variables` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
| `image` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
| `services:[]` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
......@@ -26,7 +26,7 @@ There are two places defined variables can be used. On the:
| `cache:key` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
| `artifacts:name` | yes | Runner | The variable expansion is made by GitLab Runner's shell environment |
| `script`, `before_script`, `after_script` | yes | Script execution shell | The variable expansion is made by the [execution shell environment](#execution-shell-environment) |
| `only:variables:[]`, `except:variables:[]` | no | n/a | The variable must be in the form of `$variable`.<br/>**Not supported:**<ul><li>variables that are based on the environment's name (`CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`)</li><li>any other variables related to environment (currently only `CI_ENVIRONMENT_URL`)</li><li>[persisted variables](#persisted-variables)</li></ul> |
| `only:variables:[]`, `except:variables:[]` | no | n/a | The variable must be in the form of `$variable`.<br/>Not supported:<ul><li>variables that are based on the environment's name (`CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`)</li><li>any other variables related to environment (currently only `CI_ENVIRONMENT_URL`)</li><li>[persisted variables](#persisted-variables)</li></ul> |
### `config.toml` file
......@@ -55,9 +55,9 @@ since the expansion is done in GitLab before any Runner will get the job.
### GitLab Runner internal variable expansion mechanism
- **Supported:** project/group variables, `.gitlab-ci.yml` variables, `config.toml` variables, and
- Supported: project/group variables, `.gitlab-ci.yml` variables, `config.toml` variables, and
variables from triggers, pipeline schedules, and manual pipelines.
- **Not supported:** variables defined inside of scripts (e.g., `export MY_VARIABLE="test"`).
- Not supported: variables defined inside of scripts (e.g., `export MY_VARIABLE="test"`).
The Runner uses Go's `os.Expand()` method for variable expansion. It means that it will handle
only variables defined as `$variable` and `${variable}`. What's also important, is that
......@@ -73,7 +73,7 @@ by bash/sh (leaving empty strings or some values depending whether the variables
defined or not), but will not work with Windows' cmd/PowerShell, since these shells
are using a different variables syntax.
**Supported:**
Supported:
- The `script` may use all available variables that are default for the shell (e.g., `$PATH` which
should be present in all bash/sh shells) and all variables defined by GitLab CI/CD (project/group variables,
......@@ -106,9 +106,11 @@ The following variables are known as "persisted":
They are:
- **Supported** for all definitions as [described in the table](#gitlab-ci-yml-file) where the "Expansion place" is "Runner".
- **Not supported:**
- By the definitions [described in the table](#gitlab-ci-yml-file) where the "Expansion place" is "GitLab".
- Supported for definitions where the ["Expansion place"](#gitlab-ci-yml-file) is:
- Runner.
- Script execution shell.
- Not supported:
- For definitions where the ["Expansion place"](#gitlab-ci-yml-file) is GitLab.
- In the `only` and `except` [variables expressions](README.md#variables-expressions).
## Variables with an environment scope
......
......@@ -239,14 +239,19 @@ project's **Settings > CI/CD > Auto DevOps**.
The available options are:
- **Continuous deployment to production** - enables [Auto Deploy](#auto-deploy)
by setting the [`STAGING_ENABLED`](#deploy-policy-for-staging-and-production-environments) and
[`INCREMENTAL_ROLLOUT_ENABLED`](#incremental-rollout-to-production) variables
to false.
- **Automatic deployment to staging, manual deployment to production** - sets the
- **Continuous deployment to production**: Enables [Auto Deploy](#auto-deploy)
with `master` branch directly deployed to production.
- **Continuous deployment to production using timed incremental rollout**: Sets the
[`INCREMENTAL_ROLLOUT_MODE`](#timed-incremental-rollout-to-production) variable
to `timed`, and production deployment will be executed with a 5 minute delay between
each increment in rollout.
- **Automatic deployment to staging, manual deployment to production**: Sets the
[`STAGING_ENABLED`](#deploy-policy-for-staging-and-production-environments) and
[`INCREMENTAL_ROLLOUT_ENABLED`](#incremental-rollout-to-production) variables
to true, and the user is responsible for manually deploying to staging and production.
[`INCREMENTAL_ROLLOUT_MODE`](#incremental-rollout-to-production) variables
to `1` and `manual`. This means:
- `master` branch is directly deployed to staging.
- Manual actions are provided for incremental rollout to production.
## Stages of Auto DevOps
......@@ -609,7 +614,7 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac
| `DB_MIGRATE` | From GitLab 11.4, this variable can be used to specify the command to run to migrate the application's PostgreSQL database. It runs inside the application pod. |
| `STAGING_ENABLED` | From GitLab 10.8, this variable can be used to define a [deploy policy for staging and production environments](#deploy-policy-for-staging-and-production-environments). |
| `CANARY_ENABLED` | From GitLab 11.0, this variable can be used to define a [deploy policy for canary environments](#deploy-policy-for-canary-environments). |
| `INCREMENTAL_ROLLOUT_ENABLED`| From GitLab 10.8, this variable can be used to enable an [incremental rollout](#incremental-rollout-to-production) of your application for the production environment. |
| `INCREMENTAL_ROLLOUT_MODE`| From GitLab 11.4, this variable, if present, can be used to enable an [incremental rollout](#incremental-rollout-to-production) of your application for the production environment.<br/>Set to: <ul><li>`manual`, for manual deployment jobs.</li><li>`timed`, for automatic rollout deployments with a 5 minute delay each one.</li></ul> |
| `TEST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `test` job. If the variable is present, the job will not be created. |
| `CODE_QUALITY_DISABLED` | From GitLab 11.0, this variable can be used to disable the `codequality` job. If the variable is present, the job will not be created. |
| `SAST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `sast` job. If the variable is present, the job will not be created. |
......@@ -730,9 +735,8 @@ to use an incremental rollout to replace just a few pods with the latest code.
This will allow you to first check how the app is behaving, and later manually
increasing the rollout up to 100%.
If `INCREMENTAL_ROLLOUT_ENABLED` is defined in your project (e.g., set
`INCREMENTAL_ROLLOUT_ENABLED` to `1` as a secret variable), then instead of the
standard `production` job, 4 different
If `INCREMENTAL_ROLLOUT_MODE` is set to `manual` in your project, then instead
of the standard `production` job, 4 different
[manual jobs](../../ci/pipelines.md#manual-actions-from-the-pipeline-graph)
will be created:
......@@ -756,21 +760,45 @@ environment page.
Below, you can see how the pipeline will look if the rollout or staging
variables are defined.
- **Without `INCREMENTAL_ROLLOUT_ENABLED` and without `STAGING_ENABLED`**
Without `INCREMENTAL_ROLLOUT_MODE` and without `STAGING_ENABLED`:
![Staging and rollout disabled](img/rollout_staging_disabled.png)
Without `INCREMENTAL_ROLLOUT_MODE` and with `STAGING_ENABLED`:
![Staging and rollout disabled](img/rollout_staging_disabled.png)
![Staging enabled](img/staging_enabled.png)
- **Without `INCREMENTAL_ROLLOUT_ENABLED` and with `STAGING_ENABLED`**
With `INCREMENTAL_ROLLOUT_MODE` set to `manual` and without `STAGING_ENABLED`:
![Staging enabled](img/staging_enabled.png)
![Rollout enabled](img/rollout_enabled.png)
- **With `INCREMENTAL_ROLLOUT_ENABLED` and without `STAGING_ENABLED`**
With `INCREMENTAL_ROLLOUT_MODE` set to `manual` and with `STAGING_ENABLED`
![Rollout and staging enabled](img/rollout_staging_enabled.png)
CAUTION: **Caution:**
Before GitLab 11.4 this feature was enabled by the presence of the
`INCREMENTAL_ROLLOUT_ENABLED` environment variable.
This configuration is deprecated and will be removed in the future.
#### Timed incremental rollout to production **[PREMIUM]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7545) in GitLab 11.4.
TIP: **Tip:**
You can also set this inside your [project's settings](#deployment-strategy).
![Rollout enabled](img/rollout_enabled.png)
This configuration based on
[incremental rollout to production](#incremental-rollout-to-production).
- **With `INCREMENTAL_ROLLOUT_ENABLED` and with `STAGING_ENABLED`**
Everything behaves the same way, except:
![Rollout and staging enabled](img/rollout_staging_enabled.png)
- It's enabled by setting the `INCREMENTAL_ROLLOUT_MODE` variable to `timed`.
- Instead of the standard `production` job, the following jobs with a 5 minute delay between each are created:
1. `timed rollout 10%`
1. `timed rollout 25%`
1. `timed rollout 50%`
1. `timed rollout 100%`
## Currently supported languages
......
......@@ -116,6 +116,7 @@ which visibility level you select on project settings.
- Disabled: disabled for everyone
- Only team members: only team members will see even if your project is public or internal
- Everyone with access: everyone can see depending on your project visibility level
- Everyone: enabled for everyone (only available for GitLab Pages)
### Protected branches
......@@ -248,6 +249,7 @@ which visibility level you select on project settings.
- Disabled: disabled for everyone
- Only team members: only team members will see even if your project is public or internal
- Everyone with access: everyone can see depending on your project visibility level
- Everyone: enabled for everyone (only available for GitLab Pages)
## GitLab CI/CD permissions
......
......@@ -12,7 +12,8 @@ module API
detail "This feature was introduced in GitLab 11.0."
end
post do
context = { only_path: false }
context = { only_path: false, current_user: current_user }
context[:pipeline] = params[:gfm] ? :full : :plain_markdown
if params[:project]
project = Project.find_by_full_path(params[:project])
......@@ -24,9 +25,7 @@ module API
context[:skip_project_check] = true
end
context[:pipeline] = params[:gfm] ? :full : :plain_markdown
{ html: Banzai.render(params[:text], context) }
{ html: Banzai.render_and_post_process(params[:text], context) }
end
end
end
......
......@@ -289,6 +289,12 @@ module API
present_projects forks
end
desc 'Check pages access of this project'
get ':id/pages_access' do
authorize! :read_pages_content, user_project unless user_project.public_pages?
status 200
end
desc 'Update an existing project' do
success Entities::Project
end
......
......@@ -243,6 +243,7 @@ module Backup
backup_created_at: Time.now,
gitlab_version: Gitlab::VERSION,
tar_version: tar_version,
installation_type: Gitlab::INSTALLATION_TYPE,
skipped: ENV["SKIP"]
}
end
......
module Banzai
# if you need to render markdown, then you probably need to post_process as well,
# such as removing references that the current user doesn't have
# permission to make
def self.render_and_post_process(text, context = {})
post_process(render(text, context), context)
end
def self.render(text, context = {})
Renderer.render(text, context)
end
......
......@@ -215,7 +215,7 @@ module Banzai
#
def projects_for_nodes(nodes)
@projects_for_nodes ||=
grouped_objects_for_nodes(nodes, Project, 'data-project')
grouped_objects_for_nodes(nodes, Project.includes(:project_feature), 'data-project')
end
def can?(user, permission, subject = :global)
......
# frozen_string_literal: true
# rubocop:disable Style/Documentation
# rubocop:disable Metrics/ClassLength
module Gitlab
module BackgroundMigration
......@@ -49,12 +50,15 @@ module Gitlab
private
def remove_non_members_todos(project_id)
Todo.where(project_id: project_id)
.where('user_id NOT IN (?)', authorized_users(project_id))
if Gitlab::Database.postgresql?
batch_remove_todos_cte(project_id)
else
unauthorized_project_todos(project_id)
.each_batch(of: 5000) do |batch|
batch.delete_all
end
end
end
def remove_confidential_issue_todos(project_id)
# min access level to access a confidential issue is reporter
......@@ -86,12 +90,15 @@ module Gitlab
next if target_types.empty?
Todo.where(project_id: project_id)
.where('user_id NOT IN (?)', authorized_users(project_id))
if Gitlab::Database.postgresql?
batch_remove_todos_cte(project_id, target_types)
else
unauthorized_project_todos(project_id)
.where(target_type: target_types)
.delete_all
end
end
end
def private?(feature_level)
feature_level == PRIVATE_FEATURE
......@@ -100,6 +107,65 @@ module Gitlab
def authorized_users(project_id)
ProjectAuthorization.select(:user_id).where(project_id: project_id)
end
def unauthorized_project_todos(project_id)
Todo.where(project_id: project_id)
.where('user_id NOT IN (?)', authorized_users(project_id))
end
def batch_remove_todos_cte(project_id, target_types = nil)
loop do
count = remove_todos_cte(project_id, target_types)
break if count == 0
end
end
def remove_todos_cte(project_id, target_types = nil)
sql = []
sql << with_all_todos_sql(project_id, target_types)
sql << as_deleted_sql
sql << "SELECT count(*) FROM deleted"
result = Todo.connection.exec_query(sql.join(' '))
result.rows[0][0].to_i
end
def with_all_todos_sql(project_id, target_types = nil)
if target_types
table = Arel::Table.new(:todos)
in_target = table[:target_type].in(target_types)
target_types_sql = " AND #{in_target.to_sql}"
end
<<-SQL
WITH all_todos AS (
SELECT id
FROM "todos"
WHERE "todos"."project_id" = #{project_id}
AND (user_id NOT IN (
SELECT "project_authorizations"."user_id"
FROM "project_authorizations"
WHERE "project_authorizations"."project_id" = #{project_id})
#{target_types_sql}
)
),
SQL
end
def as_deleted_sql
<<-SQL
deleted AS (
DELETE FROM todos
WHERE id IN (
SELECT id
FROM all_todos
LIMIT 5000
)
RETURNING id
)
SQL
end
end
end
end
......@@ -25,8 +25,9 @@
# level, or manually added below.
#
# Continuous deployment to production is enabled by default.
# If you want to deploy to staging first, or enable incremental rollouts,
# set STAGING_ENABLED or INCREMENTAL_ROLLOUT_ENABLED environment variables.
# If you want to deploy to staging first, set STAGING_ENABLED environment variable.
# If you want to enable incremental rollout, either manual or time based,
# set INCREMENTAL_ROLLOUT_TYPE environment variable to "manual" or "timed".
# If you want to use canary deployments, set CANARY_ENABLED environment variable.
#
# If Auto DevOps fails to detect the proper buildpack, or if you want to
......@@ -61,6 +62,10 @@ stages:
- staging
- canary
- production
- incremental rollout 10%
- incremental rollout 25%
- incremental rollout 50%
- incremental rollout 100%
- performance
- cleanup
......@@ -282,11 +287,6 @@ stop_review:
variables:
- $REVIEW_DISABLED
# Keys that start with a dot (.) will not be processed by GitLab CI.
# Staging and canary jobs are disabled by default, to enable them
# remove the dot (.) before the job name.
# https://docs.gitlab.com/ee/ci/yaml/README.html#hidden-keys
# Staging deploys are disabled by default since
# continuous deployment to production is enabled by default
# If you prefer to automatically deploy to staging and
......@@ -368,6 +368,7 @@ production:
- $STAGING_ENABLED
- $CANARY_ENABLED
- $INCREMENTAL_ROLLOUT_ENABLED
- $INCREMENTAL_ROLLOUT_MODE
production_manual:
<<: *production_template
......@@ -383,11 +384,11 @@ production_manual:
except:
variables:
- $INCREMENTAL_ROLLOUT_ENABLED
- $INCREMENTAL_ROLLOUT_MODE
# This job implements incremental rollout on for every push to `master`.
.rollout: &rollout_template
stage: production
script:
- check_kube_domain
- install_dependencies
......@@ -405,52 +406,77 @@ production_manual:
artifacts:
paths: [environment_url.txt]
rollout 10%:
.manual_rollout_template: &manual_rollout_template
<<: *rollout_template
variables:
ROLLOUT_PERCENTAGE: 10
stage: production
when: manual
# This selectors are backward compatible mode with $INCREMENTAL_ROLLOUT_ENABLED (before 11.4)
only:
refs:
- master
kubernetes: active
variables:
- $INCREMENTAL_ROLLOUT_MODE == "manual"
- $INCREMENTAL_ROLLOUT_ENABLED
except:
variables:
- $INCREMENTAL_ROLLOUT_MODE == "timed"
rollout 25%:
.timed_rollout_template: &timed_rollout_template
<<: *rollout_template
variables:
ROLLOUT_PERCENTAGE: 25
when: manual
when: delayed
start_in: 5 minutes
only:
refs:
- master
kubernetes: active
variables:
- $INCREMENTAL_ROLLOUT_ENABLED
- $INCREMENTAL_ROLLOUT_MODE == "timed"
rollout 50%:
<<: *rollout_template
timed rollout 10%:
<<: *timed_rollout_template
stage: incremental rollout 10%
variables:
ROLLOUT_PERCENTAGE: 10
timed rollout 25%:
<<: *timed_rollout_template
stage: incremental rollout 25%
variables:
ROLLOUT_PERCENTAGE: 25
timed rollout 50%:
<<: *timed_rollout_template
stage: incremental rollout 50%
variables:
ROLLOUT_PERCENTAGE: 50
when: manual
only:
refs:
- master
kubernetes: active
timed rollout 100%:
<<: *timed_rollout_template
<<: *production_template
stage: incremental rollout 100%
variables:
- $INCREMENTAL_ROLLOUT_ENABLED
ROLLOUT_PERCENTAGE: 100
rollout 10%:
<<: *manual_rollout_template
variables:
ROLLOUT_PERCENTAGE: 10
rollout 25%:
<<: *manual_rollout_template
variables:
ROLLOUT_PERCENTAGE: 25
rollout 50%:
<<: *manual_rollout_template
variables:
ROLLOUT_PERCENTAGE: 50
rollout 100%:
<<: *manual_rollout_template
<<: *production_template
when: manual
allow_failure: false
only:
refs:
- master
kubernetes: active
variables:
- $INCREMENTAL_ROLLOUT_ENABLED
# ---------------------------------------------------------------------------
......
......@@ -1297,6 +1297,9 @@ msgstr ""
msgid "CICD|Continuous deployment to production"
msgstr ""
msgid "CICD|Continuous deployment to production using timed incremental rollout"
msgstr ""
msgid "CICD|Default to Auto DevOps pipeline"
msgstr ""
......
......@@ -42,4 +42,15 @@ describe Admin::ProjectsController do
expect { get :index }.not_to exceed_query_limit(control_count)
end
end
describe 'GET /projects/:id' do
render_views
it 'renders show page' do
get :show, namespace_id: project.namespace.path, id: project.path
expect(response).to have_gitlab_http_status(200)
expect(response.body).to match(project.name)
end
end
end
......@@ -234,8 +234,8 @@ describe GroupsController do
end
describe 'GET #issues' do
let(:issue_1) { create(:issue, project: project) }
let(:issue_2) { create(:issue, project: project) }
let(:issue_1) { create(:issue, project: project, title: 'foo') }
let(:issue_2) { create(:issue, project: project, title: 'bar') }
before do
create_list(:award_emoji, 3, awardable: issue_2)
......@@ -256,6 +256,31 @@ describe GroupsController do
expect(assigns(:issues)).to eq [issue_2, issue_1]
end
end
context 'searching' do
# Remove as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/52271
before do
stub_feature_flags(use_cte_for_group_issues_search: false)
end
it 'works with popularity sort' do
get :issues, id: group.to_param, search: 'foo', sort: 'popularity'
expect(assigns(:issues)).to eq([issue_1])
end
it 'works with priority sort' do
get :issues, id: group.to_param, search: 'foo', sort: 'priority'
expect(assigns(:issues)).to eq([issue_1])
end
it 'works with label priority sort' do
get :issues, id: group.to_param, search: 'foo', sort: 'label_priority'
expect(assigns(:issues)).to eq([issue_1])
end
end
end
describe 'GET #merge_requests' do
......
......@@ -19,15 +19,17 @@ describe Projects::ArtifactsController do
end
describe 'GET download' do
subject { get :download, namespace_id: project.namespace, project_id: project, job_id: job, file_type: file_type }
def download_artifact(extra_params = {})
params = { namespace_id: project.namespace, project_id: project, job_id: job }.merge(extra_params)
context 'when no file type is supplied' do
let(:file_type) { nil }
get :download, params
end
context 'when no file type is supplied' do
it 'sends the artifacts file' do
expect(controller).to receive(:send_file).with(job.artifacts_file.path, hash_including(disposition: 'attachment')).and_call_original
subject
download_artifact
end
end
......@@ -36,7 +38,7 @@ describe Projects::ArtifactsController do
let(:file_type) { 'invalid' }
it 'returns 404' do
subject
download_artifact(file_type: file_type)
expect(response).to have_gitlab_http_status(404)
end
......@@ -52,7 +54,7 @@ describe Projects::ArtifactsController do
it 'sends the codequality report' do
expect(controller).to receive(:send_file).with(job.job_artifacts_codequality.file.path, hash_including(disposition: 'attachment')).and_call_original
subject
download_artifact(file_type: file_type)
end
end
end
......
......@@ -772,16 +772,26 @@ describe Projects::MergeRequestsController do
create(:merge_request, source_project: forked, target_project: project)
end
before do
it 'links to the environment on that project' do
get_ci_environments_status
expect(json_response.first['url']).to match /#{forked.full_path}/
end
# we're trying to reduce the overall number of queries for this method.
# set a hard limit for now. https://gitlab.com/gitlab-org/gitlab-ce/issues/52287
it 'keeps queries in check' do
control_count = ActiveRecord::QueryRecorder.new { get_ci_environments_status }.count
expect(control_count).to be <= 137
end
def get_ci_environments_status
get :ci_environments_status,
namespace_id: merge_request.project.namespace.to_param,
project_id: merge_request.project,
id: merge_request.iid, format: 'json'
end
it 'links to the environment on that project' do
expect(json_response.first['url']).to match /#{forked.full_path}/
end
end
end
......
......@@ -5,8 +5,16 @@ FactoryBot.define do
domain "example.com"
deploy_strategy :continuous
trait :manual do
deploy_strategy :manual
trait :continuous_deployment do
deploy_strategy ProjectAutoDevops.deploy_strategies[:continuous] # rubocop:disable FactoryBot/DynamicAttributeDefinedStatically
end
trait :manual_deployment do
deploy_strategy ProjectAutoDevops.deploy_strategies[:manual] # rubocop:disable FactoryBot/DynamicAttributeDefinedStatically
end
trait :timed_incremental_deployment do
deploy_strategy ProjectAutoDevops.deploy_strategies[:timed_incremental] # rubocop:disable FactoryBot/DynamicAttributeDefinedStatically
end
trait :disabled do
......
require_relative '../support/helpers/test_env'
FactoryBot.define do
PAGES_ACCESS_LEVEL_SCHEMA_VERSION = 20180423204600
# Project without repository
#
# Project does not have bare repository.
......@@ -23,6 +25,7 @@ FactoryBot.define do
issues_access_level ProjectFeature::ENABLED
merge_requests_access_level ProjectFeature::ENABLED
repository_access_level ProjectFeature::ENABLED
pages_access_level ProjectFeature::ENABLED
# we can't assign the delegated `#ci_cd_settings` attributes directly, as the
# `#ci_cd_settings` relation needs to be created first
......@@ -34,13 +37,20 @@ FactoryBot.define do
builds_access_level = [evaluator.builds_access_level, evaluator.repository_access_level].min
merge_requests_access_level = [evaluator.merge_requests_access_level, evaluator.repository_access_level].min
project.project_feature.update(
hash = {
wiki_access_level: evaluator.wiki_access_level,
builds_access_level: builds_access_level,
snippets_access_level: evaluator.snippets_access_level,
issues_access_level: evaluator.issues_access_level,
merge_requests_access_level: merge_requests_access_level,
repository_access_level: evaluator.repository_access_level)
repository_access_level: evaluator.repository_access_level
}
if ActiveRecord::Migrator.current_version >= PAGES_ACCESS_LEVEL_SCHEMA_VERSION
hash.store("pages_access_level", evaluator.pages_access_level)
end
project.project_feature.update(hash)
# Normally the class Projects::CreateService is used for creating
# projects, and this class takes care of making sure the owner and current
......@@ -287,6 +297,10 @@ FactoryBot.define do
trait(:repository_enabled) { repository_access_level ProjectFeature::ENABLED }
trait(:repository_disabled) { repository_access_level ProjectFeature::DISABLED }
trait(:repository_private) { repository_access_level ProjectFeature::PRIVATE }
trait(:pages_public) { pages_access_level ProjectFeature::PUBLIC }
trait(:pages_enabled) { pages_access_level ProjectFeature::ENABLED }
trait(:pages_disabled) { pages_access_level ProjectFeature::DISABLED }
trait(:pages_private) { pages_access_level ProjectFeature::PRIVATE }
trait :auto_devops do
association :auto_devops, factory: :project_auto_devops
......
......@@ -568,12 +568,17 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end
it 'shows delayed job', :js do
<<<<<<< HEAD
time_diff = [0, job.scheduled_at - Time.now].max
expect(page).to have_content(job.detailed_status(user).illustration[:title])
expect(page).to have_content('This is a scheduled to run in')
expect(page).to have_content("This job will automatically run after it's timer finishes.")
expect(page).to have_content(Time.at(time_diff).utc.strftime("%H:%M:%S"))
=======
expect(page).to have_content('This is a scheduled to run in')
expect(page).to have_content("This job will automatically run after it's timer finishes.")
>>>>>>> upstream/master
expect(page).to have_link('Unschedule job')
end
......
{
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": ["string"] }
},
"additionalProperties": false
}
{
"type": "object",
"required": [
"id",
"state",
"avatar_url",
"path",
"name",
"username"
],
"properties": {
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "string" },
"path": { "type": "string" },
"name": { "type": "string" },
"username": { "type": "string" },
"status_tooltip_html": { "$ref": "../types/nullable_string.json" }
},
"additionalProperties": false
}
......@@ -10,7 +10,7 @@ describe Types::PermissionTypes::Project do
:read_commit_status, :request_access, :create_pipeline, :create_pipeline_schedule,
:create_merge_request_from, :create_wiki, :push_code, :create_deployment, :push_to_delete_protected_branch,
:admin_wiki, :admin_project, :update_pages, :admin_remote_mirror, :create_label,
:update_wiki, :destroy_wiki, :create_pages, :destroy_pages
:update_wiki, :destroy_wiki, :create_pages, :destroy_pages, :read_pages_content
]
expect(described_class).to have_graphql_fields(expected_permissions)
......
......@@ -8,13 +8,12 @@ import diffFileMockData from '../mock_data/diff_file';
describe('DiffContent', () => {
const Component = Vue.extend(DiffContentComponent);
let vm;
const getDiffFileMock = () => Object.assign({}, diffFileMockData);
beforeEach(() => {
vm = mountComponentWithStore(Component, {
store,
props: {
diffFile: getDiffFileMock(),
diffFile: JSON.parse(JSON.stringify(diffFileMockData)),
},
});
});
......@@ -43,7 +42,7 @@ describe('DiffContent', () => {
describe('Non-Text diffs', () => {
beforeEach(() => {
vm.diffFile.text = false;
vm.diffFile.viewer.name = 'image';
});
describe('image diff', () => {
......
......@@ -6,11 +6,10 @@ import diffFileMockData from '../mock_data/diff_file';
describe('DiffFile', () => {
let vm;
const getDiffFileMock = () => Object.assign({}, diffFileMockData);
beforeEach(() => {
vm = createComponentWithStore(Vue.extend(DiffFileComponent), store, {
file: getDiffFileMock(),
file: JSON.parse(JSON.stringify(diffFileMockData)),
canCurrentUserFork: false,
}).$mount();
});
......@@ -18,7 +17,7 @@ describe('DiffFile', () => {
describe('template', () => {
it('should render component with file header, file content components', () => {
const el = vm.$el;
const { fileHash, filePath } = diffFileMockData;
const { fileHash, filePath } = vm.file;
expect(el.id).toEqual(fileHash);
expect(el.classList.contains('diff-file')).toEqual(true);
......
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