summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/api/branches.rb20
-rw-r--r--lib/api/commits.rb18
-rw-r--r--lib/api/entities.rb45
-rw-r--r--lib/api/helpers.rb2
-rw-r--r--lib/api/issues.rb3
-rw-r--r--lib/api/merge_requests.rb8
-rw-r--r--lib/api/notes.rb2
-rw-r--r--lib/api/repositories.rb4
-rw-r--r--lib/api/tags.rb12
-rw-r--r--lib/api/templates.rb8
-rw-r--r--lib/api/users.rb10
-rw-r--r--lib/api/v3/branches.rb8
-rw-r--r--lib/api/v3/commits.rb16
-rw-r--r--lib/api/v3/entities.rb4
-rw-r--r--lib/api/v3/merge_requests.rb4
-rw-r--r--lib/api/v3/repositories.rb4
-rw-r--r--lib/api/v3/tags.rb4
-rw-r--r--lib/api/v3/templates.rb8
-rw-r--r--lib/banzai/renderer.rb7
-rw-r--r--lib/declarative_policy/rule.rb20
-rw-r--r--lib/declarative_policy/runner.rb31
-rw-r--r--lib/github/import.rb46
-rw-r--r--lib/github/representation/branch.rb20
-rw-r--r--lib/github/representation/issuable.rb12
-rw-r--r--lib/github/representation/issue.rb20
-rw-r--r--lib/github/representation/pull_request.rb75
-rw-r--r--lib/gitlab/background_migration/create_fork_network_memberships_range.rb65
-rw-r--r--lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys.rb53
-rw-r--r--lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb313
-rw-r--r--lib/gitlab/background_migration/populate_fork_networks_range.rb59
-rw-r--r--lib/gitlab/ci/ansi2html.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/config.rb2
-rw-r--r--lib/gitlab/ci/stage/seed.rb2
-rw-r--r--lib/gitlab/ci/trace.rb6
-rw-r--r--lib/gitlab/ci/trace/section_parser.rb97
-rw-r--r--lib/gitlab/ci/trace/stream.rb17
-rw-r--r--lib/gitlab/closing_issue_extractor.rb3
-rw-r--r--lib/gitlab/data_builder/push.rb2
-rw-r--r--lib/gitlab/database.rb9
-rw-r--r--lib/gitlab/diff/file.rb29
-rw-r--r--lib/gitlab/diff/formatters/base_formatter.rb61
-rw-r--r--lib/gitlab/diff/formatters/image_formatter.rb43
-rw-r--r--lib/gitlab/diff/formatters/text_formatter.rb49
-rw-r--r--lib/gitlab/diff/image_point.rb23
-rw-r--r--lib/gitlab/diff/position.rb90
-rw-r--r--lib/gitlab/ee_compat_check.rb6
-rw-r--r--lib/gitlab/gcp/model.rb13
-rw-r--r--lib/gitlab/git/repository.rb4
-rw-r--r--lib/gitlab/git_access.rb9
-rw-r--r--lib/gitlab/git_access_wiki.rb5
-rw-r--r--lib/gitlab/git_ref_validator.rb2
-rw-r--r--lib/gitlab/gitaly_client.rb21
-rw-r--r--lib/gitlab/gitaly_client/namespace_service.rb39
-rw-r--r--lib/gitlab/gpg.rb15
-rw-r--r--lib/gitlab/gpg/commit.rb10
-rw-r--r--lib/gitlab/gpg/invalid_gpg_signature_updater.rb4
-rw-r--r--lib/gitlab/hook_data/issuable_builder.rb56
-rw-r--r--lib/gitlab/hook_data/issue_builder.rb55
-rw-r--r--lib/gitlab/hook_data/merge_request_builder.rb62
-rw-r--r--lib/gitlab/import_export/import_export.yml1
-rw-r--r--lib/gitlab/import_export/relation_factory.rb2
-rw-r--r--lib/gitlab/ldap/auth_hash.rb4
-rw-r--r--lib/gitlab/ldap/dn.rb301
-rw-r--r--lib/gitlab/ldap/person.rb24
-rw-r--r--lib/gitlab/middleware/read_only.rb88
-rw-r--r--lib/gitlab/o_auth/user.rb2
-rw-r--r--lib/gitlab/path_regex.rb1
-rw-r--r--lib/gitlab/regex.rb4
-rw-r--r--lib/gitlab/shell.rb50
-rw-r--r--lib/gitlab/usage_data.rb1
-rw-r--r--lib/gitlab/workhorse.rb46
-rw-r--r--lib/google_api/auth.rb54
-rw-r--r--lib/google_api/cloud_platform/client.rb88
-rw-r--r--lib/rspec_flaky/config.rb21
-rw-r--r--lib/rspec_flaky/flaky_example.rb21
-rw-r--r--lib/rspec_flaky/flaky_examples_collection.rb37
-rw-r--r--lib/rspec_flaky/listener.rb63
-rw-r--r--lib/system_check/app/git_user_default_ssh_config_check.rb4
-rw-r--r--lib/tasks/gitlab/dev.rake5
-rw-r--r--lib/tasks/gitlab/gitaly.rake7
-rw-r--r--lib/tasks/import.rake27
81 files changed, 2113 insertions, 375 deletions
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 643c8e6fb8e..61a2d688282 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -13,7 +13,7 @@ module API
end
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get a project repository branches' do
- success Entities::RepoBranch
+ success Entities::Branch
end
params do
use :pagination
@@ -23,13 +23,13 @@ module API
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37442
Gitlab::GitalyClient.allow_n_plus_1_calls do
- present paginate(branches), with: Entities::RepoBranch, project: user_project
+ present paginate(branches), with: Entities::Branch, project: user_project
end
end
resource ':id/repository/branches/:branch', requirements: BRANCH_ENDPOINT_REQUIREMENTS do
desc 'Get a single branch' do
- success Entities::RepoBranch
+ success Entities::Branch
end
params do
requires :branch, type: String, desc: 'The name of the branch'
@@ -41,7 +41,7 @@ module API
branch = user_project.repository.find_branch(params[:branch])
not_found!('Branch') unless branch
- present branch, with: Entities::RepoBranch, project: user_project
+ present branch, with: Entities::Branch, project: user_project
end
end
@@ -50,7 +50,7 @@ module API
# in `gitlab-org/gitlab-ce!5081`. The API interface has not been changed (to maintain compatibility),
# but it works with the changed data model to infer `developers_can_merge` and `developers_can_push`.
desc 'Protect a single branch' do
- success Entities::RepoBranch
+ success Entities::Branch
end
params do
requires :branch, type: String, desc: 'The name of the branch'
@@ -80,7 +80,7 @@ module API
end
if protected_branch.valid?
- present branch, with: Entities::RepoBranch, project: user_project
+ present branch, with: Entities::Branch, project: user_project
else
render_api_error!(protected_branch.errors.full_messages, 422)
end
@@ -88,7 +88,7 @@ module API
# Note: This API will be deprecated in favor of the protected branches API.
desc 'Unprotect a single branch' do
- success Entities::RepoBranch
+ success Entities::Branch
end
params do
requires :branch, type: String, desc: 'The name of the branch'
@@ -101,11 +101,11 @@ module API
protected_branch = user_project.protected_branches.find_by(name: branch.name)
protected_branch&.destroy
- present branch, with: Entities::RepoBranch, project: user_project
+ present branch, with: Entities::Branch, project: user_project
end
desc 'Create branch' do
- success Entities::RepoBranch
+ success Entities::Branch
end
params do
requires :branch, type: String, desc: 'The name of the branch'
@@ -119,7 +119,7 @@ module API
if result[:status] == :success
present result[:branch],
- with: Entities::RepoBranch,
+ with: Entities::Branch,
project: user_project
else
render_api_error!(result[:message], 400)
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 4b8d248f5f7..4af37a2ad1d 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -13,7 +13,7 @@ module API
end
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get a project repository commits' do
- success Entities::RepoCommit
+ success Entities::Commit
end
params do
optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
@@ -46,11 +46,11 @@ module API
paginated_commits = Kaminari.paginate_array(commits, total_count: commit_count)
- present paginate(paginated_commits), with: Entities::RepoCommit
+ present paginate(paginated_commits), with: Entities::Commit
end
desc 'Commit multiple file changes as one commit' do
- success Entities::RepoCommitDetail
+ success Entities::CommitDetail
detail 'This feature was introduced in GitLab 8.13'
end
params do
@@ -72,14 +72,14 @@ module API
if result[:status] == :success
commit_detail = user_project.repository.commit(result[:result])
- present commit_detail, with: Entities::RepoCommitDetail
+ present commit_detail, with: Entities::CommitDetail
else
render_api_error!(result[:message], 400)
end
end
desc 'Get a specific commit of a project' do
- success Entities::RepoCommitDetail
+ success Entities::CommitDetail
failure [[404, 'Commit Not Found']]
end
params do
@@ -90,7 +90,7 @@ module API
not_found! 'Commit' unless commit
- present commit, with: Entities::RepoCommitDetail
+ present commit, with: Entities::CommitDetail
end
desc 'Get the diff for a specific commit of a project' do
@@ -104,7 +104,7 @@ module API
not_found! 'Commit' unless commit
- present commit.raw_diffs.to_a, with: Entities::RepoDiff
+ present commit.raw_diffs.to_a, with: Entities::Diff
end
desc "Get a commit's comments" do
@@ -126,7 +126,7 @@ module API
desc 'Cherry pick commit into a branch' do
detail 'This feature was introduced in GitLab 8.15'
- success Entities::RepoCommit
+ success Entities::Commit
end
params do
requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag to be cherry picked'
@@ -151,7 +151,7 @@ module API
if result[:status] == :success
branch = user_project.repository.find_branch(params[:branch])
- present user_project.repository.commit(branch.dereferenced_target), with: Entities::RepoCommit
+ present user_project.repository.commit(branch.dereferenced_target), with: Entities::Commit
else
render_api_error!(result[:message], 400)
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 7082f31b5b8..5f0bad14839 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -220,7 +220,7 @@ module API
expose :shared_projects, using: Entities::Project
end
- class RepoCommit < Grape::Entity
+ class Commit < Grape::Entity
expose :id, :short_id, :title, :created_at
expose :parent_ids
expose :safe_message, as: :message
@@ -228,20 +228,20 @@ module API
expose :committer_name, :committer_email, :committed_date
end
- class RepoCommitStats < Grape::Entity
+ class CommitStats < Grape::Entity
expose :additions, :deletions, :total
end
- class RepoCommitDetail < RepoCommit
- expose :stats, using: Entities::RepoCommitStats
+ class CommitDetail < Commit
+ expose :stats, using: Entities::CommitStats
expose :status
expose :last_pipeline, using: 'API::Entities::PipelineBasic'
end
- class RepoBranch < Grape::Entity
+ class Branch < Grape::Entity
expose :name
- expose :commit, using: Entities::RepoCommit do |repo_branch, options|
+ expose :commit, using: Entities::Commit do |repo_branch, options|
options[:project].repository.commit(repo_branch.dereferenced_target)
end
@@ -265,7 +265,7 @@ module API
end
end
- class RepoTreeObject < Grape::Entity
+ class TreeObject < Grape::Entity
expose :id, :name, :type, :path
expose :mode do |obj, options|
@@ -305,7 +305,7 @@ module API
expose :state, :created_at, :updated_at
end
- class RepoDiff < Grape::Entity
+ class Diff < Grape::Entity
expose :old_path, :new_path, :a_mode, :b_mode
expose :new_file?, as: :new_file
expose :renamed_file?, as: :renamed_file
@@ -368,6 +368,7 @@ module API
end
expose :due_date
expose :confidential
+ expose :discussion_locked
expose :web_url do |issue, options|
Gitlab::UrlBuilder.build(issue)
@@ -464,6 +465,7 @@ module API
expose :diff_head_sha, as: :sha
expose :merge_commit_sha
expose :user_notes_count
+ expose :discussion_locked
expose :should_remove_source_branch?, as: :should_remove_source_branch
expose :force_remove_source_branch?, as: :force_remove_source_branch
@@ -483,7 +485,7 @@ module API
end
class MergeRequestChanges < MergeRequest
- expose :diffs, as: :changes, using: Entities::RepoDiff do |compare, _|
+ expose :diffs, as: :changes, using: Entities::Diff do |compare, _|
compare.raw_diffs(limits: false).to_a
end
end
@@ -494,9 +496,9 @@ module API
end
class MergeRequestDiffFull < MergeRequestDiff
- expose :commits, using: Entities::RepoCommit
+ expose :commits, using: Entities::Commit
- expose :diffs, using: Entities::RepoDiff do |compare, _|
+ expose :diffs, using: Entities::Diff do |compare, _|
compare.raw_diffs(limits: false).to_a
end
end
@@ -592,8 +594,7 @@ module API
expose :target_type
expose :target do |todo, options|
- target = todo.target_type == 'Commit' ? 'RepoCommit' : todo.target_type
- Entities.const_get(target).represent(todo.target, options)
+ Entities.const_get(todo.target_type).represent(todo.target, options)
end
expose :target_url do |todo, options|
@@ -729,15 +730,15 @@ module API
end
class Compare < Grape::Entity
- expose :commit, using: Entities::RepoCommit do |compare, options|
- Commit.decorate(compare.commits, nil).last
+ expose :commit, using: Entities::Commit do |compare, options|
+ ::Commit.decorate(compare.commits, nil).last
end
- expose :commits, using: Entities::RepoCommit do |compare, options|
- Commit.decorate(compare.commits, nil)
+ expose :commits, using: Entities::Commit do |compare, options|
+ ::Commit.decorate(compare.commits, nil)
end
- expose :diffs, using: Entities::RepoDiff do |compare, options|
+ expose :diffs, using: Entities::Diff do |compare, options|
compare.diffs(limits: false).to_a
end
@@ -773,10 +774,10 @@ module API
expose :description
end
- class RepoTag < Grape::Entity
+ class Tag < Grape::Entity
expose :name, :message
- expose :commit, using: Entities::RepoCommit do |repo_tag, options|
+ expose :commit, using: Entities::Commit do |repo_tag, options|
options[:project].repository.commit(repo_tag.dereferenced_target)
end
@@ -827,7 +828,7 @@ module API
expose :created_at, :started_at, :finished_at
expose :user, with: User
expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? }
- expose :commit, with: RepoCommit
+ expose :commit, with: Commit
expose :runner, with: Runner
expose :pipeline, with: PipelineBasic
end
@@ -880,7 +881,7 @@ module API
expose :deployable, using: Entities::Job
end
- class RepoLicense < Grape::Entity
+ class License < Grape::Entity
expose :key, :name, :nickname
expose :featured, as: :popular
expose :url, as: :html_url
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 4964a76bef6..a87297a604c 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -287,7 +287,7 @@ module API
if sentry_enabled? && report_exception?(exception)
define_params_for_grape_middleware
sentry_context
- Raven.capture_exception(exception)
+ Raven.capture_exception(exception, extra: params)
end
# lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 1729df2aad0..0df41dcc903 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -48,6 +48,7 @@ module API
optional :labels, type: String, desc: 'Comma-separated list of label names'
optional :due_date, type: String, desc: 'Date string in the format YEAR-MONTH-DAY'
optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential'
+ optional :discussion_locked, type: Boolean, desc: " Boolean parameter indicating if the issue's discussion is locked"
end
params :issue_params do
@@ -193,7 +194,7 @@ module API
desc: 'Date time when the issue was updated. Available only for admins and project owners.'
optional :state_event, type: String, values: %w[reopen close], desc: 'State of the issue'
use :issue_params
- at_least_one_of :title, :description, :assignee_ids, :assignee_id, :milestone_id,
+ at_least_one_of :title, :description, :assignee_ids, :assignee_id, :milestone_id, :discussion_locked,
:labels, :created_at, :due_date, :confidential, :state_event
end
put ':id/issues/:issue_iid' do
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 8aa1e0216ee..be843ec8251 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -183,13 +183,13 @@ module API
end
desc 'Get the commits of a merge request' do
- success Entities::RepoCommit
+ success Entities::Commit
end
get ':id/merge_requests/:merge_request_iid/commits' do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
commits = ::Kaminari.paginate_array(merge_request.commits)
- present paginate(commits), with: Entities::RepoCommit
+ present paginate(commits), with: Entities::Commit
end
desc 'Show the merge request changes' do
@@ -214,12 +214,14 @@ module API
:remove_source_branch,
:state_event,
:target_branch,
- :title
+ :title,
+ :discussion_locked
]
optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
optional :target_branch, type: String, allow_blank: false, desc: 'The target branch'
optional :state_event, type: String, values: %w[close reopen],
desc: 'Status of the merge request'
+ optional :discussion_locked, type: Boolean, desc: 'Whether the MR discussion is locked'
use :optional_params
at_least_one_of(*at_least_one_of_ce)
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index d6e7203adaf..0b9ab4eeb05 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -78,6 +78,8 @@ module API
}
if can?(current_user, noteable_read_ability_name(noteable), noteable)
+ authorize! :create_note, noteable
+
if params[:created_at] && (current_user.admin? || user_project.owner == current_user)
opts[:created_at] = params[:created_at]
end
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index 2255fb1b70d..ceee3226732 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -35,7 +35,7 @@ module API
end
desc 'Get a project repository tree' do
- success Entities::RepoTreeObject
+ success Entities::TreeObject
end
params do
optional :ref, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
@@ -52,7 +52,7 @@ module API
tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive])
entries = ::Kaminari.paginate_array(tree.sorted_entries)
- present paginate(entries), with: Entities::RepoTreeObject
+ present paginate(entries), with: Entities::TreeObject
end
desc 'Get raw blob contents from the repository'
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index 912415e3a7f..0d394a7b441 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -11,18 +11,18 @@ module API
end
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get a project repository tags' do
- success Entities::RepoTag
+ success Entities::Tag
end
params do
use :pagination
end
get ':id/repository/tags' do
tags = ::Kaminari.paginate_array(user_project.repository.tags.sort_by(&:name).reverse)
- present paginate(tags), with: Entities::RepoTag, project: user_project
+ present paginate(tags), with: Entities::Tag, project: user_project
end
desc 'Get a single repository tag' do
- success Entities::RepoTag
+ success Entities::Tag
end
params do
requires :tag_name, type: String, desc: 'The name of the tag'
@@ -31,11 +31,11 @@ module API
tag = user_project.repository.find_tag(params[:tag_name])
not_found!('Tag') unless tag
- present tag, with: Entities::RepoTag, project: user_project
+ present tag, with: Entities::Tag, project: user_project
end
desc 'Create a new repository tag' do
- success Entities::RepoTag
+ success Entities::Tag
end
params do
requires :tag_name, type: String, desc: 'The name of the tag'
@@ -51,7 +51,7 @@ module API
if result[:status] == :success
present result[:tag],
- with: Entities::RepoTag,
+ with: Entities::Tag,
project: user_project
else
render_api_error!(result[:message], 400)
diff --git a/lib/api/templates.rb b/lib/api/templates.rb
index f70bc0622b7..6550b331fb8 100644
--- a/lib/api/templates.rb
+++ b/lib/api/templates.rb
@@ -49,7 +49,7 @@ module API
desc 'Get the list of the available license template' do
detail 'This feature was introduced in GitLab 8.7.'
- success ::API::Entities::RepoLicense
+ success ::API::Entities::License
end
params do
optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses'
@@ -60,12 +60,12 @@ module API
featured: declared(params)[:popular].present? ? true : nil
}
licences = ::Kaminari.paginate_array(Licensee::License.all(options))
- present paginate(licences), with: Entities::RepoLicense
+ present paginate(licences), with: Entities::License
end
desc 'Get the text for a specific license' do
detail 'This feature was introduced in GitLab 8.7.'
- success ::API::Entities::RepoLicense
+ success ::API::Entities::License
end
params do
requires :name, type: String, desc: 'The name of the template'
@@ -75,7 +75,7 @@ module API
template = parsed_license_template
- present template, with: ::API::Entities::RepoLicense
+ present template, with: ::API::Entities::License
end
GLOBAL_TEMPLATE_TYPES.each do |template_type, properties|
diff --git a/lib/api/users.rb b/lib/api/users.rb
index d07dc302717..b6f97a1eac2 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -331,7 +331,6 @@ module API
email = Emails::CreateService.new(current_user, declared_params(include_missing: false).merge(user: user)).execute
if email.errors.blank?
- NotificationService.new.new_email(email)
present email, with: Entities::Email
else
render_validation_error!(email)
@@ -369,10 +368,8 @@ module API
not_found!('Email') unless email
destroy_conditionally!(email) do |email|
- Emails::DestroyService.new(current_user, user: user, email: email.email).execute
+ Emails::DestroyService.new(current_user, user: user).execute(email)
end
-
- user.update_secondary_emails!
end
desc 'Delete a user. Available only for admins.' do
@@ -677,7 +674,6 @@ module API
email = Emails::CreateService.new(current_user, declared_params.merge(user: current_user)).execute
if email.errors.blank?
- NotificationService.new.new_email(email)
present email, with: Entities::Email
else
render_validation_error!(email)
@@ -693,10 +689,8 @@ module API
not_found!('Email') unless email
destroy_conditionally!(email) do |email|
- Emails::DestroyService.new(current_user, user: current_user, email: email.email).execute
+ Emails::DestroyService.new(current_user, user: current_user).execute(email)
end
-
- current_user.update_secondary_emails!
end
desc 'Get a list of user activities'
diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb
index 81b13249892..69cd12de72c 100644
--- a/lib/api/v3/branches.rb
+++ b/lib/api/v3/branches.rb
@@ -11,12 +11,12 @@ module API
end
resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository branches' do
- success ::API::Entities::RepoBranch
+ success ::API::Entities::Branch
end
get ":id/repository/branches" do
branches = user_project.repository.branches.sort_by(&:name)
- present branches, with: ::API::Entities::RepoBranch, project: user_project
+ present branches, with: ::API::Entities::Branch, project: user_project
end
desc 'Delete a branch'
@@ -47,7 +47,7 @@ module API
end
desc 'Create branch' do
- success ::API::Entities::RepoBranch
+ success ::API::Entities::Branch
end
params do
requires :branch_name, type: String, desc: 'The name of the branch'
@@ -60,7 +60,7 @@ module API
if result[:status] == :success
present result[:branch],
- with: ::API::Entities::RepoBranch,
+ with: ::API::Entities::Branch,
project: user_project
else
render_api_error!(result[:message], 400)
diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb
index 5936f4700aa..345cb7e7c11 100644
--- a/lib/api/v3/commits.rb
+++ b/lib/api/v3/commits.rb
@@ -13,7 +13,7 @@ module API
end
resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository commits' do
- success ::API::Entities::RepoCommit
+ success ::API::Entities::Commit
end
params do
optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
@@ -34,11 +34,11 @@ module API
after: params[:since],
before: params[:until])
- present commits, with: ::API::Entities::RepoCommit
+ present commits, with: ::API::Entities::Commit
end
desc 'Commit multiple file changes as one commit' do
- success ::API::Entities::RepoCommitDetail
+ success ::API::Entities::CommitDetail
detail 'This feature was introduced in GitLab 8.13'
end
params do
@@ -59,14 +59,14 @@ module API
if result[:status] == :success
commit_detail = user_project.repository.commits(result[:result], limit: 1).first
- present commit_detail, with: ::API::Entities::RepoCommitDetail
+ present commit_detail, with: ::API::Entities::CommitDetail
else
render_api_error!(result[:message], 400)
end
end
desc 'Get a specific commit of a project' do
- success ::API::Entities::RepoCommitDetail
+ success ::API::Entities::CommitDetail
failure [[404, 'Not Found']]
end
params do
@@ -77,7 +77,7 @@ module API
not_found! "Commit" unless commit
- present commit, with: ::API::Entities::RepoCommitDetail
+ present commit, with: ::API::Entities::CommitDetail
end
desc 'Get the diff for a specific commit of a project' do
@@ -113,7 +113,7 @@ module API
desc 'Cherry pick commit into a branch' do
detail 'This feature was introduced in GitLab 8.15'
- success ::API::Entities::RepoCommit
+ success ::API::Entities::Commit
end
params do
requires :sha, type: String, desc: 'A commit sha to be cherry picked'
@@ -138,7 +138,7 @@ module API
if result[:status] == :success
branch = user_project.repository.find_branch(params[:branch])
- present user_project.repository.commit(branch.dereferenced_target), with: ::API::Entities::RepoCommit
+ present user_project.repository.commit(branch.dereferenced_target), with: ::API::Entities::Commit
else
render_api_error!(result[:message], 400)
end
diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb
index c928ce5265b..afdd7b83998 100644
--- a/lib/api/v3/entities.rb
+++ b/lib/api/v3/entities.rb
@@ -220,7 +220,7 @@ module API
expose :created_at, :started_at, :finished_at
expose :user, with: ::API::Entities::User
expose :artifacts_file, using: ::API::Entities::JobArtifactFile, if: -> (build, opts) { build.artifacts? }
- expose :commit, with: ::API::Entities::RepoCommit
+ expose :commit, with: ::API::Entities::Commit
expose :runner, with: ::API::Entities::Runner
expose :pipeline, with: ::API::Entities::PipelineBasic
end
@@ -237,7 +237,7 @@ module API
end
class MergeRequestChanges < MergeRequest
- expose :diffs, as: :changes, using: ::API::Entities::RepoDiff do |compare, _|
+ expose :diffs, as: :changes, using: ::API::Entities::Diff do |compare, _|
compare.raw_diffs(limits: false).to_a
end
end
diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb
index b6b7254ae29..1d6d823f32b 100644
--- a/lib/api/v3/merge_requests.rb
+++ b/lib/api/v3/merge_requests.rb
@@ -135,12 +135,12 @@ module API
end
desc 'Get the commits of a merge request' do
- success ::API::Entities::RepoCommit
+ success ::API::Entities::Commit
end
get "#{path}/commits" do
merge_request = find_merge_request_with_access(params[:merge_request_id])
- present merge_request.commits, with: ::API::Entities::RepoCommit
+ present merge_request.commits, with: ::API::Entities::Commit
end
desc 'Show the merge request changes' do
diff --git a/lib/api/v3/repositories.rb b/lib/api/v3/repositories.rb
index 0eaa0de2eef..41a7c6b83ae 100644
--- a/lib/api/v3/repositories.rb
+++ b/lib/api/v3/repositories.rb
@@ -19,7 +19,7 @@ module API
end
desc 'Get a project repository tree' do
- success ::API::Entities::RepoTreeObject
+ success ::API::Entities::TreeObject
end
params do
optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
@@ -35,7 +35,7 @@ module API
tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive])
- present tree.sorted_entries, with: ::API::Entities::RepoTreeObject
+ present tree.sorted_entries, with: ::API::Entities::TreeObject
end
desc 'Get a raw file contents'
diff --git a/lib/api/v3/tags.rb b/lib/api/v3/tags.rb
index 7e5875cd030..6e37d31d153 100644
--- a/lib/api/v3/tags.rb
+++ b/lib/api/v3/tags.rb
@@ -8,11 +8,11 @@ module API
end
resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository tags' do
- success ::API::Entities::RepoTag
+ success ::API::Entities::Tag
end
get ":id/repository/tags" do
tags = user_project.repository.tags.sort_by(&:name).reverse
- present tags, with: ::API::Entities::RepoTag, project: user_project
+ present tags, with: ::API::Entities::Tag, project: user_project
end
desc 'Delete a repository tag'
diff --git a/lib/api/v3/templates.rb b/lib/api/v3/templates.rb
index 2a2fb59045c..7298203df10 100644
--- a/lib/api/v3/templates.rb
+++ b/lib/api/v3/templates.rb
@@ -52,7 +52,7 @@ module API
detailed_desc = 'This feature was introduced in GitLab 8.7.'
detailed_desc << DEPRECATION_MESSAGE unless status == :ok
detail detailed_desc
- success ::API::Entities::RepoLicense
+ success ::API::Entities::License
end
params do
optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses'
@@ -61,7 +61,7 @@ module API
options = {
featured: declared(params)[:popular].present? ? true : nil
}
- present Licensee::License.all(options), with: ::API::Entities::RepoLicense
+ present Licensee::License.all(options), with: ::API::Entities::License
end
end
@@ -70,7 +70,7 @@ module API
detailed_desc = 'This feature was introduced in GitLab 8.7.'
detailed_desc << DEPRECATION_MESSAGE unless status == :ok
detail detailed_desc
- success ::API::Entities::RepoLicense
+ success ::API::Entities::License
end
params do
requires :name, type: String, desc: 'The name of the template'
@@ -80,7 +80,7 @@ module API
template = parsed_license_template
- present template, with: ::API::Entities::RepoLicense
+ present template, with: ::API::Entities::License
end
end
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
index ceca9296851..5f91884a878 100644
--- a/lib/banzai/renderer.rb
+++ b/lib/banzai/renderer.rb
@@ -40,7 +40,7 @@ module Banzai
return cacheless_render_field(object, field)
end
- object.refresh_markdown_cache!(do_update: update_object?(object)) unless object.cached_html_up_to_date?(field)
+ object.refresh_markdown_cache! unless object.cached_html_up_to_date?(field)
object.cached_html_for(field)
end
@@ -162,10 +162,5 @@ module Banzai
return unless cache_key
Rails.cache.__send__(:expanded_key, full_cache_key(cache_key, pipeline_name)) # rubocop:disable GitlabSecurity/PublicSend
end
-
- # GitLab EE needs to disable updates on GET requests in Geo
- def self.update_object?(object)
- true
- end
end
end
diff --git a/lib/declarative_policy/rule.rb b/lib/declarative_policy/rule.rb
index bfcec241489..7cfa82a9a9f 100644
--- a/lib/declarative_policy/rule.rb
+++ b/lib/declarative_policy/rule.rb
@@ -206,11 +206,13 @@ module DeclarativePolicy
end
def cached_pass?(context)
- passes = @rules.map { |r| r.cached_pass?(context) }
- return false if passes.any? { |p| p == false }
- return true if passes.all? { |p| p == true }
+ @rules.each do |rule|
+ pass = rule.cached_pass?(context)
- nil
+ return pass if pass.nil? || pass == false
+ end
+
+ true
end
def repr
@@ -245,11 +247,13 @@ module DeclarativePolicy
end
def cached_pass?(context)
- passes = @rules.map { |r| r.cached_pass?(context) }
- return true if passes.any? { |p| p == true }
- return false if passes.all? { |p| p == false }
+ @rules.each do |rule|
+ pass = rule.cached_pass?(context)
- nil
+ return pass if pass.nil? || pass == true
+ end
+
+ false
end
def score(context)
diff --git a/lib/declarative_policy/runner.rb b/lib/declarative_policy/runner.rb
index 56afd1f1392..45ff2ef9ced 100644
--- a/lib/declarative_policy/runner.rb
+++ b/lib/declarative_policy/runner.rb
@@ -107,7 +107,7 @@ module DeclarativePolicy
end
# This is the core spot where all those `#score` methods matter.
- # It is critcal for performance to run steps in the correct order,
+ # It is critical for performance to run steps in the correct order,
# so that we don't compute expensive conditions (potentially n times
# if we're called on, say, a large list of users).
#
@@ -139,30 +139,39 @@ module DeclarativePolicy
return
end
- steps = Set.new(@steps)
- remaining_enablers = steps.count { |s| s.enable? }
+ remaining_steps = Set.new(@steps)
+ remaining_enablers, remaining_preventers = remaining_steps.partition(&:enable?).map { |s| Set.new(s) }
loop do
- return if steps.empty?
+ if @state.enabled?
+ # Once we set this, we never need to unset it, because a single
+ # prevent will stop this from being enabled
+ remaining_steps = remaining_preventers
+ else
+ # if the permission hasn't yet been enabled and we only have
+ # prevent steps left, we short-circuit the state here
+ @state.prevent! if remaining_enablers.empty?
+ end
- # if the permission hasn't yet been enabled and we only have
- # prevent steps left, we short-circuit the state here
- @state.prevent! if !@state.enabled? && remaining_enablers == 0
+ return if remaining_steps.empty?
lowest_score = Float::INFINITY
next_step = nil
- steps.each do |step|
+ remaining_steps.each do |step|
score = step.score
+
if score < lowest_score
next_step = step
lowest_score = score
end
- end
- steps.delete(next_step)
+ break if lowest_score.zero?
+ end
- remaining_enablers -= 1 if next_step.enable?
+ [remaining_steps, remaining_enablers, remaining_preventers].each do |set|
+ set.delete(next_step)
+ end
yield next_step, lowest_score
end
diff --git a/lib/github/import.rb b/lib/github/import.rb
index c0cd8382875..55f8387f27a 100644
--- a/lib/github/import.rb
+++ b/lib/github/import.rb
@@ -9,7 +9,7 @@ module Github
include Gitlab::ShellAdapter
attr_reader :project, :repository, :repo, :repo_url, :wiki_url,
- :options, :errors, :cached, :verbose
+ :options, :errors, :cached, :verbose, :last_fetched_at
def initialize(project, options = {})
@project = project
@@ -21,12 +21,13 @@ module Github
@verbose = options.fetch(:verbose, false)
@cached = Hash.new { |hash, key| hash[key] = Hash.new }
@errors = []
+ @last_fetched_at = nil
end
# rubocop: disable Rails/Output
def execute
puts 'Fetching repository...'.color(:aqua) if verbose
- fetch_repository
+ setup_and_fetch_repository
puts 'Fetching labels...'.color(:aqua) if verbose
fetch_labels
puts 'Fetching milestones...'.color(:aqua) if verbose
@@ -42,7 +43,7 @@ module Github
puts 'Expiring repository cache...'.color(:aqua) if verbose
expire_repository_cache
- true
+ errors.empty?
rescue Github::RepositoryFetchError
expire_repository_cache
false
@@ -52,18 +53,24 @@ module Github
private
- def fetch_repository
+ def setup_and_fetch_repository
begin
project.ensure_repository
project.repository.add_remote('github', repo_url)
- project.repository.set_remote_as_mirror('github')
- project.repository.fetch_remote('github', forced: true)
+ project.repository.set_import_remote_as_mirror('github')
+ project.repository.add_remote_fetch_config('github', '+refs/pull/*/head:refs/merge-requests/*/head')
+ fetch_remote(forced: true)
rescue Gitlab::Git::Repository::NoRepository, Gitlab::Shell::Error => e
error(:project, repo_url, e.message)
raise Github::RepositoryFetchError
end
end
+ def fetch_remote(forced: false)
+ @last_fetched_at = Time.now
+ project.repository.fetch_remote('github', forced: forced)
+ end
+
def fetch_wiki_repository
return if project.wiki.repository_exists?
@@ -92,7 +99,7 @@ module Github
label.color = representation.color
end
- cached[:label_ids][label.title] = label.id
+ cached[:label_ids][representation.title] = label.id
rescue => e
error(:label, representation.url, e.message)
end
@@ -143,7 +150,9 @@ module Github
next unless merge_request.new_record? && pull_request.valid?
begin
- pull_request.restore_branches!
+ # If the PR has been created/updated after we last fetched the
+ # remote, we fetch again to get the up-to-date refs.
+ fetch_remote if pull_request.updated_at > last_fetched_at
author_id = user_id(pull_request.author, project.creator_id)
description = format_description(pull_request.description, pull_request.author)
@@ -152,6 +161,7 @@ module Github
iid: pull_request.iid,
title: pull_request.title,
description: description,
+ ref_fetched: true,
source_project: pull_request.source_project,
source_branch: pull_request.source_branch_name,
source_branch_sha: pull_request.source_branch_sha,
@@ -173,8 +183,6 @@ module Github
fetch_comments(merge_request, :review_comment, review_comments_url, LegacyDiffNote)
rescue => e
error(:pull_request, pull_request.url, e.message)
- ensure
- pull_request.remove_restored_branches!
end
end
@@ -203,11 +211,11 @@ module Github
# for both features, like manipulating assignees, labels
# and milestones, are provided within the Issues API.
if representation.pull_request?
- return unless representation.has_labels? || representation.has_comments?
+ return unless representation.labels? || representation.comments?
merge_request = MergeRequest.find_by!(target_project_id: project.id, iid: representation.iid)
- if representation.has_labels?
+ if representation.labels?
merge_request.update_attribute(:label_ids, label_ids(representation.labels))
end
@@ -222,14 +230,16 @@ module Github
issue.title = representation.title
issue.description = format_description(representation.description, representation.author)
issue.state = representation.state
- issue.label_ids = label_ids(representation.labels)
issue.milestone_id = milestone_id(representation.milestone)
issue.author_id = author_id
- issue.assignee_ids = [user_id(representation.assignee)]
issue.created_at = representation.created_at
issue.updated_at = representation.updated_at
issue.save!(validate: false)
+ issue.update(
+ label_ids: label_ids(representation.labels),
+ assignee_ids: assignee_ids(representation.assignees))
+
fetch_comments_conditionally(issue, representation)
end
rescue => e
@@ -238,7 +248,7 @@ module Github
end
def fetch_comments_conditionally(issuable, representation)
- if representation.has_comments?
+ if representation.comments?
comments_url = "/repos/#{repo}/issues/#{issuable.iid}/comments"
fetch_comments(issuable, :comment, comments_url)
end
@@ -302,7 +312,11 @@ module Github
end
def label_ids(labels)
- labels.map { |attrs| cached[:label_ids][attrs.fetch('name')] }.compact
+ labels.map { |label| cached[:label_ids][label.title] }.compact
+ end
+
+ def assignee_ids(assignees)
+ assignees.map { |assignee| user_id(assignee) }.compact
end
def milestone_id(milestone)
diff --git a/lib/github/representation/branch.rb b/lib/github/representation/branch.rb
index 823e8e9a9c4..0087a3d3c4f 100644
--- a/lib/github/representation/branch.rb
+++ b/lib/github/representation/branch.rb
@@ -7,10 +7,14 @@ module Github
raw.dig('user', 'login') || 'unknown'
end
+ def repo?
+ raw['repo'].present?
+ end
+
def repo
- return @repo if defined?(@repo)
+ return unless repo?
- @repo = Github::Representation::Repo.new(raw['repo']) if raw['repo'].present?
+ @repo ||= Github::Representation::Repo.new(raw['repo'])
end
def ref
@@ -25,10 +29,6 @@ module Github
Commit.truncate_sha(sha)
end
- def exists?
- @exists ||= branch_exists? && commit_exists?
- end
-
def valid?
sha.present? && ref.present?
end
@@ -47,14 +47,6 @@ module Github
private
- def branch_exists?
- repository.branch_exists?(ref)
- end
-
- def commit_exists?
- repository.branch_names_contains(sha).include?(ref)
- end
-
def repository
@repository ||= options.fetch(:repository)
end
diff --git a/lib/github/representation/issuable.rb b/lib/github/representation/issuable.rb
index 9713b82615d..768ba3b993c 100644
--- a/lib/github/representation/issuable.rb
+++ b/lib/github/representation/issuable.rb
@@ -23,14 +23,14 @@ module Github
@author ||= Github::Representation::User.new(raw['user'], options)
end
- def assignee
- return unless assigned?
-
- @assignee ||= Github::Representation::User.new(raw['assignee'], options)
+ def labels?
+ raw['labels'].any?
end
- def assigned?
- raw['assignee'].present?
+ def labels
+ @labels ||= Array(raw['labels']).map do |label|
+ Github::Representation::Label.new(label, options)
+ end
end
end
end
diff --git a/lib/github/representation/issue.rb b/lib/github/representation/issue.rb
index df3540a6e6c..4f1a02cb90f 100644
--- a/lib/github/representation/issue.rb
+++ b/lib/github/representation/issue.rb
@@ -1,25 +1,27 @@
module Github
module Representation
class Issue < Representation::Issuable
- def labels
- raw['labels']
- end
-
def state
raw['state'] == 'closed' ? 'closed' : 'opened'
end
- def has_comments?
+ def comments?
raw['comments'] > 0
end
- def has_labels?
- labels.count > 0
- end
-
def pull_request?
raw['pull_request'].present?
end
+
+ def assigned?
+ raw['assignees'].present?
+ end
+
+ def assignees
+ @assignees ||= Array(raw['assignees']).map do |user|
+ Github::Representation::User.new(user, options)
+ end
+ end
end
end
end
diff --git a/lib/github/representation/pull_request.rb b/lib/github/representation/pull_request.rb
index 55461097e8a..0171179bb0f 100644
--- a/lib/github/representation/pull_request.rb
+++ b/lib/github/representation/pull_request.rb
@@ -1,26 +1,17 @@
module Github
module Representation
class PullRequest < Representation::Issuable
- delegate :user, :repo, :ref, :sha, to: :source_branch, prefix: true
- delegate :user, :exists?, :repo, :ref, :sha, :short_sha, to: :target_branch, prefix: true
+ delegate :sha, to: :source_branch, prefix: true
+ delegate :sha, to: :target_branch, prefix: true
def source_project
project
end
def source_branch_name
- @source_branch_name ||=
- if cross_project? || !source_branch_exists?
- source_branch_name_prefixed
- else
- source_branch_ref
- end
- end
-
- def source_branch_exists?
- return @source_branch_exists if defined?(@source_branch_exists)
-
- @source_branch_exists = !cross_project? && source_branch.exists?
+ # Mimic the "user:branch" displayed in the MR widget,
+ # i.e. "Request to merge rymai:add-external-mounts into master"
+ cross_project? ? "#{source_branch.user}:#{source_branch.ref}" : source_branch.ref
end
def target_project
@@ -28,11 +19,7 @@ module Github
end
def target_branch_name
- @target_branch_name ||= target_branch_exists? ? target_branch_ref : target_branch_name_prefixed
- end
-
- def target_branch_exists?
- @target_branch_exists ||= target_branch.exists?
+ target_branch.ref
end
def state
@@ -50,16 +37,14 @@ module Github
source_branch.valid? && target_branch.valid?
end
- def restore_branches!
- restore_source_branch!
- restore_target_branch!
+ def assigned?
+ raw['assignee'].present?
end
- def remove_restored_branches!
- return if opened?
+ def assignee
+ return unless assigned?
- remove_source_branch!
- remove_target_branch!
+ @assignee ||= Github::Representation::User.new(raw['assignee'], options)
end
private
@@ -72,48 +57,14 @@ module Github
@source_branch ||= Representation::Branch.new(raw['head'], repository: project.repository)
end
- def source_branch_name_prefixed
- "gh-#{target_branch_short_sha}/#{iid}/#{source_branch_user}/#{source_branch_ref}"
- end
-
def target_branch
@target_branch ||= Representation::Branch.new(raw['base'], repository: project.repository)
end
- def target_branch_name_prefixed
- "gl-#{target_branch_short_sha}/#{iid}/#{target_branch_user}/#{target_branch_ref}"
- end
-
def cross_project?
- return true if source_branch_repo.nil?
-
- source_branch_repo.id != target_branch_repo.id
- end
-
- def restore_source_branch!
- return if source_branch_exists?
-
- source_branch.restore!(source_branch_name)
- end
-
- def restore_target_branch!
- return if target_branch_exists?
-
- target_branch.restore!(target_branch_name)
- end
-
- def remove_source_branch!
- # We should remove the source/target branches only if they were
- # restored. Otherwise, we'll remove branches like 'master' that
- # target_branch_exists? returns true. In other words, we need
- # to clean up only the restored branches that (source|target)_branch_exists?
- # returns false for the first time it has been called, because of
- # this that is important to memoize these values.
- source_branch.remove!(source_branch_name) unless source_branch_exists?
- end
+ return true unless source_branch.repo?
- def remove_target_branch!
- target_branch.remove!(target_branch_name) unless target_branch_exists?
+ source_branch.repo.id != target_branch.repo.id
end
end
end
diff --git a/lib/gitlab/background_migration/create_fork_network_memberships_range.rb b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb
new file mode 100644
index 00000000000..c88eb9783ed
--- /dev/null
+++ b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb
@@ -0,0 +1,65 @@
+module Gitlab
+ module BackgroundMigration
+ class CreateForkNetworkMembershipsRange
+ RESCHEDULE_DELAY = 15
+
+ class ForkedProjectLink < ActiveRecord::Base
+ self.table_name = 'forked_project_links'
+ end
+
+ def perform(start_id, end_id)
+ log("Creating memberships for forks: #{start_id} - #{end_id}")
+
+ ActiveRecord::Base.connection.execute <<~INSERT_MEMBERS
+ INSERT INTO fork_network_members (fork_network_id, project_id, forked_from_project_id)
+
+ SELECT fork_network_members.fork_network_id,
+ forked_project_links.forked_to_project_id,
+ forked_project_links.forked_from_project_id
+
+ FROM forked_project_links
+
+ INNER JOIN fork_network_members
+ ON forked_project_links.forked_from_project_id = fork_network_members.project_id
+
+ WHERE forked_project_links.id BETWEEN #{start_id} AND #{end_id}
+ AND NOT EXISTS (
+ SELECT true
+ FROM fork_network_members existing_members
+ WHERE existing_members.project_id = forked_project_links.forked_to_project_id
+ )
+ INSERT_MEMBERS
+
+ if missing_members?(start_id, end_id)
+ BackgroundMigrationWorker.perform_in(RESCHEDULE_DELAY, "CreateForkNetworkMembershipsRange", [start_id, end_id])
+ end
+ end
+
+ def missing_members?(start_id, end_id)
+ count_sql = <<~MISSING_MEMBERS
+ SELECT COUNT(*)
+
+ FROM forked_project_links
+
+ WHERE NOT EXISTS (
+ SELECT true
+ FROM fork_network_members
+ WHERE fork_network_members.project_id = forked_project_links.forked_to_project_id
+ )
+ AND EXISTS (
+ SELECT true
+ FROM projects
+ WHERE forked_project_links.forked_from_project_id = projects.id
+ )
+ AND forked_project_links.id BETWEEN #{start_id} AND #{end_id}
+ MISSING_MEMBERS
+
+ ForkNetworkMember.count_by_sql(count_sql) > 0
+ end
+
+ def log(message)
+ Rails.logger.info("#{self.class.name} - #{message}")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys.rb b/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys.rb
new file mode 100644
index 00000000000..e94719db72e
--- /dev/null
+++ b/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys.rb
@@ -0,0 +1,53 @@
+class Gitlab::BackgroundMigration::CreateGpgKeySubkeysFromGpgKeys
+ class GpgKey < ActiveRecord::Base
+ self.table_name = 'gpg_keys'
+
+ include EachBatch
+ include ShaAttribute
+
+ sha_attribute :primary_keyid
+ sha_attribute :fingerprint
+
+ has_many :subkeys, class_name: 'GpgKeySubkey'
+ end
+
+ class GpgKeySubkey < ActiveRecord::Base
+ self.table_name = 'gpg_key_subkeys'
+
+ include ShaAttribute
+
+ sha_attribute :keyid
+ sha_attribute :fingerprint
+ end
+
+ def perform(gpg_key_id)
+ gpg_key = GpgKey.find_by(id: gpg_key_id)
+
+ return if gpg_key.nil?
+ return if gpg_key.subkeys.any?
+
+ create_subkeys(gpg_key)
+ update_signatures(gpg_key)
+ end
+
+ private
+
+ def create_subkeys(gpg_key)
+ gpg_subkeys = Gitlab::Gpg.subkeys_from_key(gpg_key.key)
+
+ gpg_subkeys[gpg_key.primary_keyid.upcase]&.each do |subkey_data|
+ gpg_key.subkeys.build(keyid: subkey_data[:keyid], fingerprint: subkey_data[:fingerprint])
+ end
+
+ # Improve latency by doing all INSERTs in a single call
+ GpgKey.transaction do
+ gpg_key.save!
+ end
+ end
+
+ def update_signatures(gpg_key)
+ return unless gpg_key.subkeys.exists?
+
+ InvalidGpgSignatureUpdateWorker.perform_async(gpg_key.id)
+ end
+end
diff --git a/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb b/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
new file mode 100644
index 00000000000..bc53e6d7f94
--- /dev/null
+++ b/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
@@ -0,0 +1,313 @@
+module Gitlab
+ module BackgroundMigration
+ class NormalizeLdapExternUidsRange
+ class Identity < ActiveRecord::Base
+ self.table_name = 'identities'
+ end
+
+ # Copied this class to make this migration resilient to future code changes.
+ # And if the normalize behavior is changed in the future, it must be
+ # accompanied by another migration.
+ module Gitlab
+ module LDAP
+ class DN
+ FormatError = Class.new(StandardError)
+ MalformedError = Class.new(FormatError)
+ UnsupportedError = Class.new(FormatError)
+
+ def self.normalize_value(given_value)
+ dummy_dn = "placeholder=#{given_value}"
+ normalized_dn = new(*dummy_dn).to_normalized_s
+ normalized_dn.sub(/\Aplaceholder=/, '')
+ end
+
+ ##
+ # Initialize a DN, escaping as required. Pass in attributes in name/value
+ # pairs. If there is a left over argument, it will be appended to the dn
+ # without escaping (useful for a base string).
+ #
+ # Most uses of this class will be to escape a DN, rather than to parse it,
+ # so storing the dn as an escaped String and parsing parts as required
+ # with a state machine seems sensible.
+ def initialize(*args)
+ if args.length > 1
+ initialize_array(args)
+ else
+ initialize_string(args[0])
+ end
+ end
+
+ ##
+ # Parse a DN into key value pairs using ASN from
+ # http://tools.ietf.org/html/rfc2253 section 3.
+ # rubocop:disable Metrics/AbcSize
+ # rubocop:disable Metrics/CyclomaticComplexity
+ # rubocop:disable Metrics/PerceivedComplexity
+ def each_pair
+ state = :key
+ key = StringIO.new
+ value = StringIO.new
+ hex_buffer = ""
+
+ @dn.each_char.with_index do |char, dn_index|
+ case state
+ when :key then
+ case char
+ when 'a'..'z', 'A'..'Z' then
+ state = :key_normal
+ key << char
+ when '0'..'9' then
+ state = :key_oid
+ key << char
+ when ' ' then state = :key
+ else raise(MalformedError, "Unrecognized first character of an RDN attribute type name \"#{char}\"")
+ end
+ when :key_normal then
+ case char
+ when '=' then state = :value
+ when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char
+ else raise(MalformedError, "Unrecognized RDN attribute type name character \"#{char}\"")
+ end
+ when :key_oid then
+ case char
+ when '=' then state = :value
+ when '0'..'9', '.', ' ' then key << char
+ else raise(MalformedError, "Unrecognized RDN OID attribute type name character \"#{char}\"")
+ end
+ when :value then
+ case char
+ when '\\' then state = :value_normal_escape
+ when '"' then state = :value_quoted
+ when ' ' then state = :value
+ when '#' then
+ state = :value_hexstring
+ value << char
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else
+ state = :value_normal
+ value << char
+ end
+ when :value_normal then
+ case char
+ when '\\' then state = :value_normal_escape
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ when '+' then raise(UnsupportedError, "Multivalued RDNs are not supported")
+ else value << char
+ end
+ when :value_normal_escape then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_normal_escape_hex
+ hex_buffer = char
+ else
+ state = :value_normal
+ value << char
+ end
+ when :value_normal_escape_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_normal
+ value << "#{hex_buffer}#{char}".to_i(16).chr
+ else raise(MalformedError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"")
+ end
+ when :value_quoted then
+ case char
+ when '\\' then state = :value_quoted_escape
+ when '"' then state = :value_end
+ else value << char
+ end
+ when :value_quoted_escape then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_quoted_escape_hex
+ hex_buffer = char
+ else
+ state = :value_quoted
+ value << char
+ end
+ when :value_quoted_escape_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_quoted
+ value << "#{hex_buffer}#{char}".to_i(16).chr
+ else raise(MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"#{char}\"")
+ end
+ when :value_hexstring then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_hexstring_hex
+ value << char
+ when ' ' then state = :value_end
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else raise(MalformedError, "Expected the first character of a hex pair, but got \"#{char}\"")
+ end
+ when :value_hexstring_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_hexstring
+ value << char
+ else raise(MalformedError, "Expected the second character of a hex pair, but got \"#{char}\"")
+ end
+ when :value_end then
+ case char
+ when ' ' then state = :value_end
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else raise(MalformedError, "Expected the end of an attribute value, but got \"#{char}\"")
+ end
+ else raise "Fell out of state machine"
+ end
+ end
+
+ # Last pair
+ raise(MalformedError, 'DN string ended unexpectedly') unless
+ [:value, :value_normal, :value_hexstring, :value_end].include? state
+
+ yield key.string.strip, rstrip_except_escaped(value.string, @dn.length)
+ end
+
+ def rstrip_except_escaped(str, dn_index)
+ str_ends_with_whitespace = str.match(/\s\z/)
+
+ if str_ends_with_whitespace
+ dn_part_ends_with_escaped_whitespace = @dn[0, dn_index].match(/\\(\s+)\z/)
+
+ if dn_part_ends_with_escaped_whitespace
+ dn_part_rwhitespace = dn_part_ends_with_escaped_whitespace[1]
+ num_chars_to_remove = dn_part_rwhitespace.length - 1
+ str = str[0, str.length - num_chars_to_remove]
+ else
+ str.rstrip!
+ end
+ end
+
+ str
+ end
+
+ ##
+ # Returns the DN as an array in the form expected by the constructor.
+ def to_a
+ a = []
+ self.each_pair { |key, value| a << key << value } unless @dn.empty?
+ a
+ end
+
+ ##
+ # Return the DN as an escaped string.
+ def to_s
+ @dn
+ end
+
+ ##
+ # Return the DN as an escaped and normalized string.
+ def to_normalized_s
+ self.class.new(*to_a).to_s.downcase
+ end
+
+ # https://tools.ietf.org/html/rfc4514 section 2.4 lists these exceptions
+ # for DN values. All of the following must be escaped in any normal string
+ # using a single backslash ('\') as escape. The space character is left
+ # out here because in a "normalized" string, spaces should only be escaped
+ # if necessary (i.e. leading or trailing space).
+ NORMAL_ESCAPES = [',', '+', '"', '\\', '<', '>', ';', '='].freeze
+
+ # The following must be represented as escaped hex
+ HEX_ESCAPES = {
+ "\n" => '\0a',
+ "\r" => '\0d'
+ }.freeze
+
+ # Compiled character class regexp using the keys from the above hash, and
+ # checking for a space or # at the start, or space at the end, of the
+ # string.
+ ESCAPE_RE = Regexp.new("(^ |^#| $|[" +
+ NORMAL_ESCAPES.map { |e| Regexp.escape(e) }.join +
+ "])")
+
+ HEX_ESCAPE_RE = Regexp.new("([" +
+ HEX_ESCAPES.keys.map { |e| Regexp.escape(e) }.join +
+ "])")
+
+ ##
+ # Escape a string for use in a DN value
+ def self.escape(string)
+ escaped = string.gsub(ESCAPE_RE) { |char| "\\" + char }
+ escaped.gsub(HEX_ESCAPE_RE) { |char| HEX_ESCAPES[char] }
+ end
+
+ private
+
+ def initialize_array(args)
+ buffer = StringIO.new
+
+ args.each_with_index do |arg, index|
+ if index.even? # key
+ buffer << "," if index > 0
+ buffer << arg
+ else # value
+ buffer << "="
+ buffer << self.class.escape(arg)
+ end
+ end
+
+ @dn = buffer.string
+ end
+
+ def initialize_string(arg)
+ @dn = arg.to_s
+ end
+
+ ##
+ # Proxy all other requests to the string object, because a DN is mainly
+ # used within the library as a string
+ # rubocop:disable GitlabSecurity/PublicSend
+ def method_missing(method, *args, &block)
+ @dn.send(method, *args, &block)
+ end
+
+ ##
+ # Redefined to be consistent with redefined `method_missing` behavior
+ def respond_to?(sym, include_private = false)
+ @dn.respond_to?(sym, include_private)
+ end
+ end
+ end
+ end
+
+ def perform(start_id, end_id)
+ return unless migrate?
+
+ ldap_identities = Identity.where("provider like 'ldap%'").where(id: start_id..end_id)
+ ldap_identities.each do |identity|
+ begin
+ identity.extern_uid = Gitlab::LDAP::DN.new(identity.extern_uid).to_normalized_s
+ unless identity.save
+ Rails.logger.info "Unable to normalize \"#{identity.extern_uid}\". Skipping."
+ end
+ rescue Gitlab::LDAP::DN::FormatError => e
+ Rails.logger.info "Unable to normalize \"#{identity.extern_uid}\" due to \"#{e.message}\". Skipping."
+ end
+ end
+ end
+
+ def migrate?
+ Identity.table_exists?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/populate_fork_networks_range.rb b/lib/gitlab/background_migration/populate_fork_networks_range.rb
new file mode 100644
index 00000000000..2ef3a207dd3
--- /dev/null
+++ b/lib/gitlab/background_migration/populate_fork_networks_range.rb
@@ -0,0 +1,59 @@
+module Gitlab
+ module BackgroundMigration
+ class PopulateForkNetworksRange
+ def perform(start_id, end_id)
+ log("Creating fork networks for forked project links: #{start_id} - #{end_id}")
+
+ ActiveRecord::Base.connection.execute <<~INSERT_NETWORKS
+ INSERT INTO fork_networks (root_project_id)
+ SELECT DISTINCT forked_project_links.forked_from_project_id
+
+ FROM forked_project_links
+
+ WHERE NOT EXISTS (
+ SELECT true
+ FROM forked_project_links inner_links
+ WHERE inner_links.forked_to_project_id = forked_project_links.forked_from_project_id
+ )
+ AND NOT EXISTS (
+ SELECT true
+ FROM fork_networks
+ WHERE forked_project_links.forked_from_project_id = fork_networks.root_project_id
+ )
+ AND EXISTS (
+ SELECT true
+ FROM projects
+ WHERE projects.id = forked_project_links.forked_from_project_id
+ )
+ AND forked_project_links.id BETWEEN #{start_id} AND #{end_id}
+ INSERT_NETWORKS
+
+ log("Creating memberships for root projects: #{start_id} - #{end_id}")
+
+ ActiveRecord::Base.connection.execute <<~INSERT_ROOT
+ INSERT INTO fork_network_members (fork_network_id, project_id)
+ SELECT DISTINCT fork_networks.id, fork_networks.root_project_id
+
+ FROM fork_networks
+
+ INNER JOIN forked_project_links
+ ON forked_project_links.forked_from_project_id = fork_networks.root_project_id
+
+ WHERE NOT EXISTS (
+ SELECT true
+ FROM fork_network_members
+ WHERE fork_network_members.project_id = fork_networks.root_project_id
+ )
+ AND forked_project_links.id BETWEEN #{start_id} AND #{end_id}
+ INSERT_ROOT
+
+ delay = BackgroundMigration::CreateForkNetworkMembershipsRange::RESCHEDULE_DELAY
+ BackgroundMigrationWorker.perform_in(delay, "CreateForkNetworkMembershipsRange", [start_id, end_id])
+ end
+
+ def log(message)
+ Rails.logger.info("#{self.class.name} - #{message}")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb
index 088adbdd267..72b75791bbb 100644
--- a/lib/gitlab/ci/ansi2html.rb
+++ b/lib/gitlab/ci/ansi2html.rb
@@ -155,7 +155,7 @@ module Gitlab
stream.each_line do |line|
s = StringScanner.new(line)
until s.eos?
- if s.scan(/section_((?:start)|(?:end)):(\d+):([^\r]+)\r\033\[0K/)
+ if s.scan(Gitlab::Regex.build_trace_section_regex)
handle_section(s)
elsif s.scan(/\e([@-_])(.*?)([@-~])/)
handle_sequence(s)
diff --git a/lib/gitlab/ci/pipeline/chain/validate/config.rb b/lib/gitlab/ci/pipeline/chain/validate/config.rb
index 489bcd79655..075504bcce5 100644
--- a/lib/gitlab/ci/pipeline/chain/validate/config.rb
+++ b/lib/gitlab/ci/pipeline/chain/validate/config.rb
@@ -13,7 +13,7 @@ module Gitlab
end
if @command.save_incompleted && @pipeline.has_yaml_errors?
- @pipeline.drop
+ @pipeline.drop!(:config_error)
end
return error(@pipeline.yaml_errors)
diff --git a/lib/gitlab/ci/stage/seed.rb b/lib/gitlab/ci/stage/seed.rb
index e19aae35a81..bc97aa63b02 100644
--- a/lib/gitlab/ci/stage/seed.rb
+++ b/lib/gitlab/ci/stage/seed.rb
@@ -3,7 +3,9 @@ module Gitlab
module Stage
class Seed
attr_reader :pipeline
+
delegate :project, to: :pipeline
+ delegate :size, to: :@jobs
def initialize(pipeline, stage, jobs)
@pipeline = pipeline
diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb
index 5b835bb669a..baf55b1fa07 100644
--- a/lib/gitlab/ci/trace.rb
+++ b/lib/gitlab/ci/trace.rb
@@ -27,6 +27,12 @@ module Gitlab
end
end
+ def extract_sections
+ read do |stream|
+ stream.extract_sections
+ end
+ end
+
def set(data)
write do |stream|
data = job.hide_secrets(data)
diff --git a/lib/gitlab/ci/trace/section_parser.rb b/lib/gitlab/ci/trace/section_parser.rb
new file mode 100644
index 00000000000..9bb0166c9e3
--- /dev/null
+++ b/lib/gitlab/ci/trace/section_parser.rb
@@ -0,0 +1,97 @@
+module Gitlab
+ module Ci
+ class Trace
+ class SectionParser
+ def initialize(lines)
+ @lines = lines
+ end
+
+ def parse!
+ @markers = {}
+
+ @lines.each do |line, pos|
+ parse_line(line, pos)
+ end
+ end
+
+ def sections
+ sanitize_markers.map do |name, markers|
+ start_, end_ = markers
+
+ {
+ name: name,
+ byte_start: start_[:marker],
+ byte_end: end_[:marker],
+ date_start: start_[:timestamp],
+ date_end: end_[:timestamp]
+ }
+ end
+ end
+
+ private
+
+ def parse_line(line, line_start_position)
+ s = StringScanner.new(line)
+ until s.eos?
+ find_next_marker(s) do |scanner|
+ marker_begins_at = line_start_position + scanner.pointer
+
+ if scanner.scan(Gitlab::Regex.build_trace_section_regex)
+ marker_ends_at = line_start_position + scanner.pointer
+ handle_line(scanner[1], scanner[2].to_i, scanner[3], marker_begins_at, marker_ends_at)
+ true
+ else
+ false
+ end
+ end
+ end
+ end
+
+ def sanitize_markers
+ @markers.select do |_, markers|
+ markers.size == 2 && markers[0][:action] == :start && markers[1][:action] == :end
+ end
+ end
+
+ def handle_line(action, time, name, marker_start, marker_end)
+ action = action.to_sym
+ timestamp = Time.at(time).utc
+ marker = if action == :start
+ marker_end
+ else
+ marker_start
+ end
+
+ @markers[name] ||= []
+ @markers[name] << {
+ name: name,
+ action: action,
+ timestamp: timestamp,
+ marker: marker
+ }
+ end
+
+ def beginning_of_section_regex
+ @beginning_of_section_regex ||= /section_/.freeze
+ end
+
+ def find_next_marker(s)
+ beginning_of_section_len = 8
+ maybe_marker = s.exist?(beginning_of_section_regex)
+
+ if maybe_marker.nil?
+ s.terminate
+ else
+ # repositioning at the beginning of the match
+ s.pos += maybe_marker - beginning_of_section_len
+ if block_given?
+ good_marker = yield(s)
+ # if not a good marker: Consuming the matched beginning_of_section_regex
+ s.pos += beginning_of_section_len unless good_marker
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb
index ab3408f48d6..d52194f688b 100644
--- a/lib/gitlab/ci/trace/stream.rb
+++ b/lib/gitlab/ci/trace/stream.rb
@@ -90,8 +90,25 @@ module Gitlab
# so we just silently ignore error for now
end
+ def extract_sections
+ return [] unless valid?
+
+ lines = to_enum(:each_line_with_pos)
+ parser = SectionParser.new(lines)
+
+ parser.parse!
+ parser.sections
+ end
+
private
+ def each_line_with_pos
+ stream.seek(0, IO::SEEK_SET)
+ stream.each_line do |line|
+ yield [line, stream.pos - line.bytesize]
+ end
+ end
+
def read_last_lines(limit)
to_enum(:reverse_line).first(limit).reverse.join
end
diff --git a/lib/gitlab/closing_issue_extractor.rb b/lib/gitlab/closing_issue_extractor.rb
index 243c1f1394d..7e7aaeeaa17 100644
--- a/lib/gitlab/closing_issue_extractor.rb
+++ b/lib/gitlab/closing_issue_extractor.rb
@@ -23,7 +23,8 @@ module Gitlab
@extractor.analyze(closing_statements.join(" "))
@extractor.issues.reject do |issue|
- @extractor.project.forked_from?(issue.project) # Don't extract issues on original project
+ # Don't extract issues from the project this project was forked from
+ @extractor.project.forked_from?(issue.project)
end
end
end
diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb
index 31a46a738c3..c169c8fe135 100644
--- a/lib/gitlab/data_builder/push.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -86,7 +86,7 @@ module Gitlab
user_name: user.name,
user_username: user.username,
user_email: user.email,
- user_avatar: user.avatar_url,
+ user_avatar: user.avatar_url(only_path: false),
project_id: project.id,
project: project.hook_attrs,
commits: commit_attrs,
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index a6ec75da385..357f16936c6 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -29,6 +29,15 @@ module Gitlab
adapter_name.casecmp('postgresql').zero?
end
+ # Overridden in EE
+ def self.read_only?
+ false
+ end
+
+ def self.read_write?
+ !self.read_only?
+ end
+
def self.version
database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
end
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index fcac85ff892..599c3c5deab 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -27,16 +27,23 @@ module Gitlab
@fallback_diff_refs = fallback_diff_refs
end
- def position(line)
+ def position(position_marker, position_type: :text)
return unless diff_refs
- Position.new(
+ data = {
+ diff_refs: diff_refs,
+ position_type: position_type.to_s,
old_path: old_path,
- new_path: new_path,
- old_line: line.old_line,
- new_line: line.new_line,
- diff_refs: diff_refs
- )
+ new_path: new_path
+ }
+
+ if position_type == :text
+ data.merge!(text_position_properties(position_marker))
+ else
+ data.merge!(image_position_properties(position_marker))
+ end
+
+ Position.new(data)
end
def line_code(line)
@@ -228,6 +235,14 @@ module Gitlab
private
+ def text_position_properties(line)
+ { old_line: line.old_line, new_line: line.new_line }
+ end
+
+ def image_position_properties(image_point)
+ image_point.to_h
+ end
+
def blobs_changed?
old_blob && new_blob && old_blob.id != new_blob.id
end
diff --git a/lib/gitlab/diff/formatters/base_formatter.rb b/lib/gitlab/diff/formatters/base_formatter.rb
new file mode 100644
index 00000000000..5e923b9e602
--- /dev/null
+++ b/lib/gitlab/diff/formatters/base_formatter.rb
@@ -0,0 +1,61 @@
+module Gitlab
+ module Diff
+ module Formatters
+ class BaseFormatter
+ attr_reader :old_path
+ attr_reader :new_path
+ attr_reader :base_sha
+ attr_reader :start_sha
+ attr_reader :head_sha
+ attr_reader :position_type
+
+ def initialize(attrs)
+ if diff_file = attrs[:diff_file]
+ attrs[:diff_refs] = diff_file.diff_refs
+ attrs[:old_path] = diff_file.old_path
+ attrs[:new_path] = diff_file.new_path
+ end
+
+ if diff_refs = attrs[:diff_refs]
+ attrs[:base_sha] = diff_refs.base_sha
+ attrs[:start_sha] = diff_refs.start_sha
+ attrs[:head_sha] = diff_refs.head_sha
+ end
+
+ @old_path = attrs[:old_path]
+ @new_path = attrs[:new_path]
+ @base_sha = attrs[:base_sha]
+ @start_sha = attrs[:start_sha]
+ @head_sha = attrs[:head_sha]
+ end
+
+ def key
+ [base_sha, start_sha, head_sha, Digest::SHA1.hexdigest(old_path || ""), Digest::SHA1.hexdigest(new_path || "")]
+ end
+
+ def to_h
+ {
+ base_sha: base_sha,
+ start_sha: start_sha,
+ head_sha: head_sha,
+ old_path: old_path,
+ new_path: new_path,
+ position_type: position_type
+ }
+ end
+
+ def position_type
+ raise NotImplementedError
+ end
+
+ def ==(other)
+ raise NotImplementedError
+ end
+
+ def complete?
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/formatters/image_formatter.rb b/lib/gitlab/diff/formatters/image_formatter.rb
new file mode 100644
index 00000000000..ccd0d309972
--- /dev/null
+++ b/lib/gitlab/diff/formatters/image_formatter.rb
@@ -0,0 +1,43 @@
+module Gitlab
+ module Diff
+ module Formatters
+ class ImageFormatter < BaseFormatter
+ attr_reader :width
+ attr_reader :height
+ attr_reader :x
+ attr_reader :y
+
+ def initialize(attrs)
+ @x = attrs[:x]
+ @y = attrs[:y]
+ @width = attrs[:width]
+ @height = attrs[:height]
+
+ super(attrs)
+ end
+
+ def key
+ @key ||= super.push(x, y)
+ end
+
+ def complete?
+ x && y && width && height
+ end
+
+ def to_h
+ super.merge(width: width, height: height, x: x, y: y)
+ end
+
+ def position_type
+ "image"
+ end
+
+ def ==(other)
+ other.is_a?(self.class) &&
+ x == other.x &&
+ y == other.y
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/formatters/text_formatter.rb b/lib/gitlab/diff/formatters/text_formatter.rb
new file mode 100644
index 00000000000..01c7e9f51ab
--- /dev/null
+++ b/lib/gitlab/diff/formatters/text_formatter.rb
@@ -0,0 +1,49 @@
+module Gitlab
+ module Diff
+ module Formatters
+ class TextFormatter < BaseFormatter
+ attr_reader :old_line
+ attr_reader :new_line
+
+ def initialize(attrs)
+ @old_line = attrs[:old_line]
+ @new_line = attrs[:new_line]
+
+ super(attrs)
+ end
+
+ def key
+ @key ||= super.push(old_line, new_line)
+ end
+
+ def complete?
+ old_line || new_line
+ end
+
+ def to_h
+ super.merge(old_line: old_line, new_line: new_line)
+ end
+
+ def line_age
+ if old_line && new_line
+ nil
+ elsif new_line
+ 'new'
+ else
+ 'old'
+ end
+ end
+
+ def position_type
+ "text"
+ end
+
+ def ==(other)
+ other.is_a?(self.class) &&
+ new_line == other.new_line &&
+ old_line == other.old_line
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/image_point.rb b/lib/gitlab/diff/image_point.rb
new file mode 100644
index 00000000000..65332dfd239
--- /dev/null
+++ b/lib/gitlab/diff/image_point.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module Diff
+ class ImagePoint
+ attr_reader :width, :height, :x, :y
+
+ def initialize(width, height, x, y)
+ @width = width
+ @height = height
+ @x = x
+ @y = y
+ end
+
+ def to_h
+ {
+ width: width,
+ height: height,
+ x: x,
+ y: y
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb
index b8db3adef0a..bd0a9502a5e 100644
--- a/lib/gitlab/diff/position.rb
+++ b/lib/gitlab/diff/position.rb
@@ -1,37 +1,25 @@
-# Defines a specific location, identified by paths and line numbers,
+# Defines a specific location, identified by paths line numbers and image coordinates,
# within a specific diff, identified by start, head and base commit ids.
module Gitlab
module Diff
class Position
- attr_reader :old_path
- attr_reader :new_path
- attr_reader :old_line
- attr_reader :new_line
- attr_reader :base_sha
- attr_reader :start_sha
- attr_reader :head_sha
-
+ attr_accessor :formatter
+
+ delegate :old_path,
+ :new_path,
+ :base_sha,
+ :start_sha,
+ :head_sha,
+ :old_line,
+ :new_line,
+ :position_type, to: :formatter
+
+ # A position can belong to a text line or to an image coordinate
+ # it depends of the position_type argument.
+ # Text position will have: new_line and old_line
+ # Image position will have: width, height, x, y
def initialize(attrs = {})
- if diff_file = attrs[:diff_file]
- attrs[:diff_refs] = diff_file.diff_refs
- attrs[:old_path] = diff_file.old_path
- attrs[:new_path] = diff_file.new_path
- end
-
- if diff_refs = attrs[:diff_refs]
- attrs[:base_sha] = diff_refs.base_sha
- attrs[:start_sha] = diff_refs.start_sha
- attrs[:head_sha] = diff_refs.head_sha
- end
-
- @old_path = attrs[:old_path]
- @new_path = attrs[:new_path]
- @base_sha = attrs[:base_sha]
- @start_sha = attrs[:start_sha]
- @head_sha = attrs[:head_sha]
-
- @old_line = attrs[:old_line]
- @new_line = attrs[:new_line]
+ @formatter = get_formatter_class(attrs[:position_type]).new(attrs)
end
# `Gitlab::Diff::Position` objects are stored as serialized attributes in
@@ -46,7 +34,11 @@ module Gitlab
end
def encode_with(coder)
- coder['attributes'] = self.to_h
+ coder['attributes'] = formatter.to_h
+ end
+
+ def key
+ formatter.key
end
def ==(other)
@@ -54,20 +46,11 @@ module Gitlab
other.diff_refs == diff_refs &&
other.old_path == old_path &&
other.new_path == new_path &&
- other.old_line == old_line &&
- other.new_line == new_line
+ other.formatter == formatter
end
def to_h
- {
- old_path: old_path,
- new_path: new_path,
- old_line: old_line,
- new_line: new_line,
- base_sha: base_sha,
- start_sha: start_sha,
- head_sha: head_sha
- }
+ formatter.to_h
end
def inspect
@@ -75,23 +58,15 @@ module Gitlab
end
def complete?
- file_path.present? &&
- (old_line || new_line) &&
- diff_refs.complete?
+ file_path.present? && formatter.complete? && diff_refs.complete?
end
def to_json(opts = nil)
- JSON.generate(self.to_h, opts)
+ JSON.generate(formatter.to_h, opts)
end
def type
- if old_line && new_line
- nil
- elsif new_line
- 'new'
- else
- 'old'
- end
+ formatter.line_age
end
def unchanged?
@@ -150,6 +125,17 @@ module Gitlab
diff_refs.compare_in(repository.project).diffs(paths: paths, expanded: true).diff_files.first
end
+
+ def get_formatter_class(type)
+ type ||= "text"
+
+ case type
+ when 'image'
+ Gitlab::Diff::Formatters::ImageFormatter
+ else
+ Gitlab::Diff::Formatters::TextFormatter
+ end
+ end
end
end
end
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index c5a8ea12245..c4c60d1dfee 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -2,7 +2,7 @@
module Gitlab
# Checks if a set of migrations requires downtime or not.
class EeCompatCheck
- CE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ce.git'.freeze
+ DEFAULT_CE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ce.git'.freeze
EE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze
CHECK_DIR = Rails.root.join('ee_compat_check')
IGNORED_FILES_REGEX = /(VERSION|CHANGELOG\.md:\d+)/.freeze
@@ -20,7 +20,7 @@ module Gitlab
attr_reader :ee_repo_dir, :patches_dir, :ce_repo, :ce_branch, :ee_branch_found
attr_reader :failed_files
- def initialize(branch:, ce_repo: CE_REPO)
+ def initialize(branch:, ce_repo: DEFAULT_CE_REPO)
@ee_repo_dir = CHECK_DIR.join('ee-repo')
@patches_dir = CHECK_DIR.join('patches')
@ce_branch = branch
@@ -132,7 +132,7 @@ module Gitlab
def check_patch(patch_path)
step("Checking out master", %w[git checkout master])
step("Resetting to latest master", %w[git reset --hard origin/master])
- step("Fetching CE/#{ce_branch}", %W[git fetch #{CE_REPO} #{ce_branch}])
+ step("Fetching CE/#{ce_branch}", %W[git fetch #{ce_repo} #{ce_branch}])
step(
"Checking if #{patch_path} applies cleanly to EE/master",
# Don't use --check here because it can result in a 0-exit status even
diff --git a/lib/gitlab/gcp/model.rb b/lib/gitlab/gcp/model.rb
new file mode 100644
index 00000000000..195391f0e3c
--- /dev/null
+++ b/lib/gitlab/gcp/model.rb
@@ -0,0 +1,13 @@
+module Gitlab
+ module Gcp
+ module Model
+ def table_name_prefix
+ "gcp_"
+ end
+
+ def model_name
+ @model_name ||= ActiveModel::Name.new(self, nil, self.name.split("::").last)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 89b654253cb..0f059bef808 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -990,7 +990,7 @@ module Gitlab
tmp_ref = fetch_ref(
start_repository,
source_ref: "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
- target_ref: "refs/tmp/#{SecureRandom.hex}/head"
+ target_ref: "refs/tmp/#{SecureRandom.hex}"
)
yield commit(sha)
@@ -1112,6 +1112,8 @@ module Gitlab
raise NoRepository.new(e)
rescue GRPC::BadStatus => e
raise CommandError.new(e)
+ rescue GRPC::InvalidArgument => e
+ raise ArgumentError.new(e)
end
private
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index db67ede9d9e..42b59c106e2 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -17,7 +17,8 @@ module Gitlab
command_not_allowed: "The command you're trying to execute is not allowed.",
upload_pack_disabled_over_http: 'Pulling over HTTP is not allowed.',
receive_pack_disabled_over_http: 'Pushing over HTTP is not allowed.',
- readonly: 'The repository is temporarily read-only. Please try again later.'
+ read_only: 'The repository is temporarily read-only. Please try again later.',
+ cannot_push_to_read_only: "You can't push code to a read-only GitLab instance."
}.freeze
DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }.freeze
@@ -161,7 +162,11 @@ module Gitlab
def check_push_access!(changes)
if project.repository_read_only?
- raise UnauthorizedError, ERROR_MESSAGES[:readonly]
+ raise UnauthorizedError, ERROR_MESSAGES[:read_only]
+ end
+
+ if Gitlab::Database.read_only?
+ raise UnauthorizedError, ERROR_MESSAGES[:cannot_push_to_read_only]
end
if deploy_key
diff --git a/lib/gitlab/git_access_wiki.rb b/lib/gitlab/git_access_wiki.rb
index 1fe5155c093..98f1f45b338 100644
--- a/lib/gitlab/git_access_wiki.rb
+++ b/lib/gitlab/git_access_wiki.rb
@@ -1,6 +1,7 @@
module Gitlab
class GitAccessWiki < GitAccess
ERROR_MESSAGES = {
+ read_only: "You can't push code to a read-only GitLab instance.",
write_to_wiki: "You are not allowed to write to this project's wiki."
}.freeze
@@ -17,6 +18,10 @@ module Gitlab
raise UnauthorizedError, ERROR_MESSAGES[:write_to_wiki]
end
+ if Gitlab::Database.read_only?
+ raise UnauthorizedError, ERROR_MESSAGES[:read_only]
+ end
+
true
end
end
diff --git a/lib/gitlab/git_ref_validator.rb b/lib/gitlab/git_ref_validator.rb
index a3c6b21a6a1..2e3e4fc3f1f 100644
--- a/lib/gitlab/git_ref_validator.rb
+++ b/lib/gitlab/git_ref_validator.rb
@@ -11,7 +11,7 @@ module Gitlab
return false if ref_name.start_with?('refs/remotes/')
Gitlab::Utils.system_silent(
- %W(#{Gitlab.config.git.bin_path} check-ref-format refs/#{ref_name}))
+ %W(#{Gitlab.config.git.bin_path} check-ref-format --branch #{ref_name}))
end
end
end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 87b300dcf7e..cf36106e23d 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -28,6 +28,7 @@ module Gitlab
SERVER_VERSION_FILE = 'GITALY_SERVER_VERSION'.freeze
MAXIMUM_GITALY_CALLS = 30
+ CLIENT_NAME = (Sidekiq.server? ? 'gitlab-sidekiq' : 'gitlab-web').freeze
MUTEX = Mutex.new
private_constant :MUTEX
@@ -79,7 +80,16 @@ module Gitlab
def self.request_metadata(storage)
encoded_token = Base64.strict_encode64(token(storage).to_s)
- { metadata: { 'authorization' => "Bearer #{encoded_token}" } }
+ metadata = {
+ 'authorization' => "Bearer #{encoded_token}",
+ 'client_name' => CLIENT_NAME
+ }
+
+ feature_stack = Thread.current[:gitaly_feature_stack]
+ feature = feature_stack && feature_stack[0]
+ metadata['call_site'] = feature.to_s if feature
+
+ { metadata: metadata }
end
def self.token(storage)
@@ -137,7 +147,14 @@ module Gitlab
Gitlab::Metrics.measure(metric_name) do
# Some migrate calls wrap other migrate calls
allow_n_plus_1_calls do
- yield is_enabled
+ feature_stack = Thread.current[:gitaly_feature_stack] ||= []
+ feature_stack.unshift(feature)
+ begin
+ yield is_enabled
+ ensure
+ feature_stack.shift
+ Thread.current[:gitaly_feature_stack] = nil if feature_stack.empty?
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/namespace_service.rb b/lib/gitlab/gitaly_client/namespace_service.rb
new file mode 100644
index 00000000000..bd7c345ac01
--- /dev/null
+++ b/lib/gitlab/gitaly_client/namespace_service.rb
@@ -0,0 +1,39 @@
+module Gitlab
+ module GitalyClient
+ class NamespaceService
+ def initialize(storage)
+ @storage = storage
+ end
+
+ def exists?(name)
+ request = Gitaly::NamespaceExistsRequest.new(storage_name: @storage, name: name)
+
+ gitaly_client_call(:namespace_exists, request).exists
+ end
+
+ def add(name)
+ request = Gitaly::AddNamespaceRequest.new(storage_name: @storage, name: name)
+
+ gitaly_client_call(:add_namespace, request)
+ end
+
+ def remove(name)
+ request = Gitaly::RemoveNamespaceRequest.new(storage_name: @storage, name: name)
+
+ gitaly_client_call(:remove_namespace, request)
+ end
+
+ def rename(from, to)
+ request = Gitaly::RenameNamespaceRequest.new(storage_name: @storage, from: from, to: to)
+
+ gitaly_client_call(:rename_namespace, request)
+ end
+
+ private
+
+ def gitaly_client_call(type, request)
+ GitalyClient.call(@storage, :namespace_service, type, request)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb
index 0d5039ddf5f..413872d7e08 100644
--- a/lib/gitlab/gpg.rb
+++ b/lib/gitlab/gpg.rb
@@ -34,6 +34,21 @@ module Gitlab
end
end
+ def subkeys_from_key(key)
+ using_tmp_keychain do
+ fingerprints = CurrentKeyChain.fingerprints_from_key(key)
+ raw_keys = GPGME::Key.find(:public, fingerprints)
+
+ raw_keys.each_with_object({}) do |raw_key, grouped_subkeys|
+ primary_subkey_id = raw_key.primary_subkey.keyid
+
+ grouped_subkeys[primary_subkey_id] = raw_key.subkeys[1..-1].map do |s|
+ { keyid: s.keyid, fingerprint: s.fingerprint }
+ end
+ end
+ end
+ end
+
def user_infos_from_key(key)
using_tmp_keychain do
fingerprints = CurrentKeyChain.fingerprints_from_key(key)
diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb
index 86bd9f5b125..0f4ba6f83fc 100644
--- a/lib/gitlab/gpg/commit.rb
+++ b/lib/gitlab/gpg/commit.rb
@@ -43,7 +43,9 @@ module Gitlab
# key belonging to the keyid.
# This way we can add the key to the temporary keychain and extract
# the proper signature.
- gpg_key = GpgKey.find_by(primary_keyid: verified_signature.fingerprint)
+ # NOTE: the invoked method is #fingerprint but it's only returning
+ # 16 characters (the format used by keyid) instead of 40.
+ gpg_key = find_gpg_key(verified_signature.fingerprint)
if gpg_key
Gitlab::Gpg::CurrentKeyChain.add(gpg_key.key)
@@ -74,7 +76,7 @@ module Gitlab
commit_sha: @commit.sha,
project: @commit.project,
gpg_key: gpg_key,
- gpg_key_primary_keyid: gpg_key&.primary_keyid || verified_signature.fingerprint,
+ gpg_key_primary_keyid: gpg_key&.keyid || verified_signature.fingerprint,
gpg_key_user_name: user_infos[:name],
gpg_key_user_email: user_infos[:email],
verification_status: verification_status
@@ -98,6 +100,10 @@ module Gitlab
def user_infos(gpg_key)
gpg_key&.verified_user_infos&.first || gpg_key&.user_infos&.first || {}
end
+
+ def find_gpg_key(keyid)
+ GpgKey.find_by(primary_keyid: keyid) || GpgKeySubkey.find_by(keyid: keyid)
+ end
end
end
end
diff --git a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb
index e085eab26c9..1991911ef6a 100644
--- a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb
+++ b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb
@@ -9,8 +9,8 @@ module Gitlab
GpgSignature
.select(:id, :commit_sha, :project_id)
.where('gpg_key_id IS NULL OR verification_status <> ?', GpgSignature.verification_statuses[:verified])
- .where(gpg_key_primary_keyid: @gpg_key.primary_keyid)
- .find_each { |sig| sig.gpg_commit.update_signature!(sig) }
+ .where(gpg_key_primary_keyid: @gpg_key.keyids)
+ .find_each { |sig| sig.gpg_commit&.update_signature!(sig) }
end
end
end
diff --git a/lib/gitlab/hook_data/issuable_builder.rb b/lib/gitlab/hook_data/issuable_builder.rb
new file mode 100644
index 00000000000..4febb0ab430
--- /dev/null
+++ b/lib/gitlab/hook_data/issuable_builder.rb
@@ -0,0 +1,56 @@
+module Gitlab
+ module HookData
+ class IssuableBuilder
+ CHANGES_KEYS = %i[previous current].freeze
+
+ attr_accessor :issuable
+
+ def initialize(issuable)
+ @issuable = issuable
+ end
+
+ def build(user: nil, changes: {})
+ hook_data = {
+ object_kind: issuable.class.name.underscore,
+ user: user.hook_attrs,
+ project: issuable.project.hook_attrs,
+ object_attributes: issuable.hook_attrs,
+ labels: issuable.labels.map(&:hook_attrs),
+ changes: final_changes(changes.slice(*safe_keys)),
+ # DEPRECATED
+ repository: issuable.project.hook_attrs.slice(:name, :url, :description, :homepage)
+ }
+
+ if issuable.is_a?(Issue)
+ hook_data[:assignees] = issuable.assignees.map(&:hook_attrs) if issuable.assignees.any?
+ else
+ hook_data[:assignee] = issuable.assignee.hook_attrs if issuable.assignee
+ end
+
+ hook_data
+ end
+
+ def safe_keys
+ issuable_builder::SAFE_HOOK_ATTRIBUTES + issuable_builder::SAFE_HOOK_RELATIONS
+ end
+
+ private
+
+ def issuable_builder
+ case issuable
+ when Issue
+ Gitlab::HookData::IssueBuilder
+ when MergeRequest
+ Gitlab::HookData::MergeRequestBuilder
+ end
+ end
+
+ def final_changes(changes_hash)
+ changes_hash.reduce({}) do |hash, (key, changes_array)|
+ hash[key] = Hash[CHANGES_KEYS.zip(changes_array)]
+ hash
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/hook_data/issue_builder.rb b/lib/gitlab/hook_data/issue_builder.rb
new file mode 100644
index 00000000000..de9cab80a02
--- /dev/null
+++ b/lib/gitlab/hook_data/issue_builder.rb
@@ -0,0 +1,55 @@
+module Gitlab
+ module HookData
+ class IssueBuilder
+ SAFE_HOOK_ATTRIBUTES = %i[
+ assignee_id
+ author_id
+ branch_name
+ closed_at
+ confidential
+ created_at
+ deleted_at
+ description
+ due_date
+ id
+ iid
+ last_edited_at
+ last_edited_by_id
+ milestone_id
+ moved_to_id
+ project_id
+ relative_position
+ state
+ time_estimate
+ title
+ updated_at
+ updated_by_id
+ ].freeze
+
+ SAFE_HOOK_RELATIONS = %i[
+ assignees
+ labels
+ ].freeze
+
+ attr_accessor :issue
+
+ def initialize(issue)
+ @issue = issue
+ end
+
+ def build
+ attrs = {
+ url: Gitlab::UrlBuilder.build(issue),
+ total_time_spent: issue.total_time_spent,
+ human_total_time_spent: issue.human_total_time_spent,
+ human_time_estimate: issue.human_time_estimate,
+ assignee_ids: issue.assignee_ids,
+ assignee_id: issue.assignee_ids.first # This key is deprecated
+ }
+
+ issue.attributes.with_indifferent_access.slice(*SAFE_HOOK_ATTRIBUTES)
+ .merge!(attrs)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/hook_data/merge_request_builder.rb b/lib/gitlab/hook_data/merge_request_builder.rb
new file mode 100644
index 00000000000..eaef19c9d04
--- /dev/null
+++ b/lib/gitlab/hook_data/merge_request_builder.rb
@@ -0,0 +1,62 @@
+module Gitlab
+ module HookData
+ class MergeRequestBuilder
+ SAFE_HOOK_ATTRIBUTES = %i[
+ assignee_id
+ author_id
+ created_at
+ deleted_at
+ description
+ head_pipeline_id
+ id
+ iid
+ last_edited_at
+ last_edited_by_id
+ merge_commit_sha
+ merge_error
+ merge_params
+ merge_status
+ merge_user_id
+ merge_when_pipeline_succeeds
+ milestone_id
+ ref_fetched
+ source_branch
+ source_project_id
+ state
+ target_branch
+ target_project_id
+ time_estimate
+ title
+ updated_at
+ updated_by_id
+ ].freeze
+
+ SAFE_HOOK_RELATIONS = %i[
+ assignee
+ labels
+ ].freeze
+
+ attr_accessor :merge_request
+
+ def initialize(merge_request)
+ @merge_request = merge_request
+ end
+
+ def build
+ attrs = {
+ url: Gitlab::UrlBuilder.build(merge_request),
+ source: merge_request.source_project.try(:hook_attrs),
+ target: merge_request.target_project.hook_attrs,
+ last_commit: merge_request.diff_head_commit&.hook_attrs,
+ work_in_progress: merge_request.work_in_progress?,
+ total_time_spent: merge_request.total_time_spent,
+ human_total_time_spent: merge_request.human_total_time_spent,
+ human_time_estimate: merge_request.human_time_estimate
+ }
+
+ merge_request.attributes.with_indifferent_access.slice(*SAFE_HOOK_ATTRIBUTES)
+ .merge!(attrs)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 2171c6c7bbb..dec8b4c5acd 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -53,6 +53,7 @@ project_tree:
- :auto_devops
- :triggers
- :pipeline_schedules
+ - :cluster
- :services
- :hooks
- protected_branches:
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 380b336395d..a76cf1addc0 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -8,6 +8,8 @@ module Gitlab
triggers: 'Ci::Trigger',
pipeline_schedules: 'Ci::PipelineSchedule',
builds: 'Ci::Build',
+ cluster: 'Gcp::Cluster',
+ clusters: 'Gcp::Cluster',
hooks: 'ProjectHook',
merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
push_access_levels: 'ProtectedBranch::PushAccessLevel',
diff --git a/lib/gitlab/ldap/auth_hash.rb b/lib/gitlab/ldap/auth_hash.rb
index 4fbc5fa5262..3123da17fd9 100644
--- a/lib/gitlab/ldap/auth_hash.rb
+++ b/lib/gitlab/ldap/auth_hash.rb
@@ -3,6 +3,10 @@
module Gitlab
module LDAP
class AuthHash < Gitlab::OAuth::AuthHash
+ def uid
+ Gitlab::LDAP::Person.normalize_dn(super)
+ end
+
private
def get_info(key)
diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb
new file mode 100644
index 00000000000..d6142dc6549
--- /dev/null
+++ b/lib/gitlab/ldap/dn.rb
@@ -0,0 +1,301 @@
+# -*- ruby encoding: utf-8 -*-
+
+# Based on the `ruby-net-ldap` gem's `Net::LDAP::DN`
+#
+# For our purposes, this class is used to normalize DNs in order to allow proper
+# comparison.
+#
+# E.g. DNs should be compared case-insensitively (in basically all LDAP
+# implementations or setups), therefore we downcase every DN.
+
+##
+# Objects of this class represent an LDAP DN ("Distinguished Name"). A DN
+# ("Distinguished Name") is a unique identifier for an entry within an LDAP
+# directory. It is made up of a number of other attributes strung together,
+# to identify the entry in the tree.
+#
+# Each attribute that makes up a DN needs to have its value escaped so that
+# the DN is valid. This class helps take care of that.
+#
+# A fully escaped DN needs to be unescaped when analysing its contents. This
+# class also helps take care of that.
+module Gitlab
+ module LDAP
+ class DN
+ FormatError = Class.new(StandardError)
+ MalformedError = Class.new(FormatError)
+ UnsupportedError = Class.new(FormatError)
+
+ def self.normalize_value(given_value)
+ dummy_dn = "placeholder=#{given_value}"
+ normalized_dn = new(*dummy_dn).to_normalized_s
+ normalized_dn.sub(/\Aplaceholder=/, '')
+ end
+
+ ##
+ # Initialize a DN, escaping as required. Pass in attributes in name/value
+ # pairs. If there is a left over argument, it will be appended to the dn
+ # without escaping (useful for a base string).
+ #
+ # Most uses of this class will be to escape a DN, rather than to parse it,
+ # so storing the dn as an escaped String and parsing parts as required
+ # with a state machine seems sensible.
+ def initialize(*args)
+ if args.length > 1
+ initialize_array(args)
+ else
+ initialize_string(args[0])
+ end
+ end
+
+ ##
+ # Parse a DN into key value pairs using ASN from
+ # http://tools.ietf.org/html/rfc2253 section 3.
+ # rubocop:disable Metrics/AbcSize
+ # rubocop:disable Metrics/CyclomaticComplexity
+ # rubocop:disable Metrics/PerceivedComplexity
+ def each_pair
+ state = :key
+ key = StringIO.new
+ value = StringIO.new
+ hex_buffer = ""
+
+ @dn.each_char.with_index do |char, dn_index|
+ case state
+ when :key then
+ case char
+ when 'a'..'z', 'A'..'Z' then
+ state = :key_normal
+ key << char
+ when '0'..'9' then
+ state = :key_oid
+ key << char
+ when ' ' then state = :key
+ else raise(MalformedError, "Unrecognized first character of an RDN attribute type name \"#{char}\"")
+ end
+ when :key_normal then
+ case char
+ when '=' then state = :value
+ when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char
+ else raise(MalformedError, "Unrecognized RDN attribute type name character \"#{char}\"")
+ end
+ when :key_oid then
+ case char
+ when '=' then state = :value
+ when '0'..'9', '.', ' ' then key << char
+ else raise(MalformedError, "Unrecognized RDN OID attribute type name character \"#{char}\"")
+ end
+ when :value then
+ case char
+ when '\\' then state = :value_normal_escape
+ when '"' then state = :value_quoted
+ when ' ' then state = :value
+ when '#' then
+ state = :value_hexstring
+ value << char
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else
+ state = :value_normal
+ value << char
+ end
+ when :value_normal then
+ case char
+ when '\\' then state = :value_normal_escape
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ when '+' then raise(UnsupportedError, "Multivalued RDNs are not supported")
+ else value << char
+ end
+ when :value_normal_escape then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_normal_escape_hex
+ hex_buffer = char
+ else
+ state = :value_normal
+ value << char
+ end
+ when :value_normal_escape_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_normal
+ value << "#{hex_buffer}#{char}".to_i(16).chr
+ else raise(MalformedError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"")
+ end
+ when :value_quoted then
+ case char
+ when '\\' then state = :value_quoted_escape
+ when '"' then state = :value_end
+ else value << char
+ end
+ when :value_quoted_escape then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_quoted_escape_hex
+ hex_buffer = char
+ else
+ state = :value_quoted
+ value << char
+ end
+ when :value_quoted_escape_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_quoted
+ value << "#{hex_buffer}#{char}".to_i(16).chr
+ else raise(MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"#{char}\"")
+ end
+ when :value_hexstring then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_hexstring_hex
+ value << char
+ when ' ' then state = :value_end
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else raise(MalformedError, "Expected the first character of a hex pair, but got \"#{char}\"")
+ end
+ when :value_hexstring_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_hexstring
+ value << char
+ else raise(MalformedError, "Expected the second character of a hex pair, but got \"#{char}\"")
+ end
+ when :value_end then
+ case char
+ when ' ' then state = :value_end
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else raise(MalformedError, "Expected the end of an attribute value, but got \"#{char}\"")
+ end
+ else raise "Fell out of state machine"
+ end
+ end
+
+ # Last pair
+ raise(MalformedError, 'DN string ended unexpectedly') unless
+ [:value, :value_normal, :value_hexstring, :value_end].include? state
+
+ yield key.string.strip, rstrip_except_escaped(value.string, @dn.length)
+ end
+
+ def rstrip_except_escaped(str, dn_index)
+ str_ends_with_whitespace = str.match(/\s\z/)
+
+ if str_ends_with_whitespace
+ dn_part_ends_with_escaped_whitespace = @dn[0, dn_index].match(/\\(\s+)\z/)
+
+ if dn_part_ends_with_escaped_whitespace
+ dn_part_rwhitespace = dn_part_ends_with_escaped_whitespace[1]
+ num_chars_to_remove = dn_part_rwhitespace.length - 1
+ str = str[0, str.length - num_chars_to_remove]
+ else
+ str.rstrip!
+ end
+ end
+
+ str
+ end
+
+ ##
+ # Returns the DN as an array in the form expected by the constructor.
+ def to_a
+ a = []
+ self.each_pair { |key, value| a << key << value } unless @dn.empty?
+ a
+ end
+
+ ##
+ # Return the DN as an escaped string.
+ def to_s
+ @dn
+ end
+
+ ##
+ # Return the DN as an escaped and normalized string.
+ def to_normalized_s
+ self.class.new(*to_a).to_s.downcase
+ end
+
+ # https://tools.ietf.org/html/rfc4514 section 2.4 lists these exceptions
+ # for DN values. All of the following must be escaped in any normal string
+ # using a single backslash ('\') as escape. The space character is left
+ # out here because in a "normalized" string, spaces should only be escaped
+ # if necessary (i.e. leading or trailing space).
+ NORMAL_ESCAPES = [',', '+', '"', '\\', '<', '>', ';', '='].freeze
+
+ # The following must be represented as escaped hex
+ HEX_ESCAPES = {
+ "\n" => '\0a',
+ "\r" => '\0d'
+ }.freeze
+
+ # Compiled character class regexp using the keys from the above hash, and
+ # checking for a space or # at the start, or space at the end, of the
+ # string.
+ ESCAPE_RE = Regexp.new("(^ |^#| $|[" +
+ NORMAL_ESCAPES.map { |e| Regexp.escape(e) }.join +
+ "])")
+
+ HEX_ESCAPE_RE = Regexp.new("([" +
+ HEX_ESCAPES.keys.map { |e| Regexp.escape(e) }.join +
+ "])")
+
+ ##
+ # Escape a string for use in a DN value
+ def self.escape(string)
+ escaped = string.gsub(ESCAPE_RE) { |char| "\\" + char }
+ escaped.gsub(HEX_ESCAPE_RE) { |char| HEX_ESCAPES[char] }
+ end
+
+ private
+
+ def initialize_array(args)
+ buffer = StringIO.new
+
+ args.each_with_index do |arg, index|
+ if index.even? # key
+ buffer << "," if index > 0
+ buffer << arg
+ else # value
+ buffer << "="
+ buffer << self.class.escape(arg)
+ end
+ end
+
+ @dn = buffer.string
+ end
+
+ def initialize_string(arg)
+ @dn = arg.to_s
+ end
+
+ ##
+ # Proxy all other requests to the string object, because a DN is mainly
+ # used within the library as a string
+ # rubocop:disable GitlabSecurity/PublicSend
+ def method_missing(method, *args, &block)
+ @dn.send(method, *args, &block)
+ end
+
+ ##
+ # Redefined to be consistent with redefined `method_missing` behavior
+ def respond_to?(sym, include_private = false)
+ @dn.respond_to?(sym, include_private)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb
index 9a6f7827b16..38d7a9ba2f5 100644
--- a/lib/gitlab/ldap/person.rb
+++ b/lib/gitlab/ldap/person.rb
@@ -36,6 +36,26 @@ module Gitlab
]
end
+ def self.normalize_dn(dn)
+ ::Gitlab::LDAP::DN.new(dn).to_normalized_s
+ rescue ::Gitlab::LDAP::DN::FormatError => e
+ Rails.logger.info("Returning original DN \"#{dn}\" due to error during normalization attempt: #{e.message}")
+
+ dn
+ end
+
+ # Returns the UID in a normalized form.
+ #
+ # 1. Excess spaces are stripped
+ # 2. The string is downcased (for case-insensitivity)
+ def self.normalize_uid(uid)
+ ::Gitlab::LDAP::DN.normalize_value(uid)
+ rescue ::Gitlab::LDAP::DN::FormatError => e
+ Rails.logger.info("Returning original UID \"#{uid}\" due to error during normalization attempt: #{e.message}")
+
+ uid
+ end
+
def initialize(entry, provider)
Rails.logger.debug { "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" }
@entry = entry
@@ -58,7 +78,9 @@ module Gitlab
attribute_value(:email)
end
- delegate :dn, to: :entry
+ def dn
+ self.class.normalize_dn(entry.dn)
+ end
private
diff --git a/lib/gitlab/middleware/read_only.rb b/lib/gitlab/middleware/read_only.rb
new file mode 100644
index 00000000000..0de0cddcce4
--- /dev/null
+++ b/lib/gitlab/middleware/read_only.rb
@@ -0,0 +1,88 @@
+module Gitlab
+ module Middleware
+ class ReadOnly
+ DISALLOWED_METHODS = %w(POST PATCH PUT DELETE).freeze
+ APPLICATION_JSON = 'application/json'.freeze
+ API_VERSIONS = (3..4)
+
+ def initialize(app)
+ @app = app
+ @whitelisted = internal_routes
+ end
+
+ def call(env)
+ @env = env
+
+ if disallowed_request? && Gitlab::Database.read_only?
+ Rails.logger.debug('GitLab ReadOnly: preventing possible non read-only operation')
+ error_message = 'You cannot do writing operations on a read-only GitLab instance'
+
+ if json_request?
+ return [403, { 'Content-Type' => 'application/json' }, [{ 'message' => error_message }.to_json]]
+ else
+ rack_flash.alert = error_message
+ rack_session['flash'] = rack_flash.to_session_value
+
+ return [301, { 'Location' => last_visited_url }, []]
+ end
+ end
+
+ @app.call(env)
+ end
+
+ private
+
+ def internal_routes
+ API_VERSIONS.flat_map { |version| "api/v#{version}/internal" }
+ end
+
+ def disallowed_request?
+ DISALLOWED_METHODS.include?(@env['REQUEST_METHOD']) && !whitelisted_routes
+ end
+
+ def json_request?
+ request.media_type == APPLICATION_JSON
+ end
+
+ def rack_flash
+ @rack_flash ||= ActionDispatch::Flash::FlashHash.from_session_value(rack_session)
+ end
+
+ def rack_session
+ @env['rack.session']
+ end
+
+ def request
+ @env['rack.request'] ||= Rack::Request.new(@env)
+ end
+
+ def last_visited_url
+ @env['HTTP_REFERER'] || rack_session['user_return_to'] || Rails.application.routes.url_helpers.root_url
+ end
+
+ def route_hash
+ @route_hash ||= Rails.application.routes.recognize_path(request.url, { method: request.request_method }) rescue {}
+ end
+
+ def whitelisted_routes
+ logout_route || grack_route || @whitelisted.any? { |path| request.path.include?(path) } || lfs_route || sidekiq_route
+ end
+
+ def logout_route
+ route_hash[:controller] == 'sessions' && route_hash[:action] == 'destroy'
+ end
+
+ def sidekiq_route
+ request.path.start_with?('/admin/sidekiq')
+ end
+
+ def grack_route
+ request.path.end_with?('.git/git-upload-pack')
+ end
+
+ def lfs_route
+ request.path.end_with?('/info/lfs/objects/batch')
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb
index 68815be4d13..47c2a422387 100644
--- a/lib/gitlab/o_auth/user.rb
+++ b/lib/gitlab/o_auth/user.rb
@@ -64,6 +64,8 @@ module Gitlab
protected
def add_or_update_user_identities
+ return unless gl_user
+
# find_or_initialize_by doesn't update `gl_user.identities`, and isn't autosaved.
identity = gl_user.identities.find { |identity| identity.provider == auth_hash.provider }
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
index e2fbcefdb74..cc37d708ce1 100644
--- a/lib/gitlab/path_regex.rb
+++ b/lib/gitlab/path_regex.rb
@@ -33,6 +33,7 @@ module Gitlab
explore
favicon.ico
files
+ google_api
groups
health_check
help
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 58f6245579a..bd677ec4bf3 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -65,5 +65,9 @@ module Gitlab
"can contain only lowercase letters, digits, and '-'. " \
"Must start with a letter, and cannot end with '-'"
end
+
+ def build_trace_section_regex
+ @build_trace_section_regexp ||= /section_((?:start)|(?:end)):(\d+):([^\r]+)\r\033\[0K/.freeze
+ end
end
end
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index a99f8e2b5f8..a37112ae5c4 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -222,10 +222,18 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385
def add_namespace(storage, name)
- path = full_path(storage, name)
- FileUtils.mkdir_p(path, mode: 0770) unless exists?(storage, name)
+ Gitlab::GitalyClient.migrate(:add_namespace) do |enabled|
+ if enabled
+ gitaly_namespace_client(storage).add(name)
+ else
+ path = full_path(storage, name)
+ FileUtils.mkdir_p(path, mode: 0770) unless exists?(storage, name)
+ end
+ end
rescue Errno::EEXIST => e
Rails.logger.warn("Directory exists as a file: #{e} at: #{path}")
+ rescue GRPC::InvalidArgument => e
+ raise ArgumentError, e.message
end
# Remove directory from repositories storage
@@ -236,7 +244,15 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385
def rm_namespace(storage, name)
- FileUtils.rm_r(full_path(storage, name), force: true)
+ Gitlab::GitalyClient.migrate(:remove_namespace) do |enabled|
+ if enabled
+ gitaly_namespace_client(storage).remove(name)
+ else
+ FileUtils.rm_r(full_path(storage, name), force: true)
+ end
+ end
+ rescue GRPC::InvalidArgument => e
+ raise ArgumentError, e.message
end
# Move namespace directory inside repositories storage
@@ -246,9 +262,17 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385
def mv_namespace(storage, old_name, new_name)
- return false if exists?(storage, new_name) || !exists?(storage, old_name)
+ Gitlab::GitalyClient.migrate(:rename_namespace) do |enabled|
+ if enabled
+ gitaly_namespace_client(storage).rename(old_name, new_name)
+ else
+ return false if exists?(storage, new_name) || !exists?(storage, old_name)
- FileUtils.mv(full_path(storage, old_name), full_path(storage, new_name))
+ FileUtils.mv(full_path(storage, old_name), full_path(storage, new_name))
+ end
+ end
+ rescue GRPC::InvalidArgument
+ false
end
def url_to_repo(path)
@@ -272,7 +296,13 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385
def exists?(storage, dir_name)
- File.exist?(full_path(storage, dir_name))
+ Gitlab::GitalyClient.migrate(:namespace_exists) do |enabled|
+ if enabled
+ gitaly_namespace_client(storage).exists?(dir_name)
+ else
+ File.exist?(full_path(storage, dir_name))
+ end
+ end
end
protected
@@ -349,6 +379,14 @@ module Gitlab
Bundler.with_original_env { Popen.popen(cmd, nil, vars) }
end
+ def gitaly_namespace_client(storage_path)
+ storage, _value = Gitlab.config.repositories.storages.find do |storage, value|
+ value['path'] == storage_path
+ end
+
+ Gitlab::GitalyClient::NamespaceService.new(storage)
+ end
+
def gitaly_migrate(method, &block)
Gitlab::GitalyClient.migrate(method, &block)
rescue GRPC::NotFound, GRPC::BadStatus => e
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 6857038dba8..3f3ba77d47f 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -48,6 +48,7 @@ module Gitlab
deploy_keys: DeployKey.count,
deployments: Deployment.count,
environments: ::Environment.count,
+ gcp_clusters: ::Gcp::Cluster.count,
in_review_folder: ::Environment.in_review_folder.count,
groups: Group.count,
issues: Issue.count,
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index f200c694562..58d5b0da1c4 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -103,11 +103,16 @@ module Gitlab
end
def send_git_diff(repository, diff_refs)
- params = {
- 'RepoPath' => repository.path_to_repo,
- 'ShaFrom' => diff_refs.base_sha,
- 'ShaTo' => diff_refs.head_sha
- }
+ params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_diff)
+ {
+ 'GitalyServer' => gitaly_server_hash(repository),
+ 'RawDiffRequest' => Gitaly::RawDiffRequest.new(
+ gitaly_diff_or_patch_hash(repository, diff_refs)
+ ).to_json
+ }
+ else
+ workhorse_diff_or_patch_hash(repository, diff_refs)
+ end
[
SEND_DATA_HEADER,
@@ -116,11 +121,16 @@ module Gitlab
end
def send_git_patch(repository, diff_refs)
- params = {
- 'RepoPath' => repository.path_to_repo,
- 'ShaFrom' => diff_refs.base_sha,
- 'ShaTo' => diff_refs.head_sha
- }
+ params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_patch)
+ {
+ 'GitalyServer' => gitaly_server_hash(repository),
+ 'RawPatchRequest' => Gitaly::RawPatchRequest.new(
+ gitaly_diff_or_patch_hash(repository, diff_refs)
+ ).to_json
+ }
+ else
+ workhorse_diff_or_patch_hash(repository, diff_refs)
+ end
[
SEND_DATA_HEADER,
@@ -216,6 +226,22 @@ module Gitlab
token: Gitlab::GitalyClient.token(repository.project.repository_storage)
}
end
+
+ def workhorse_diff_or_patch_hash(repository, diff_refs)
+ {
+ 'RepoPath' => repository.path_to_repo,
+ 'ShaFrom' => diff_refs.base_sha,
+ 'ShaTo' => diff_refs.head_sha
+ }
+ end
+
+ def gitaly_diff_or_patch_hash(repository, diff_refs)
+ {
+ repository: repository.gitaly_repository,
+ left_commit_id: diff_refs.base_sha,
+ right_commit_id: diff_refs.head_sha
+ }
+ end
end
end
end
diff --git a/lib/google_api/auth.rb b/lib/google_api/auth.rb
new file mode 100644
index 00000000000..99a82c849e0
--- /dev/null
+++ b/lib/google_api/auth.rb
@@ -0,0 +1,54 @@
+module GoogleApi
+ class Auth
+ attr_reader :access_token, :redirect_uri, :state
+
+ ConfigMissingError = Class.new(StandardError)
+
+ def initialize(access_token, redirect_uri, state: nil)
+ @access_token = access_token
+ @redirect_uri = redirect_uri
+ @state = state
+ end
+
+ def authorize_url
+ client.auth_code.authorize_url(
+ redirect_uri: redirect_uri,
+ scope: scope,
+ state: state # This is used for arbitary redirection
+ )
+ end
+
+ def get_token(code)
+ ret = client.auth_code.get_token(code, redirect_uri: redirect_uri)
+ return ret.token, ret.expires_at
+ end
+
+ protected
+
+ def scope
+ raise NotImplementedError
+ end
+
+ private
+
+ def config
+ Gitlab.config.omniauth.providers.find { |provider| provider.name == "google_oauth2" }
+ end
+
+ def client
+ return @client if defined?(@client)
+
+ unless config
+ raise ConfigMissingError
+ end
+
+ @client = ::OAuth2::Client.new(
+ config.app_id,
+ config.app_secret,
+ site: 'https://accounts.google.com',
+ token_url: '/o/oauth2/token',
+ authorize_url: '/o/oauth2/auth'
+ )
+ end
+ end
+end
diff --git a/lib/google_api/cloud_platform/client.rb b/lib/google_api/cloud_platform/client.rb
new file mode 100644
index 00000000000..a440a3e3562
--- /dev/null
+++ b/lib/google_api/cloud_platform/client.rb
@@ -0,0 +1,88 @@
+require 'google/apis/container_v1'
+
+module GoogleApi
+ module CloudPlatform
+ class Client < GoogleApi::Auth
+ DEFAULT_MACHINE_TYPE = 'n1-standard-1'.freeze
+ SCOPE = 'https://www.googleapis.com/auth/cloud-platform'.freeze
+ LEAST_TOKEN_LIFE_TIME = 10.minutes
+
+ class << self
+ def session_key_for_token
+ :cloud_platform_access_token
+ end
+
+ def session_key_for_expires_at
+ :cloud_platform_expires_at
+ end
+
+ def new_session_key_for_redirect_uri
+ SecureRandom.hex.tap do |state|
+ yield session_key_for_redirect_uri(state)
+ end
+ end
+
+ def session_key_for_redirect_uri(state)
+ "cloud_platform_second_redirect_uri_#{state}"
+ end
+ end
+
+ def scope
+ SCOPE
+ end
+
+ def validate_token(expires_at)
+ return false unless access_token
+ return false unless expires_at
+
+ # Making sure that the token will have been still alive during the cluster creation.
+ return false if token_life_time(expires_at) < LEAST_TOKEN_LIFE_TIME
+
+ true
+ end
+
+ def projects_zones_clusters_get(project_id, zone, cluster_id)
+ service = Google::Apis::ContainerV1::ContainerService.new
+ service.authorization = access_token
+
+ service.get_zone_cluster(project_id, zone, cluster_id)
+ end
+
+ def projects_zones_clusters_create(project_id, zone, cluster_name, cluster_size, machine_type:)
+ service = Google::Apis::ContainerV1::ContainerService.new
+ service.authorization = access_token
+
+ request_body = Google::Apis::ContainerV1::CreateClusterRequest.new(
+ {
+ "cluster": {
+ "name": cluster_name,
+ "initial_node_count": cluster_size,
+ "node_config": {
+ "machine_type": machine_type
+ }
+ }
+ } )
+
+ service.create_cluster(project_id, zone, request_body)
+ end
+
+ def projects_zones_operations(project_id, zone, operation_id)
+ service = Google::Apis::ContainerV1::ContainerService.new
+ service.authorization = access_token
+
+ service.get_zone_operation(project_id, zone, operation_id)
+ end
+
+ def parse_operation_id(self_link)
+ m = self_link.match(%r{projects/.*/zones/.*/operations/(.*)})
+ m[1] if m
+ end
+
+ private
+
+ def token_life_time(expires_at)
+ DateTime.strptime(expires_at, '%s').to_time.utc - Time.now.utc
+ end
+ end
+ end
+end
diff --git a/lib/rspec_flaky/config.rb b/lib/rspec_flaky/config.rb
new file mode 100644
index 00000000000..a17ae55910e
--- /dev/null
+++ b/lib/rspec_flaky/config.rb
@@ -0,0 +1,21 @@
+require 'json'
+
+module RspecFlaky
+ class Config
+ def self.generate_report?
+ ENV['FLAKY_RSPEC_GENERATE_REPORT'] == 'true'
+ end
+
+ def self.suite_flaky_examples_report_path
+ ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'] || Rails.root.join("rspec_flaky/suite-report.json")
+ end
+
+ def self.flaky_examples_report_path
+ ENV['FLAKY_RSPEC_REPORT_PATH'] || Rails.root.join("rspec_flaky/report.json")
+ end
+
+ def self.new_flaky_examples_report_path
+ ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] || Rails.root.join("rspec_flaky/new-report.json")
+ end
+ end
+end
diff --git a/lib/rspec_flaky/flaky_example.rb b/lib/rspec_flaky/flaky_example.rb
index f81fb90e870..6be24014d89 100644
--- a/lib/rspec_flaky/flaky_example.rb
+++ b/lib/rspec_flaky/flaky_example.rb
@@ -9,24 +9,21 @@ module RspecFlaky
line: example.line,
description: example.description,
last_attempts_count: example.attempts,
- flaky_reports: 1)
+ flaky_reports: 0)
else
super
end
end
- def first_flaky_at
- self[:first_flaky_at] || Time.now
- end
-
- def last_flaky_at
- Time.now
- end
+ def update_flakiness!(last_attempts_count: nil)
+ self.first_flaky_at ||= Time.now
+ self.last_flaky_at = Time.now
+ self.flaky_reports += 1
+ self.last_attempts_count = last_attempts_count if last_attempts_count
- def last_flaky_job
- return unless ENV['CI_PROJECT_URL'] && ENV['CI_JOB_ID']
-
- "#{ENV['CI_PROJECT_URL']}/-/jobs/#{ENV['CI_JOB_ID']}"
+ if ENV['CI_PROJECT_URL'] && ENV['CI_JOB_ID']
+ self.last_flaky_job = "#{ENV['CI_PROJECT_URL']}/-/jobs/#{ENV['CI_JOB_ID']}"
+ end
end
def to_h
diff --git a/lib/rspec_flaky/flaky_examples_collection.rb b/lib/rspec_flaky/flaky_examples_collection.rb
new file mode 100644
index 00000000000..973c95b0212
--- /dev/null
+++ b/lib/rspec_flaky/flaky_examples_collection.rb
@@ -0,0 +1,37 @@
+require 'json'
+
+module RspecFlaky
+ class FlakyExamplesCollection < SimpleDelegator
+ def self.from_json(json)
+ new(JSON.parse(json))
+ end
+
+ def initialize(collection = {})
+ unless collection.is_a?(Hash)
+ raise ArgumentError, "`collection` must be a Hash, #{collection.class} given!"
+ end
+
+ collection_of_flaky_examples =
+ collection.map do |uid, example|
+ [
+ uid,
+ example.is_a?(RspecFlaky::FlakyExample) ? example : RspecFlaky::FlakyExample.new(example)
+ ]
+ end
+
+ super(Hash[collection_of_flaky_examples])
+ end
+
+ def to_report
+ Hash[map { |uid, example| [uid, example.to_h] }].deep_symbolize_keys
+ end
+
+ def -(other)
+ unless other.respond_to?(:key)
+ raise ArgumentError, "`other` must respond to `#key?`, #{other.class} does not!"
+ end
+
+ self.class.new(reject { |uid, _| other.key?(uid) })
+ end
+ end
+end
diff --git a/lib/rspec_flaky/listener.rb b/lib/rspec_flaky/listener.rb
index ec2fbd9e36c..4a5bfec9967 100644
--- a/lib/rspec_flaky/listener.rb
+++ b/lib/rspec_flaky/listener.rb
@@ -2,11 +2,15 @@ require 'json'
module RspecFlaky
class Listener
- attr_reader :all_flaky_examples, :new_flaky_examples
-
- def initialize
- @new_flaky_examples = {}
- @all_flaky_examples = init_all_flaky_examples
+ # - suite_flaky_examples: contains all the currently tracked flacky example
+ # for the whole RSpec suite
+ # - flaky_examples: contains the examples detected as flaky during the
+ # current RSpec run
+ attr_reader :suite_flaky_examples, :flaky_examples
+
+ def initialize(suite_flaky_examples_json = nil)
+ @flaky_examples = FlakyExamplesCollection.new
+ @suite_flaky_examples = init_suite_flaky_examples(suite_flaky_examples_json)
end
def example_passed(notification)
@@ -14,29 +18,21 @@ module RspecFlaky
return unless current_example.attempts > 1
- flaky_example_hash = all_flaky_examples[current_example.uid]
-
- all_flaky_examples[current_example.uid] =
- if flaky_example_hash
- FlakyExample.new(flaky_example_hash).tap do |ex|
- ex.last_attempts_count = current_example.attempts
- ex.flaky_reports += 1
- end
- else
- FlakyExample.new(current_example).tap do |ex|
- new_flaky_examples[current_example.uid] = ex
- end
- end
+ flaky_example = suite_flaky_examples.fetch(current_example.uid) { FlakyExample.new(current_example) }
+ flaky_example.update_flakiness!(last_attempts_count: current_example.attempts)
+
+ flaky_examples[current_example.uid] = flaky_example
end
def dump_summary(_)
- write_report_file(all_flaky_examples, all_flaky_examples_report_path)
+ write_report_file(flaky_examples, RspecFlaky::Config.flaky_examples_report_path)
+ new_flaky_examples = flaky_examples - suite_flaky_examples
if new_flaky_examples.any?
Rails.logger.warn "\nNew flaky examples detected:\n"
- Rails.logger.warn JSON.pretty_generate(to_report(new_flaky_examples))
+ Rails.logger.warn JSON.pretty_generate(new_flaky_examples.to_report)
- write_report_file(new_flaky_examples, new_flaky_examples_report_path)
+ write_report_file(new_flaky_examples, RspecFlaky::Config.new_flaky_examples_report_path)
end
end
@@ -46,30 +42,23 @@ module RspecFlaky
private
- def init_all_flaky_examples
- return {} unless File.exist?(all_flaky_examples_report_path)
+ def init_suite_flaky_examples(suite_flaky_examples_json = nil)
+ unless suite_flaky_examples_json
+ return {} unless File.exist?(RspecFlaky::Config.suite_flaky_examples_report_path)
- all_flaky_examples = JSON.parse(File.read(all_flaky_examples_report_path))
+ suite_flaky_examples_json = File.read(RspecFlaky::Config.suite_flaky_examples_report_path)
+ end
- Hash[(all_flaky_examples || {}).map { |k, ex| [k, FlakyExample.new(ex)] }]
+ FlakyExamplesCollection.from_json(suite_flaky_examples_json)
end
- def write_report_file(examples, file_path)
- return unless ENV['FLAKY_RSPEC_GENERATE_REPORT'] == 'true'
+ def write_report_file(examples_collection, file_path)
+ return unless RspecFlaky::Config.generate_report?
report_path_dir = File.dirname(file_path)
FileUtils.mkdir_p(report_path_dir) unless Dir.exist?(report_path_dir)
- File.write(file_path, JSON.pretty_generate(to_report(examples)))
- end
-
- def all_flaky_examples_report_path
- @all_flaky_examples_report_path ||= ENV['ALL_FLAKY_RSPEC_REPORT_PATH'] ||
- Rails.root.join("rspec_flaky/all-report.json")
- end
- def new_flaky_examples_report_path
- @new_flaky_examples_report_path ||= ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] ||
- Rails.root.join("rspec_flaky/new-report.json")
+ File.write(file_path, JSON.pretty_generate(examples_collection.to_report))
end
end
end
diff --git a/lib/system_check/app/git_user_default_ssh_config_check.rb b/lib/system_check/app/git_user_default_ssh_config_check.rb
index dfa8b8b3f5b..9af21078403 100644
--- a/lib/system_check/app/git_user_default_ssh_config_check.rb
+++ b/lib/system_check/app/git_user_default_ssh_config_check.rb
@@ -11,10 +11,10 @@ module SystemCheck
].freeze
set_name 'Git user has default SSH configuration?'
- set_skip_reason 'skipped (git user is not present or configured)'
+ set_skip_reason 'skipped (GitLab read-only, or git user is not present / configured)'
def skip?
- !home_dir || !File.directory?(home_dir)
+ Gitlab::Database.read_only? || !home_dir || !File.directory?(home_dir)
end
def check?
diff --git a/lib/tasks/gitlab/dev.rake b/lib/tasks/gitlab/dev.rake
index 3eade7bf553..b4d05f5995a 100644
--- a/lib/tasks/gitlab/dev.rake
+++ b/lib/tasks/gitlab/dev.rake
@@ -4,7 +4,10 @@ namespace :gitlab do
task :ee_compat_check, [:branch] => :environment do |_, args|
opts =
if ENV['CI']
- { branch: ENV['CI_COMMIT_REF_NAME'] }
+ {
+ ce_repo: ENV['CI_REPOSITORY_URL'],
+ branch: ENV['CI_COMMIT_REF_NAME']
+ }
else
unless args[:branch]
puts "Must specify a branch as an argument".color(:red)
diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake
index 08677a98fc1..8377fe3269d 100644
--- a/lib/tasks/gitlab/gitaly.rake
+++ b/lib/tasks/gitlab/gitaly.rake
@@ -50,6 +50,8 @@ namespace :gitlab do
# only generate a configuration for the most common and simplest case: when
# we have exactly one Gitaly process and we are sure it is running locally
# because it uses a Unix socket.
+ # For development and testing purposes, an extra storage is added to gitaly,
+ # which is not known to Rails, but must be explicitly stubbed.
def gitaly_configuration_toml(gitaly_ruby: true)
storages = []
address = nil
@@ -67,6 +69,11 @@ namespace :gitlab do
storages << { name: key, path: val['path'] }
end
+
+ if Rails.env.test?
+ storages << { name: 'test_second_storage', path: Rails.root.join('tmp', 'tests', 'second_storage').to_s }
+ end
+
config = { socket_path: address.sub(%r{\Aunix:}, ''), storage: storages }
config[:auth] = { token: 'secret' } if Rails.env.test?
config[:'gitaly-ruby'] = { dir: File.join(Dir.pwd, 'ruby') } if gitaly_ruby
diff --git a/lib/tasks/import.rake b/lib/tasks/import.rake
index 4d485108cf6..7f86fd7b45e 100644
--- a/lib/tasks/import.rake
+++ b/lib/tasks/import.rake
@@ -39,13 +39,19 @@ class GithubImport
def import!
@project.force_import_start
+ import_success = false
+
timings = Benchmark.measure do
- Github::Import.new(@project, @options).execute
+ import_success = Github::Import.new(@project, @options).execute
end
- puts "Import finished. Timings: #{timings}".color(:green)
-
- @project.import_finish
+ if import_success
+ @project.import_finish
+ puts "Import finished. Timings: #{timings}".color(:green)
+ else
+ puts "Import was not successful. Errors were as follows:"
+ puts @project.import_error
+ end
end
def new_project
@@ -53,18 +59,23 @@ class GithubImport
namespace_path, _sep, name = @project_path.rpartition('/')
namespace = find_or_create_namespace(namespace_path)
- Projects::CreateService.new(
+ project = Projects::CreateService.new(
@current_user,
name: name,
path: name,
description: @repo['description'],
namespace_id: namespace.id,
visibility_level: visibility_level,
- import_type: 'github',
- import_source: @repo['full_name'],
- import_url: @repo['clone_url'].sub('://', "://#{@options[:token]}@"),
skip_wiki: @repo['has_wiki']
).execute
+
+ project.update!(
+ import_type: 'github',
+ import_source: @repo['full_name'],
+ import_url: @repo['clone_url'].sub('://', "://#{@options[:token]}@")
+ )
+
+ project
end
end