Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-06-01 21:10:06 +00:00
parent c3afdb42dd
commit f7bc7dc5ea
115 changed files with 1375 additions and 575 deletions

View File

@ -1 +1 @@
7807bc4670c11dad1d2981e6d1090fe0ddd9e088
37934de32d680c4166ec35dcd47b59fa0cc8c397

View File

@ -1 +1 @@
2.11.0
2.12.0

View File

@ -146,7 +146,7 @@ GEM
coderay (>= 1.0.0)
erubi (>= 1.0.0)
rack (>= 0.9.0)
bindata (2.4.8)
bindata (2.4.10)
binding_ninja (0.2.3)
bootsnap (1.4.6)
msgpack (~> 1.0)

View File

@ -195,6 +195,7 @@ export default {
'var',
],
ALLOWED_ATTR: ['class', 'style', 'href', 'src'],
ALLOW_DATA_ATTR: false,
});
},
},

View File

@ -31,6 +31,7 @@ class JiraConnect::AppDescriptorController < JiraConnect::ApplicationController
scopes: %w(READ WRITE DELETE),
apiVersion: 1,
apiMigrations: {
'context-qsh': true,
gdpr: true
}
}

View File

@ -14,8 +14,9 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
if pre_auth.authorizable?
if skip_authorization? || matching_token?
auth = authorization.authorize
parsed_redirect_uri = URI.parse(auth.redirect_uri)
session.delete(:user_return_to)
redirect_to auth.redirect_uri
render "doorkeeper/authorizations/redirect", locals: { redirect_uri: parsed_redirect_uri }, layout: false
else
render "doorkeeper/authorizations/new"
end

View File

@ -7,106 +7,88 @@ module Resolvers
type ::Types::TimelogType.connection_type, null: false
argument :start_date, Types::TimeType,
required: false,
description: 'List time logs within a date range where the logged date is equal to or after startDate.'
required: false,
description: 'List time logs within a date range where the logged date is equal to or after startDate.'
argument :end_date, Types::TimeType,
required: false,
description: 'List time logs within a date range where the logged date is equal to or before endDate.'
required: false,
description: 'List time logs within a date range where the logged date is equal to or before endDate.'
argument :start_time, Types::TimeType,
required: false,
description: 'List time-logs within a time range where the logged time is equal to or after startTime.'
required: false,
description: 'List time-logs within a time range where the logged time is equal to or after startTime.'
argument :end_time, Types::TimeType,
required: false,
description: 'List time-logs within a time range where the logged time is equal to or before endTime.'
required: false,
description: 'List time-logs within a time range where the logged time is equal to or before endTime.'
def resolve_with_lookahead(**args)
return Timelog.none unless timelogs_available_for_user?
build_timelogs
validate_params_presence!(args)
transformed_args = transform_args(args)
validate_time_difference!(transformed_args)
if args.any?
validate_args!(args)
build_parsed_args(args)
validate_time_difference!
apply_time_filter
end
find_timelogs(transformed_args)
apply_lookahead(timelogs)
end
private
attr_reader :parsed_args, :timelogs
def preloads
{
note: [:note]
}
end
def find_timelogs(args)
apply_lookahead(group.timelogs(args[:start_time], args[:end_time]))
end
def timelogs_available_for_user?
group&.user_can_access_group_timelogs?(context[:current_user])
end
def validate_params_presence!(args)
message = case time_params_count(args)
when 0
'Start and End arguments must be present'
when 1
'Both Start and End arguments must be present'
when 2
validate_duplicated_args(args)
when 3 || 4
'Only Time or Date arguments must be present'
end
raise_argument_error(message) if message
end
def validate_time_difference!(args)
message = if args[:end_time] < args[:start_time]
'Start argument must be before End argument'
elsif args[:end_time] - args[:start_time] > 60.days
'The time range period cannot contain more than 60 days'
end
raise_argument_error(message) if message
end
def transform_args(args)
return args if args.keys == [:start_time, :end_time]
time_args = args.except(:start_date, :end_date)
if time_args.empty?
time_args[:start_time] = args[:start_date].beginning_of_day
time_args[:end_time] = args[:end_date].end_of_day
elsif time_args.key?(:start_time)
time_args[:end_time] = args[:end_date].end_of_day
elsif time_args.key?(:end_time)
time_args[:start_time] = args[:start_date].beginning_of_day
def validate_args!(args)
if args[:start_time] && args[:start_date]
raise_argument_error('Provide either a start date or time, but not both')
elsif args[:end_time] && args[:end_date]
raise_argument_error('Provide either an end date or time, but not both')
end
time_args
end
def time_params_count(args)
[:start_time, :end_time, :start_date, :end_date].count { |param| args.key?(param) }
end
def build_parsed_args(args)
if times_provided?(args)
@parsed_args = args
else
@parsed_args = args.except(:start_date, :end_date)
def validate_duplicated_args(args)
if args.key?(:start_time) && args.key?(:start_date) ||
args.key?(:end_time) && args.key?(:end_date)
'Both Start and End arguments must be present'
@parsed_args[:start_time] = args[:start_date].beginning_of_day if args[:start_date]
@parsed_args[:end_time] = args[:end_date].end_of_day if args[:end_date]
end
end
def times_provided?(args)
args[:start_time] && args[:end_time]
end
def validate_time_difference!
return unless end_time_before_start_time?
raise_argument_error('Start argument must be before End argument')
end
def end_time_before_start_time?
times_provided?(parsed_args) && parsed_args[:end_time] < parsed_args[:start_time]
end
def build_timelogs
@timelogs = Timelog.in_group(object)
end
def apply_time_filter
@timelogs = timelogs.at_or_after(parsed_args[:start_time]) if parsed_args[:start_time]
@timelogs = timelogs.at_or_before(parsed_args[:end_time]) if parsed_args[:end_time]
end
def raise_argument_error(message)
raise Gitlab::Graphql::Errors::ArgumentError, message
end
def group
@group ||= object.respond_to?(:sync) ? object.sync : object
end
end
end

View File

@ -4,7 +4,7 @@ module Types
class TimelogType < BaseObject
graphql_name 'Timelog'
authorize :read_group_timelogs
authorize :read_issue
field :spent_at,
Types::TimeType,

View File

@ -118,6 +118,7 @@ module MarkupHelper
def markup(file_name, text, context = {})
context[:project] ||= @project
context[:text_source] ||= :blob
html = context.delete(:rendered) || markup_unsafe(file_name, text, context)
prepare_for_rendering(html, context)
end

View File

@ -1,20 +0,0 @@
# frozen_string_literal: true
module HasTimelogsReport
extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
def timelogs(start_time, end_time)
strong_memoize(:timelogs) { timelogs_for(start_time, end_time) }
end
def user_can_access_group_timelogs?(current_user)
Ability.allowed?(current_user, :read_group_timelogs, self)
end
private
def timelogs_for(start_time, end_time)
Timelog.between_times(start_time, end_time).in_group(self)
end
end

View File

@ -16,7 +16,6 @@ class Group < Namespace
include Gitlab::Utils::StrongMemoize
include GroupAPICompatibility
include EachBatch
include HasTimelogsReport
include BulkMemberAccessLoad
has_many :all_group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent

View File

@ -36,6 +36,10 @@ class GroupMember < Member
Gitlab::Access.sym_options_with_owner
end
def self.pluck_user_ids
pluck(:user_id)
end
def group
source
end

View File

@ -900,6 +900,10 @@ class Project < ApplicationRecord
alias_method :ancestors, :ancestors_upto
def ancestors_upto_ids(...)
ancestors_upto(...).pluck(:id)
end
def emails_disabled?
strong_memoize(:emails_disabled) do
# disabling in the namespace overrides the project setting

View File

@ -18,8 +18,12 @@ class Timelog < ApplicationRecord
joins(:project).where(projects: { namespace: group.self_and_descendants })
end
scope :between_times, -> (start_time, end_time) do
where('spent_at BETWEEN ? AND ?', start_time, end_time)
scope :at_or_after, -> (start_time) do
where('spent_at >= ?', start_time)
end
scope :at_or_before, -> (end_time) do
where('spent_at <= ?', end_time)
end
def issuable

View File

@ -80,6 +80,10 @@ module PolicyActor
def can_read_all_resources?
false
end
def password_expired?
false
end
end
PolicyActor.prepend_mod_with('PolicyActor')

View File

