summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorKamil Trzcinski <ayufan@ayufan.eu>2017-01-19 15:31:04 +0100
committerKamil Trzcinski <ayufan@ayufan.eu>2017-01-19 15:31:04 +0100
commit8171a1932b3c5e55ad3ea8402ac68ff14692ca32 (patch)
treecdcef619d3df923e634bd61228179d80e88c61f6 /lib
parent8c9a4ed373f4b517aeae669e64023dc52c8d704a (diff)
parent1cc6d206b5d4cf09bb502a254703f3a2de2dbeb7 (diff)
downloadgitlab-ce-8171a1932b3c5e55ad3ea8402ac68ff14692ca32.tar.gz
Merge remote-tracking branch 'origin/master' into 21698-redis-runner-last-build
Diffstat (limited to 'lib')
-rw-r--r--lib/api/api.rb8
-rw-r--r--lib/api/api_guard.rb50
-rw-r--r--lib/api/commits.rb1
-rw-r--r--lib/api/entities.rb52
-rw-r--r--lib/api/environments.rb3
-rw-r--r--lib/api/files.rb4
-rw-r--r--lib/api/groups.rb37
-rw-r--r--lib/api/helpers.rb60
-rw-r--r--lib/api/helpers/custom_validators.rb14
-rw-r--r--lib/api/helpers/internal_helpers.rb8
-rw-r--r--lib/api/helpers/pagination.rb45
-rw-r--r--lib/api/internal.rb14
-rw-r--r--lib/api/issues.rb63
-rw-r--r--lib/api/merge_requests.rb22
-rw-r--r--lib/api/notes.rb2
-rw-r--r--lib/api/project_hooks.rb2
-rw-r--r--lib/api/projects.rb92
-rw-r--r--lib/api/repositories.rb6
-rw-r--r--lib/api/services.rb50
-rw-r--r--lib/api/settings.rb122
-rw-r--r--lib/api/templates.rb12
-rw-r--r--lib/api/time_tracking_endpoints.rb114
-rw-r--r--lib/api/users.rb20
-rw-r--r--lib/backup/manager.rb44
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb19
-rw-r--r--lib/banzai/filter/external_link_filter.rb2
-rw-r--r--lib/banzai/filter/math_filter.rb46
-rw-r--r--lib/banzai/filter/reference_filter.rb8
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb1
-rw-r--r--lib/bitbucket/client.rb58
-rw-r--r--lib/bitbucket/collection.rb21
-rw-r--r--lib/bitbucket/connection.rb69
-rw-r--r--lib/bitbucket/error/unauthorized.rb6
-rw-r--r--lib/bitbucket/page.rb34
-rw-r--r--lib/bitbucket/paginator.rb36
-rw-r--r--lib/bitbucket/representation/base.rb17
-rw-r--r--lib/bitbucket/representation/comment.rb27
-rw-r--r--lib/bitbucket/representation/issue.rb53
-rw-r--r--lib/bitbucket/representation/pull_request.rb65
-rw-r--r--lib/bitbucket/representation/pull_request_comment.rb39
-rw-r--r--lib/bitbucket/representation/repo.rb71
-rw-r--r--lib/bitbucket/representation/user.rb9
-rw-r--r--lib/ci/ansi2html.rb2
-rw-r--r--lib/ci/api/api.rb10
-rw-r--r--lib/ci/api/builds.rb19
-rw-r--r--lib/ci/api/helpers.rb17
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb2
-rw-r--r--lib/email_template_interceptor.rb4
-rw-r--r--lib/gitlab/allowable.rb7
-rw-r--r--lib/gitlab/asciidoc.rb39
-rw-r--r--lib/gitlab/auth.rb25
-rw-r--r--lib/gitlab/auth/result.rb3
-rw-r--r--lib/gitlab/badge/build/status.rb4
-rw-r--r--lib/gitlab/bitbucket_import.rb6
-rw-r--r--lib/gitlab/bitbucket_import/client.rb142
-rw-r--r--lib/gitlab/bitbucket_import/importer.rb255
-rw-r--r--lib/gitlab/bitbucket_import/key_adder.rb24
-rw-r--r--lib/gitlab/bitbucket_import/key_deleter.rb23
-rw-r--r--lib/gitlab/bitbucket_import/project_creator.rb28
-rw-r--r--lib/gitlab/chat_commands/base_command.rb4
-rw-r--r--lib/gitlab/chat_commands/command.rb10
-rw-r--r--lib/gitlab/chat_commands/deploy.rb5
-rw-r--r--lib/gitlab/chat_commands/issue_create.rb2
-rw-r--r--lib/gitlab/chat_commands/presenter.rb (renamed from lib/mattermost/presenter.rb)22
-rw-r--r--lib/gitlab/checks/change_access.rb18
-rw-r--r--lib/gitlab/checks/force_push.rb11
-rw-r--r--lib/gitlab/ci/config/entry/environment.rb1
-rw-r--r--lib/gitlab/ci/status/build/cancelable.rb37
-rw-r--r--lib/gitlab/ci/status/build/common.rb19
-rw-r--r--lib/gitlab/ci/status/build/factory.rb18
-rw-r--r--lib/gitlab/ci/status/build/play.rb57
-rw-r--r--lib/gitlab/ci/status/build/retryable.rb37
-rw-r--r--lib/gitlab/ci/status/build/stop.rb53
-rw-r--r--lib/gitlab/ci/status/core.rb38
-rw-r--r--lib/gitlab/ci/status/extended.rb8
-rw-r--r--lib/gitlab/ci/status/factory.rb30
-rw-r--r--lib/gitlab/ci/status/pipeline/common.rb8
-rw-r--r--lib/gitlab/ci/status/pipeline/factory.rb8
-rw-r--r--lib/gitlab/ci/status/pipeline/success_with_warnings.rb6
-rw-r--r--lib/gitlab/ci/status/stage/common.rb10
-rw-r--r--lib/gitlab/ci/status/stage/factory.rb6
-rw-r--r--lib/gitlab/current_settings.rb3
-rw-r--r--lib/gitlab/cycle_analytics/base_event_fetcher.rb (renamed from lib/gitlab/cycle_analytics/base_event.rb)26
-rw-r--r--lib/gitlab/cycle_analytics/base_query.rb31
-rw-r--r--lib/gitlab/cycle_analytics/base_stage.rb54
-rw-r--r--lib/gitlab/cycle_analytics/code_event_fetcher.rb (renamed from lib/gitlab/cycle_analytics/review_event.rb)8
-rw-r--r--lib/gitlab/cycle_analytics/code_stage.rb21
-rw-r--r--lib/gitlab/cycle_analytics/event_fetcher.rb9
-rw-r--r--lib/gitlab/cycle_analytics/events.rb38
-rw-r--r--lib/gitlab/cycle_analytics/events_query.rb37
-rw-r--r--lib/gitlab/cycle_analytics/issue_event.rb27
-rw-r--r--lib/gitlab/cycle_analytics/issue_event_fetcher.rb (renamed from lib/gitlab/cycle_analytics/production_event.rb)5
-rw-r--r--lib/gitlab/cycle_analytics/issue_stage.rb22
-rw-r--r--lib/gitlab/cycle_analytics/metrics_fetcher.rb60
-rw-r--r--lib/gitlab/cycle_analytics/plan_event_fetcher.rb (renamed from lib/gitlab/cycle_analytics/plan_event.rb)10
-rw-r--r--lib/gitlab/cycle_analytics/plan_stage.rb22
-rw-r--r--lib/gitlab/cycle_analytics/production_event_fetcher.rb6
-rw-r--r--lib/gitlab/cycle_analytics/production_helper.rb9
-rw-r--r--lib/gitlab/cycle_analytics/production_stage.rb28
-rw-r--r--lib/gitlab/cycle_analytics/review_event_fetcher.rb (renamed from lib/gitlab/cycle_analytics/code_event.rb)8
-rw-r--r--lib/gitlab/cycle_analytics/review_stage.rb21
-rw-r--r--lib/gitlab/cycle_analytics/stage.rb9
-rw-r--r--lib/gitlab/cycle_analytics/stage_summary.rb23
-rw-r--r--lib/gitlab/cycle_analytics/staging_event_fetcher.rb (renamed from lib/gitlab/cycle_analytics/staging_event.rb)9
-rw-r--r--lib/gitlab/cycle_analytics/staging_stage.rb22
-rw-r--r--lib/gitlab/cycle_analytics/summary/base.rb20
-rw-r--r--lib/gitlab/cycle_analytics/summary/commit.rb39
-rw-r--r--lib/gitlab/cycle_analytics/summary/deploy.rb11
-rw-r--r--lib/gitlab/cycle_analytics/summary/issue.rb21
-rw-r--r--lib/gitlab/cycle_analytics/test_event.rb13
-rw-r--r--lib/gitlab/cycle_analytics/test_event_fetcher.rb6
-rw-r--r--lib/gitlab/cycle_analytics/test_stage.rb29
-rw-r--r--lib/gitlab/database/median.rb5
-rw-r--r--lib/gitlab/diff/file_collection/merge_request_diff.rb5
-rw-r--r--lib/gitlab/email/reply_parser.rb36
-rw-r--r--lib/gitlab/gfm/reference_rewriter.rb2
-rw-r--r--lib/gitlab/gfm/uploads_rewriter.rb19
-rw-r--r--lib/gitlab/git/attributes.rb131
-rw-r--r--lib/gitlab/git/blame.rb75
-rw-r--r--lib/gitlab/git/blob.rb330
-rw-r--r--lib/gitlab/git/blob_snippet.rb32
-rw-r--r--lib/gitlab/git/branch.rb6
-rw-r--r--lib/gitlab/git/commit.rb310
-rw-r--r--lib/gitlab/git/commit_stats.rb26
-rw-r--r--lib/gitlab/git/compare.rb43
-rw-r--r--lib/gitlab/git/diff.rb322
-rw-r--r--lib/gitlab/git/diff_collection.rb129
-rw-r--r--lib/gitlab/git/encoding_helper.rb58
-rw-r--r--lib/gitlab/git/path_helper.rb16
-rw-r--r--lib/gitlab/git/popen.rb26
-rw-r--r--lib/gitlab/git/ref.rb49
-rw-r--r--lib/gitlab/git/repository.rb1251
-rw-r--r--lib/gitlab/git/rev_list.rb42
-rw-r--r--lib/gitlab/git/tag.rb17
-rw-r--r--lib/gitlab/git/tree.rb104
-rw-r--r--lib/gitlab/git/util.rb18
-rw-r--r--lib/gitlab/git_access.rb155
-rw-r--r--lib/gitlab/git_access_wiki.rb4
-rw-r--r--lib/gitlab/github_import/base_formatter.rb4
-rw-r--r--lib/gitlab/github_import/client.rb16
-rw-r--r--lib/gitlab/github_import/importer.rb70
-rw-r--r--lib/gitlab/github_import/issuable_formatter.rb60
-rw-r--r--lib/gitlab/github_import/issue_formatter.rb52
-rw-r--r--lib/gitlab/github_import/milestone_formatter.rb12
-rw-r--r--lib/gitlab/github_import/project_creator.rb9
-rw-r--r--lib/gitlab/github_import/pull_request_formatter.rb60
-rw-r--r--lib/gitlab/gon_helper.rb3
-rw-r--r--lib/gitlab/import_export/import_export.yml2
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb2
-rw-r--r--lib/gitlab/import_export/relation_factory.rb39
-rw-r--r--lib/gitlab/import_sources.rb39
-rw-r--r--lib/gitlab/kubernetes.rb80
-rw-r--r--lib/gitlab/ldap/access.rb26
-rw-r--r--lib/gitlab/ldap/auth_hash.rb2
-rw-r--r--lib/gitlab/ldap/config.rb12
-rw-r--r--lib/gitlab/ldap/person.rb17
-rw-r--r--lib/gitlab/metrics/rack_middleware.rb15
-rw-r--r--lib/gitlab/middleware/multipart.rb103
-rw-r--r--lib/gitlab/popen.rb4
-rw-r--r--lib/gitlab/project_search_results.rb2
-rw-r--r--lib/gitlab/redis.rb2
-rw-r--r--lib/gitlab/regex.rb21
-rw-r--r--lib/gitlab/routing.rb6
-rw-r--r--lib/gitlab/serialize/ci/variables.rb27
-rw-r--r--lib/gitlab/sql/union.rb4
-rw-r--r--lib/gitlab/template/dockerfile_template.rb30
-rw-r--r--lib/gitlab/template/gitlab_ci_yml_template.rb10
-rw-r--r--lib/gitlab/themes.rb2
-rw-r--r--lib/gitlab/time_tracking_formatter.rb34
-rw-r--r--lib/gitlab/update_path_error.rb3
-rw-r--r--lib/gitlab/user_access.rb16
-rw-r--r--lib/gitlab/workhorse.rb19
-rw-r--r--lib/mattermost/client.rb41
-rw-r--r--lib/mattermost/command.rb10
-rw-r--r--lib/mattermost/error.rb3
-rw-r--r--lib/mattermost/session.rb160
-rw-r--r--lib/mattermost/team.rb7
-rw-r--r--lib/omniauth/strategies/bitbucket.rb41
-rw-r--r--lib/rouge/lexers/math.rb21
-rw-r--r--lib/support/nginx/gitlab7
-rw-r--r--lib/support/nginx/gitlab-ssl8
-rw-r--r--lib/tasks/dev.rake5
-rw-r--r--lib/tasks/gitlab/git.rake8
-rw-r--r--lib/tasks/gitlab/import.rake3
-rw-r--r--lib/tasks/gitlab/ldap.rake40
-rw-r--r--lib/tasks/gitlab/update_commit_count.rake20
-rw-r--r--lib/tasks/gitlab/update_templates.rake2
187 files changed, 6359 insertions, 1122 deletions
diff --git a/lib/api/api.rb b/lib/api/api.rb
index cec2702e44d..6cf6b501021 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -3,6 +3,8 @@ module API
include APIGuard
version 'v3', using: :path
+ before { allow_access_with_scope :api }
+
rescue_from Gitlab::Access::AccessDeniedError do
rack_response({ 'message' => '403 Forbidden' }.to_json, 403)
end
@@ -12,7 +14,11 @@ module API
end
# Retain 405 error rather than a 500 error for Grape 0.15.0+.
- # See: https://github.com/ruby-grape/grape/commit/252bfd27c320466ec3c0751812cf44245e97e5de
+ # https://github.com/ruby-grape/grape/blob/a3a28f5b5dfbb2797442e006dbffd750b27f2a76/UPGRADING.md#changes-to-method-not-allowed-routes
+ rescue_from Grape::Exceptions::MethodNotAllowed do |e|
+ error! e.message, e.status, e.headers
+ end
+
rescue_from Grape::Exceptions::Base do |e|
error! e.message, e.status, e.headers
end
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index 8cc7a26f1fa..df6db140d0e 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -6,6 +6,9 @@ module API
module APIGuard
extend ActiveSupport::Concern
+ PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
+ PRIVATE_TOKEN_PARAM = :private_token
+
included do |base|
# OAuth2 Resource Server Authentication
use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request|
@@ -44,27 +47,60 @@ module API
access_token = find_access_token
return nil unless access_token
- case validate_access_token(access_token, scopes)
- when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
+ case AccessTokenValidationService.new(access_token).validate(scopes: scopes)
+ when AccessTokenValidationService::INSUFFICIENT_SCOPE
raise InsufficientScopeError.new(scopes)
- when Oauth2::AccessTokenValidationService::EXPIRED
+ when AccessTokenValidationService::EXPIRED
raise ExpiredError
- when Oauth2::AccessTokenValidationService::REVOKED
+ when AccessTokenValidationService::REVOKED
raise RevokedError
- when Oauth2::AccessTokenValidationService::VALID
+ when AccessTokenValidationService::VALID
@current_user = User.find(access_token.resource_owner_id)
end
end
+ def find_user_by_private_token(scopes: [])
+ token_string = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
+
+ return nil unless token_string.present?
+
+ find_user_by_authentication_token(token_string) || find_user_by_personal_access_token(token_string, scopes)
+ end
+
def current_user
@current_user
end
+ # Set the authorization scope(s) allowed for the current request.
+ #
+ # Note: A call to this method adds to any previous scopes in place. This is done because
+ # `Grape` callbacks run from the outside-in: the top-level callback (API::API) runs first, then
+ # the next-level callback (API::API::Users, for example) runs. All these scopes are valid for the
+ # given endpoint (GET `/api/users` is accessible by the `api` and `read_user` scopes), and so they
+ # need to be stored.
+ def allow_access_with_scope(*scopes)
+ @scopes ||= []
+ @scopes.concat(scopes.map(&:to_s))
+ end
+
private
+ def find_user_by_authentication_token(token_string)
+ User.find_by_authentication_token(token_string)
+ end
+
+ def find_user_by_personal_access_token(token_string, scopes)
+ access_token = PersonalAccessToken.active.find_by_token(token_string)
+ return unless access_token
+
+ if AccessTokenValidationService.new(access_token).include_any_scope?(scopes)
+ User.find(access_token.user_id)
+ end
+ end
+
def find_access_token
@access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods)
end
@@ -72,10 +108,6 @@ module API
def doorkeeper_request
@doorkeeper_request ||= ActionDispatch::Request.new(env)
end
-
- def validate_access_token(access_token, scopes)
- Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes)
- end
end
module ClassMethods
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index cf2489dbb67..e6d707f3c3d 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -44,7 +44,6 @@ module API
detail 'This feature was introduced in GitLab 8.13'
end
params do
- requires :id, type: Integer, desc: 'The project ID'
requires :branch_name, type: String, desc: 'The name of branch'
requires :commit_message, type: String, desc: 'Commit message'
requires :actions, type: Array[Hash], desc: 'Actions to perform in commit'
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 01c0f5072ba..9f59939e9ae 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -78,21 +78,21 @@ module API
expose :container_registry_enabled
# Expose old field names with the new permissions methods to keep API compatible
- expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:user]) }
- expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:user]) }
- expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:user]) }
- expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:user]) }
- expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:user]) }
+ expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) }
+ expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) }
+ expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) }
+ expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) }
+ expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) }
expose :created_at, :last_activity_at
expose :shared_runners_enabled
expose :lfs_enabled?, as: :lfs_enabled
expose :creator_id
- expose :namespace
+ expose :namespace, using: 'API::Entities::Namespace'
expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? }
expose :avatar_url
expose :star_count, :forks_count
- expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:user]) && project.default_issues_tracker? }
+ expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) && project.default_issues_tracker? }
expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
expose :public_builds
expose :shared_with_groups do |project, options|
@@ -101,6 +101,16 @@ module API
expose :only_allow_merge_if_build_succeeds
expose :request_access_enabled
expose :only_allow_merge_if_all_discussions_are_resolved
+
+ expose :statistics, using: 'API::Entities::ProjectStatistics', if: :statistics
+ end
+
+ class ProjectStatistics < Grape::Entity
+ expose :commit_count
+ expose :storage_size
+ expose :repository_size
+ expose :lfs_objects_size
+ expose :build_artifacts_size
end
class Member < UserBasic
@@ -127,6 +137,15 @@ module API
expose :avatar_url
expose :web_url
expose :request_access_enabled
+
+ expose :statistics, if: :statistics do
+ with_options format_with: -> (value) { value.to_i } do
+ expose :storage_size
+ expose :repository_size
+ expose :lfs_objects_size
+ expose :build_artifacts_size
+ end
+ end
end
class GroupDetail < Group
@@ -249,6 +268,13 @@ module API
end
end
+ class IssuableTimeStats < Grape::Entity
+ expose :time_estimate
+ expose :total_time_spent
+ expose :human_time_estimate
+ expose :human_total_time_spent
+ end
+
class ExternalIssue < Grape::Entity
expose :title
expose :id
@@ -298,7 +324,7 @@ module API
end
class SSHKey < Grape::Entity
- expose :id, :title, :key, :created_at
+ expose :id, :title, :key, :created_at, :can_push
end
class SSHKeyWithUser < SSHKey
@@ -391,7 +417,7 @@ module API
end
class Namespace < Grape::Entity
- expose :id, :path, :kind
+ expose :id, :name, :path, :kind
end
class MemberAccess < Grape::Entity
@@ -440,12 +466,12 @@ module API
class ProjectWithAccess < Project
expose :permissions do
expose :project_access, using: Entities::ProjectAccess do |project, options|
- project.project_members.find_by(user_id: options[:user].id)
+ project.project_members.find_by(user_id: options[:current_user].id)
end
expose :group_access, using: Entities::GroupAccess do |project, options|
if project.group
- project.group.group_members.find_by(user_id: options[:user].id)
+ project.group.group_members.find_by(user_id: options[:current_user].id)
end
end
end
@@ -546,6 +572,8 @@ module API
expose :repository_storages
expose :koding_enabled
expose :koding_url
+ expose :plantuml_enabled
+ expose :plantuml_url
end
class Release < Grape::Entity
@@ -629,7 +657,7 @@ module API
end
class EnvironmentBasic < Grape::Entity
- expose :id, :name, :external_url
+ expose :id, :name, :slug, :external_url
end
class Environment < EnvironmentBasic
diff --git a/lib/api/environments.rb b/lib/api/environments.rb
index 80bbd9bb6e4..1a7e68f0528 100644
--- a/lib/api/environments.rb
+++ b/lib/api/environments.rb
@@ -1,6 +1,7 @@
module API
# Environments RESTfull API endpoints
class Environments < Grape::API
+ include ::API::Helpers::CustomValidators
include PaginationParams
before { authenticate! }
@@ -29,6 +30,7 @@ module API
params do
requires :name, type: String, desc: 'The name of the environment to be created'
optional :external_url, type: String, desc: 'URL on which this deployment is viewable'
+ optional :slug, absence: { message: "is automatically generated and cannot be changed" }
end
post ':id/environments' do
authorize! :create_environment, user_project
@@ -50,6 +52,7 @@ module API
requires :environment_id, type: Integer, desc: 'The environment ID'
optional :name, type: String, desc: 'The new environment name'
optional :external_url, type: String, desc: 'The new URL on which this deployment is viewable'
+ optional :slug, absence: { message: "is automatically generated and cannot be changed" }
end
put ':id/environments/:environment_id' do
authorize! :update_environment, user_project
diff --git a/lib/api/files.rb b/lib/api/files.rb
index 28f306e45f3..2e79e22e649 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -1,8 +1,6 @@
module API
# Projects API
class Files < Grape::API
- before { authenticate! }
-
helpers do
def commit_params(attrs)
{
@@ -70,7 +68,7 @@ module API
ref: params[:ref],
blob_id: blob.id,
commit_id: commit.id,
- last_commit_id: repo.last_commit_for_path(commit.sha, params[:file_path]).id
+ last_commit_id: repo.last_commit_id_for_path(commit.sha, params[:file_path])
}
end
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 9b9d3df7435..7682d286866 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -11,6 +11,20 @@ module API
optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group'
optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
end
+
+ params :statistics_params do
+ optional :statistics, type: Boolean, default: false, desc: 'Include project statistics'
+ end
+
+ def present_groups(groups, options = {})
+ options = options.reverse_merge(
+ with: Entities::Group,
+ current_user: current_user,
+ )
+
+ groups = groups.with_statistics if options[:statistics]
+ present paginate(groups), options
+ end
end
resource :groups do
@@ -18,6 +32,7 @@ module API
success Entities::Group
end
params do
+ use :statistics_params
optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list'
optional :all_available, type: Boolean, desc: 'Show all group that you have access to'
optional :search, type: String, desc: 'Search for a specific group'
@@ -38,7 +53,7 @@ module API
groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
groups = groups.reorder(params[:order_by] => params[:sort])
- present paginate(groups), with: Entities::Group
+ present_groups groups, statistics: params[:statistics] && current_user.is_admin?
end
desc 'Get list of owned groups for authenticated user' do
@@ -46,10 +61,10 @@ module API
end
params do
use :pagination
+ use :statistics_params
end
get '/owned' do
- groups = current_user.owned_groups
- present paginate(groups), with: Entities::Group, user: current_user
+ present_groups current_user.owned_groups, statistics: params[:statistics]
end
desc 'Create a group. Available only for users who can create groups.' do
@@ -66,7 +81,7 @@ module API
group = ::Groups::CreateService.new(current_user, declared_params(include_missing: false)).execute
if group.persisted?
- present group, with: Entities::Group
+ present group, with: Entities::Group, current_user: current_user
else
render_api_error!("Failed to save group #{group.errors.messages}", 400)
end
@@ -92,7 +107,7 @@ module API
authorize! :admin_group, group
if ::Groups::UpdateService.new(group, current_user, declared_params(include_missing: false)).execute
- present group, with: Entities::GroupDetail
+ present group, with: Entities::GroupDetail, current_user: current_user
else
render_validation_error!(group)
end
@@ -103,7 +118,7 @@ module API
end
get ":id" do
group = find_group!(params[:id])
- present group, with: Entities::GroupDetail
+ present group, with: Entities::GroupDetail, current_user: current_user
end
desc 'Remove a group.'
@@ -134,23 +149,23 @@ module API
projects = GroupProjectsFinder.new(group).execute(current_user)
projects = filter_projects(projects)
entity = params[:simple] ? Entities::BasicProjectDetails : Entities::Project
- present paginate(projects), with: entity, user: current_user
+ present paginate(projects), with: entity, current_user: current_user
end
desc 'Transfer a project to the group namespace. Available only for admin.' do
success Entities::GroupDetail
end
params do
- requires :project_id, type: String, desc: 'The ID of the project'
+ requires :project_id, type: String, desc: 'The ID or path of the project'
end
post ":id/projects/:project_id" do
authenticated_as_admin!
- group = Group.find_by(id: params[:id])
- project = Project.find(params[:project_id])
+ group = find_group!(params[:id])
+ project = find_project!(params[:project_id])
result = ::Projects::TransferService.new(project, current_user).execute(group)
if result
- present group, with: Entities::GroupDetail
+ present group, with: Entities::GroupDetail, current_user: current_user
else
render_api_error!("Failed to transfer project #{project.errors.messages}", 400)
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 3324001c468..49c5f0652ab 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -1,9 +1,8 @@
module API
module Helpers
include Gitlab::Utils
+ include Helpers::Pagination
- PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
- PRIVATE_TOKEN_PARAM = :private_token
SUDO_HEADER = "HTTP_SUDO"
SUDO_PARAM = :sudo
@@ -87,10 +86,8 @@ module API
IssuesFinder.new(current_user, project_id: user_project.id).find(id)
end
- def paginate(relation)
- relation.page(params[:page]).per(params[:per_page].to_i).tap do |data|
- add_pagination_headers(data)
- end
+ def find_project_merge_request(id)
+ MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id)
end
def authenticate!
@@ -98,7 +95,7 @@ module API
end
def authenticate_non_get!
- authenticate! unless %w[GET HEAD].include?(route.route_method)
+ authenticate! unless %w[GET HEAD].include?(route.request_method)
end
def authenticate_by_gitlab_shell_token!
@@ -250,7 +247,7 @@ module API
rack_response({ 'message' => '500 Internal Server Error' }.to_json, 500)
end
- # Projects helpers
+ # project helpers
def filter_projects(projects)
if params[:search].present?
@@ -301,14 +298,14 @@ module API
header['X-Sendfile'] = path
body
else
- file FileStreamer.new(path)
+ path
end
end
private
def private_token
- params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]
+ params[APIGuard::PRIVATE_TOKEN_PARAM] || env[APIGuard::PRIVATE_TOKEN_HEADER]
end
def warden
@@ -323,18 +320,11 @@ module API
warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD'])
end
- def find_user_by_private_token
- token = private_token
- return nil unless token.present?
-
- User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
- end
-
def initial_current_user
return @initial_current_user if defined?(@initial_current_user)
- @initial_current_user ||= find_user_by_private_token
- @initial_current_user ||= doorkeeper_guard
+ @initial_current_user ||= find_user_by_private_token(scopes: @scopes)
+ @initial_current_user ||= doorkeeper_guard(scopes: @scopes)
@initial_current_user ||= find_user_from_warden
unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed?
@@ -370,38 +360,6 @@ module API
@sudo_identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER]
end
- def add_pagination_headers(paginated_data)
- header 'X-Total', paginated_data.total_count.to_s
- header 'X-Total-Pages', paginated_data.total_pages.to_s
- header 'X-Per-Page', paginated_data.limit_value.to_s
- header 'X-Page', paginated_data.current_page.to_s
- header 'X-Next-Page', paginated_data.next_page.to_s
- header 'X-Prev-Page', paginated_data.prev_page.to_s
- header 'Link', pagination_links(paginated_data)
- end
-
- def pagination_links(paginated_data)
- request_url = request.url.split('?').first
- request_params = params.clone
- request_params[:per_page] = paginated_data.limit_value
-
- links = []
-
- request_params[:page] = paginated_data.current_page - 1
- links << %(<#{request_url}?#{request_params.to_query}>; rel="prev") unless paginated_data.first_page?
-
- request_params[:page] = paginated_data.current_page + 1
- links << %(<#{request_url}?#{request_params.to_query}>; rel="next") unless paginated_data.last_page?
-
- request_params[:page] = 1
- links << %(<#{request_url}?#{request_params.to_query}>; rel="first")
-
- request_params[:page] = paginated_data.total_pages
- links << %(<#{request_url}?#{request_params.to_query}>; rel="last")
-
- links.join(', ')
- end
-
def secret_token
Gitlab::Shell.secret_token
end
diff --git a/lib/api/helpers/custom_validators.rb b/lib/api/helpers/custom_validators.rb
new file mode 100644
index 00000000000..0a8f3073a50
--- /dev/null
+++ b/lib/api/helpers/custom_validators.rb
@@ -0,0 +1,14 @@
+module API
+ module Helpers
+ module CustomValidators
+ class Absence < Grape::Validations::Base
+ def validate_param!(attr_name, params)
+ return if params.respond_to?(:key?) && !params.key?(attr_name)
+ raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:absence)
+ end
+ end
+ end
+ end
+end
+
+Grape::Validations.register_validator(:absence, ::API::Helpers::CustomValidators::Absence)
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index eb223c1101d..e8975eb57e0 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -52,6 +52,14 @@ module API
:push_code
]
end
+
+ def parse_allowed_environment_variables
+ return if params[:env].blank?
+
+ JSON.parse(params[:env])
+
+ rescue JSON::ParserError
+ end
end
end
end
diff --git a/lib/api/helpers/pagination.rb b/lib/api/helpers/pagination.rb
new file mode 100644
index 00000000000..2199eea7e5f
--- /dev/null
+++ b/lib/api/helpers/pagination.rb
@@ -0,0 +1,45 @@
+module API
+ module Helpers
+ module Pagination
+ def paginate(relation)
+ relation.page(params[:page]).per(params[:per_page].to_i).tap do |data|
+ add_pagination_headers(data)
+ end
+ end
+
+ private
+
+ def add_pagination_headers(paginated_data)
+ header 'X-Total', paginated_data.total_count.to_s
+ header 'X-Total-Pages', paginated_data.total_pages.to_s
+ header 'X-Per-Page', paginated_data.limit_value.to_s
+ header 'X-Page', paginated_data.current_page.to_s
+ header 'X-Next-Page', paginated_data.next_page.to_s
+ header 'X-Prev-Page', paginated_data.prev_page.to_s
+ header 'Link', pagination_links(paginated_data)
+ end
+
+ def pagination_links(paginated_data)
+ request_url = request.url.split('?').first
+ request_params = params.clone
+ request_params[:per_page] = paginated_data.limit_value
+
+ links = []
+
+ request_params[:page] = paginated_data.current_page - 1
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="prev") unless paginated_data.first_page?
+
+ request_params[:page] = paginated_data.current_page + 1
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="next") unless paginated_data.last_page?
+
+ request_params[:page] = 1
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="first")
+
+ request_params[:page] = paginated_data.total_pages
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="last")
+
+ links.join(', ')
+ end
+ end
+ end
+end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 7087ce11401..d235977fbd8 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -28,11 +28,17 @@ module API
protocol = params[:protocol]
+ actor.update_last_used_at if actor.is_a?(Key)
+
access =
if wiki?
Gitlab::GitAccessWiki.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
else
- Gitlab::GitAccess.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
+ Gitlab::GitAccess.new(actor,
+ project,
+ protocol,
+ authentication_abilities: ssh_authentication_abilities,
+ env: parse_allowed_environment_variables)
end
access_status = access.check(params[:action], params[:changes])
@@ -57,6 +63,8 @@ module API
status 200
key = Key.find(params[:key_id])
+ key.update_last_used_at
+
token_handler = Gitlab::LfsToken.new(key)
{
@@ -99,7 +107,9 @@ module API
key = Key.find_by(id: params[:key_id])
- unless key
+ if key
+ key.update_last_used_at
+ else
return { 'success' => false, 'message' => 'Could not find the given key' }
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index c9124649cbb..fe016c1ec0a 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -5,28 +5,36 @@ module API
before { authenticate! }
helpers do
- def filter_issues_state(issues, state)
- case state
- when 'opened' then issues.opened
- when 'closed' then issues.closed
- else issues
- end
- end
+ def find_issues(args = {})
+ args = params.merge(args)
- def filter_issues_labels(issues, labels)
- issues.includes(:labels).where('labels.title' => labels.split(','))
- end
+ args.delete(:id)
+ args[:milestone_title] = args.delete(:milestone)
+
+ match_all_labels = args.delete(:match_all_labels)
+ labels = args.delete(:labels)
+ args[:label_name] = labels if match_all_labels
+
+ args[:search] = "#{Issue.reference_prefix}#{args.delete(:iid)}" if args.key?(:iid)
+
+ issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations
- def filter_issues_milestone(issues, milestone)
- issues.includes(:milestone).where('milestones.title' => milestone)
+ # TODO: Remove in 9.0 pass `label_name: args.delete(:labels)` to IssuesFinder
+ if !match_all_labels && labels.present?
+ issues = issues.includes(:labels).where('labels.title' => labels.split(','))
+ end
+
+ issues.reorder(args[:order_by] => args[:sort])
end
params :issues_params do
optional :labels, type: String, desc: 'Comma-separated list of label names'
+ optional :milestone, type: String, desc: 'Milestone title'
optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
desc: 'Return issues ordered by `created_at` or `updated_at` fields.'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return issues sorted in `asc` or `desc` order.'
+ optional :milestone, type: String, desc: 'Return issues for a specific milestone'
use :pagination
end
@@ -37,8 +45,6 @@ module API
optional :labels, type: String, desc: 'Comma-separated list of label names'
optional :due_date, type: String, desc: 'Date time string in the format YEAR-MONTH-DAY'
optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential'
- optional :state_event, type: String, values: %w[open close],
- desc: 'State of the issue'
end
end
@@ -52,10 +58,7 @@ module API
use :issues_params
end
get do
- issues = current_user.issues.inc_notes_with_associations
- issues = filter_issues_state(issues, params[:state])
- issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
- issues = issues.reorder(params[:order_by] => params[:sort])
+ issues = find_issues(scope: 'authored')
present paginate(issues), with: Entities::Issue, current_user: current_user
end
@@ -74,15 +77,10 @@ module API
use :issues_params
end
get ":id/issues" do
- group = find_group!(params.delete(:id))
-
- params[:group_id] = group.id
- params[:milestone_title] = params.delete(:milestone)
- params[:label_name] = params.delete(:labels)
+ group = find_group!(params[:id])
- issues = IssuesFinder.new(current_user, params).execute
+ issues = find_issues(group_id: group.id, state: params[:state] || 'opened', match_all_labels: true)
- issues = issues.reorder(params[:order_by] => params[:sort])
present paginate(issues), with: Entities::Issue, current_user: current_user
end
end
@@ -91,26 +89,22 @@ module API
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects do
+ include TimeTrackingEndpoints
+
desc 'Get a list of project issues' do
success Entities::Issue
end
params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues'
- optional :iid, type: Integer, desc: 'The IID of the issue'
+ optional :iid, type: Integer, desc: 'Return the issue having the given `iid`'
use :issues_params
end
get ":id/issues" do
- issues = IssuesFinder.new(current_user, project_id: user_project.id).execute.inc_notes_with_associations
- issues = filter_issues_state(issues, params[:state])
- issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
- issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil?
+ project = find_project(params[:id])
- unless params[:milestone].nil?
- issues = filter_issues_milestone(issues, params[:milestone])
- end
+ issues = find_issues(project_id: project.id)
- issues = issues.reorder(params[:order_by] => params[:sort])
present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project
end
@@ -172,6 +166,7 @@ module API
optional :title, type: String, desc: 'The title of an issue'
optional :updated_at, type: DateTime,
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_id, :milestone_id,
:labels, :created_at, :due_date, :confidential, :state_event
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 5d1fe22f2df..e77af4b7a0d 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -10,6 +10,8 @@ module API
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects do
+ include TimeTrackingEndpoints
+
helpers do
def handle_merge_request_errors!(errors)
if errors[:project_access].any?
@@ -96,7 +98,7 @@ module API
requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
end
delete ":id/merge_requests/:merge_request_id" do
- merge_request = user_project.merge_requests.find_by(id: params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
authorize!(:destroy_merge_request, merge_request)
merge_request.destroy
@@ -116,7 +118,7 @@ module API
success Entities::MergeRequest
end
get path do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
authorize! :read_merge_request, merge_request
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
end
@@ -125,7 +127,7 @@ module API
success Entities::RepoCommit
end
get "#{path}/commits" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
authorize! :read_merge_request, merge_request
present merge_request.commits, with: Entities::RepoCommit
end
@@ -134,7 +136,7 @@ module API
success Entities::MergeRequestChanges
end
get "#{path}/changes" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
authorize! :read_merge_request, merge_request
present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
end
@@ -153,7 +155,7 @@ module API
:remove_source_branch
end
put path do
- merge_request = user_project.merge_requests.find(params.delete(:merge_request_id))
+ merge_request = find_project_merge_request(params.delete(:merge_request_id))
authorize! :update_merge_request, merge_request
mr_params = declared_params(include_missing: false)
@@ -180,7 +182,7 @@ module API
optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
end
put "#{path}/merge" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
# Merge request can not be merged
# because user dont have permissions to push into target branch
@@ -216,7 +218,7 @@ module API
success Entities::MergeRequest
end
post "#{path}/cancel_merge_when_build_succeeds" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user)
@@ -233,7 +235,7 @@ module API
use :pagination
end
get "#{path}/comments" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
authorize! :read_merge_request, merge_request
@@ -248,7 +250,7 @@ module API
requires :note, type: String, desc: 'The text of the comment'
end
post "#{path}/comments" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
authorize! :create_note, merge_request
opts = {
@@ -273,7 +275,7 @@ module API
use :pagination
end
get "#{path}/closes_issues" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
present paginate(issues), with: issue_entity(user_project), current_user: current_user
end
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index d0faf17714b..284e4cf549a 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -69,8 +69,6 @@ module API
optional :created_at, type: String, desc: 'The creation date of the note'
end
post ":id/#{noteables_str}/:noteable_id/notes" do
- required_attributes! [:body]
-
opts = {
note: params[:body],
noteable_type: noteables_str.classify,
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index dcc0fb7a911..cb679e6658a 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -15,7 +15,7 @@ module API
optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events"
optional :build_events, type: Boolean, desc: "Trigger hook on build events"
optional :pipeline_events, type: Boolean, desc: "Trigger hook on pipeline events"
- optional :wiki_events, type: Boolean, desc: "Trigger hook on wiki events"
+ optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events"
optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook"
optional :token, type: String, desc: "Secret token to validate received payloads; this will not be returned in the response"
end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 2929d2157dc..941f47114a4 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -40,6 +40,15 @@ module API
resource :projects do
helpers do
+ params :collection_params do
+ use :sort_params
+ use :filter_params
+ use :pagination
+
+ optional :simple, type: Boolean, default: false,
+ desc: 'Return only the ID, URL, name, and path of each project'
+ end
+
params :sort_params do
optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at],
default: 'created_at', desc: 'Return projects ordered by field'
@@ -52,97 +61,94 @@ module API
optional :visibility, type: String, values: %w[public internal private],
desc: 'Limit by visibility'
optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
- use :sort_params
+ end
+
+ params :statistics_params do
+ optional :statistics, type: Boolean, default: false, desc: 'Include project statistics'
end
params :create_params do
optional :namespace_id, type: Integer, desc: 'Namespace ID for the new project. Default to the user namespace.'
optional :import_url, type: String, desc: 'URL from which the project is imported'
end
+
+ def present_projects(projects, options = {})
+ options = options.reverse_merge(
+ with: Entities::Project,
+ current_user: current_user,
+ simple: params[:simple],
+ )
+
+ projects = filter_projects(projects)
+ projects = projects.with_statistics if options[:statistics]
+ options[:with] = Entities::BasicProjectDetails if options[:simple]
+
+ present paginate(projects), options
+ end
end
desc 'Get a list of visible projects for authenticated user' do
success Entities::BasicProjectDetails
end
params do
- optional :simple, type: Boolean, default: false,
- desc: 'Return only the ID, URL, name, and path of each project'
- use :filter_params
- use :pagination
+ use :collection_params
end
get '/visible' do
- projects = ProjectsFinder.new.execute(current_user)
- projects = filter_projects(projects)
- entity = params[:simple] || !current_user ? Entities::BasicProjectDetails : Entities::ProjectWithAccess
-
- present paginate(projects), with: entity, user: current_user
+ entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails
+ present_projects ProjectsFinder.new.execute(current_user), with: entity
end
desc 'Get a projects list for authenticated user' do
success Entities::BasicProjectDetails
end
params do
- optional :simple, type: Boolean, default: false,
- desc: 'Return only the ID, URL, name, and path of each project'
- use :filter_params
- use :pagination
+ use :collection_params
end
get do
authenticate!
- projects = current_user.authorized_projects
- projects = filter_projects(projects)
- entity = params[:simple] ? Entities::BasicProjectDetails : Entities::ProjectWithAccess
-
- present paginate(projects), with: entity, user: current_user
+ present_projects current_user.authorized_projects,
+ with: Entities::ProjectWithAccess
end
desc 'Get an owned projects list for authenticated user' do
success Entities::BasicProjectDetails
end
params do
- use :filter_params
- use :pagination
+ use :collection_params
+ use :statistics_params
end
get '/owned' do
authenticate!
- projects = current_user.owned_projects
- projects = filter_projects(projects)
-
- present paginate(projects), with: Entities::ProjectWithAccess, user: current_user
+ present_projects current_user.owned_projects,
+ with: Entities::ProjectWithAccess,
+ statistics: params[:statistics]
end
desc 'Gets starred project for the authenticated user' do
success Entities::BasicProjectDetails
end
params do
- use :filter_params
- use :pagination
+ use :collection_params
end
get '/starred' do
authenticate!
- projects = current_user.viewable_starred_projects
- projects = filter_projects(projects)
-
- present paginate(projects), with: Entities::Project, user: current_user
+ present_projects current_user.viewable_starred_projects
end
desc 'Get all projects for admin user' do
success Entities::BasicProjectDetails
end
params do
- use :filter_params
- use :pagination
+ use :collection_params
+ use :statistics_params
end
get '/all' do
authenticated_as_admin!
- projects = Project.all
- projects = filter_projects(projects)
-
- present paginate(projects), with: Entities::ProjectWithAccess, user: current_user
+ present_projects Project.all, with: Entities::ProjectWithAccess, statistics: params[:statistics]
end
desc 'Search for projects the current user has access to' do
@@ -153,7 +159,7 @@ module API
use :sort_params
use :pagination
end
- get "/search/:query" do
+ get "/search/:query", requirements: { query: /[^\/]+/ } do
search_service = Search::GlobalService.new(current_user, search: params[:query]).execute
projects = search_service.objects('projects', params[:page])
projects = projects.reorder(params[:order_by] => params[:sort])
@@ -221,7 +227,7 @@ module API
end
get ":id" do
entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails
- present user_project, with: entity, user: current_user,
+ present user_project, with: entity, current_user: current_user,
user_can_admin_project: can?(current_user, :admin_project, user_project)
end
@@ -289,13 +295,13 @@ module API
authorize! :rename_project, user_project if attrs[:name].present?
authorize! :change_visibility_level, user_project if attrs[:visibility_level].present?
- ::Projects::UpdateService.new(user_project, current_user, attrs).execute
+ result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
- if user_project.errors.any?
- render_validation_error!(user_project)
- else
+ if result[:status] == :success
present user_project, with: Entities::Project,
user_can_admin_project: can?(current_user, :admin_project, user_project)
+ else
+ render_validation_error!(user_project)
end
end
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index c287ee34a68..4ca6646a6f1 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -2,7 +2,6 @@ require 'mime/types'
module API
class Repositories < Grape::API
- before { authenticate! }
before { authorize! :download_code, user_project }
params do
@@ -79,8 +78,6 @@ module API
optional :format, type: String, desc: 'The archive format'
end
get ':id/repository/archive', requirements: { format: Gitlab::Regex.archive_formats_regex } do
- authorize! :download_code, user_project
-
begin
send_git_archive user_project.repository, ref: params[:sha], format: params[:format]
rescue
@@ -96,7 +93,6 @@ module API
requires :to, type: String, desc: 'The commit, branch name, or tag name to stop comparison'
end
get ':id/repository/compare' do
- authorize! :download_code, user_project
compare = Gitlab::Git::Compare.new(user_project.repository.raw_repository, params[:from], params[:to])
present compare, with: Entities::Compare
end
@@ -105,8 +101,6 @@ module API
success Entities::Contributor
end
get ':id/repository/contributors' do
- authorize! :download_code, user_project
-
begin
present user_project.repository.contributors,
with: Entities::Contributor
diff --git a/lib/api/services.rb b/lib/api/services.rb
index fde2e2746f1..3a9dfbb237c 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -351,6 +351,33 @@ module API
desc: 'The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`'
}
],
+
+ 'kubernetes' => [
+ {
+ required: true,
+ name: :namespace,
+ type: String,
+ desc: 'The Kubernetes namespace to use'
+ },
+ {
+ required: true,
+ name: :api_url,
+ type: String,
+ desc: 'The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com'
+ },
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The service token to authenticate against the Kubernetes cluster with'
+ },
+ {
+ required: false,
+ name: :ca_pem,
+ type: String,
+ desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)'
+ },
+ ],
'mattermost-slash-commands' => [
{
required: true,
@@ -359,6 +386,14 @@ module API
desc: 'The Mattermost token'
}
],
+ 'slack-slash-commands' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Slack token'
+ }
+ ],
'pipelines-email' => [
{
required: true,
@@ -465,6 +500,14 @@ module API
desc: 'The channel name'
}
],
+ 'mattermost' => [
+ {
+ required: true,
+ name: :webhook,
+ type: String,
+ desc: 'The Mattermost webhook. e.g. http://mattermost_host/hooks/...'
+ }
+ ],
'teamcity' => [
{
required: true,
@@ -500,6 +543,13 @@ module API
type: String,
desc: 'The Mattermost token'
}
+ ],
+ 'slack-slash-commands' => [
+ {
+ name: :token,
+ type: String,
+ desc: 'The Slack token'
+ }
]
}.freeze
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index c4cb1c7924a..c5eff16a5de 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -9,23 +9,121 @@ module API
end
end
- # Get current applicaiton settings
- #
- # Example Request:
- # GET /application/settings
+ desc 'Get the current application settings' do
+ success Entities::ApplicationSetting
+ end
get "application/settings" do
present current_settings, with: Entities::ApplicationSetting
end
- # Modify application settings
- #
- # Example Request:
- # PUT /application/settings
+ desc 'Modify application settings' do
+ success Entities::ApplicationSetting
+ end
+ params do
+ optional :default_branch_protection, type: Integer, values: [0, 1, 2], desc: 'Determine if developers can push to master'
+ optional :default_project_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default project visibility'
+ optional :default_snippet_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default snippet visibility'
+ optional :default_group_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default group visibility'
+ optional :restricted_visibility_levels, type: Array[String], desc: 'Selected levels cannot be used by non-admin users for projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.'
+ optional :import_sources, type: Array[String], values: %w[github bitbucket gitlab google_code fogbugz git gitlab_project],
+ desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com'
+ optional :disabled_oauth_sign_in_sources, type: Array[String], desc: 'Disable certain OAuth sign-in sources'
+ optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.'
+ optional :gravatar_enabled, type: Boolean, desc: 'Flag indicating if the Gravatar service is enabled'
+ optional :default_projects_limit, type: Integer, desc: 'The maximum number of personal projects'
+ optional :max_attachment_size, type: Integer, desc: 'Maximum attachment size in MB'
+ optional :session_expire_delay, type: Integer, desc: 'Session duration in minutes. GitLab restart is required to apply changes.'
+ optional :user_oauth_applications, type: Boolean, desc: 'Allow users to register any application to use GitLab as an OAuth provider'
+ optional :user_default_external, type: Boolean, desc: 'Newly registered users will by default be external'
+ optional :signup_enabled, type: Boolean, desc: 'Flag indicating if sign up is enabled'
+ optional :send_user_confirmation_email, type: Boolean, desc: 'Send confirmation email on sign-up'
+ optional :domain_whitelist, type: String, desc: 'ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
+ optional :domain_blacklist_enabled, type: Boolean, desc: 'Enable domain blacklist for sign ups'
+ given domain_blacklist_enabled: ->(val) { val } do
+ requires :domain_blacklist, type: String, desc: 'Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
+ end
+ optional :after_sign_up_text, type: String, desc: 'Text shown after sign up'
+ optional :signin_enabled, type: Boolean, desc: 'Flag indicating if sign in is enabled'
+ optional :require_two_factor_authentication, type: Boolean, desc: 'Require all users to setup Two-factor authentication'
+ given require_two_factor_authentication: ->(val) { val } do
+ requires :two_factor_grace_period, type: Integer, desc: 'Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication'
+ end
+ optional :home_page_url, type: String, desc: 'We will redirect non-logged in users to this page'
+ optional :after_sign_out_path, type: String, desc: 'We will redirect users to this page after they sign out'
+ optional :sign_in_text, type: String, desc: 'The sign in text of the GitLab application'
+ optional :help_page_text, type: String, desc: 'Custom text displayed on the help page'
+ optional :shared_runners_enabled, type: Boolean, desc: 'Enable shared runners for new projects'
+ given shared_runners_enabled: ->(val) { val } do
+ requires :shared_runners_text, type: String, desc: 'Shared runners text '
+ end
+ optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size each build's artifacts can have"
+ optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)'
+ optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics'
+ given metrics_enabled: ->(val) { val } do
+ requires :metrics_host, type: String, desc: 'The InfluxDB host'
+ requires :metrics_port, type: Integer, desc: 'The UDP port to use for connecting to InfluxDB'
+ requires :metrics_pool_size, type: Integer, desc: 'The amount of InfluxDB connections to open'
+ requires :metrics_timeout, type: Integer, desc: 'The amount of seconds after which an InfluxDB connection will time out'
+ requires :metrics_method_call_threshold, type: Integer, desc: 'A method call is only tracked when it takes longer to complete than the given amount of milliseconds.'
+ requires :metrics_sample_interval, type: Integer, desc: 'The sampling interval in seconds'
+ requires :metrics_packet_size, type: Integer, desc: 'The amount of points to store in a single UDP packet'
+ end
+ optional :sidekiq_throttling_enabled, type: Boolean, desc: 'Enable Sidekiq Job Throttling'
+ given sidekiq_throttling_enabled: ->(val) { val } do
+ requires :sidekiq_throttling_queus, type: Array[String], desc: 'Choose which queues you wish to throttle'
+ requires :sidekiq_throttling_factor, type: Float, desc: 'The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.'
+ end
+ optional :recaptcha_enabled, type: Boolean, desc: 'Helps prevent bots from creating accounts'
+ given recaptcha_enabled: ->(val) { val } do
+ requires :recaptcha_site_key, type: String, desc: 'Generate site key at http://www.google.com/recaptcha'
+ requires :recaptcha_private_key, type: String, desc: 'Generate private key at http://www.google.com/recaptcha'
+ end
+ optional :akismet_enabled, type: Boolean, desc: 'Helps prevent bots from creating issues'
+ given akismet_enabled: ->(val) { val } do
+ requires :akismet_api_key, type: String, desc: 'Generate API key at http://www.akismet.com'
+ end
+ optional :admin_notification_email, type: String, desc: 'Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.'
+ optional :sentry_enabled, type: Boolean, desc: 'Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here: https://getsentry.com'
+ given sentry_enabled: ->(val) { val } do
+ requires :sentry_dsn, type: String, desc: 'Sentry Data Source Name'
+ end
+ optional :repository_storage, type: String, desc: 'Storage paths for new projects'
+ optional :repository_checks_enabled, type: Boolean, desc: "GitLab will periodically run 'git fsck' in all project and wiki repositories to look for silent disk corruption issues."
+ optional :koding_enabled, type: Boolean, desc: 'Enable Koding'
+ given koding_enabled: ->(val) { val } do
+ requires :koding_url, type: String, desc: 'The Koding team URL'
+ end
+ optional :plantuml_enabled, type: Boolean, desc: 'Enable PlantUML'
+ given plantuml_enabled: ->(val) { val } do
+ requires :plantuml_url, type: String, desc: 'The PlantUML server URL'
+ end
+ optional :version_check_enabled, type: Boolean, desc: 'Let GitLab inform you when an update is available.'
+ optional :email_author_in_body, type: Boolean, desc: 'Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead.'
+ optional :html_emails_enabled, type: Boolean, desc: 'By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format.'
+ optional :housekeeping_enabled, type: Boolean, desc: 'Enable automatic repository housekeeping (git repack, git gc)'
+ given housekeeping_enabled: ->(val) { val } do
+ requires :housekeeping_bitmaps_enabled, type: Boolean, desc: "Creating pack file bitmaps makes housekeeping take a little longer but bitmaps should accelerate 'git clone' performance."
+ requires :housekeeping_incremental_repack_period, type: Integer, desc: "Number of Git pushes after which an incremental 'git repack' is run."
+ requires :housekeeping_full_repack_period, type: Integer, desc: "Number of Git pushes after which a full 'git repack' is run."
+ requires :housekeeping_gc_period, type: Integer, desc: "Number of Git pushes after which 'git gc' is run."
+ end
+ at_least_one_of :default_branch_protection, :default_project_visibility, :default_snippet_visibility,
+ :default_group_visibility, :restricted_visibility_levels, :import_sources,
+ :enabled_git_access_protocol, :gravatar_enabled, :default_projects_limit,
+ :max_attachment_size, :session_expire_delay, :disabled_oauth_sign_in_sources,
+ :user_oauth_applications, :user_default_external, :signup_enabled,
+ :send_user_confirmation_email, :domain_whitelist, :domain_blacklist_enabled,
+ :after_sign_up_text, :signin_enabled, :require_two_factor_authentication,
+ :home_page_url, :after_sign_out_path, :sign_in_text, :help_page_text,
+ :shared_runners_enabled, :max_artifacts_size, :container_registry_token_expire_delay,
+ :metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled,
+ :akismet_enabled, :admin_notification_email, :sentry_enabled,
+ :repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled,
+ :version_check_enabled, :email_author_in_body, :html_emails_enabled,
+ :housekeeping_enabled
+ end
put "application/settings" do
- attributes = ["repository_storage"] + current_settings.attributes.keys - ["id"]
- attrs = attributes_for_keys(attributes)
-
- if current_settings.update_attributes(attrs)
+ if current_settings.update_attributes(declared_params(include_missing: false))
present current_settings, with: Entities::ApplicationSetting
else
render_validation_error!(current_settings)
diff --git a/lib/api/templates.rb b/lib/api/templates.rb
index 8a53d9c0095..e23f99256a5 100644
--- a/lib/api/templates.rb
+++ b/lib/api/templates.rb
@@ -8,6 +8,10 @@ module API
gitlab_ci_ymls: {
klass: Gitlab::Template::GitlabCiYmlTemplate,
gitlab_version: 8.9
+ },
+ dockerfiles: {
+ klass: Gitlab::Template::DockerfileTemplate,
+ gitlab_version: 8.15
}
}.freeze
PROJECT_TEMPLATE_REGEX =
@@ -51,7 +55,7 @@ module API
end
params do
optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses'
- end
+ end
get route do
options = {
featured: declared(params).popular.present? ? true : nil
@@ -69,7 +73,7 @@ module API
end
params do
requires :name, type: String, desc: 'The name of the template'
- end
+ end
get route, requirements: { name: /[\w\.-]+/ } do
not_found!('License') unless Licensee::License.find(declared(params).name)
@@ -78,7 +82,7 @@ module API
present template, with: Entities::RepoLicense
end
end
-
+
GLOBAL_TEMPLATE_TYPES.each do |template_type, properties|
klass = properties[:klass]
gitlab_version = properties[:gitlab_version]
@@ -104,7 +108,7 @@ module API
end
params do
requires :name, type: String, desc: 'The name of the template'
- end
+ end
get route do
new_template = klass.find(declared(params).name)
diff --git a/lib/api/time_tracking_endpoints.rb b/lib/api/time_tracking_endpoints.rb
new file mode 100644
index 00000000000..85b5f7d98b8
--- /dev/null
+++ b/lib/api/time_tracking_endpoints.rb
@@ -0,0 +1,114 @@
+module API
+ module TimeTrackingEndpoints
+ extend ActiveSupport::Concern
+
+ included do
+ helpers do
+ def issuable_name
+ declared_params.has_key?(:issue_id) ? 'issue' : 'merge_request'
+ end
+
+ def issuable_key
+ "#{issuable_name}_id".to_sym
+ end
+
+ def update_issuable_key
+ "update_#{issuable_name}".to_sym
+ end
+
+ def read_issuable_key
+ "read_#{issuable_name}".to_sym
+ end
+
+ def load_issuable
+ @issuable ||= begin
+ case issuable_name
+ when 'issue'
+ find_project_issue(params.delete(issuable_key))
+ when 'merge_request'
+ find_project_merge_request(params.delete(issuable_key))
+ end
+ end
+ end
+
+ def update_issuable(attrs)
+ custom_params = declared_params(include_missing: false)
+ custom_params.merge!(attrs)
+
+ issuable = update_service.new(user_project, current_user, custom_params).execute(load_issuable)
+ if issuable.valid?
+ present issuable, with: Entities::IssuableTimeStats
+ else
+ render_validation_error!(issuable)
+ end
+ end
+
+ def update_service
+ issuable_name == 'issue' ? ::Issues::UpdateService : ::MergeRequests::UpdateService
+ end
+ end
+
+ issuable_name = name.end_with?('Issues') ? 'issue' : 'merge_request'
+ issuable_collection_name = issuable_name.pluralize
+ issuable_key = "#{issuable_name}_id".to_sym
+
+ desc "Set a time estimate for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ requires :duration, type: String, desc: 'The duration to be parsed'
+ end
+ post ":id/#{issuable_collection_name}/:#{issuable_key}/time_estimate" do
+ authorize! update_issuable_key, load_issuable
+
+ status :ok
+ update_issuable(time_estimate: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)))
+ end
+
+ desc "Reset the time estimate for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ end
+ post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_time_estimate" do
+ authorize! update_issuable_key, load_issuable
+
+ status :ok
+ update_issuable(time_estimate: 0)
+ end
+
+ desc "Add spent time for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ requires :duration, type: String, desc: 'The duration to be parsed'
+ end
+ post ":id/#{issuable_collection_name}/:#{issuable_key}/add_spent_time" do
+ authorize! update_issuable_key, load_issuable
+
+ update_issuable(spend_time: {
+ duration: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)),
+ user: current_user
+ })
+ end
+
+ desc "Reset spent time for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ end
+ post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_spent_time" do
+ authorize! update_issuable_key, load_issuable
+
+ status :ok
+ update_issuable(spend_time: { duration: :reset, user: current_user })
+ end
+
+ desc "Show time stats for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ end
+ get ":id/#{issuable_collection_name}/:#{issuable_key}/time_stats" do
+ authorize! read_issuable_key, load_issuable
+
+ present load_issuable, with: Entities::IssuableTimeStats
+ end
+ end
+ end
+end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index c7db2d71017..11a7368b4c0 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -2,7 +2,10 @@ module API
class Users < Grape::API
include PaginationParams
- before { authenticate! }
+ before do
+ allow_access_with_scope :read_user if request.get?
+ authenticate!
+ end
resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
helpers do
@@ -13,7 +16,7 @@ module API
optional :website_url, type: String, desc: 'The website of the user'
optional :organization, type: String, desc: 'The organization of the user'
optional :projects_limit, type: Integer, desc: 'The number of projects a user can create'
- optional :extern_uid, type: Integer, desc: 'The external authentication provider UID'
+ optional :extern_uid, type: String, desc: 'The external authentication provider UID'
optional :provider, type: String, desc: 'The external provider'
optional :bio, type: String, desc: 'The biography of the user'
optional :location, type: String, desc: 'The location of the user'
@@ -88,10 +91,11 @@ module API
authenticated_as_admin!
# Filter out params which are used later
- identity_attrs = params.slice(:provider, :extern_uid)
- confirm = params.delete(:confirm)
+ user_params = declared_params(include_missing: false)
+ identity_attrs = user_params.slice(:provider, :extern_uid)
+ confirm = user_params.delete(:confirm)
- user = User.build_user(declared_params(include_missing: false))
+ user = User.new(user_params.except(:extern_uid, :provider))
user.skip_confirmation! unless confirm
if identity_attrs.any?
@@ -156,11 +160,7 @@ module API
end
end
- # Delete already handled parameters
- user_params.delete(:extern_uid)
- user_params.delete(:provider)
-
- if user.update_attributes(user_params)
+ if user.update_attributes(user_params.except(:extern_uid, :provider))
present user, with: Entities::UserPublic
else
render_validation_error!(user)
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index 7e6537e3d9e..cefbfdce3bb 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -2,6 +2,7 @@ module Backup
class Manager
ARCHIVES_TO_BACKUP = %w[uploads builds artifacts lfs registry]
FOLDERS_TO_BACKUP = %w[repositories db]
+ FILE_NAME_SUFFIX = '_gitlab_backup.tar'
def pack
# Make sure there is a connection
@@ -14,7 +15,7 @@ module Backup
s[:gitlab_version] = Gitlab::VERSION
s[:tar_version] = tar_version
s[:skipped] = ENV["SKIP"]
- tar_file = s[:backup_created_at].strftime('%s_%Y_%m_%d') + '_gitlab_backup.tar'
+ tar_file = "#{s[:backup_created_at].strftime('%s_%Y_%m_%d')}#{FILE_NAME_SUFFIX}"
Dir.chdir(Gitlab.config.backup.path) do
File.open("#{Gitlab.config.backup.path}/backup_information.yml",
@@ -82,7 +83,7 @@ module Backup
removed = 0
Dir.chdir(Gitlab.config.backup.path) do
- Dir.glob('*_gitlab_backup.tar').each do |file|
+ Dir.glob("*#{FILE_NAME_SUFFIX}").each do |file|
next unless file =~ /(\d+)(?:_\d{4}_\d{2}_\d{2})?_gitlab_backup\.tar/
timestamp = $1.to_i
@@ -108,41 +109,50 @@ module Backup
Dir.chdir(Gitlab.config.backup.path)
# check for existing backups in the backup dir
- file_list = Dir.glob("*_gitlab_backup.tar")
- puts "no backups found" if file_list.count == 0
+ file_list = Dir.glob("*#{FILE_NAME_SUFFIX}")
+
+ if file_list.count == 0
+ $progress.puts "No backups found in #{Gitlab.config.backup.path}"
+ $progress.puts "Please make sure that file name ends with #{FILE_NAME_SUFFIX}"
+ exit 1
+ end
if file_list.count > 1 && ENV["BACKUP"].nil?
- puts "Found more than one backup, please specify which one you want to restore:"
- puts "rake gitlab:backup:restore BACKUP=timestamp_of_backup"
+ $progress.puts 'Found more than one backup, please specify which one you want to restore:'
+ $progress.puts 'rake gitlab:backup:restore BACKUP=timestamp_of_backup'
exit 1
end
- tar_file = ENV["BACKUP"].nil? ? file_list.first : file_list.grep(ENV['BACKUP']).first
+ if ENV['BACKUP'].present?
+ tar_file = "#{ENV['BACKUP']}#{FILE_NAME_SUFFIX}"
+ else
+ tar_file = file_list.first
+ end
unless File.exist?(tar_file)
- puts "The specified backup doesn't exist!"
+ $progress.puts "The backup file #{tar_file} does not exist!"
exit 1
end
- $progress.print "Unpacking backup ... "
+ $progress.print 'Unpacking backup ... '
unless Kernel.system(*%W(tar -xf #{tar_file}))
- puts "unpacking backup failed".color(:red)
+ $progress.puts 'unpacking backup failed'.color(:red)
exit 1
else
- $progress.puts "done".color(:green)
+ $progress.puts 'done'.color(:green)
end
ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0
# restoring mismatching backups can lead to unexpected problems
if settings[:gitlab_version] != Gitlab::VERSION
- puts "GitLab version mismatch:".color(:red)
- puts " Your current GitLab version (#{Gitlab::VERSION}) differs from the GitLab version in the backup!".color(:red)
- puts " Please switch to the following version and try again:".color(:red)
- puts " version: #{settings[:gitlab_version]}".color(:red)
- puts
- puts "Hint: git checkout v#{settings[:gitlab_version]}"
+ $progress.puts 'GitLab version mismatch:'.color(:red)
+ $progress.puts " Your current GitLab version (#{Gitlab::VERSION}) differs from the GitLab version in the backup!".color(:red)
+ $progress.puts ' Please switch to the following version and try again:'.color(:red)
+ $progress.puts " version: #{settings[:gitlab_version]}".color(:red)
+ $progress.puts
+ $progress.puts "Hint: git checkout v#{settings[:gitlab_version]}"
exit 1
end
end
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index fd74eeaebe7..6d04f68c8f9 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -254,15 +254,26 @@ module Banzai
# Returns projects for the given paths.
def find_projects_for_paths(paths)
if RequestStore.active?
- to_query = paths - project_refs_cache.keys
+ cache = project_refs_cache
+ to_query = paths - cache.keys
unless to_query.empty?
- projects_relation_for_paths(to_query).each do |project|
- get_or_set_cache(project_refs_cache, project.path_with_namespace) { project }
+ projects = projects_relation_for_paths(to_query)
+
+ found = []
+ projects.each do |project|
+ ref = project.path_with_namespace
+ get_or_set_cache(cache, ref) { project }
+ found << ref
+ end
+
+ not_found = to_query - found
+ not_found.each do |ref|
+ get_or_set_cache(cache, ref) { nil }
end
end
- project_refs_cache.slice(*paths).values
+ cache.slice(*paths).values.compact
else
projects_relation_for_paths(paths)
end
diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb
index 2f19b59e725..d67d466bce8 100644
--- a/lib/banzai/filter/external_link_filter.rb
+++ b/lib/banzai/filter/external_link_filter.rb
@@ -10,7 +10,7 @@ module Banzai
node.set_attribute('href', href)
end
- if href =~ /\Ahttp(s)?:\/\// && external_url?(href)
+ if href =~ %r{\A(https?:)?//[^/]} && external_url?(href)
node.set_attribute('rel', 'nofollow noreferrer')
node.set_attribute('target', '_blank')
end
diff --git a/lib/banzai/filter/math_filter.rb b/lib/banzai/filter/math_filter.rb
new file mode 100644
index 00000000000..b6e784c886b
--- /dev/null
+++ b/lib/banzai/filter/math_filter.rb
@@ -0,0 +1,46 @@
+require 'uri'
+
+module Banzai
+ module Filter
+ # HTML filter that adds class="code math" and removes the dollar sign in $`2+2`$.
+ #
+ class MathFilter < HTML::Pipeline::Filter
+ # Attribute indicating inline or display math.
+ STYLE_ATTRIBUTE = 'data-math-style'.freeze
+
+ # Class used for tagging elements that should be rendered
+ TAG_CLASS = 'js-render-math'.freeze
+
+ INLINE_CLASSES = "code math #{TAG_CLASS}".freeze
+
+ DOLLAR_SIGN = '$'.freeze
+
+ def call
+ doc.css('code').each do |code|
+ closing = code.next
+ opening = code.previous
+
+ # We need a sibling before and after.
+ # They should end and start with $ respectively.
+ if closing && opening &&
+ closing.text? && opening.text? &&
+ closing.content.first == DOLLAR_SIGN &&
+ opening.content.last == DOLLAR_SIGN
+
+ code[:class] = INLINE_CLASSES
+ code[STYLE_ATTRIBUTE] = 'inline'
+ closing.content = closing.content[1..-1]
+ opening.content = opening.content[0..-2]
+ end
+ end
+
+ doc.css('pre.code.math').each do |el|
+ el[STYLE_ATTRIBUTE] = 'display'
+ el[:class] += " #{TAG_CLASS}"
+ end
+
+ doc
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index 84bfeac8041..ab7af1cad21 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -20,10 +20,10 @@ module Banzai
# Examples:
#
# data_attribute(project: 1, issue: 2)
- # # => "data-reference-filter=\"SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\""
+ # # => "data-reference-type=\"SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\""
#
# data_attribute(project: 3, merge_request: 4)
- # # => "data-reference-filter=\"SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\""
+ # # => "data-reference-type=\"SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\""
#
# Returns a String
def data_attribute(attributes = {})
@@ -31,7 +31,9 @@ module Banzai
attributes[:reference_type] ||= self.class.reference_type
attributes.delete(:original) if context[:no_original_data]
- attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") }.join(" ")
+ attributes.map do |key, value|
+ %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}")
+ end.join(' ')
end
def escape_once(html)
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 5da2d0b008c..5a1f873496c 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -6,6 +6,7 @@ module Banzai
Filter::SyntaxHighlightFilter,
Filter::SanitizationFilter,
+ Filter::MathFilter,
Filter::UploadLinkFilter,
Filter::VideoLinkFilter,
Filter::ImageLinkFilter,
diff --git a/lib/bitbucket/client.rb b/lib/bitbucket/client.rb
new file mode 100644
index 00000000000..f8ee7e0f9ae
--- /dev/null
+++ b/lib/bitbucket/client.rb
@@ -0,0 +1,58 @@
+module Bitbucket
+ class Client
+ attr_reader :connection
+
+ def initialize(options = {})
+ @connection = Connection.new(options)
+ end
+
+ def issues(repo)
+ path = "/repositories/#{repo}/issues"
+ get_collection(path, :issue)
+ end
+
+ def issue_comments(repo, issue_id)
+ path = "/repositories/#{repo}/issues/#{issue_id}/comments"
+ get_collection(path, :comment)
+ end
+
+ def pull_requests(repo)
+ path = "/repositories/#{repo}/pullrequests?state=ALL"
+ get_collection(path, :pull_request)
+ end
+
+ def pull_request_comments(repo, pull_request)
+ path = "/repositories/#{repo}/pullrequests/#{pull_request}/comments"
+ get_collection(path, :pull_request_comment)
+ end
+
+ def pull_request_diff(repo, pull_request)
+ path = "/repositories/#{repo}/pullrequests/#{pull_request}/diff"
+ connection.get(path)
+ end
+
+ def repo(name)
+ parsed_response = connection.get("/repositories/#{name}")
+ Representation::Repo.new(parsed_response)
+ end
+
+ def repos
+ path = "/repositories?role=member"
+ get_collection(path, :repo)
+ end
+
+ def user
+ @user ||= begin
+ parsed_response = connection.get('/user')
+ Representation::User.new(parsed_response)
+ end
+ end
+
+ private
+
+ def get_collection(path, type)
+ paginator = Paginator.new(connection, path, type)
+ Collection.new(paginator)
+ end
+ end
+end
diff --git a/lib/bitbucket/collection.rb b/lib/bitbucket/collection.rb
new file mode 100644
index 00000000000..3a9379ff680
--- /dev/null
+++ b/lib/bitbucket/collection.rb
@@ -0,0 +1,21 @@
+module Bitbucket
+ class Collection < Enumerator
+ def initialize(paginator)
+ super() do |yielder|
+ loop do
+ paginator.items.each { |item| yielder << item }
+ end
+ end
+
+ lazy
+ end
+
+ def method_missing(method, *args)
+ return super unless self.respond_to?(method)
+
+ self.send(method, *args) do |item|
+ block_given? ? yield(item) : item
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket/connection.rb b/lib/bitbucket/connection.rb
new file mode 100644
index 00000000000..7e55cf4deab
--- /dev/null
+++ b/lib/bitbucket/connection.rb
@@ -0,0 +1,69 @@
+module Bitbucket
+ class Connection
+ DEFAULT_API_VERSION = '2.0'
+ DEFAULT_BASE_URI = 'https://api.bitbucket.org/'
+ DEFAULT_QUERY = {}
+
+ attr_reader :expires_at, :expires_in, :refresh_token, :token
+
+ def initialize(options = {})
+ @api_version = options.fetch(:api_version, DEFAULT_API_VERSION)
+ @base_uri = options.fetch(:base_uri, DEFAULT_BASE_URI)
+ @default_query = options.fetch(:query, DEFAULT_QUERY)
+
+ @token = options[:token]
+ @expires_at = options[:expires_at]
+ @expires_in = options[:expires_in]
+ @refresh_token = options[:refresh_token]
+ end
+
+ def get(path, extra_query = {})
+ refresh! if expired?
+
+ response = connection.get(build_url(path), params: @default_query.merge(extra_query))
+ response.parsed
+ end
+
+ def expired?
+ connection.expired?
+ end
+
+ def refresh!
+ response = connection.refresh!
+
+ @token = response.token
+ @expires_at = response.expires_at
+ @expires_in = response.expires_in
+ @refresh_token = response.refresh_token
+ @connection = nil
+ end
+
+ private
+
+ def client
+ @client ||= OAuth2::Client.new(provider.app_id, provider.app_secret, options)
+ end
+
+ def connection
+ @connection ||= OAuth2::AccessToken.new(client, @token, refresh_token: @refresh_token, expires_at: @expires_at, expires_in: @expires_in)
+ end
+
+ def build_url(path)
+ return path if path.starts_with?(root_url)
+
+ "#{root_url}#{path}"
+ end
+
+ def root_url
+ @root_url ||= "#{@base_uri}#{@api_version}"
+ end
+
+ def provider
+ Gitlab::OAuth::Provider.config_for('bitbucket')
+ end
+
+ def options
+ OmniAuth::Strategies::Bitbucket.default_options[:client_options].deep_symbolize_keys
+ end
+ end
+end
diff --git a/lib/bitbucket/error/unauthorized.rb b/lib/bitbucket/error/unauthorized.rb
new file mode 100644
index 00000000000..5e2eb57bb0e
--- /dev/null
+++ b/lib/bitbucket/error/unauthorized.rb
@@ -0,0 +1,6 @@
+module Bitbucket
+ module Error
+ class Unauthorized < StandardError
+ end
+ end
+end
diff --git a/lib/bitbucket/page.rb b/lib/bitbucket/page.rb
new file mode 100644
index 00000000000..2b0a3fe7b1a
--- /dev/null
+++ b/lib/bitbucket/page.rb
@@ -0,0 +1,34 @@
+module Bitbucket
+ class Page
+ attr_reader :attrs, :items
+
+ def initialize(raw, type)
+ @attrs = parse_attrs(raw)
+ @items = parse_values(raw, representation_class(type))
+ end
+
+ def next?
+ attrs.fetch(:next, false)
+ end
+
+ def next
+ attrs.fetch(:next)
+ end
+
+ private
+
+ def parse_attrs(raw)
+ raw.slice(*%w(size page pagelen next previous)).symbolize_keys
+ end
+
+ def parse_values(raw, bitbucket_rep_class)
+ return [] unless raw['values'] && raw['values'].is_a?(Array)
+
+ bitbucket_rep_class.decorate(raw['values'])
+ end
+
+ def representation_class(type)
+ Bitbucket::Representation.const_get(type.to_s.camelize)
+ end
+ end
+end
diff --git a/lib/bitbucket/paginator.rb b/lib/bitbucket/paginator.rb
new file mode 100644
index 00000000000..135d0d55674
--- /dev/null
+++ b/lib/bitbucket/paginator.rb
@@ -0,0 +1,36 @@
+module Bitbucket
+ class Paginator
+ PAGE_LENGTH = 50 # The minimum length is 10 and the maximum is 100.
+
+ def initialize(connection, url, type)
+ @connection = connection
+ @type = type
+ @url = url
+ @page = nil
+ end
+
+ def items
+ raise StopIteration unless has_next_page?
+
+ @page = fetch_next_page
+ @page.items
+ end
+
+ private
+
+ attr_reader :connection, :page, :url, :type
+
+ def has_next_page?
+ page.nil? || page.next?
+ end
+
+ def next_url
+ page.nil? ? url : page.next
+ end
+
+ def fetch_next_page
+ parsed_response = connection.get(next_url, pagelen: PAGE_LENGTH, sort: :created_on)
+ Page.new(parsed_response, type)
+ end
+ end
+end
diff --git a/lib/bitbucket/representation/base.rb b/lib/bitbucket/representation/base.rb
new file mode 100644
index 00000000000..94adaacc9b5
--- /dev/null
+++ b/lib/bitbucket/representation/base.rb
@@ -0,0 +1,17 @@
+module Bitbucket
+ module Representation
+ class Base
+ def initialize(raw)
+ @raw = raw
+ end
+
+ def self.decorate(entries)
+ entries.map { |entry| new(entry)}
+ end
+
+ private
+
+ attr_reader :raw
+ end
+ end
+end
diff --git a/lib/bitbucket/representation/comment.rb b/lib/bitbucket/representation/comment.rb
new file mode 100644
index 00000000000..4937aa9728f
--- /dev/null
+++ b/lib/bitbucket/representation/comment.rb
@@ -0,0 +1,27 @@
+module Bitbucket
+ module Representation
+ class Comment < Representation::Base
+ def author
+ user['username']
+ end
+
+ def note
+ raw.fetch('content', {}).fetch('raw', nil)
+ end
+
+ def created_at
+ raw['created_on']
+ end
+
+ def updated_at
+ raw['updated_on'] || raw['created_on']
+ end
+
+ private
+
+ def user
+ raw.fetch('user', {})
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket/representation/issue.rb b/lib/bitbucket/representation/issue.rb
new file mode 100644
index 00000000000..054064395c3
--- /dev/null
+++ b/lib/bitbucket/representation/issue.rb
@@ -0,0 +1,53 @@
+module Bitbucket
+ module Representation
+ class Issue < Representation::Base
+ CLOSED_STATUS = %w(resolved invalid duplicate wontfix closed).freeze
+
+ def iid
+ raw['id']
+ end
+
+ def kind
+ raw['kind']
+ end
+
+ def author
+ raw.fetch('reporter', {}).fetch('username', nil)
+ end
+
+ def description
+ raw.fetch('content', {}).fetch('raw', nil)
+ end
+
+ def state
+ closed? ? 'closed' : 'opened'
+ end
+
+ def title
+ raw['title']
+ end
+
+ def milestone
+ raw['milestone']['name'] if raw['milestone'].present?
+ end
+
+ def created_at
+ raw['created_on']
+ end
+
+ def updated_at
+ raw['edited_on']
+ end
+
+ def to_s
+ iid
+ end
+
+ private
+
+ def closed?
+ CLOSED_STATUS.include?(raw['state'])
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket/representation/pull_request.rb b/lib/bitbucket/representation/pull_request.rb
new file mode 100644
index 00000000000..eebf8093380
--- /dev/null
+++ b/lib/bitbucket/representation/pull_request.rb
@@ -0,0 +1,65 @@
+module Bitbucket
+ module Representation
+ class PullRequest < Representation::Base
+ def author
+ raw.fetch('author', {}).fetch('username', nil)
+ end
+
+ def description
+ raw['description']
+ end
+
+ def iid
+ raw['id']
+ end
+
+ def state
+ if raw['state'] == 'MERGED'
+ 'merged'
+ elsif raw['state'] == 'DECLINED'
+ 'closed'
+ else
+ 'opened'
+ end
+ end
+
+ def created_at
+ raw['created_on']
+ end
+
+ def updated_at
+ raw['updated_on']
+ end
+
+ def title
+ raw['title']
+ end
+
+ def source_branch_name
+ source_branch.fetch('branch', {}).fetch('name', nil)
+ end
+
+ def source_branch_sha
+ source_branch.fetch('commit', {}).fetch('hash', nil)
+ end
+
+ def target_branch_name
+ target_branch.fetch('branch', {}).fetch('name', nil)
+ end
+
+ def target_branch_sha
+ target_branch.fetch('commit', {}).fetch('hash', nil)
+ end
+
+ private
+
+ def source_branch
+ raw['source']
+ end
+
+ def target_branch
+ raw['destination']
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket/representation/pull_request_comment.rb b/lib/bitbucket/representation/pull_request_comment.rb
new file mode 100644
index 00000000000..4f8efe03bae
--- /dev/null
+++ b/lib/bitbucket/representation/pull_request_comment.rb
@@ -0,0 +1,39 @@
+module Bitbucket
+ module Representation
+ class PullRequestComment < Comment
+ def iid
+ raw['id']
+ end
+
+ def file_path
+ inline.fetch('path')
+ end
+
+ def old_pos
+ inline.fetch('from')
+ end
+
+ def new_pos
+ inline.fetch('to')
+ end
+
+ def parent_id
+ raw.fetch('parent', {}).fetch('id', nil)
+ end
+
+ def inline?
+ raw.has_key?('inline')
+ end
+
+ def has_parent?
+ raw.has_key?('parent')
+ end
+
+ private
+
+ def inline
+ raw.fetch('inline', {})
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket/representation/repo.rb b/lib/bitbucket/representation/repo.rb
new file mode 100644
index 00000000000..423eff8f2a5
--- /dev/null
+++ b/lib/bitbucket/representation/repo.rb
@@ -0,0 +1,71 @@
+module Bitbucket
+ module Representation
+ class Repo < Representation::Base
+ attr_reader :owner, :slug
+
+ def initialize(raw)
+ super(raw)
+ end
+
+ def owner_and_slug
+ @owner_and_slug ||= full_name.split('/', 2)
+ end
+
+ def owner
+ owner_and_slug.first
+ end
+
+ def slug
+ owner_and_slug.last
+ end
+
+ def clone_url(token = nil)
+ url = raw['links']['clone'].find { |link| link['name'] == 'https' }.fetch('href')
+
+ if token.present?
+ clone_url = URI::parse(url)
+ clone_url.user = "x-token-auth:#{token}"
+ clone_url.to_s
+ else
+ url
+ end
+ end
+
+ def description
+ raw['description']
+ end
+
+ def full_name
+ raw['full_name']
+ end
+
+ def issues_enabled?
+ raw['has_issues']
+ end
+
+ def name
+ raw['name']
+ end
+
+ def valid?
+ raw['scm'] == 'git'
+ end
+
+ def has_wiki?
+ raw['has_wiki']
+ end
+
+ def visibility_level
+ if raw['is_private']
+ Gitlab::VisibilityLevel::PRIVATE
+ else
+ Gitlab::VisibilityLevel::PUBLIC
+ end
+ end
+
+ def to_s
+ full_name
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket/representation/user.rb b/lib/bitbucket/representation/user.rb
new file mode 100644
index 00000000000..ba6b7667b49
--- /dev/null
+++ b/lib/bitbucket/representation/user.rb
@@ -0,0 +1,9 @@
+module Bitbucket
+ module Representation
+ class User < Representation::Base
+ def username
+ raw['username']
+ end
+ end
+ end
+end
diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb
index 229050151d3..c10d3616f31 100644
--- a/lib/ci/ansi2html.rb
+++ b/lib/ci/ansi2html.rb
@@ -105,7 +105,7 @@ module Ci
break
elsif s.scan(/</)
@out << '&lt;'
- elsif s.scan(/\n/)
+ elsif s.scan(/\r?\n/)
@out << '<br>'
else
@out << s.scan(/./m)
diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb
index a6b9beecded..24bb3649a76 100644
--- a/lib/ci/api/api.rb
+++ b/lib/ci/api/api.rb
@@ -8,6 +8,16 @@ module Ci
rack_response({ 'message' => '404 Not found' }.to_json, 404)
end
+ # Retain 405 error rather than a 500 error for Grape 0.15.0+.
+ # https://github.com/ruby-grape/grape/blob/a3a28f5b5dfbb2797442e006dbffd750b27f2a76/UPGRADING.md#changes-to-method-not-allowed-routes
+ rescue_from Grape::Exceptions::MethodNotAllowed do |e|
+ error! e.message, e.status, e.headers
+ end
+
+ rescue_from Grape::Exceptions::Base do |e|
+ error! e.message, e.status, e.headers
+ end
+
rescue_from :all do |exception|
handle_api_exception(exception)
end
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index de3e224bcee..c4bdef781f7 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -50,7 +50,7 @@ module Ci
put ":id" do
authenticate_runner!
build = Ci::Build.where(runner_id: current_runner.id).running.find(params[:id])
- forbidden!('Build has been erased!') if build.erased?
+ validate_build!(build)
update_runner_info
@@ -80,9 +80,7 @@ module Ci
# PATCH /builds/:id/trace.txt
patch ":id/trace.txt" do
build = Ci::Build.find_by_id(params[:id])
- not_found! unless build
- authenticate_build_token!(build)
- forbidden!('Build has been erased!') if build.erased?
+ authenticate_build!(build)
error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range')
content_range = request.headers['Content-Range']
@@ -113,8 +111,7 @@ module Ci
Gitlab::Workhorse.verify_api_request!(headers)
not_allowed! unless Gitlab.config.artifacts.enabled
build = Ci::Build.find_by_id(params[:id])
- not_found! unless build
- authenticate_build_token!(build)
+ authenticate_build!(build)
forbidden!('build is not running') unless build.running?
if params[:filesize]
@@ -151,10 +148,8 @@ module Ci
require_gitlab_workhorse!
not_allowed! unless Gitlab.config.artifacts.enabled
build = Ci::Build.find_by_id(params[:id])
- not_found! unless build
- authenticate_build_token!(build)
+ authenticate_build!(build)
forbidden!('Build is not running!') unless build.running?
- forbidden!('Build has been erased!') if build.erased?
artifacts_upload_path = ArtifactUploader.artifacts_upload_path
artifacts = uploaded_file(:file, artifacts_upload_path)
@@ -185,8 +180,7 @@ module Ci
# GET /builds/:id/artifacts
get ":id/artifacts" do
build = Ci::Build.find_by_id(params[:id])
- not_found! unless build
- authenticate_build_token!(build)
+ authenticate_build!(build)
artifacts_file = build.artifacts_file
unless artifacts_file.file_storage?
@@ -211,8 +205,7 @@ module Ci
# DELETE /builds/:id/artifacts
delete ":id/artifacts" do
build = Ci::Build.find_by_id(params[:id])
- not_found! unless build
- authenticate_build_token!(build)
+ authenticate_build!(build)
build.erase_artifacts!
end
diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb
index e608f5f6cad..5ff25a3a9b2 100644
--- a/lib/ci/api/helpers.rb
+++ b/lib/ci/api/helpers.rb
@@ -13,8 +13,19 @@ module Ci
forbidden! unless current_runner
end
- def authenticate_build_token!(build)
- forbidden! unless build_token_valid?(build)
+ def authenticate_build!(build)
+ validate_build!(build) do
+ forbidden! unless build_token_valid?(build)
+ end
+ end
+
+ def validate_build!(build)
+ not_found! unless build
+
+ yield if block_given?
+
+ forbidden!('Project has been deleted!') unless build.project
+ forbidden!('Build has been erased!') if build.erased?
end
def runner_registration_token_valid?
@@ -49,7 +60,7 @@ module Ci
end
def build_not_found!
- if headers['User-Agent'].match(/gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /)
+ if headers['User-Agent'].to_s.match(/gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /)
no_content!
else
not_found!
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index fef652cb975..7463bd719d5 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -118,7 +118,7 @@ module Ci
.merge(job_variables(name))
variables.map do |key, value|
- { key: key, value: value, public: true }
+ { key: key.to_s, value: value, public: true }
end
end
diff --git a/lib/email_template_interceptor.rb b/lib/email_template_interceptor.rb
index fb04a7824b8..63f9f8d7a5a 100644
--- a/lib/email_template_interceptor.rb
+++ b/lib/email_template_interceptor.rb
@@ -5,8 +5,8 @@ class EmailTemplateInterceptor
def self.delivering_email(message)
# Remove HTML part if HTML emails are disabled.
unless current_application_settings.html_emails_enabled
- message.part.delete_if do |part|
- part.content_type.try(:start_with?, 'text/html')
+ message.parts.delete_if do |part|
+ part.content_type.start_with?('text/html')
end
end
end
diff --git a/lib/gitlab/allowable.rb b/lib/gitlab/allowable.rb
new file mode 100644
index 00000000000..f48abcc86d5
--- /dev/null
+++ b/lib/gitlab/allowable.rb
@@ -0,0 +1,7 @@
+module Gitlab
+ module Allowable
+ def can?(user, action, subject)
+ Ability.allowed?(user, action, subject)
+ end
+ end
+end
diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb
index 9667df4ffb8..0618107e2c3 100644
--- a/lib/gitlab/asciidoc.rb
+++ b/lib/gitlab/asciidoc.rb
@@ -1,4 +1,6 @@
require 'asciidoctor'
+require 'asciidoctor/converter/html5'
+require "asciidoctor-plantuml"
module Gitlab
# Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters
@@ -23,16 +25,51 @@ module Gitlab
def self.render(input, context, asciidoc_opts = {})
asciidoc_opts.reverse_merge!(
safe: :secure,
- backend: :html5,
+ backend: :gitlab_html5,
attributes: []
)
asciidoc_opts[:attributes].unshift(*DEFAULT_ADOC_ATTRS)
+ plantuml_setup
+
html = ::Asciidoctor.convert(input, asciidoc_opts)
html = Banzai.post_process(html, context)
html.html_safe
end
+
+ def self.plantuml_setup
+ Asciidoctor::PlantUml.configure do |conf|
+ conf.url = ApplicationSetting.current.plantuml_url
+ conf.svg_enable = ApplicationSetting.current.plantuml_enabled
+ conf.png_enable = ApplicationSetting.current.plantuml_enabled
+ conf.txt_enable = false
+ end
+ end
+
+ class Html5Converter < Asciidoctor::Converter::Html5Converter
+ extend Asciidoctor::Converter::Config
+
+ register_for 'gitlab_html5'
+
+ def stem(node)
+ return super unless node.style.to_sym == :latexmath
+
+ %(<pre#{id_attribute(node)} class="code math js-render-math #{node.role}" data-math-style="display"><code>#{node.content}</code></pre>)
+ end
+
+ def inline_quoted(node)
+ return super unless node.type.to_sym == :latexmath
+
+ %(<code#{id_attribute(node)} class="code math js-render-math #{node.role}" data-math-style="inline">#{node.text}</code>)
+ end
+
+ private
+
+ def id_attribute(node)
+ node.id ? %( id="#{node.id}") : nil
+ end
+ end
end
end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index aca5d0020cf..8dda65c71ef 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -2,6 +2,10 @@ module Gitlab
module Auth
class MissingPersonalTokenError < StandardError; end
+ SCOPES = [:api, :read_user]
+ DEFAULT_SCOPES = [:api]
+ OPTIONAL_SCOPES = SCOPES - DEFAULT_SCOPES
+
class << self
def find_for_git_client(login, password, project:, ip:)
raise "Must provide an IP for rate limiting" if ip.nil?
@@ -88,7 +92,7 @@ module Gitlab
def oauth_access_token_check(login, password)
if login == "oauth2" && password.present?
token = Doorkeeper::AccessToken.by_token(password)
- if token && token.accessible?
+ if valid_oauth_token?(token)
user = User.find_by(id: token.resource_owner_id)
Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)
end
@@ -97,12 +101,27 @@ module Gitlab
def personal_access_token_check(login, password)
if login && password
- user = User.find_by_personal_access_token(password)
+ token = PersonalAccessToken.active.find_by_token(password)
validation = User.by_login(login)
- Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities) if user.present? && user == validation
+
+ if valid_personal_access_token?(token, validation)
+ Gitlab::Auth::Result.new(validation, nil, :personal_token, full_authentication_abilities)
+ end
end
end
+ def valid_oauth_token?(token)
+ token && token.accessible? && valid_api_token?(token)
+ end
+
+ def valid_personal_access_token?(token, user)
+ token && token.user == user && valid_api_token?(token)
+ end
+
+ def valid_api_token?(token)
+ AccessTokenValidationService.new(token).include_any_scope?(['api'])
+ end
+
def lfs_token_check(login, password)
deploy_key_matches = login.match(/\Alfs\+deploy-key-(\d+)\z/)
diff --git a/lib/gitlab/auth/result.rb b/lib/gitlab/auth/result.rb
index 6be7f690676..39b86c61a18 100644
--- a/lib/gitlab/auth/result.rb
+++ b/lib/gitlab/auth/result.rb
@@ -9,8 +9,7 @@ module Gitlab
def lfs_deploy_token?(for_project)
type == :lfs_deploy_token &&
- actor &&
- actor.projects.include?(for_project)
+ actor.try(:has_access_to?, for_project)
end
def success?
diff --git a/lib/gitlab/badge/build/status.rb b/lib/gitlab/badge/build/status.rb
index 50aa45e5406..b762d85b6e5 100644
--- a/lib/gitlab/badge/build/status.rb
+++ b/lib/gitlab/badge/build/status.rb
@@ -20,8 +20,8 @@ module Gitlab
def status
@project.pipelines
- .where(sha: @sha, ref: @ref)
- .status || 'unknown'
+ .where(sha: @sha)
+ .latest_status(@ref) || 'unknown'
end
def metadata
diff --git a/lib/gitlab/bitbucket_import.rb b/lib/gitlab/bitbucket_import.rb
deleted file mode 100644
index 7298152e7e9..00000000000
--- a/lib/gitlab/bitbucket_import.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-module Gitlab
- module BitbucketImport
- mattr_accessor :public_key
- @public_key = nil
- end
-end
diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb
deleted file mode 100644
index 8d1ad62fae0..00000000000
--- a/lib/gitlab/bitbucket_import/client.rb
+++ /dev/null
@@ -1,142 +0,0 @@
-module Gitlab
- module BitbucketImport
- class Client
- class Unauthorized < StandardError; end
-
- attr_reader :consumer, :api
-
- def self.from_project(project)
- import_data_credentials = project.import_data.credentials if project.import_data
- if import_data_credentials && import_data_credentials[:bb_session]
- token = import_data_credentials[:bb_session][:bitbucket_access_token]
- token_secret = import_data_credentials[:bb_session][:bitbucket_access_token_secret]
- new(token, token_secret)
- else
- raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{project.id}"
- end
- end
-
- def initialize(access_token = nil, access_token_secret = nil)
- @consumer = ::OAuth::Consumer.new(
- config.app_id,
- config.app_secret,
- bitbucket_options
- )
-
- if access_token && access_token_secret
- @api = ::OAuth::AccessToken.new(@consumer, access_token, access_token_secret)
- end
- end
-
- def request_token(redirect_uri)
- request_token = consumer.get_request_token(oauth_callback: redirect_uri)
-
- {
- oauth_token: request_token.token,
- oauth_token_secret: request_token.secret,
- oauth_callback_confirmed: request_token.callback_confirmed?.to_s
- }
- end
-
- def authorize_url(request_token, redirect_uri)
- request_token = ::OAuth::RequestToken.from_hash(consumer, request_token) if request_token.is_a?(Hash)
-
- if request_token.callback_confirmed?
- request_token.authorize_url
- else
- request_token.authorize_url(oauth_callback: redirect_uri)
- end
- end
-
- def get_token(request_token, oauth_verifier, redirect_uri)
- request_token = ::OAuth::RequestToken.from_hash(consumer, request_token) if request_token.is_a?(Hash)
-
- if request_token.callback_confirmed?
- request_token.get_access_token(oauth_verifier: oauth_verifier)
- else
- request_token.get_access_token(oauth_callback: redirect_uri)
- end
- end
-
- def user
- JSON.parse(get("/api/1.0/user").body)
- end
-
- def issues(project_identifier)
- all_issues = []
- offset = 0
- per_page = 50 # Maximum number allowed by Bitbucket
- index = 0
-
- begin
- issues = JSON.parse(get(issue_api_endpoint(project_identifier, per_page, offset)).body)
- # Find out how many total issues are present
- total = issues["count"] if index == 0
- all_issues.concat(issues["issues"])
- offset += issues["issues"].count
- index += 1
- end while all_issues.count < total
-
- all_issues
- end
-
- def issue_comments(project_identifier, issue_id)
- comments = JSON.parse(get("/api/1.0/repositories/#{project_identifier}/issues/#{issue_id}/comments").body)
- comments.sort_by { |comment| comment["utc_created_on"] }
- end
-
- def project(project_identifier)
- JSON.parse(get("/api/1.0/repositories/#{project_identifier}").body)
- end
-
- def find_deploy_key(project_identifier, key)
- JSON.parse(get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find do |deploy_key|
- deploy_key["key"].chomp == key.chomp
- end
- end
-
- def add_deploy_key(project_identifier, key)
- deploy_key = find_deploy_key(project_identifier, key)
- return if deploy_key
-
- JSON.parse(api.post("/api/1.0/repositories/#{project_identifier}/deploy-keys", key: key, label: "GitLab import key").body)
- end
-
- def delete_deploy_key(project_identifier, key)
- deploy_key = find_deploy_key(project_identifier, key)
- return unless deploy_key
-
- api.delete("/api/1.0/repositories/#{project_identifier}/deploy-keys/#{deploy_key["pk"]}").code == "204"
- end
-
- def projects
- JSON.parse(get("/api/1.0/user/repositories").body).select { |repo| repo["scm"] == "git" }
- end
-
- def incompatible_projects
- JSON.parse(get("/api/1.0/user/repositories").body).reject { |repo| repo["scm"] == "git" }
- end
-
- private
-
- def get(url)
- response = api.get(url)
- raise Unauthorized if (400..499).cover?(response.code.to_i)
-
- response
- end
-
- def issue_api_endpoint(project_identifier, per_page, offset)
- "/api/1.0/repositories/#{project_identifier}/issues?sort=utc_created_on&limit=#{per_page}&start=#{offset}"
- end
-
- def config
- Gitlab.config.omniauth.providers.find { |provider| provider.name == "bitbucket" }
- end
-
- def bitbucket_options
- OmniAuth::Strategies::Bitbucket.default_options[:client_options].symbolize_keys
- end
- end
- end
-end
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
index f4b5097adb1..44323b47dca 100644
--- a/lib/gitlab/bitbucket_import/importer.rb
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -1,84 +1,247 @@
module Gitlab
module BitbucketImport
class Importer
- attr_reader :project, :client
+ include Gitlab::ShellAdapter
+
+ LABELS = [{ title: 'bug', color: '#FF0000' },
+ { title: 'enhancement', color: '#428BCA' },
+ { title: 'proposal', color: '#69D100' },
+ { title: 'task', color: '#7F8C8D' }].freeze
+
+ attr_reader :project, :client, :errors, :users
def initialize(project)
@project = project
- @client = Client.from_project(@project)
+ @client = Bitbucket::Client.new(project.import_data.credentials)
@formatter = Gitlab::ImportFormatter.new
+ @labels = {}
+ @errors = []
+ @users = {}
end
def execute
- import_issues if has_issues?
+ import_wiki
+ import_issues
+ import_pull_requests
+ handle_errors
true
- rescue ActiveRecord::RecordInvalid => e
- raise Projects::ImportService::Error.new, e.message
- ensure
- Gitlab::BitbucketImport::KeyDeleter.new(project).execute
end
private
- def gitlab_user_id(project, bitbucket_id)
- if bitbucket_id
- user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", bitbucket_id.to_s)
- (user && user.id) || project.creator_id
- else
- project.creator_id
- end
+ def handle_errors
+ return unless errors.any?
+
+ project.update_column(:import_error, {
+ message: 'The remote data could not be fully imported.',
+ errors: errors
+ }.to_json)
+ end
+
+ def gitlab_user_id(project, username)
+ find_user_id(username) || project.creator_id
end
- def identifier
- project.import_source
+ def find_user_id(username)
+ return nil unless username
+
+ return users[username] if users.key?(username)
+
+ users[username] = User.select(:id)
+ .joins(:identities)
+ .find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", username)
+ .try(:id)
end
- def has_issues?
- client.project(identifier)["has_issues"]
+ def repo
+ @repo ||= client.repo(project.import_source)
end
- def import_issues
- issues = client.issues(identifier)
+ def import_wiki
+ return if project.wiki.repository_exists?
- issues.each do |issue|
- body = ''
- reporter = nil
- author = 'Anonymous'
+ path_with_namespace = "#{project.path_with_namespace}.wiki"
+ import_url = project.import_url.sub(/\.git\z/, ".git/wiki")
+ gitlab_shell.import_repository(project.repository_storage_path, path_with_namespace, import_url)
+ rescue StandardError => e
+ errors << { type: :wiki, errors: e.message }
+ end
- if issue["reported_by"] && issue["reported_by"]["username"]
- reporter = issue["reported_by"]["username"]
- author = reporter
+ def import_issues
+ return unless repo.issues_enabled?
+
+ create_labels
+
+ client.issues(repo).each do |issue|
+ begin
+ description = ''
+ description += @formatter.author_line(issue.author) unless find_user_id(issue.author)
+ description += issue.description
+
+ label_name = issue.kind
+ milestone = issue.milestone ? project.milestones.find_or_create_by(title: issue.milestone) : nil
+
+ gitlab_issue = project.issues.create!(
+ iid: issue.iid,
+ title: issue.title,
+ description: description,
+ state: issue.state,
+ author_id: gitlab_user_id(project, issue.author),
+ milestone: milestone,
+ created_at: issue.created_at,
+ updated_at: issue.updated_at
+ )
+
+ gitlab_issue.labels << @labels[label_name]
+
+ import_issue_comments(issue, gitlab_issue) if gitlab_issue.persisted?
+ rescue StandardError => e
+ errors << { type: :issue, iid: issue.iid, errors: e.message }
end
+ end
+ end
- body = @formatter.author_line(author)
- body += issue["content"]
+ def import_issue_comments(issue, gitlab_issue)
+ client.issue_comments(repo, issue.iid).each do |comment|
+ # The note can be blank for issue service messages like "Changed title: ..."
+ # We would like to import those comments as well but there is no any
+ # specific parameter that would allow to process them, it's just an empty comment.
+ # To prevent our importer from just crashing or from creating useless empty comments
+ # we do this check.
+ next unless comment.note.present?
+
+ note = ''
+ note += @formatter.author_line(comment.author) unless find_user_id(comment.author)
+ note += comment.note
+
+ begin
+ gitlab_issue.notes.create!(
+ project: project,
+ note: note,
+ author_id: gitlab_user_id(project, comment.author),
+ created_at: comment.created_at,
+ updated_at: comment.updated_at
+ )
+ rescue StandardError => e
+ errors << { type: :issue_comment, iid: issue.iid, errors: e.message }
+ end
+ end
+ end
- comments = client.issue_comments(identifier, issue["local_id"])
+ def create_labels
+ LABELS.each do |label|
+ @labels[label[:title]] = project.labels.create!(label)
+ end
+ end
- if comments.any?
- body += @formatter.comments_header
+ def import_pull_requests
+ pull_requests = client.pull_requests(repo)
+
+ pull_requests.each do |pull_request|
+ begin
+ description = ''
+ description += @formatter.author_line(pull_request.author) unless find_user_id(pull_request.author)
+ description += pull_request.description
+
+ merge_request = project.merge_requests.create(
+ iid: pull_request.iid,
+ title: pull_request.title,
+ description: description,
+ source_project: project,
+ source_branch: pull_request.source_branch_name,
+ source_branch_sha: pull_request.source_branch_sha,
+ target_project: project,
+ target_branch: pull_request.target_branch_name,
+ target_branch_sha: pull_request.target_branch_sha,
+ state: pull_request.state,
+ author_id: gitlab_user_id(project, pull_request.author),
+ assignee_id: nil,
+ created_at: pull_request.created_at,
+ updated_at: pull_request.updated_at
+ )
+
+ import_pull_request_comments(pull_request, merge_request) if merge_request.persisted?
+ rescue StandardError => e
+ errors << { type: :pull_request, iid: pull_request.iid, errors: e.message }
end
+ end
+ end
+
+ def import_pull_request_comments(pull_request, merge_request)
+ comments = client.pull_request_comments(repo, pull_request.iid)
+
+ inline_comments, pr_comments = comments.partition(&:inline?)
+
+ import_inline_comments(inline_comments, pull_request, merge_request)
+ import_standalone_pr_comments(pr_comments, merge_request)
+ end
- comments.each do |comment|
- author = 'Anonymous'
+ def import_inline_comments(inline_comments, pull_request, merge_request)
+ line_code_map = {}
- if comment["author_info"] && comment["author_info"]["username"]
- author = comment["author_info"]["username"]
- end
+ children, parents = inline_comments.partition(&:has_parent?)
- body += @formatter.comment(author, comment["utc_created_on"], comment["content"])
+ # The Bitbucket API returns threaded replies as parent-child
+ # relationships. We assume that the child can appear in any order in
+ # the JSON.
+ parents.each do |comment|
+ line_code_map[comment.iid] = generate_line_code(comment)
+ end
+
+ children.each do |comment|
+ line_code_map[comment.iid] = line_code_map.fetch(comment.parent_id, nil)
+ end
+
+ inline_comments.each do |comment|
+ begin
+ attributes = pull_request_comment_attributes(comment)
+ attributes.merge!(
+ position: build_position(merge_request, comment),
+ line_code: line_code_map.fetch(comment.iid),
+ type: 'DiffNote')
+
+ merge_request.notes.create!(attributes)
+ rescue StandardError => e
+ errors << { type: :pull_request, iid: comment.iid, errors: e.message }
end
+ end
+ end
+
+ def build_position(merge_request, pr_comment)
+ params = {
+ diff_refs: merge_request.diff_refs,
+ old_path: pr_comment.file_path,
+ new_path: pr_comment.file_path,
+ old_line: pr_comment.old_pos,
+ new_line: pr_comment.new_pos
+ }
- project.issues.create!(
- description: body,
- title: issue["title"],
- state: %w(resolved invalid duplicate wontfix closed).include?(issue["status"]) ? 'closed' : 'opened',
- author_id: gitlab_user_id(project, reporter)
- )
+ Gitlab::Diff::Position.new(params)
+ end
+
+ def import_standalone_pr_comments(pr_comments, merge_request)
+ pr_comments.each do |comment|
+ begin
+ merge_request.notes.create!(pull_request_comment_attributes(comment))
+ rescue StandardError => e
+ errors << { type: :pull_request, iid: comment.iid, errors: e.message }
+ end
end
- rescue ActiveRecord::RecordInvalid => e
- raise Projects::ImportService::Error, e.message
+ end
+
+ def generate_line_code(pr_comment)
+ Gitlab::Diff::LineCode.generate(pr_comment.file_path, pr_comment.new_pos, pr_comment.old_pos)
+ end
+
+ def pull_request_comment_attributes(comment)
+ {
+ project: project,
+ note: comment.note,
+ author_id: gitlab_user_id(project, comment.author),
+ created_at: comment.created_at,
+ updated_at: comment.updated_at
+ }
end
end
end
diff --git a/lib/gitlab/bitbucket_import/key_adder.rb b/lib/gitlab/bitbucket_import/key_adder.rb
deleted file mode 100644
index 0b63f025d0a..00000000000
--- a/lib/gitlab/bitbucket_import/key_adder.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-module Gitlab
- module BitbucketImport
- class KeyAdder
- attr_reader :repo, :current_user, :client
-
- def initialize(repo, current_user, access_params)
- @repo, @current_user = repo, current_user
- @client = Client.new(access_params[:bitbucket_access_token],
- access_params[:bitbucket_access_token_secret])
- end
-
- def execute
- return false unless BitbucketImport.public_key.present?
-
- project_identifier = "#{repo["owner"]}/#{repo["slug"]}"
- client.add_deploy_key(project_identifier, BitbucketImport.public_key)
-
- true
- rescue
- false
- end
- end
- end
-end
diff --git a/lib/gitlab/bitbucket_import/key_deleter.rb b/lib/gitlab/bitbucket_import/key_deleter.rb
deleted file mode 100644
index e03c3155b3e..00000000000
--- a/lib/gitlab/bitbucket_import/key_deleter.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-module Gitlab
- module BitbucketImport
- class KeyDeleter
- attr_reader :project, :current_user, :client
-
- def initialize(project)
- @project = project
- @current_user = project.creator
- @client = Client.from_project(@project)
- end
-
- def execute
- return false unless BitbucketImport.public_key.present?
-
- client.delete_deploy_key(project.import_source, BitbucketImport.public_key)
-
- true
- rescue
- false
- end
- end
- end
-end
diff --git a/lib/gitlab/bitbucket_import/project_creator.rb b/lib/gitlab/bitbucket_import/project_creator.rb
index b90ef0b0fba..d94f70fd1fb 100644
--- a/lib/gitlab/bitbucket_import/project_creator.rb
+++ b/lib/gitlab/bitbucket_import/project_creator.rb
@@ -1,10 +1,11 @@
module Gitlab
module BitbucketImport
class ProjectCreator
- attr_reader :repo, :namespace, :current_user, :session_data
+ attr_reader :repo, :name, :namespace, :current_user, :session_data
- def initialize(repo, namespace, current_user, session_data)
+ def initialize(repo, name, namespace, current_user, session_data)
@repo = repo
+ @name = name
@namespace = namespace
@current_user = current_user
@session_data = session_data
@@ -13,17 +14,24 @@ module Gitlab
def execute
::Projects::CreateService.new(
current_user,
- name: repo["name"],
- path: repo["slug"],
- description: repo["description"],
+ name: name,
+ path: name,
+ description: repo.description,
namespace_id: namespace.id,
- visibility_level: repo["is_private"] ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC,
- import_type: "bitbucket",
- import_source: "#{repo["owner"]}/#{repo["slug"]}",
- import_url: "ssh://git@bitbucket.org/#{repo["owner"]}/#{repo["slug"]}.git",
- import_data: { credentials: { bb_session: session_data } }
+ visibility_level: repo.visibility_level,
+ import_type: 'bitbucket',
+ import_source: repo.full_name,
+ import_url: repo.clone_url(session_data[:token]),
+ import_data: { credentials: session_data },
+ skip_wiki: skip_wiki
).execute
end
+
+ private
+
+ def skip_wiki
+ repo.has_wiki?
+ end
end
end
end
diff --git a/lib/gitlab/chat_commands/base_command.rb b/lib/gitlab/chat_commands/base_command.rb
index 25da8474e95..4fe53ce93a9 100644
--- a/lib/gitlab/chat_commands/base_command.rb
+++ b/lib/gitlab/chat_commands/base_command.rb
@@ -42,6 +42,10 @@ module Gitlab
def find_by_iid(iid)
collection.find_by(iid: iid)
end
+
+ def presenter
+ Gitlab::ChatCommands::Presenter.new
+ end
end
end
end
diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb
index b0d3fdbc48a..145086755e4 100644
--- a/lib/gitlab/chat_commands/command.rb
+++ b/lib/gitlab/chat_commands/command.rb
@@ -22,8 +22,6 @@ module Gitlab
end
end
- private
-
def match_command
match = nil
service = available_commands.find do |klass|
@@ -33,6 +31,8 @@ module Gitlab
[service, match]
end
+ private
+
def help_messages
available_commands.map(&:help_message)
end
@@ -48,15 +48,15 @@ module Gitlab
end
def help(messages)
- Mattermost::Presenter.help(messages, params[:command])
+ presenter.help(messages, params[:command])
end
def access_denied
- Mattermost::Presenter.access_denied
+ presenter.access_denied
end
def present(resource)
- Mattermost::Presenter.present(resource)
+ presenter.present(resource)
end
end
end
diff --git a/lib/gitlab/chat_commands/deploy.rb b/lib/gitlab/chat_commands/deploy.rb
index 0eed1fce0dc..7127d2f6d04 100644
--- a/lib/gitlab/chat_commands/deploy.rb
+++ b/lib/gitlab/chat_commands/deploy.rb
@@ -4,7 +4,7 @@ module Gitlab
include Gitlab::Routing.url_helpers
def self.match(text)
- /\Adeploy\s+(?<from>.*)\s+to+\s+(?<to>.*)\z/.match(text)
+ /\Adeploy\s+(?<from>\S+.*)\s+to+\s+(?<to>\S+.*)\z/.match(text)
end
def self.help_message
@@ -50,7 +50,8 @@ module Gitlab
def url(subject)
polymorphic_url(
- [ subject.project.namespace.becomes(Namespace), subject.project, subject ])
+ [subject.project.namespace.becomes(Namespace), subject.project, subject]
+ )
end
end
end
diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb
index 1dba85c1b51..cefb6775db8 100644
--- a/lib/gitlab/chat_commands/issue_create.rb
+++ b/lib/gitlab/chat_commands/issue_create.rb
@@ -8,7 +8,7 @@ module Gitlab
end
def self.help_message
- 'issue new <title>\n<description>'
+ 'issue new <title> *`⇧ Shift`*+*`↵ Enter`* <description>'
end
def self.allowed?(project, user)
diff --git a/lib/mattermost/presenter.rb b/lib/gitlab/chat_commands/presenter.rb
index 67eda983a74..8930a21f406 100644
--- a/lib/mattermost/presenter.rb
+++ b/lib/gitlab/chat_commands/presenter.rb
@@ -1,7 +1,7 @@
-module Mattermost
- class Presenter
- class << self
- include Gitlab::Routing.url_helpers
+module Gitlab
+ module ChatCommands
+ class Presenter
+ include Gitlab::Routing
def authorize_chat_name(url)
message = if url
@@ -30,12 +30,12 @@ module Mattermost
if subject.is_a?(Gitlab::ChatCommands::Result)
show_result(subject)
elsif subject.respond_to?(:count)
- if subject.many?
- multiple_resources(subject)
- elsif subject.none?
+ if subject.none?
not_found
+ elsif subject.one?
+ single_resource(subject.first)
else
- single_resource(subject)
+ multiple_resources(subject)
end
else
single_resource(subject)
@@ -64,16 +64,16 @@ module Mattermost
def single_resource(resource)
return error(resource) if resource.errors.any? || !resource.persisted?
- message = "### #{title(resource)}"
+ message = "#{title(resource)}:"
message << "\n\n#{resource.description}" if resource.try(:description)
in_channel_response(message)
end
def multiple_resources(resources)
- resources.map! { |resource| title(resource) }
+ titles = resources.map { |resource| title(resource) }
- message = header_with_list("Multiple results were found:", resources)
+ message = header_with_list("Multiple results were found:", titles)
ephemeral_response(message)
end
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index cb1065223d4..273118135a9 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -1,13 +1,16 @@
module Gitlab
module Checks
class ChangeAccess
- attr_reader :user_access, :project
+ attr_reader :user_access, :project, :skip_authorization
- def initialize(change, user_access:, project:)
+ def initialize(
+ change, user_access:, project:, env: {}, skip_authorization: false)
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
@branch_name = Gitlab::Git.branch_name(@ref)
@user_access = user_access
@project = project
+ @env = env
+ @skip_authorization = skip_authorization
end
def exec
@@ -23,12 +26,13 @@ module Gitlab
protected
def protected_branch_checks
+ return if skip_authorization
return unless @branch_name
return unless project.protected_branch?(@branch_name)
- if forced_push? && user_access.cannot_do_action?(:force_push_code_to_protected_branches)
+ if forced_push?
return "You are not allowed to force push code to a protected branch on this project."
- elsif Gitlab::Git.blank_ref?(@newrev) && user_access.cannot_do_action?(:remove_protected_branches)
+ elsif Gitlab::Git.blank_ref?(@newrev)
return "You are not allowed to delete protected branches from this project."
end
@@ -48,6 +52,8 @@ module Gitlab
end
def tag_checks
+ return if skip_authorization
+
tag_ref = Gitlab::Git.tag_name(@ref)
if tag_ref && protected_tag?(tag_ref) && user_access.cannot_do_action?(:admin_project)
@@ -56,6 +62,8 @@ module Gitlab
end
def push_checks
+ return if skip_authorization
+
if user_access.cannot_do_action?(:push_code)
"You are not allowed to push code to this project."
end
@@ -68,7 +76,7 @@ module Gitlab
end
def forced_push?
- Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev)
+ Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev, env: @env)
end
def matching_merge_request?
diff --git a/lib/gitlab/checks/force_push.rb b/lib/gitlab/checks/force_push.rb
index 5fe86553bd0..de0c9049ebf 100644
--- a/lib/gitlab/checks/force_push.rb
+++ b/lib/gitlab/checks/force_push.rb
@@ -1,15 +1,20 @@
module Gitlab
module Checks
class ForcePush
- def self.force_push?(project, oldrev, newrev)
+ def self.force_push?(project, oldrev, newrev, env: {})
return false if project.empty_repo?
# Created or deleted branch
if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev)
false
else
- missed_ref, _ = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} --git-dir=#{project.repository.path_to_repo} rev-list --max-count=1 #{oldrev} ^#{newrev}))
- missed_ref.present?
+ missed_ref, exit_status = Gitlab::Git::RevList.new(oldrev, newrev, project: project, env: env).execute
+
+ if exit_status == 0
+ missed_ref.present?
+ else
+ raise "Got a non-zero exit code while calling out to `git rev-list` in the force-push check."
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/entry/environment.rb b/lib/gitlab/ci/config/entry/environment.rb
index b7b4b91eb51..f7c530c7d9f 100644
--- a/lib/gitlab/ci/config/entry/environment.rb
+++ b/lib/gitlab/ci/config/entry/environment.rb
@@ -33,7 +33,6 @@ module Gitlab
validates :url,
length: { maximum: 255 },
- addressable_url: true,
allow_nil: true
validates :action,
diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb
new file mode 100644
index 00000000000..a979fe7d573
--- /dev/null
+++ b/lib/gitlab/ci/status/build/cancelable.rb
@@ -0,0 +1,37 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class Cancelable < SimpleDelegator
+ include Status::Extended
+
+ def has_action?
+ can?(user, :update_build, subject)
+ end
+
+ def action_icon
+ 'ban'
+ end
+
+ def action_path
+ cancel_namespace_project_build_path(subject.project.namespace,
+ subject.project,
+ subject)
+ end
+
+ def action_method
+ :post
+ end
+
+ def action_title
+ 'Cancel'
+ end
+
+ def self.matches?(build, user)
+ build.cancelable?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/common.rb b/lib/gitlab/ci/status/build/common.rb
new file mode 100644
index 00000000000..3fec2c5d4db
--- /dev/null
+++ b/lib/gitlab/ci/status/build/common.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ module Common
+ def has_details?
+ can?(user, :read_build, subject)
+ end
+
+ def details_path
+ namespace_project_build_path(subject.project.namespace,
+ subject.project,
+ subject)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb
new file mode 100644
index 00000000000..eee9a64120b
--- /dev/null
+++ b/lib/gitlab/ci/status/build/factory.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class Factory < Status::Factory
+ def self.extended_statuses
+ [Status::Build::Stop, Status::Build::Play,
+ Status::Build::Cancelable, Status::Build::Retryable]
+ end
+
+ def self.common_helpers
+ Status::Build::Common
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb
new file mode 100644
index 00000000000..1bf949c96dd
--- /dev/null
+++ b/lib/gitlab/ci/status/build/play.rb
@@ -0,0 +1,57 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class Play < SimpleDelegator
+ include Status::Extended
+
+ def text
+ 'manual'
+ end
+
+ def label
+ 'manual play action'
+ end
+
+ def icon
+ 'icon_status_manual'
+ end
+
+ def group
+ 'manual'
+ end
+
+ def has_action?
+ can?(user, :update_build, subject)
+ end
+
+ def action_icon
+ 'play'
+ end
+
+ def action_title
+ 'Play'
+ end
+
+ def action_class
+ 'ci-play-icon'
+ end
+
+ def action_path
+ play_namespace_project_build_path(subject.project.namespace,
+ subject.project,
+ subject)
+ end
+
+ def action_method
+ :post
+ end
+
+ def self.matches?(build, user)
+ build.playable? && !build.stops_environment?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb
new file mode 100644
index 00000000000..8e38d6a8523
--- /dev/null
+++ b/lib/gitlab/ci/status/build/retryable.rb
@@ -0,0 +1,37 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class Retryable < SimpleDelegator
+ include Status::Extended
+
+ def has_action?
+ can?(user, :update_build, subject)
+ end
+
+ def action_icon
+ 'refresh'
+ end
+
+ def action_title
+ 'Retry'
+ end
+
+ def action_path
+ retry_namespace_project_build_path(subject.project.namespace,
+ subject.project,
+ subject)
+ end
+
+ def action_method
+ :post
+ end
+
+ def self.matches?(build, user)
+ build.retryable?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb
new file mode 100644
index 00000000000..e1dfdb76d41
--- /dev/null
+++ b/lib/gitlab/ci/status/build/stop.rb
@@ -0,0 +1,53 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class Stop < SimpleDelegator
+ include Status::Extended
+
+ def text
+ 'manual'
+ end
+
+ def label
+ 'manual stop action'
+ end
+
+ def icon
+ 'icon_status_manual'
+ end
+
+ def group
+ 'manual'
+ end
+
+ def has_action?
+ can?(user, :update_build, subject)
+ end
+
+ def action_icon
+ 'stop'
+ end
+
+ def action_title
+ 'Stop'
+ end
+
+ def action_path
+ play_namespace_project_build_path(subject.project.namespace,
+ subject.project,
+ subject)
+ end
+
+ def action_method
+ :post
+ end
+
+ def self.matches?(build, user)
+ build.playable? && build.stops_environment?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb
index ce4108fdcf2..73b6ab5a635 100644
--- a/lib/gitlab/ci/status/core.rb
+++ b/lib/gitlab/ci/status/core.rb
@@ -4,10 +4,14 @@ module Gitlab
# Base abstract class fore core status
#
class Core
- include Gitlab::Routing.url_helpers
+ include Gitlab::Routing
+ include Gitlab::Allowable
- def initialize(subject)
+ attr_reader :subject, :user
+
+ def initialize(subject, user)
@subject = subject
+ @user = user
end
def icon
@@ -18,23 +22,12 @@ module Gitlab
raise NotImplementedError
end
- def title
- "#{@subject.class.name.demodulize}: #{label}"
- end
-
- # Deprecation warning: this method is here because we need to maintain
- # backwards compatibility with legacy statuses. We often do something
- # like "ci-status ci-status-#{status}" to set CSS class.
- #
- # `to_s` method should be renamed to `group` at some point, after
- # phasing legacy satuses out.
- #
- def to_s
- self.class.name.demodulize.downcase.underscore
+ def group
+ self.class.name.demodulize.underscore
end
def has_details?
- raise NotImplementedError
+ false
end
def details_path
@@ -42,16 +35,27 @@ module Gitlab
end
def has_action?
- raise NotImplementedError
+ false
end
def action_icon
raise NotImplementedError
end
+ def action_class
+ end
+
def action_path
raise NotImplementedError
end
+
+ def action_method
+ raise NotImplementedError
+ end
+
+ def action_title
+ raise NotImplementedError
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/extended.rb b/lib/gitlab/ci/status/extended.rb
index 6bfb5d38c1f..d367c9bda69 100644
--- a/lib/gitlab/ci/status/extended.rb
+++ b/lib/gitlab/ci/status/extended.rb
@@ -2,8 +2,12 @@ module Gitlab
module Ci
module Status
module Extended
- def matches?(_subject)
- raise NotImplementedError
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def matches?(_subject, _user)
+ raise NotImplementedError
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb
index b2f896f2211..ae9ef895df4 100644
--- a/lib/gitlab/ci/status/factory.rb
+++ b/lib/gitlab/ci/status/factory.rb
@@ -2,10 +2,9 @@ module Gitlab
module Ci
module Status
class Factory
- attr_reader :subject
-
- def initialize(subject)
+ def initialize(subject, user)
@subject = subject
+ @user = user
end
def fabricate!
@@ -16,27 +15,32 @@ module Gitlab
end
end
+ def self.extended_statuses
+ []
+ end
+
+ def self.common_helpers
+ Module.new
+ end
+
private
- def subject_status
- @subject_status ||= subject.status
+ def simple_status
+ @simple_status ||= @subject.status || :created
end
def core_status
Gitlab::Ci::Status
- .const_get(subject_status.capitalize)
- .new(subject)
+ .const_get(simple_status.capitalize)
+ .new(@subject, @user)
+ .extend(self.class.common_helpers)
end
def extended_status
- @extended ||= extended_statuses.find do |status|
- status.matches?(subject)
+ @extended ||= self.class.extended_statuses.find do |status|
+ status.matches?(@subject, @user)
end
end
-
- def extended_statuses
- []
- end
end
end
end
diff --git a/lib/gitlab/ci/status/pipeline/common.rb b/lib/gitlab/ci/status/pipeline/common.rb
index 25e52bec3da..76bfd18bf40 100644
--- a/lib/gitlab/ci/status/pipeline/common.rb
+++ b/lib/gitlab/ci/status/pipeline/common.rb
@@ -4,13 +4,13 @@ module Gitlab
module Pipeline
module Common
def has_details?
- true
+ can?(user, :read_pipeline, subject)
end
def details_path
- namespace_project_pipeline_path(@subject.project.namespace,
- @subject.project,
- @subject)
+ namespace_project_pipeline_path(subject.project.namespace,
+ subject.project,
+ subject)
end
def has_action?
diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb
index 4ac4ec671d0..16dcb326be9 100644
--- a/lib/gitlab/ci/status/pipeline/factory.rb
+++ b/lib/gitlab/ci/status/pipeline/factory.rb
@@ -3,14 +3,12 @@ module Gitlab
module Status
module Pipeline
class Factory < Status::Factory
- private
-
- def extended_statuses
+ def self.extended_statuses
[Pipeline::SuccessWithWarnings]
end
- def core_status
- super.extend(Status::Pipeline::Common)
+ def self.common_helpers
+ Status::Pipeline::Common
end
end
end
diff --git a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb
index 4b040d60df8..24bf8b869e0 100644
--- a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb
+++ b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb
@@ -3,7 +3,7 @@ module Gitlab
module Status
module Pipeline
class SuccessWithWarnings < SimpleDelegator
- extend Status::Extended
+ include Status::Extended
def text
'passed'
@@ -17,11 +17,11 @@ module Gitlab
'icon_status_warning'
end
- def to_s
+ def group
'success_with_warnings'
end
- def self.matches?(pipeline)
+ def self.matches?(pipeline, user)
pipeline.success? && pipeline.has_warnings?
end
end
diff --git a/lib/gitlab/ci/status/stage/common.rb b/lib/gitlab/ci/status/stage/common.rb
index 14c437d2b98..7852f492e1d 100644
--- a/lib/gitlab/ci/status/stage/common.rb
+++ b/lib/gitlab/ci/status/stage/common.rb
@@ -4,14 +4,14 @@ module Gitlab
module Stage
module Common
def has_details?
- true
+ can?(user, :read_pipeline, subject.pipeline)
end
def details_path
- namespace_project_pipeline_path(@subject.project.namespace,
- @subject.project,
- @subject.pipeline,
- anchor: @subject.name)
+ namespace_project_pipeline_path(subject.project.namespace,
+ subject.project,
+ subject.pipeline,
+ anchor: subject.name)
end
def has_action?
diff --git a/lib/gitlab/ci/status/stage/factory.rb b/lib/gitlab/ci/status/stage/factory.rb
index c6522d5ada1..689a5dd45bc 100644
--- a/lib/gitlab/ci/status/stage/factory.rb
+++ b/lib/gitlab/ci/status/stage/factory.rb
@@ -3,10 +3,8 @@ module Gitlab
module Status
module Stage
class Factory < Status::Factory
- private
-
- def core_status
- super.extend(Status::Stage::Common)
+ def self.common_helpers
+ Status::Stage::Common
end
end
end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index c6bb8f9c8ed..2ff27e46d64 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -35,6 +35,7 @@ module Gitlab
signin_enabled: Settings.gitlab['signin_enabled'],
gravatar_enabled: Settings.gravatar['enabled'],
koding_enabled: false,
+ plantuml_enabled: false,
sign_in_text: nil,
after_sign_up_text: nil,
help_page_text: nil,
@@ -45,7 +46,7 @@ module Gitlab
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
domain_whitelist: Settings.gitlab['domain_whitelist'],
- import_sources: %w[github bitbucket gitlab google_code fogbugz git gitlab_project],
+ import_sources: %w[gitea github bitbucket gitlab google_code fogbugz git gitlab_project],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
diff --git a/lib/gitlab/cycle_analytics/base_event.rb b/lib/gitlab/cycle_analytics/base_event_fetcher.rb
index 53a148ad703..0d8791d396b 100644
--- a/lib/gitlab/cycle_analytics/base_event.rb
+++ b/lib/gitlab/cycle_analytics/base_event_fetcher.rb
@@ -1,13 +1,13 @@
module Gitlab
module CycleAnalytics
- class BaseEvent
- include MetricsTables
+ class BaseEventFetcher
+ include BaseQuery
- attr_reader :stage, :start_time_attrs, :end_time_attrs, :projections, :query
+ attr_reader :projections, :query, :stage, :order
- def initialize(project:, options:)
- @query = EventsQuery.new(project: project, options: options)
+ def initialize(project:, stage:, options:)
@project = project
+ @stage = stage
@options = options
end
@@ -19,10 +19,8 @@ module Gitlab
end.compact
end
- def custom_query(_base_query); end
-
def order
- @order || @start_time_attrs
+ @order || default_order
end
private
@@ -34,7 +32,17 @@ module Gitlab
end
def event_result
- @event_result ||= @query.execute(self).to_a
+ @event_result ||= ActiveRecord::Base.connection.exec_query(events_query.to_sql).to_a
+ end
+
+ def events_query
+ diff_fn = subtract_datetimes_diff(base_query, @options[:start_time_attrs], @options[:end_time_attrs])
+
+ base_query.project(extract_diff_epoch(diff_fn).as('total_time'), *projections).order(order.desc)
+ end
+
+ def default_order
+ [@options[:start_time_attrs]].flatten.first
end
def serialize(_event)
diff --git a/lib/gitlab/cycle_analytics/base_query.rb b/lib/gitlab/cycle_analytics/base_query.rb
new file mode 100644
index 00000000000..d560dca45c8
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/base_query.rb
@@ -0,0 +1,31 @@
+module Gitlab
+ module CycleAnalytics
+ module BaseQuery
+ include MetricsTables
+ include Gitlab::Database::Median
+ include Gitlab::Database::DateTime
+
+ private
+
+ def base_query
+ @base_query ||= stage_query
+ end
+
+ def stage_query
+ query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])).
+ join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])).
+ where(issue_table[:project_id].eq(@project.id)).
+ where(issue_table[:deleted_at].eq(nil)).
+ where(issue_table[:created_at].gteq(@options[:from]))
+
+ # Load merge_requests
+ query = query.join(mr_table, Arel::Nodes::OuterJoin).
+ on(mr_table[:id].eq(mr_closing_issues_table[:merge_request_id])).
+ join(mr_metrics_table).
+ on(mr_table[:id].eq(mr_metrics_table[:merge_request_id]))
+
+ query
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb
new file mode 100644
index 00000000000..74bbcdcb3dd
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/base_stage.rb
@@ -0,0 +1,54 @@
+module Gitlab
+ module CycleAnalytics
+ class BaseStage
+ include BaseQuery
+
+ def initialize(project:, options:)
+ @project = project
+ @options = options
+ end
+
+ def events
+ event_fetcher.fetch
+ end
+
+ def as_json
+ AnalyticsStageSerializer.new.represent(self).as_json
+ end
+
+ def title
+ name.to_s.capitalize
+ end
+
+ def median
+ cte_table = Arel::Table.new("cte_table_for_#{name}")
+
+ # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
+ # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
+ # We compute the (end_time - start_time) interval, and give it an alias based on the current
+ # cycle analytics stage.
+ interval_query = Arel::Nodes::As.new(
+ cte_table,
+ subtract_datetimes(base_query.dup, start_time_attrs, end_time_attrs, name.to_s))
+
+ median_datetime(cte_table, interval_query, name)
+ end
+
+ def name
+ raise NotImplementedError.new("Expected #{self.name} to implement name")
+ end
+
+ private
+
+ def event_fetcher
+ @event_fetcher ||= Gitlab::CycleAnalytics::EventFetcher[name].new(project: @project,
+ stage: name,
+ options: event_options)
+ end
+
+ def event_options
+ @options.merge(start_time_attrs: start_time_attrs, end_time_attrs: end_time_attrs)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/review_event.rb b/lib/gitlab/cycle_analytics/code_event_fetcher.rb
index b394a02cc52..5245b9ca8fc 100644
--- a/lib/gitlab/cycle_analytics/review_event.rb
+++ b/lib/gitlab/cycle_analytics/code_event_fetcher.rb
@@ -1,22 +1,22 @@
module Gitlab
module CycleAnalytics
- class ReviewEvent < BaseEvent
+ class CodeEventFetcher < BaseEventFetcher
include MergeRequestAllowed
def initialize(*args)
- @stage = :review
- @start_time_attrs = mr_table[:created_at]
- @end_time_attrs = mr_metrics_table[:merged_at]
@projections = [mr_table[:title],
mr_table[:iid],
mr_table[:id],
mr_table[:created_at],
mr_table[:state],
mr_table[:author_id]]
+ @order = mr_table[:created_at]
super(*args)
end
+ private
+
def serialize(event)
AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json
end
diff --git a/lib/gitlab/cycle_analytics/code_stage.rb b/lib/gitlab/cycle_analytics/code_stage.rb
new file mode 100644
index 00000000000..d1bc2055ba8
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/code_stage.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module CycleAnalytics
+ class CodeStage < BaseStage
+ def start_time_attrs
+ @start_time_attrs ||= issue_metrics_table[:first_mentioned_in_commit_at]
+ end
+
+ def end_time_attrs
+ @end_time_attrs ||= mr_table[:created_at]
+ end
+
+ def name
+ :code
+ end
+
+ def description
+ "Time until first merge request"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/event_fetcher.rb b/lib/gitlab/cycle_analytics/event_fetcher.rb
new file mode 100644
index 00000000000..50e126cf00b
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/event_fetcher.rb
@@ -0,0 +1,9 @@
+module Gitlab
+ module CycleAnalytics
+ module EventFetcher
+ def self.[](stage_name)
+ CycleAnalytics.const_get("#{stage_name.to_s.camelize}EventFetcher")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/events.rb b/lib/gitlab/cycle_analytics/events.rb
deleted file mode 100644
index 2d703d76cbb..00000000000
--- a/lib/gitlab/cycle_analytics/events.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-module Gitlab
- module CycleAnalytics
- class Events
- def initialize(project:, options:)
- @project = project
- @options = options
- end
-
- def issue_events
- IssueEvent.new(project: @project, options: @options).fetch
- end
-
- def plan_events
- PlanEvent.new(project: @project, options: @options).fetch
- end
-
- def code_events
- CodeEvent.new(project: @project, options: @options).fetch
- end
-
- def test_events
- TestEvent.new(project: @project, options: @options).fetch
- end
-
- def review_events
- ReviewEvent.new(project: @project, options: @options).fetch
- end
-
- def staging_events
- StagingEvent.new(project: @project, options: @options).fetch
- end
-
- def production_events
- ProductionEvent.new(project: @project, options: @options).fetch
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/events_query.rb b/lib/gitlab/cycle_analytics/events_query.rb
deleted file mode 100644
index 2418832ccc2..00000000000
--- a/lib/gitlab/cycle_analytics/events_query.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-module Gitlab
- module CycleAnalytics
- class EventsQuery
- attr_reader :project
-
- def initialize(project:, options: {})
- @project = project
- @from = options[:from]
- @branch = options[:branch]
- @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: @from, branch: @branch)
- end
-
- def execute(stage_class)
- @stage_class = stage_class
-
- ActiveRecord::Base.connection.exec_query(query.to_sql)
- end
-
- private
-
- def query
- base_query = @fetcher.base_query_for(@stage_class.stage)
- diff_fn = @fetcher.subtract_datetimes_diff(base_query, @stage_class.start_time_attrs, @stage_class.end_time_attrs)
-
- @stage_class.custom_query(base_query)
-
- base_query.project(extract_epoch(diff_fn).as('total_time'), *@stage_class.projections).order(@stage_class.order.desc)
- end
-
- def extract_epoch(arel_attribute)
- return arel_attribute unless Gitlab::Database.postgresql?
-
- Arel.sql(%Q{EXTRACT(EPOCH FROM (#{arel_attribute.to_sql}))})
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/issue_event.rb b/lib/gitlab/cycle_analytics/issue_event.rb
deleted file mode 100644
index 705b7e5ce24..00000000000
--- a/lib/gitlab/cycle_analytics/issue_event.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module Gitlab
- module CycleAnalytics
- class IssueEvent < BaseEvent
- include IssueAllowed
-
- def initialize(*args)
- @stage = :issue
- @start_time_attrs = issue_table[:created_at]
- @end_time_attrs = [issue_metrics_table[:first_associated_with_milestone_at],
- issue_metrics_table[:first_added_to_board_at]]
- @projections = [issue_table[:title],
- issue_table[:iid],
- issue_table[:id],
- issue_table[:created_at],
- issue_table[:author_id]]
-
- super(*args)
- end
-
- private
-
- def serialize(event)
- AnalyticsIssueSerializer.new(project: @project).represent(event).as_json
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/production_event.rb b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
index 4868c3c6237..0d8da99455e 100644
--- a/lib/gitlab/cycle_analytics/production_event.rb
+++ b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
@@ -1,12 +1,9 @@
module Gitlab
module CycleAnalytics
- class ProductionEvent < BaseEvent
+ class IssueEventFetcher < BaseEventFetcher
include IssueAllowed
def initialize(*args)
- @stage = :production
- @start_time_attrs = issue_table[:created_at]
- @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at]
@projections = [issue_table[:title],
issue_table[:iid],
issue_table[:id],
diff --git a/lib/gitlab/cycle_analytics/issue_stage.rb b/lib/gitlab/cycle_analytics/issue_stage.rb
new file mode 100644
index 00000000000..d2068fbc38f
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/issue_stage.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module CycleAnalytics
+ class IssueStage < BaseStage
+ def start_time_attrs
+ @start_time_attrs ||= issue_table[:created_at]
+ end
+
+ def end_time_attrs
+ @end_time_attrs ||= [issue_metrics_table[:first_associated_with_milestone_at],
+ issue_metrics_table[:first_added_to_board_at]]
+ end
+
+ def name
+ :issue
+ end
+
+ def description
+ "Time before an issue gets scheduled"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/metrics_fetcher.rb b/lib/gitlab/cycle_analytics/metrics_fetcher.rb
deleted file mode 100644
index b71e8735e27..00000000000
--- a/lib/gitlab/cycle_analytics/metrics_fetcher.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-module Gitlab
- module CycleAnalytics
- class MetricsFetcher
- include Gitlab::Database::Median
- include Gitlab::Database::DateTime
- include MetricsTables
-
- DEPLOYMENT_METRIC_STAGES = %i[production staging]
-
- def initialize(project:, from:, branch:)
- @project = project
- @project = project
- @from = from
- @branch = branch
- end
-
- def calculate_metric(name, start_time_attrs, end_time_attrs)
- cte_table = Arel::Table.new("cte_table_for_#{name}")
-
- # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
- # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
- # We compute the (end_time - start_time) interval, and give it an alias based on the current
- # cycle analytics stage.
- interval_query = Arel::Nodes::As.new(
- cte_table,
- subtract_datetimes(base_query_for(name), start_time_attrs, end_time_attrs, name.to_s))
-
- median_datetime(cte_table, interval_query, name)
- end
-
- # Join table with a row for every <issue,merge_request> pair (where the merge request
- # closes the given issue) with issue and merge request metrics included. The metrics
- # are loaded with an inner join, so issues / merge requests without metrics are
- # automatically excluded.
- def base_query_for(name)
- # Load issues
- query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])).
- join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])).
- where(issue_table[:project_id].eq(@project.id)).
- where(issue_table[:deleted_at].eq(nil)).
- where(issue_table[:created_at].gteq(@from))
-
- query = query.where(build_table[:ref].eq(@branch)) if name == :test && @branch
-
- # Load merge_requests
- query = query.join(mr_table, Arel::Nodes::OuterJoin).
- on(mr_table[:id].eq(mr_closing_issues_table[:merge_request_id])).
- join(mr_metrics_table).
- on(mr_table[:id].eq(mr_metrics_table[:merge_request_id]))
-
- if DEPLOYMENT_METRIC_STAGES.include?(name)
- # Limit to merge requests that have been deployed to production after `@from`
- query.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@from))
- end
-
- query
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/plan_event.rb b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
index 7c3f0e9989f..88a8710dbe6 100644
--- a/lib/gitlab/cycle_analytics/plan_event.rb
+++ b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
@@ -1,19 +1,17 @@
module Gitlab
module CycleAnalytics
- class PlanEvent < BaseEvent
+ class PlanEventFetcher < BaseEventFetcher
def initialize(*args)
- @stage = :plan
- @start_time_attrs = issue_metrics_table[:first_associated_with_milestone_at]
- @end_time_attrs = [issue_metrics_table[:first_added_to_board_at],
- issue_metrics_table[:first_mentioned_in_commit_at]]
@projections = [mr_diff_table[:st_commits].as('commits'),
issue_metrics_table[:first_mentioned_in_commit_at]]
super(*args)
end
- def custom_query(base_query)
+ def events_query
base_query.join(mr_diff_table).on(mr_diff_table[:merge_request_id].eq(mr_table[:id]))
+
+ super
end
private
diff --git a/lib/gitlab/cycle_analytics/plan_stage.rb b/lib/gitlab/cycle_analytics/plan_stage.rb
new file mode 100644
index 00000000000..3b4dfc6a30e
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/plan_stage.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module CycleAnalytics
+ class PlanStage < BaseStage
+ def start_time_attrs
+ @start_time_attrs ||= [issue_metrics_table[:first_associated_with_milestone_at],
+ issue_metrics_table[:first_added_to_board_at]]
+ end
+
+ def end_time_attrs
+ @end_time_attrs ||= issue_metrics_table[:first_mentioned_in_commit_at]
+ end
+
+ def name
+ :plan
+ end
+
+ def description
+ "Time before an issue starts implementation"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/production_event_fetcher.rb b/lib/gitlab/cycle_analytics/production_event_fetcher.rb
new file mode 100644
index 00000000000..0fa2e87f673
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/production_event_fetcher.rb
@@ -0,0 +1,6 @@
+module Gitlab
+ module CycleAnalytics
+ class ProductionEventFetcher < IssueEventFetcher
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/production_helper.rb b/lib/gitlab/cycle_analytics/production_helper.rb
new file mode 100644
index 00000000000..d693443bfa4
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/production_helper.rb
@@ -0,0 +1,9 @@
+module Gitlab
+ module CycleAnalytics
+ module ProductionHelper
+ def stage_query
+ super.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@options[:from]))
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/production_stage.rb b/lib/gitlab/cycle_analytics/production_stage.rb
new file mode 100644
index 00000000000..2a6bcc80116
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/production_stage.rb
@@ -0,0 +1,28 @@
+module Gitlab
+ module CycleAnalytics
+ class ProductionStage < BaseStage
+ include ProductionHelper
+
+ def start_time_attrs
+ @start_time_attrs ||= issue_table[:created_at]
+ end
+
+ def end_time_attrs
+ @end_time_attrs ||= mr_metrics_table[:first_deployed_to_production_at]
+ end
+
+ def name
+ :production
+ end
+
+ def description
+ "From issue creation until deploy to production"
+ end
+
+ def query
+ # Limit to merge requests that have been deployed to production after `@from`
+ query.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@from))
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/code_event.rb b/lib/gitlab/cycle_analytics/review_event_fetcher.rb
index 2afdf0b8518..4df0bd06393 100644
--- a/lib/gitlab/cycle_analytics/code_event.rb
+++ b/lib/gitlab/cycle_analytics/review_event_fetcher.rb
@@ -1,25 +1,19 @@
module Gitlab
module CycleAnalytics
- class CodeEvent < BaseEvent
+ class ReviewEventFetcher < BaseEventFetcher
include MergeRequestAllowed
def initialize(*args)
- @stage = :code
- @start_time_attrs = issue_metrics_table[:first_mentioned_in_commit_at]
- @end_time_attrs = mr_table[:created_at]
@projections = [mr_table[:title],
mr_table[:iid],
mr_table[:id],
mr_table[:created_at],
mr_table[:state],
mr_table[:author_id]]
- @order = mr_table[:created_at]
super(*args)
end
- private
-
def serialize(event)
AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json
end
diff --git a/lib/gitlab/cycle_analytics/review_stage.rb b/lib/gitlab/cycle_analytics/review_stage.rb
new file mode 100644
index 00000000000..fbaa3010d81
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/review_stage.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module CycleAnalytics
+ class ReviewStage < BaseStage
+ def start_time_attrs
+ @start_time_attrs ||= mr_table[:created_at]
+ end
+
+ def end_time_attrs
+ @end_time_attrs ||= mr_metrics_table[:merged_at]
+ end
+
+ def name
+ :review
+ end
+
+ def description
+ "Time between merge request creation and merge/close"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/stage.rb b/lib/gitlab/cycle_analytics/stage.rb
new file mode 100644
index 00000000000..28e0455df59
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/stage.rb
@@ -0,0 +1,9 @@
+module Gitlab
+ module CycleAnalytics
+ module Stage
+ def self.[](stage_name)
+ CycleAnalytics.const_get("#{stage_name.to_s.camelize}Stage")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/stage_summary.rb b/lib/gitlab/cycle_analytics/stage_summary.rb
new file mode 100644
index 00000000000..b34baf5b081
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/stage_summary.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module CycleAnalytics
+ class StageSummary
+ def initialize(project, from:, current_user:)
+ @project = project
+ @from = from
+ @current_user = current_user
+ end
+
+ def data
+ [serialize(Summary::Issue.new(project: @project, from: @from, current_user: @current_user)),
+ serialize(Summary::Commit.new(project: @project, from: @from)),
+ serialize(Summary::Deploy.new(project: @project, from: @from))]
+ end
+
+ private
+
+ def serialize(summary_object)
+ AnalyticsSummarySerializer.new.represent(summary_object).as_json
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/staging_event.rb b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb
index a1f30b716f6..a34731a5fcd 100644
--- a/lib/gitlab/cycle_analytics/staging_event.rb
+++ b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb
@@ -1,10 +1,7 @@
module Gitlab
module CycleAnalytics
- class StagingEvent < BaseEvent
+ class StagingEventFetcher < BaseEventFetcher
def initialize(*args)
- @stage = :staging
- @start_time_attrs = mr_metrics_table[:merged_at]
- @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at]
@projections = [build_table[:id]]
@order = build_table[:created_at]
@@ -17,8 +14,10 @@ module Gitlab
super
end
- def custom_query(base_query)
+ def events_query
base_query.join(build_table).on(mr_metrics_table[:pipeline_id].eq(build_table[:commit_id]))
+
+ super
end
private
diff --git a/lib/gitlab/cycle_analytics/staging_stage.rb b/lib/gitlab/cycle_analytics/staging_stage.rb
new file mode 100644
index 00000000000..945909a4d62
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/staging_stage.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module CycleAnalytics
+ class StagingStage < BaseStage
+ include ProductionHelper
+ def start_time_attrs
+ @start_time_attrs ||= mr_metrics_table[:merged_at]
+ end
+
+ def end_time_attrs
+ @end_time_attrs ||= mr_metrics_table[:first_deployed_to_production_at]
+ end
+
+ def name
+ :staging
+ end
+
+ def description
+ "From merge request merge until deploy to production"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/summary/base.rb b/lib/gitlab/cycle_analytics/summary/base.rb
new file mode 100644
index 00000000000..43fa3795e5c
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/summary/base.rb
@@ -0,0 +1,20 @@
+module Gitlab
+ module CycleAnalytics
+ module Summary
+ class Base
+ def initialize(project:, from:)
+ @project = project
+ @from = from
+ end
+
+ def title
+ self.class.name.demodulize
+ end
+
+ def value
+ raise NotImplementedError.new("Expected #{self.name} to implement value")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/summary/commit.rb b/lib/gitlab/cycle_analytics/summary/commit.rb
new file mode 100644
index 00000000000..7b8faa4d854
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/summary/commit.rb
@@ -0,0 +1,39 @@
+module Gitlab
+ module CycleAnalytics
+ module Summary
+ class Commit < Base
+ def value
+ @value ||= count_commits
+ end
+
+ private
+
+ # Don't use the `Gitlab::Git::Repository#log` method, because it enforces
+ # a limit. Since we need a commit count, we _can't_ enforce a limit, so
+ # the easiest way forward is to replicate the relevant portions of the
+ # `log` function here.
+ def count_commits
+ return unless ref
+
+ repository = @project.repository.raw_repository
+ sha = @project.repository.commit(ref).sha
+
+ cmd = %W(git --git-dir=#{repository.path} log)
+ cmd << '--format=%H'
+ cmd << "--after=#{@from.iso8601}"
+ cmd << sha
+
+ output, status = Gitlab::Popen.popen(cmd)
+
+ raise IOError, output unless status.zero?
+
+ output.lines.count
+ end
+
+ def ref
+ @ref ||= @project.default_branch.presence
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/summary/deploy.rb b/lib/gitlab/cycle_analytics/summary/deploy.rb
new file mode 100644
index 00000000000..06032e9200e
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/summary/deploy.rb
@@ -0,0 +1,11 @@
+module Gitlab
+ module CycleAnalytics
+ module Summary
+ class Deploy < Base
+ def value
+ @value ||= @project.deployments.where("created_at > ?", @from).count
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/summary/issue.rb b/lib/gitlab/cycle_analytics/summary/issue.rb
new file mode 100644
index 00000000000..008468f24b9
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/summary/issue.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module CycleAnalytics
+ module Summary
+ class Issue < Base
+ def initialize(project:, from:, current_user:)
+ @project = project
+ @from = from
+ @current_user = current_user
+ end
+
+ def title
+ 'New Issue'
+ end
+
+ def value
+ @value ||= IssuesFinder.new(@current_user, project_id: @project.id).execute.created_after(@from).count
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/test_event.rb b/lib/gitlab/cycle_analytics/test_event.rb
deleted file mode 100644
index d553d0b5aec..00000000000
--- a/lib/gitlab/cycle_analytics/test_event.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module Gitlab
- module CycleAnalytics
- class TestEvent < StagingEvent
- def initialize(*args)
- super(*args)
-
- @stage = :test
- @start_time_attrs = mr_metrics_table[:latest_build_started_at]
- @end_time_attrs = mr_metrics_table[:latest_build_finished_at]
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/test_event_fetcher.rb b/lib/gitlab/cycle_analytics/test_event_fetcher.rb
new file mode 100644
index 00000000000..a2589c6601a
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/test_event_fetcher.rb
@@ -0,0 +1,6 @@
+module Gitlab
+ module CycleAnalytics
+ class TestEventFetcher < StagingEventFetcher
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb
new file mode 100644
index 00000000000..0079d56e0e4
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/test_stage.rb
@@ -0,0 +1,29 @@
+module Gitlab
+ module CycleAnalytics
+ class TestStage < BaseStage
+ def start_time_attrs
+ @start_time_attrs ||= mr_metrics_table[:latest_build_started_at]
+ end
+
+ def end_time_attrs
+ @end_time_attrs ||= mr_metrics_table[:latest_build_finished_at]
+ end
+
+ def name
+ :test
+ end
+
+ def description
+ "Total test time for all commits/merges"
+ end
+
+ def stage_query
+ if @options[:branch]
+ super.where(build_table[:ref].eq(@options[:branch]))
+ else
+ super
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/median.rb b/lib/gitlab/database/median.rb
index 1444d25ebc7..08607c27c09 100644
--- a/lib/gitlab/database/median.rb
+++ b/lib/gitlab/database/median.rb
@@ -103,6 +103,11 @@ module Gitlab
Arel.sql(%Q{EXTRACT(EPOCH FROM "#{arel_attribute.relation.name}"."#{arel_attribute.name}")})
end
+ def extract_diff_epoch(diff)
+ return diff unless Gitlab::Database.postgresql?
+
+ Arel.sql(%Q{EXTRACT(EPOCH FROM (#{diff.to_sql}))})
+ end
# Need to cast '0' to an INTERVAL before we can check if the interval is positive
def zero_interval
Arel::Nodes::NamedFunction.new("CAST", [Arel.sql("'0' AS INTERVAL")])
diff --git a/lib/gitlab/diff/file_collection/merge_request_diff.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb
index 56530448f36..329d12f13d1 100644
--- a/lib/gitlab/diff/file_collection/merge_request_diff.rb
+++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb
@@ -61,7 +61,10 @@ module Gitlab
end
def cacheable?(diff_file)
- @merge_request_diff.present? && diff_file.blob && diff_file.blob.text?
+ @merge_request_diff.present? &&
+ diff_file.blob &&
+ diff_file.blob.text? &&
+ @project.repository.diffable?(diff_file.blob)
end
def cache_key
diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb
index 85402c2a278..8c8dd1b9cef 100644
--- a/lib/gitlab/email/reply_parser.rb
+++ b/lib/gitlab/email/reply_parser.rb
@@ -13,9 +13,17 @@ module Gitlab
encoding = body.encoding
- body = discourse_email_trimmer(body)
+ body = EmailReplyTrimmer.trim(body)
- body = EmailReplyParser.parse_reply(body)
+ return '' unless body
+
+ # not using /\s+$/ here because that deletes empty lines
+ body = body.gsub(/[ \t]$/, '')
+
+ # NOTE: We currently don't support empty quotes.
+ # EmailReplyTrimmer allows this as a special case,
+ # so we detect it manually here.
+ return "" if body.lines.all? { |l| l.strip.empty? || l.start_with?('>') }
body.force_encoding(encoding).encode("UTF-8")
end
@@ -57,30 +65,6 @@ module Gitlab
rescue
nil
end
-
- REPLYING_HEADER_LABELS = %w(From Sent To Subject Reply To Cc Bcc Date)
- REPLYING_HEADER_REGEX = Regexp.union(REPLYING_HEADER_LABELS.map { |label| "#{label}:" })
-
- def discourse_email_trimmer(body)
- lines = body.scrub.lines.to_a
- range_end = 0
-
- lines.each_with_index do |l, idx|
- # This one might be controversial but so many reply lines have years, times and end with a colon.
- # Let's try it and see how well it works.
- break if (l =~ /\d{4}/ && l =~ /\d:\d\d/ && l =~ /\:$/) ||
- (l =~ /On \w+ \d+,? \d+,?.*wrote:/)
-
- # Headers on subsequent lines
- break if (0..2).all? { |off| lines[idx + off] =~ REPLYING_HEADER_REGEX }
- # Headers on the same line
- break if REPLYING_HEADER_LABELS.count { |label| l.include?(label) } >= 3
-
- range_end = idx
- end
-
- lines[0..range_end].join.strip
- end
end
end
end
diff --git a/lib/gitlab/gfm/reference_rewriter.rb b/lib/gitlab/gfm/reference_rewriter.rb
index a7c596dced0..b984492d369 100644
--- a/lib/gitlab/gfm/reference_rewriter.rb
+++ b/lib/gitlab/gfm/reference_rewriter.rb
@@ -76,7 +76,7 @@ module Gitlab
if referable.respond_to?(:project)
referable.to_reference(target_project)
else
- referable.to_reference(@source_project, target_project)
+ referable.to_reference(@source_project, target_project: target_project)
end
end
diff --git a/lib/gitlab/gfm/uploads_rewriter.rb b/lib/gitlab/gfm/uploads_rewriter.rb
index abc8c8c55e6..8fab5489616 100644
--- a/lib/gitlab/gfm/uploads_rewriter.rb
+++ b/lib/gitlab/gfm/uploads_rewriter.rb
@@ -1,3 +1,5 @@
+require 'fileutils'
+
module Gitlab
module Gfm
##
@@ -22,7 +24,9 @@ module Gitlab
return markdown unless file.try(:exists?)
new_uploader = FileUploader.new(target_project)
- new_uploader.store!(file)
+ with_link_in_tmp_dir(file.file) do |open_tmp_file|
+ new_uploader.store!(open_tmp_file)
+ end
new_uploader.to_markdown
end
end
@@ -46,6 +50,19 @@ module Gitlab
uploader.retrieve_from_store!(file)
uploader.file
end
+
+ # Because the uploaders use 'move_to_store' we must have a temporary
+ # file that is allowed to be (re)moved.
+ def with_link_in_tmp_dir(file)
+ dir = Dir.mktmpdir('UploadsRewriter', File.dirname(file))
+ # The filename matters to Carrierwave so we make sure to preserve it
+ tmp_file = File.join(dir, File.basename(file))
+ File.link(file, tmp_file)
+ # Open the file to placate Carrierwave
+ File.open(tmp_file) { |open_file| yield open_file }
+ ensure
+ FileUtils.rm_rf(dir)
+ end
end
end
end
diff --git a/lib/gitlab/git/attributes.rb b/lib/gitlab/git/attributes.rb
new file mode 100644
index 00000000000..42140ecc993
--- /dev/null
+++ b/lib/gitlab/git/attributes.rb
@@ -0,0 +1,131 @@
+module Gitlab
+ module Git
+ # Class for parsing Git attribute files and extracting the attributes for
+ # file patterns.
+ #
+ # Unlike Rugged this parser only needs a single IO call (a call to `open`),
+ # vastly reducing the time spent in extracting attributes.
+ #
+ # This class _only_ supports parsing the attributes file located at
+ # `$GIT_DIR/info/attributes` as GitLab doesn't use any other files
+ # (`.gitattributes` is copied to this particular path).
+ #
+ # Basic usage:
+ #
+ # attributes = Gitlab::Git::Attributes.new(some_repo.path)
+ #
+ # attributes.attributes('README.md') # => { "eol" => "lf }
+ class Attributes
+ # path - The path to the Git repository.
+ def initialize(path)
+ @path = File.expand_path(path)
+ @patterns = nil
+ end
+
+ # Returns all the Git attributes for the given path.
+ #
+ # path - A path to a file for which to get the attributes.
+ #
+ # Returns a Hash.
+ def attributes(path)
+ full_path = File.join(@path, path)
+
+ patterns.each do |pattern, attrs|
+ return attrs if File.fnmatch?(pattern, full_path)
+ end
+
+ {}
+ end
+
+ # Returns a Hash containing the file patterns and their attributes.
+ def patterns
+ @patterns ||= parse_file
+ end
+
+ # Parses an attribute string.
+ #
+ # These strings can be in the following formats:
+ #
+ # text # => { "text" => true }
+ # -text # => { "text" => false }
+ # key=value # => { "key" => "value" }
+ #
+ # string - The string to parse.
+ #
+ # Returns a Hash containing the attributes and their values.
+ def parse_attributes(string)
+ values = {}
+ dash = '-'
+ equal = '='
+ binary = 'binary'
+
+ string.split(/\s+/).each do |chunk|
+ # Data such as "foo = bar" should be treated as "foo" and "bar" being
+ # separate boolean attributes.
+ next if chunk == equal
+
+ key = chunk
+
+ # Input: "-foo"
+ if chunk.start_with?(dash)
+ key = chunk.byteslice(1, chunk.length - 1)
+ value = false
+
+ # Input: "foo=bar"
+ elsif chunk.include?(equal)
+ key, value = chunk.split(equal, 2)
+
+ # Input: "foo"
+ else
+ value = true
+ end
+
+ values[key] = value
+
+ # When the "binary" option is set the "diff" option should be set to
+ # the inverse. If "diff" is later set it should overwrite the
+ # automatically set value.
+ values['diff'] = false if key == binary && value
+ end
+
+ values
+ end
+
+ # Iterates over every line in the attributes file.
+ def each_line
+ full_path = File.join(@path, 'info/attributes')
+
+ return unless File.exist?(full_path)
+
+ File.open(full_path, 'r') do |handle|
+ handle.each_line do |line|
+ break unless line.valid_encoding?
+
+ yield line.strip
+ end
+ end
+ end
+
+ private
+
+ # Parses the Git attributes file.
+ def parse_file
+ pairs = []
+ comment = '#'
+
+ each_line do |line|
+ next if line.start_with?(comment) || line.empty?
+
+ pattern, attrs = line.split(/\s+/, 2)
+
+ parsed = attrs ? parse_attributes(attrs) : {}
+
+ pairs << [File.join(@path, pattern), parsed]
+ end
+
+ # Newer entries take precedence over older entries.
+ pairs.reverse.to_h
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/blame.rb b/lib/gitlab/git/blame.rb
new file mode 100644
index 00000000000..58193391926
--- /dev/null
+++ b/lib/gitlab/git/blame.rb
@@ -0,0 +1,75 @@
+module Gitlab
+ module Git
+ class Blame
+ include Gitlab::Git::EncodingHelper
+
+ attr_reader :lines, :blames
+
+ def initialize(repository, sha, path)
+ @repo = repository
+ @sha = sha
+ @path = path
+ @lines = []
+ @blames = load_blame
+ end
+
+ def each
+ @blames.each do |blame|
+ yield(
+ Gitlab::Git::Commit.new(blame.commit),
+ blame.line
+ )
+ end
+ end
+
+ private
+
+ def load_blame
+ cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{@repo.path} blame -p #{@sha} -- #{@path})
+ # Read in binary mode to ensure ASCII-8BIT
+ raw_output = IO.popen(cmd, 'rb') {|io| io.read }
+ output = encode_utf8(raw_output)
+ process_raw_blame output
+ end
+
+ def process_raw_blame(output)
+ lines, final = [], []
+ info, commits = {}, {}
+
+ # process the output
+ output.split("\n").each do |line|
+ if line[0, 1] == "\t"
+ lines << line[1, line.size]
+ elsif m = /^(\w{40}) (\d+) (\d+)/.match(line)
+ commit_id, old_lineno, lineno = m[1], m[2].to_i, m[3].to_i
+ commits[commit_id] = nil unless commits.key?(commit_id)
+ info[lineno] = [commit_id, old_lineno]
+ end
+ end
+
+ # load all commits in single call
+ commits.keys.each do |key|
+ commits[key] = @repo.lookup(key)
+ end
+
+ # get it together
+ info.sort.each do |lineno, (commit_id, old_lineno)|
+ commit = commits[commit_id]
+ final << BlameLine.new(lineno, old_lineno, commit, lines[lineno - 1])
+ end
+
+ @lines = final
+ end
+ end
+
+ class BlameLine
+ attr_accessor :lineno, :oldlineno, :commit, :line
+ def initialize(lineno, oldlineno, commit, line)
+ @lineno = lineno
+ @oldlineno = oldlineno
+ @commit = commit
+ @line = line
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
new file mode 100644
index 00000000000..b742d9e1e4b
--- /dev/null
+++ b/lib/gitlab/git/blob.rb
@@ -0,0 +1,330 @@
+module Gitlab
+ module Git
+ class Blob
+ include Linguist::BlobHelper
+ include Gitlab::Git::EncodingHelper
+
+ # This number is the maximum amount of data that we want to display to
+ # the user. We load as much as we can for encoding detection
+ # (Linguist) and LFS pointer parsing. All other cases where we need full
+ # blob data should use load_all_data!.
+ MAX_DATA_DISPLAY_SIZE = 10485760
+
+ attr_accessor :name, :path, :size, :data, :mode, :id, :commit_id, :loaded_size, :binary
+
+ class << self
+ def find(repository, sha, path)
+ commit = repository.lookup(sha)
+ root_tree = commit.tree
+
+ blob_entry = find_entry_by_path(repository, root_tree.oid, path)
+
+ return nil unless blob_entry
+
+ if blob_entry[:type] == :commit
+ submodule_blob(blob_entry, path, sha)
+ else
+ blob = repository.lookup(blob_entry[:oid])
+
+ if blob
+ new(
+ id: blob.oid,
+ name: blob_entry[:name],
+ size: blob.size,
+ data: blob.content(MAX_DATA_DISPLAY_SIZE),
+ mode: blob_entry[:filemode].to_s(8),
+ path: path,
+ commit_id: sha,
+ binary: blob.binary?
+ )
+ end
+ end
+ end
+
+ def raw(repository, sha)
+ blob = repository.lookup(sha)
+
+ new(
+ id: blob.oid,
+ size: blob.size,
+ data: blob.content(MAX_DATA_DISPLAY_SIZE),
+ binary: blob.binary?
+ )
+ end
+
+ # Recursive search of blob id by path
+ #
+ # Ex.
+ # blog/ # oid: 1a
+ # app/ # oid: 2a
+ # models/ # oid: 3a
+ # file.rb # oid: 4a
+ #
+ #
+ # Blob.find_entry_by_path(repo, '1a', 'app/file.rb') # => '4a'
+ #
+ def find_entry_by_path(repository, root_id, path)
+ root_tree = repository.lookup(root_id)
+ # Strip leading slashes
+ path[/^\/*/] = ''
+ path_arr = path.split('/')
+
+ entry = root_tree.find do |entry|
+ entry[:name] == path_arr[0]
+ end
+
+ return nil unless entry
+
+ if path_arr.size > 1
+ return nil unless entry[:type] == :tree
+ path_arr.shift
+ find_entry_by_path(repository, entry[:oid], path_arr.join('/'))
+ else
+ [:blob, :commit].include?(entry[:type]) ? entry : nil
+ end
+ end
+
+ def submodule_blob(blob_entry, path, sha)
+ new(
+ id: blob_entry[:oid],
+ name: blob_entry[:name],
+ data: '',
+ path: path,
+ commit_id: sha,
+ )
+ end
+
+ # Commit file in repository and return commit sha
+ #
+ # options should contain next structure:
+ # file: {
+ # content: 'Lorem ipsum...',
+ # path: 'documents/story.txt',
+ # update: true
+ # },
+ # author: {
+ # email: 'user@example.com',
+ # name: 'Test User',
+ # time: Time.now
+ # },
+ # committer: {
+ # email: 'user@example.com',
+ # name: 'Test User',
+ # time: Time.now
+ # },
+ # commit: {
+ # message: 'Wow such commit',
+ # branch: 'master',
+ # update_ref: false
+ # }
+ #
+ # rubocop:disable Metrics/AbcSize
+ # rubocop:disable Metrics/CyclomaticComplexity
+ # rubocop:disable Metrics/PerceivedComplexity
+ def commit(repository, options, action = :add)
+ file = options[:file]
+ update = file[:update].nil? ? true : file[:update]
+ author = options[:author]
+ committer = options[:committer]
+ commit = options[:commit]
+ repo = repository.rugged
+ ref = commit[:branch]
+ update_ref = commit[:update_ref].nil? ? true : commit[:update_ref]
+ parents = []
+ mode = 0o100644
+
+ unless ref.start_with?('refs/')
+ ref = 'refs/heads/' + ref
+ end
+
+ path_name = Gitlab::Git::PathHelper.normalize_path(file[:path])
+ # Abort if any invalid characters remain (e.g. ../foo)
+ raise Gitlab::Git::Repository::InvalidBlobName.new("Invalid path") if path_name.each_filename.to_a.include?('..')
+
+ filename = path_name.to_s
+ index = repo.index
+
+ unless repo.empty?
+ rugged_ref = repo.references[ref]
+ raise Gitlab::Git::Repository::InvalidRef.new("Invalid branch name") unless rugged_ref
+ last_commit = rugged_ref.target
+ index.read_tree(last_commit.tree)
+ parents = [last_commit]
+ end
+
+ if action == :remove
+ index.remove(filename)
+ else
+ file_entry = index.get(filename)
+
+ if action == :rename
+ old_path_name = Gitlab::Git::PathHelper.normalize_path(file[:previous_path])
+ old_filename = old_path_name.to_s
+ file_entry = index.get(old_filename)
+ index.remove(old_filename) unless file_entry.blank?
+ end
+
+ if file_entry
+ raise Gitlab::Git::Repository::InvalidBlobName.new("Filename already exists; update not allowed") unless update
+
+ # Preserve the current file mode if one is available
+ mode = file_entry[:mode] if file_entry[:mode]
+ end
+
+ content = file[:content]
+ detect = CharlockHolmes::EncodingDetector.new.detect(content) if content
+
+ unless detect && detect[:type] == :binary
+ # When writing to the repo directly as we are doing here,
+ # the `core.autocrlf` config isn't taken into account.
+ content.gsub!("\r\n", "\n") if repository.autocrlf
+ end
+
+ oid = repo.write(content, :blob)
+ index.add(path: filename, oid: oid, mode: mode)
+ end
+
+ opts = {}
+ opts[:tree] = index.write_tree(repo)
+ opts[:author] = author
+ opts[:committer] = committer
+ opts[:message] = commit[:message]
+ opts[:parents] = parents
+ opts[:update_ref] = ref if update_ref
+
+ Rugged::Commit.create(repo, opts)
+ end
+ # rubocop:enable Metrics/AbcSize
+ # rubocop:enable Metrics/CyclomaticComplexity
+ # rubocop:enable Metrics/PerceivedComplexity
+
+ # Remove file from repository and return commit sha
+ #
+ # options should contain next structure:
+ # file: {
+ # path: 'documents/story.txt'
+ # },
+ # author: {
+ # email: 'user@example.com',
+ # name: 'Test User',
+ # time: Time.now
+ # },
+ # committer: {
+ # email: 'user@example.com',
+ # name: 'Test User',
+ # time: Time.now
+ # },
+ # commit: {
+ # message: 'Remove FILENAME',
+ # branch: 'master'
+ # }
+ #
+ def remove(repository, options)
+ commit(repository, options, :remove)
+ end
+
+ # Rename file from repository and return commit sha
+ #
+ # options should contain next structure:
+ # file: {
+ # previous_path: 'documents/old_story.txt'
+ # path: 'documents/story.txt'
+ # content: 'Lorem ipsum...',
+ # update: true
+ # },
+ # author: {
+ # email: 'user@example.com',
+ # name: 'Test User',
+ # time: Time.now
+ # },
+ # committer: {
+ # email: 'user@example.com',
+ # name: 'Test User',
+ # time: Time.now
+ # },
+ # commit: {
+ # message: 'Rename FILENAME',
+ # branch: 'master'
+ # }
+ #
+ def rename(repository, options)
+ commit(repository, options, :rename)
+ end
+ end
+
+ def initialize(options)
+ %w(id name path size data mode commit_id binary).each do |key|
+ self.send("#{key}=", options[key.to_sym])
+ end
+
+ @loaded_all_data = false
+ # Retain the actual size before it is encoded
+ @loaded_size = @data.bytesize if @data
+ end
+
+ def binary?
+ @binary.nil? ? super : @binary == true
+ end
+
+ def empty?
+ !data || data == ''
+ end
+
+ def data
+ encode! @data
+ end
+
+ # Load all blob data (not just the first MAX_DATA_DISPLAY_SIZE bytes) into
+ # memory as a Ruby string.
+ def load_all_data!(repository)
+ return if @data == '' # don't mess with submodule blobs
+ return @data if @loaded_all_data
+
+ @loaded_all_data = true
+ @data = repository.lookup(id).content
+ @loaded_size = @data.bytesize
+ end
+
+ def name
+ encode! @name
+ end
+
+ # Valid LFS object pointer is a text file consisting of
+ # version
+ # oid
+ # size
+ # see https://github.com/github/git-lfs/blob/v1.1.0/docs/spec.md#the-pointer
+ def lfs_pointer?
+ has_lfs_version_key? && lfs_oid.present? && lfs_size.present?
+ end
+
+ def lfs_oid
+ if has_lfs_version_key?
+ oid = data.match(/(?<=sha256:)([0-9a-f]{64})/)
+ return oid[1] if oid
+ end
+
+ nil
+ end
+
+ def lfs_size
+ if has_lfs_version_key?
+ size = data.match(/(?<=size )([0-9]+)/)
+ return size[1] if size
+ end
+
+ nil
+ end
+
+ def truncated?
+ size && (size > loaded_size)
+ end
+
+ private
+
+ def has_lfs_version_key?
+ !empty? && text? && data.start_with?("version https://git-lfs.github.com/spec")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/blob_snippet.rb b/lib/gitlab/git/blob_snippet.rb
new file mode 100644
index 00000000000..e98de57fc22
--- /dev/null
+++ b/lib/gitlab/git/blob_snippet.rb
@@ -0,0 +1,32 @@
+module Gitlab
+ module Git
+ class BlobSnippet
+ include Linguist::BlobHelper
+
+ attr_accessor :ref
+ attr_accessor :lines
+ attr_accessor :filename
+ attr_accessor :startline
+
+ def initialize(ref, lines, startline, filename)
+ @ref, @lines, @startline, @filename = ref, lines, startline, filename
+ end
+
+ def data
+ lines.join("\n") if lines
+ end
+
+ def name
+ filename
+ end
+
+ def size
+ data.length
+ end
+
+ def mode
+ nil
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/branch.rb b/lib/gitlab/git/branch.rb
new file mode 100644
index 00000000000..586380da94a
--- /dev/null
+++ b/lib/gitlab/git/branch.rb
@@ -0,0 +1,6 @@
+module Gitlab
+ module Git
+ class Branch < Ref
+ end
+ end
+end
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
new file mode 100644
index 00000000000..d785516ebdd
--- /dev/null
+++ b/lib/gitlab/git/commit.rb
@@ -0,0 +1,310 @@
+# Gitlab::Git::Commit is a wrapper around native Rugged::Commit object
+module Gitlab
+ module Git
+ class Commit
+ include Gitlab::Git::EncodingHelper
+
+ attr_accessor :raw_commit, :head, :refs
+
+ SERIALIZE_KEYS = [
+ :id, :message, :parent_ids,
+ :authored_date, :author_name, :author_email,
+ :committed_date, :committer_name, :committer_email
+ ].freeze
+
+ attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator
+
+ def ==(other)
+ return false unless other.is_a?(Gitlab::Git::Commit)
+
+ methods = [:message, :parent_ids, :authored_date, :author_name,
+ :author_email, :committed_date, :committer_name,
+ :committer_email]
+
+ methods.all? do |method|
+ send(method) == other.send(method)
+ end
+ end
+
+ class << self
+ # Get commits collection
+ #
+ # Ex.
+ # Commit.where(
+ # repo: repo,
+ # ref: 'master',
+ # path: 'app/models',
+ # limit: 10,
+ # offset: 5,
+ # )
+ #
+ def where(options)
+ repo = options.delete(:repo)
+ raise 'Gitlab::Git::Repository is required' unless repo.respond_to?(:log)
+
+ repo.log(options).map { |c| decorate(c) }
+ end
+
+ # Get single commit
+ #
+ # Ex.
+ # Commit.find(repo, '29eda46b')
+ #
+ # Commit.find(repo, 'master')
+ #
+ def find(repo, commit_id = "HEAD")
+ return decorate(commit_id) if commit_id.is_a?(Rugged::Commit)
+
+ obj = if commit_id.is_a?(String)
+ repo.rev_parse_target(commit_id)
+ else
+ Gitlab::Git::Ref.dereference_object(commit_id)
+ end
+
+ return nil unless obj.is_a?(Rugged::Commit)
+
+ decorate(obj)
+ rescue Rugged::ReferenceError, Rugged::InvalidError, Rugged::ObjectError, Gitlab::Git::Repository::NoRepository
+ nil
+ end
+
+ # Get last commit for HEAD
+ #
+ # Ex.
+ # Commit.last(repo)
+ #
+ def last(repo)
+ find(repo)
+ end
+
+ # Get last commit for specified path and ref
+ #
+ # Ex.
+ # Commit.last_for_path(repo, '29eda46b', 'app/models')
+ #
+ # Commit.last_for_path(repo, 'master', 'Gemfile')
+ #
+ def last_for_path(repo, ref, path = nil)
+ where(
+ repo: repo,
+ ref: ref,
+ path: path,
+ limit: 1
+ ).first
+ end
+
+ # Get commits between two revspecs
+ # See also #repository.commits_between
+ #
+ # Ex.
+ # Commit.between(repo, '29eda46b', 'master')
+ #
+ def between(repo, base, head)
+ repo.commits_between(base, head).map do |commit|
+ decorate(commit)
+ end
+ rescue Rugged::ReferenceError
+ []
+ end
+
+ # Delegate Repository#find_commits
+ def find_all(repo, options = {})
+ repo.find_commits(options)
+ end
+
+ def decorate(commit, ref = nil)
+ Gitlab::Git::Commit.new(commit, ref)
+ end
+
+ # Returns a diff object for the changes introduced by +rugged_commit+.
+ # If +rugged_commit+ doesn't have a parent, then the diff is between
+ # this commit and an empty repo. See Repository#diff for the keys
+ # allowed in the +options+ hash.
+ def diff_from_parent(rugged_commit, options = {})
+ options ||= {}
+ break_rewrites = options[:break_rewrites]
+ actual_options = Gitlab::Git::Diff.filter_diff_options(options)
+
+ diff = if rugged_commit.parents.empty?
+ rugged_commit.diff(actual_options.merge(reverse: true))
+ else
+ rugged_commit.parents[0].diff(rugged_commit, actual_options)
+ end
+
+ diff.find_similar!(break_rewrites: break_rewrites)
+ diff
+ end
+ end
+
+ def initialize(raw_commit, head = nil)
+ raise "Nil as raw commit passed" unless raw_commit
+
+ if raw_commit.is_a?(Hash)
+ init_from_hash(raw_commit)
+ elsif raw_commit.is_a?(Rugged::Commit)
+ init_from_rugged(raw_commit)
+ else
+ raise "Invalid raw commit type: #{raw_commit.class}"
+ end
+
+ @head = head
+ end
+
+ def sha
+ id
+ end
+
+ def short_id(length = 10)
+ id.to_s[0..length]
+ end
+
+ def safe_message
+ @safe_message ||= message
+ end
+
+ def created_at
+ committed_date
+ end
+
+ # Was this commit committed by a different person than the original author?
+ def different_committer?
+ author_name != committer_name || author_email != committer_email
+ end
+
+ def parent_id
+ parent_ids.first
+ end
+
+ # Shows the diff between the commit's parent and the commit.
+ #
+ # Cuts out the header and stats from #to_patch and returns only the diff.
+ def to_diff(options = {})
+ diff_from_parent(options).patch
+ end
+
+ # Returns a diff object for the changes from this commit's first parent.
+ # If there is no parent, then the diff is between this commit and an
+ # empty repo. See Repository#diff for keys allowed in the +options+
+ # hash.
+ def diff_from_parent(options = {})
+ Commit.diff_from_parent(raw_commit, options)
+ end
+
+ def has_zero_stats?
+ stats.total.zero?
+ rescue
+ true
+ end
+
+ def no_commit_message
+ "--no commit message"
+ end
+
+ def to_hash
+ serialize_keys.map.with_object({}) do |key, hash|
+ hash[key] = send(key)
+ end
+ end
+
+ def date
+ committed_date
+ end
+
+ def diffs(options = {})
+ Gitlab::Git::DiffCollection.new(diff_from_parent(options), options)
+ end
+
+ def parents
+ raw_commit.parents.map { |c| Gitlab::Git::Commit.new(c) }
+ end
+
+ def tree
+ raw_commit.tree
+ end
+
+ def stats
+ Gitlab::Git::CommitStats.new(self)
+ end
+
+ def to_patch(options = {})
+ begin
+ raw_commit.to_mbox(options)
+ rescue Rugged::InvalidError => ex
+ if ex.message =~ /Commit \w+ is a merge commit/
+ 'Patch format is not currently supported for merge commits.'
+ end
+ end
+ end
+
+ # Get a collection of Rugged::Reference objects for this commit.
+ #
+ # Ex.
+ # commit.ref(repo)
+ #
+ def refs(repo)
+ repo.refs_hash[id]
+ end
+
+ # Get ref names collection
+ #
+ # Ex.
+ # commit.ref_names(repo)
+ #
+ def ref_names(repo)
+ refs(repo).map do |ref|
+ ref.name.sub(%r{^refs/(heads|remotes|tags)/}, "")
+ end
+ end
+
+ def message
+ encode! @message
+ end
+
+ def author_name
+ encode! @author_name
+ end
+
+ def author_email
+ encode! @author_email
+ end
+
+ def committer_name
+ encode! @committer_name
+ end
+
+ def committer_email
+ encode! @committer_email
+ end
+
+ private
+
+ def init_from_hash(hash)
+ raw_commit = hash.symbolize_keys
+
+ serialize_keys.each do |key|
+ send("#{key}=", raw_commit[key])
+ end
+ end
+
+ def init_from_rugged(commit)
+ author = commit.author
+ committer = commit.committer
+
+ @raw_commit = commit
+ @id = commit.oid
+ @message = commit.message
+ @authored_date = author[:time]
+ @committed_date = committer[:time]
+ @author_name = author[:name]
+ @author_email = author[:email]
+ @committer_name = committer[:name]
+ @committer_email = committer[:email]
+ @parent_ids = commit.parents.map(&:oid)
+ end
+
+ def serialize_keys
+ SERIALIZE_KEYS
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/commit_stats.rb b/lib/gitlab/git/commit_stats.rb
new file mode 100644
index 00000000000..e9118bbed0e
--- /dev/null
+++ b/lib/gitlab/git/commit_stats.rb
@@ -0,0 +1,26 @@
+# Gitlab::Git::CommitStats counts the additions, deletions, and total changes
+# in a commit.
+module Gitlab
+ module Git
+ class CommitStats
+ attr_reader :id, :additions, :deletions, :total
+
+ # Instantiate a CommitStats object
+ def initialize(commit)
+ @id = commit.id
+ @additions = 0
+ @deletions = 0
+ @total = 0
+
+ diff = commit.diff_from_parent
+
+ diff.each_patch do |p|
+ # TODO: Use the new Rugged convenience methods when they're released
+ @additions += p.stat[0]
+ @deletions += p.stat[1]
+ @total += p.changes
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/compare.rb b/lib/gitlab/git/compare.rb
new file mode 100644
index 00000000000..696a2acd5e3
--- /dev/null
+++ b/lib/gitlab/git/compare.rb
@@ -0,0 +1,43 @@
+module Gitlab
+ module Git
+ class Compare
+ attr_reader :head, :base, :straight
+
+ def initialize(repository, base, head, straight = false)
+ @repository = repository
+ @straight = straight
+
+ unless base && head
+ @commits = []
+ return
+ end
+
+ @base = Gitlab::Git::Commit.find(repository, base.try(:strip))
+ @head = Gitlab::Git::Commit.find(repository, head.try(:strip))
+
+ @commits = [] unless @base && @head
+ @commits = [] if same
+ end
+
+ def same
+ @base && @head && @base.id == @head.id
+ end
+
+ def commits
+ return @commits if defined?(@commits)
+
+ @commits = Gitlab::Git::Commit.between(@repository, @base.id, @head.id)
+ end
+
+ def diffs(options = {})
+ unless @head && @base
+ return Gitlab::Git::DiffCollection.new([])
+ end
+
+ paths = options.delete(:paths) || []
+ options[:straight] = @straight
+ Gitlab::Git::Diff.between(@repository, @head.id, @base.id, options, *paths)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb
new file mode 100644
index 00000000000..d6b3b5705a9
--- /dev/null
+++ b/lib/gitlab/git/diff.rb
@@ -0,0 +1,322 @@
+# Gitlab::Git::Diff is a wrapper around native Rugged::Diff object
+module Gitlab
+ module Git
+ class Diff
+ class TimeoutError < StandardError; end
+ include Gitlab::Git::EncodingHelper
+
+ # Diff properties
+ attr_accessor :old_path, :new_path, :a_mode, :b_mode, :diff
+
+ # Stats properties
+ attr_accessor :new_file, :renamed_file, :deleted_file
+
+ attr_accessor :too_large
+
+ # The maximum size of a diff to display.
+ DIFF_SIZE_LIMIT = 102400 # 100 KB
+
+ # The maximum size before a diff is collapsed.
+ DIFF_COLLAPSE_LIMIT = 10240 # 10 KB
+
+ class << self
+ def between(repo, head, base, options = {}, *paths)
+ straight = options.delete(:straight) || false
+
+ common_commit = if straight
+ base
+ else
+ # Only show what is new in the source branch
+ # compared to the target branch, not the other way
+ # around. The linex below with merge_base is
+ # equivalent to diff with three dots (git diff
+ # branch1...branch2) From the git documentation:
+ # "git diff A...B" is equivalent to "git diff
+ # $(git-merge-base A B) B"
+ repo.merge_base_commit(head, base)
+ end
+
+ options ||= {}
+ actual_options = filter_diff_options(options)
+ repo.diff(common_commit, head, actual_options, *paths)
+ end
+
+ # Return a copy of the +options+ hash containing only keys that can be
+ # passed to Rugged. Allowed options are:
+ #
+ # :max_size ::
+ # An integer specifying the maximum byte size of a file before a it
+ # will be treated as binary. The default value is 512MB.
+ #
+ # :context_lines ::
+ # The number of unchanged lines that define the boundary of a hunk
+ # (and to display before and after the actual changes). The default is
+ # 3.
+ #
+ # :interhunk_lines ::
+ # The maximum number of unchanged lines between hunk boundaries before
+ # the hunks will be merged into a one. The default is 0.
+ #
+ # :old_prefix ::
+ # The virtual "directory" to prefix to old filenames in hunk headers.
+ # The default is "a".
+ #
+ # :new_prefix ::
+ # The virtual "directory" to prefix to new filenames in hunk headers.
+ # The default is "b".
+ #
+ # :reverse ::
+ # If true, the sides of the diff will be reversed.
+ #
+ # :force_text ::
+ # If true, all files will be treated as text, disabling binary
+ # attributes & detection.
+ #
+ # :ignore_whitespace ::
+ # If true, all whitespace will be ignored.
+ #
+ # :ignore_whitespace_change ::
+ # If true, changes in amount of whitespace will be ignored.
+ #
+ # :ignore_whitespace_eol ::
+ # If true, whitespace at end of line will be ignored.
+ #
+ # :ignore_submodules ::
+ # if true, submodules will be excluded from the diff completely.
+ #
+ # :patience ::
+ # If true, the "patience diff" algorithm will be used (currenlty
+ # unimplemented).
+ #
+ # :include_ignored ::
+ # If true, ignored files will be included in the diff.
+ #
+ # :include_untracked ::
+ # If true, untracked files will be included in the diff.
+ #
+ # :include_unmodified ::
+ # If true, unmodified files will be included in the diff.
+ #
+ # :recurse_untracked_dirs ::
+ # Even if +:include_untracked+ is true, untracked directories will
+ # only be marked with a single entry in the diff. If this flag is set
+ # to true, all files under ignored directories will be included in the
+ # diff, too.
+ #
+ # :disable_pathspec_match ::
+ # If true, the given +*paths+ will be applied as exact matches,
+ # instead of as fnmatch patterns.
+ #
+ # :deltas_are_icase ::
+ # If true, filename comparisons will be made with case-insensitivity.
+ #
+ # :include_untracked_content ::
+ # if true, untracked content will be contained in the the diff patch
+ # text.
+ #
+ # :skip_binary_check ::
+ # If true, diff deltas will be generated without spending time on
+ # binary detection. This is useful to improve performance in cases
+ # where the actual file content difference is not needed.
+ #
+ # :include_typechange ::
+ # If true, type changes for files will not be interpreted as deletion
+ # of the "old file" and addition of the "new file", but will generate
+ # typechange records.
+ #
+ # :include_typechange_trees ::
+ # Even if +:include_typechange+ is true, blob -> tree changes will
+ # still usually be handled as a deletion of the blob. If this flag is
+ # set to true, blob -> tree changes will be marked as typechanges.
+ #
+ # :ignore_filemode ::
+ # If true, file mode changes will be ignored.
+ #
+ # :recurse_ignored_dirs ::
+ # Even if +:include_ignored+ is true, ignored directories will only be
+ # marked with a single entry in the diff. If this flag is set to true,
+ # all files under ignored directories will be included in the diff,
+ # too.
+ def filter_diff_options(options, default_options = {})
+ allowed_options = [:max_size, :context_lines, :interhunk_lines,
+ :old_prefix, :new_prefix, :reverse, :force_text,
+ :ignore_whitespace, :ignore_whitespace_change,
+ :ignore_whitespace_eol, :ignore_submodules,
+ :patience, :include_ignored, :include_untracked,
+ :include_unmodified, :recurse_untracked_dirs,
+ :disable_pathspec_match, :deltas_are_icase,
+ :include_untracked_content, :skip_binary_check,
+ :include_typechange, :include_typechange_trees,
+ :ignore_filemode, :recurse_ignored_dirs, :paths,
+ :max_files, :max_lines, :all_diffs, :no_collapse]
+
+ if default_options
+ actual_defaults = default_options.dup
+ actual_defaults.keep_if do |key|
+ allowed_options.include?(key)
+ end
+ else
+ actual_defaults = {}
+ end
+
+ if options
+ filtered_opts = options.dup
+ filtered_opts.keep_if do |key|
+ allowed_options.include?(key)
+ end
+ filtered_opts = actual_defaults.merge(filtered_opts)
+ else
+ filtered_opts = actual_defaults
+ end
+
+ filtered_opts
+ end
+ end
+
+ def initialize(raw_diff, collapse: false)
+ case raw_diff
+ when Hash
+ init_from_hash(raw_diff, collapse: collapse)
+ when Rugged::Patch, Rugged::Diff::Delta
+ init_from_rugged(raw_diff, collapse: collapse)
+ when nil
+ raise "Nil as raw diff passed"
+ else
+ raise "Invalid raw diff type: #{raw_diff.class}"
+ end
+ end
+
+ def serialize_keys
+ @serialize_keys ||= %i(diff new_path old_path a_mode b_mode new_file renamed_file deleted_file too_large)
+ end
+
+ def to_hash
+ hash = {}
+
+ keys = serialize_keys
+
+ keys.each do |key|
+ hash[key] = send(key)
+ end
+
+ hash
+ end
+
+ def submodule?
+ a_mode == '160000' || b_mode == '160000'
+ end
+
+ def line_count
+ @line_count ||= Util.count_lines(@diff)
+ end
+
+ def too_large?
+ if @too_large.nil?
+ @too_large = @diff.bytesize >= DIFF_SIZE_LIMIT
+ else
+ @too_large
+ end
+ end
+
+ def collapsible?
+ @diff.bytesize >= DIFF_COLLAPSE_LIMIT
+ end
+
+ def prune_large_diff!
+ @diff = ''
+ @line_count = 0
+ @too_large = true
+ end
+
+ def collapsed?
+ return @collapsed if defined?(@collapsed)
+ false
+ end
+
+ def prune_collapsed_diff!
+ @diff = ''
+ @line_count = 0
+ @collapsed = true
+ end
+
+ private
+
+ def init_from_rugged(rugged, collapse: false)
+ if rugged.is_a?(Rugged::Patch)
+ init_from_rugged_patch(rugged, collapse: collapse)
+ d = rugged.delta
+ else
+ d = rugged
+ end
+
+ @new_path = encode!(d.new_file[:path])
+ @old_path = encode!(d.old_file[:path])
+ @a_mode = d.old_file[:mode].to_s(8)
+ @b_mode = d.new_file[:mode].to_s(8)
+ @new_file = d.added?
+ @renamed_file = d.renamed?
+ @deleted_file = d.deleted?
+ end
+
+ def init_from_rugged_patch(patch, collapse: false)
+ # Don't bother initializing diffs that are too large. If a diff is
+ # binary we're not going to display anything so we skip the size check.
+ return if !patch.delta.binary? && prune_large_patch(patch, collapse)
+
+ @diff = encode!(strip_diff_headers(patch.to_s))
+ end
+
+ def init_from_hash(hash, collapse: false)
+ raw_diff = hash.symbolize_keys
+
+ serialize_keys.each do |key|
+ send(:"#{key}=", raw_diff[key.to_sym])
+ end
+
+ prune_large_diff! if too_large?
+ prune_collapsed_diff! if collapse && collapsible?
+ end
+
+ # If the patch surpasses any of the diff limits it calls the appropiate
+ # prune method and returns true. Otherwise returns false.
+ def prune_large_patch(patch, collapse)
+ size = 0
+
+ patch.each_hunk do |hunk|
+ hunk.each_line do |line|
+ size += line.content.bytesize
+
+ if size >= DIFF_SIZE_LIMIT
+ prune_large_diff!
+ return true
+ end
+ end
+ end
+
+ if collapse && size >= DIFF_COLLAPSE_LIMIT
+ prune_collapsed_diff!
+ return true
+ end
+
+ false
+ end
+
+ # Strip out the information at the beginning of the patch's text to match
+ # Grit's output
+ def strip_diff_headers(diff_text)
+ # Delete everything up to the first line that starts with '---' or
+ # 'Binary'
+ diff_text.sub!(/\A.*?^(---|Binary)/m, '\1')
+
+ if diff_text.start_with?('---', 'Binary')
+ diff_text
+ else
+ # If the diff_text did not contain a line starting with '---' or
+ # 'Binary', return the empty string. No idea why; we are just
+ # preserving behavior from before the refactor.
+ ''
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb
new file mode 100644
index 00000000000..65e06f5065d
--- /dev/null
+++ b/lib/gitlab/git/diff_collection.rb
@@ -0,0 +1,129 @@
+module Gitlab
+ module Git
+ class DiffCollection
+ include Enumerable
+
+ DEFAULT_LIMITS = { max_files: 100, max_lines: 5000 }.freeze
+
+ def initialize(iterator, options = {})
+ @iterator = iterator
+ @max_files = options.fetch(:max_files, DEFAULT_LIMITS[:max_files])
+ @max_lines = options.fetch(:max_lines, DEFAULT_LIMITS[:max_lines])
+ @max_bytes = @max_files * 5120 # Average 5 KB per file
+ @safe_max_files = [@max_files, DEFAULT_LIMITS[:max_files]].min
+ @safe_max_lines = [@max_lines, DEFAULT_LIMITS[:max_lines]].min
+ @safe_max_bytes = @safe_max_files * 5120 # Average 5 KB per file
+ @all_diffs = !!options.fetch(:all_diffs, false)
+ @no_collapse = !!options.fetch(:no_collapse, true)
+ @deltas_only = !!options.fetch(:deltas_only, false)
+
+ @line_count = 0
+ @byte_count = 0
+ @overflow = false
+ @array = Array.new
+ end
+
+ def each(&block)
+ if @populated
+ # @iterator.each is slower than just iterating the array in place
+ @array.each(&block)
+ elsif @deltas_only
+ each_delta(&block)
+ else
+ each_patch(&block)
+ end
+ end
+
+ def empty?
+ !@iterator.any?
+ end
+
+ def overflow?
+ populate!
+ !!@overflow
+ end
+
+ def size
+ @size ||= count # forces a loop using each method
+ end
+
+ def real_size
+ populate!
+
+ if @overflow
+ "#{size}+"
+ else
+ size.to_s
+ end
+ end
+
+ def decorate!
+ collection = each_with_index do |element, i|
+ @array[i] = yield(element)
+ end
+ @populated = true
+ collection
+ end
+
+ private
+
+ def populate!
+ return if @populated
+
+ each { nil } # force a loop through all diffs
+ @populated = true
+ nil
+ end
+
+ def over_safe_limits?(files)
+ files >= @safe_max_files || @line_count > @safe_max_lines || @byte_count >= @safe_max_bytes
+ end
+
+ def each_delta
+ @iterator.each_delta.with_index do |delta, i|
+ diff = Gitlab::Git::Diff.new(delta)
+
+ yield @array[i] = diff
+ end
+ end
+
+ def each_patch
+ @iterator.each_with_index do |raw, i|
+ # First yield cached Diff instances from @array
+ if @array[i]
+ yield @array[i]
+ next
+ end
+
+ # We have exhausted @array, time to create new Diff instances or stop.
+ break if @overflow
+
+ if !@all_diffs && i >= @max_files
+ @overflow = true
+ break
+ end
+
+ collapse = !@all_diffs && !@no_collapse
+
+ diff = Gitlab::Git::Diff.new(raw, collapse: collapse)
+
+ if collapse && over_safe_limits?(i)
+ diff.prune_collapsed_diff!
+ end
+
+ @line_count += diff.line_count
+ @byte_count += diff.diff.bytesize
+
+ if !@all_diffs && (@line_count >= @max_lines || @byte_count >= @max_bytes)
+ # This last Diff instance pushes us over the lines limit. We stop and
+ # discard it.
+ @overflow = true
+ break
+ end
+
+ yield @array[i] = diff
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/encoding_helper.rb b/lib/gitlab/git/encoding_helper.rb
new file mode 100644
index 00000000000..e57d228e688
--- /dev/null
+++ b/lib/gitlab/git/encoding_helper.rb
@@ -0,0 +1,58 @@
+module Gitlab
+ module Git
+ module EncodingHelper
+ extend self
+
+ # This threshold is carefully tweaked to prevent usage of encodings detected
+ # by CharlockHolmes with low confidence. If CharlockHolmes confidence is low,
+ # we're better off sticking with utf8 encoding.
+ # Reason: git diff can return strings with invalid utf8 byte sequences if it
+ # truncates a diff in the middle of a multibyte character. In this case
+ # CharlockHolmes will try to guess the encoding and will likely suggest an
+ # obscure encoding with low confidence.
+ # There is a lot more info with this merge request:
+ # https://gitlab.com/gitlab-org/gitlab_git/merge_requests/77#note_4754193
+ ENCODING_CONFIDENCE_THRESHOLD = 40
+
+ def encode!(message)
+ return nil unless message.respond_to? :force_encoding
+
+ # if message is utf-8 encoding, just return it
+ message.force_encoding("UTF-8")
+ return message if message.valid_encoding?
+
+ # return message if message type is binary
+ detect = CharlockHolmes::EncodingDetector.detect(message)
+ return message.force_encoding("BINARY") if detect && detect[:type] == :binary
+
+ # force detected encoding if we have sufficient confidence.
+ if detect && detect[:encoding] && detect[:confidence] > ENCODING_CONFIDENCE_THRESHOLD
+ message.force_encoding(detect[:encoding])
+ end
+
+ # encode and clean the bad chars
+ message.replace clean(message)
+ rescue
+ encoding = detect ? detect[:encoding] : "unknown"
+ "--broken encoding: #{encoding}"
+ end
+
+ def encode_utf8(message)
+ detect = CharlockHolmes::EncodingDetector.detect(message)
+ if detect
+ CharlockHolmes::Converter.convert(message, detect[:encoding], 'UTF-8')
+ else
+ clean(message)
+ end
+ end
+
+ private
+
+ def clean(message)
+ message.encode("UTF-16BE", undef: :replace, invalid: :replace, replace: "")
+ .encode("UTF-8")
+ .gsub("\0".encode("UTF-8"), "")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/path_helper.rb b/lib/gitlab/git/path_helper.rb
new file mode 100644
index 00000000000..0148cd8df05
--- /dev/null
+++ b/lib/gitlab/git/path_helper.rb
@@ -0,0 +1,16 @@
+module Gitlab
+ module Git
+ class PathHelper
+ class << self
+ def normalize_path(filename)
+ # Strip all leading slashes so that //foo -> foo
+ filename[/^\/*/] = ''
+
+ # Expand relative paths (e.g. foo/../bar)
+ filename = Pathname.new(filename)
+ filename.relative_path_from(Pathname.new(''))
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/popen.rb b/lib/gitlab/git/popen.rb
new file mode 100644
index 00000000000..df9ca3ee5ac
--- /dev/null
+++ b/lib/gitlab/git/popen.rb
@@ -0,0 +1,26 @@
+require 'open3'
+
+module Gitlab
+ module Git
+ module Popen
+ def popen(cmd, path)
+ unless cmd.is_a?(Array)
+ raise "System commands must be given as an array of strings"
+ end
+
+ vars = { "PWD" => path }
+ options = { chdir: path }
+
+ @cmd_output = ""
+ @cmd_status = 0
+ Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
+ @cmd_output << stdout.read
+ @cmd_output << stderr.read
+ @cmd_status = wait_thr.value.exitstatus
+ end
+
+ [@cmd_output, @cmd_status]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/ref.rb b/lib/gitlab/git/ref.rb
new file mode 100644
index 00000000000..37ef6836742
--- /dev/null
+++ b/lib/gitlab/git/ref.rb
@@ -0,0 +1,49 @@
+module Gitlab
+ module Git
+ class Ref
+ include Gitlab::Git::EncodingHelper
+
+ # Branch or tag name
+ # without "refs/tags|heads" prefix
+ attr_reader :name
+
+ # Target sha.
+ # Usually it is commit sha but in case
+ # when tag reference on other tag it can be tag sha
+ attr_reader :target
+
+ # Dereferenced target
+ # Commit object to which the Ref points to
+ attr_reader :dereferenced_target
+
+ # Extract branch name from full ref path
+ #
+ # Ex.
+ # Ref.extract_branch_name('refs/heads/master') #=> 'master'
+ def self.extract_branch_name(str)
+ str.gsub(/\Arefs\/heads\//, '')
+ end
+
+ def self.dereference_object(object)
+ object = object.target while object.is_a?(Rugged::Tag::Annotation)
+
+ object
+ end
+
+ def initialize(repository, name, target)
+ encode! name
+ @name = name.gsub(/\Arefs\/(tags|heads)\//, '')
+ @dereferenced_target = Gitlab::Git::Commit.find(repository, target)
+ @target = if target.respond_to?(:oid)
+ target.oid
+ elsif target.respond_to?(:name)
+ target.name
+ elsif target.is_a? String
+ target
+ else
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
new file mode 100644
index 00000000000..7068e68a855
--- /dev/null
+++ b/lib/gitlab/git/repository.rb
@@ -0,0 +1,1251 @@
+# Gitlab::Git::Repository is a wrapper around native Rugged::Repository object
+require 'forwardable'
+require 'tempfile'
+require 'forwardable'
+require "rubygems/package"
+
+module Gitlab
+ module Git
+ class Repository
+ extend Forwardable
+ include Gitlab::Git::Popen
+
+ SEARCH_CONTEXT_LINES = 3
+
+ class NoRepository < StandardError; end
+ class InvalidBlobName < StandardError; end
+ class InvalidRef < StandardError; end
+
+ # Full path to repo
+ attr_reader :path
+
+ # Directory name of repo
+ attr_reader :name
+
+ # Rugged repo object
+ attr_reader :rugged
+
+ # 'path' must be the path to a _bare_ git repository, e.g.
+ # /path/to/my-repo.git
+ def initialize(path)
+ @path = path
+ @name = path.split("/").last
+ @attributes = Gitlab::Git::Attributes.new(path)
+ end
+
+ # Default branch in the repository
+ def root_ref
+ @root_ref ||= discover_default_branch
+ end
+
+ # Alias to old method for compatibility
+ def raw
+ rugged
+ end
+
+ def rugged
+ @rugged ||= Rugged::Repository.new(path)
+ rescue Rugged::RepositoryError, Rugged::OSError
+ raise NoRepository.new('no repository for such path')
+ end
+
+ # Returns an Array of branch names
+ # sorted by name ASC
+ def branch_names
+ branches.map(&:name)
+ end
+
+ # Returns an Array of Branches
+ def branches
+ rugged.branches.map do |rugged_ref|
+ begin
+ Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target)
+ rescue Rugged::ReferenceError
+ # Omit invalid branch
+ end
+ end.compact.sort_by(&:name)
+ end
+
+ def reload_rugged
+ @rugged = nil
+ end
+
+ # Directly find a branch with a simple name (e.g. master)
+ #
+ # force_reload causes a new Rugged repository to be instantiated
+ #
+ # This is to work around a bug in libgit2 that causes in-memory refs to
+ # be stale/invalid when packed-refs is changed.
+ # See https://gitlab.com/gitlab-org/gitlab-ce/issues/15392#note_14538333
+ def find_branch(name, force_reload = false)
+ reload_rugged if force_reload
+
+ rugged_ref = rugged.branches[name]
+ Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target) if rugged_ref
+ end
+
+ def local_branches
+ rugged.branches.each(:local).map do |branch|
+ Gitlab::Git::Branch.new(self, branch.name, branch.target)
+ end
+ end
+
+ # Returns the number of valid branches
+ def branch_count
+ rugged.branches.count do |ref|
+ begin
+ ref.name && ref.target # ensures the branch is valid
+
+ true
+ rescue Rugged::ReferenceError
+ false
+ end
+ end
+ end
+
+ # Returns an Array of tag names
+ def tag_names
+ rugged.tags.map { |t| t.name }
+ end
+
+ # Returns an Array of Tags
+ def tags
+ rugged.references.each("refs/tags/*").map do |ref|
+ message = nil
+
+ if ref.target.is_a?(Rugged::Tag::Annotation)
+ tag_message = ref.target.message
+
+ if tag_message.respond_to?(:chomp)
+ message = tag_message.chomp
+ end
+ end
+
+ Gitlab::Git::Tag.new(self, ref.name, ref.target, message)
+ end.sort_by(&:name)
+ end
+
+ # Returns true if the given tag exists
+ #
+ # name - The name of the tag as a String.
+ def tag_exists?(name)
+ !!rugged.tags[name]
+ end
+
+ # Returns true if the given branch exists
+ #
+ # name - The name of the branch as a String.
+ def branch_exists?(name)
+ rugged.branches.exists?(name)
+
+ # If the branch name is invalid (e.g. ".foo") Rugged will raise an error.
+ # Whatever code calls this method shouldn't have to deal with that so
+ # instead we just return `false` (which is true since a branch doesn't
+ # exist when it has an invalid name).
+ rescue Rugged::ReferenceError
+ false
+ end
+
+ # Returns an Array of branch and tag names
+ def ref_names
+ branch_names + tag_names
+ end
+
+ # Deprecated. Will be removed in 5.2
+ def heads
+ rugged.references.each("refs/heads/*").map do |head|
+ Gitlab::Git::Ref.new(self, head.name, head.target)
+ end.sort_by(&:name)
+ end
+
+ def has_commits?
+ !empty?
+ end
+
+ def empty?
+ rugged.empty?
+ end
+
+ def bare?
+ rugged.bare?
+ end
+
+ def repo_exists?
+ !!rugged
+ end
+
+ # Discovers the default branch based on the repository's available branches
+ #
+ # - If no branches are present, returns nil
+ # - If one branch is present, returns its name
+ # - If two or more branches are present, returns current HEAD or master or first branch
+ def discover_default_branch
+ names = branch_names
+
+ return if names.empty?
+
+ return names[0] if names.length == 1
+
+ if rugged_head
+ extracted_name = Ref.extract_branch_name(rugged_head.name)
+
+ return extracted_name if names.include?(extracted_name)
+ end
+
+ if names.include?('master')
+ 'master'
+ else
+ names[0]
+ end
+ end
+
+ def rugged_head
+ rugged.head
+ rescue Rugged::ReferenceError
+ nil
+ end
+
+ def archive_metadata(ref, storage_path, format = "tar.gz")
+ ref ||= root_ref
+ commit = Gitlab::Git::Commit.find(self, ref)
+ return {} if commit.nil?
+
+ project_name = self.name.chomp('.git')
+ prefix = "#{project_name}-#{ref}-#{commit.id}"
+
+ {
+ 'RepoPath' => path,
+ 'ArchivePrefix' => prefix,
+ 'ArchivePath' => archive_file_path(prefix, storage_path, format),
+ 'CommitId' => commit.id,
+ }
+ end
+
+ def archive_file_path(name, storage_path, format = "tar.gz")
+ # Build file path
+ return nil unless name
+
+ extension =
+ case format
+ when "tar.bz2", "tbz", "tbz2", "tb2", "bz2"
+ "tar.bz2"
+ when "tar"
+ "tar"
+ when "zip"
+ "zip"
+ else
+ # everything else should fall back to tar.gz
+ "tar.gz"
+ end
+
+ file_name = "#{name}.#{extension}"
+ File.join(storage_path, self.name, file_name)
+ end
+
+ # Return repo size in megabytes
+ def size
+ size = popen(%w(du -sk), path).first.strip.to_i
+ (size.to_f / 1024).round(2)
+ end
+
+ # Returns an array of BlobSnippets for files at the specified +ref+ that
+ # contain the +query+ string.
+ def search_files(query, ref = nil)
+ greps = []
+ ref ||= root_ref
+
+ populated_index(ref).each do |entry|
+ # Discard submodules
+ next if submodule?(entry)
+
+ blob = Gitlab::Git::Blob.raw(self, entry[:oid])
+
+ # Skip binary files
+ next if blob.data.encoding == Encoding::ASCII_8BIT
+
+ blob.load_all_data!(self)
+ greps += build_greps(blob.data, query, ref, entry[:path])
+ end
+
+ greps
+ end
+
+ # Use the Rugged Walker API to build an array of commits.
+ #
+ # Usage.
+ # repo.log(
+ # ref: 'master',
+ # path: 'app/models',
+ # limit: 10,
+ # offset: 5,
+ # after: Time.new(2016, 4, 21, 14, 32, 10)
+ # )
+ #
+ def log(options)
+ default_options = {
+ limit: 10,
+ offset: 0,
+ path: nil,
+ follow: false,
+ skip_merges: false,
+ disable_walk: false,
+ after: nil,
+ before: nil
+ }
+
+ options = default_options.merge(options)
+ options[:limit] ||= 0
+ options[:offset] ||= 0
+ actual_ref = options[:ref] || root_ref
+ begin
+ sha = sha_from_ref(actual_ref)
+ rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError
+ # Return an empty array if the ref wasn't found
+ return []
+ end
+
+ if log_using_shell?(options)
+ log_by_shell(sha, options)
+ else
+ log_by_walk(sha, options)
+ end
+ end
+
+ def log_using_shell?(options)
+ options[:path].present? ||
+ options[:disable_walk] ||
+ options[:skip_merges] ||
+ options[:after] ||
+ options[:before]
+ end
+
+ def log_by_walk(sha, options)
+ walk_options = {
+ show: sha,
+ sort: Rugged::SORT_DATE,
+ limit: options[:limit],
+ offset: options[:offset]
+ }
+ Rugged::Walker.walk(rugged, walk_options).to_a
+ end
+
+ def log_by_shell(sha, options)
+ cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} log)
+ cmd += %W(-n #{options[:limit].to_i})
+ cmd += %w(--format=%H)
+ cmd += %W(--skip=#{options[:offset].to_i})
+ cmd += %w(--follow) if options[:follow]
+ cmd += %w(--no-merges) if options[:skip_merges]
+ cmd += %W(--after=#{options[:after].iso8601}) if options[:after]
+ cmd += %W(--before=#{options[:before].iso8601}) if options[:before]
+ cmd += [sha]
+ cmd += %W(-- #{options[:path]}) if options[:path].present?
+
+ raw_output = IO.popen(cmd) {|io| io.read }
+
+ log = raw_output.lines.map do |c|
+ Rugged::Commit.new(rugged, c.strip)
+ end
+
+ log.is_a?(Array) ? log : []
+ end
+
+ def sha_from_ref(ref)
+ rev_parse_target(ref).oid
+ end
+
+ # Return the object that +revspec+ points to. If +revspec+ is an
+ # annotated tag, then return the tag's target instead.
+ def rev_parse_target(revspec)
+ obj = rugged.rev_parse(revspec)
+ Ref.dereference_object(obj)
+ end
+
+ # Return a collection of Rugged::Commits between the two revspec arguments.
+ # See http://git-scm.com/docs/git-rev-parse.html#_specifying_revisions for
+ # a detailed list of valid arguments.
+ def commits_between(from, to)
+ walker = Rugged::Walker.new(rugged)
+ walker.sorting(Rugged::SORT_DATE | Rugged::SORT_REVERSE)
+
+ sha_from = sha_from_ref(from)
+ sha_to = sha_from_ref(to)
+
+ walker.push(sha_to)
+ walker.hide(sha_from)
+
+ commits = walker.to_a
+ walker.reset
+
+ commits
+ end
+
+ # Counts the amount of commits between `from` and `to`.
+ def count_commits_between(from, to)
+ commits_between(from, to).size
+ end
+
+ # Returns the SHA of the most recent common ancestor of +from+ and +to+
+ def merge_base_commit(from, to)
+ rugged.merge_base(from, to)
+ end
+
+ # Return an array of Diff objects that represent the diff
+ # between +from+ and +to+. See Diff::filter_diff_options for the allowed
+ # diff options. The +options+ hash can also include :break_rewrites to
+ # split larger rewrites into delete/add pairs.
+ def diff(from, to, options = {}, *paths)
+ Gitlab::Git::DiffCollection.new(diff_patches(from, to, options, *paths), options)
+ end
+
+ # Returns commits collection
+ #
+ # Ex.
+ # repo.find_commits(
+ # ref: 'master',
+ # max_count: 10,
+ # skip: 5,
+ # order: :date
+ # )
+ #
+ # +options+ is a Hash of optional arguments to git
+ # :ref is the ref from which to begin (SHA1 or name)
+ # :contains is the commit contained by the refs from which to begin (SHA1 or name)
+ # :max_count is the maximum number of commits to fetch
+ # :skip is the number of commits to skip
+ # :order is the commits order and allowed value is :date(default) or :topo
+ #
+ def find_commits(options = {})
+ actual_options = options.dup
+
+ allowed_options = [:ref, :max_count, :skip, :contains, :order]
+
+ actual_options.keep_if do |key|
+ allowed_options.include?(key)
+ end
+
+ default_options = { skip: 0 }
+ actual_options = default_options.merge(actual_options)
+
+ walker = Rugged::Walker.new(rugged)
+
+ if actual_options[:ref]
+ walker.push(rugged.rev_parse_oid(actual_options[:ref]))
+ elsif actual_options[:contains]
+ branches_contains(actual_options[:contains]).each do |branch|
+ walker.push(branch.target_id)
+ end
+ else
+ rugged.references.each("refs/heads/*") do |ref|
+ walker.push(ref.target_id)
+ end
+ end
+
+ if actual_options[:order] == :topo
+ walker.sorting(Rugged::SORT_TOPO)
+ else
+ walker.sorting(Rugged::SORT_DATE)
+ end
+
+ commits = []
+ offset = actual_options[:skip]
+ limit = actual_options[:max_count]
+ walker.each(offset: offset, limit: limit) do |commit|
+ gitlab_commit = Gitlab::Git::Commit.decorate(commit)
+ commits.push(gitlab_commit)
+ end
+
+ walker.reset
+
+ commits
+ rescue Rugged::OdbError
+ []
+ end
+
+ # Returns branch names collection that contains the special commit(SHA1
+ # or name)
+ #
+ # Ex.
+ # repo.branch_names_contains('master')
+ #
+ def branch_names_contains(commit)
+ branches_contains(commit).map { |c| c.name }
+ end
+
+ # Returns branch collection that contains the special commit(SHA1 or name)
+ #
+ # Ex.
+ # repo.branch_names_contains('master')
+ #
+ def branches_contains(commit)
+ commit_obj = rugged.rev_parse(commit)
+ parent = commit_obj.parents.first unless commit_obj.parents.empty?
+
+ walker = Rugged::Walker.new(rugged)
+
+ rugged.branches.select do |branch|
+ walker.push(branch.target_id)
+ walker.hide(parent) if parent
+ result = walker.any? { |c| c.oid == commit_obj.oid }
+ walker.reset
+
+ result
+ end
+ end
+
+ # Get refs hash which key is SHA1
+ # and value is a Rugged::Reference
+ def refs_hash
+ # Initialize only when first call
+ if @refs_hash.nil?
+ @refs_hash = Hash.new { |h, k| h[k] = [] }
+
+ rugged.references.each do |r|
+ # Symbolic/remote references may not have an OID; skip over them
+ target_oid = r.target.try(:oid)
+ if target_oid
+ sha = rev_parse_target(target_oid).oid
+ @refs_hash[sha] << r
+ end
+ end
+ end
+ @refs_hash
+ end
+
+ # Lookup for rugged object by oid or ref name
+ def lookup(oid_or_ref_name)
+ rugged.rev_parse(oid_or_ref_name)
+ end
+
+ # Return hash with submodules info for this repository
+ #
+ # Ex.
+ # {
+ # "rack" => {
+ # "id" => "c67be4624545b4263184c4a0e8f887efd0a66320",
+ # "path" => "rack",
+ # "url" => "git://github.com/chneukirchen/rack.git"
+ # },
+ # "encoding" => {
+ # "id" => ....
+ # }
+ # }
+ #
+ def submodules(ref)
+ commit = rev_parse_target(ref)
+ return {} unless commit
+
+ begin
+ content = blob_content(commit, ".gitmodules")
+ rescue InvalidBlobName
+ return {}
+ end
+
+ parse_gitmodules(commit, content)
+ end
+
+ # Return total commits count accessible from passed ref
+ def commit_count(ref)
+ walker = Rugged::Walker.new(rugged)
+ walker.sorting(Rugged::SORT_TOPO | Rugged::SORT_REVERSE)
+ oid = rugged.rev_parse_oid(ref)
+ walker.push(oid)
+ walker.count
+ end
+
+ # Sets HEAD to the commit specified by +ref+; +ref+ can be a branch or
+ # tag name or a commit SHA. Valid +reset_type+ values are:
+ #
+ # [:soft]
+ # the head will be moved to the commit.
+ # [:mixed]
+ # will trigger a +:soft+ reset, plus the index will be replaced
+ # with the content of the commit tree.
+ # [:hard]
+ # will trigger a +:mixed+ reset and the working directory will be
+ # replaced with the content of the index. (Untracked and ignored files
+ # will be left alone)
+ def reset(ref, reset_type)
+ rugged.reset(ref, reset_type)
+ end
+
+ # Mimic the `git clean` command and recursively delete untracked files.
+ # Valid keys that can be passed in the +options+ hash are:
+ #
+ # :d - Remove untracked directories
+ # :f - Remove untracked directories that are managed by a different
+ # repository
+ # :x - Remove ignored files
+ #
+ # The value in +options+ must evaluate to true for an option to take
+ # effect.
+ #
+ # Examples:
+ #
+ # repo.clean(d: true, f: true) # Enable the -d and -f options
+ #
+ # repo.clean(d: false, x: true) # -x is enabled, -d is not
+ def clean(options = {})
+ strategies = [:remove_untracked]
+ strategies.push(:force) if options[:f]
+ strategies.push(:remove_ignored) if options[:x]
+
+ # TODO: implement this method
+ end
+
+ # Check out the specified ref. Valid options are:
+ #
+ # :b - Create a new branch at +start_point+ and set HEAD to the new
+ # branch.
+ #
+ # * These options are passed to the Rugged::Repository#checkout method:
+ #
+ # :progress ::
+ # A callback that will be executed for checkout progress notifications.
+ # Up to 3 parameters are passed on each execution:
+ #
+ # - The path to the last updated file (or +nil+ on the very first
+ # invocation).
+ # - The number of completed checkout steps.
+ # - The number of total checkout steps to be performed.
+ #
+ # :notify ::
+ # A callback that will be executed for each checkout notification
+ # types specified with +:notify_flags+. Up to 5 parameters are passed
+ # on each execution:
+ #
+ # - An array containing the +:notify_flags+ that caused the callback
+ # execution.
+ # - The path of the current file.
+ # - A hash describing the baseline blob (or +nil+ if it does not
+ # exist).
+ # - A hash describing the target blob (or +nil+ if it does not exist).
+ # - A hash describing the workdir blob (or +nil+ if it does not
+ # exist).
+ #
+ # :strategy ::
+ # A single symbol or an array of symbols representing the strategies
+ # to use when performing the checkout. Possible values are:
+ #
+ # :none ::
+ # Perform a dry run (default).
+ #
+ # :safe ::
+ # Allow safe updates that cannot overwrite uncommitted data.
+ #
+ # :safe_create ::
+ # Allow safe updates plus creation of missing files.
+ #
+ # :force ::
+ # Allow all updates to force working directory to look like index.
+ #
+ # :allow_conflicts ::
+ # Allow checkout to make safe updates even if conflicts are found.
+ #
+ # :remove_untracked ::
+ # Remove untracked files not in index (that are not ignored).
+ #
+ # :remove_ignored ::
+ # Remove ignored files not in index.
+ #
+ # :update_only ::
+ # Only update existing files, don't create new ones.
+ #
+ # :dont_update_index ::
+ # Normally checkout updates index entries as it goes; this stops
+ # that.
+ #
+ # :no_refresh ::
+ # Don't refresh index/config/etc before doing checkout.
+ #
+ # :disable_pathspec_match ::
+ # Treat pathspec as simple list of exact match file paths.
+ #
+ # :skip_locked_directories ::
+ # Ignore directories in use, they will be left empty.
+ #
+ # :skip_unmerged ::
+ # Allow checkout to skip unmerged files (NOT IMPLEMENTED).
+ #
+ # :use_ours ::
+ # For unmerged files, checkout stage 2 from index (NOT IMPLEMENTED).
+ #
+ # :use_theirs ::
+ # For unmerged files, checkout stage 3 from index (NOT IMPLEMENTED).
+ #
+ # :update_submodules ::
+ # Recursively checkout submodules with same options (NOT
+ # IMPLEMENTED).
+ #
+ # :update_submodules_if_changed ::
+ # Recursively checkout submodules if HEAD moved in super repo (NOT
+ # IMPLEMENTED).
+ #
+ # :disable_filters ::
+ # If +true+, filters like CRLF line conversion will be disabled.
+ #
+ # :dir_mode ::
+ # Mode for newly created directories. Default: +0755+.
+ #
+ # :file_mode ::
+ # Mode for newly created files. Default: +0755+ or +0644+.
+ #
+ # :file_open_flags ::
+ # Mode for opening files. Default:
+ # <code>IO::CREAT | IO::TRUNC | IO::WRONLY</code>.
+ #
+ # :notify_flags ::
+ # A single symbol or an array of symbols representing the cases in
+ # which the +:notify+ callback should be invoked. Possible values are:
+ #
+ # :none ::
+ # Do not invoke the +:notify+ callback (default).
+ #
+ # :conflict ::
+ # Invoke the callback for conflicting paths.
+ #
+ # :dirty ::
+ # Invoke the callback for "dirty" files, i.e. those that do not need
+ # an update but no longer match the baseline.
+ #
+ # :updated ::
+ # Invoke the callback for any file that was changed.
+ #
+ # :untracked ::
+ # Invoke the callback for untracked files.
+ #
+ # :ignored ::
+ # Invoke the callback for ignored files.
+ #
+ # :all ::
+ # Invoke the callback for all these cases.
+ #
+ # :paths ::
+ # A glob string or an array of glob strings specifying which paths
+ # should be taken into account for the checkout operation. +nil+ will
+ # match all files. Default: +nil+.
+ #
+ # :baseline ::
+ # A Rugged::Tree that represents the current, expected contents of the
+ # workdir. Default: +HEAD+.
+ #
+ # :target_directory ::
+ # A path to an alternative workdir directory in which the checkout
+ # should be performed.
+ def checkout(ref, options = {}, start_point = "HEAD")
+ if options[:b]
+ rugged.branches.create(ref, start_point)
+ options.delete(:b)
+ end
+ default_options = { strategy: [:recreate_missing, :safe] }
+ rugged.checkout(ref, default_options.merge(options))
+ end
+
+ # Delete the specified branch from the repository
+ def delete_branch(branch_name)
+ rugged.branches.delete(branch_name)
+ end
+
+ # Create a new branch named **ref+ based on **stat_point+, HEAD by default
+ #
+ # Examples:
+ # create_branch("feature")
+ # create_branch("other-feature", "master")
+ def create_branch(ref, start_point = "HEAD")
+ rugged_ref = rugged.branches.create(ref, start_point)
+ Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target)
+ rescue Rugged::ReferenceError => e
+ raise InvalidRef.new("Branch #{ref} already exists") if e.to_s =~ /'refs\/heads\/#{ref}'/
+ raise InvalidRef.new("Invalid reference #{start_point}")
+ end
+
+ # Return an array of this repository's remote names
+ def remote_names
+ rugged.remotes.each_name.to_a
+ end
+
+ # Delete the specified remote from this repository.
+ def remote_delete(remote_name)
+ rugged.remotes.delete(remote_name)
+ end
+
+ # Add a new remote to this repository. Returns a Rugged::Remote object
+ def remote_add(remote_name, url)
+ rugged.remotes.create(remote_name, url)
+ end
+
+ # Update the specified remote using the values in the +options+ hash
+ #
+ # Example
+ # repo.update_remote("origin", url: "path/to/repo")
+ def remote_update(remote_name, options = {})
+ # TODO: Implement other remote options
+ rugged.remotes.set_url(remote_name, options[:url]) if options[:url]
+ end
+
+ # Fetch the specified remote
+ def fetch(remote_name)
+ rugged.remotes[remote_name].fetch
+ end
+
+ # Push +*refspecs+ to the remote identified by +remote_name+.
+ def push(remote_name, *refspecs)
+ rugged.remotes[remote_name].push(refspecs)
+ end
+
+ # Merge the +source_name+ branch into the +target_name+ branch. This is
+ # equivalent to `git merge --no_ff +source_name+`, since a merge commit
+ # is always created.
+ def merge(source_name, target_name, options = {})
+ our_commit = rugged.branches[target_name].target
+ their_commit = rugged.branches[source_name].target
+
+ raise "Invalid merge target" if our_commit.nil?
+ raise "Invalid merge source" if their_commit.nil?
+
+ merge_index = rugged.merge_commits(our_commit, their_commit)
+ return false if merge_index.conflicts?
+
+ actual_options = options.merge(
+ parents: [our_commit, their_commit],
+ tree: merge_index.write_tree(rugged),
+ update_ref: "refs/heads/#{target_name}"
+ )
+ Rugged::Commit.create(rugged, actual_options)
+ end
+
+ def commits_since(from_date)
+ walker = Rugged::Walker.new(rugged)
+ walker.sorting(Rugged::SORT_DATE | Rugged::SORT_REVERSE)
+
+ rugged.references.each("refs/heads/*") do |ref|
+ walker.push(ref.target_id)
+ end
+
+ commits = []
+ walker.each do |commit|
+ break if commit.author[:time].to_date < from_date
+ commits.push(commit)
+ end
+
+ commits
+ end
+
+ AUTOCRLF_VALUES = {
+ "true" => true,
+ "false" => false,
+ "input" => :input
+ }.freeze
+
+ def autocrlf
+ AUTOCRLF_VALUES[rugged.config['core.autocrlf']]
+ end
+
+ def autocrlf=(value)
+ rugged.config['core.autocrlf'] = AUTOCRLF_VALUES.invert[value]
+ end
+
+ # Create a new directory with a .gitkeep file. Creates
+ # all required nested directories (i.e. mkdir -p behavior)
+ #
+ # options should contain next structure:
+ # author: {
+ # email: 'user@example.com',
+ # name: 'Test User',
+ # time: Time.now
+ # },
+ # committer: {
+ # email: 'user@example.com',
+ # name: 'Test User',
+ # time: Time.now
+ # },
+ # commit: {
+ # message: 'Wow such commit',
+ # branch: 'master',
+ # update_ref: false
+ # }
+ def mkdir(path, options = {})
+ # Check if this directory exists; if it does, then don't bother
+ # adding .gitkeep file.
+ ref = options[:commit][:branch]
+ path = Gitlab::Git::PathHelper.normalize_path(path).to_s
+ rugged_ref = rugged.ref(ref)
+
+ raise InvalidRef.new("Invalid ref") if rugged_ref.nil?
+
+ target_commit = rugged_ref.target
+
+ raise InvalidRef.new("Invalid target commit") if target_commit.nil?
+
+ entry = tree_entry(target_commit, path)
+
+ if entry
+ if entry[:type] == :blob
+ raise InvalidBlobName.new("Directory already exists as a file")
+ else
+ raise InvalidBlobName.new("Directory already exists")
+ end
+ end
+
+ options[:file] = {
+ content: '',
+ path: "#{path}/.gitkeep",
+ update: true
+ }
+
+ Gitlab::Git::Blob.commit(self, options)
+ end
+
+ # Returns result like "git ls-files" , recursive and full file path
+ #
+ # Ex.
+ # repo.ls_files('master')
+ #
+ def ls_files(ref)
+ actual_ref = ref || root_ref
+
+ begin
+ sha_from_ref(actual_ref)
+ rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError
+ # Return an empty array if the ref wasn't found
+ return []
+ end
+
+ cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} ls-tree)
+ cmd += %w(-r)
+ cmd += %w(--full-tree)
+ cmd += %w(--full-name)
+ cmd += %W(-- #{actual_ref})
+
+ raw_output = IO.popen(cmd, &:read).split("\n").map do |f|
+ stuff, path = f.split("\t")
+ _mode, type, _sha = stuff.split(" ")
+ path if type == "blob"
+ # Contain only blob type
+ end
+
+ raw_output.compact
+ end
+
+ def copy_gitattributes(ref)
+ begin
+ commit = lookup(ref)
+ rescue Rugged::ReferenceError
+ raise InvalidRef.new("Ref #{ref} is invalid")
+ end
+
+ # Create the paths
+ info_dir_path = File.join(path, 'info')
+ info_attributes_path = File.join(info_dir_path, 'attributes')
+
+ begin
+ # Retrieve the contents of the blob
+ gitattributes_content = blob_content(commit, '.gitattributes')
+ rescue InvalidBlobName
+ # No .gitattributes found. Should now remove any info/attributes and return
+ File.delete(info_attributes_path) if File.exist?(info_attributes_path)
+ return
+ end
+
+ # Create the info directory if needed
+ Dir.mkdir(info_dir_path) unless File.directory?(info_dir_path)
+
+ # Write the contents of the .gitattributes file to info/attributes
+ # Use binary mode to prevent Rails from converting ASCII-8BIT to UTF-8
+ File.open(info_attributes_path, "wb") do |file|
+ file.write(gitattributes_content)
+ end
+ end
+
+ # Checks if the blob should be diffable according to its attributes
+ def diffable?(blob)
+ attributes(blob.path).fetch('diff') { blob.text? }
+ end
+
+ # Returns the Git attributes for the given file path.
+ #
+ # See `Gitlab::Git::Attributes` for more information.
+ def attributes(path)
+ @attributes.attributes(path)
+ end
+
+ private
+
+ # Get the content of a blob for a given commit. If the blob is a commit
+ # (for submodules) then return the blob's OID.
+ def blob_content(commit, blob_name)
+ blob_entry = tree_entry(commit, blob_name)
+
+ unless blob_entry
+ raise InvalidBlobName.new("Invalid blob name: #{blob_name}")
+ end
+
+ case blob_entry[:type]
+ when :commit
+ blob_entry[:oid]
+ when :tree
+ raise InvalidBlobName.new("#{blob_name} is a tree, not a blob")
+ when :blob
+ rugged.lookup(blob_entry[:oid]).content
+ end
+ end
+
+ # Parses the contents of a .gitmodules file and returns a hash of
+ # submodule information.
+ def parse_gitmodules(commit, content)
+ results = {}
+
+ current = ""
+ content.split("\n").each do |txt|
+ if txt =~ /^\s*\[/
+ current = txt.match(/(?<=").*(?=")/)[0]
+ results[current] = {}
+ else
+ next unless results[current]
+ match_data = txt.match(/(\w+)\s*=\s*(.*)/)
+ next unless match_data
+ target = match_data[2].chomp
+ results[current][match_data[1]] = target
+
+ if match_data[1] == "path"
+ begin
+ results[current]["id"] = blob_content(commit, target)
+ rescue InvalidBlobName
+ results.delete(current)
+ end
+ end
+ end
+ end
+
+ results
+ end
+
+ # Returns true if +commit+ introduced changes to +path+, using commit
+ # trees to make that determination. Uses the history simplification
+ # rules that `git log` uses by default, where a commit is omitted if it
+ # is TREESAME to any parent.
+ #
+ # If the +follow+ option is true and the file specified by +path+ was
+ # renamed, then the path value is set to the old path.
+ def commit_touches_path?(commit, path, follow, walker)
+ entry = tree_entry(commit, path)
+
+ if commit.parents.empty?
+ # This is the root commit, return true if it has +path+ in its tree
+ return !entry.nil?
+ end
+
+ num_treesame = 0
+ commit.parents.each do |parent|
+ parent_entry = tree_entry(parent, path)
+
+ # Only follow the first TREESAME parent for merge commits
+ if num_treesame > 0
+ walker.hide(parent)
+ next
+ end
+
+ if entry.nil? && parent_entry.nil?
+ num_treesame += 1
+ elsif entry && parent_entry && entry[:oid] == parent_entry[:oid]
+ num_treesame += 1
+ end
+ end
+
+ case num_treesame
+ when 0
+ detect_rename(commit, commit.parents.first, path) if follow
+ true
+ else false
+ end
+ end
+
+ # Find the entry for +path+ in the tree for +commit+
+ def tree_entry(commit, path)
+ pathname = Pathname.new(path)
+ first = true
+ tmp_entry = nil
+
+ pathname.each_filename do |dir|
+ if first
+ tmp_entry = commit.tree[dir]
+ first = false
+ elsif tmp_entry.nil?
+ return nil
+ else
+ tmp_entry = rugged.lookup(tmp_entry[:oid])
+ return nil unless tmp_entry.type == :tree
+ tmp_entry = tmp_entry[dir]
+ end
+ end
+
+ tmp_entry
+ end
+
+ # Compare +commit+ and +parent+ for +path+. If +path+ is a file and was
+ # renamed in +commit+, then set +path+ to the old filename.
+ def detect_rename(commit, parent, path)
+ diff = parent.diff(commit, paths: [path], disable_pathspec_match: true)
+
+ # If +path+ is a filename, not a directory, then we should only have
+ # one delta. We don't need to follow renames for directories.
+ return nil if diff.each_delta.count > 1
+
+ delta = diff.each_delta.first
+ if delta.added?
+ full_diff = parent.diff(commit)
+ full_diff.find_similar!
+
+ full_diff.each_delta do |full_delta|
+ if full_delta.renamed? && path == full_delta.new_file[:path]
+ # Look for the old path in ancestors
+ path.replace(full_delta.old_file[:path])
+ end
+ end
+ end
+ end
+
+ def archive_to_file(treeish = 'master', filename = 'archive.tar.gz', format = nil, compress_cmd = %w(gzip -n))
+ git_archive_cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} archive)
+
+ # Put files into a directory before archiving
+ prefix = "#{archive_name(treeish)}/"
+ git_archive_cmd << "--prefix=#{prefix}"
+
+ # Format defaults to tar
+ git_archive_cmd << "--format=#{format}" if format
+
+ git_archive_cmd += %W(-- #{treeish})
+
+ open(filename, 'w') do |file|
+ # Create a pipe to act as the '|' in 'git archive ... | gzip'
+ pipe_rd, pipe_wr = IO.pipe
+
+ # Get the compression process ready to accept data from the read end
+ # of the pipe
+ compress_pid = spawn(*nice(compress_cmd), in: pipe_rd, out: file)
+ # The read end belongs to the compression process now; we should
+ # close our file descriptor for it.
+ pipe_rd.close
+
+ # Start 'git archive' and tell it to write into the write end of the
+ # pipe.
+ git_archive_pid = spawn(*nice(git_archive_cmd), out: pipe_wr)
+ # The write end belongs to 'git archive' now; close it.
+ pipe_wr.close
+
+ # When 'git archive' and the compression process are finished, we are
+ # done.
+ Process.waitpid(git_archive_pid)
+ raise "#{git_archive_cmd.join(' ')} failed" unless $?.success?
+ Process.waitpid(compress_pid)
+ raise "#{compress_cmd.join(' ')} failed" unless $?.success?
+ end
+ end
+
+ def nice(cmd)
+ nice_cmd = %w(nice -n 20)
+ unless unsupported_platform?
+ nice_cmd += %w(ionice -c 2 -n 7)
+ end
+ nice_cmd + cmd
+ end
+
+ def unsupported_platform?
+ %w[darwin freebsd solaris].map { |platform| RUBY_PLATFORM.include?(platform) }.any?
+ end
+
+ # Returns true if the index entry has the special file mode that denotes
+ # a submodule.
+ def submodule?(index_entry)
+ index_entry[:mode] == 57344
+ end
+
+ # Return a Rugged::Index that has read from the tree at +ref_name+
+ def populated_index(ref_name)
+ commit = rev_parse_target(ref_name)
+ index = rugged.index
+ index.read_tree(commit.tree)
+ index
+ end
+
+ # Return an array of BlobSnippets for lines in +file_contents+ that match
+ # +query+
+ def build_greps(file_contents, query, ref, filename)
+ # The file_contents string is potentially huge so we make sure to loop
+ # through it one line at a time. This gives Ruby the chance to GC lines
+ # we are not interested in.
+ #
+ # We need to do a little extra work because we are not looking for just
+ # the lines that matches the query, but also for the context
+ # (surrounding lines). We will use Enumerable#each_cons to efficiently
+ # loop through the lines while keeping surrounding lines on hand.
+ #
+ # First, we turn "foo\nbar\nbaz" into
+ # [
+ # [nil, -3], [nil, -2], [nil, -1],
+ # ['foo', 0], ['bar', 1], ['baz', 3],
+ # [nil, 4], [nil, 5], [nil, 6]
+ # ]
+ lines_with_index = Enumerator.new do |yielder|
+ # Yield fake 'before' lines for the first line of file_contents
+ (-SEARCH_CONTEXT_LINES..-1).each do |i|
+ yielder.yield [nil, i]
+ end
+
+ # Yield the actual file contents
+ count = 0
+ file_contents.each_line do |line|
+ line.chomp!
+ yielder.yield [line, count]
+ count += 1
+ end
+
+ # Yield fake 'after' lines for the last line of file_contents
+ (count + 1..count + SEARCH_CONTEXT_LINES).each do |i|
+ yielder.yield [nil, i]
+ end
+ end
+
+ greps = []
+
+ # Loop through consecutive blocks of lines with indexes
+ lines_with_index.each_cons(2 * SEARCH_CONTEXT_LINES + 1) do |line_block|
+ # Get the 'middle' line and index from the block
+ line, _ = line_block[SEARCH_CONTEXT_LINES]
+
+ next unless line && line.match(/#{Regexp.escape(query)}/i)
+
+ # Yay, 'line' contains a match!
+ # Get an array with just the context lines (no indexes)
+ match_with_context = line_block.map(&:first)
+ # Remove 'nil' lines in case we are close to the first or last line
+ match_with_context.compact!
+
+ # Get the line number (1-indexed) of the first context line
+ first_context_line_number = line_block[0][1] + 1
+
+ greps << Gitlab::Git::BlobSnippet.new(
+ ref,
+ match_with_context,
+ first_context_line_number,
+ filename
+ )
+ end
+
+ greps
+ end
+
+ # Return the Rugged patches for the diff between +from+ and +to+.
+ def diff_patches(from, to, options = {}, *paths)
+ options ||= {}
+ break_rewrites = options[:break_rewrites]
+ actual_options = Gitlab::Git::Diff.filter_diff_options(options.merge(paths: paths))
+
+ diff = rugged.diff(from, to, actual_options)
+ diff.find_similar!(break_rewrites: break_rewrites)
+ diff.each_patch
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb
new file mode 100644
index 00000000000..79dd0cf7df2
--- /dev/null
+++ b/lib/gitlab/git/rev_list.rb
@@ -0,0 +1,42 @@
+module Gitlab
+ module Git
+ class RevList
+ attr_reader :project, :env
+
+ ALLOWED_VARIABLES = %w[GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES].freeze
+
+ def initialize(oldrev, newrev, project:, env: nil)
+ @project = project
+ @env = env.presence || {}
+ @args = [Gitlab.config.git.bin_path,
+ "--git-dir=#{project.repository.path_to_repo}",
+ "rev-list",
+ "--max-count=1",
+ oldrev,
+ "^#{newrev}"]
+ end
+
+ def execute
+ Gitlab::Popen.popen(@args, nil, parse_environment_variables)
+ end
+
+ def valid?
+ environment_variables.all? do |(name, value)|
+ value.to_s.start_with?(project.repository.path_to_repo)
+ end
+ end
+
+ private
+
+ def parse_environment_variables
+ return {} unless valid?
+
+ environment_variables
+ end
+
+ def environment_variables
+ @environment_variables ||= env.slice(*ALLOWED_VARIABLES).compact
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/tag.rb b/lib/gitlab/git/tag.rb
new file mode 100644
index 00000000000..b5342c3d310
--- /dev/null
+++ b/lib/gitlab/git/tag.rb
@@ -0,0 +1,17 @@
+module Gitlab
+ module Git
+ class Tag < Ref
+ attr_reader :object_sha
+
+ def initialize(repository, name, target, message = nil)
+ super(repository, name, target)
+
+ @message = message
+ end
+
+ def message
+ encode! @message
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb
new file mode 100644
index 00000000000..f7450e8b58f
--- /dev/null
+++ b/lib/gitlab/git/tree.rb
@@ -0,0 +1,104 @@
+module Gitlab
+ module Git
+ class Tree
+ include Gitlab::Git::EncodingHelper
+
+ attr_accessor :id, :root_id, :name, :path, :type,
+ :mode, :commit_id, :submodule_url
+
+ class << self
+ # Get list of tree objects
+ # for repository based on commit sha and path
+ # Uses rugged for raw objects
+ def where(repository, sha, path = nil)
+ path = nil if path == '' || path == '/'
+
+ commit = repository.lookup(sha)
+ root_tree = commit.tree
+
+ tree = if path
+ id = find_id_by_path(repository, root_tree.oid, path)
+ if id
+ repository.lookup(id)
+ else
+ []
+ end
+ else
+ root_tree
+ end
+
+ tree.map do |entry|
+ new(
+ id: entry[:oid],
+ root_id: root_tree.oid,
+ name: entry[:name],
+ type: entry[:type],
+ mode: entry[:filemode],
+ path: path ? File.join(path, entry[:name]) : entry[:name],
+ commit_id: sha,
+ )
+ end
+ end
+
+ # Recursive search of tree id for path
+ #
+ # Ex.
+ # blog/ # oid: 1a
+ # app/ # oid: 2a
+ # models/ # oid: 3a
+ # views/ # oid: 4a
+ #
+ #
+ # Tree.find_id_by_path(repo, '1a', 'app/models') # => '3a'
+ #
+ def find_id_by_path(repository, root_id, path)
+ root_tree = repository.lookup(root_id)
+ path_arr = path.split('/')
+
+ entry = root_tree.find do |entry|
+ entry[:name] == path_arr[0] && entry[:type] == :tree
+ end
+
+ return nil unless entry
+
+ if path_arr.size > 1
+ path_arr.shift
+ find_id_by_path(repository, entry[:oid], path_arr.join('/'))
+ else
+ entry[:oid]
+ end
+ end
+ end
+
+ def initialize(options)
+ %w(id root_id name path type mode commit_id).each do |key|
+ self.send("#{key}=", options[key.to_sym])
+ end
+ end
+
+ def name
+ encode! @name
+ end
+
+ def dir?
+ type == :tree
+ end
+
+ def file?
+ type == :blob
+ end
+
+ def submodule?
+ type == :commit
+ end
+
+ def readme?
+ name =~ /^readme/i
+ end
+
+ def contributing?
+ name =~ /^contributing/i
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/util.rb b/lib/gitlab/git/util.rb
new file mode 100644
index 00000000000..7973da2e8f8
--- /dev/null
+++ b/lib/gitlab/git/util.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module Git
+ module Util
+ LINE_SEP = "\n".freeze
+
+ def self.count_lines(string)
+ case string[-1]
+ when nil
+ 0
+ when LINE_SEP
+ string.count(LINE_SEP)
+ else
+ string.count(LINE_SEP) + 1
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index db07b7c5fcc..7e1484613f2 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -7,7 +7,8 @@ module Gitlab
ERROR_MESSAGES = {
upload: 'You are not allowed to upload code for this project.',
download: 'You are not allowed to download code from this project.',
- deploy_key: 'Deploy keys are not allowed to push code.',
+ deploy_key_upload:
+ 'This deploy key does not have write access to this project.',
no_repo: 'A repository for this project does not exist yet.'
}
@@ -17,12 +18,13 @@ module Gitlab
attr_reader :actor, :project, :protocol, :user_access, :authentication_abilities
- def initialize(actor, project, protocol, authentication_abilities:)
+ def initialize(actor, project, protocol, authentication_abilities:, env: {})
@actor = actor
@project = project
@protocol = protocol
@authentication_abilities = authentication_abilities
@user_access = UserAccess.new(user, project: project)
+ @env = env
end
def check(cmd, changes)
@@ -30,12 +32,13 @@ module Gitlab
check_active_user!
check_project_accessibility!
check_command_existence!(cmd)
+ check_repository_existence!
case cmd
when *DOWNLOAD_COMMANDS
- download_access_check
+ check_download_access!
when *PUSH_COMMANDS
- push_access_check(changes)
+ check_push_access!(changes)
end
build_status_object(true)
@@ -43,32 +46,10 @@ module Gitlab
build_status_object(false, ex.message)
end
- def download_access_check
- if user
- user_download_access_check
- elsif deploy_key.nil? && !guest_can_downlod_code?
- raise UnauthorizedError, ERROR_MESSAGES[:download]
- end
- end
-
- def push_access_check(changes)
- if user
- user_push_access_check(changes)
- else
- raise UnauthorizedError, ERROR_MESSAGES[deploy_key ? :deploy_key : :upload]
- end
- end
-
- def guest_can_downlod_code?
+ def guest_can_download_code?
Guest.can?(:download_code, project)
end
- def user_download_access_check
- unless user_can_download_code? || build_can_download_code?
- raise UnauthorizedError, ERROR_MESSAGES[:download]
- end
- end
-
def user_can_download_code?
authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_code)
end
@@ -77,35 +58,6 @@ module Gitlab
authentication_abilities.include?(:build_download_code) && user_access.can_do_action?(:build_download_code)
end
- def user_push_access_check(changes)
- unless authentication_abilities.include?(:push_code)
- raise UnauthorizedError, ERROR_MESSAGES[:upload]
- end
-
- if changes.blank?
- return # Allow access.
- end
-
- unless project.repository.exists?
- raise UnauthorizedError, ERROR_MESSAGES[:no_repo]
- end
-
- changes_list = Gitlab::ChangesList.new(changes)
-
- # Iterate over all changes to find if user allowed all of them to be applied
- changes_list.each do |change|
- status = change_access_check(change)
- unless status.allowed?
- # If user does not have access to make at least one change - cancel all push
- raise UnauthorizedError, status.message
- end
- end
- end
-
- def change_access_check(change)
- Checks::ChangeAccess.new(change, user_access: user_access, project: project).exec
- end
-
def protocol_allowed?
Gitlab::ProtocolAccess.allowed?(protocol)
end
@@ -119,6 +71,8 @@ module Gitlab
end
def check_active_user!
+ return if deploy_key?
+
if user && !user_access.allowed?
raise UnauthorizedError, "Your account has been blocked."
end
@@ -136,33 +90,92 @@ module Gitlab
end
end
- def matching_merge_request?(newrev, branch_name)
- Checks::MatchingMergeRequest.new(newrev, branch_name, project).match?
+ def check_repository_existence!
+ unless project.repository.exists?
+ raise UnauthorizedError, ERROR_MESSAGES[:no_repo]
+ end
end
- def deploy_key
- actor if actor.is_a?(DeployKey)
+ def check_download_access!
+ return if deploy_key?
+
+ passed = user_can_download_code? ||
+ build_can_download_code? ||
+ guest_can_download_code?
+
+ unless passed
+ raise UnauthorizedError, ERROR_MESSAGES[:download]
+ end
end
- def deploy_key_can_read_project?
+ def check_push_access!(changes)
if deploy_key
- return true if project.public?
- deploy_key.projects.include?(project)
+ check_deploy_key_push_access!
+ elsif user
+ check_user_push_access!
else
- false
+ raise UnauthorizedError, ERROR_MESSAGES[:upload]
end
+
+ return if changes.blank? # Allow access.
+
+ check_change_access!(changes)
end
- def can_read_project?
- if user
- user_access.can_read_project?
- elsif deploy_key
- deploy_key_can_read_project?
- else
- Guest.can?(:read_project, project)
+ def check_user_push_access!
+ unless authentication_abilities.include?(:push_code)
+ raise UnauthorizedError, ERROR_MESSAGES[:upload]
end
end
+ def check_deploy_key_push_access!
+ unless deploy_key.can_push_to?(project)
+ raise UnauthorizedError, ERROR_MESSAGES[:deploy_key_upload]
+ end
+ end
+
+ def check_change_access!(changes)
+ changes_list = Gitlab::ChangesList.new(changes)
+
+ # Iterate over all changes to find if user allowed all of them to be applied
+ changes_list.each do |change|
+ status = check_single_change_access(change)
+ unless status.allowed?
+ # If user does not have access to make at least one change - cancel all push
+ raise UnauthorizedError, status.message
+ end
+ end
+ end
+
+ def check_single_change_access(change)
+ Checks::ChangeAccess.new(
+ change,
+ user_access: user_access,
+ project: project,
+ env: @env,
+ skip_authorization: deploy_key?).exec
+ end
+
+ def matching_merge_request?(newrev, branch_name)
+ Checks::MatchingMergeRequest.new(newrev, branch_name, project).match?
+ end
+
+ def deploy_key
+ actor if deploy_key?
+ end
+
+ def deploy_key?
+ actor.is_a?(DeployKey)
+ end
+
+ def can_read_project?
+ if deploy_key
+ deploy_key.has_access_to?(project)
+ elsif user
+ user.can?(:read_project, project)
+ end || Guest.can?(:read_project, project)
+ end
+
protected
def user
diff --git a/lib/gitlab/git_access_wiki.rb b/lib/gitlab/git_access_wiki.rb
index 2c06c4ff1ef..67eaa5e088d 100644
--- a/lib/gitlab/git_access_wiki.rb
+++ b/lib/gitlab/git_access_wiki.rb
@@ -1,6 +1,6 @@
module Gitlab
class GitAccessWiki < GitAccess
- def guest_can_downlod_code?
+ def guest_can_download_code?
Guest.can?(:download_wiki_code, project)
end
@@ -8,7 +8,7 @@ module Gitlab
authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_wiki_code)
end
- def change_access_check(change)
+ def check_single_change_access(change)
if user_access.can_do_action?(:create_wiki)
build_status_object(true)
else
diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb
index 6dbae64a9fe..95dba9a327b 100644
--- a/lib/gitlab/github_import/base_formatter.rb
+++ b/lib/gitlab/github_import/base_formatter.rb
@@ -15,6 +15,10 @@ module Gitlab
end
end
+ def url
+ raw_data.url || ''
+ end
+
private
def gitlab_user_id(github_id)
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index 85df6547a67..ba869faa92e 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -4,10 +4,12 @@ module Gitlab
GITHUB_SAFE_REMAINING_REQUESTS = 100
GITHUB_SAFE_SLEEP_TIME = 500
- attr_reader :access_token
+ attr_reader :access_token, :host, :api_version
- def initialize(access_token)
+ def initialize(access_token, host: nil, api_version: 'v3')
@access_token = access_token
+ @host = host.to_s.sub(%r{/+\z}, '')
+ @api_version = api_version
if access_token
::Octokit.auto_paginate = false
@@ -17,7 +19,7 @@ module Gitlab
def api
@api ||= ::Octokit::Client.new(
access_token: access_token,
- api_endpoint: github_options[:site],
+ api_endpoint: api_endpoint,
# If there is no config, we're connecting to github.com and we
# should verify ssl.
connection_options: {
@@ -64,6 +66,14 @@ module Gitlab
private
+ def api_endpoint
+ if host.present? && api_version.present?
+ "#{host}/api/#{api_version}"
+ else
+ github_options[:site]
+ end
+ end
+
def config
Gitlab.config.omniauth.providers.find { |provider| provider.name == "github" }
end
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index 281b65bdeba..ec1318ab33c 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -3,7 +3,7 @@ module Gitlab
class Importer
include Gitlab::ShellAdapter
- attr_reader :client, :errors, :project, :repo, :repo_url
+ attr_reader :errors, :project, :repo, :repo_url
def initialize(project)
@project = project
@@ -11,12 +11,27 @@ module Gitlab
@repo_url = project.import_url
@errors = []
@labels = {}
+ end
+
+ def client
+ return @client if defined?(@client)
+ unless credentials
+ raise Projects::ImportService::Error,
+ "Unable to find project import data credentials for project ID: #{@project.id}"
+ end
- if credentials
- @client = Client.new(credentials[:user])
- else
- raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}"
+ opts = {}
+ # Gitea plan to be GitHub compliant
+ if project.gitea_import?
+ uri = URI.parse(project.import_url)
+ host = "#{uri.scheme}://#{uri.host}:#{uri.port}#{uri.path}".sub(%r{/?[\w-]+/[\w-]+\.git\z}, '')
+ opts = {
+ host: host,
+ api_version: 'v1'
+ }
end
+
+ @client = Client.new(credentials[:user], opts)
end
def execute
@@ -35,7 +50,13 @@ module Gitlab
import_comments(:issues)
import_comments(:pull_requests)
import_wiki
- import_releases
+
+ # Gitea doesn't have a Release API yet
+ # See https://github.com/go-gitea/gitea/issues/330
+ unless project.gitea_import?
+ import_releases
+ end
+
handle_errors
true
@@ -44,7 +65,9 @@ module Gitlab
private
def credentials
- @credentials ||= project.import_data.credentials if project.import_data
+ return @credentials if defined?(@credentials)
+
+ @credentials = project.import_data ? project.import_data.credentials : nil
end
def handle_errors
@@ -60,9 +83,10 @@ module Gitlab
fetch_resources(:labels, repo, per_page: 100) do |labels|
labels.each do |raw|
begin
- LabelFormatter.new(project, raw).create!
+ gh_label = LabelFormatter.new(project, raw)
+ gh_label.create!
rescue => e
- errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(gh_label.url), errors: e.message }
end
end
end
@@ -74,9 +98,10 @@ module Gitlab
fetch_resources(:milestones, repo, state: :all, per_page: 100) do |milestones|
milestones.each do |raw|
begin
- MilestoneFormatter.new(project, raw).create!
+ gh_milestone = MilestoneFormatter.new(project, raw)
+ gh_milestone.create!
rescue => e
- errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(gh_milestone.url), errors: e.message }
end
end
end
@@ -97,7 +122,7 @@ module Gitlab
apply_labels(issuable, raw)
rescue => e
- errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(gh_issue.url), errors: e.message }
end
end
end
@@ -106,18 +131,23 @@ module Gitlab
def import_pull_requests
fetch_resources(:pull_requests, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests|
pull_requests.each do |raw|
- pull_request = PullRequestFormatter.new(project, raw)
- next unless pull_request.valid?
+ gh_pull_request = PullRequestFormatter.new(project, raw)
+ next unless gh_pull_request.valid?
begin
- restore_source_branch(pull_request) unless pull_request.source_branch_exists?
- restore_target_branch(pull_request) unless pull_request.target_branch_exists?
+ restore_source_branch(gh_pull_request) unless gh_pull_request.source_branch_exists?
+ restore_target_branch(gh_pull_request) unless gh_pull_request.target_branch_exists?
+
+ merge_request = gh_pull_request.create!
- pull_request.create!
+ # Gitea doesn't return PR in the Issue API endpoint, so labels must be assigned at this stage
+ if project.gitea_import?
+ apply_labels(merge_request, raw)
+ end
rescue => e
- errors << { type: :pull_request, url: Gitlab::UrlSanitizer.sanitize(pull_request.url), errors: e.message }
+ errors << { type: :pull_request, url: Gitlab::UrlSanitizer.sanitize(gh_pull_request.url), errors: e.message }
ensure
- clean_up_restored_branches(pull_request)
+ clean_up_restored_branches(gh_pull_request)
end
end
end
@@ -233,7 +263,7 @@ module Gitlab
gh_release = ReleaseFormatter.new(project, raw)
gh_release.create! if gh_release.valid?
rescue => e
- errors << { type: :release, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ errors << { type: :release, url: Gitlab::UrlSanitizer.sanitize(gh_release.url), errors: e.message }
end
end
end
diff --git a/lib/gitlab/github_import/issuable_formatter.rb b/lib/gitlab/github_import/issuable_formatter.rb
new file mode 100644
index 00000000000..256f360efc7
--- /dev/null
+++ b/lib/gitlab/github_import/issuable_formatter.rb
@@ -0,0 +1,60 @@
+module Gitlab
+ module GithubImport
+ class IssuableFormatter < BaseFormatter
+ def project_association
+ raise NotImplementedError
+ end
+
+ def number
+ raw_data.number
+ end
+
+ def find_condition
+ { iid: number }
+ end
+
+ private
+
+ def state
+ raw_data.state == 'closed' ? 'closed' : 'opened'
+ end
+
+ def assigned?
+ raw_data.assignee.present?
+ end
+
+ def assignee_id
+ if assigned?
+ gitlab_user_id(raw_data.assignee.id)
+ end
+ end
+
+ def author
+ raw_data.user.login
+ end
+
+ def author_id
+ gitlab_author_id || project.creator_id
+ end
+
+ def body
+ raw_data.body || ""
+ end
+
+ def description
+ if gitlab_author_id
+ body
+ else
+ formatter.author_line(author) + body
+ end
+ end
+
+ def milestone
+ if raw_data.milestone.present?
+ milestone = MilestoneFormatter.new(project, raw_data.milestone)
+ project.milestones.find_by(milestone.find_condition)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb
index 887690bcc7c..6f5ac4dac0d 100644
--- a/lib/gitlab/github_import/issue_formatter.rb
+++ b/lib/gitlab/github_import/issue_formatter.rb
@@ -1,6 +1,6 @@
module Gitlab
module GithubImport
- class IssueFormatter < BaseFormatter
+ class IssueFormatter < IssuableFormatter
def attributes
{
iid: number,
@@ -24,59 +24,9 @@ module Gitlab
:issues
end
- def find_condition
- { iid: number }
- end
-
- def number
- raw_data.number
- end
-
def pull_request?
raw_data.pull_request.present?
end
-
- private
-
- def assigned?
- raw_data.assignee.present?
- end
-
- def assignee_id
- if assigned?
- gitlab_user_id(raw_data.assignee.id)
- end
- end
-
- def author
- raw_data.user.login
- end
-
- def author_id
- gitlab_author_id || project.creator_id
- end
-
- def body
- raw_data.body || ""
- end
-
- def description
- if gitlab_author_id
- body
- else
- formatter.author_line(author) + body
- end
- end
-
- def milestone
- if raw_data.milestone.present?
- project.milestones.find_by(iid: raw_data.milestone.number)
- end
- end
-
- def state
- raw_data.state == 'closed' ? 'closed' : 'opened'
- end
end
end
end
diff --git a/lib/gitlab/github_import/milestone_formatter.rb b/lib/gitlab/github_import/milestone_formatter.rb
index 401dd962521..dd782eff059 100644
--- a/lib/gitlab/github_import/milestone_formatter.rb
+++ b/lib/gitlab/github_import/milestone_formatter.rb
@@ -3,7 +3,7 @@ module Gitlab
class MilestoneFormatter < BaseFormatter
def attributes
{
- iid: raw_data.number,
+ iid: number,
project: project,
title: raw_data.title,
description: raw_data.description,
@@ -19,7 +19,15 @@ module Gitlab
end
def find_condition
- { iid: raw_data.number }
+ { iid: number }
+ end
+
+ def number
+ if project.gitea_import?
+ raw_data.id
+ else
+ raw_data.number
+ end
end
private
diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb
index a2410068845..3f635be22ba 100644
--- a/lib/gitlab/github_import/project_creator.rb
+++ b/lib/gitlab/github_import/project_creator.rb
@@ -1,14 +1,15 @@
module Gitlab
module GithubImport
class ProjectCreator
- attr_reader :repo, :name, :namespace, :current_user, :session_data
+ attr_reader :repo, :name, :namespace, :current_user, :session_data, :type
- def initialize(repo, name, namespace, current_user, session_data)
+ def initialize(repo, name, namespace, current_user, session_data, type: 'github')
@repo = repo
@name = name
@namespace = namespace
@current_user = current_user
@session_data = session_data
+ @type = type
end
def execute
@@ -19,7 +20,7 @@ module Gitlab
description: repo.description,
namespace_id: namespace.id,
visibility_level: visibility_level,
- import_type: "github",
+ import_type: type,
import_source: repo.full_name,
import_url: import_url,
skip_wiki: skip_wiki
@@ -29,7 +30,7 @@ module Gitlab
private
def import_url
- repo.clone_url.sub('https://', "https://#{session_data[:github_access_token]}@")
+ repo.clone_url.sub('://', "://#{session_data[:github_access_token]}@")
end
def visibility_level
diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb
index b9a227fb11a..4ea0200e89b 100644
--- a/lib/gitlab/github_import/pull_request_formatter.rb
+++ b/lib/gitlab/github_import/pull_request_formatter.rb
@@ -1,6 +1,6 @@
module Gitlab
module GithubImport
- class PullRequestFormatter < BaseFormatter
+ class PullRequestFormatter < IssuableFormatter
delegate :exists?, :project, :ref, :repo, :sha, to: :source_branch, prefix: true
delegate :exists?, :project, :ref, :repo, :sha, to: :target_branch, prefix: true
@@ -28,14 +28,6 @@ module Gitlab
:merge_requests
end
- def find_condition
- { iid: number }
- end
-
- def number
- raw_data.number
- end
-
def valid?
source_branch.valid? && target_branch.valid?
end
@@ -60,57 +52,15 @@ module Gitlab
end
end
- def url
- raw_data.url
- end
-
private
- def assigned?
- raw_data.assignee.present?
- end
-
- def assignee_id
- if assigned?
- gitlab_user_id(raw_data.assignee.id)
- end
- end
-
- def author
- raw_data.user.login
- end
-
- def author_id
- gitlab_author_id || project.creator_id
- end
-
- def body
- raw_data.body || ""
- end
-
- def description
- if gitlab_author_id
- body
+ def state
+ if raw_data.state == 'closed' && raw_data.merged_at.present?
+ 'merged'
else
- formatter.author_line(author) + body
+ super
end
end
-
- def milestone
- if raw_data.milestone.present?
- project.milestones.find_by(iid: raw_data.milestone.number)
- end
- end
-
- def state
- @state ||= if raw_data.state == 'closed' && raw_data.merged_at.present?
- 'merged'
- elsif raw_data.state == 'closed'
- 'closed'
- else
- 'opened'
- end
- end
end
end
end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 2c21804fe7a..b8a5ac907a4 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -8,9 +8,12 @@ module Gitlab
gon.shortcuts_path = help_page_path('shortcuts')
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
gon.award_menu_url = emojis_path
+ gon.katex_css_url = ActionController::Base.helpers.asset_path('katex.css')
+ gon.katex_js_url = ActionController::Base.helpers.asset_path('katex.js')
if current_user
gon.current_user_id = current_user.id
+ gon.current_username = current_user.username
end
end
end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index e6ecd118609..08ad3274b38 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -6,6 +6,7 @@ project_tree:
- :events
- issues:
- :events
+ - :timelogs
- notes:
- :author
- :events
@@ -27,6 +28,7 @@ project_tree:
- :events
- :merge_request_diff
- :events
+ - :timelogs
- label_links:
- label:
:priorities
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index c551321c18d..cda6ddf0443 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -120,7 +120,7 @@ module Gitlab
members_mapper: members_mapper,
user: @user,
project_id: restored_project.id)
- end
+ end.compact
relation_hash_list.is_a?(Array) ? relation_array : relation_array.first
end
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index a0e80fccad9..7a649f28340 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -14,7 +14,7 @@ module Gitlab
priorities: :label_priorities,
label: :project_label }.freeze
- USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id].freeze
+ USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id merge_user_id].freeze
PROJECT_REFERENCES = %w[project_id source_project_id gl_project_id target_project_id].freeze
@@ -22,7 +22,7 @@ module Gitlab
IMPORTED_OBJECT_MAX_RETRIES = 5.freeze
- EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels project_label group_label].freeze
+ EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels].freeze
def self.create(*args)
new(*args).create
@@ -40,6 +40,8 @@ module Gitlab
# the relation_hash, updating references with new object IDs, mapping users using
# the "members_mapper" object, also updating notes if required.
def create
+ return nil if unknown_service?
+
setup_models
generate_imported_object
@@ -99,6 +101,8 @@ module Gitlab
def generate_imported_object
if BUILD_MODELS.include?(@relation_name) # call #trace= method after assigning the other attributes
trace = @relation_hash.delete('trace')
+ @relation_hash.delete('token')
+
imported_object do |object|
object.trace = trace
object.commit_id = nil
@@ -185,7 +189,7 @@ module Gitlab
# Otherwise always create the record, skipping the extra SELECT clause.
@existing_or_new_object ||= begin
if EXISTING_OBJECT_CHECK.include?(@relation_name)
- attribute_hash = attribute_hash_for(['events', 'priorities'])
+ attribute_hash = attribute_hash_for(['events'])
existing_object.assign_attributes(attribute_hash) if attribute_hash.any?
@@ -206,15 +210,38 @@ module Gitlab
def existing_object
@existing_object ||=
begin
- finder_attributes = @relation_name == :group_label ? %w[title group_id] : %w[title project_id]
- finder_hash = parsed_relation_hash.slice(*finder_attributes)
- existing_object = relation_class.find_or_create_by(finder_hash)
+ existing_object = find_or_create_object!
+
# Done in two steps, as MySQL behaves differently than PostgreSQL using
# the +find_or_create_by+ method and does not return the ID the second time.
existing_object.update!(parsed_relation_hash)
existing_object
end
end
+
+ def unknown_service?
+ @relation_name == :services && parsed_relation_hash['type'] &&
+ !Object.const_defined?(parsed_relation_hash['type'])
+ end
+
+ def find_or_create_object!
+ finder_attributes = @relation_name == :group_label ? %w[title group_id] : %w[title project_id]
+ finder_hash = parsed_relation_hash.slice(*finder_attributes)
+
+ if label?
+ label = relation_class.find_or_initialize_by(finder_hash)
+ parsed_relation_hash.delete('priorities') if label.persisted?
+
+ label.save!
+ label
+ else
+ relation_class.find_or_create_by(finder_hash)
+ end
+ end
+
+ def label?
+ @relation_name.to_s.include?('label')
+ end
end
end
end
diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb
index 94261b7eeed..45958710c13 100644
--- a/lib/gitlab/import_sources.rb
+++ b/lib/gitlab/import_sources.rb
@@ -7,21 +7,38 @@ module Gitlab
module ImportSources
extend CurrentSettings
+ ImportSource = Struct.new(:name, :title, :importer)
+
+ ImportTable = [
+ ImportSource.new('github', 'GitHub', Gitlab::GithubImport::Importer),
+ ImportSource.new('bitbucket', 'Bitbucket', Gitlab::BitbucketImport::Importer),
+ ImportSource.new('gitlab', 'GitLab.com', Gitlab::GitlabImport::Importer),
+ ImportSource.new('google_code', 'Google Code', Gitlab::GoogleCodeImport::Importer),
+ ImportSource.new('fogbugz', 'FogBugz', Gitlab::FogbugzImport::Importer),
+ ImportSource.new('git', 'Repo by URL', nil),
+ ImportSource.new('gitlab_project', 'GitLab export', Gitlab::ImportExport::Importer),
+ ImportSource.new('gitea', 'Gitea', Gitlab::GithubImport::Importer)
+ ].freeze
+
class << self
+ def options
+ @options ||= Hash[ImportTable.map { |importer| [importer.title, importer.name] }]
+ end
+
def values
- options.values
+ @values ||= ImportTable.map(&:name)
end
- def options
- {
- 'GitHub' => 'github',
- 'Bitbucket' => 'bitbucket',
- 'GitLab.com' => 'gitlab',
- 'Google Code' => 'google_code',
- 'FogBugz' => 'fogbugz',
- 'Repo by URL' => 'git',
- 'GitLab export' => 'gitlab_project'
- }
+ def importer_names
+ @importer_names ||= ImportTable.select(&:importer).map(&:name)
+ end
+
+ def importer(name)
+ ImportTable.find { |import_source| import_source.name == name }.importer
+ end
+
+ def title(name)
+ options.key(name)
end
end
end
diff --git a/lib/gitlab/kubernetes.rb b/lib/gitlab/kubernetes.rb
new file mode 100644
index 00000000000..288771c1c12
--- /dev/null
+++ b/lib/gitlab/kubernetes.rb
@@ -0,0 +1,80 @@
+module Gitlab
+ # Helper methods to do with Kubernetes network services & resources
+ module Kubernetes
+ # This is the comand that is run to start a terminal session. Kubernetes
+ # expects `command=foo&command=bar, not `command[]=foo&command[]=bar`
+ EXEC_COMMAND = URI.encode_www_form(
+ ['sh', '-c', 'bash || sh'].map { |value| ['command', value] }
+ )
+
+ # Filters an array of pods (as returned by the kubernetes API) by their labels
+ def filter_pods(pods, labels = {})
+ pods.select do |pod|
+ metadata = pod.fetch("metadata", {})
+ pod_labels = metadata.fetch("labels", nil)
+ next unless pod_labels
+
+ labels.all? { |k, v| pod_labels[k.to_s] == v }
+ end
+ end
+
+ # Converts a pod (as returned by the kubernetes API) into a terminal
+ def terminals_for_pod(api_url, namespace, pod)
+ metadata = pod.fetch("metadata", {})
+ status = pod.fetch("status", {})
+ spec = pod.fetch("spec", {})
+
+ containers = spec["containers"]
+ pod_name = metadata["name"]
+ phase = status["phase"]
+
+ return unless containers.present? && pod_name.present? && phase == "Running"
+
+ created_at = DateTime.parse(metadata["creationTimestamp"]) rescue nil
+
+ containers.map do |container|
+ {
+ selectors: { pod: pod_name, container: container["name"] },
+ url: container_exec_url(api_url, namespace, pod_name, container["name"]),
+ subprotocols: ['channel.k8s.io'],
+ headers: Hash.new { |h, k| h[k] = [] },
+ created_at: created_at,
+ }
+ end
+ end
+
+ def add_terminal_auth(terminal, token, ca_pem = nil)
+ terminal[:headers]['Authorization'] << "Bearer #{token}"
+ terminal[:ca_pem] = ca_pem if ca_pem.present?
+ terminal
+ end
+
+ def container_exec_url(api_url, namespace, pod_name, container_name)
+ url = URI.parse(api_url)
+ url.path = [
+ url.path.sub(%r{/+\z}, ''),
+ 'api', 'v1',
+ 'namespaces', ERB::Util.url_encode(namespace),
+ 'pods', ERB::Util.url_encode(pod_name),
+ 'exec'
+ ].join('/')
+
+ url.query = {
+ container: container_name,
+ tty: true,
+ stdin: true,
+ stdout: true,
+ stderr: true,
+ }.to_query + '&' + EXEC_COMMAND
+
+ case url.scheme
+ when 'http'
+ url.scheme = 'ws'
+ when 'https'
+ url.scheme = 'wss'
+ end
+
+ url.to_s
+ end
+ end
+end
diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb
index 7e06bd2b0fb..54a5b1d31cd 100644
--- a/lib/gitlab/ldap/access.rb
+++ b/lib/gitlab/ldap/access.rb
@@ -34,21 +34,21 @@ module Gitlab
def allowed?
if ldap_user
unless ldap_config.active_directory
- user.activate if user.ldap_blocked?
+ unblock_user(user, 'is available again') if user.ldap_blocked?
return true
end
# Block user in GitLab if he/she was blocked in AD
if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter)
- user.ldap_block
+ block_user(user, 'is disabled in Active Directory')
false
else
- user.activate if user.ldap_blocked?
+ unblock_user(user, 'is not disabled anymore') if user.ldap_blocked?
true
end
else
# Block the user if they no longer exist in LDAP/AD
- user.ldap_block
+ block_user(user, 'does not exist anymore')
false
end
end
@@ -64,6 +64,24 @@ module Gitlab
def ldap_user
@ldap_user ||= Gitlab::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter)
end
+
+ def block_user(user, reason)
+ user.ldap_block
+
+ Gitlab::AppLogger.info(
+ "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \
+ "blocking Gitlab user \"#{user.name}\" (#{user.email})"
+ )
+ end
+
+ def unblock_user(user, reason)
+ user.activate
+
+ Gitlab::AppLogger.info(
+ "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \
+ "unblocking Gitlab user \"#{user.name}\" (#{user.email})"
+ )
+ end
end
end
end
diff --git a/lib/gitlab/ldap/auth_hash.rb b/lib/gitlab/ldap/auth_hash.rb
index bf4dd9542d5..95378e5a769 100644
--- a/lib/gitlab/ldap/auth_hash.rb
+++ b/lib/gitlab/ldap/auth_hash.rb
@@ -25,7 +25,7 @@ module Gitlab
end
def get_raw(key)
- auth_hash.extra[:raw_info][key]
+ auth_hash.extra[:raw_info][key] if auth_hash.extra
end
def ldap_config
diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb
index de52ef3fc65..28129198438 100644
--- a/lib/gitlab/ldap/config.rb
+++ b/lib/gitlab/ldap/config.rb
@@ -107,7 +107,7 @@ module Gitlab
end
def attributes
- options['attributes']
+ default_attributes.merge(options['attributes'])
end
def timeout
@@ -130,6 +130,16 @@ module Gitlab
end
end
+ def default_attributes
+ {
+ 'username' => %w(uid userid sAMAccountName),
+ 'email' => %w(mail email userPrincipalName),
+ 'name' => 'cn',
+ 'first_name' => 'givenName',
+ 'last_name' => 'sn'
+ }
+ end
+
protected
def base_options
diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb
index b81f3e8e8f5..7084fd1767d 100644
--- a/lib/gitlab/ldap/person.rb
+++ b/lib/gitlab/ldap/person.rb
@@ -28,7 +28,7 @@ module Gitlab
end
def name
- entry.cn.first
+ attribute_value(:name).first
end
def uid
@@ -40,7 +40,7 @@ module Gitlab
end
def email
- entry.try(:mail)
+ attribute_value(:email)
end
def dn
@@ -56,6 +56,19 @@ module Gitlab
def config
@config ||= Gitlab::LDAP::Config.new(provider)
end
+
+ # Using the LDAP attributes configuration, find and return the first
+ # attribute with a value. For example, by default, when given 'email',
+ # this method looks for 'mail', 'email' and 'userPrincipalName' and
+ # returns the first with a value.
+ def attribute_value(attribute)
+ attributes = Array(config.attributes[attribute.to_s])
+ selected_attr = attributes.find { |attr| entry.respond_to?(attr) }
+
+ return nil unless selected_attr
+
+ entry.public_send(selected_attr)
+ end
end
end
end
diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
index 01c96a6fe96..47f88727fc8 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -70,8 +70,19 @@ module Gitlab
def tag_endpoint(trans, env)
endpoint = env[ENDPOINT_KEY]
- path = endpoint_paths_cache[endpoint.route.route_method][endpoint.route.route_path]
- trans.action = "Grape##{endpoint.route.route_method} #{path}"
+
+ begin
+ route = endpoint.route
+ rescue
+ # endpoint.route is calling env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info]
+ # but env[Grape::Env::GRAPE_ROUTING_ARGS] is nil in the case of a 405 response
+ # so we're rescuing exceptions and bailing out
+ end
+
+ if route
+ path = endpoint_paths_cache[route.request_method][route.path]
+ trans.action = "Grape##{route.request_method} #{path}"
+ end
end
private
diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb
new file mode 100644
index 00000000000..dd99f9bb7d7
--- /dev/null
+++ b/lib/gitlab/middleware/multipart.rb
@@ -0,0 +1,103 @@
+# Gitlab::Middleware::Multipart - a Rack::Multipart replacement
+#
+# Rack::Multipart leaves behind tempfiles in /tmp and uses valuable Ruby
+# process time to copy files around. This alternative solution uses
+# gitlab-workhorse to clean up the tempfiles and puts the tempfiles in a
+# location where copying should not be needed.
+#
+# When gitlab-workhorse finds files in a multipart MIME body it sends
+# a signed message via a request header. This message lists the names of
+# the multipart entries that gitlab-workhorse filtered out of the
+# multipart structure and saved to tempfiles. Workhorse adds new entries
+# in the multipart structure with paths to the tempfiles.
+#
+# The job of this Rack middleware is to detect and decode the message
+# from workhorse. If present, it walks the Rack 'params' hash for the
+# current request, opens the respective tempfiles, and inserts the open
+# Ruby File objects in the params hash where Rack::Multipart would have
+# put them. The goal is that application code deeper down can keep
+# working the way it did with Rack::Multipart without changes.
+#
+# CAVEAT: the code that modifies the params hash is a bit complex. It is
+# conceivable that certain Rack params structures will not be modified
+# correctly. We are not aware of such bugs at this time though.
+#
+
+module Gitlab
+ module Middleware
+ class Multipart
+ RACK_ENV_KEY = 'HTTP_GITLAB_WORKHORSE_MULTIPART_FIELDS'
+
+ class Handler
+ def initialize(env, message)
+ @request = Rack::Request.new(env)
+ @rewritten_fields = message['rewritten_fields']
+ @open_files = []
+ end
+
+ def with_open_files
+ @rewritten_fields.each do |field, tmp_path|
+ parsed_field = Rack::Utils.parse_nested_query(field)
+ raise "unexpected field: #{field.inspect}" unless parsed_field.count == 1
+
+ key, value = parsed_field.first
+ if value.nil?
+ value = open_file(tmp_path)
+ @open_files << value
+ else
+ value = decorate_params_value(value, @request.params[key], tmp_path)
+ end
+ @request.update_param(key, value)
+ end
+
+ yield
+ ensure
+ @open_files.each(&:close)
+ end
+
+ # This function calls itself recursively
+ def decorate_params_value(path_hash, value_hash, tmp_path)
+ unless path_hash.is_a?(Hash) && path_hash.count == 1
+ raise "invalid path: #{path_hash.inspect}"
+ end
+ path_key, path_value = path_hash.first
+
+ unless value_hash.is_a?(Hash) && value_hash[path_key]
+ raise "invalid value hash: #{value_hash.inspect}"
+ end
+
+ case path_value
+ when nil
+ value_hash[path_key] = open_file(tmp_path)
+ @open_files << value_hash[path_key]
+ value_hash
+ when Hash
+ decorate_params_value(path_value, value_hash[path_key], tmp_path)
+ value_hash
+ else
+ raise "unexpected path value: #{path_value.inspect}"
+ end
+ end
+
+ def open_file(path)
+ ::UploadedFile.new(path, File.basename(path), 'application/octet-stream')
+ end
+ end
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ encoded_message = env.delete(RACK_ENV_KEY)
+ return @app.call(env) if encoded_message.blank?
+
+ message = Gitlab::Workhorse.decode_jwt(encoded_message)[0]
+
+ Handler.new(env, message).with_open_files do
+ @app.call(env)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/popen.rb b/lib/gitlab/popen.rb
index cc74bb29087..4bc5cda8cb5 100644
--- a/lib/gitlab/popen.rb
+++ b/lib/gitlab/popen.rb
@@ -5,13 +5,13 @@ module Gitlab
module Popen
extend self
- def popen(cmd, path = nil)
+ def popen(cmd, path = nil, vars = {})
unless cmd.is_a?(Array)
raise "System commands must be given as an array of strings"
end
path ||= Dir.pwd
- vars = { "PWD" => path }
+ vars['PWD'] = path
options = { chdir: path }
unless File.directory?(path)
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 66e6b29e798..6bdf3db9cb8 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -110,7 +110,7 @@ module Gitlab
end
def notes
- @notes ||= project.notes.user.search(query, as_user: @current_user).order('updated_at DESC')
+ @notes ||= NotesFinder.new(project, @current_user, search: query).execute.user.order('updated_at DESC')
end
def commits
diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb
index 9226da2d6b1..9384102acec 100644
--- a/lib/gitlab/redis.rb
+++ b/lib/gitlab/redis.rb
@@ -42,7 +42,7 @@ module Gitlab
return @_raw_config if defined?(@_raw_config)
begin
- @_raw_config = File.read(CONFIG_FILE).freeze
+ @_raw_config = ERB.new(File.read(CONFIG_FILE)).result.freeze
rescue Errno::ENOENT
@_raw_config = false
end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index d9d1e3cccca..a3fa7c1331a 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -61,11 +61,11 @@ module Gitlab
end
def file_name_regex
- @file_name_regex ||= /\A[[[:alnum:]]_\-\.\@]*\z/.freeze
+ @file_name_regex ||= /\A[[[:alnum:]]_\-\.\@\+]*\z/.freeze
end
def file_name_regex_message
- "can contain only letters, digits, '_', '-', '@' and '.'."
+ "can contain only letters, digits, '_', '-', '@', '+' and '.'."
end
def file_path_regex
@@ -123,5 +123,22 @@ module Gitlab
def environment_name_regex_message
"can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.' and spaces"
end
+
+ def kubernetes_namespace_regex
+ /\A[a-z0-9]([-a-z0-9]*[a-z0-9])?\z/
+ end
+
+ def kubernetes_namespace_regex_message
+ "can contain only letters, digits or '-', and cannot start or end with '-'"
+ end
+
+ def environment_slug_regex
+ @environment_slug_regex ||= /\A[a-z]([a-z0-9-]*[a-z0-9])?\z/.freeze
+ end
+
+ def environment_slug_regex_message
+ "can contain only lowercase letters, digits, and '-'. " \
+ "Must start with a letter, and cannot end with '-'"
+ end
end
end
diff --git a/lib/gitlab/routing.rb b/lib/gitlab/routing.rb
index 5132177de51..632e2d87500 100644
--- a/lib/gitlab/routing.rb
+++ b/lib/gitlab/routing.rb
@@ -1,5 +1,11 @@
module Gitlab
module Routing
+ extend ActiveSupport::Concern
+
+ included do
+ include Gitlab::Routing.url_helpers
+ end
+
# Returns the URL helpers Module.
#
# This method caches the output as Rails' "url_helpers" method creates an
diff --git a/lib/gitlab/serialize/ci/variables.rb b/lib/gitlab/serialize/ci/variables.rb
new file mode 100644
index 00000000000..3a9443bfcd9
--- /dev/null
+++ b/lib/gitlab/serialize/ci/variables.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module Serialize
+ module Ci
+ # This serializer could make sure our YAML variables' keys and values
+ # are always strings. This is more for legacy build data because
+ # from now on we convert them into strings before saving to database.
+ module Variables
+ extend self
+
+ def load(string)
+ return unless string
+
+ object = YAML.safe_load(string, [Symbol])
+
+ object.map do |variable|
+ variable[:key] = variable[:key].to_s
+ variable
+ end
+ end
+
+ def dump(object)
+ YAML.dump(object)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sql/union.rb b/lib/gitlab/sql/union.rb
index 1cd89b3a9c4..222021e8802 100644
--- a/lib/gitlab/sql/union.rb
+++ b/lib/gitlab/sql/union.rb
@@ -22,9 +22,7 @@ module Gitlab
# By using "unprepared_statements" we remove the usage of placeholders
# (thus fixing this problem), at a slight performance cost.
fragments = ActiveRecord::Base.connection.unprepared_statement do
- @relations.map do |rel|
- rel.reorder(nil).to_sql
- end
+ @relations.map { |rel| rel.reorder(nil).to_sql }.reject(&:blank?)
end
fragments.join("\nUNION\n")
diff --git a/lib/gitlab/template/dockerfile_template.rb b/lib/gitlab/template/dockerfile_template.rb
new file mode 100644
index 00000000000..d5d3e045a42
--- /dev/null
+++ b/lib/gitlab/template/dockerfile_template.rb
@@ -0,0 +1,30 @@
+module Gitlab
+ module Template
+ class DockerfileTemplate < BaseTemplate
+ def content
+ explanation = "# This file is a template, and might need editing before it works on your project."
+ [explanation, super].join("\n")
+ end
+
+ class << self
+ def extension
+ 'Dockerfile'
+ end
+
+ def categories
+ {
+ "General" => ''
+ }
+ end
+
+ def base_dir
+ Rails.root.join('vendor/dockerfile')
+ end
+
+ def finder(project = nil)
+ Gitlab::Template::Finders::GlobalTemplateFinder.new(self.base_dir, self.extension, self.categories)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/template/gitlab_ci_yml_template.rb b/lib/gitlab/template/gitlab_ci_yml_template.rb
index 8d1a1ed54c9..9d2ecee9756 100644
--- a/lib/gitlab/template/gitlab_ci_yml_template.rb
+++ b/lib/gitlab/template/gitlab_ci_yml_template.rb
@@ -13,8 +13,9 @@ module Gitlab
def categories
{
- "General" => '',
- "Pages" => 'Pages'
+ 'General' => '',
+ 'Pages' => 'Pages',
+ 'Auto deploy' => 'autodeploy'
}
end
@@ -25,6 +26,11 @@ module Gitlab
def finder(project = nil)
Gitlab::Template::Finders::GlobalTemplateFinder.new(self.base_dir, self.extension, self.categories)
end
+
+ def dropdown_names(context)
+ categories = context == 'autodeploy' ? ['Auto deploy'] : ['General', 'Pages']
+ super().slice(*categories)
+ end
end
end
end
diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb
index d4020af76f9..19ab76ae80f 100644
--- a/lib/gitlab/themes.rb
+++ b/lib/gitlab/themes.rb
@@ -15,7 +15,7 @@ module Gitlab
Theme.new(1, 'Graphite', 'ui_graphite'),
Theme.new(2, 'Charcoal', 'ui_charcoal'),
Theme.new(3, 'Green', 'ui_green'),
- Theme.new(4, 'Gray', 'ui_gray'),
+ Theme.new(4, 'Black', 'ui_black'),
Theme.new(5, 'Violet', 'ui_violet'),
Theme.new(6, 'Blue', 'ui_blue')
].freeze
diff --git a/lib/gitlab/time_tracking_formatter.rb b/lib/gitlab/time_tracking_formatter.rb
new file mode 100644
index 00000000000..d615c24149a
--- /dev/null
+++ b/lib/gitlab/time_tracking_formatter.rb
@@ -0,0 +1,34 @@
+module Gitlab
+ module TimeTrackingFormatter
+ extend self
+
+ def parse(string)
+ with_custom_config do
+ string.sub!(/\A-/, '')
+
+ seconds = ChronicDuration.parse(string, default_unit: 'hours') rescue nil
+ seconds *= -1 if seconds && Regexp.last_match
+ seconds
+ end
+ end
+
+ def output(seconds)
+ with_custom_config do
+ ChronicDuration.output(seconds, format: :short, limit_to_hours: false, weeks: true) rescue nil
+ end
+ end
+
+ def with_custom_config
+ # We may want to configure it through project settings in a future version.
+ ChronicDuration.hours_per_day = 8
+ ChronicDuration.days_per_week = 5
+
+ result = yield
+
+ ChronicDuration.hours_per_day = 24
+ ChronicDuration.days_per_week = 7
+
+ result
+ end
+ end
+end
diff --git a/lib/gitlab/update_path_error.rb b/lib/gitlab/update_path_error.rb
new file mode 100644
index 00000000000..ce14cc887d0
--- /dev/null
+++ b/lib/gitlab/update_path_error.rb
@@ -0,0 +1,3 @@
+module Gitlab
+ class UpdatePathError < StandardError; end
+end
diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb
index 9858d2e7d83..6c7e673fb9f 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -8,6 +8,8 @@ module Gitlab
end
def can_do_action?(action)
+ return false if no_user_or_blocked?
+
@permission_cache ||= {}
@permission_cache[action] ||= user.can?(action, project)
end
@@ -17,7 +19,7 @@ module Gitlab
end
def allowed?
- return false if user.blank? || user.blocked?
+ return false if no_user_or_blocked?
if user.requires_ldap_check? && user.try_obtain_ldap_lease
return false unless Gitlab::LDAP::Access.allowed?(user)
@@ -27,7 +29,7 @@ module Gitlab
end
def can_push_to_branch?(ref)
- return false unless user
+ return false if no_user_or_blocked?
if project.protected_branch?(ref)
return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user)
@@ -40,7 +42,7 @@ module Gitlab
end
def can_merge_to_branch?(ref)
- return false unless user
+ return false if no_user_or_blocked?
if project.protected_branch?(ref)
access_levels = project.protected_branches.matching(ref).map(&:merge_access_levels).flatten
@@ -51,9 +53,15 @@ module Gitlab
end
def can_read_project?
- return false unless user
+ return false if no_user_or_blocked?
user.can?(:read_project, project)
end
+
+ private
+
+ def no_user_or_blocked?
+ user.nil? || user.blocked?
+ end
end
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 594439a5d4b..d28bb583fe7 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -95,6 +95,19 @@ module Gitlab
]
end
+ def terminal_websocket(terminal)
+ details = {
+ 'Terminal' => {
+ 'Subprotocols' => terminal[:subprotocols],
+ 'Url' => terminal[:url],
+ 'Header' => terminal[:headers]
+ }
+ }
+ details['Terminal']['CAPem'] = terminal[:ca_pem] if terminal.has_key?(:ca_pem)
+
+ details
+ end
+
def version
path = Rails.root.join(VERSION_FILE)
path.readable? ? path.read.chomp : 'unknown'
@@ -117,8 +130,12 @@ module Gitlab
end
def verify_api_request!(request_headers)
+ decode_jwt(request_headers[INTERNAL_API_REQUEST_HEADER])
+ end
+
+ def decode_jwt(encoded_message)
JWT.decode(
- request_headers[INTERNAL_API_REQUEST_HEADER],
+ encoded_message,
secret,
true,
{ iss: 'gitlab-workhorse', verify_iss: true, algorithm: 'HS256' },
diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb
new file mode 100644
index 00000000000..ec2903b7ec6
--- /dev/null
+++ b/lib/mattermost/client.rb
@@ -0,0 +1,41 @@
+module Mattermost
+ class ClientError < Mattermost::Error; end
+
+ class Client
+ attr_reader :user
+
+ def initialize(user)
+ @user = user
+ end
+
+ private
+
+ def with_session(&blk)
+ Mattermost::Session.new(user).with_session(&blk)
+ end
+
+ def json_get(path, options = {})
+ with_session do |session|
+ json_response session.get(path, options)
+ end
+ end
+
+ def json_post(path, options = {})
+ with_session do |session|
+ json_response session.post(path, options)
+ end
+ end
+
+ def json_response(response)
+ json_response = JSON.parse(response.body)
+
+ unless response.success?
+ raise Mattermost::ClientError.new(json_response['message'] || 'Undefined error')
+ end
+
+ json_response
+ rescue JSON::JSONError
+ raise Mattermost::ClientError.new('Cannot parse response')
+ end
+ end
+end
diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb
new file mode 100644
index 00000000000..d1e4bb0eccf
--- /dev/null
+++ b/lib/mattermost/command.rb
@@ -0,0 +1,10 @@
+module Mattermost
+ class Command < Client
+ def create(params)
+ response = json_post("/api/v3/teams/#{params[:team_id]}/commands/create",
+ body: params.to_json)
+
+ response['token']
+ end
+ end
+end
diff --git a/lib/mattermost/error.rb b/lib/mattermost/error.rb
new file mode 100644
index 00000000000..014df175be0
--- /dev/null
+++ b/lib/mattermost/error.rb
@@ -0,0 +1,3 @@
+module Mattermost
+ class Error < StandardError; end
+end
diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb
new file mode 100644
index 00000000000..377cb7b1021
--- /dev/null
+++ b/lib/mattermost/session.rb
@@ -0,0 +1,160 @@
+module Mattermost
+ class NoSessionError < Mattermost::Error
+ def message
+ 'No session could be set up, is Mattermost configured with Single Sign On?'
+ end
+ end
+
+ class ConnectionError < Mattermost::Error; end
+
+ # This class' prime objective is to obtain a session token on a Mattermost
+ # instance with SSO configured where this GitLab instance is the provider.
+ #
+ # The process depends on OAuth, but skips a step in the authentication cycle.
+ # For example, usually a user would click the 'login in GitLab' button on
+ # Mattermost, which would yield a 302 status code and redirects you to GitLab
+ # to approve the use of your account on Mattermost. Which would trigger a
+ # callback so Mattermost knows this request is approved and gets the required
+ # data to create the user account etc.
+ #
+ # This class however skips the button click, and also the approval phase to
+ # speed up the process and keep it without manual action and get a session
+ # going.
+ class Session
+ include Doorkeeper::Helpers::Controller
+ include HTTParty
+
+ LEASE_TIMEOUT = 60
+
+ base_uri Settings.mattermost.host
+
+ attr_accessor :current_resource_owner, :token
+
+ def initialize(current_user)
+ @current_resource_owner = current_user
+ end
+
+ def with_session
+ with_lease do
+ raise Mattermost::NoSessionError unless create
+
+ begin
+ yield self
+ rescue Errno::ECONNREFUSED
+ raise Mattermost::NoSessionError
+ ensure
+ destroy
+ end
+ end
+ end
+
+ # Next methods are needed for Doorkeeper
+ def pre_auth
+ @pre_auth ||= Doorkeeper::OAuth::PreAuthorization.new(
+ Doorkeeper.configuration, server.client_via_uid, params)
+ end
+
+ def authorization
+ @authorization ||= strategy.request
+ end
+
+ def strategy
+ @strategy ||= server.authorization_request(pre_auth.response_type)
+ end
+
+ def request
+ @request ||= OpenStruct.new(parameters: params)
+ end
+
+ def params
+ Rack::Utils.parse_query(oauth_uri.query).symbolize_keys
+ end
+
+ def get(path, options = {})
+ handle_exceptions do
+ self.class.get(path, options.merge(headers: @headers))
+ end
+ end
+
+ def post(path, options = {})
+ handle_exceptions do
+ self.class.post(path, options.merge(headers: @headers))
+ end
+ end
+
+ private
+
+ def create
+ return unless oauth_uri
+ return unless token_uri
+
+ @token = request_token
+ @headers = {
+ Authorization: "Bearer #{@token}"
+ }
+
+ @token
+ end
+
+ def destroy
+ post('/api/v3/users/logout')
+ end
+
+ def oauth_uri
+ return @oauth_uri if defined?(@oauth_uri)
+
+ @oauth_uri = nil
+
+ response = get("/api/v3/oauth/gitlab/login", follow_redirects: false)
+ return unless 300 <= response.code && response.code < 400
+
+ redirect_uri = response.headers['location']
+ return unless redirect_uri
+
+ @oauth_uri = URI.parse(redirect_uri)
+ end
+
+ def token_uri
+ @token_uri ||=
+ if oauth_uri
+ authorization.authorize.redirect_uri if pre_auth.authorizable?
+ end
+ end
+
+ def request_token
+ response = get(token_uri, follow_redirects: false)
+
+ if 200 <= response.code && response.code < 400
+ response.headers['token']
+ end
+ end
+
+ def with_lease
+ lease_uuid = lease_try_obtain
+ raise NoSessionError unless lease_uuid
+
+ begin
+ yield
+ ensure
+ Gitlab::ExclusiveLease.cancel(lease_key, lease_uuid)
+ end
+ end
+
+ def lease_key
+ "mattermost:session"
+ end
+
+ def lease_try_obtain
+ lease = ::Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)
+ lease.try_obtain
+ end
+
+ def handle_exceptions
+ yield
+ rescue HTTParty::Error => e
+ raise Mattermost::ConnectionError.new(e.message)
+ rescue Errno::ECONNREFUSED
+ raise Mattermost::ConnectionError.new(e.message)
+ end
+ end
+end
diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb
new file mode 100644
index 00000000000..784eca6ab5a
--- /dev/null
+++ b/lib/mattermost/team.rb
@@ -0,0 +1,7 @@
+module Mattermost
+ class Team < Client
+ def all
+ json_get('/api/v3/teams/all')
+ end
+ end
+end
diff --git a/lib/omniauth/strategies/bitbucket.rb b/lib/omniauth/strategies/bitbucket.rb
new file mode 100644
index 00000000000..5a7d67c2390
--- /dev/null
+++ b/lib/omniauth/strategies/bitbucket.rb
@@ -0,0 +1,41 @@
+require 'omniauth-oauth2'
+
+module OmniAuth
+ module Strategies
+ class Bitbucket < OmniAuth::Strategies::OAuth2
+ option :name, 'bitbucket'
+
+ option :client_options, {
+ site: 'https://bitbucket.org',
+ authorize_url: 'https://bitbucket.org/site/oauth2/authorize',
+ token_url: 'https://bitbucket.org/site/oauth2/access_token'
+ }
+
+ uid do
+ raw_info['username']
+ end
+
+ info do
+ {
+ name: raw_info['display_name'],
+ avatar: raw_info['links']['avatar']['href'],
+ email: primary_email
+ }
+ end
+
+ def raw_info
+ @raw_info ||= access_token.get('api/2.0/user').parsed
+ end
+
+ def primary_email
+ primary = emails.find { |i| i['is_primary'] && i['is_confirmed'] }
+ primary && primary['email'] || nil
+ end
+
+ def emails
+ email_response = access_token.get('api/2.0/user/emails').parsed
+ @emails ||= email_response && email_response['values'] || nil
+ end
+ end
+ end
+end
diff --git a/lib/rouge/lexers/math.rb b/lib/rouge/lexers/math.rb
new file mode 100644
index 00000000000..80784adfd76
--- /dev/null
+++ b/lib/rouge/lexers/math.rb
@@ -0,0 +1,21 @@
+module Rouge
+ module Lexers
+ class Math < Lexer
+ title "A passthrough lexer used for LaTeX input"
+ desc "A boring lexer that doesn't highlight anything"
+
+ tag 'math'
+ mimetypes 'text/plain'
+
+ default_options token: 'Text'
+
+ def token
+ @token ||= Token[option :token]
+ end
+
+ def stream_tokens(string, &b)
+ yield self.token, string
+ end
+ end
+ end
+end
diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab
index d521de28e8a..2f7c34a3f31 100644
--- a/lib/support/nginx/gitlab
+++ b/lib/support/nginx/gitlab
@@ -20,6 +20,11 @@ upstream gitlab-workhorse {
server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0;
}
+map $http_upgrade $connection_upgrade_gitlab {
+ default upgrade;
+ '' close;
+}
+
## Normal HTTP host
server {
## Either remove "default_server" from the listen line below,
@@ -53,6 +58,8 @@ server {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade_gitlab;
proxy_pass http://gitlab-workhorse;
}
diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl
index bf014b56cf6..5661394058d 100644
--- a/lib/support/nginx/gitlab-ssl
+++ b/lib/support/nginx/gitlab-ssl
@@ -24,6 +24,11 @@ upstream gitlab-workhorse {
server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0;
}
+map $http_upgrade $connection_upgrade_gitlab_ssl {
+ default upgrade;
+ '' close;
+}
+
## Redirects all HTTP traffic to the HTTPS host
server {
## Either remove "default_server" from the listen line below,
@@ -98,6 +103,9 @@ server {
proxy_set_header X-Forwarded-Ssl on;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade_gitlab_ssl;
+
proxy_pass http://gitlab-workhorse;
}
diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake
index 6f27972c4e4..5e94fba97bf 100644
--- a/lib/tasks/dev.rake
+++ b/lib/tasks/dev.rake
@@ -7,9 +7,4 @@ namespace :dev do
Rake::Task["gitlab:setup"].invoke
Rake::Task["gitlab:shell:setup"].invoke
end
-
- desc 'GitLab | Start/restart foreman and watch for changes'
- task :foreman => :environment do
- sh 'rerun --dir app,config,lib -- foreman start'
- end
end
diff --git a/lib/tasks/gitlab/git.rake b/lib/tasks/gitlab/git.rake
index f9834a4dae8..a67c1fe1f27 100644
--- a/lib/tasks/gitlab/git.rake
+++ b/lib/tasks/gitlab/git.rake
@@ -3,7 +3,7 @@ namespace :gitlab do
desc "GitLab | Git | Repack"
task repack: :environment do
- failures = perform_git_cmd(%W(git repack -a --quiet), "Repacking repo")
+ failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} repack -a --quiet), "Repacking repo")
if failures.empty?
puts "Done".color(:green)
else
@@ -13,17 +13,17 @@ namespace :gitlab do
desc "GitLab | Git | Run garbage collection on all repos"
task gc: :environment do
- failures = perform_git_cmd(%W(git gc --auto --quiet), "Garbage Collecting")
+ failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} gc --auto --quiet), "Garbage Collecting")
if failures.empty?
puts "Done".color(:green)
else
output_failures(failures)
end
end
-
+
desc "GitLab | Git | Prune all repos"
task prune: :environment do
- failures = perform_git_cmd(%W(git prune), "Git Prune")
+ failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} prune), "Git Prune")
if failures.empty?
puts "Done".color(:green)
else
diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake
index dbdd4e977e8..a2eca74a3c8 100644
--- a/lib/tasks/gitlab/import.rake
+++ b/lib/tasks/gitlab/import.rake
@@ -63,8 +63,7 @@ namespace :gitlab do
if project.persisted?
puts " * Created #{project.name} (#{repo_path})".color(:green)
- project.update_repository_size
- project.update_commit_count
+ ProjectCacheWorker.perform(project.id)
else
puts " * Failed trying to create #{project.name} (#{repo_path})".color(:red)
puts " Errors: #{project.errors.messages}".color(:red)
diff --git a/lib/tasks/gitlab/ldap.rake b/lib/tasks/gitlab/ldap.rake
new file mode 100644
index 00000000000..c66a2a263dc
--- /dev/null
+++ b/lib/tasks/gitlab/ldap.rake
@@ -0,0 +1,40 @@
+namespace :gitlab do
+ namespace :ldap do
+ desc 'GitLab | LDAP | Rename provider'
+ task :rename_provider, [:old_provider, :new_provider] => :environment do |_, args|
+ old_provider = args[:old_provider] ||
+ prompt('What is the old provider? Ex. \'ldapmain\': '.color(:blue))
+ new_provider = args[:new_provider] ||
+ prompt('What is the new provider ID? Ex. \'ldapcustom\': '.color(:blue))
+ puts '' # Add some separation in the output
+
+ identities = Identity.where(provider: old_provider)
+ identity_count = identities.count
+
+ if identities.empty?
+ puts "Found no user identities with '#{old_provider}' provider."
+ puts 'Please check the provider name and try again.'
+ exit 1
+ end
+
+ plural_id_count = ActionController::Base.helpers.pluralize(identity_count, 'user')
+
+ unless ENV['force'] == 'yes'
+ puts "#{plural_id_count} with provider '#{old_provider}' will be updated to '#{new_provider}'"
+ puts 'If the new provider is incorrect, users will be unable to sign in'
+ ask_to_continue
+ puts ''
+ end
+
+ updated_count = identities.update_all(provider: new_provider)
+
+ if updated_count == identity_count
+ puts 'User identities were successfully updated'.color(:green)
+ else
+ plural_updated_count = ActionController::Base.helpers.pluralize(updated_count, 'user')
+ puts 'Some user identities could not be updated'.color(:red)
+ puts "Successfully updated #{plural_updated_count} out of #{plural_id_count} total"
+ end
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/update_commit_count.rake b/lib/tasks/gitlab/update_commit_count.rake
deleted file mode 100644
index 3bd10b0208b..00000000000
--- a/lib/tasks/gitlab/update_commit_count.rake
+++ /dev/null
@@ -1,20 +0,0 @@
-namespace :gitlab do
- desc "GitLab | Update commit count for projects"
- task update_commit_count: :environment do
- projects = Project.where(commit_count: 0)
- puts "#{projects.size} projects need to be updated. This might take a while."
- ask_to_continue unless ENV['force'] == 'yes'
-
- projects.find_each(batch_size: 100) do |project|
- print "#{project.name_with_namespace.color(:yellow)} ... "
-
- unless project.repo_exists?
- puts "skipping, because the repo is empty".color(:magenta)
- next
- end
-
- project.update_commit_count
- puts project.commit_count.to_s.color(:green)
- end
- end
-end
diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake
index 4f76dad7286..b77a5bb62d1 100644
--- a/lib/tasks/gitlab/update_templates.rake
+++ b/lib/tasks/gitlab/update_templates.rake
@@ -44,7 +44,7 @@ namespace :gitlab do
),
Template.new(
"https://gitlab.com/gitlab-org/gitlab-ci-yml.git",
- /(\.{1,2}|LICENSE|Pages|\.gitlab-ci.yml)\z/
+ /(\.{1,2}|LICENSE|Pages|autodeploy|\.gitlab-ci.yml)\z/
)
]