summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2016-08-19 13:01:58 +0300
committerDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2016-08-19 13:01:58 +0300
commit6db65143db5003f74ddb1c9868a3d852e5661a0a (patch)
tree07a792396398c16d78f8f3ed87926237aeb68271 /lib
parent74461ccc1f1503c86102b7d8e790ebac0d28fc0b (diff)
parent12fe6a6fd733110acc72aa0f5bdaec2b1fa1f358 (diff)
downloadgitlab-ce-6db65143db5003f74ddb1c9868a3d852e5661a0a.tar.gz
Merge branch 'master' into dz-merge-request-version
Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
Diffstat (limited to 'lib')
-rw-r--r--lib/api/api.rb2
-rw-r--r--lib/api/branches.rb29
-rw-r--r--lib/api/builds.rb21
-rw-r--r--lib/api/deployments.rb40
-rw-r--r--lib/api/entities.rb32
-rw-r--r--lib/api/issues.rb2
-rw-r--r--lib/api/pipelines.rb74
-rw-r--r--lib/api/project_hooks.rb2
-rw-r--r--lib/api/session.rb1
-rw-r--r--lib/api/templates.rb26
-rw-r--r--lib/ci/api/builds.rb8
-rw-r--r--lib/gitlab/akismet_helper.rb47
-rw-r--r--lib/gitlab/auth.rb44
-rw-r--r--lib/gitlab/badge/base.rb21
-rw-r--r--lib/gitlab/badge/build.rb30
-rw-r--r--lib/gitlab/badge/build/metadata.rb22
-rw-r--r--lib/gitlab/badge/build/status.rb37
-rw-r--r--lib/gitlab/badge/build/template.rb32
-rw-r--r--lib/gitlab/badge/coverage/metadata.rb30
-rw-r--r--lib/gitlab/badge/coverage/report.rb56
-rw-r--r--lib/gitlab/badge/coverage/template.rb52
-rw-r--r--lib/gitlab/badge/metadata.rb36
-rw-r--r--lib/gitlab/badge/template.rb49
-rw-r--r--lib/gitlab/conflict/file.rb186
-rw-r--r--lib/gitlab/conflict/file_collection.rb57
-rw-r--r--lib/gitlab/conflict/parser.rb62
-rw-r--r--lib/gitlab/data_builder/build.rb (renamed from lib/gitlab/build_data_builder.rb)6
-rw-r--r--lib/gitlab/data_builder/note.rb (renamed from lib/gitlab/note_data_builder.rb)6
-rw-r--r--lib/gitlab/data_builder/pipeline.rb62
-rw-r--r--lib/gitlab/data_builder/push.rb (renamed from lib/gitlab/push_data_builder.rb)6
-rw-r--r--lib/gitlab/diff/line.rb20
-rw-r--r--lib/gitlab/downtime_check/message.rb19
-rw-r--r--lib/gitlab/email/handler/base_handler.rb1
-rw-r--r--lib/gitlab/import_export/json_hash_builder.rb9
-rw-r--r--lib/gitlab/metrics.rb9
-rw-r--r--lib/gitlab/metrics/metric.rb9
-rw-r--r--lib/gitlab/metrics/rack_middleware.rb4
-rw-r--r--lib/gitlab/metrics/sidekiq_middleware.rb4
-rw-r--r--lib/gitlab/metrics/transaction.rb21
-rw-r--r--lib/gitlab/slash_commands/command_definition.rb57
-rw-r--r--lib/gitlab/slash_commands/dsl.rb98
-rw-r--r--lib/gitlab/slash_commands/extractor.rb122
-rw-r--r--lib/gitlab/template/base_template.rb71
-rw-r--r--lib/gitlab/template/finders/base_template_finder.rb35
-rw-r--r--lib/gitlab/template/finders/global_template_finder.rb38
-rw-r--r--lib/gitlab/template/finders/repo_template_finder.rb59
-rw-r--r--lib/gitlab/template/gitignore_template.rb (renamed from lib/gitlab/template/gitignore.rb)6
-rw-r--r--lib/gitlab/template/gitlab_ci_yml_template.rb (renamed from lib/gitlab/template/gitlab_ci_yml.rb)6
-rw-r--r--lib/gitlab/template/issue_template.rb19
-rw-r--r--lib/gitlab/template/merge_request_template.rb19
-rw-r--r--lib/gitlab/user_access.rb4
51 files changed, 1507 insertions, 201 deletions
diff --git a/lib/api/api.rb b/lib/api/api.rb
index aa34110c34b..382d29f8dc4 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -43,6 +43,7 @@ module API
mount ::API::CommitStatuses
mount ::API::Commits
mount ::API::DeployKeys
+ mount ::API::Deployments
mount ::API::Environments
mount ::API::Files
mount ::API::Groups
@@ -56,6 +57,7 @@ module API
mount ::API::Milestones
mount ::API::Namespaces
mount ::API::Notes
+ mount ::API::Pipelines
mount ::API::ProjectHooks
mount ::API::ProjectSnippets
mount ::API::Projects
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index a77afe634f6..b615703df93 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -61,22 +61,27 @@ module API
name: @branch.name
}
- unless developers_can_merge.nil?
- protected_branch_params.merge!({
- merge_access_level_attributes: {
- access_level: developers_can_merge ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
- }
- })
+ # If `developers_can_merge` is switched off, _all_ `DEVELOPER`
+ # merge_access_levels need to be deleted.
+ if developers_can_merge == false
+ protected_branch.merge_access_levels.where(access_level: Gitlab::Access::DEVELOPER).destroy_all
end
- unless developers_can_push.nil?
- protected_branch_params.merge!({
- push_access_level_attributes: {
- access_level: developers_can_push ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
- }
- })
+ # If `developers_can_push` is switched off, _all_ `DEVELOPER`
+ # push_access_levels need to be deleted.
+ if developers_can_push == false
+ protected_branch.push_access_levels.where(access_level: Gitlab::Access::DEVELOPER).destroy_all
end
+ protected_branch_params.merge!(
+ merge_access_levels_attributes: [{
+ access_level: developers_can_merge ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
+ }],
+ push_access_levels_attributes: [{
+ access_level: developers_can_push ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
+ }]
+ )
+
if protected_branch
service = ProtectedBranches::UpdateService.new(user_project, current_user, protected_branch_params)
service.execute(protected_branch)
diff --git a/lib/api/builds.rb b/lib/api/builds.rb
index be5a3484ec8..52bdbcae5a8 100644
--- a/lib/api/builds.rb
+++ b/lib/api/builds.rb
@@ -189,6 +189,27 @@ module API
present build, with: Entities::Build,
user_can_download_artifacts: can?(current_user, :read_build, user_project)
end
+
+ desc 'Trigger a manual build' do
+ success Entities::Build
+ detail 'This feature was added in GitLab 8.11'
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a Build'
+ end
+ post ":id/builds/:build_id/play" do
+ authorize_read_builds!
+
+ build = get_build!(params[:build_id])
+
+ bad_request!("Unplayable Build") unless build.playable?
+
+ build.play(current_user)
+
+ status 200
+ present build, with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ end
end
helpers do
diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb
new file mode 100644
index 00000000000..f782bcaf7e9
--- /dev/null
+++ b/lib/api/deployments.rb
@@ -0,0 +1,40 @@
+module API
+ # Deployments RESTfull API endpoints
+ class Deployments < Grape::API
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The project ID'
+ end
+ resource :projects do
+ desc 'Get all deployments of the project' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Deployment
+ end
+ params do
+ optional :page, type: Integer, desc: 'Page number of the current request'
+ optional :per_page, type: Integer, desc: 'Number of items per page'
+ end
+ get ':id/deployments' do
+ authorize! :read_deployment, user_project
+
+ present paginate(user_project.deployments), with: Entities::Deployment
+ end
+
+ desc 'Gets a specific deployment' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Deployment
+ end
+ params do
+ requires :deployment_id, type: Integer, desc: 'The deployment ID'
+ end
+ get ':id/deployments/:deployment_id' do
+ authorize! :read_deployment, user_project
+
+ deployment = user_project.deployments.find(params[:deployment_id])
+
+ present deployment, with: Entities::Deployment
+ end
+ end
+ end
+end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index b305fce9fcf..fcb0b12c191 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -48,7 +48,8 @@ module API
class ProjectHook < Hook
expose :project_id, :push_events
- expose :issues_events, :merge_requests_events, :tag_push_events, :note_events, :build_events
+ expose :issues_events, :merge_requests_events, :tag_push_events
+ expose :note_events, :build_events, :pipeline_events
expose :enable_ssl_verification
end
@@ -129,12 +130,14 @@ module API
expose :developers_can_push do |repo_branch, options|
project = options[:project]
- project.protected_branches.matching(repo_branch.name).any? { |protected_branch| protected_branch.push_access_level.access_level == Gitlab::Access::DEVELOPER }
+ access_levels = project.protected_branches.matching(repo_branch.name).map(&:push_access_levels).flatten
+ access_levels.any? { |access_level| access_level.access_level == Gitlab::Access::DEVELOPER }
end
expose :developers_can_merge do |repo_branch, options|
project = options[:project]
- project.protected_branches.matching(repo_branch.name).any? { |protected_branch| protected_branch.merge_access_level.access_level == Gitlab::Access::DEVELOPER }
+ access_levels = project.protected_branches.matching(repo_branch.name).map(&:merge_access_levels).flatten
+ access_levels.any? { |access_level| access_level.access_level == Gitlab::Access::DEVELOPER }
end
end
@@ -357,7 +360,8 @@ module API
class ProjectService < Grape::Entity
expose :id, :title, :created_at, :updated_at, :active
- expose :push_events, :issues_events, :merge_requests_events, :tag_push_events, :note_events, :build_events
+ expose :push_events, :issues_events, :merge_requests_events
+ expose :tag_push_events, :note_events, :build_events, :pipeline_events
# Expose serialized properties
expose :properties do |service, options|
field_names = service.fields.
@@ -511,8 +515,28 @@ module API
expose :key, :value
end
+ class Pipeline < Grape::Entity
+ expose :id, :status, :ref, :sha, :before_sha, :tag, :yaml_errors
+
+ expose :user, with: Entities::UserBasic
+ expose :created_at, :updated_at, :started_at, :finished_at, :committed_at
+ expose :duration
+ end
+
class Environment < Grape::Entity
expose :id, :name, :external_url
+ expose :project, using: Entities::Project
+ end
+
+ class EnvironmentBasic < Grape::Entity
+ expose :id, :name, :external_url
+ end
+
+ class Deployment < Grape::Entity
+ expose :id, :iid, :ref, :sha, :created_at
+ expose :user, using: Entities::UserBasic
+ expose :environment, using: Entities::EnvironmentBasic
+ expose :deployable, using: Entities::Build
end
class RepoLicense < Grape::Entity
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index c4d3134da6c..077258faee1 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -3,8 +3,6 @@ module API
class Issues < Grape::API
before { authenticate! }
- helpers ::Gitlab::AkismetHelper
-
helpers do
def filter_issues_state(issues, state)
case state
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
new file mode 100644
index 00000000000..2aae75c471d
--- /dev/null
+++ b/lib/api/pipelines.rb
@@ -0,0 +1,74 @@
+module API
+ class Pipelines < Grape::API
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The project ID'
+ end
+ resource :projects do
+ desc 'Get all Pipelines of the project' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Pipeline
+ end
+ params do
+ optional :page, type: Integer, desc: 'Page number of the current request'
+ optional :per_page, type: Integer, desc: 'Number of items per page'
+ end
+ get ':id/pipelines' do
+ authorize! :read_pipeline, user_project
+
+ present paginate(user_project.pipelines), with: Entities::Pipeline
+ end
+
+ desc 'Gets a specific pipeline for the project' do
+ detail 'This feature was introduced in GitLab 8.11'
+ success Entities::Pipeline
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+ end
+ get ':id/pipelines/:pipeline_id' do
+ authorize! :read_pipeline, user_project
+
+ present pipeline, with: Entities::Pipeline
+ end
+
+ desc 'Retry failed builds in the pipeline' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Pipeline
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+ end
+ post ':id/pipelines/:pipeline_id/retry' do
+ authorize! :update_pipeline, user_project
+
+ pipeline.retry_failed(current_user)
+
+ present pipeline, with: Entities::Pipeline
+ end
+
+ desc 'Cancel all builds in the pipeline' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Pipeline
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+ end
+ post ':id/pipelines/:pipeline_id/cancel' do
+ authorize! :update_pipeline, user_project
+
+ pipeline.cancel_running
+
+ status 200
+ present pipeline.reload, with: Entities::Pipeline
+ end
+ end
+
+ helpers do
+ def pipeline
+ @pipeline ||= user_project.pipelines.find(params[:pipeline_id])
+ end
+ end
+ end
+end
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index 6bb70bc8bc3..3f63cd678e8 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -45,6 +45,7 @@ module API
:tag_push_events,
:note_events,
:build_events,
+ :pipeline_events,
:enable_ssl_verification
]
@hook = user_project.hooks.new(attrs)
@@ -78,6 +79,7 @@ module API
:tag_push_events,
:note_events,
:build_events,
+ :pipeline_events,
:enable_ssl_verification
]
diff --git a/lib/api/session.rb b/lib/api/session.rb
index 56c202f1294..55ec66a6d67 100644
--- a/lib/api/session.rb
+++ b/lib/api/session.rb
@@ -14,6 +14,7 @@ module API
user = Gitlab::Auth.find_with_user_password(params[:email] || params[:login], params[:password])
return unauthorized! unless user
+ return render_api_error!('401 Unauthorized. You have 2FA enabled. Please use a personal access token to access the API', 401) if user.two_factor_enabled?
present user, with: Entities::UserLogin
end
end
diff --git a/lib/api/templates.rb b/lib/api/templates.rb
index 18408797756..b9e718147e1 100644
--- a/lib/api/templates.rb
+++ b/lib/api/templates.rb
@@ -1,21 +1,28 @@
module API
class Templates < Grape::API
- TEMPLATE_TYPES = {
- gitignores: Gitlab::Template::Gitignore,
- gitlab_ci_ymls: Gitlab::Template::GitlabCiYml
+ GLOBAL_TEMPLATE_TYPES = {
+ gitignores: Gitlab::Template::GitignoreTemplate,
+ gitlab_ci_ymls: Gitlab::Template::GitlabCiYmlTemplate
}.freeze
- TEMPLATE_TYPES.each do |template, klass|
+ helpers do
+ def render_response(template_type, template)
+ not_found!(template_type.to_s.singularize) unless template
+ present template, with: Entities::Template
+ end
+ end
+
+ GLOBAL_TEMPLATE_TYPES.each do |template_type, klass|
# Get the list of the available template
#
# Example Request:
# GET /gitignores
# GET /gitlab_ci_ymls
- get template.to_s do
+ get template_type.to_s do
present klass.all, with: Entities::TemplatesList
end
- # Get the text for a specific template
+ # Get the text for a specific template present in local filesystem
#
# Parameters:
# name (required) - The name of a template
@@ -23,13 +30,10 @@ module API
# Example Request:
# GET /gitignores/Elixir
# GET /gitlab_ci_ymls/Ruby
- get "#{template}/:name" do
+ get "#{template_type}/:name" do
required_attributes! [:name]
-
new_template = klass.find(params[:name])
- not_found!(template.to_s.singularize) unless new_template
-
- present new_template, with: Entities::Template
+ render_response(template_type, new_template)
end
end
end
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 260ac81f5fa..9f3b582a263 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -20,8 +20,13 @@ module Ci
build = Ci::RegisterBuildService.new.execute(current_runner)
if build
+ Gitlab::Metrics.add_event(:build_found,
+ project: build.project.path_with_namespace)
+
present build, with: Entities::BuildDetails
else
+ Gitlab::Metrics.add_event(:build_not_found)
+
not_found!
end
end
@@ -42,6 +47,9 @@ module Ci
build.update_attributes(trace: params[:trace]) if params[:trace]
+ Gitlab::Metrics.add_event(:update_build,
+ project: build.project.path_with_namespace)
+
case params[:state].to_s
when 'success'
build.success
diff --git a/lib/gitlab/akismet_helper.rb b/lib/gitlab/akismet_helper.rb
deleted file mode 100644
index 207736b59db..00000000000
--- a/lib/gitlab/akismet_helper.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-module Gitlab
- module AkismetHelper
- def akismet_enabled?
- current_application_settings.akismet_enabled
- end
-
- def akismet_client
- @akismet_client ||= ::Akismet::Client.new(current_application_settings.akismet_api_key,
- Gitlab.config.gitlab.url)
- end
-
- def client_ip(env)
- env['action_dispatch.remote_ip'].to_s
- end
-
- def user_agent(env)
- env['HTTP_USER_AGENT']
- end
-
- def check_for_spam?(project)
- akismet_enabled? && project.public?
- end
-
- def is_spam?(environment, user, text)
- client = akismet_client
- ip_address = client_ip(environment)
- user_agent = user_agent(environment)
-
- params = {
- type: 'comment',
- text: text,
- created_at: DateTime.now,
- author: user.name,
- author_email: user.email,
- referrer: environment['HTTP_REFERER'],
- }
-
- begin
- is_spam, is_blatant = client.check(ip_address, user_agent, params)
- is_spam || is_blatant
- rescue => e
- Rails.logger.error("Unable to connect to Akismet: #{e}, skipping check")
- false
- end
- end
- end
-end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index db1704af75e..91f0270818a 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -10,13 +10,12 @@ module Gitlab
if valid_ci_request?(login, password, project)
result.type = :ci
- elsif result.user = find_with_user_password(login, password)
- result.type = :gitlab_or_ldap
- elsif result.user = oauth_access_token_check(login, password)
- result.type = :oauth
+ else
+ result = populate_result(login, password)
end
- rate_limit!(ip, success: !!result.user || (result.type == :ci), login: login)
+ success = result.user.present? || [:ci, :missing_personal_token].include?(result.type)
+ rate_limit!(ip, success: success, login: login)
result
end
@@ -76,10 +75,43 @@ module Gitlab
end
end
+ def populate_result(login, password)
+ result =
+ user_with_password_for_git(login, password) ||
+ oauth_access_token_check(login, password) ||
+ personal_access_token_check(login, password)
+
+ if result
+ result.type = nil unless result.user
+
+ if result.user && result.user.two_factor_enabled? && result.type == :gitlab_or_ldap
+ result.type = :missing_personal_token
+ end
+ end
+
+ result || Result.new
+ end
+
+ def user_with_password_for_git(login, password)
+ user = find_with_user_password(login, password)
+ Result.new(user, :gitlab_or_ldap) if user
+ end
+
def oauth_access_token_check(login, password)
if login == "oauth2" && password.present?
token = Doorkeeper::AccessToken.by_token(password)
- token && token.accessible? && User.find_by(id: token.resource_owner_id)
+ if token && token.accessible?
+ user = User.find_by(id: token.resource_owner_id)
+ Result.new(user, :oauth)
+ end
+ end
+ end
+
+ def personal_access_token_check(login, password)
+ if login && password
+ user = User.find_by_personal_access_token(password)
+ validation = User.by_login(login)
+ Result.new(user, :personal_token) if user == validation
end
end
end
diff --git a/lib/gitlab/badge/base.rb b/lib/gitlab/badge/base.rb
new file mode 100644
index 00000000000..909fa24fa90
--- /dev/null
+++ b/lib/gitlab/badge/base.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module Badge
+ class Base
+ def entity
+ raise NotImplementedError
+ end
+
+ def status
+ raise NotImplementedError
+ end
+
+ def metadata
+ raise NotImplementedError
+ end
+
+ def template
+ raise NotImplementedError
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/badge/build.rb b/lib/gitlab/badge/build.rb
deleted file mode 100644
index 1de721a2269..00000000000
--- a/lib/gitlab/badge/build.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module Gitlab
- module Badge
- ##
- # Build badge
- #
- class Build
- delegate :key_text, :value_text, to: :template
-
- def initialize(project, ref)
- @project = project
- @ref = ref
- @sha = @project.commit(@ref).try(:sha)
- end
-
- def status
- @project.pipelines
- .where(sha: @sha, ref: @ref)
- .status || 'unknown'
- end
-
- def metadata
- @metadata ||= Build::Metadata.new(@project, @ref)
- end
-
- def template
- @template ||= Build::Template.new(status)
- end
- end
- end
-end
diff --git a/lib/gitlab/badge/build/metadata.rb b/lib/gitlab/badge/build/metadata.rb
index 553ef8d7b16..f87a7b7942e 100644
--- a/lib/gitlab/badge/build/metadata.rb
+++ b/lib/gitlab/badge/build/metadata.rb
@@ -1,25 +1,17 @@
module Gitlab
module Badge
- class Build
+ module Build
##
# Class that describes build badge metadata
#
- class Metadata
- include Gitlab::Application.routes.url_helpers
- include ActionView::Helpers::AssetTagHelper
- include ActionView::Helpers::UrlHelper
-
- def initialize(project, ref)
- @project = project
- @ref = ref
- end
-
- def to_html
- link_to(image_tag(image_url, alt: 'build status'), link_url)
+ class Metadata < Badge::Metadata
+ def initialize(badge)
+ @project = badge.project
+ @ref = badge.ref
end
- def to_markdown
- "[![build status](#{image_url})](#{link_url})"
+ def title
+ 'build status'
end
def image_url
diff --git a/lib/gitlab/badge/build/status.rb b/lib/gitlab/badge/build/status.rb
new file mode 100644
index 00000000000..50aa45e5406
--- /dev/null
+++ b/lib/gitlab/badge/build/status.rb
@@ -0,0 +1,37 @@
+module Gitlab
+ module Badge
+ module Build
+ ##
+ # Build status badge
+ #
+ class Status < Badge::Base
+ attr_reader :project, :ref
+
+ def initialize(project, ref)
+ @project = project
+ @ref = ref
+
+ @sha = @project.commit(@ref).try(:sha)
+ end
+
+ def entity
+ 'build'
+ end
+
+ def status
+ @project.pipelines
+ .where(sha: @sha, ref: @ref)
+ .status || 'unknown'
+ end
+
+ def metadata
+ @metadata ||= Build::Metadata.new(self)
+ end
+
+ def template
+ @template ||= Build::Template.new(self)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/badge/build/template.rb b/lib/gitlab/badge/build/template.rb
index deba3b669b3..2b95ddfcb53 100644
--- a/lib/gitlab/badge/build/template.rb
+++ b/lib/gitlab/badge/build/template.rb
@@ -1,12 +1,12 @@
module Gitlab
module Badge
- class Build
+ module Build
##
# Class that represents a build badge template.
#
# Template object will be passed to badge.svg.erb template.
#
- class Template
+ class Template < Badge::Template
STATUS_COLOR = {
success: '#4c1',
failed: '#e05d44',
@@ -17,16 +17,17 @@ module Gitlab
unknown: '#9f9f9f'
}
- def initialize(status)
- @status = status
+ def initialize(badge)
+ @entity = badge.entity
+ @status = badge.status
end
def key_text
- 'build'
+ @entity.to_s
end
def value_text
- @status
+ @status.to_s
end
def key_width
@@ -37,25 +38,8 @@ module Gitlab
54
end
- def key_color
- '#555'
- end
-
def value_color
- STATUS_COLOR[@status.to_sym] ||
- STATUS_COLOR[:unknown]
- end
-
- def key_text_anchor
- key_width / 2
- end
-
- def value_text_anchor
- key_width + (value_width / 2)
- end
-
- def width
- key_width + value_width
+ STATUS_COLOR[@status.to_sym] || STATUS_COLOR[:unknown]
end
end
end
diff --git a/lib/gitlab/badge/coverage/metadata.rb b/lib/gitlab/badge/coverage/metadata.rb
new file mode 100644
index 00000000000..53588185622
--- /dev/null
+++ b/lib/gitlab/badge/coverage/metadata.rb
@@ -0,0 +1,30 @@
+module Gitlab
+ module Badge
+ module Coverage
+ ##
+ # Class that describes coverage badge metadata
+ #
+ class Metadata < Badge::Metadata
+ def initialize(badge)
+ @project = badge.project
+ @ref = badge.ref
+ @job = badge.job
+ end
+
+ def title
+ 'coverage report'
+ end
+
+ def image_url
+ coverage_namespace_project_badges_url(@project.namespace,
+ @project, @ref,
+ format: :svg)
+ end
+
+ def link_url
+ namespace_project_commits_url(@project.namespace, @project, id: @ref)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/badge/coverage/report.rb b/lib/gitlab/badge/coverage/report.rb
new file mode 100644
index 00000000000..3d56ea3e47a
--- /dev/null
+++ b/lib/gitlab/badge/coverage/report.rb
@@ -0,0 +1,56 @@
+module Gitlab
+ module Badge
+ module Coverage
+ ##
+ # Test coverage report badge
+ #
+ class Report < Badge::Base
+ attr_reader :project, :ref, :job
+
+ def initialize(project, ref, job = nil)
+ @project = project
+ @ref = ref
+ @job = job
+
+ @pipeline = @project.pipelines
+ .where(ref: @ref)
+ .where(sha: @project.commit(@ref).try(:sha))
+ .first
+ end
+
+ def entity
+ 'coverage'
+ end
+
+ def status
+ @coverage ||= raw_coverage
+ return unless @coverage
+
+ @coverage.to_i
+ end
+
+ def metadata
+ @metadata ||= Coverage::Metadata.new(self)
+ end
+
+ def template
+ @template ||= Coverage::Template.new(self)
+ end
+
+ private
+
+ def raw_coverage
+ return unless @pipeline
+
+ if @job.blank?
+ @pipeline.coverage
+ else
+ @pipeline.builds
+ .find_by(name: @job)
+ .try(:coverage)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/badge/coverage/template.rb b/lib/gitlab/badge/coverage/template.rb
new file mode 100644
index 00000000000..06e0d084e9f
--- /dev/null
+++ b/lib/gitlab/badge/coverage/template.rb
@@ -0,0 +1,52 @@
+module Gitlab
+ module Badge
+ module Coverage
+ ##
+ # Class that represents a coverage badge template.
+ #
+ # Template object will be passed to badge.svg.erb template.
+ #
+ class Template < Badge::Template
+ STATUS_COLOR = {
+ good: '#4c1',
+ acceptable: '#a3c51c',
+ medium: '#dfb317',
+ low: '#e05d44',
+ unknown: '#9f9f9f'
+ }
+
+ def initialize(badge)
+ @entity = badge.entity
+ @status = badge.status
+ end
+
+ def key_text
+ @entity.to_s
+ end
+
+ def value_text
+ @status ? "#{@status}%" : 'unknown'
+ end
+
+ def key_width
+ 62
+ end
+
+ def value_width
+ @status ? 36 : 58
+ end
+
+ def value_color
+ case @status
+ when 95..100 then STATUS_COLOR[:good]
+ when 90..95 then STATUS_COLOR[:acceptable]
+ when 75..90 then STATUS_COLOR[:medium]
+ when 0..75 then STATUS_COLOR[:low]
+ else
+ STATUS_COLOR[:unknown]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/badge/metadata.rb b/lib/gitlab/badge/metadata.rb
new file mode 100644
index 00000000000..548f85b78bb
--- /dev/null
+++ b/lib/gitlab/badge/metadata.rb
@@ -0,0 +1,36 @@
+module Gitlab
+ module Badge
+ ##
+ # Abstract class for badge metadata
+ #
+ class Metadata
+ include Gitlab::Application.routes.url_helpers
+ include ActionView::Helpers::AssetTagHelper
+ include ActionView::Helpers::UrlHelper
+
+ def initialize(badge)
+ @badge = badge
+ end
+
+ def to_html
+ link_to(image_tag(image_url, alt: title), link_url)
+ end
+
+ def to_markdown
+ "[![#{title}](#{image_url})](#{link_url})"
+ end
+
+ def title
+ raise NotImplementedError
+ end
+
+ def image_url
+ raise NotImplementedError
+ end
+
+ def link_url
+ raise NotImplementedError
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/badge/template.rb b/lib/gitlab/badge/template.rb
new file mode 100644
index 00000000000..bfeb0052642
--- /dev/null
+++ b/lib/gitlab/badge/template.rb
@@ -0,0 +1,49 @@
+module Gitlab
+ module Badge
+ ##
+ # Abstract template class for badges
+ #
+ class Template
+ def initialize(badge)
+ @entity = badge.entity
+ @status = badge.status
+ end
+
+ def key_text
+ raise NotImplementedError
+ end
+
+ def value_text
+ raise NotImplementedError
+ end
+
+ def key_width
+ raise NotImplementedError
+ end
+
+ def value_width
+ raise NotImplementedError
+ end
+
+ def value_color
+ raise NotImplementedError
+ end
+
+ def key_color
+ '#555'
+ end
+
+ def key_text_anchor
+ key_width / 2
+ end
+
+ def value_text_anchor
+ key_width + (value_width / 2)
+ end
+
+ def width
+ key_width + value_width
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/conflict/file.rb b/lib/gitlab/conflict/file.rb
new file mode 100644
index 00000000000..0a1fd27ced5
--- /dev/null
+++ b/lib/gitlab/conflict/file.rb
@@ -0,0 +1,186 @@
+module Gitlab
+ module Conflict
+ class File
+ include Gitlab::Routing.url_helpers
+ include IconsHelper
+
+ class MissingResolution < StandardError
+ end
+
+ CONTEXT_LINES = 3
+
+ attr_reader :merge_file_result, :their_path, :our_path, :our_mode, :merge_request, :repository
+
+ def initialize(merge_file_result, conflict, merge_request:)
+ @merge_file_result = merge_file_result
+ @their_path = conflict[:theirs][:path]
+ @our_path = conflict[:ours][:path]
+ @our_mode = conflict[:ours][:mode]
+ @merge_request = merge_request
+ @repository = merge_request.project.repository
+ @match_line_headers = {}
+ end
+
+ # Array of Gitlab::Diff::Line objects
+ def lines
+ @lines ||= Gitlab::Conflict::Parser.new.parse(merge_file_result[:data],
+ our_path: our_path,
+ their_path: their_path,
+ parent_file: self)
+ end
+
+ def resolve_lines(resolution)
+ section_id = nil
+
+ lines.map do |line|
+ unless line.type
+ section_id = nil
+ next line
+ end
+
+ section_id ||= line_code(line)
+
+ case resolution[section_id]
+ when 'head'
+ next unless line.type == 'new'
+ when 'origin'
+ next unless line.type == 'old'
+ else
+ raise MissingResolution, "Missing resolution for section ID: #{section_id}"
+ end
+
+ line
+ end.compact
+ end
+
+ def highlight_lines!
+ their_file = lines.reject { |line| line.type == 'new' }.map(&:text).join("\n")
+ our_file = lines.reject { |line| line.type == 'old' }.map(&:text).join("\n")
+
+ their_highlight = Gitlab::Highlight.highlight(their_path, their_file, repository: repository).lines
+ our_highlight = Gitlab::Highlight.highlight(our_path, our_file, repository: repository).lines
+
+ lines.each do |line|
+ if line.type == 'old'
+ line.rich_text = their_highlight[line.old_line - 1].try(:html_safe)
+ else
+ line.rich_text = our_highlight[line.new_line - 1].try(:html_safe)
+ end
+ end
+ end
+
+ def sections
+ return @sections if @sections
+
+ chunked_lines = lines.chunk { |line| line.type.nil? }.to_a
+ match_line = nil
+
+ sections_count = chunked_lines.size
+
+ @sections = chunked_lines.flat_map.with_index do |(no_conflict, lines), i|
+ section = nil
+
+ # We need to reduce context sections to CONTEXT_LINES. Conflict sections are
+ # always shown in full.
+ if no_conflict
+ conflict_before = i > 0
+ conflict_after = (sections_count - i) > 1
+
+ if conflict_before && conflict_after
+ # Create a gap in a long context section.
+ if lines.length > CONTEXT_LINES * 2
+ head_lines = lines.first(CONTEXT_LINES)
+ tail_lines = lines.last(CONTEXT_LINES)
+
+ # Ensure any existing match line has text for all lines up to the last
+ # line of its context.
+ update_match_line_text(match_line, head_lines.last)
+
+ # Insert a new match line after the created gap.
+ match_line = create_match_line(tail_lines.first)
+
+ section = [
+ { conflict: false, lines: head_lines },
+ { conflict: false, lines: tail_lines.unshift(match_line) }
+ ]
+ end
+ elsif conflict_after
+ tail_lines = lines.last(CONTEXT_LINES)
+
+ # Create a gap and insert a match line at the start.
+ if lines.length > tail_lines.length
+ match_line = create_match_line(tail_lines.first)
+
+ tail_lines.unshift(match_line)
+ end
+
+ lines = tail_lines
+ elsif conflict_before
+ # We're at the end of the file (no conflicts after), so just remove extra
+ # trailing lines.
+ lines = lines.first(CONTEXT_LINES)
+ end
+ end
+
+ # We want to update the match line's text every time unless we've already
+ # created a gap and its corresponding match line.
+ update_match_line_text(match_line, lines.last) unless section
+
+ section ||= { conflict: !no_conflict, lines: lines }
+ section[:id] = line_code(lines.first) unless no_conflict
+ section
+ end
+ end
+
+ def line_code(line)
+ Gitlab::Diff::LineCode.generate(our_path, line.new_pos, line.old_pos)
+ end
+
+ def create_match_line(line)
+ Gitlab::Diff::Line.new('', 'match', line.index, line.old_pos, line.new_pos)
+ end
+
+ # Any line beginning with a letter, an underscore, or a dollar can be used in a
+ # match line header. Only context sections can contain match lines, as match lines
+ # have to exist in both versions of the file.
+ def find_match_line_header(index)
+ return @match_line_headers[index] if @match_line_headers.key?(index)
+
+ @match_line_headers[index] = begin
+ if index >= 0
+ line = lines[index]
+
+ if line.type.nil? && line.text.match(/\A[A-Za-z$_]/)
+ " #{line.text}"
+ else
+ find_match_line_header(index - 1)
+ end
+ end
+ end
+ end
+
+ # Set the match line's text for the current line. A match line takes its start
+ # position and context header (where present) from itself, and its end position from
+ # the line passed in.
+ def update_match_line_text(match_line, line)
+ return unless match_line
+
+ header = find_match_line_header(match_line.index - 1)
+
+ match_line.text = "@@ -#{match_line.old_pos},#{line.old_pos} +#{match_line.new_pos},#{line.new_pos} @@#{header}"
+ end
+
+ def as_json(opts = nil)
+ {
+ old_path: their_path,
+ new_path: our_path,
+ blob_icon: file_type_icon_class('file', our_mode, our_path),
+ blob_path: namespace_project_blob_path(merge_request.project.namespace,
+ merge_request.project,
+ ::File.join(merge_request.diff_refs.head_sha, our_path)),
+ sections: sections
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/conflict/file_collection.rb b/lib/gitlab/conflict/file_collection.rb
new file mode 100644
index 00000000000..bbd0427a2c8
--- /dev/null
+++ b/lib/gitlab/conflict/file_collection.rb
@@ -0,0 +1,57 @@
+module Gitlab
+ module Conflict
+ class FileCollection
+ class ConflictSideMissing < StandardError
+ end
+
+ attr_reader :merge_request, :our_commit, :their_commit
+
+ def initialize(merge_request)
+ @merge_request = merge_request
+ @our_commit = merge_request.source_branch_head.raw.raw_commit
+ @their_commit = merge_request.target_branch_head.raw.raw_commit
+ end
+
+ def repository
+ merge_request.project.repository
+ end
+
+ def merge_index
+ @merge_index ||= repository.rugged.merge_commits(our_commit, their_commit)
+ end
+
+ def files
+ @files ||= merge_index.conflicts.map do |conflict|
+ raise ConflictSideMissing unless conflict[:theirs] && conflict[:ours]
+
+ Gitlab::Conflict::File.new(merge_index.merge_file(conflict[:ours][:path]),
+ conflict,
+ merge_request: merge_request)
+ end
+ end
+
+ def as_json(opts = nil)
+ {
+ target_branch: merge_request.target_branch,
+ source_branch: merge_request.source_branch,
+ commit_sha: merge_request.diff_head_sha,
+ commit_message: default_commit_message,
+ files: files
+ }
+ end
+
+ def default_commit_message
+ conflict_filenames = merge_index.conflicts.map do |conflict|
+ "# #{conflict[:ours][:path]}"
+ end
+
+ <<EOM.chomp
+Merge branch '#{merge_request.target_branch}' into '#{merge_request.source_branch}'
+
+# Conflicts:
+#{conflict_filenames.join("\n")}
+EOM
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/conflict/parser.rb b/lib/gitlab/conflict/parser.rb
new file mode 100644
index 00000000000..6eccded7872
--- /dev/null
+++ b/lib/gitlab/conflict/parser.rb
@@ -0,0 +1,62 @@
+module Gitlab
+ module Conflict
+ class Parser
+ class ParserError < StandardError
+ end
+
+ class UnexpectedDelimiter < ParserError
+ end
+
+ class MissingEndDelimiter < ParserError
+ end
+
+ class UnmergeableFile < ParserError
+ end
+
+ def parse(text, our_path:, their_path:, parent_file: nil)
+ raise UnmergeableFile if text.blank? # Typically a binary file
+ raise UnmergeableFile if text.length > 102400
+
+ line_obj_index = 0
+ line_old = 1
+ line_new = 1
+ type = nil
+ lines = []
+ conflict_start = "<<<<<<< #{our_path}"
+ conflict_middle = '======='
+ conflict_end = ">>>>>>> #{their_path}"
+
+ text.each_line.map do |line|
+ full_line = line.delete("\n")
+
+ if full_line == conflict_start
+ raise UnexpectedDelimiter unless type.nil?
+
+ type = 'new'
+ elsif full_line == conflict_middle
+ raise UnexpectedDelimiter unless type == 'new'
+
+ type = 'old'
+ elsif full_line == conflict_end
+ raise UnexpectedDelimiter unless type == 'old'
+
+ type = nil
+ elsif line[0] == '\\'
+ type = 'nonewline'
+ lines << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new, parent_file: parent_file)
+ else
+ lines << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new, parent_file: parent_file)
+ line_old += 1 if type != 'new'
+ line_new += 1 if type != 'old'
+
+ line_obj_index += 1
+ end
+ end
+
+ raise MissingEndDelimiter unless type.nil?
+
+ lines
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/build_data_builder.rb b/lib/gitlab/data_builder/build.rb
index 9f45aefda0f..6548e6475c6 100644
--- a/lib/gitlab/build_data_builder.rb
+++ b/lib/gitlab/data_builder/build.rb
@@ -1,6 +1,8 @@
module Gitlab
- class BuildDataBuilder
- class << self
+ module DataBuilder
+ module Build
+ extend self
+
def build(build)
project = build.project
commit = build.pipeline
diff --git a/lib/gitlab/note_data_builder.rb b/lib/gitlab/data_builder/note.rb
index 8bdc89a7751..50fea1232af 100644
--- a/lib/gitlab/note_data_builder.rb
+++ b/lib/gitlab/data_builder/note.rb
@@ -1,6 +1,8 @@
module Gitlab
- class NoteDataBuilder
- class << self
+ module DataBuilder
+ module Note
+ extend self
+
# Produce a hash of post-receive data
#
# For all notes:
diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb
new file mode 100644
index 00000000000..06a783ebc1c
--- /dev/null
+++ b/lib/gitlab/data_builder/pipeline.rb
@@ -0,0 +1,62 @@
+module Gitlab
+ module DataBuilder
+ module Pipeline
+ extend self
+
+ def build(pipeline)
+ {
+ object_kind: 'pipeline',
+ object_attributes: hook_attrs(pipeline),
+ user: pipeline.user.try(:hook_attrs),
+ project: pipeline.project.hook_attrs(backward: false),
+ commit: pipeline.commit.try(:hook_attrs),
+ builds: pipeline.builds.map(&method(:build_hook_attrs))
+ }
+ end
+
+ def hook_attrs(pipeline)
+ {
+ id: pipeline.id,
+ ref: pipeline.ref,
+ tag: pipeline.tag,
+ sha: pipeline.sha,
+ before_sha: pipeline.before_sha,
+ status: pipeline.status,
+ stages: pipeline.stages,
+ created_at: pipeline.created_at,
+ finished_at: pipeline.finished_at,
+ duration: pipeline.duration
+ }
+ end
+
+ def build_hook_attrs(build)
+ {
+ id: build.id,
+ stage: build.stage,
+ name: build.name,
+ status: build.status,
+ created_at: build.created_at,
+ started_at: build.started_at,
+ finished_at: build.finished_at,
+ when: build.when,
+ manual: build.manual?,
+ user: build.user.try(:hook_attrs),
+ runner: build.runner && runner_hook_attrs(build.runner),
+ artifacts_file: {
+ filename: build.artifacts_file.filename,
+ size: build.artifacts_size
+ }
+ }
+ end
+
+ def runner_hook_attrs(runner)
+ {
+ id: runner.id,
+ description: runner.description,
+ active: runner.active?,
+ is_shared: runner.is_shared?
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/data_builder/push.rb
index c8f12577112..4f81863da35 100644
--- a/lib/gitlab/push_data_builder.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -1,6 +1,8 @@
module Gitlab
- class PushDataBuilder
- class << self
+ module DataBuilder
+ module Push
+ extend self
+
# Produce a hash of post-receive data
#
# data = {
diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb
index cf097e0d0de..80a146b4a5a 100644
--- a/lib/gitlab/diff/line.rb
+++ b/lib/gitlab/diff/line.rb
@@ -2,11 +2,13 @@ module Gitlab
module Diff
class Line
attr_reader :type, :index, :old_pos, :new_pos
+ attr_writer :rich_text
attr_accessor :text
- def initialize(text, type, index, old_pos, new_pos)
+ def initialize(text, type, index, old_pos, new_pos, parent_file: nil)
@text, @type, @index = text, type, index
@old_pos, @new_pos = old_pos, new_pos
+ @parent_file = parent_file
end
def self.init_from_hash(hash)
@@ -43,9 +45,25 @@ module Gitlab
type == 'old'
end
+ def rich_text
+ @parent_file.highlight_lines! if @parent_file && !@rich_text
+
+ @rich_text
+ end
+
def meta?
type == 'match' || type == 'nonewline'
end
+
+ def as_json(opts = nil)
+ {
+ type: type,
+ old_line: old_line,
+ new_line: new_line,
+ text: text,
+ rich_text: rich_text || text
+ }
+ end
end
end
end
diff --git a/lib/gitlab/downtime_check/message.rb b/lib/gitlab/downtime_check/message.rb
index 4446e921e0d..40a4815a9a0 100644
--- a/lib/gitlab/downtime_check/message.rb
+++ b/lib/gitlab/downtime_check/message.rb
@@ -1,10 +1,10 @@
module Gitlab
class DowntimeCheck
class Message
- attr_reader :path, :offline, :reason
+ attr_reader :path, :offline
- OFFLINE = "\e[32moffline\e[0m"
- ONLINE = "\e[31monline\e[0m"
+ OFFLINE = "\e[31moffline\e[0m"
+ ONLINE = "\e[32monline\e[0m"
# path - The file path of the migration.
# offline - When set to `true` the migration will require downtime.
@@ -19,10 +19,21 @@ module Gitlab
label = offline ? OFFLINE : ONLINE
message = "[#{label}]: #{path}"
- message += ": #{reason}" if reason
+
+ if reason?
+ message += ":\n\n#{reason}\n\n"
+ end
message
end
+
+ def reason?
+ @reason.present?
+ end
+
+ def reason
+ @reason.strip.lines.map(&:strip).join("\n")
+ end
end
end
end
diff --git a/lib/gitlab/email/handler/base_handler.rb b/lib/gitlab/email/handler/base_handler.rb
index b7ed11cb638..7cccf465334 100644
--- a/lib/gitlab/email/handler/base_handler.rb
+++ b/lib/gitlab/email/handler/base_handler.rb
@@ -45,6 +45,7 @@ module Gitlab
def verify_record!(record:, invalid_exception:, record_name:)
return if record.persisted?
+ return if record.errors.key?(:commands_only)
error_title = "The #{record_name} could not be created for the following reasons:"
diff --git a/lib/gitlab/import_export/json_hash_builder.rb b/lib/gitlab/import_export/json_hash_builder.rb
index 008300bde45..0cc10f40087 100644
--- a/lib/gitlab/import_export/json_hash_builder.rb
+++ b/lib/gitlab/import_export/json_hash_builder.rb
@@ -57,19 +57,16 @@ module Gitlab
# +value+ existing model to be included in the hash
# +json_config_hash+ the original hash containing the root model
def create_model_value(current_key, value, json_config_hash)
- parsed_hash = { include: value }
- parse_hash(value, parsed_hash)
-
- json_config_hash[current_key] = parsed_hash
+ json_config_hash[current_key] = parse_hash(value) || { include: value }
end
# Calls attributes finder to parse the hash and add any attributes to it
#
# +value+ existing model to be included in the hash
# +parsed_hash+ the original hash
- def parse_hash(value, parsed_hash)
+ def parse_hash(value)
@attributes_finder.parse(value) do |hash|
- parsed_hash = { include: hash_or_merge(value, hash) }
+ { include: hash_or_merge(value, hash) }
end
end
diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb
index 41fcd971c22..3d1ba33ec68 100644
--- a/lib/gitlab/metrics.rb
+++ b/lib/gitlab/metrics.rb
@@ -124,6 +124,15 @@ module Gitlab
trans.action = action if trans
end
+ # Tracks an event.
+ #
+ # See `Gitlab::Metrics::Transaction#add_event` for more details.
+ def self.add_event(*args)
+ trans = current_transaction
+
+ trans.add_event(*args) if trans
+ end
+
# Returns the prefix to use for the name of a series.
def self.series_prefix
@series_prefix ||= Sidekiq.server? ? 'sidekiq_' : 'rails_'
diff --git a/lib/gitlab/metrics/metric.rb b/lib/gitlab/metrics/metric.rb
index f23d67e1e38..bd0afe53c51 100644
--- a/lib/gitlab/metrics/metric.rb
+++ b/lib/gitlab/metrics/metric.rb
@@ -4,15 +4,20 @@ module Gitlab
class Metric
JITTER_RANGE = 0.000001..0.001
- attr_reader :series, :values, :tags
+ attr_reader :series, :values, :tags, :type
# series - The name of the series (as a String) to store the metric in.
# values - A Hash containing the values to store.
# tags - A Hash containing extra tags to add to the metrics.
- def initialize(series, values, tags = {})
+ def initialize(series, values, tags = {}, type = :metric)
@values = values
@series = series
@tags = tags
+ @type = type
+ end
+
+ def event?
+ type == :event
end
# Returns a Hash in a format that can be directly written to InfluxDB.
diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
index e61670f491c..b4493bf44d2 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -17,6 +17,10 @@ module Gitlab
begin
retval = trans.run { @app.call(env) }
+ rescue Exception => error # rubocop: disable Lint/RescueException
+ trans.add_event(:rails_exception)
+
+ raise error
# Even in the event of an error we want to submit any metrics we
# might've gathered up to this point.
ensure
diff --git a/lib/gitlab/metrics/sidekiq_middleware.rb b/lib/gitlab/metrics/sidekiq_middleware.rb
index a1240fd33ee..f9dd8e41912 100644
--- a/lib/gitlab/metrics/sidekiq_middleware.rb
+++ b/lib/gitlab/metrics/sidekiq_middleware.rb
@@ -11,6 +11,10 @@ module Gitlab
# Old gitlad-shell messages don't provide enqueued_at/created_at attributes
trans.set(:sidekiq_queue_duration, Time.now.to_f - (message['enqueued_at'] || message['created_at'] || 0))
trans.run { yield }
+ rescue Exception => error # rubocop: disable Lint/RescueException
+ trans.add_event(:sidekiq_exception)
+
+ raise error
ensure
trans.finish
end
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
index 968f3218950..7bc16181be6 100644
--- a/lib/gitlab/metrics/transaction.rb
+++ b/lib/gitlab/metrics/transaction.rb
@@ -4,7 +4,10 @@ module Gitlab
class Transaction
THREAD_KEY = :_gitlab_metrics_transaction
- attr_reader :tags, :values, :methods
+ # The series to store events (e.g. Git pushes) in.
+ EVENT_SERIES = 'events'
+
+ attr_reader :tags, :values, :method, :metrics
attr_accessor :action
@@ -55,6 +58,20 @@ module Gitlab
@metrics << Metric.new("#{Metrics.series_prefix}#{series}", values, tags)
end
+ # Tracks a business level event
+ #
+ # Business level events including events such as Git pushes, Emails being
+ # sent, etc.
+ #
+ # event_name - The name of the event (e.g. "git_push").
+ # tags - A set of tags to attach to the event.
+ def add_event(event_name, tags = {})
+ @metrics << Metric.new(EVENT_SERIES,
+ { count: 1 },
+ { event: event_name }.merge(tags),
+ :event)
+ end
+
# Returns a MethodCall object for the given name.
def method_call_for(name)
unless method = @methods[name]
@@ -101,7 +118,7 @@ module Gitlab
submit_hashes = submit.map do |metric|
hash = metric.to_hash
- hash[:tags][:action] ||= @action if @action
+ hash[:tags][:action] ||= @action if @action && !metric.event?
hash
end
diff --git a/lib/gitlab/slash_commands/command_definition.rb b/lib/gitlab/slash_commands/command_definition.rb
new file mode 100644
index 00000000000..60d35be2599
--- /dev/null
+++ b/lib/gitlab/slash_commands/command_definition.rb
@@ -0,0 +1,57 @@
+module Gitlab
+ module SlashCommands
+ class CommandDefinition
+ attr_accessor :name, :aliases, :description, :params, :condition_block, :action_block
+
+ def initialize(name, attributes = {})
+ @name = name
+
+ @aliases = attributes[:aliases] || []
+ @description = attributes[:description] || ''
+ @params = attributes[:params] || []
+ @condition_block = attributes[:condition_block]
+ @action_block = attributes[:action_block]
+ end
+
+ def all_names
+ [name, *aliases]
+ end
+
+ def noop?
+ action_block.nil?
+ end
+
+ def available?(opts)
+ return true unless condition_block
+
+ context = OpenStruct.new(opts)
+ context.instance_exec(&condition_block)
+ end
+
+ def execute(context, opts, arg)
+ return if noop? || !available?(opts)
+
+ if arg.present?
+ context.instance_exec(arg, &action_block)
+ elsif action_block.arity == 0
+ context.instance_exec(&action_block)
+ end
+ end
+
+ def to_h(opts)
+ desc = description
+ if desc.respond_to?(:call)
+ context = OpenStruct.new(opts)
+ desc = context.instance_exec(&desc) rescue ''
+ end
+
+ {
+ name: name,
+ aliases: aliases,
+ description: desc,
+ params: params
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/dsl.rb b/lib/gitlab/slash_commands/dsl.rb
new file mode 100644
index 00000000000..50b0937d267
--- /dev/null
+++ b/lib/gitlab/slash_commands/dsl.rb
@@ -0,0 +1,98 @@
+module Gitlab
+ module SlashCommands
+ module Dsl
+ extend ActiveSupport::Concern
+
+ included do
+ cattr_accessor :command_definitions, instance_accessor: false do
+ []
+ end
+
+ cattr_accessor :command_definitions_by_name, instance_accessor: false do
+ {}
+ end
+ end
+
+ class_methods do
+ # Allows to give a description to the next slash command.
+ # This description is shown in the autocomplete menu.
+ # It accepts a block that will be evaluated with the context given to
+ # `CommandDefintion#to_h`.
+ #
+ # Example:
+ #
+ # desc do
+ # "This is a dynamic description for #{noteable.to_ability_name}"
+ # end
+ # command :command_key do |arguments|
+ # # Awesome code block
+ # end
+ def desc(text = '', &block)
+ @description = block_given? ? block : text
+ end
+
+ # Allows to define params for the next slash command.
+ # These params are shown in the autocomplete menu.
+ #
+ # Example:
+ #
+ # params "~label ~label2"
+ # command :command_key do |arguments|
+ # # Awesome code block
+ # end
+ def params(*params)
+ @params = params
+ end
+
+ # Allows to define conditions that must be met in order for the command
+ # to be returned by `.command_names` & `.command_definitions`.
+ # It accepts a block that will be evaluated with the context given to
+ # `CommandDefintion#to_h`.
+ #
+ # Example:
+ #
+ # condition do
+ # project.public?
+ # end
+ # command :command_key do |arguments|
+ # # Awesome code block
+ # end
+ def condition(&block)
+ @condition_block = block
+ end
+
+ # Registers a new command which is recognizeable from body of email or
+ # comment.
+ # It accepts aliases and takes a block.
+ #
+ # Example:
+ #
+ # command :my_command, :alias_for_my_command do |arguments|
+ # # Awesome code block
+ # end
+ def command(*command_names, &block)
+ name, *aliases = command_names
+
+ definition = CommandDefinition.new(
+ name,
+ aliases: aliases,
+ description: @description,
+ params: @params,
+ condition_block: @condition_block,
+ action_block: block
+ )
+
+ self.command_definitions << definition
+
+ definition.all_names.each do |name|
+ self.command_definitions_by_name[name] = definition
+ end
+
+ @description = nil
+ @params = nil
+ @condition_block = nil
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/extractor.rb b/lib/gitlab/slash_commands/extractor.rb
new file mode 100644
index 00000000000..a672e5e4855
--- /dev/null
+++ b/lib/gitlab/slash_commands/extractor.rb
@@ -0,0 +1,122 @@
+module Gitlab
+ module SlashCommands
+ # This class takes an array of commands that should be extracted from a
+ # given text.
+ #
+ # ```
+ # extractor = Gitlab::SlashCommands::Extractor.new([:open, :assign, :labels])
+ # ```
+ class Extractor
+ attr_reader :command_definitions
+
+ def initialize(command_definitions)
+ @command_definitions = command_definitions
+ end
+
+ # Extracts commands from content and return an array of commands.
+ # The array looks like the following:
+ # [
+ # ['command1'],
+ # ['command3', 'arg1 arg2'],
+ # ]
+ # The command and the arguments are stripped.
+ # The original command text is removed from the given `content`.
+ #
+ # Usage:
+ # ```
+ # extractor = Gitlab::SlashCommands::Extractor.new([:open, :assign, :labels])
+ # msg = %(hello\n/labels ~foo ~"bar baz"\nworld)
+ # commands = extractor.extract_commands(msg) #=> [['labels', '~foo ~"bar baz"']]
+ # msg #=> "hello\nworld"
+ # ```
+ def extract_commands(content, opts = {})
+ return [content, []] unless content
+
+ content = content.dup
+
+ commands = []
+
+ content.delete!("\r")
+ content.gsub!(commands_regex(opts)) do
+ if $~[:cmd]
+ commands << [$~[:cmd], $~[:arg]].reject(&:blank?)
+ ''
+ else
+ $~[0]
+ end
+ end
+
+ [content.strip, commands]
+ end
+
+ private
+
+ # Builds a regular expression to match known commands.
+ # First match group captures the command name and
+ # second match group captures its arguments.
+ #
+ # It looks something like:
+ #
+ # /^\/(?<cmd>close|reopen|...)(?:( |$))(?<arg>[^\/\n]*)(?:\n|$)/
+ def commands_regex(opts)
+ names = command_names(opts).map(&:to_s)
+
+ @commands_regex ||= %r{
+ (?<code>
+ # Code blocks:
+ # ```
+ # Anything, including `/cmd arg` which are ignored by this filter
+ # ```
+
+ ^```
+ .+?
+ \n```$
+ )
+ |
+ (?<html>
+ # HTML block:
+ # <tag>
+ # Anything, including `/cmd arg` which are ignored by this filter
+ # </tag>
+
+ ^<[^>]+?>\n
+ .+?
+ \n<\/[^>]+?>$
+ )
+ |
+ (?<html>
+ # Quote block:
+ # >>>
+ # Anything, including `/cmd arg` which are ignored by this filter
+ # >>>
+
+ ^>>>
+ .+?
+ \n>>>$
+ )
+ |
+ (?:
+ # Command not in a blockquote, blockcode, or HTML tag:
+ # /close
+
+ ^\/
+ (?<cmd>#{Regexp.union(names)})
+ (?:
+ [ ]
+ (?<arg>[^\/\n]*)
+ )?
+ (?:\n|$)
+ )
+ }mx
+ end
+
+ def command_names(opts)
+ command_definitions.flat_map do |command|
+ next if command.noop?
+
+ command.all_names
+ end.compact
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/template/base_template.rb b/lib/gitlab/template/base_template.rb
index 760ff3e614a..7ebec8e2cff 100644
--- a/lib/gitlab/template/base_template.rb
+++ b/lib/gitlab/template/base_template.rb
@@ -1,8 +1,9 @@
module Gitlab
module Template
class BaseTemplate
- def initialize(path)
+ def initialize(path, project = nil)
@path = path
+ @finder = self.class.finder(project)
end
def name
@@ -10,23 +11,32 @@ module Gitlab
end
def content
- File.read(@path)
+ @finder.read(@path)
+ end
+
+ def to_json
+ { name: name, content: content }
end
class << self
- def all
- self.categories.keys.flat_map { |cat| by_category(cat) }
+ def all(project = nil)
+ if categories.any?
+ categories.keys.flat_map { |cat| by_category(cat, project) }
+ else
+ by_category("", project)
+ end
end
- def find(key)
- file_name = "#{key}#{self.extension}"
-
- directory = select_directory(file_name)
- directory ? new(File.join(category_directory(directory), file_name)) : nil
+ def find(key, project = nil)
+ path = self.finder(project).find(key)
+ path.present? ? new(path, project) : nil
end
+ # Set categories as sub directories
+ # Example: { "category_name_1" => "directory_path_1", "category_name_2" => "directory_name_2" }
+ # Default is no category with all files in base dir of each class
def categories
- raise NotImplementedError
+ {}
end
def extension
@@ -37,29 +47,40 @@ module Gitlab
raise NotImplementedError
end
- def by_category(category)
- templates_for_directory(category_directory(category))
+ # Defines which strategy will be used to get templates files
+ # RepoTemplateFinder - Finds templates on project repository, templates are filtered perproject
+ # GlobalTemplateFinder - Finds templates on gitlab installation source, templates can be used in all projects
+ def finder(project = nil)
+ raise NotImplementedError
end
- def category_directory(category)
- File.join(base_dir, categories[category])
+ def by_category(category, project = nil)
+ directory = category_directory(category)
+ files = finder(project).list_files_for(directory)
+
+ files.map { |f| new(f, project) }
end
- private
+ def category_directory(category)
+ return base_dir unless category.present?
- def select_directory(file_name)
- categories.keys.find do |category|
- File.exist?(File.join(category_directory(category), file_name))
- end
+ File.join(base_dir, categories[category])
end
- def templates_for_directory(dir)
- dir << '/' unless dir.end_with?('/')
- Dir.glob(File.join(dir, "*#{self.extension}")).select { |f| f =~ filter_regex }.map { |f| new(f) }
- end
+ # If template is organized by category it returns { category_name: [{ name: template_name }, { name: template2_name }] }
+ # If no category is present returns [{ name: template_name }, { name: template2_name}]
+ def dropdown_names(project = nil)
+ return [] if project && !project.repository.exists?
- def filter_regex
- @filter_reges ||= /#{Regexp.escape(extension)}\z/
+ if categories.any?
+ categories.keys.map do |category|
+ files = self.by_category(category, project)
+ [category, files.map { |t| { name: t.name } }]
+ end.to_h
+ else
+ files = self.all(project)
+ files.map { |t| { name: t.name } }
+ end
end
end
end
diff --git a/lib/gitlab/template/finders/base_template_finder.rb b/lib/gitlab/template/finders/base_template_finder.rb
new file mode 100644
index 00000000000..473b05257c6
--- /dev/null
+++ b/lib/gitlab/template/finders/base_template_finder.rb
@@ -0,0 +1,35 @@
+module Gitlab
+ module Template
+ module Finders
+ class BaseTemplateFinder
+ def initialize(base_dir)
+ @base_dir = base_dir
+ end
+
+ def list_files_for
+ raise NotImplementedError
+ end
+
+ def read
+ raise NotImplementedError
+ end
+
+ def find
+ raise NotImplementedError
+ end
+
+ def category_directory(category)
+ return @base_dir unless category.present?
+
+ @base_dir + @categories[category]
+ end
+
+ class << self
+ def filter_regex(extension)
+ /#{Regexp.escape(extension)}\z/
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/template/finders/global_template_finder.rb b/lib/gitlab/template/finders/global_template_finder.rb
new file mode 100644
index 00000000000..831da45191f
--- /dev/null
+++ b/lib/gitlab/template/finders/global_template_finder.rb
@@ -0,0 +1,38 @@
+# Searches and reads file present on Gitlab installation directory
+module Gitlab
+ module Template
+ module Finders
+ class GlobalTemplateFinder < BaseTemplateFinder
+ def initialize(base_dir, extension, categories = {})
+ @categories = categories
+ @extension = extension
+ super(base_dir)
+ end
+
+ def read(path)
+ File.read(path)
+ end
+
+ def find(key)
+ file_name = "#{key}#{@extension}"
+
+ directory = select_directory(file_name)
+ directory ? File.join(category_directory(directory), file_name) : nil
+ end
+
+ def list_files_for(dir)
+ dir << '/' unless dir.end_with?('/')
+ Dir.glob(File.join(dir, "*#{@extension}")).select { |f| f =~ self.class.filter_regex(@extension) }
+ end
+
+ private
+
+ def select_directory(file_name)
+ @categories.keys.find do |category|
+ File.exist?(File.join(category_directory(category), file_name))
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/template/finders/repo_template_finder.rb b/lib/gitlab/template/finders/repo_template_finder.rb
new file mode 100644
index 00000000000..22c39436cb2
--- /dev/null
+++ b/lib/gitlab/template/finders/repo_template_finder.rb
@@ -0,0 +1,59 @@
+# Searches and reads files present on each Gitlab project repository
+module Gitlab
+ module Template
+ module Finders
+ class RepoTemplateFinder < BaseTemplateFinder
+ # Raised when file is not found
+ class FileNotFoundError < StandardError; end
+
+ def initialize(project, base_dir, extension, categories = {})
+ @categories = categories
+ @extension = extension
+ @repository = project.repository
+ @commit = @repository.head_commit if @repository.exists?
+
+ super(base_dir)
+ end
+
+ def read(path)
+ blob = @repository.blob_at(@commit.id, path) if @commit
+ raise FileNotFoundError if blob.nil?
+ blob.data
+ end
+
+ def find(key)
+ file_name = "#{key}#{@extension}"
+ directory = select_directory(file_name)
+ raise FileNotFoundError if directory.nil?
+
+ category_directory(directory) + file_name
+ end
+
+ def list_files_for(dir)
+ return [] unless @commit
+
+ dir << '/' unless dir.end_with?('/')
+
+ entries = @repository.tree(:head, dir).entries
+
+ names = entries.map(&:name)
+ names.select { |f| f =~ self.class.filter_regex(@extension) }
+ end
+
+ private
+
+ def select_directory(file_name)
+ return [] unless @commit
+
+ # Insert root as directory
+ directories = ["", @categories.keys]
+
+ directories.find do |category|
+ path = category_directory(category) + file_name
+ @repository.blob_at(@commit.id, path)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/template/gitignore.rb b/lib/gitlab/template/gitignore_template.rb
index 964fbfd4de3..8d2a9d2305c 100644
--- a/lib/gitlab/template/gitignore.rb
+++ b/lib/gitlab/template/gitignore_template.rb
@@ -1,6 +1,6 @@
module Gitlab
module Template
- class Gitignore < BaseTemplate
+ class GitignoreTemplate < BaseTemplate
class << self
def extension
'.gitignore'
@@ -16,6 +16,10 @@ module Gitlab
def base_dir
Rails.root.join('vendor/gitignore')
end
+
+ def finder(project = nil)
+ Gitlab::Template::Finders::GlobalTemplateFinder.new(self.base_dir, self.extension, self.categories)
+ end
end
end
end
diff --git a/lib/gitlab/template/gitlab_ci_yml.rb b/lib/gitlab/template/gitlab_ci_yml_template.rb
index 7f480fe33c0..8d1a1ed54c9 100644
--- a/lib/gitlab/template/gitlab_ci_yml.rb
+++ b/lib/gitlab/template/gitlab_ci_yml_template.rb
@@ -1,6 +1,6 @@
module Gitlab
module Template
- class GitlabCiYml < BaseTemplate
+ class GitlabCiYmlTemplate < BaseTemplate
def content
explanation = "# This file is a template, and might need editing before it works on your project."
[explanation, super].join("\n")
@@ -21,6 +21,10 @@ module Gitlab
def base_dir
Rails.root.join('vendor/gitlab-ci-yml')
end
+
+ def finder(project = nil)
+ Gitlab::Template::Finders::GlobalTemplateFinder.new(self.base_dir, self.extension, self.categories)
+ end
end
end
end
diff --git a/lib/gitlab/template/issue_template.rb b/lib/gitlab/template/issue_template.rb
new file mode 100644
index 00000000000..c6fa8d3eafc
--- /dev/null
+++ b/lib/gitlab/template/issue_template.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Template
+ class IssueTemplate < BaseTemplate
+ class << self
+ def extension
+ '.md'
+ end
+
+ def base_dir
+ '.gitlab/issue_templates/'
+ end
+
+ def finder(project)
+ Gitlab::Template::Finders::RepoTemplateFinder.new(project, self.base_dir, self.extension, self.categories)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/template/merge_request_template.rb b/lib/gitlab/template/merge_request_template.rb
new file mode 100644
index 00000000000..f826c02f3b5
--- /dev/null
+++ b/lib/gitlab/template/merge_request_template.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Template
+ class MergeRequestTemplate < BaseTemplate
+ class << self
+ def extension
+ '.md'
+ end
+
+ def base_dir
+ '.gitlab/merge_request_templates/'
+ end
+
+ def finder(project)
+ Gitlab::Template::Finders::RepoTemplateFinder.new(project, self.base_dir, self.extension, self.categories)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb
index c55a7fc4d3d..9858d2e7d83 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -32,7 +32,7 @@ module Gitlab
if project.protected_branch?(ref)
return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user)
- access_levels = project.protected_branches.matching(ref).map(&:push_access_level)
+ access_levels = project.protected_branches.matching(ref).map(&:push_access_levels).flatten
access_levels.any? { |access_level| access_level.check_access(user) }
else
user.can?(:push_code, project)
@@ -43,7 +43,7 @@ module Gitlab
return false unless user
if project.protected_branch?(ref)
- access_levels = project.protected_branches.matching(ref).map(&:merge_access_level)
+ access_levels = project.protected_branches.matching(ref).map(&:merge_access_levels).flatten
access_levels.any? { |access_level| access_level.check_access(user) }
else
user.can?(:push_code, project)