@ -15,6 +15,10 @@ class GlobalPolicy < BasePolicy
@user&.required_terms_not_accepted?
end
condition(:password_expired, scope: :user) do
@user&.password_expired?
end
condition(:project_bot, scope: :user) { @user&.project_bot? }
condition(:migration_bot, scope: :user) { @user&.migration_bot? }
@ -73,6 +77,12 @@ class GlobalPolicy < BasePolicy
prevent :access_git
end
rule { password_expired }.policy do
prevent :access_api
prevent :access_git
prevent :use_slash_commands
end
rule { can_create_group }.policy do
enable :create_group
end

View File

@ -131,7 +131,6 @@ class GroupPolicy < BasePolicy
enable :read_prometheus
enable :read_package
enable :read_package_settings
enable :read_group_timelogs
end
rule { maintainer }.policy do

View File

@ -263,7 +263,6 @@ class ProjectPolicy < BasePolicy
enable :read_confidential_issues
enable :read_package
enable :read_product_analytics
enable :read_group_timelogs
end
# We define `:public_user_access` separately because there are cases in gitlab-ee

View File

@ -1,5 +1,5 @@
# frozen_string_literal: true
class TimelogPolicy < BasePolicy
delegate { @subject.issuable.resource_parent }
delegate { @subject.issuable }
end

View File

@ -40,7 +40,9 @@ class MemberEntity < Grape::Entity
expose :valid_level_roles, as: :valid_roles
expose :user, if: -> (member) { member.user.present? }, using: MemberUserEntity
expose :user, if: -> (member) { member.user.present? } do |member, options|
MemberUserEntity.represent(member.user, source: options[:source])
end
expose :invite, if: -> (member) { member.invite? } do
expose :email do |member|

View File

@ -0,0 +1,8 @@
%h3.page-title= _("Redirecting")
%div
%a{ :href => redirect_uri } Click here to redirect to #{redirect_uri}
= javascript_tag do
:plain
window.location= "#{redirect_uri}";

View File

@ -5,7 +5,10 @@
= render_if_exists "shared/shared_runners_minutes_limit_flash_message"
#pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json, code_quality_walkthrough: params[:code_quality_walkthrough]),
- list_url = project_pipelines_path(@project, format: :json, code_quality_walkthrough: params[:code_quality_walkthrough])
- add_page_startup_api_call list_url
#pipelines-list-vue{ data: { endpoint: list_url,
project_id: @project.id,
params: params.to_json,
"artifacts-endpoint" => downloadable_artifacts_project_pipeline_path(@project, artifacts_endpoint_placeholder, format: :json),

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/321631
milestone: '13.9'
type: development
group: group::pipeline authoring
default_enabled: false
default_enabled: true

View File

@ -0,0 +1,8 @@
---
name: group_level_protected_environments
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61575
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331085
milestone: '14.0'
type: development
group: group::release
default_enabled: false

View File

@ -18,6 +18,7 @@ unless Gitlab::Runtime.sidekiq?
data[:db_duration_s] = Gitlab::Utils.ms_to_round_sec(data.delete(:db)) if data[:db]
data[:view_duration_s] = Gitlab::Utils.ms_to_round_sec(data.delete(:view)) if data[:view]
data[:duration_s] = Gitlab::Utils.ms_to_round_sec(data.delete(:duration)) if data[:duration]
data[:location] = Gitlab::Utils.removes_sensitive_data_from_url(data[:location]) if data[:location]
# Remove empty hashes to prevent type mismatches
# These are set to empty hashes in Lograge's ActionCable subscriber

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
class ScheduleUpdateUsersWhereTwoFactorAuthRequiredFromGroup < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
MIGRATION = 'UpdateUsersWhereTwoFactorAuthRequiredFromGroup'
DELAY_INTERVAL = 2.minutes
BATCH_SIZE = 10_000
INDEX_NAME = 'index_users_require_two_factor_authentication_from_group_false'
disable_ddl_transaction!
class User < ActiveRecord::Base
include EachBatch
self.table_name = 'users'
end
def up
add_concurrent_index :users,
:require_two_factor_authentication_from_group,
where: 'require_two_factor_authentication_from_group = FALSE',
name: INDEX_NAME
relation = User.where(require_two_factor_authentication_from_group: false)
queue_background_migration_jobs_by_range_at_intervals(
relation, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
end
def down
remove_concurrent_index_by_name :users, INDEX_NAME
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class GroupProtectedEnvironmentsAddColumn < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
def up
add_column :protected_environments, :group_id, :bigint
change_column_null :protected_environments, :project_id, true
end
def down
change_column_null :protected_environments, :project_id, false
remove_column :protected_environments, :group_id
end
end

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
class GroupProtectedEnvironmentsAddIndexAndConstraint < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
INDEX_NAME = 'index_protected_environments_on_group_id_and_name'
disable_ddl_transaction!
def up
add_concurrent_index :protected_environments, [:group_id, :name], unique: true,
name: INDEX_NAME, where: 'group_id IS NOT NULL'
add_concurrent_foreign_key :protected_environments, :namespaces, column: :group_id, on_delete: :cascade
add_check_constraint :protected_environments,
"((project_id IS NULL) != (group_id IS NULL))",
:protected_environments_project_or_group_existence
end
def down
remove_group_protected_environments!
remove_check_constraint :protected_environments, :protected_environments_project_or_group_existence
remove_foreign_key_if_exists :protected_environments, column: :group_id
remove_concurrent_index_by_name :protected_environments, name: INDEX_NAME
end
private
def remove_group_protected_environments!
execute <<-SQL
DELETE FROM protected_environments WHERE group_id IS NOT NULL
SQL
end
end

View File

@ -0,0 +1,89 @@
# frozen_string_literal: true
class BackfillEscalationPoliciesForOncallSchedules < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
# Creates a single new escalation policy for projects which have
# existing on-call schedules. Only one schedule is expected
# per project, but it is possible to have multiple.
#
# An escalation rule is created for each existing schedule,
# configured to immediately notify the schedule of an incoming
# alert payload unless the alert has already been acknowledged.
# For projects with multiple schedules, the name of the first saved
# schedule will be used for the policy's description.
#
# Skips projects which already have escalation policies & schedules.
#
# EX)
# For these existing records:
# Project #3
# IncidentManagement::OncallSchedules #13
# project_id: 3
# name: 'Awesome Schedule'
# description: null
# IncidentManagement::OncallSchedules #14
# project_id: 3
# name: '2ndary sched'
# description: 'Backup on-call'
#
# These will be inserted:
# EscalationPolicy #1
# project_id: 3
# name: 'On-call Escalation Policy'
# description: 'Immediately notify Awesome Schedule'
# EscalationRule #1
# policy_id: 1,
# oncall_schedule_id: 13
# status: 1 # Acknowledged status
# elapsed_time_seconds: 0
# EscalationRule #2
# policy_id: 1,
# oncall_schedule_id: 14
# status: 1 # Acknowledged status
# elapsed_time_seconds: 0
def up
ApplicationRecord.connection.exec_query(<<~SQL.squish)
WITH new_escalation_policies AS (
INSERT INTO incident_management_escalation_policies (
project_id,
name,
description
)
SELECT
DISTINCT ON (project_id) project_id,
'On-call Escalation Policy',
CONCAT('Immediately notify ', name)
FROM incident_management_oncall_schedules
WHERE project_id NOT IN (
SELECT DISTINCT project_id
FROM incident_management_escalation_policies
)
ORDER BY project_id, id
RETURNING id, project_id
)
INSERT INTO incident_management_escalation_rules (
policy_id,
oncall_schedule_id,
status,
elapsed_time_seconds
)
SELECT
new_escalation_policies.id,
incident_management_oncall_schedules.id,
1,
0
FROM new_escalation_policies
INNER JOIN incident_management_oncall_schedules
ON new_escalation_policies.project_id = incident_management_oncall_schedules.project_id
SQL
end
# There is no way to distinguish between policies created
# via the backfill or as a result of a user creating a new
# on-call schedule.
def down
# no-op
end
end

View File

@ -0,0 +1 @@
bdd82fc5cb2bbb322125c153c741002725853e23cd0ae0edbfd80563a4a87f2f

View File

@ -0,0 +1 @@
6c687ffd41f242dcd0ecf1ff82652aba79130d2d54016729a817dafa0bac6184

View File

@ -0,0 +1 @@
88d2c1507503de626dfdb3f2f0eaf0f51fad5fc2279fd147d901c5dcc7ae91eb

View File

@ -0,0 +1 @@
2c5c0756757a181cf8bf7968de5184664004a82c093ae3fc14c5d6931a1ab44f

View File

@ -17168,10 +17168,12 @@ ALTER SEQUENCE protected_environment_deploy_access_levels_id_seq OWNED BY protec
CREATE TABLE protected_environments (
id integer NOT NULL,
project_id integer NOT NULL,
project_id integer,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
name character varying NOT NULL
name character varying NOT NULL,
group_id bigint,
CONSTRAINT protected_environments_project_or_group_existence CHECK (((project_id IS NULL) <> (group_id IS NULL)))
);
CREATE SEQUENCE protected_environments_id_seq
@ -24285,6 +24287,8 @@ CREATE INDEX index_protected_environment_deploy_access_levels_on_group_id ON pro
CREATE INDEX index_protected_environment_deploy_access_levels_on_user_id ON protected_environment_deploy_access_levels USING btree (user_id);
CREATE UNIQUE INDEX index_protected_environments_on_group_id_and_name ON protected_environments USING btree (group_id, name) WHERE (group_id IS NOT NULL);
CREATE INDEX index_protected_environments_on_project_id ON protected_environments USING btree (project_id);
CREATE UNIQUE INDEX index_protected_environments_on_project_id_and_name ON protected_environments USING btree (project_id, name);
@ -24767,6 +24771,8 @@ CREATE INDEX index_users_ops_dashboard_projects_on_project_id ON users_ops_dashb
CREATE UNIQUE INDEX index_users_ops_dashboard_projects_on_user_id_and_project_id ON users_ops_dashboard_projects USING btree (user_id, project_id);
CREATE INDEX index_users_require_two_factor_authentication_from_group_false ON users USING btree (require_two_factor_authentication_from_group) WHERE (require_two_factor_authentication_from_group = false);
CREATE INDEX index_users_security_dashboard_projects_on_user_id ON users_security_dashboard_projects USING btree (user_id);
CREATE INDEX index_users_star_projects_on_project_id ON users_star_projects USING btree (project_id);
@ -25718,6 +25724,9 @@ ALTER TABLE ONLY issues
ALTER TABLE ONLY epics
ADD CONSTRAINT fk_9d480c64b2 FOREIGN KEY (start_date_sourcing_epic_id) REFERENCES epics(id) ON DELETE SET NULL;
ALTER TABLE ONLY protected_environments
ADD CONSTRAINT fk_9e112565b7 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY alert_management_alerts
ADD CONSTRAINT fk_9e49e5c2b7 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;

View File

@ -5,7 +5,7 @@ group: Import
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Project/Group Import/Export rate limits
# Project/group import/export rate limits **(FREE SELF)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35728) in GitLab 13.2.

View File

@ -4,7 +4,7 @@ stage: Manage
group: Import
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Group Import/Export
# Group import/export **(FREE)**
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2888) in GitLab 13.0 as an experimental feature. May change in future releases.

View File

@ -80,6 +80,7 @@ The following table lists project permissions available for each role:
| Label issues | | ✓ | ✓ | ✓ | ✓ |
| Set issue weight | | ✓ | ✓ | ✓ | ✓ |
| [Set issue estimate and record time spent](project/time_tracking.md) | | ✓ | ✓ | ✓ | ✓ |
| View a time tracking report | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| Lock issue threads | | ✓ | ✓ | ✓ | ✓ |
| Manage issue tracker | | ✓ | ✓ | ✓ | ✓ |
| Manage linked issues | | ✓ | ✓ | ✓ | ✓ |

View File

@ -5,7 +5,7 @@ group: Import
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Import your project from Bitbucket Cloud to GitLab
# Import your project from Bitbucket Cloud to GitLab **(FREE)**
NOTE:
The Bitbucket Cloud importer works only with Bitbucket.org, not with Bitbucket

View File

@ -5,7 +5,7 @@ group: Import
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Import your project from Bitbucket Server to GitLab
# Import your project from Bitbucket Server to GitLab **(FREE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/20164) in GitLab 11.2.

View File

@ -5,7 +5,7 @@ group: Import
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Migrating from ClearCase
# Migrating from ClearCase **(FREE)**
[ClearCase](https://www.ibm.com/products/rational-clearcase) is a set of
tools developed by IBM which also include a centralized version control system

View File

@ -5,7 +5,7 @@ group: Import
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Migrating from CVS
# Migrating from CVS **(FREE)**
[CVS](https://savannah.nongnu.org/projects/cvs) is an old centralized version
control system similar to [SVN](svn.md).

View File

@ -5,7 +5,7 @@ group: Import
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Import your project from FogBugz to GitLab
# Import your project from FogBugz to GitLab **(FREE)**
It only takes a few simple steps to import your project from FogBugz.
The importer imports all your cases and comments with original case

View File

@ -5,7 +5,7 @@ group: Import
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Import your project from Gitea to GitLab
# Import your project from Gitea to GitLab **(FREE)**
Import your projects from Gitea to GitLab with minimal effort.

View File

@ -5,7 +5,7 @@ group: Import
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Import your project from GitHub to GitLab
# Import your project from GitHub to GitLab **(FREE)**
Using the importer, you can import your GitHub repositories to GitLab.com or to
your self-managed GitLab instance.

View File

@ -5,7 +5,7 @@ group: Import
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Project importing from GitLab.com to your private GitLab instance
# Project importing from GitLab.com to your private GitLab instance **(FREE)**
You can import your existing GitLab.com projects to your GitLab instance, but keep in
mind that it is possible only if GitLab.com integration is enabled on your GitLab instance.

View File

@ -5,7 +5,7 @@ group: Import
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Migrate projects to a GitLab instance
# Migrate projects to a GitLab instance **(FREE)**
See these documents to migrate to GitLab:

View File

@ -5,7 +5,7 @@ group: Import
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Import multiple repositories by uploading a manifest file
# Import multiple repositories by uploading a manifest file **(FREE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/28811) in GitLab 11.2.

View File

@ -5,7 +5,7 @@ group: Import
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Migrating from Perforce Helix
# Migrating from Perforce Helix **(FREE)**
[Perforce Helix](https://www.perforce.com/) provides a set of tools which also
include a centralized, proprietary version control system similar to Git.

View File

@ -5,7 +5,7 @@ group: Import
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Import Phabricator tasks into a GitLab project
# Import Phabricator tasks into a GitLab project **(FREE SELF)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/60562) in GitLab 12.0.

View File

@ -5,7 +5,7 @@ group: Import
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Import project from repository by URL
# Import project from repository by URL **(FREE)**
You can import your existing repositories by providing the Git URL:

View File

@ -5,7 +5,7 @@ group: Import
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Migrating from SVN to GitLab
# Migrating from SVN to GitLab **(FREE)**
Subversion (SVN) is a central version control system (VCS) while
Git is a distributed version control system. There are some major differences

View File

@ -1,11 +1,11 @@
---
stage: none
group: unassigned
stage: Manage
group: Import
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
type: concepts
---
# Migrate from TFVC to Git
# Migrate from TFVC to Git **(FREE)**
Team Foundation Server (TFS), renamed [Azure DevOps Server](https://azure.microsoft.com/en-us/services/devops/server/)
in 2019, is a set of tools developed by Microsoft which also includes

View File

@ -4,7 +4,7 @@ group: Import
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Importing issues from CSV
# Importing issues from CSV **(FREE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/23532) in GitLab 11.7.

View File

@ -5,7 +5,7 @@ info: "To determine the technical writer assigned to the Stage/Group associated
type: reference, howto
---
# Project import/export
# Project import/export **(FREE)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/3050) in GitLab 8.9.
> - From GitLab 10.0, administrators can disable the project export option on the GitLab instance.

View File

@ -82,6 +82,10 @@ To remove all the time spent at once, use `/remove_time_spent`.
You can view a breakdown of time spent on an issue or merge request.
Prerequisites:
- A minimum of [Reporter](../permissions.md#project-members-permissions) access to a private project in GitLab.
To view a time tracking report, go to an issue or a merge request and select **Time tracking report**
in the right sidebar.

View File

@ -6,10 +6,13 @@ module Banzai
module Filter
# HTML filter that converts relative urls into absolute ones.
class AbsoluteLinkFilter < HTML::Pipeline::Filter
CSS = 'a.gfm'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
def call
return doc unless context[:only_path] == false
doc.search('a.gfm').each do |el|
doc.xpath(XPATH).each do |el|
process_link_attr el.attribute('href')
end

View File

@ -3,14 +3,20 @@
module Banzai
module Filter
class AsciiDocPostProcessingFilter < HTML::Pipeline::Filter
CSS_MATH = '[data-math-style]'
XPATH_MATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_MATH).freeze
CSS_MERM = '[data-mermaid-style]'
XPATH_MERM = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_MERM).freeze
def call
doc.search('[data-math-style]').each do |node|
doc.xpath(XPATH_MATH).each do |node|
node.set_attribute('class', 'code math js-render-math')
end
doc.search('[data-mermaid-style]').each do |node|
doc.xpath(XPATH_MERM).each do |node|
node.set_attribute('class', 'js-render-mermaid')
end
doc
end
end

View File

@ -7,6 +7,9 @@ module Banzai
class BaseRelativeLinkFilter < HTML::Pipeline::Filter
include Gitlab::Utils::StrongMemoize
CSS = 'a:not(.gfm), img:not(.gfm), video:not(.gfm), audio:not(.gfm)'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
protected
def linkable_attributes
@ -35,7 +38,7 @@ module Banzai
def fetch_linkable_attributes
attrs = []
attrs += doc.search('a:not(.gfm), img:not(.gfm), video:not(.gfm), audio:not(.gfm)').flat_map do |el|
attrs += doc.xpath(XPATH).flat_map do |el|
[el.attribute('href'), el.attribute('src'), el.attribute('data-src')]
end

View File

@ -7,8 +7,11 @@ module Banzai
class ColorFilter < HTML::Pipeline::Filter
COLOR_CHIP_CLASS = 'gfm-color_chip'
CSS = 'code'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
def call
doc.css('code').each do |node|
doc.xpath(XPATH).each do |node|
color = ColorParser.parse(node.content)
node << color_chip(color) if color
end

View File

@ -11,7 +11,7 @@ module Banzai
return doc unless context[:project]
return doc unless Feature.enabled?(:custom_emoji, context[:project])
doc.search(".//text()").each do |node|
doc.xpath('descendant-or-self::text()').each do |node|
content = node.to_html
next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)

View File

@ -11,7 +11,7 @@ module Banzai
IGNORE_UNICODE_EMOJIS = %w(™ © ®).freeze
def call
doc.search(".//text()").each do |node|
doc.xpath('descendant-or-self::text()').each do |node|
content = node.to_html
next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)

View File

@ -23,17 +23,23 @@ module Banzai
FOOTNOTE_LINK_REFERENCE_PATTERN = /\A#{FOOTNOTE_LINK_ID_PREFIX}\d+\z/.freeze
FOOTNOTE_START_NUMBER = 1
CSS_SECTION = "ol > li[id=#{FOOTNOTE_ID_PREFIX}#{FOOTNOTE_START_NUMBER}]"
XPATH_SECTION = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_SECTION).freeze
CSS_FOOTNOTE = 'sup > a[id]'
XPATH_FOOTNOTE = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_FOOTNOTE).freeze
def call
return doc unless first_footnote = doc.at_css("ol > li[id=#{fn_id(FOOTNOTE_START_NUMBER)}]")
return doc unless first_footnote = doc.at_xpath(XPATH_SECTION)
# Sanitization stripped off the section wrapper - add it back in
first_footnote.parent.wrap('<section class="footnotes">')
rand_suffix = "-#{random_number}"
modified_footnotes = {}
doc.css('sup > a[id]').each do |link_node|
doc.xpath(XPATH_FOOTNOTE).each do |link_node|
ref_num = link_node[:id].delete_prefix(FOOTNOTE_LINK_ID_PREFIX)
footnote_node = doc.at_css("li[id=#{fn_id(ref_num)}]")
node_xpath = Gitlab::Utils::Nokogiri.css_to_xpath("li[id=#{fn_id(ref_num)}]")
footnote_node = doc.at_xpath(node_xpath)
if INTEGER_PATTERN.match?(ref_num) && (footnote_node || modified_footnotes[ref_num])
link_node[:href] += rand_suffix

View File

@ -60,7 +60,7 @@ module Banzai
IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set
def call
doc.search(".//text()").each do |node|
doc.xpath('descendant-or-self::text()').each do |node|
next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
next unless node.content =~ TAGS_PATTERN

View File

@ -6,8 +6,11 @@ module Banzai
# HTML filter that moves the value of image `src` attributes to `data-src`
# so they can be lazy loaded.
class ImageLazyLoadFilter < HTML::Pipeline::Filter
CSS = 'img'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
def call
doc.xpath('descendant-or-self::img').each do |img|
doc.xpath(XPATH).each do |img|
img.add_class('lazy')
img['data-src'] = img['src']
img['src'] = LazyImageTagHelper.placeholder_image

View File

@ -7,7 +7,7 @@ module Banzai
IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set
def call
doc.search(".//text()").each do |node|
doc.xpath('descendant-or-self::text()').each do |node|
next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
content = node.to_html

View File

@ -8,6 +8,7 @@ module Banzai
include Gitlab::Utils::StrongMemoize
METRICS_CSS_CLASS = '.js-render-metrics'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(METRICS_CSS_CLASS).freeze
EMBED_LIMIT = 100
Route = Struct.new(:regex, :permission)
@ -41,7 +42,7 @@ module Banzai
# @return [Nokogiri::XML::NodeSet]
def nodes
strong_memoize(:nodes) do
nodes = doc.css(METRICS_CSS_CLASS)
nodes = doc.xpath(XPATH)
nodes.drop(EMBED_LIMIT).each(&:remove)
nodes

View File

@ -15,10 +15,11 @@ module Banzai
.map { |diagram_type| %(pre[lang="#{diagram_type}"] > code) }
.join(', ')
return doc unless doc.at(diagram_selectors)
xpath = Gitlab::Utils::Nokogiri.css_to_xpath(diagram_selectors)
return doc unless doc.at_xpath(xpath)
diagram_format = "svg"
doc.css(diagram_selectors).each do |node|
doc.xpath(xpath).each do |node|
diagram_type = node.parent['lang']
img_tag = Nokogiri::HTML::DocumentFragment.parse(%(<img src="#{create_image_src(diagram_type, diagram_format, node.content)}"/>))
node.parent.replace(img_tag)

View File

@ -8,6 +8,11 @@ module Banzai
NOT_LITERAL_REGEX = %r{#{LITERAL_KEYWORD}-((%5C|\\).+?)-#{LITERAL_KEYWORD}}.freeze
SPAN_REGEX = %r{<span>(.*?)</span>}.freeze
CSS_A = 'a'
XPATH_A = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_A).freeze
CSS_CODE = 'code'
XPATH_CODE = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_CODE).freeze
def call
return doc unless result[:escaped_literals]
@ -24,12 +29,12 @@ module Banzai
# Banzai::Renderer::CommonMark::HTML. However, we eventually want to use
# the built-in compiled renderer, rather than the ruby version, for speed.
# So let's do this work here.
doc.css('a').each do |node|
doc.xpath(XPATH_A).each do |node|
node.attributes['href'].value = node.attributes['href'].value.gsub(SPAN_REGEX, '\1') if node.attributes['href']
node.attributes['title'].value = node.attributes['title'].value.gsub(SPAN_REGEX, '\1') if node.attributes['title']
end
doc.css('code').each do |node|
doc.xpath(XPATH_CODE).each do |node|
node.attributes['lang'].value = node.attributes['lang'].value.gsub(SPAN_REGEX, '\1') if node.attributes['lang']
end

View File

@ -10,6 +10,11 @@ module Banzai
# HTML filter that adds class="code math" and removes the dollar sign in $`2+2`$.
#
class MathFilter < HTML::Pipeline::Filter
CSS_MATH = 'pre.code.language-math'
XPATH_MATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_MATH).freeze
CSS_CODE = 'code'
XPATH_CODE = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_CODE).freeze
# Attribute indicating inline or display math.
STYLE_ATTRIBUTE = 'data-math-style'
@ -21,7 +26,7 @@ module Banzai
DOLLAR_SIGN = '$'
def call
doc.css('code').each do |code|
doc.xpath(XPATH_CODE).each do |code|
closing = code.next
opening = code.previous
@ -39,7 +44,7 @@ module Banzai
end
end
doc.css('pre.code.language-math').each do |el|
doc.xpath(XPATH_MATH).each do |el|
el[STYLE_ATTRIBUTE] = 'display'
el[:class] += " #{TAG_CLASS}"
end

View File

@ -4,8 +4,11 @@
module Banzai
module Filter
class MermaidFilter < HTML::Pipeline::Filter
CSS = 'pre[lang="mermaid"] > code'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
def call
doc.css('pre[lang="mermaid"] > code').add_class('js-render-mermaid')
doc.xpath(XPATH).add_class('js-render-mermaid')
doc
end

View File

@ -8,12 +8,15 @@ module Banzai
# HTML that replaces all `code plantuml` tags with PlantUML img tags.
#
class PlantumlFilter < HTML::Pipeline::Filter
CSS = 'pre > code[lang="plantuml"]'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
def call
return doc unless settings.plantuml_enabled? && doc.at('pre > code[lang="plantuml"]')
return doc unless settings.plantuml_enabled? && doc.at_xpath(XPATH)
plantuml_setup
doc.css('pre > code[lang="plantuml"]').each do |node|
doc.xpath(XPATH).each do |node|
img_tag = Nokogiri::HTML::DocumentFragment.parse(
Asciidoctor::PlantUml::Processor.plantuml_content(node.content, {}))
node.parent.replace(img_tag)

View File

@ -7,10 +7,13 @@ module Banzai
# Class used for tagging elements that should be rendered
TAG_CLASS = 'js-render-suggestion'
CSS = 'pre.language-suggestion > code'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
def call
return doc unless suggestions_filter_enabled?
doc.search('pre.language-suggestion > code').each do |node|
doc.xpath(XPATH).each do |node|
node.add_class(TAG_CLASS)
end

View File

@ -14,8 +14,11 @@ module Banzai
PARAMS_DELIMITER = ':'
LANG_PARAMS_ATTR = 'data-lang-params'
CSS = 'pre:not([data-math-style]):not([data-mermaid-style]):not([data-kroki-style]) > code'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
def call
doc.search('pre:not([data-math-style]):not([data-mermaid-style]):not([data-kroki-style]) > code').each do |node|
doc.xpath(XPATH).each do |node|
highlight_node(node)
end

View File

@ -19,6 +19,9 @@ module Banzai
class TableOfContentsFilter < HTML::Pipeline::Filter
include Gitlab::Utils::Markdown
CSS = 'h1, h2, h3, h4, h5, h6'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
def call
return doc if context[:no_header_anchors]
@ -27,7 +30,7 @@ module Banzai
headers = Hash.new(0)
header_root = current_header = HeaderNode.new
doc.css('h1, h2, h3, h4, h5, h6').each do |node|
doc.xpath(XPATH).each do |node|
if header_content = node.children.first
id = string_to_anchor(node.text)

View File

@ -3,12 +3,29 @@
module Banzai
module Filter
class TruncateSourceFilter < HTML::Pipeline::TextFilter
CHARACTER_COUNT_LIMIT = 1.megabyte
USER_MSG_LIMIT = 10_000
def call
return text unless context.key?(:limit)
# don't truncate if it's a :blob and no limit is set
return text if context[:text_source] == :blob && !context.key?(:limit)
limit = context[:limit] || CHARACTER_COUNT_LIMIT
# no sense in allowing `truncate_bytes` to duplicate a large
# string unless it's too big
return text if text.bytesize <= limit
# Use three dots instead of the ellipsis Unicode character because
# some clients show the raw Unicode value in the merge commit.
text.truncate_bytes(context[:limit], omission: '...')
trunc = text.truncate_bytes(limit, omission: '...')
# allows us to indicate to the user that what they see is a truncated copy
if limit > USER_MSG_LIMIT
trunc.prepend("_The text is longer than #{limit} characters and has been visually truncated._\n\n")
end
trunc
end
end
end

View File

@ -10,14 +10,21 @@ module Banzai
class WikiLinkFilter < HTML::Pipeline::Filter
include Gitlab::Utils::SanitizeNodeLink
CSS_A = 'a:not(.gfm)'
XPATH_A = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_A).freeze
CSS_VA = 'video, audio'
XPATH_VA = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_VA).freeze
CSS_IMG = 'img'
XPATH_IMG = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_IMG).freeze
def call
return doc unless wiki?
doc.search('a:not(.gfm)').each { |el| process_link(el.attribute('href'), el) }
doc.xpath(XPATH_A).each { |el| process_link(el.attribute('href'), el) }
doc.search('video, audio').each { |el| process_link(el.attribute('src'), el) }
doc.xpath(XPATH_VA).each { |el| process_link(el.attribute('src'), el) }
doc.search('img').each do |el|
doc.xpath(XPATH_IMG).each do |el|
attr = el.attribute('data-src') || el.attribute('src')
process_link(attr, el)

View File

@ -84,7 +84,7 @@ module Gitlab
Gitlab::Auth::UniqueIpsLimiter.limit_user! do
user = User.by_login(login)
break if user && !user.can?(:log_in)
break if user && !can_user_login_with_non_expired_password?(user)
authenticators = []
@ -182,7 +182,7 @@ module Gitlab
if valid_oauth_token?(token)
user = User.id_in(token.resource_owner_id).first
return unless user&.can?(:log_in)
return unless user && can_user_login_with_non_expired_password?(user)
Gitlab::Auth::Result.new(user, nil, :oauth, full_authentication_abilities)
end
@ -200,7 +200,7 @@ module Gitlab
return if project && token.user.project_bot? && !project.bots.include?(token.user)
if token.user.can?(:log_in) || token.user.project_bot?
if can_user_login_with_non_expired_password?(token.user) || token.user.project_bot?
Gitlab::Auth::Result.new(token.user, nil, :personal_access_token, abilities_for_scopes(token.scopes))
end
end
@ -285,7 +285,7 @@ module Gitlab
return unless build.project.builds_enabled?
if build.user
return unless build.user.can?(:log_in) || (build.user.project_bot? && build.project.bots&.include?(build.user))
return unless can_user_login_with_non_expired_password?(build.user) || (build.user.project_bot? && build.project.bots&.include?(build.user))
# If user is assigned to build, use restricted credentials of user
Gitlab::Auth::Result.new(build.user, build.project, :build, build_authentication_abilities)
@ -380,6 +380,10 @@ module Gitlab
user.increment_failed_attempts!
end
def can_user_login_with_non_expired_password?(user)
user.can?(:log_in) && !user.password_expired?
end
end
end
end

View File

@ -23,6 +23,9 @@ module Gitlab
"Your primary email address is not confirmed. "\
"Please check your inbox for the confirmation instructions. "\
"In case the link is expired, you can request a new confirmation email at #{Rails.application.routes.url_helpers.new_user_confirmation_url}"
when :password_expired
"Your password expired. "\
"Please access GitLab from a web browser to update your password."
else
"Your account has been blocked."
end
@ -41,6 +44,8 @@ module Gitlab
:deactivated
elsif !@user.confirmed?
:unconfirmed
elsif @user.password_expired?
:password_expired
else
:blocked
end

View File

@ -0,0 +1,129 @@
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
class UpdateUsersWhereTwoFactorAuthRequiredFromGroup # rubocop:disable Metrics/ClassLength
def perform(start_id, stop_id)
ActiveRecord::Base.connection.execute <<~SQL
UPDATE
users
SET
require_two_factor_authentication_from_group = TRUE
WHERE
users.id BETWEEN #{start_id}
AND #{stop_id}
AND users.require_two_factor_authentication_from_group = FALSE
AND users.id IN (
SELECT
DISTINCT users_groups_query.user_id
FROM
(
SELECT
users.id AS user_id,
members.source_id AS group_ids
FROM
users
LEFT JOIN members ON members.source_type = 'Namespace'
AND members.requested_at IS NULL
AND members.user_id = users.id
AND members.type = 'GroupMember'
WHERE
users.require_two_factor_authentication_from_group = FALSE
AND users.id BETWEEN #{start_id}
AND #{stop_id}) AS users_groups_query
INNER JOIN LATERAL (
WITH RECURSIVE "base_and_ancestors" AS (
(
SELECT
"namespaces"."type",
"namespaces"."id",
"namespaces"."parent_id",
"namespaces"."require_two_factor_authentication"
FROM
"namespaces"
WHERE
"namespaces"."type" = 'Group'
AND "namespaces"."id" = users_groups_query.group_ids
)
UNION
(
SELECT
"namespaces"."type",
"namespaces"."id",
"namespaces"."parent_id",
"namespaces"."require_two_factor_authentication"
FROM
"namespaces",
"base_and_ancestors"
WHERE
"namespaces"."type" = 'Group'
AND "namespaces"."id" = "base_and_ancestors"."parent_id"
)
),
"base_and_descendants" AS (
(
SELECT
"namespaces"."type",
"namespaces"."id",
"namespaces"."parent_id",
"namespaces"."require_two_factor_authentication"
FROM
"namespaces"
WHERE
"namespaces"."type" = 'Group'
AND "namespaces"."id" = users_groups_query.group_ids
)
UNION
(
SELECT
"namespaces"."type",
"namespaces"."id",
"namespaces"."parent_id",
"namespaces"."require_two_factor_authentication"
FROM
"namespaces",
"base_and_descendants"
WHERE
"namespaces"."type" = 'Group'
AND "namespaces"."parent_id" = "base_and_descendants"."id"
)
)
SELECT
"namespaces".*
FROM
(
(
SELECT
"namespaces"."type",
"namespaces"."id",
"namespaces"."parent_id",
"namespaces"."require_two_factor_authentication"
FROM
"base_and_ancestors" AS "namespaces"
WHERE
"namespaces"."type" = 'Group'
)
UNION
(
SELECT
"namespaces"."type",
"namespaces"."id",
"namespaces"."parent_id",
"namespaces"."require_two_factor_authentication"
FROM
"base_and_descendants" AS "namespaces"
WHERE
"namespaces"."type" = 'Group'
)
) namespaces
WHERE
"namespaces"."type" = 'Group'
AND "namespaces"."require_two_factor_authentication" = TRUE
) AS hierarchy_tree ON TRUE
);
SQL
end
end
end
end

View File

@ -6,6 +6,9 @@ module Gitlab
# Matches for instance "-1", "+1" or "-1+2".
SUGGESTION_CONTEXT = /^(\-(?<above>\d+))?(\+(?<below>\d+))?$/.freeze
CSS = 'pre.language-suggestion'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
class << self
# Returns an array of Gitlab::Diff::Suggestion which represents each
# suggestion in the given text.
@ -17,7 +20,7 @@ module Gitlab
no_original_data: true,
suggestions_filter_enabled: supports_suggestion)
doc = Nokogiri::HTML(html)
suggestion_nodes = doc.search('pre.language-suggestion')
suggestion_nodes = doc.xpath(XPATH)
return [] if suggestion_nodes.empty?

View File

@ -197,6 +197,24 @@ module Gitlab
rescue Addressable::URI::InvalidURIError, TypeError
end
def removes_sensitive_data_from_url(uri_string)
uri = parse_url(uri_string)
return unless uri
return uri_string unless uri.fragment
stripped_params = CGI.parse(uri.fragment)
if stripped_params['access_token']
stripped_params['access_token'] = 'filtered'
filtered_query = Addressable::URI.new
filtered_query.query_values = stripped_params
uri.fragment = filtered_query.query
end
uri.to_s
end
# Invert a hash, collecting all keys that map to a given value in an array.
#
# Unlike `Hash#invert`, where the last encountered pair wins, and which has the

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
module Gitlab
module Utils
class Nokogiri
class << self
# Use Nokogiri to convert a css selector into an xpath selector.
# Nokogiri can use css selectors with `doc.search()`. However
# for large node trees, it is _much_ slower than using xpath,
# by several orders of magnitude.
# https://gitlab.com/gitlab-org/gitlab/-/issues/329186
def css_to_xpath(css)
xpath = ::Nokogiri::CSS.xpath_for(css)
# Due to https://github.com/sparklemotion/nokogiri/issues/572,
# we remove the leading `//` and add `descendant-or-self::`
# in order to ensure we're searching from this node and all
# descendants.
xpath.map { |t| "descendant-or-self::#{t[2..-1]}" }.join('|')
end
end
end
end
end

View File

@ -23,7 +23,7 @@ module Gitlab
end
def user
User.find_by_any_email(@email)
strong_memoize(:user) { User.find_by_any_email(@email) }
end
def verified_signature
@ -31,9 +31,13 @@ module Gitlab
end
def verification_status
return :unverified if x509_certificate.nil? || x509_certificate.revoked?
return :unverified if
x509_certificate.nil? ||
x509_certificate.revoked? ||
!verified_signature ||
user.nil?
if verified_signature && certificate_email == @email
if user.verified_emails.include?(@email) && certificate_email == @email
:verified
else
:unverified

View File

@ -27077,6 +27077,9 @@ msgstr ""
msgid "Redirect to SAML provider to test configuration"
msgstr ""
msgid "Redirecting"
msgstr ""
msgid "Redis"
msgstr ""

View File

@ -70,77 +70,60 @@ RSpec.describe Oauth::AuthorizationsController do
describe 'GET #new' do
subject { get :new, params: params }
include_examples 'OAuth Authorizations require confirmed user'
include_examples "Implicit grant can't be used in confidential application"
context 'rendering of views based on the ownership of the application' do
shared_examples 'render views' do
context 'when the user is confirmed' do
let(:confirmed_at) { 1.hour.ago }
context 'when there is already an access token for the application with a matching scope' do
before do
scopes = Doorkeeper::OAuth::Scopes.from_string('api')
allow(Doorkeeper.configuration).to receive(:scopes).and_return(scopes)
create(:oauth_access_token, application: application, resource_owner_id: user.id, scopes: scopes)
end
it 'authorizes the request and shows the user a page that redirects' do
subject
expect(request.session['user_return_to']).to be_nil
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('doorkeeper/authorizations/redirect')
end
end
context 'without valid params' do
it 'returns 200 code and renders error view' do
get :new
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('doorkeeper/authorizations/error')
end
end
context 'with valid params' do
render_views
it 'returns 200 and renders view with correct info', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to include(application.owner.name)
expect(response).to render_template('doorkeeper/authorizations/new')
end
end
subject { get :new, params: params }
context 'when auth app owner is a user' do
context 'with valid params' do
it_behaves_like 'render views'
end
end
context 'when auth app owner is a group' do
let(:group) { create(:group) }
context 'when auth app owner is a root group' do
let(:application) { create(:oauth_application, owner_id: group.id, owner_type: 'Namespace') }
it_behaves_like 'render views'
end
context 'when auth app owner is a subgroup' do
let(:subgroup) { create(:group, parent: group) }
let(:application) { create(:oauth_application, owner_id: subgroup.id, owner_type: 'Namespace') }
it_behaves_like 'render views'
end
end
context 'when there is no owner associated' do
let(:application) { create(:oauth_application, owner_id: nil, owner_type: nil) }
it 'renders view' do
it 'returns 200 code and renders view' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('doorkeeper/authorizations/new')
end
it 'deletes session.user_return_to and redirects when skip authorization' do
application.update!(trusted: true)
request.session['user_return_to'] = 'http://example.com'
subject
expect(request.session['user_return_to']).to be_nil
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('doorkeeper/authorizations/redirect')
end
end
end
context 'without valid params' do
it 'returns 200 code and renders error view' do
get :new
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('doorkeeper/authorizations/error')
end
end
it 'deletes session.user_return_to and redirects when skip authorization' do
application.update!(trusted: true)
request.session['user_return_to'] = 'http://example.com'
subject
expect(request.session['user_return_to']).to be_nil
expect(response).to have_gitlab_http_status(:found)
end
end
describe 'POST #create' do

View File

@ -16,19 +16,19 @@ FactoryBot.define do
end
trait :production do
tier { :production }
name { 'production' }
end
trait :staging do
tier { :staging }
name { 'staging' }
end
trait :testing do
tier { :testing }
name { 'testing' }
end
trait :development do
tier { :development }
name { 'development' }
end
trait :with_review_app do |environment|

View File

@ -394,6 +394,25 @@ RSpec.describe 'Login' do
gitlab_sign_in(user)
end
context 'when the users password is expired' do
before do
user.update!(password_expires_at: Time.parse('2018-05-08 11:29:46 UTC'))
end
it 'asks for a new password' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
visit new_user_session_path
fill_in 'user_login', with: user.email
fill_in 'user_password', with: '12345678'
click_button 'Sign in'
expect(current_path).to eq(new_profile_password_path)
end
end
end
context 'with invalid username and password' do

View File

@ -39,7 +39,7 @@ describe('Markdown component', () => {
expect(vm.$el.querySelector('.markdown h1')).not.toBeNull();
});
it('sanitizes output', async () => {
it('sanitizes Markdown output', async () => {
Object.assign(cell, {
source: [
'[XSS](data:text/html;base64,PHNjcmlwdD5hbGVydChkb2N1bWVudC5kb21haW4pPC9zY3JpcHQ+Cg==)\n',
@ -50,6 +50,17 @@ describe('Markdown component', () => {
expect(vm.$el.querySelector('a').getAttribute('href')).toBeNull();
});
it('sanitizes HTML', async () => {
const findLink = () => vm.$el.querySelector('.xss-link');
Object.assign(cell, {
source: ['<a href="test.js" data-remote=true data-type="script" class="xss-link">XSS</a>\n'],
});
await vm.$nextTick();
expect(findLink().getAttribute('data-remote')).toBe(null);
expect(findLink().getAttribute('data-type')).toBe(null);
});
describe('tables', () => {
beforeEach(() => {
json = getJSONFixture('blob/notebook/markdown-table.json');

View File

@ -11,26 +11,27 @@ RSpec.describe Resolvers::TimelogResolver do
context "with a group" do
let_it_be(:current_user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :public, group: group) }
before_all do
group.add_developer(current_user)
project.add_developer(current_user)
end
before do
group.clear_memoization(:timelogs)
end
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :empty_repo, :public, group: group) }
describe '#resolve' do
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:issue2) { create(:issue, project: project) }
let_it_be(:timelog1) { create(:issue_timelog, issue: issue, spent_at: 2.days.ago.beginning_of_day) }
let_it_be(:timelog2) { create(:issue_timelog, issue: issue2, spent_at: 2.days.ago.end_of_day) }
let_it_be(:timelog3) { create(:issue_timelog, issue: issue2, spent_at: 10.days.ago) }
let_it_be(:short_time_ago) { 5.days.ago.beginning_of_day }
let_it_be(:medium_time_ago) { 15.days.ago.beginning_of_day }
let(:args) { { start_time: 6.days.ago, end_time: 2.days.ago.noon } }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
let_it_be(:timelog1) { create(:issue_timelog, issue: issue, spent_at: short_time_ago.beginning_of_day) }
let_it_be(:timelog2) { create(:issue_timelog, issue: issue, spent_at: short_time_ago.end_of_day) }
let_it_be(:timelog3) { create(:merge_request_timelog, merge_request: merge_request, spent_at: medium_time_ago) }
let(:args) { { start_time: short_time_ago, end_time: short_time_ago.noon } }
it 'finds all timelogs' do
timelogs = resolve_timelogs
expect(timelogs).to contain_exactly(timelog1, timelog2, timelog3)
end
it 'finds all timelogs within given dates' do
timelogs = resolve_timelogs(**args)
@ -38,15 +39,28 @@ RSpec.describe Resolvers::TimelogResolver do
expect(timelogs).to contain_exactly(timelog1)
end
it 'return nothing when user has insufficient permissions' do
user = create(:user)
group.add_guest(current_user)
context 'when only start_date is present' do
let(:args) { { start_date: short_time_ago } }
expect(resolve_timelogs(user: user, **args)).to be_empty
it 'finds timelogs until the end of day of end_date' do
timelogs = resolve_timelogs(**args)
expect(timelogs).to contain_exactly(timelog1, timelog2)
end
end
context 'when only end_date is present' do
let(:args) { { end_date: medium_time_ago } }
it 'finds timelogs until the end of day of end_date' do
timelogs = resolve_timelogs(**args)
expect(timelogs).to contain_exactly(timelog3)
end
end
context 'when start_time and end_date are present' do
let(:args) { { start_time: 6.days.ago, end_date: 2.days.ago } }
let(:args) { { start_time: short_time_ago, end_date: short_time_ago } }
it 'finds timelogs until the end of day of end_date' do
timelogs = resolve_timelogs(**args)
@ -56,7 +70,7 @@ RSpec.describe Resolvers::TimelogResolver do
end
context 'when start_date and end_time are present' do
let(:args) { { start_date: 6.days.ago, end_time: 2.days.ago.noon } }
let(:args) { { start_date: short_time_ago, end_time: short_time_ago.noon } }
it 'finds all timelogs within start_date and end_time' do
timelogs = resolve_timelogs(**args)
@ -68,95 +82,32 @@ RSpec.describe Resolvers::TimelogResolver do
context 'when arguments are invalid' do
let_it_be(:error_class) { Gitlab::Graphql::Errors::ArgumentError }
context 'when no time or date arguments are present' do
let(:args) { {} }
it 'returns correct error' do
expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Start and End arguments must be present/)
end
end
context 'when only start_time is present' do
let(:args) { { start_time: 6.days.ago } }
it 'returns correct error' do
expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Both Start and End arguments must be present/)
end
end
context 'when only end_time is present' do
let(:args) { { end_time: 2.days.ago } }
it 'returns correct error' do
expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Both Start and End arguments must be present/)
end
end
context 'when only start_date is present' do
let(:args) { { start_date: 6.days.ago } }
it 'returns correct error' do
expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Both Start and End arguments must be present/)
end
end
context 'when only end_date is present' do
let(:args) { { end_date: 2.days.ago } }
it 'returns correct error' do
expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Both Start and End arguments must be present/)
end
end
context 'when start_time and start_date are present' do
let(:args) { { start_time: 6.days.ago, start_date: 6.days.ago } }
let(:args) { { start_time: short_time_ago, start_date: short_time_ago } }
it 'returns correct error' do
expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Both Start and End arguments must be present/)
.to raise_error(error_class, /Provide either a start date or time, but not both/)
end
end
context 'when end_time and end_date are present' do
let(:args) { { end_time: 2.days.ago, end_date: 2.days.ago } }
let(:args) { { end_time: short_time_ago, end_date: short_time_ago } }
it 'returns correct error' do
expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Both Start and End arguments must be present/)
end
end
context 'when three arguments are present' do
let(:args) { { start_date: 6.days.ago, end_date: 2.days.ago, end_time: 2.days.ago } }
it 'returns correct error' do
expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Only Time or Date arguments must be present/)
.to raise_error(error_class, /Provide either an end date or time, but not both/)
end
end
context 'when start argument is after end argument' do
let(:args) { { start_time: 2.days.ago, end_time: 6.days.ago } }
let(:args) { { start_time: short_time_ago, end_time: medium_time_ago } }
it 'returns correct error' do
expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Start argument must be before End argument/)
end
end
context 'when time range is more than 60 days' do
let(:args) { { start_time: 3.months.ago, end_time: 2.days.ago } }
it 'returns correct error' do
expect { resolve_timelogs(**args) }
.to raise_error(error_class, /The time range period cannot contain more than 60 days/)
end
end
end
end
end

View File

@ -7,7 +7,7 @@ RSpec.describe GitlabSchema.types['Timelog'] do
it { expect(described_class.graphql_name).to eq('Timelog') }
it { expect(described_class).to have_graphql_fields(fields) }
it { expect(described_class).to require_graphql_authorizations(:read_group_timelogs) }
it { expect(described_class).to require_graphql_authorizations(:read_issue) }
describe 'user field' do
subject { described_class.fields['user'] }

View File

@ -418,6 +418,13 @@ FooBar
describe '#markup' do
let(:content) { 'Noël' }
it 'sets the :text_source to :blob in the context' do
context = {}
helper.markup('foo.md', content, context)
expect(context).to include(text_source: :blob)
end
it 'preserves encoding' do
expect(content.encoding.name).to eq('UTF-8')
expect(helper.markup('foo.rst', content).encoding.name).to eq('UTF-8')

View File

@ -173,6 +173,27 @@ RSpec.describe 'lograge', type: :request do
end
end
describe 'with access token in url' do
before do
event.payload[:location] = 'http://example.com/auth.html#access_token=secret_token&token_type=Bearer'
end
it 'strips location from sensitive information' do
subscriber.redirect_to(event)
subscriber.process_action(event)
expect(log_data['location']).not_to include('secret_token')
expect(log_data['location']).to include('filtered')
end
it 'leaves non-sensitive information from location' do
subscriber.redirect_to(event)
subscriber.process_action(event)
expect(log_data['location']).to include('&token_type=Bearer')
end
end
context 'with db payload' do
context 'when RequestStore is enabled', :request_store do
it 'includes db counters' do

View File

@ -8,24 +8,68 @@ RSpec.describe Banzai::Filter::TruncateSourceFilter do
let(:short_text) { 'foo' * 10 }
let(:long_text) { ([short_text] * 10).join(' ') }
it 'does nothing when limit is unspecified' do
output = filter(long_text)
expect(output).to eq(long_text)
before do
stub_const("#{described_class}::CHARACTER_COUNT_LIMIT", 50)
stub_const("#{described_class}::USER_MSG_LIMIT", 20)
end
it 'does nothing to a short-enough text' do
output = filter(short_text, limit: short_text.bytesize)
context 'when markdown belongs to a blob' do
it 'does nothing when limit is unspecified' do
output = filter(long_text, text_source: :blob)
expect(output).to eq(short_text)
expect(output).to eq(long_text)
end
it 'truncates normally when limit specified' do
truncated = 'foofoof...'
output = filter(long_text, text_source: :blob, limit: 10)
expect(output).to eq(truncated)
end
end
it 'truncates UTF-8 text by bytes, on a character boundary' do
utf8_text = '日本語の文字が大きい'
truncated = '日...'
context 'when markdown belongs to a field (non-blob)' do
it 'does nothing when limit is greater' do
output = filter(long_text, limit: 1.megabyte)
expect(filter(utf8_text, limit: truncated.bytesize)).to eq(truncated)
expect(filter(utf8_text, limit: utf8_text.bytesize)).to eq(utf8_text)
expect(filter(utf8_text, limit: utf8_text.mb_chars.size)).not_to eq(utf8_text)
expect(output).to eq(long_text)
end
it 'truncates to the default when limit is unspecified' do
stub_const("#{described_class}::USER_MSG_LIMIT", 200)
truncated = 'foofoofoofoofoofoofoofoofoofoo foofoofoofoofoof...'
output = filter(long_text)
expect(output).to eq(truncated)
end
it 'prepends the user message' do
truncated = <<~TEXT
_The text is longer than 50 characters and has been visually truncated._
foofoofoofoofoofoofoofoofoofoo foofoofoofoofoof...
TEXT
output = filter(long_text)
expect(output).to eq(truncated.strip)
end
it 'does nothing to a short-enough text' do
output = filter(short_text, limit: short_text.bytesize)
expect(output).to eq(short_text)
end
it 'truncates UTF-8 text by bytes, on a character boundary' do
utf8_text = '日本語の文字が大きい'
truncated = '日...'
expect(filter(utf8_text, limit: truncated.bytesize)).to eq(truncated)
expect(filter(utf8_text, limit: utf8_text.bytesize)).to eq(utf8_text)
expect(filter(utf8_text, limit: utf8_text.mb_chars.size)).not_to eq(utf8_text)
end
end
end

View File

@ -36,9 +36,11 @@ RSpec.describe Banzai::Pipeline::PostProcessPipeline do
end
let(:doc) { HTML::Pipeline.parse(html) }
let(:non_related_xpath_calls) { 2 }
it 'searches for attributes only once' do
expect(doc).to receive(:search).once.and_call_original
expect(doc).to receive(:xpath).exactly(non_related_xpath_calls + 1).times
.and_call_original
subject
end

View File

@ -57,5 +57,13 @@ RSpec.describe Gitlab::Auth::UserAccessDeniedReason do
it { is_expected.to eq('Your account is pending approval from your administrator and hence blocked.') }
end
context 'when the user has expired password' do
before do
user.update!(password_expires_at: 2.days.ago)
end
it { is_expected.to eq('Your password expired. Please access GitLab from a web browser to update your password.') }
end
end
end

View File

@ -0,0 +1,84 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::UpdateUsersWhereTwoFactorAuthRequiredFromGroup, :migration, schema: 20210519154058 do
include MigrationHelpers::NamespacesHelpers
let(:group_with_2fa_parent) { create_namespace('parent', Gitlab::VisibilityLevel::PRIVATE, require_two_factor_authentication: true) }
let(:group_with_2fa_child) { create_namespace('child', Gitlab::VisibilityLevel::PRIVATE, parent_id: group_with_2fa_parent.id) }
let(:members_table) { table(:members) }
let(:users_table) { table(:users) }
subject { described_class.new }
describe '#perform' do
context 'with group members' do
let(:user_1) { create_user('user@example.com') }
let!(:member) { create_group_member(user_1, group_with_2fa_parent) }
let!(:user_without_group) { create_user('user_without@example.com') }
let(:user_other) { create_user('user_other@example.com') }
let!(:member_other) { create_group_member(user_other, group_with_2fa_parent) }
it 'updates user when user should be required to establish two factor authentication' do
subject.perform(user_1.id, user_without_group.id)
expect(user_1.reload.require_two_factor_authentication_from_group).to eq(true)
end
it 'does not update user who is not in current batch' do
subject.perform(user_1.id, user_without_group.id)
expect(user_other.reload.require_two_factor_authentication_from_group).to eq(false)
end
it 'updates all users in current batch' do
subject.perform(user_1.id, user_other.id)
expect(user_other.reload.require_two_factor_authentication_from_group).to eq(true)
end
it 'updates user when user is member of group in which parent group requires two factor authentication' do
member.destroy!
subgroup = create_namespace('subgroup', Gitlab::VisibilityLevel::PRIVATE, require_two_factor_authentication: false, parent_id: group_with_2fa_child.id)
create_group_member(user_1, subgroup)
subject.perform(user_1.id, user_other.id)
expect(user_1.reload.require_two_factor_authentication_from_group).to eq(true)
end
it 'updates user when user is member of a group and the subgroup requires two factor authentication' do
member.destroy!
parent = create_namespace('other_parent', Gitlab::VisibilityLevel::PRIVATE, require_two_factor_authentication: false)
create_namespace('other_subgroup', Gitlab::VisibilityLevel::PRIVATE, require_two_factor_authentication: true, parent_id: parent.id)
create_group_member(user_1, parent)
subject.perform(user_1.id, user_other.id)
expect(user_1.reload.require_two_factor_authentication_from_group).to eq(true)
end
it 'does not update user when not a member of a group that requires two factor authentication' do
member_other.destroy!
other_group = create_namespace('other_group', Gitlab::VisibilityLevel::PRIVATE, require_two_factor_authentication: false)
create_group_member(user_other, other_group)
subject.perform(user_1.id, user_other.id)
expect(user_other.reload.require_two_factor_authentication_from_group).to eq(false)
end
end
end
def create_user(email, require_2fa: false)
users_table.create!(email: email, projects_limit: 10, require_two_factor_authentication_from_group: require_2fa)
end
def create_group_member(user, group)
members_table.create!(user_id: user.id, source_id: group.id, access_level: GroupMember::MAINTAINER, source_type: "Namespace", type: "GroupMember", notification_level: 3)
end
end

View File

@ -128,7 +128,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Environment do
context 'when environment has already been created' do
before do
create(:environment, :staging, project: project, name: 'customer-portal')
create(:environment, project: project, name: 'customer-portal', tier: :staging)
end
it 'does not overwrite the specified deployment tier' do

View File

@ -433,6 +433,13 @@ RSpec.describe Gitlab::GitAccess do
expect { pull_access_check }.to raise_forbidden("Your account has been deactivated by your administrator. Please log back in from a web browser to reactivate your account at #{Gitlab.config.gitlab.url}")
end
it 'disallows users with expired password to pull' do
project.add_maintainer(user)
user.update!(password_expires_at: 2.minutes.ago)
expect { pull_access_check }.to raise_forbidden("Your password expired. Please access GitLab from a web browser to update your password.")
end
context 'when the project repository does not exist' do
before do
project.add_guest(user)
@ -969,6 +976,13 @@ RSpec.describe Gitlab::GitAccess do
expect { push_access_check }.to raise_forbidden("Your account has been deactivated by your administrator. Please log back in from a web browser to reactivate your account at #{Gitlab.config.gitlab.url}")
end
it 'disallows users with expired password to push' do
project.add_maintainer(user)
user.update!(password_expires_at: 2.minutes.ago)
expect { push_access_check }.to raise_forbidden("Your password expired. Please access GitLab from a web browser to update your password.")
end
it 'cleans up the files' do
expect(project.repository).to receive(:clean_stale_repository_files).and_call_original
expect { push_access_check }.not_to raise_error

View File

@ -624,6 +624,7 @@ metrics_setting:
- project
protected_environments:
- project
- group
- deploy_access_levels
deploy_access_levels:
- protected_environment

View File

@ -701,6 +701,7 @@ ProjectSetting:
ProtectedEnvironment:
- id
- project_id
- group_id
- name
- created_at
- updated_at

Some files were not shown because too many files have changed in this diff Show More