Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorLin Jen-Shin <godfat@godfat.org>2017-03-07 11:55:03 +0300
committerLin Jen-Shin <godfat@godfat.org>2017-03-07 11:55:03 +0300
commitfb167787f2c470649195a5d623f489ec2c3a9117 (patch)
treead60222b53a11ee2b29131f57e24ba66dd823c3d /lib
parent7c9cff3a744a64848207bfde1aedcfe652f07dce (diff)
parent24f1ee5e9b1f4d9bc8cff581419b091756da8deb (diff)
Merge remote-tracking branch 'upstream/master' into set-default-cache-key-for-jobs
* upstream/master: (289 commits) re-add Assign to Me link on new MR/Issue forms thinner bottom header border make header match old 16px padding of body contents Update font-awesome-rails to 4.7.0.1 Relax font-awesome-rails dependency to ~> 4.7 Restore keyboard shortcuts for "Activity" and "Charts" fix border radius bottom for header match padding for mr-widget sections Update changelog Fix project-last-commit alignment Docs: update GL Pages IP on GL.com Fix up @DouweM review Remove readme-only project view preference Add `uploads` to known models for Import/Export spec Add `has_many` associations for models that can have Upload records Handle relative and absolute Upload paths in the Uploaders Change the default CarrierWave root path for tests Fix Projects::UploadService spec Add a Project::UploadsController spec to ensure an Upload is created Add `RecordsUploads` module to record Upload records via callbacks ...
Diffstat (limited to 'lib')
-rw-r--r--lib/api/api.rb11
-rw-r--r--lib/api/commits.rb2
-rw-r--r--lib/api/deployments.rb2
-rw-r--r--lib/api/entities.rb70
-rw-r--r--lib/api/groups.rb6
-rw-r--r--lib/api/helpers.rb27
-rw-r--r--lib/api/issues.rb12
-rw-r--r--lib/api/jobs.rb (renamed from lib/api/builds.rb)130
-rw-r--r--lib/api/merge_requests.rb12
-rw-r--r--lib/api/milestones.rb15
-rw-r--r--lib/api/pipelines.rb4
-rw-r--r--lib/api/project_snippets.rb16
-rw-r--r--lib/api/projects.rb15
-rw-r--r--lib/api/services.rb10
-rw-r--r--lib/api/settings.rb10
-rw-r--r--lib/api/snippets.rb16
-rw-r--r--lib/api/triggers.rb76
-rw-r--r--lib/api/v3/builds.rb263
-rw-r--r--lib/api/v3/commits.rb4
-rw-r--r--lib/api/v3/deployments.rb43
-rw-r--r--lib/api/v3/entities.rb125
-rw-r--r--lib/api/v3/environments.rb62
-rw-r--r--lib/api/v3/groups.rb147
-rw-r--r--lib/api/v3/merge_request_diffs.rb43
-rw-r--r--lib/api/v3/merge_requests.rb8
-rw-r--r--lib/api/v3/milestones.rb64
-rw-r--r--lib/api/v3/pipelines.rb36
-rw-r--r--lib/api/v3/project_hooks.rb106
-rw-r--r--lib/api/v3/services.rb68
-rw-r--r--lib/api/v3/settings.rb137
-rw-r--r--lib/api/v3/snippets.rb138
-rw-r--r--lib/api/v3/triggers.rb77
-rw-r--r--lib/backup/manager.rb3
-rw-r--r--lib/backup/repository.rb5
-rw-r--r--lib/banzai/filter/emoji_filter.rb64
-rw-r--r--lib/banzai/filter/sanitization_filter.rb4
-rw-r--r--lib/banzai/filter/user_reference_filter.rb2
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb2
-rw-r--r--lib/gitlab/auth.rb21
-rw-r--r--lib/gitlab/auth/too_many_ips.rb17
-rw-r--r--lib/gitlab/auth/unique_ips_limiter.rb43
-rw-r--r--lib/gitlab/award_emoji.rb84
-rw-r--r--lib/gitlab/ci/config/entry/job.rb11
-rw-r--r--lib/gitlab/ci/status/build/play.rb12
-rw-r--r--lib/gitlab/ci/status/build/stop.rb12
-rw-r--r--lib/gitlab/ci/status/manual.rb19
-rw-r--r--lib/gitlab/data_builder/pipeline.rb2
-rw-r--r--lib/gitlab/database.rb6
-rw-r--r--lib/gitlab/emoji.rb43
-rw-r--r--lib/gitlab/etag_caching/middleware.rb66
-rw-r--r--lib/gitlab/etag_caching/store.rb32
-rw-r--r--lib/gitlab/gon_helper.rb3
-rw-r--r--lib/gitlab/request_context.rb21
-rw-r--r--lib/gitlab/seeder.rb19
-rw-r--r--lib/gitlab/sidekiq_status.rb35
-rw-r--r--lib/gitlab/visibility_level.rb39
-rw-r--r--lib/gitlab/workhorse.rb13
-rw-r--r--lib/mattermost/client.rb2
-rw-r--r--lib/mattermost/session.rb2
-rw-r--r--lib/mattermost/team.rb13
-rw-r--r--lib/support/nginx/gitlab-ssl3
-rw-r--r--lib/tasks/gemojione.rake88
62 files changed, 2009 insertions, 432 deletions
diff --git a/lib/api/api.rb b/lib/api/api.rb
index b27ac3f1d15..8dbe8875fe8 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -9,6 +9,7 @@ module API
mount ::API::V3::Boards
mount ::API::V3::Branches
mount ::API::V3::BroadcastMessages
+ mount ::API::V3::Builds
mount ::API::V3::Commits
mount ::API::V3::DeployKeys
mount ::API::V3::Environments
@@ -20,12 +21,16 @@ module API
mount ::API::V3::MergeRequestDiffs
mount ::API::V3::MergeRequests
mount ::API::V3::Notes
+ mount ::API::V3::Pipelines
mount ::API::V3::ProjectHooks
+ mount ::API::V3::Milestones
mount ::API::V3::Projects
mount ::API::V3::ProjectSnippets
mount ::API::V3::Repositories
mount ::API::V3::Runners
mount ::API::V3::Services
+ mount ::API::V3::Settings
+ mount ::API::V3::Snippets
mount ::API::V3::Subscriptions
mount ::API::V3::SystemHooks
mount ::API::V3::Tags
@@ -56,6 +61,10 @@ module API
error! e.message, e.status, e.headers
end
+ rescue_from Gitlab::Auth::TooManyIps do |e|
+ rack_response({ 'message' => '403 Forbidden' }.to_json, 403)
+ end
+
rescue_from :all do |exception|
handle_api_exception(exception)
end
@@ -73,7 +82,6 @@ module API
mount ::API::Boards
mount ::API::Branches
mount ::API::BroadcastMessages
- mount ::API::Builds
mount ::API::Commits
mount ::API::CommitStatuses
mount ::API::DeployKeys
@@ -83,6 +91,7 @@ module API
mount ::API::Groups
mount ::API::Internal
mount ::API::Issues
+ mount ::API::Jobs
mount ::API::Keys
mount ::API::Labels
mount ::API::Lint
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index fd03e92264d..b0aa10f8bf2 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -127,7 +127,7 @@ module API
commit_params = {
commit: commit,
- create_merge_request: false,
+ start_branch: params[:branch],
target_branch: params[:branch]
}
diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb
index c5feb49b22f..2f1ad12c38c 100644
--- a/lib/api/deployments.rb
+++ b/lib/api/deployments.rb
@@ -1,5 +1,5 @@
module API
- # Deployments RESTfull API endpoints
+ # Deployments RESTful API endpoints
class Deployments < Grape::API
include PaginationParams
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 9dccaff369e..2230aa0706b 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -49,7 +49,8 @@ module API
class ProjectHook < Hook
expose :project_id, :issues_events, :merge_requests_events
- expose :note_events, :build_events, :pipeline_events, :wiki_page_events
+ expose :note_events, :pipeline_events, :wiki_page_events
+ expose :build_events, as: :job_events
end
class BasicProjectDetails < Grape::Entity
@@ -69,9 +70,8 @@ module API
class Project < Grape::Entity
expose :id, :description, :default_branch, :tag_list
- expose :public?, as: :public
expose :archived?, as: :archived
- expose :visibility_level, :ssh_url_to_repo, :http_url_to_repo, :web_url
+ expose :visibility, :ssh_url_to_repo, :http_url_to_repo, :web_url
expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group }
expose :name, :name_with_namespace
expose :path, :path_with_namespace
@@ -81,7 +81,7 @@ module API
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(:jobs_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
@@ -94,7 +94,7 @@ module API
expose :star_count, :forks_count
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 :public_builds, as: :public_jobs
expose :shared_with_groups do |project, options|
SharedGroup.represent(project.project_group_links.all, options)
end
@@ -110,7 +110,7 @@ module API
expose :storage_size
expose :repository_size
expose :lfs_objects_size
- expose :build_artifacts_size
+ expose :build_artifacts_size, as: :job_artifacts_size
end
class Member < UserBasic
@@ -132,7 +132,7 @@ module API
end
class Group < Grape::Entity
- expose :id, :name, :path, :description, :visibility_level
+ expose :id, :name, :path, :description, :visibility
expose :lfs_enabled?, as: :lfs_enabled
expose :avatar_url
expose :web_url
@@ -145,7 +145,7 @@ module API
expose :storage_size
expose :repository_size
expose :lfs_objects_size
- expose :build_artifacts_size
+ expose :build_artifacts_size, as: :job_artifacts_size
end
end
end
@@ -250,14 +250,11 @@ module API
expose :start_date
end
- class Issue < ProjectEntity
+ class IssueBasic < ProjectEntity
expose :label_names, as: :labels
expose :milestone, using: Entities::Milestone
expose :assignee, :author, using: Entities::UserBasic
- expose :subscribed do |issue, options|
- issue.subscribed?(options[:current_user], options[:project] || issue.project)
- end
expose :user_notes_count
expose :upvotes, :downvotes
expose :due_date
@@ -268,6 +265,12 @@ module API
end
end
+ class Issue < IssueBasic
+ expose :subscribed do |issue, options|
+ issue.subscribed?(options[:current_user], options[:project] || issue.project)
+ end
+ end
+
class IssuableTimeStats < Grape::Entity
expose :time_estimate
expose :total_time_spent
@@ -280,7 +283,7 @@ module API
expose :id
end
- class MergeRequest < ProjectEntity
+ class MergeRequestBasic < ProjectEntity
expose :target_branch, :source_branch
expose :upvotes, :downvotes
expose :author, :assignee, using: Entities::UserBasic
@@ -292,9 +295,6 @@ module API
expose :merge_status
expose :diff_head_sha, as: :sha
expose :merge_commit_sha
- expose :subscribed do |merge_request, options|
- merge_request.subscribed?(options[:current_user], options[:project])
- end
expose :user_notes_count
expose :should_remove_source_branch?, as: :should_remove_source_branch
expose :force_remove_source_branch?, as: :force_remove_source_branch
@@ -304,6 +304,12 @@ module API
end
end
+ class MergeRequest < MergeRequestBasic
+ expose :subscribed do |merge_request, options|
+ merge_request.subscribed?(options[:current_user], options[:project])
+ end
+ end
+
class MergeRequestChanges < MergeRequest
expose :diffs, as: :changes, using: Entities::RepoDiff do |compare, _|
compare.raw_diffs(all_diffs: true).to_a
@@ -449,7 +455,8 @@ module API
class ProjectService < Grape::Entity
expose :id, :title, :created_at, :updated_at, :active
expose :push_events, :issues_events, :merge_requests_events
- expose :tag_push_events, :note_events, :build_events, :pipeline_events
+ expose :tag_push_events, :note_events, :pipeline_events
+ expose :build_events, as: :job_events
# Expose serialized properties
expose :properties do |service, options|
field_names = service.fields.
@@ -552,12 +559,14 @@ module API
expose :updated_at
expose :home_page_url
expose :default_branch_protection
- expose :restricted_visibility_levels
+ expose(:restricted_visibility_levels) do |setting, _options|
+ setting.restricted_visibility_levels.map { |level| Gitlab::VisibilityLevel.string_level(level) }
+ end
expose :max_attachment_size
expose :session_expire_delay
- expose :default_project_visibility
- expose :default_snippet_visibility
- expose :default_group_visibility
+ expose(:default_project_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_project_visibility) }
+ expose(:default_snippet_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_snippet_visibility) }
+ expose(:default_group_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_group_visibility) }
expose :default_artifacts_expire_in
expose :domain_whitelist
expose :domain_blacklist_enabled
@@ -591,10 +600,6 @@ module API
end
end
- class TriggerRequest < Grape::Entity
- expose :id, :variables
- end
-
class Runner < Grape::Entity
expose :id
expose :description
@@ -623,7 +628,7 @@ module API
expose :id, :token
end
- class BuildArtifactFile < Grape::Entity
+ class JobArtifactFile < Grape::Entity
expose :filename, :size
end
@@ -631,18 +636,21 @@ module API
expose :id, :sha, :ref, :status
end
- class Build < Grape::Entity
+ class Job < Grape::Entity
expose :id, :status, :stage, :name, :ref, :tag, :coverage
expose :created_at, :started_at, :finished_at
expose :user, with: User
- expose :artifacts_file, using: BuildArtifactFile, if: -> (build, opts) { build.artifacts? }
+ expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? }
expose :commit, with: RepoCommit
expose :runner, with: Runner
expose :pipeline, with: PipelineBasic
end
class Trigger < Grape::Entity
- expose :token, :created_at, :updated_at, :deleted_at, :last_used
+ expose :id
+ expose :token, :description
+ expose :created_at, :updated_at, :deleted_at, :last_used
+ expose :owner, using: Entities::UserBasic
end
class Variable < Grape::Entity
@@ -663,14 +671,14 @@ module API
end
class Environment < EnvironmentBasic
- expose :project, using: Entities::Project
+ expose :project, using: Entities::BasicProjectDetails
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
+ expose :deployable, using: Entities::Job
end
class RepoLicense < Grape::Entity
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 9cffd6180ae..b862ff70b31 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -7,7 +7,7 @@ module API
helpers do
params :optional_params do
optional :description, type: String, desc: 'The description of the group'
- optional :visibility_level, type: Integer, desc: 'The visibility level of the group'
+ optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the group'
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
@@ -92,7 +92,7 @@ module API
optional :name, type: String, desc: 'The name of the group'
optional :path, type: String, desc: 'The path of the group'
use :optional_params
- at_least_one_of :name, :path, :description, :visibility_level,
+ at_least_one_of :name, :path, :description, :visibility,
:lfs_enabled, :request_access_enabled
end
put ':id' do
@@ -126,7 +126,7 @@ module API
end
params do
optional :archived, type: Boolean, default: false, desc: 'Limit by archived status'
- optional :visibility, type: String, values: %w[public internal private],
+ optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values,
desc: 'Limit by visibility'
optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at],
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 4600abc7dc7..f325f0a3050 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -252,6 +252,10 @@ module API
# project helpers
def filter_projects(projects)
+ if params[:membership]
+ projects = projects.merge(current_user.authorized_projects)
+ end
+
if params[:owned]
projects = projects.merge(current_user.owned_projects)
end
@@ -332,16 +336,17 @@ module API
def initial_current_user
return @initial_current_user if defined?(@initial_current_user)
+ Gitlab::Auth::UniqueIpsLimiter.limit_user! do
+ @initial_current_user ||= find_user_by_private_token(scopes: @scopes)
+ @initial_current_user ||= doorkeeper_guard(scopes: @scopes)
+ @initial_current_user ||= find_user_from_warden
- @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?
+ @initial_current_user = nil
+ end
- unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed?
- @initial_current_user = nil
+ @initial_current_user
end
-
- @initial_current_user
end
def sudo!
@@ -384,14 +389,6 @@ module API
header(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format))
end
- def issue_entity(project)
- if project.has_external_issue_tracker?
- Entities::ExternalIssue
- else
- Entities::Issue
- end
- end
-
# The Grape Error Middleware only has access to env but no params. We workaround this by
# defining a method that returns the right value.
def define_params_for_grape_middleware
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 1d6d0b05750..bda74069ad5 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -41,7 +41,7 @@ module API
resource :issues do
desc "Get currently authenticated user's issues" do
- success Entities::Issue
+ success Entities::IssueBasic
end
params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
@@ -51,7 +51,7 @@ module API
get do
issues = find_issues(scope: 'authored')
- present paginate(issues), with: Entities::Issue, current_user: current_user
+ present paginate(issues), with: Entities::IssueBasic, current_user: current_user
end
end
@@ -60,7 +60,7 @@ module API
end
resource :groups do
desc 'Get a list of group issues' do
- success Entities::Issue
+ success Entities::IssueBasic
end
params do
optional :state, type: String, values: %w[opened closed all], default: 'opened',
@@ -72,7 +72,7 @@ module API
issues = find_issues(group_id: group.id, state: params[:state] || 'opened')
- present paginate(issues), with: Entities::Issue, current_user: current_user
+ present paginate(issues), with: Entities::IssueBasic, current_user: current_user
end
end
@@ -83,7 +83,7 @@ module API
include TimeTrackingEndpoints
desc 'Get a list of project issues' do
- success Entities::Issue
+ success Entities::IssueBasic
end
params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
@@ -95,7 +95,7 @@ module API
issues = find_issues(project_id: project.id)
- present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project
+ present paginate(issues), with: Entities::IssueBasic, current_user: current_user, project: user_project
end
desc 'Get a single project issue' do
diff --git a/lib/api/builds.rb b/lib/api/jobs.rb
index 5b76913fe45..33c05e8aa63 100644
--- a/lib/api/builds.rb
+++ b/lib/api/jobs.rb
@@ -1,5 +1,5 @@
module API
- class Builds < Grape::API
+ class Jobs < Grape::API
include PaginationParams
before { authenticate! }
@@ -13,9 +13,10 @@ module API
optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show',
values: ::CommitStatus::AVAILABLE_STATUSES,
coerce_with: ->(scope) {
- if scope.is_a?(String)
+ case scope
+ when String
[scope]
- elsif scope.is_a?(Hashie::Mash)
+ when Hashie::Mash
scope.values
else
['unknown']
@@ -24,79 +25,58 @@ module API
end
end
- desc 'Get a project builds' do
- success Entities::Build
+ desc 'Get a projects jobs' do
+ success Entities::Job
end
params do
use :optional_scope
use :pagination
end
- get ':id/builds' do
+ get ':id/jobs' do
builds = user_project.builds.order('id DESC')
builds = filter_builds(builds, params[:scope])
- present paginate(builds), with: Entities::Build,
+ present paginate(builds), with: Entities::Job,
user_can_download_artifacts: can?(current_user, :read_build, user_project)
end
- desc 'Get builds for a specific commit of a project' do
- success Entities::Build
+ desc 'Get a specific job of a project' do
+ success Entities::Job
end
params do
- requires :sha, type: String, desc: 'The SHA id of a commit'
- use :optional_scope
- use :pagination
- end
- get ':id/repository/commits/:sha/builds' do
- authorize_read_builds!
-
- return not_found! unless user_project.commit(params[:sha])
-
- pipelines = user_project.pipelines.where(sha: params[:sha])
- builds = user_project.builds.where(pipeline: pipelines).order('id DESC')
- builds = filter_builds(builds, params[:scope])
-
- present paginate(builds), with: Entities::Build,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
- end
-
- desc 'Get a specific build of a project' do
- success Entities::Build
- end
- params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
+ requires :job_id, type: Integer, desc: 'The ID of a job'
end
- get ':id/builds/:build_id' do
+ get ':id/jobs/:job_id' do
authorize_read_builds!
- build = get_build!(params[:build_id])
+ build = get_build!(params[:job_id])
- present build, with: Entities::Build,
+ present build, with: Entities::Job,
user_can_download_artifacts: can?(current_user, :read_build, user_project)
end
- desc 'Download the artifacts file from build' do
+ desc 'Download the artifacts file from a job' do
detail 'This feature was introduced in GitLab 8.5'
end
params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
+ requires :job_id, type: Integer, desc: 'The ID of a job'
end
- get ':id/builds/:build_id/artifacts' do
+ get ':id/jobs/:job_id/artifacts' do
authorize_read_builds!
- build = get_build!(params[:build_id])
+ build = get_build!(params[:job_id])
present_artifacts!(build.artifacts_file)
end
- desc 'Download the artifacts file from build' do
+ desc 'Download the artifacts file from a job' do
detail 'This feature was introduced in GitLab 8.10'
end
params do
requires :ref_name, type: String, desc: 'The ref from repository'
- requires :job, type: String, desc: 'The name for the build'
+ requires :job, type: String, desc: 'The name for the job'
end
- get ':id/builds/artifacts/:ref_name/download',
+ get ':id/jobs/artifacts/:ref_name/download',
requirements: { ref_name: /.+/ } do
authorize_read_builds!
@@ -109,14 +89,14 @@ module API
# TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace
# is saved in the DB instead of file). But before that, we need to consider how to replace the value of
# `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse.
- desc 'Get a trace of a specific build of a project'
+ desc 'Get a trace of a specific job of a project'
params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
+ requires :job_id, type: Integer, desc: 'The ID of a job'
end
- get ':id/builds/:build_id/trace' do
+ get ':id/jobs/:job_id/trace' do
authorize_read_builds!
- build = get_build!(params[:build_id])
+ build = get_build!(params[:job_id])
header 'Content-Disposition', "infile; filename=\"#{build.id}.log\""
content_type 'text/plain'
@@ -126,95 +106,95 @@ module API
body trace
end
- desc 'Cancel a specific build of a project' do
- success Entities::Build
+ desc 'Cancel a specific job of a project' do
+ success Entities::Job
end
params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
+ requires :job_id, type: Integer, desc: 'The ID of a job'
end
- post ':id/builds/:build_id/cancel' do
+ post ':id/jobs/:job_id/cancel' do
authorize_update_builds!
- build = get_build!(params[:build_id])
+ build = get_build!(params[:job_id])
build.cancel
- present build, with: Entities::Build,
+ present build, with: Entities::Job,
user_can_download_artifacts: can?(current_user, :read_build, user_project)
end
desc 'Retry a specific build of a project' do
- success Entities::Build
+ success Entities::Job
end
params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
+ requires :job_id, type: Integer, desc: 'The ID of a build'
end
- post ':id/builds/:build_id/retry' do
+ post ':id/jobs/:job_id/retry' do
authorize_update_builds!
- build = get_build!(params[:build_id])
- return forbidden!('Build is not retryable') unless build.retryable?
+ build = get_build!(params[:job_id])
+ return forbidden!('Job is not retryable') unless build.retryable?
build = Ci::Build.retry(build, current_user)
- present build, with: Entities::Build,
+ present build, with: Entities::Job,
user_can_download_artifacts: can?(current_user, :read_build, user_project)
end
- desc 'Erase build (remove artifacts and build trace)' do
- success Entities::Build
+ desc 'Erase job (remove artifacts and the trace)' do
+ success Entities::Job
end
params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
+ requires :job_id, type: Integer, desc: 'The ID of a build'
end
- post ':id/builds/:build_id/erase' do
+ post ':id/jobs/:job_id/erase' do
authorize_update_builds!
- build = get_build!(params[:build_id])
- return forbidden!('Build is not erasable!') unless build.erasable?
+ build = get_build!(params[:job_id])
+ return forbidden!('Job is not erasable!') unless build.erasable?
build.erase(erased_by: current_user)
- present build, with: Entities::Build,
+ present build, with: Entities::Job,
user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
end
desc 'Keep the artifacts to prevent them from being deleted' do
- success Entities::Build
+ success Entities::Job
end
params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
+ requires :job_id, type: Integer, desc: 'The ID of a job'
end
- post ':id/builds/:build_id/artifacts/keep' do
+ post ':id/jobs/:job_id/artifacts/keep' do
authorize_update_builds!
- build = get_build!(params[:build_id])
+ build = get_build!(params[:job_id])
return not_found!(build) unless build.artifacts?
build.keep_artifacts!
status 200
- present build, with: Entities::Build,
+ present build, with: Entities::Job,
user_can_download_artifacts: can?(current_user, :read_build, user_project)
end
- desc 'Trigger a manual build' do
- success Entities::Build
+ desc 'Trigger a manual job' do
+ success Entities::Job
detail 'This feature was added in GitLab 8.11'
end
params do
- requires :build_id, type: Integer, desc: 'The ID of a Build'
+ requires :job_id, type: Integer, desc: 'The ID of a Job'
end
- post ":id/builds/:build_id/play" do
+ post ":id/jobs/:job_id/play" do
authorize_read_builds!
- build = get_build!(params[:build_id])
+ build = get_build!(params[:job_id])
bad_request!("Unplayable Job") unless build.playable?
build.play(current_user)
status 200
- present build, with: Entities::Build,
+ present build, with: Entities::Job,
user_can_download_artifacts: can?(current_user, :read_build, user_project)
end
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 4638a66811d..6fc33a7a54a 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -25,6 +25,14 @@ module API
render_api_error!(errors, 400)
end
+ def issue_entity(project)
+ if project.has_external_issue_tracker?
+ Entities::ExternalIssue
+ else
+ Entities::IssueBasic
+ end
+ end
+
params :optional_params do
optional :description, type: String, desc: 'The description of the merge request'
optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request'
@@ -35,7 +43,7 @@ module API
end
desc 'List merge requests' do
- success Entities::MergeRequest
+ success Entities::MergeRequestBasic
end
params do
optional :state, type: String, values: %w[opened closed merged all], default: 'all',
@@ -62,7 +70,7 @@ module API
end
merge_requests = merge_requests.reorder(params[:order_by] => params[:sort])
- present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user, project: user_project
+ present paginate(merge_requests), with: Entities::MergeRequestBasic, current_user: current_user, project: user_project
end
desc 'Create a merge request' do
diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb
index 44bdaea7fa4..e7f7edd95c7 100644
--- a/lib/api/milestones.rb
+++ b/lib/api/milestones.rb
@@ -30,7 +30,7 @@ module API
params do
optional :state, type: String, values: %w[active closed all], default: 'all',
desc: 'Return "active", "closed", or "all" milestones'
- optional :iid, type: Array[Integer], desc: 'The IID of the milestone'
+ optional :iids, type: Array[Integer], desc: 'The IIDs of the milestones'
optional :search, type: String, desc: 'The search criteria for the title or description of the milestone'
use :pagination
end
@@ -39,7 +39,7 @@ module API
milestones = user_project.milestones
milestones = filter_milestones_state(milestones, params[:state])
- milestones = filter_by_iid(milestones, params[:iid]) if params[:iid].present?
+ milestones = filter_by_iid(milestones, params[:iids]) if params[:iids].present?
milestones = filter_by_search(milestones, params[:search]) if params[:search]
present paginate(milestones), with: Entities::Milestone
@@ -103,7 +103,7 @@ module API
end
desc 'Get all issues for a single project milestone' do
- success Entities::Issue
+ success Entities::IssueBasic
end
params do
requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
@@ -120,12 +120,12 @@ module API
}
issues = IssuesFinder.new(current_user, finder_params).execute
- present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project
+ present paginate(issues), with: Entities::IssueBasic, current_user: current_user, project: user_project
end
desc 'Get all merge requests for a single project milestone' do
detail 'This feature was introduced in GitLab 9.'
- success Entities::MergeRequest
+ success Entities::MergeRequestBasic
end
params do
requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
@@ -142,7 +142,10 @@ module API
}
merge_requests = MergeRequestsFinder.new(current_user, finder_params).execute
- present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user, project: user_project
+ present paginate(merge_requests),
+ with: Entities::MergeRequestBasic,
+ current_user: current_user,
+ project: user_project
end
end
end
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
index 3afc1e385fe..0721b975ba4 100644
--- a/lib/api/pipelines.rb
+++ b/lib/api/pipelines.rb
@@ -10,7 +10,7 @@ module API
resource :projects do
desc 'Get all Pipelines of the project' do
detail 'This feature was introduced in GitLab 8.11.'
- success Entities::Pipeline
+ success Entities::PipelineBasic
end
params do
use :pagination
@@ -21,7 +21,7 @@ module API
authorize! :read_pipeline, user_project
pipelines = PipelinesFinder.new(user_project).execute(scope: params[:scope])
- present paginate(pipelines), with: Entities::Pipeline
+ present paginate(pipelines), with: Entities::PipelineBasic
end
desc 'Create a new pipeline' do
diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb
index 2a1cce73f3f..f57e7ea4032 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -50,11 +50,9 @@ module API
requires :title, type: String, desc: 'The title of the snippet'
requires :file_name, type: String, desc: 'The file name of the snippet'
requires :code, type: String, desc: 'The content of the snippet'
- requires :visibility_level, type: Integer,
- values: [Gitlab::VisibilityLevel::PRIVATE,
- Gitlab::VisibilityLevel::INTERNAL,
- Gitlab::VisibilityLevel::PUBLIC],
- desc: 'The visibility level of the snippet'
+ requires :visibility, type: String,
+ values: Gitlab::VisibilityLevel.string_values,
+ desc: 'The visibility of the snippet'
end
post ":id/snippets" do
authorize! :create_project_snippet, user_project
@@ -80,11 +78,9 @@ module API
optional :title, type: String, desc: 'The title of the snippet'
optional :file_name, type: String, desc: 'The file name of the snippet'
optional :code, type: String, desc: 'The content of the snippet'
- optional :visibility_level, type: Integer,
- values: [Gitlab::VisibilityLevel::PRIVATE,
- Gitlab::VisibilityLevel::INTERNAL,
- Gitlab::VisibilityLevel::PUBLIC],
- desc: 'The visibility level of the snippet'
+ optional :visibility, type: String,
+ values: Gitlab::VisibilityLevel.string_values,
+ desc: 'The visibility of the snippet'
at_least_one_of :title, :file_name, :code, :visibility_level
end
put ":id/snippets/:snippet_id" do
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 996404e0e49..63a4cdd5954 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -16,11 +16,7 @@ module API
optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project'
optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project'
optional :lfs_enabled, type: Boolean, desc: 'Flag indication if Git LFS is enabled for that project'
- optional :visibility_level, type: Integer, values: [
- Gitlab::VisibilityLevel::PRIVATE,
- Gitlab::VisibilityLevel::INTERNAL,
- Gitlab::VisibilityLevel::PUBLIC
- ], desc: 'Create a public project. The same as visibility_level = 20.'
+ optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the project.'
optional :public_builds, type: Boolean, desc: 'Perform public builds'
optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
@@ -48,11 +44,12 @@ module API
params :filter_params do
optional :archived, type: Boolean, default: false, desc: 'Limit by archived status'
- optional :visibility, type: String, values: %w[public internal private],
+ optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values,
desc: 'Limit by visibility'
- optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
+ optional :search, type: String, desc: 'Return list of projects matching the search criteria'
optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user'
optional :starred, type: Boolean, default: false, desc: 'Limit by starred status'
+ optional :membership, type: Boolean, default: false, desc: 'Limit by projects that the current user is a member of'
end
params :statistics_params do
@@ -208,7 +205,7 @@ module API
at_least_one_of :name, :description, :issues_enabled, :merge_requests_enabled,
:wiki_enabled, :builds_enabled, :snippets_enabled,
:shared_runners_enabled, :container_registry_enabled,
- :lfs_enabled, :visibility_level, :public_builds,
+ :lfs_enabled, :visibility, :public_builds,
:request_access_enabled, :only_allow_merge_if_pipeline_succeeds,
:only_allow_merge_if_all_discussions_are_resolved, :path,
:default_branch
@@ -217,7 +214,7 @@ module API
authorize_admin_project
attrs = declared_params(include_missing: false)
authorize! :rename_project, user_project if attrs[:name].present?
- authorize! :change_visibility_level, user_project if attrs[:visibility_level].present?
+ authorize! :change_visibility_level, user_project if attrs[:visibility].present?
result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 79a5f27dc4d..1cf29d9a1a3 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -122,9 +122,9 @@ module API
},
{
required: false,
- name: :notify_only_broken_builds,
+ name: :notify_only_broken_jobs,
type: Boolean,
- desc: 'Notify only broken builds'
+ desc: 'Notify only broken jobs'
}
],
'campfire' => [
@@ -403,9 +403,9 @@ module API
},
{
required: false,
- name: :notify_only_broken_builds,
+ name: :notify_only_broken_jobs,
type: Boolean,
- desc: 'Notify only broken builds'
+ desc: 'Notify only broken jobs'
}
],
'pivotaltracker' => [
@@ -611,7 +611,7 @@ module API
desc "Set #{service_slug} service for project"
params do
service_classes.each do |service|
- event_names = service.try(:event_names) || []
+ event_names = service.try(:event_names) || next
event_names.each do |event_name|
services[service.to_param.tr("_", "-")] << {
required: false,
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index 936c7e0930b..d4d3229f0d1 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -21,9 +21,9 @@ module API
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 :default_project_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default project visibility'
+ optional :default_snippet_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default snippet visibility'
+ optional :default_group_visibility, type: String, values: Gitlab::VisibilityLevel.string_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'
@@ -128,7 +128,9 @@ module API
:housekeeping_enabled, :terminal_max_session_time
end
put "application/settings" do
- if current_settings.update_attributes(declared_params(include_missing: false))
+ attrs = declared_params(include_missing: false)
+
+ if current_settings.update_attributes(attrs)
present current_settings, with: Entities::ApplicationSetting
else
render_validation_error!(current_settings)
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
index 0f86fdb3075..b93fdc62808 100644
--- a/lib/api/snippets.rb
+++ b/lib/api/snippets.rb
@@ -58,10 +58,10 @@ module API
requires :title, type: String, desc: 'The title of a snippet'
requires :file_name, type: String, desc: 'The name of a snippet file'
requires :content, type: String, desc: 'The content of a snippet'
- optional :visibility_level, type: Integer,
- values: Gitlab::VisibilityLevel.values,
- default: Gitlab::VisibilityLevel::INTERNAL,
- desc: 'The visibility level of the snippet'
+ optional :visibility, type: String,
+ values: Gitlab::VisibilityLevel.string_values,
+ default: 'internal',
+ desc: 'The visibility of the snippet'
end
post do
attrs = declared_params(include_missing: false).merge(request: request, api: true)
@@ -85,10 +85,10 @@ module API
optional :title, type: String, desc: 'The title of a snippet'
optional :file_name, type: String, desc: 'The name of a snippet file'
optional :content, type: String, desc: 'The content of a snippet'
- optional :visibility_level, type: Integer,
- values: Gitlab::VisibilityLevel.values,
- desc: 'The visibility level of the snippet'
- at_least_one_of :title, :file_name, :content, :visibility_level
+ optional :visibility, type: String,
+ values: Gitlab::VisibilityLevel.string_values,
+ desc: 'The visibility of the snippet'
+ at_least_one_of :title, :file_name, :content, :visibility
end
put ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index b7c9c5f2b7f..119e9024712 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -6,15 +6,15 @@ module API
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects do
- desc 'Trigger a GitLab project build' do
- success Entities::TriggerRequest
+ desc 'Trigger a GitLab project pipeline' do
+ success Entities::Pipeline
end
params do
requires :ref, type: String, desc: 'The commit sha or name of a branch or tag'
requires :token, type: String, desc: 'The unique token of trigger'
optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
end
- post ":id/(ref/:ref/)trigger/builds" do
+ post ":id/(ref/:ref/)trigger/pipeline" do
project = find_project(params[:id])
trigger = Ci::Trigger.find_by_token(params[:token].to_s)
not_found! unless project && trigger
@@ -29,9 +29,9 @@ module API
# create request and trigger builds
trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables)
if trigger_request
- present trigger_request, with: Entities::TriggerRequest
+ present trigger_request.pipeline, with: Entities::Pipeline
else
- errors = 'No builds created'
+ errors = 'No pipeline created'
render_api_error!(errors, 400)
end
end
@@ -55,13 +55,13 @@ module API
success Entities::Trigger
end
params do
- requires :token, type: String, desc: 'The unique token of trigger'
+ requires :trigger_id, type: Integer, desc: 'The trigger ID'
end
- get ':id/triggers/:token' do
+ get ':id/triggers/:trigger_id' do
authenticate!
authorize! :admin_build, user_project
- trigger = user_project.triggers.find_by(token: params[:token].to_s)
+ trigger = user_project.triggers.find(params.delete(:trigger_id))
return not_found!('Trigger') unless trigger
present trigger, with: Entities::Trigger
@@ -70,26 +70,76 @@ module API
desc 'Create a trigger' do
success Entities::Trigger
end
+ params do
+ requires :description, type: String, desc: 'The trigger description'
+ end
post ':id/triggers' do
authenticate!
authorize! :admin_build, user_project
- trigger = user_project.triggers.create
+ trigger = user_project.triggers.create(
+ declared_params(include_missing: false).merge(owner: current_user))
- present trigger, with: Entities::Trigger
+ if trigger.valid?
+ present trigger, with: Entities::Trigger
+ else
+ render_validation_error!(trigger)
+ end
+ end
+
+ desc 'Update a trigger' do
+ success Entities::Trigger
+ end
+ params do
+ requires :trigger_id, type: Integer, desc: 'The trigger ID'
+ optional :description, type: String, desc: 'The trigger description'
+ end
+ put ':id/triggers/:trigger_id' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ trigger = user_project.triggers.find(params.delete(:trigger_id))
+ return not_found!('Trigger') unless trigger
+
+ if trigger.update(declared_params(include_missing: false))
+ present trigger, with: Entities::Trigger
+ else
+ render_validation_error!(trigger)
+ end
+ end
+
+ desc 'Take ownership of trigger' do
+ success Entities::Trigger
+ end
+ params do
+ requires :trigger_id, type: Integer, desc: 'The trigger ID'
+ end
+ post ':id/triggers/:trigger_id/take_ownership' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ trigger = user_project.triggers.find(params.delete(:trigger_id))
+ return not_found!('Trigger') unless trigger
+
+ if trigger.update(owner: current_user)
+ status :ok
+ present trigger, with: Entities::Trigger
+ else
+ render_validation_error!(trigger)
+ end
end
desc 'Delete a trigger' do
success Entities::Trigger
end
params do
- requires :token, type: String, desc: 'The unique token of trigger'
+ requires :trigger_id, type: Integer, desc: 'The trigger ID'
end
- delete ':id/triggers/:token' do
+ delete ':id/triggers/:trigger_id' do
authenticate!
authorize! :admin_build, user_project
- trigger = user_project.triggers.find_by(token: params[:token].to_s)
+ trigger = user_project.triggers.find(params.delete(:trigger_id))
return not_found!('Trigger') unless trigger
trigger.destroy
diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb
new file mode 100644
index 00000000000..c8feba13527
--- /dev/null
+++ b/lib/api/v3/builds.rb
@@ -0,0 +1,263 @@
+module API
+ module V3
+ class Builds < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ helpers do
+ params :optional_scope do
+ optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show',
+ values: %w(pending running failed success canceled skipped),
+ coerce_with: ->(scope) {
+ if scope.is_a?(String)
+ [scope]
+ elsif scope.is_a?(Hashie::Mash)
+ scope.values
+ else
+ ['unknown']
+ end
+ }
+ end
+ end
+
+ desc 'Get a project builds' do
+ success ::API::V3::Entities::Build
+ end
+ params do
+ use :optional_scope
+ use :pagination
+ end
+ get ':id/builds' do
+ builds = user_project.builds.order('id DESC')
+ builds = filter_builds(builds, params[:scope])
+
+ present paginate(builds), with: ::API::V3::Entities::Build,
+ user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ end
+
+ desc 'Get builds for a specific commit of a project' do
+ success ::API::V3::Entities::Build
+ end
+ params do
+ requires :sha, type: String, desc: 'The SHA id of a commit'
+ use :optional_scope
+ use :pagination
+ end
+ get ':id/repository/commits/:sha/builds' do
+ authorize_read_builds!
+
+ return not_found! unless user_project.commit(params[:sha])
+
+ pipelines = user_project.pipelines.where(sha: params[:sha])
+ builds = user_project.builds.where(pipeline: pipelines).order('id DESC')
+ builds = filter_builds(builds, params[:scope])
+
+ present paginate(builds), with: ::API::V3::Entities::Build,
+ user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ end
+
+ desc 'Get a specific build of a project' do
+ success ::API::V3::Entities::Build
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ get ':id/builds/:build_id' do
+ authorize_read_builds!
+
+ build = get_build!(params[:build_id])
+
+ present build, with: ::API::V3::Entities::Build,
+ user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ end
+
+ desc 'Download the artifacts file from build' do
+ detail 'This feature was introduced in GitLab 8.5'
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ get ':id/builds/:build_id/artifacts' do
+ authorize_read_builds!
+
+ build = get_build!(params[:build_id])
+
+ present_artifacts!(build.artifacts_file)
+ end
+
+ desc 'Download the artifacts file from build' do
+ detail 'This feature was introduced in GitLab 8.10'
+ end
+ params do
+ requires :ref_name, type: String, desc: 'The ref from repository'
+ requires :job, type: String, desc: 'The name for the build'
+ end
+ get ':id/builds/artifacts/:ref_name/download',
+ requirements: { ref_name: /.+/ } do
+ authorize_read_builds!
+
+ builds = user_project.latest_successful_builds_for(params[:ref_name])
+ latest_build = builds.find_by!(name: params[:job])
+
+ present_artifacts!(latest_build.artifacts_file)
+ end
+
+ # TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace
+ # is saved in the DB instead of file). But before that, we need to consider how to replace the value of
+ # `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse.
+ desc 'Get a trace of a specific build of a project'
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ get ':id/builds/:build_id/trace' do
+ authorize_read_builds!
+
+ build = get_build!(params[:build_id])
+
+ header 'Content-Disposition', "infile; filename=\"#{build.id}.log\""
+ content_type 'text/plain'
+ env['api.format'] = :binary
+
+ trace = build.trace
+ body trace
+ end
+
+ desc 'Cancel a specific build of a project' do
+ success ::API::V3::Entities::Build
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ post ':id/builds/:build_id/cancel' do
+ authorize_update_builds!
+
+ build = get_build!(params[:build_id])
+
+ build.cancel
+
+ present build, with: ::API::V3::Entities::Build,
+ user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ end
+
+ desc 'Retry a specific build of a project' do
+ success ::API::V3::Entities::Build
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ post ':id/builds/:build_id/retry' do
+ authorize_update_builds!
+
+ build = get_build!(params[:build_id])
+ return forbidden!('Build is not retryable') unless build.retryable?
+
+ build = Ci::Build.retry(build, current_user)
+
+ present build, with: ::API::V3::Entities::Build,
+ user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ end
+
+ desc 'Erase build (remove artifacts and build trace)' do
+ success ::API::V3::Entities::Build
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ post ':id/builds/:build_id/erase' do
+ authorize_update_builds!
+
+ build = get_build!(params[:build_id])
+ return forbidden!('Build is not erasable!') unless build.erasable?
+
+ build.erase(erased_by: current_user)
+ present build, with: ::API::V3::Entities::Build,
+ user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
+ end
+
+ desc 'Keep the artifacts to prevent them from being deleted' do
+ success ::API::V3::Entities::Build
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ post ':id/builds/:build_id/artifacts/keep' do
+ authorize_update_builds!
+
+ build = get_build!(params[:build_id])
+ return not_found!(build) unless build.artifacts?
+
+ build.keep_artifacts!
+
+ status 200
+ present build, with: ::API::V3::Entities::Build,
+ user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ end
+
+ desc 'Trigger a manual build' do
+ success ::API::V3::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 Job") unless build.playable?
+
+ build.play(current_user)
+
+ status 200
+ present build, with: ::API::V3::Entities::Build,
+ user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ end
+ end
+
+ helpers do
+ def get_build(id)
+ user_project.builds.find_by(id: id.to_i)
+ end
+
+ def get_build!(id)
+ get_build(id) || not_found!
+ end
+
+ def present_artifacts!(artifacts_file)
+ if !artifacts_file.file_storage?
+ redirect_to(build.artifacts_file.url)
+ elsif artifacts_file.exists?
+ present_file!(artifacts_file.path, artifacts_file.filename)
+ else
+ not_found!
+ end
+ end
+
+ def filter_builds(builds, scope)
+ return builds if scope.nil? || scope.empty?
+
+ available_statuses = ::CommitStatus::AVAILABLE_STATUSES
+
+ unknown = scope - available_statuses
+ render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty?
+
+ builds.where(status: available_statuses && scope)
+ end
+
+ def authorize_read_builds!
+ authorize! :read_build, user_project
+ end
+
+ def authorize_update_builds!
+ authorize! :update_build, user_project
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb
index 506204b3517..d254d247042 100644
--- a/lib/api/v3/commits.rb
+++ b/lib/api/v3/commits.rb
@@ -130,9 +130,7 @@ module API
commit_params = {
commit: commit,
- create_merge_request: false,
- source_project: user_project,
- source_branch: commit.cherry_pick_branch_name,
+ start_branch: params[:branch],
target_branch: params[:branch]
}
diff --git a/lib/api/v3/deployments.rb b/lib/api/v3/deployments.rb
new file mode 100644
index 00000000000..95114ad1fe1
--- /dev/null
+++ b/lib/api/v3/deployments.rb
@@ -0,0 +1,43 @@
+module API
+ module V3
+ # Deployments RESTful API endpoints
+ class Deployments < Grape::API
+ include PaginationParams
+
+ 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 ::API::V3::Deployments
+ end
+ params do
+ use :pagination
+ end
+ get ':id/deployments' do
+ authorize! :read_deployment, user_project
+
+ present paginate(user_project.deployments), with: ::API::V3::Deployments
+ end
+
+ desc 'Gets a specific deployment' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success ::API::V3::Deployments
+ 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: ::API::V3::Deployments
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb
index 2a3dcb7f288..832b4bdeb4f 100644
--- a/lib/api/v3/entities.rb
+++ b/lib/api/v3/entities.rb
@@ -81,7 +81,7 @@ module API
expose :request_access_enabled
expose :only_allow_merge_if_all_discussions_are_resolved
- expose :statistics, using: 'API::Entities::ProjectStatistics', if: :statistics
+ expose :statistics, using: '::API::V3::Entities::ProjectStatistics', if: :statistics
end
class ProjectWithAccess < Project
@@ -125,6 +125,129 @@ module API
Gitlab::UrlBuilder.build(merge_request)
end
end
+
+ class Group < Grape::Entity
+ expose :id, :name, :path, :description, :visibility_level
+ expose :lfs_enabled?, as: :lfs_enabled
+ expose :avatar_url
+ expose :web_url
+ expose :request_access_enabled
+ expose :full_name, :full_path
+ expose :parent_id
+
+ 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
+ expose :projects, using: Entities::Project
+ expose :shared_projects, using: Entities::Project
+ end
+
+ class ApplicationSetting < Grape::Entity
+ expose :id
+ expose :default_projects_limit
+ expose :signup_enabled
+ expose :signin_enabled
+ expose :gravatar_enabled
+ expose :sign_in_text
+ expose :after_sign_up_text
+ expose :created_at
+ expose :updated_at
+ expose :home_page_url
+ expose :default_branch_protection
+ expose :restricted_visibility_levels
+ expose :max_attachment_size
+ expose :session_expire_delay
+ expose :default_project_visibility
+ expose :default_snippet_visibility
+ expose :default_group_visibility
+ expose :domain_whitelist
+ expose :domain_blacklist_enabled
+ expose :domain_blacklist
+ expose :user_oauth_applications
+ expose :after_sign_out_path
+ expose :container_registry_token_expire_delay
+ expose :repository_storage
+ expose :repository_storages
+ expose :koding_enabled
+ expose :koding_url
+ expose :plantuml_enabled
+ expose :plantuml_url
+ expose :terminal_max_session_time
+ end
+
+ class Environment < ::API::Entities::EnvironmentBasic
+ expose :project, using: Entities::Project
+ end
+
+ class Trigger < Grape::Entity
+ expose :token, :created_at, :updated_at, :deleted_at, :last_used
+ expose :owner, using: ::API::Entities::UserBasic
+ end
+
+ class TriggerRequest < Grape::Entity
+ expose :id, :variables
+ end
+
+ class Build < Grape::Entity
+ expose :id, :status, :stage, :name, :ref, :tag, :coverage
+ expose :created_at, :started_at, :finished_at
+ expose :user, with: ::API::Entities::User
+ expose :artifacts_file, using: ::API::Entities::JobArtifactFile, if: -> (build, opts) { build.artifacts? }
+ expose :commit, with: ::API::Entities::RepoCommit
+ expose :runner, with: ::API::Entities::Runner
+ expose :pipeline, with: ::API::Entities::PipelineBasic
+ end
+
+ class BuildArtifactFile < Grape::Entity
+ expose :filename, :size
+ end
+
+ class Deployment < Grape::Entity
+ expose :id, :iid, :ref, :sha, :created_at
+ expose :user, using: ::API::Entities::UserBasic
+ expose :environment, using: ::API::Entities::EnvironmentBasic
+ expose :deployable, using: Entities::Build
+ end
+
+ class MergeRequestChanges < MergeRequest
+ expose :diffs, as: :changes, using: ::API::Entities::RepoDiff do |compare, _|
+ compare.raw_diffs(all_diffs: true).to_a
+ end
+ end
+
+ class ProjectStatistics < Grape::Entity
+ expose :commit_count
+ expose :storage_size
+ expose :repository_size
+ expose :lfs_objects_size
+ expose :build_artifacts_size
+ end
+
+ class ProjectService < Grape::Entity
+ expose :id, :title, :created_at, :updated_at, :active
+ 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.
+ select { |field| options[:include_passwords] || field[:type] != 'password' }.
+ map { |field| field[:name] }
+ service.properties.slice(*field_names)
+ end
+ end
+
+ class ProjectHook < ::API::Entities::Hook
+ expose :project_id, :issues_events, :merge_requests_events
+ expose :note_events, :build_events, :pipeline_events, :wiki_page_events
+ end
end
end
end
diff --git a/lib/api/v3/environments.rb b/lib/api/v3/environments.rb
index 3effccfa708..3056b70e6ef 100644
--- a/lib/api/v3/environments.rb
+++ b/lib/api/v3/environments.rb
@@ -1,6 +1,7 @@
module API
module V3
class Environments < Grape::API
+ include ::API::Helpers::CustomValidators
include PaginationParams
before { authenticate! }
@@ -9,9 +10,66 @@ module API
requires :id, type: String, desc: 'The project ID'
end
resource :projects do
+ desc 'Get all environments of the project' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Environment
+ end
+ params do
+ use :pagination
+ end
+ get ':id/environments' do
+ authorize! :read_environment, user_project
+
+ present paginate(user_project.environments), with: Entities::Environment
+ end
+
+ desc 'Creates a new environment' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Environment
+ end
+ 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
+
+ environment = user_project.environments.create(declared_params)
+
+ if environment.persisted?
+ present environment, with: Entities::Environment
+ else
+ render_validation_error!(environment)
+ end
+ end
+
+ desc 'Updates an existing environment' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Environment
+ end
+ params do
+ 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
+
+ environment = user_project.environments.find(params[:environment_id])
+
+ update_params = declared_params(include_missing: false).extract!(:name, :external_url)
+ if environment.update(update_params)
+ present environment, with: Entities::Environment
+ else
+ render_validation_error!(environment)
+ end
+ end
+
desc 'Deletes an existing environment' do
detail 'This feature was introduced in GitLab 8.11.'
- success ::API::Entities::Environment
+ success Entities::Environment
end
params do
requires :environment_id, type: Integer, desc: 'The environment ID'
@@ -21,7 +79,7 @@ module API
environment = user_project.environments.find(params[:environment_id])
- present environment.destroy, with: ::API::Entities::Environment
+ present environment.destroy, with: Entities::Environment
end
end
end
diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb
index c826bc4fe0b..0aad87a3f58 100644
--- a/lib/api/v3/groups.rb
+++ b/lib/api/v3/groups.rb
@@ -6,13 +6,20 @@ module API
before { authenticate! }
helpers do
+ params :optional_params do
+ optional :description, type: String, desc: 'The description of the group'
+ optional :visibility_level, type: Integer, desc: 'The visibility level of the group'
+ 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: ::API::Entities::Group,
+ with: Entities::Group,
current_user: current_user,
)
@@ -22,8 +29,36 @@ module API
end
resource :groups do
+ desc 'Get a groups list' do
+ 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'
+ optional :order_by, type: String, values: %w[name path], default: 'name', desc: 'Order by name or path'
+ optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)'
+ use :pagination
+ end
+ get do
+ groups = if current_user.admin
+ Group.all
+ elsif params[:all_available]
+ GroupsFinder.new.execute(current_user)
+ else
+ current_user.groups
+ end
+
+ groups = groups.search(params[:search]) if params[:search].present?
+ groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
+ groups = groups.reorder(params[:order_by] => params[:sort])
+
+ present_groups groups, statistics: params[:statistics] && current_user.is_admin?
+ end
+
desc 'Get list of owned groups for authenticated user' do
- success ::API::Entities::Group
+ success Entities::Group
end
params do
use :pagination
@@ -32,6 +67,114 @@ module API
get '/owned' do
present_groups current_user.owned_groups, statistics: params[:statistics]
end
+
+ desc 'Create a group. Available only for users who can create groups.' do
+ success Entities::Group
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the group'
+ requires :path, type: String, desc: 'The path of the group'
+ optional :parent_id, type: Integer, desc: 'The parent group id for creating nested group'
+ use :optional_params
+ end
+ post do
+ authorize! :create_group
+
+ group = ::Groups::CreateService.new(current_user, declared_params(include_missing: false)).execute
+
+ if group.persisted?
+ present group, with: Entities::Group, current_user: current_user
+ else
+ render_api_error!("Failed to save group #{group.errors.messages}", 400)
+ end
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a group'
+ end
+ resource :groups do
+ desc 'Update a group. Available only for users who can administrate groups.' do
+ success Entities::Group
+ end
+ params do
+ optional :name, type: String, desc: 'The name of the group'
+ optional :path, type: String, desc: 'The path of the group'
+ use :optional_params
+ at_least_one_of :name, :path, :description, :visibility_level,
+ :lfs_enabled, :request_access_enabled
+ end
+ put ':id' do
+ group = find_group!(params[:id])
+ authorize! :admin_group, group
+
+ if ::Groups::UpdateService.new(group, current_user, declared_params(include_missing: false)).execute
+ present group, with: Entities::GroupDetail, current_user: current_user
+ else
+ render_validation_error!(group)
+ end
+ end
+
+ desc 'Get a single group, with containing projects.' do
+ success Entities::GroupDetail
+ end
+ get ":id" do
+ group = find_group!(params[:id])
+ present group, with: Entities::GroupDetail, current_user: current_user
+ end
+
+ desc 'Remove a group.'
+ delete ":id" do
+ group = find_group!(params[:id])
+ authorize! :admin_group, group
+ present ::Groups::DestroyService.new(group, current_user).execute, with: Entities::GroupDetail, current_user: current_user
+ end
+
+ desc 'Get a list of projects in this group.' do
+ success Entities::Project
+ end
+ params do
+ optional :archived, type: Boolean, default: false, desc: 'Limit by archived status'
+ 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'
+ 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'
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return projects sorted in ascending and descending order'
+ optional :simple, type: Boolean, default: false,
+ desc: 'Return only the ID, URL, name, and path of each project'
+ optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user'
+ optional :starred, type: Boolean, default: false, desc: 'Limit by starred status'
+
+ use :pagination
+ end
+ get ":id/projects" do
+ group = find_group!(params[:id])
+ projects = GroupProjectsFinder.new(group).execute(current_user)
+ projects = filter_projects(projects)
+ entity = params[:simple] ? ::API::Entities::BasicProjectDetails : Entities::Project
+ 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 or path of the project'
+ end
+ post ":id/projects/:project_id" do
+ authenticated_as_admin!
+ 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, current_user: current_user
+ else
+ render_api_error!("Failed to transfer project #{project.errors.messages}", 400)
+ end
+ end
end
end
end
diff --git a/lib/api/v3/merge_request_diffs.rb b/lib/api/v3/merge_request_diffs.rb
new file mode 100644
index 00000000000..a462803e26c
--- /dev/null
+++ b/lib/api/v3/merge_request_diffs.rb
@@ -0,0 +1,43 @@
+module API
+ module V3
+ # MergeRequestDiff API
+ class MergeRequestDiffs < Grape::API
+ before { authenticate! }
+
+ resource :projects do
+ desc 'Get a list of merge request diff versions' do
+ detail 'This feature was introduced in GitLab 8.12.'
+ success ::API::Entities::MergeRequestDiff
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ end
+
+ get ":id/merge_requests/:merge_request_id/versions" do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+
+ present merge_request.merge_request_diffs, with: ::API::Entities::MergeRequestDiff
+ end
+
+ desc 'Get a single merge request diff version' do
+ detail 'This feature was introduced in GitLab 8.12.'
+ success ::API::Entities::MergeRequestDiffFull
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ requires :version_id, type: Integer, desc: 'The ID of a merge request diff version'
+ end
+
+ get ":id/merge_requests/:merge_request_id/versions/:version_id" do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+
+ present merge_request.merge_request_diffs.find(params[:version_id]), with: ::API::Entities::MergeRequestDiffFull
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb
index 654e818e1b5..7dbd4691a94 100644
--- a/lib/api/v3/merge_requests.rb
+++ b/lib/api/v3/merge_requests.rb
@@ -28,6 +28,14 @@ module API
render_api_error!(errors, 400)
end
+ def issue_entity(project)
+ if project.has_external_issue_tracker?
+ ::API::Entities::ExternalIssue
+ else
+ ::API::Entities::Issue
+ end
+ end
+
params :optional_params do
optional :description, type: String, desc: 'The description of the merge request'
optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request'
diff --git a/lib/api/v3/milestones.rb b/lib/api/v3/milestones.rb
new file mode 100644
index 00000000000..2a850a08a8a
--- /dev/null
+++ b/lib/api/v3/milestones.rb
@@ -0,0 +1,64 @@
+module API
+ module V3
+ class Milestones < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ helpers do
+ def filter_milestones_state(milestones, state)
+ case state
+ when 'active' then milestones.active
+ when 'closed' then milestones.closed
+ else milestones
+ end
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ desc 'Get a list of project milestones' do
+ success ::API::Entities::Milestone
+ end
+ params do
+ optional :state, type: String, values: %w[active closed all], default: 'all',
+ desc: 'Return "active", "closed", or "all" milestones'
+ optional :iid, type: Array[Integer], desc: 'The IID of the milestone'
+ use :pagination
+ end
+ get ":id/milestones" do
+ authorize! :read_milestone, user_project
+
+ milestones = user_project.milestones
+ milestones = filter_milestones_state(milestones, params[:state])
+ milestones = filter_by_iid(milestones, params[:iid]) if params[:iid].present?
+
+ present paginate(milestones), with: ::API::Entities::Milestone
+ end
+
+ desc 'Get all issues for a single project milestone' do
+ success ::API::Entities::Issue
+ end
+ params do
+ requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
+ use :pagination
+ end
+ get ':id/milestones/:milestone_id/issues' do
+ authorize! :read_milestone, user_project
+
+ milestone = user_project.milestones.find(params[:milestone_id])
+
+ finder_params = {
+ project_id: user_project.id,
+ milestone_title: milestone.title
+ }
+
+ issues = IssuesFinder.new(current_user, finder_params).execute
+ present paginate(issues), with: ::API::Entities::Issue, current_user: current_user, project: user_project
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/pipelines.rb b/lib/api/v3/pipelines.rb
new file mode 100644
index 00000000000..2c26a5f7d35
--- /dev/null
+++ b/lib/api/v3/pipelines.rb
@@ -0,0 +1,36 @@
+module API
+ module V3
+ class Pipelines < Grape::API
+ include PaginationParams
+
+ 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 ::API::Entities::Pipeline
+ end
+ params do
+ use :pagination
+ optional :scope, type: String, values: %w(running branches tags),
+ desc: 'Either running, branches, or tags'
+ end
+ get ':id/pipelines' do
+ authorize! :read_pipeline, user_project
+
+ pipelines = PipelinesFinder.new(user_project).execute(scope: params[:scope])
+ present paginate(pipelines), with: ::API::Entities::Pipeline
+ end
+ end
+
+ helpers do
+ def pipeline
+ @pipeline ||= user_project.pipelines.find(params[:pipeline_id])
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/project_hooks.rb b/lib/api/v3/project_hooks.rb
new file mode 100644
index 00000000000..861b991b8e1
--- /dev/null
+++ b/lib/api/v3/project_hooks.rb
@@ -0,0 +1,106 @@
+module API
+ module V3
+ class ProjectHooks < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+ before { authorize_admin_project }
+
+ helpers do
+ params :project_hook_properties do
+ requires :url, type: String, desc: "The URL to send the request to"
+ optional :push_events, type: Boolean, desc: "Trigger hook on push events"
+ optional :issues_events, type: Boolean, desc: "Trigger hook on issues events"
+ optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events"
+ optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events"
+ 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_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
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ desc 'Get project hooks' do
+ success ::API::V3::Entities::ProjectHook
+ end
+ params do
+ use :pagination
+ end
+ get ":id/hooks" do
+ hooks = paginate user_project.hooks
+
+ present hooks, with: ::API::V3::Entities::ProjectHook
+ end
+
+ desc 'Get a project hook' do
+ success ::API::V3::Entities::ProjectHook
+ end
+ params do
+ requires :hook_id, type: Integer, desc: 'The ID of a project hook'
+ end
+ get ":id/hooks/:hook_id" do
+ hook = user_project.hooks.find(params[:hook_id])
+ present hook, with: ::API::V3::Entities::ProjectHook
+ end
+
+ desc 'Add hook to project' do
+ success ::API::V3::Entities::ProjectHook
+ end
+ params do
+ use :project_hook_properties
+ end
+ post ":id/hooks" do
+ hook = user_project.hooks.new(declared_params(include_missing: false))
+
+ if hook.save
+ present hook, with: ::API::V3::Entities::ProjectHook
+ else
+ error!("Invalid url given", 422) if hook.errors[:url].present?
+
+ not_found!("Project hook #{hook.errors.messages}")
+ end
+ end
+
+ desc 'Update an existing project hook' do
+ success ::API::V3::Entities::ProjectHook
+ end
+ params do
+ requires :hook_id, type: Integer, desc: "The ID of the hook to update"
+ use :project_hook_properties
+ end
+ put ":id/hooks/:hook_id" do
+ hook = user_project.hooks.find(params.delete(:hook_id))
+
+ if hook.update_attributes(declared_params(include_missing: false))
+ present hook, with: ::API::V3::Entities::ProjectHook
+ else
+ error!("Invalid url given", 422) if hook.errors[:url].present?
+
+ not_found!("Project hook #{hook.errors.messages}")
+ end
+ end
+
+ desc 'Deletes project hook' do
+ success ::API::V3::Entities::ProjectHook
+ end
+ params do
+ requires :hook_id, type: Integer, desc: 'The ID of the hook to delete'
+ end
+ delete ":id/hooks/:hook_id" do
+ begin
+ present user_project.hooks.destroy(params[:hook_id]), with: ::API::V3::Entities::ProjectHook
+ rescue
+ # ProjectHook can raise Error if hook_id not found
+ not_found!("Error deleting hook #{params[:hook_id]}")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/services.rb b/lib/api/v3/services.rb
index af0a058f69b..d77185ffe5a 100644
--- a/lib/api/v3/services.rb
+++ b/lib/api/v3/services.rb
@@ -537,6 +537,23 @@ module API
]
}
+ trigger_services = {
+ 'mattermost-slash-commands' => [
+ {
+ name: :token,
+ type: String,
+ desc: 'The Mattermost token'
+ }
+ ],
+ 'slack-slash-commands' => [
+ {
+ name: :token,
+ type: String,
+ desc: 'The Slack token'
+ }
+ ]
+ }.freeze
+
resource :projects do
before { authenticate! }
before { authorize_admin_project }
@@ -567,6 +584,57 @@ module API
render_api_error!('400 Bad Request', 400)
end
end
+
+ desc 'Get the service settings for project' do
+ success Entities::ProjectService
+ end
+ params do
+ requires :service_slug, type: String, values: services.keys, desc: 'The name of the service'
+ end
+ get ":id/services/:service_slug" do
+ service = user_project.find_or_initialize_service(params[:service_slug].underscore)
+ present service, with: Entities::ProjectService, include_passwords: current_user.is_admin?
+ end
+ end
+
+ trigger_services.each do |service_slug, settings|
+ helpers do
+ def chat_command_service(project, service_slug, params)
+ project.services.active.where(template: false).find do |service|
+ service.try(:token) == params[:token] && service.to_param == service_slug.underscore
+ end
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ desc "Trigger a slash command for #{service_slug}" do
+ detail 'Added in GitLab 8.13'
+ end
+ params do
+ settings.each do |setting|
+ requires setting[:name], type: setting[:type], desc: setting[:desc]
+ end
+ end
+ post ":id/services/#{service_slug.underscore}/trigger" do
+ project = find_project(params[:id])
+
+ # This is not accurate, but done to prevent leakage of the project names
+ not_found!('Service') unless project
+
+ service = chat_command_service(project, service_slug, params)
+ result = service.try(:trigger, params)
+
+ if result
+ status result[:status] || 200
+ present result
+ else
+ not_found!('Service')
+ end
+ end
+ end
end
end
end
diff --git a/lib/api/v3/settings.rb b/lib/api/v3/settings.rb
new file mode 100644
index 00000000000..748d6b97d4f
--- /dev/null
+++ b/lib/api/v3/settings.rb
@@ -0,0 +1,137 @@
+module API
+ module V3
+ class Settings < Grape::API
+ before { authenticated_as_admin! }
+
+ helpers do
+ def current_settings
+ @current_setting ||=
+ (ApplicationSetting.current || ApplicationSetting.create_from_defaults)
+ end
+ end
+
+ desc 'Get the current application settings' do
+ success Entities::ApplicationSetting
+ end
+ get "application/settings" do
+ present current_settings, with: Entities::ApplicationSetting
+ end
+
+ 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 :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB'
+ 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
+ optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.'
+ 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, :max_pages_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, :terminal_max_session_time
+ end
+ put "application/settings" do
+ if current_settings.update_attributes(declared_params(include_missing: false))
+ present current_settings, with: Entities::ApplicationSetting
+ else
+ render_validation_error!(current_settings)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/snippets.rb b/lib/api/v3/snippets.rb
new file mode 100644
index 00000000000..07dac7e9904
--- /dev/null
+++ b/lib/api/v3/snippets.rb
@@ -0,0 +1,138 @@
+module API
+ module V3
+ class Snippets < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ resource :snippets do
+ helpers do
+ def snippets_for_current_user
+ SnippetsFinder.new.execute(current_user, filter: :by_user, user: current_user)
+ end
+
+ def public_snippets
+ SnippetsFinder.new.execute(current_user, filter: :public)
+ end
+ end
+
+ desc 'Get a snippets list for authenticated user' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success ::API::Entities::PersonalSnippet
+ end
+ params do
+ use :pagination
+ end
+ get do
+ present paginate(snippets_for_current_user), with: ::API::Entities::PersonalSnippet
+ end
+
+ desc 'List all public snippets current_user has access to' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success ::API::Entities::PersonalSnippet
+ end
+ params do
+ use :pagination
+ end
+ get 'public' do
+ present paginate(public_snippets), with: ::API::Entities::PersonalSnippet
+ end
+
+ desc 'Get a single snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success ::API::Entities::PersonalSnippet
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a snippet'
+ end
+ get ':id' do
+ snippet = snippets_for_current_user.find(params[:id])
+ present snippet, with: ::API::Entities::PersonalSnippet
+ end
+
+ desc 'Create new snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success ::API::Entities::PersonalSnippet
+ end
+ params do
+ requires :title, type: String, desc: 'The title of a snippet'
+ requires :file_name, type: String, desc: 'The name of a snippet file'
+ requires :content, type: String, desc: 'The content of a snippet'
+ optional :visibility_level, type: Integer,
+ values: Gitlab::VisibilityLevel.values,
+ default: Gitlab::VisibilityLevel::INTERNAL,
+ desc: 'The visibility level of the snippet'
+ end
+ post do
+ attrs = declared_params(include_missing: false).merge(request: request, api: true)
+ snippet = CreateSnippetService.new(nil, current_user, attrs).execute
+
+ if snippet.persisted?
+ present snippet, with: ::API::Entities::PersonalSnippet
+ else
+ render_validation_error!(snippet)
+ end
+ end
+
+ desc 'Update an existing snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success ::API::Entities::PersonalSnippet
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a snippet'
+ optional :title, type: String, desc: 'The title of a snippet'
+ optional :file_name, type: String, desc: 'The name of a snippet file'
+ optional :content, type: String, desc: 'The content of a snippet'
+ optional :visibility_level, type: Integer,
+ values: Gitlab::VisibilityLevel.values,
+ desc: 'The visibility level of the snippet'
+ at_least_one_of :title, :file_name, :content, :visibility_level
+ end
+ put ':id' do
+ snippet = snippets_for_current_user.find_by(id: params.delete(:id))
+ return not_found!('Snippet') unless snippet
+ authorize! :update_personal_snippet, snippet
+
+ attrs = declared_params(include_missing: false)
+
+ UpdateSnippetService.new(nil, current_user, snippet, attrs).execute
+ if snippet.persisted?
+ present snippet, with: ::API::Entities::PersonalSnippet
+ else
+ render_validation_error!(snippet)
+ end
+ end
+
+ desc 'Remove snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success ::API::Entities::PersonalSnippet
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a snippet'
+ end
+ delete ':id' do
+ snippet = snippets_for_current_user.find_by(id: params.delete(:id))
+ return not_found!('Snippet') unless snippet
+ authorize! :destroy_personal_snippet, snippet
+ snippet.destroy
+ no_content!
+ end
+
+ desc 'Get a raw snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a snippet'
+ end
+ get ":id/raw" do
+ snippet = snippets_for_current_user.find_by(id: params.delete(:id))
+ return not_found!('Snippet') unless snippet
+
+ env['api.format'] = :txt
+ content_type 'text/plain'
+ present snippet.content
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb
index 4051d4bca8d..1dfdb6a5956 100644
--- a/lib/api/v3/triggers.rb
+++ b/lib/api/v3/triggers.rb
@@ -7,8 +7,81 @@ module API
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects do
+ desc 'Trigger a GitLab project build' do
+ success ::API::V3::Entities::TriggerRequest
+ end
+ params do
+ requires :ref, type: String, desc: 'The commit sha or name of a branch or tag'
+ requires :token, type: String, desc: 'The unique token of trigger'
+ optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
+ end
+ post ":id/(ref/:ref/)trigger/builds" do
+ project = find_project(params[:id])
+ trigger = Ci::Trigger.find_by_token(params[:token].to_s)
+ not_found! unless project && trigger
+ unauthorized! unless trigger.project == project
+
+ # validate variables
+ variables = params[:variables].to_h
+ unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
+ render_api_error!('variables needs to be a map of key-valued strings', 400)
+ end
+
+ # create request and trigger builds
+ trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables)
+ if trigger_request
+ present trigger_request, with: ::API::V3::Entities::TriggerRequest
+ else
+ errors = 'No builds created'
+ render_api_error!(errors, 400)
+ end
+ end
+
+ desc 'Get triggers list' do
+ success ::API::V3::Entities::Trigger
+ end
+ params do
+ use :pagination
+ end
+ get ':id/triggers' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ triggers = user_project.triggers.includes(:trigger_requests)
+
+ present paginate(triggers), with: ::API::V3::Entities::Trigger
+ end
+
+ desc 'Get specific trigger of a project' do
+ success ::API::V3::Entities::Trigger
+ end
+ params do
+ requires :token, type: String, desc: 'The unique token of trigger'
+ end
+ get ':id/triggers/:token' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ trigger = user_project.triggers.find_by(token: params[:token].to_s)
+ return not_found!('Trigger') unless trigger
+
+ present trigger, with: ::API::V3::Entities::Trigger
+ end
+
+ desc 'Create a trigger' do
+ success ::API::V3::Entities::Trigger
+ end
+ post ':id/triggers' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ trigger = user_project.triggers.create
+
+ present trigger, with: ::API::V3::Entities::Trigger
+ end
+
desc 'Delete a trigger' do
- success ::API::Entities::Trigger
+ success ::API::V3::Entities::Trigger
end
params do
requires :token, type: String, desc: 'The unique token of trigger'
@@ -22,7 +95,7 @@ module API
trigger.destroy
- present trigger, with: ::API::Entities::Trigger
+ present trigger, with: ::API::V3::Entities::Trigger
end
end
end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index 5cc164a6325..7b4476fa4db 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -51,7 +51,8 @@ module Backup
if directory.files.create(key: tar_file, body: File.open(tar_file), public: false,
multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size,
- encryption: Gitlab.config.backup.upload.encryption)
+ encryption: Gitlab.config.backup.upload.encryption,
+ storage_class: Gitlab.config.backup.upload.storage_class)
$progress.puts "done".color(:green)
else
puts "uploading backup to #{remote_directory} failed".color(:red)
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index d16d5ba4960..3c4ba5d50e6 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -180,9 +180,8 @@ module Backup
return unless Dir.exist?(path)
dir_entries = Dir.entries(path)
- %w[annex custom_hooks].each do |entry|
- yield(entry) if dir_entries.include?(entry)
- end
+
+ yield('custom_hooks') if dir_entries.include?('custom_hooks')
end
def prepare
diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb
index a8c1ca0c60a..d6138816e70 100644
--- a/lib/banzai/filter/emoji_filter.rb
+++ b/lib/banzai/filter/emoji_filter.rb
@@ -17,8 +17,8 @@ module Banzai
next unless content.include?(':') || node.text.match(emoji_unicode_pattern)
- html = emoji_name_image_filter(content)
- html = emoji_unicode_image_filter(html)
+ html = emoji_unicode_element_unicode_filter(content)
+ html = emoji_name_element_unicode_filter(html)
next if html == content
@@ -27,33 +27,30 @@ module Banzai
doc
end
- # Replace :emoji: with corresponding images.
+ # Replace :emoji: with corresponding gl-emoji unicode.
#
# text - String text to replace :emoji: in.
#
- # Returns a String with :emoji: replaced with images.
- def emoji_name_image_filter(text)
+ # Returns a String with :emoji: replaced with gl-emoji unicode.
+ def emoji_name_element_unicode_filter(text)
text.gsub(emoji_pattern) do |match|
name = $1
- emoji_image_tag(name, emoji_url(name))
+ Gitlab::Emoji.gl_emoji_tag(name)
end
end
- # Replace unicode emoji with corresponding images if they exist.
+ # Replace unicode emoji with corresponding gl-emoji unicode.
#
# text - String text to replace unicode emoji in.
#
- # Returns a String with unicode emoji replaced with images.
- def emoji_unicode_image_filter(text)
+ # Returns a String with unicode emoji replaced with gl-emoji unicode.
+ def emoji_unicode_element_unicode_filter(text)
text.gsub(emoji_unicode_pattern) do |moji|
- emoji_image_tag(Gitlab::Emoji.emojis_by_moji[moji]['name'], emoji_unicode_url(moji))
+ emoji_info = Gitlab::Emoji.emojis_by_moji[moji]
+ Gitlab::Emoji.gl_emoji_tag(emoji_info['name'])
end
end
- def emoji_image_tag(emoji_name, emoji_url)
- "<img class='emoji' title=':#{emoji_name}:' alt=':#{emoji_name}:' src='#{emoji_url}' height='20' width='20' align='absmiddle' />"
- end
-
# Build a regexp that matches all valid :emoji: names.
def self.emoji_pattern
@emoji_pattern ||= /:(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/
@@ -66,52 +63,13 @@ module Banzai
private
- def emoji_url(name)
- emoji_path = emoji_filename(name)
-
- if context[:asset_host]
- # Asset host is specified.
- url_to_image(emoji_path)
- elsif context[:asset_root]
- # Gitlab url is specified
- File.join(context[:asset_root], url_to_image(emoji_path))
- else
- # All other cases
- url_to_image(emoji_path)
- end
- end
-
- def emoji_unicode_url(moji)
- emoji_unicode_path = emoji_unicode_filename(moji)
-
- if context[:asset_host]
- url_to_image(emoji_unicode_path)
- elsif context[:asset_root]
- File.join(context[:asset_root], url_to_image(emoji_unicode_path))
- else
- url_to_image(emoji_unicode_path)
- end
- end
-
- def url_to_image(image)
- ActionController::Base.helpers.url_to_image(image)
- end
-
def emoji_pattern
self.class.emoji_pattern
end
- def emoji_filename(name)
- "#{Gitlab::Emoji.emoji_filename(name)}.png"
- end
-
def emoji_unicode_pattern
self.class.emoji_unicode_pattern
end
-
- def emoji_unicode_filename(name)
- "#{Gitlab::Emoji.emoji_unicode_filename(name)}.png"
- end
end
end
end
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index af1e575fc89..d5f9e252f62 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -35,6 +35,10 @@ module Banzai
# Allow span elements
whitelist[:elements].push('span')
+ # Allow html5 details/summary elements
+ whitelist[:elements].push('details')
+ whitelist[:elements].push('summary')
+
# Allow abbr elements with title attribute
whitelist[:elements].push('abbr')
whitelist[:attributes]['abbr'] = %w(title)
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index c973897f420..849e1142841 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -133,7 +133,7 @@ module Banzai
data = data_attribute(group: namespace.id)
content = link_content || Group.reference_prefix + group
- link_tag(url, data, content, namespace.name)
+ link_tag(url, data, content, namespace.full_name)
end
def link_to_user(user, namespace, link_content: nil)
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index e390919ae1d..15a461a16dd 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -58,7 +58,7 @@ module Ci
commands: job[:commands],
tag_list: job[:tags] || [],
name: job[:name].to_s,
- allow_failure: job[:allow_failure] || false,
+ allow_failure: job[:ignore],
when: job[:when] || 'on_success',
environment: job[:environment_name],
coverage_regex: job[:coverage],
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 0a5abc92190..0a0bd0e781c 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -23,22 +23,25 @@ module Gitlab
Gitlab::Auth::Result.new
rate_limit!(ip, success: result.success?, login: login)
+ Gitlab::Auth::UniqueIpsLimiter.limit_user!(result.actor)
result
end
def find_with_user_password(login, password)
- user = User.by_login(login)
+ Gitlab::Auth::UniqueIpsLimiter.limit_user! do
+ user = User.by_login(login)
- # If no user is found, or it's an LDAP server, try LDAP.
- # LDAP users are only authenticated via LDAP
- if user.nil? || user.ldap_user?
- # Second chance - try LDAP authentication
- return nil unless Gitlab::LDAP::Config.enabled?
+ # If no user is found, or it's an LDAP server, try LDAP.
+ # LDAP users are only authenticated via LDAP
+ if user.nil? || user.ldap_user?
+ # Second chance - try LDAP authentication
+ return nil unless Gitlab::LDAP::Config.enabled?
- Gitlab::LDAP::Authentication.login(login, password)
- else
- user if user.valid_password?(password)
+ Gitlab::LDAP::Authentication.login(login, password)
+ else
+ user if user.valid_password?(password)
+ end
end
end
diff --git a/lib/gitlab/auth/too_many_ips.rb b/lib/gitlab/auth/too_many_ips.rb
new file mode 100644
index 00000000000..ed862791551
--- /dev/null
+++ b/lib/gitlab/auth/too_many_ips.rb
@@ -0,0 +1,17 @@
+module Gitlab
+ module Auth
+ class TooManyIps < StandardError
+ attr_reader :user_id, :ip, :unique_ips_count
+
+ def initialize(user_id, ip, unique_ips_count)
+ @user_id = user_id
+ @ip = ip
+ @unique_ips_count = unique_ips_count
+ end
+
+ def message
+ "User #{user_id} from IP: #{ip} tried logging from too many ips: #{unique_ips_count}"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/unique_ips_limiter.rb b/lib/gitlab/auth/unique_ips_limiter.rb
new file mode 100644
index 00000000000..bf2239ca150
--- /dev/null
+++ b/lib/gitlab/auth/unique_ips_limiter.rb
@@ -0,0 +1,43 @@
+module Gitlab
+ module Auth
+ class UniqueIpsLimiter
+ USER_UNIQUE_IPS_PREFIX = 'user_unique_ips'.freeze
+
+ class << self
+ def limit_user_id!(user_id)
+ if config.unique_ips_limit_enabled
+ ip = RequestContext.client_ip
+ unique_ips = update_and_return_ips_count(user_id, ip)
+
+ raise TooManyIps.new(user_id, ip, unique_ips) if unique_ips > config.unique_ips_limit_per_user
+ end
+ end
+
+ def limit_user!(user = nil)
+ user ||= yield if block_given?
+ limit_user_id!(user.id) unless user.nil?
+ user
+ end
+
+ def config
+ Gitlab::CurrentSettings.current_application_settings
+ end
+
+ def update_and_return_ips_count(user_id, ip)
+ time = Time.now.utc.to_i
+ key = "#{USER_UNIQUE_IPS_PREFIX}:#{user_id}"
+
+ Gitlab::Redis.with do |redis|
+ unique_ips_count = nil
+ redis.multi do |r|
+ r.zadd(key, time, ip)
+ r.zremrangebyscore(key, 0, time - config.unique_ips_limit_time_window)
+ unique_ips_count = r.zcard(key)
+ end
+ unique_ips_count.value
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/award_emoji.rb b/lib/gitlab/award_emoji.rb
deleted file mode 100644
index 7555326d384..00000000000
--- a/lib/gitlab/award_emoji.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-module Gitlab
- class AwardEmoji
- CATEGORIES = {
- objects: "Objects",
- travel: "Travel",
- symbols: "Symbols",
- nature: "Nature",
- people: "People",
- activity: "Activity",
- flags: "Flags",
- food: "Food"
- }.with_indifferent_access
-
- def self.normalize_emoji_name(name)
- aliases[name] || name
- end
-
- def self.emoji_by_category
- unless @emoji_by_category
- @emoji_by_category = Hash.new { |h, key| h[key] = [] }
-
- emojis.each do |emoji_name, data|
- data["name"] = emoji_name
-
- # Skip Fitzpatrick(tone) modifiers
- next if data["category"] == "modifier"
-
- category = data["category"]
-
- @emoji_by_category[category] << data
- end
-
- @emoji_by_category = @emoji_by_category.sort.to_h
- end
-
- @emoji_by_category
- end
-
- def self.emojis
- @emojis ||=
- begin
- json_path = File.join(Rails.root, 'fixtures', 'emojis', 'index.json' )
- JSON.parse(File.read(json_path))
- end
- end
-
- def self.aliases
- @aliases ||=
- begin
- json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json')
- JSON.parse(File.read(json_path))
- end
- end
-
- # Returns an Array of Emoji names and their asset URLs.
- def self.urls
- @urls ||= begin
- path = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
- # Construct the full asset path ourselves because
- # ActionView::Helpers::AssetUrlHelper.asset_url is slow for hundreds
- # of entries since it has to do a lot of extra work (e.g. regexps).
- prefix = Gitlab::Application.config.assets.prefix
- digest = Gitlab::Application.config.assets.digest
- base =
- if defined?(Gitlab::Application.config.relative_url_root) && Gitlab::Application.config.relative_url_root
- Gitlab::Application.config.relative_url_root
- else
- ''
- end
-
- JSON.parse(File.read(path)).map do |hash|
- fname =
- if digest
- "#{hash['unicode']}-#{hash['digest']}"
- else
- hash['unicode']
- end
-
- { name: hash['name'], path: File.join(base, prefix, "#{fname}.png") }
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index 7f7662f2776..176301bcca1 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -104,6 +104,14 @@ module Gitlab
(before_script_value.to_a + script_value.to_a).join("\n")
end
+ def manual_action?
+ self.when == 'manual'
+ end
+
+ def ignored?
+ allow_failure.nil? ? manual_action? : allow_failure
+ end
+
private
def inherit!(deps)
@@ -135,7 +143,8 @@ module Gitlab
environment_name: environment_defined? ? environment_value[:name] : nil,
coverage: coverage_defined? ? coverage_value : nil,
artifacts: artifacts_value,
- after_script: after_script_value }
+ after_script: after_script_value,
+ ignore: ignored? }
end
end
end
diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb
index 0f4b7b24cef..3495b8d0448 100644
--- a/lib/gitlab/ci/status/build/play.rb
+++ b/lib/gitlab/ci/status/build/play.rb
@@ -5,22 +5,10 @@ module Gitlab
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
diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb
index 90401cad0d2..e8530f2aaae 100644
--- a/lib/gitlab/ci/status/build/stop.rb
+++ b/lib/gitlab/ci/status/build/stop.rb
@@ -5,22 +5,10 @@ module Gitlab
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
diff --git a/lib/gitlab/ci/status/manual.rb b/lib/gitlab/ci/status/manual.rb
new file mode 100644
index 00000000000..5f28521901d
--- /dev/null
+++ b/lib/gitlab/ci/status/manual.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Ci
+ module Status
+ class Manual < Status::Core
+ def text
+ 'manual'
+ end
+
+ def label
+ 'manual action'
+ end
+
+ def icon
+ 'icon_status_manual'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb
index e50e54b6e99..182a30fd74d 100644
--- a/lib/gitlab/data_builder/pipeline.rb
+++ b/lib/gitlab/data_builder/pipeline.rb
@@ -39,7 +39,7 @@ module Gitlab
started_at: build.started_at,
finished_at: build.finished_at,
when: build.when,
- manual: build.manual?,
+ manual: build.action?,
user: build.user.try(:hook_attrs),
runner: build.runner && runner_hook_attrs(build.runner),
artifacts_file: {
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index d160cadc2d0..f3f417c1a63 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -24,7 +24,7 @@ module Gitlab
def self.nulls_last_order(field, direction = 'ASC')
order = "#{field} #{direction}"
- if Gitlab::Database.postgresql?
+ if postgresql?
order << ' NULLS LAST'
else
# `field IS NULL` will be `0` for non-NULL columns and `1` for NULL
@@ -38,7 +38,7 @@ module Gitlab
def self.nulls_first_order(field, direction = 'ASC')
order = "#{field} #{direction}"
- if Gitlab::Database.postgresql?
+ if postgresql?
order << ' NULLS FIRST'
else
# `field IS NULL` will be `0` for non-NULL columns and `1` for NULL
@@ -50,7 +50,7 @@ module Gitlab
end
def self.random
- Gitlab::Database.postgresql? ? "RANDOM()" : "RAND()"
+ postgresql? ? "RANDOM()" : "RAND()"
end
def true_value
diff --git a/lib/gitlab/emoji.rb b/lib/gitlab/emoji.rb
index bbbca8acc40..42703545c4f 100644
--- a/lib/gitlab/emoji.rb
+++ b/lib/gitlab/emoji.rb
@@ -1,7 +1,7 @@
module Gitlab
module Emoji
extend self
-
+
def emojis
Gemojione.index.instance_variable_get(:@emoji_by_name)
end
@@ -18,6 +18,10 @@ module Gitlab
emojis.keys
end
+ def emojis_aliases
+ @emoji_aliases ||= JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'aliases.json')))
+ end
+
def emoji_filename(name)
emojis[name]["unicode"]
end
@@ -25,5 +29,42 @@ module Gitlab
def emoji_unicode_filename(moji)
emojis_by_moji[moji]["unicode"]
end
+
+ def emoji_unicode_version(name)
+ @emoji_unicode_versions_by_name ||= JSON.parse(File.read(Rails.root.join('node_modules', 'emoji-unicode-version', 'emoji-unicode-version-map.json')))
+ @emoji_unicode_versions_by_name[name]
+ end
+
+ def normalize_emoji_name(name)
+ emojis_aliases[name] || name
+ end
+
+ def emoji_image_tag(name, src)
+ "<img class='emoji' title=':#{name}:' alt=':#{name}:' src='#{src}' height='20' width='20' align='absmiddle' />"
+ end
+
+ # CSS sprite fallback takes precedence over image fallback
+ def gl_emoji_tag(name, image: false, sprite: false, force_fallback: false)
+ emoji_name = emojis_aliases[name] || name
+ emoji_info = emojis[emoji_name]
+ emoji_fallback_image_source = ActionController::Base.helpers.url_to_image("emoji/#{emoji_info['name']}.png")
+ emoji_fallback_sprite_class = "emoji-#{emoji_name}"
+
+ data = {
+ name: emoji_name,
+ unicode_version: emoji_unicode_version(emoji_name)
+ }
+ data[:fallback_src] = emoji_fallback_image_source if image
+ data[:fallback_sprite_class] = emoji_fallback_sprite_class if sprite
+ ActionController::Base.helpers.content_tag 'gl-emoji',
+ class: ("emoji-icon #{emoji_fallback_sprite_class}" if force_fallback && sprite),
+ data: data do
+ if force_fallback && !sprite
+ emoji_image_tag(emoji_name, emoji_fallback_image_source)
+ else
+ emoji_info['moji']
+ end
+ end
+ end
end
end
diff --git a/lib/gitlab/etag_caching/middleware.rb b/lib/gitlab/etag_caching/middleware.rb
new file mode 100644
index 00000000000..0f24f9bbfde
--- /dev/null
+++ b/lib/gitlab/etag_caching/middleware.rb
@@ -0,0 +1,66 @@
+module Gitlab
+ module EtagCaching
+ class Middleware
+ RESERVED_WORDS = ProjectPathValidator::RESERVED.map { |word| "/#{word}/" }.join('|')
+ ROUTE_REGEXP = Regexp.union(
+ %r(^(?!.*(#{RESERVED_WORDS})).*/noteable/issue/\d+/notes\z)
+ )
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ return @app.call(env) unless enabled_for_current_route?(env)
+ Gitlab::Metrics.add_event(:etag_caching_middleware_used)
+
+ etag, cached_value_present = get_etag(env)
+ if_none_match = env['HTTP_IF_NONE_MATCH']
+
+ if if_none_match == etag
+ Gitlab::Metrics.add_event(:etag_caching_cache_hit)
+ [304, { 'ETag' => etag }, ['']]
+ else
+ track_cache_miss(if_none_match, cached_value_present)
+
+ status, headers, body = @app.call(env)
+ headers['ETag'] = etag
+ [status, headers, body]
+ end
+ end
+
+ private
+
+ def enabled_for_current_route?(env)
+ ROUTE_REGEXP.match(env['PATH_INFO'])
+ end
+
+ def get_etag(env)
+ cache_key = env['PATH_INFO']
+ store = Store.new
+ current_value = store.get(cache_key)
+ cached_value_present = current_value.present?
+
+ unless cached_value_present
+ current_value = store.touch(cache_key, only_if_missing: true)
+ end
+
+ [weak_etag_format(current_value), cached_value_present]
+ end
+
+ def weak_etag_format(value)
+ %Q{W/"#{value}"}
+ end
+
+ def track_cache_miss(if_none_match, cached_value_present)
+ if if_none_match.blank?
+ Gitlab::Metrics.add_event(:etag_caching_header_missing)
+ elsif !cached_value_present
+ Gitlab::Metrics.add_event(:etag_caching_key_not_found)
+ else
+ Gitlab::Metrics.add_event(:etag_caching_resource_changed)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/etag_caching/store.rb b/lib/gitlab/etag_caching/store.rb
new file mode 100644
index 00000000000..9532e432f78
--- /dev/null
+++ b/lib/gitlab/etag_caching/store.rb
@@ -0,0 +1,32 @@
+module Gitlab
+ module EtagCaching
+ class Store
+ EXPIRY_TIME = 10.minutes
+ REDIS_NAMESPACE = 'etag:'.freeze
+
+ def get(key)
+ Gitlab::Redis.with { |redis| redis.get(redis_key(key)) }
+ end
+
+ def touch(key, only_if_missing: false)
+ etag = generate_etag
+
+ Gitlab::Redis.with do |redis|
+ redis.set(redis_key(key), etag, ex: EXPIRY_TIME, nx: only_if_missing)
+ end
+
+ etag
+ end
+
+ private
+
+ def generate_etag
+ SecureRandom.hex
+ end
+
+ def redis_key(key)
+ "#{REDIS_NAMESPACE}#{key}"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 9c384069661..6c275a8d5de 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -4,16 +4,17 @@ module Gitlab
gon.api_version = 'v3' # v4 Is not officially released yet, therefore can't be considered as "frozen"
gon.default_avatar_url = URI.join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s
gon.max_file_size = current_application_settings.max_attachment_size
+ gon.asset_host = ActionController::Base.asset_host
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
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
+ gon.current_user_fullname = current_user.name
end
end
end
diff --git a/lib/gitlab/request_context.rb b/lib/gitlab/request_context.rb
new file mode 100644
index 00000000000..fef536ecb0b
--- /dev/null
+++ b/lib/gitlab/request_context.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ class RequestContext
+ class << self
+ def client_ip
+ RequestStore[:client_ip]
+ end
+ end
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ req = Rack::Request.new(env)
+
+ RequestStore[:client_ip] = req.ip
+
+ @app.call(env)
+ end
+ end
+end
diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb
index b7f825e8284..823f697f51c 100644
--- a/lib/gitlab/seeder.rb
+++ b/lib/gitlab/seeder.rb
@@ -1,24 +1,23 @@
+module DeliverNever
+ def deliver_later
+ self
+ end
+end
+
module Gitlab
class Seeder
def self.quiet
mute_mailer
SeedFu.quiet = true
+
yield
+
SeedFu.quiet = false
puts "\nOK".color(:green)
end
- def self.by_user(user)
- yield
- end
-
def self.mute_mailer
- code = <<-eos
-def Notify.deliver_later
- self
-end
- eos
- eval(code) # rubocop:disable Security/Eval
+ ActionMailer::MessageDelivery.prepend(DeliverNever)
end
end
end
diff --git a/lib/gitlab/sidekiq_status.rb b/lib/gitlab/sidekiq_status.rb
index aadc401ff8d..11e5f1b645c 100644
--- a/lib/gitlab/sidekiq_status.rb
+++ b/lib/gitlab/sidekiq_status.rb
@@ -44,19 +44,42 @@ module Gitlab
# Returns true if all the given job have been completed.
#
- # jids - The Sidekiq job IDs to check.
+ # job_ids - The Sidekiq job IDs to check.
#
# Returns true or false.
- def self.all_completed?(jids)
- keys = jids.map { |jid| key_for(jid) }
+ def self.all_completed?(job_ids)
+ self.num_running(job_ids).zero?
+ end
+
+ # Returns the number of jobs that are running.
+ #
+ # job_ids - The Sidekiq job IDs to check.
+ def self.num_running(job_ids)
+ responses = self.job_status(job_ids)
- responses = Sidekiq.redis do |redis|
+ responses.select(&:present?).count
+ end
+
+ # Returns the number of jobs that have completed.
+ #
+ # job_ids - The Sidekiq job IDs to check.
+ def self.num_completed(job_ids)
+ job_ids.size - self.num_running(job_ids)
+ end
+
+ # Returns the job status for each of the given job IDs.
+ #
+ # job_ids - The Sidekiq job IDs to check.
+ #
+ # Returns an array of true or false indicating job completion.
+ def self.job_status(job_ids)
+ keys = job_ids.map { |jid| key_for(jid) }
+
+ Sidekiq.redis do |redis|
redis.pipelined do
keys.each { |key| redis.exists(key) }
end
end
-
- responses.all? { |value| !value }
end
def self.key_for(jid)
diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb
index b28708c34e1..2248763c106 100644
--- a/lib/gitlab/visibility_level.rb
+++ b/lib/gitlab/visibility_level.rb
@@ -35,6 +35,10 @@ module Gitlab
class << self
delegate :values, to: :options
+ def string_values
+ string_options.keys
+ end
+
def options
{
'Private' => PRIVATE,
@@ -43,6 +47,14 @@ module Gitlab
}
end
+ def string_options
+ {
+ 'private' => PRIVATE,
+ 'internal' => INTERNAL,
+ 'public' => PUBLIC
+ }
+ end
+
def highest_allowed_level
restricted_levels = current_application_settings.restricted_visibility_levels
@@ -82,18 +94,39 @@ module Gitlab
level_name
end
+
+ def level_value(level)
+ return string_options[level] if level.is_a? String
+ level
+ end
+
+ def string_level(level)
+ string_options.key(level)
+ end
end
def private?
- visibility_level_field == PRIVATE
+ visibility_level_value == PRIVATE
end
def internal?
- visibility_level_field == INTERNAL
+ visibility_level_value == INTERNAL
end
def public?
- visibility_level_field == PUBLIC
+ visibility_level_value == PUBLIC
+ end
+
+ def visibility_level_value
+ self[visibility_level_field]
+ end
+
+ def visibility
+ Gitlab::VisibilityLevel.string_level(visibility_level_value)
+ end
+
+ def visibility=(level)
+ self[visibility_level_field] = Gitlab::VisibilityLevel.level_value(level)
end
end
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 3ff9f9eb5e7..eae1a0abf06 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -8,6 +8,7 @@ module Gitlab
VERSION_FILE = 'GITLAB_WORKHORSE_VERSION'.freeze
INTERNAL_API_CONTENT_TYPE = 'application/vnd.gitlab-workhorse+json'.freeze
INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request'.freeze
+ NOTIFICATION_CHANNEL = 'workhorse:notifications'.freeze
# Supposedly the effective key size for HMAC-SHA256 is 256 bits, i.e. 32
# bytes https://tools.ietf.org/html/rfc4868#section-2.6
@@ -154,6 +155,18 @@ module Gitlab
Rails.root.join('.gitlab_workhorse_secret')
end
+ def set_key_and_notify(key, value, expire: nil, overwrite: true)
+ Gitlab::Redis.with do |redis|
+ result = redis.set(key, value, ex: expire, nx: !overwrite)
+ if result
+ redis.publish(NOTIFICATION_CHANNEL, "#{key}=#{value}")
+ value
+ else
+ redis.get(key)
+ end
+ end
+ end
+
protected
def encode(hash)
diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb
index ad6df246091..3d60618006c 100644
--- a/lib/mattermost/client.rb
+++ b/lib/mattermost/client.rb
@@ -26,7 +26,7 @@ module Mattermost
def session_get(path, options = {})
with_session do |session|
- get(session, path, options)
+ get(session, path, options)
end
end
diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb
index 5388966605d..688a79c0441 100644
--- a/lib/mattermost/session.rb
+++ b/lib/mattermost/session.rb
@@ -153,7 +153,7 @@ module Mattermost
yield
rescue HTTParty::Error => e
raise Mattermost::ConnectionError.new(e.message)
- rescue Errno::ECONNREFUSED
+ rescue Errno::ECONNREFUSED => e
raise Mattermost::ConnectionError.new(e.message)
end
end
diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb
index 09dfd082b3a..2cdbbdece16 100644
--- a/lib/mattermost/team.rb
+++ b/lib/mattermost/team.rb
@@ -1,7 +1,18 @@
module Mattermost
class Team < Client
+ # Returns **all** teams for an admin
def all
- session_get('/api/v3/teams/all')
+ session_get('/api/v3/teams/all').values
+ end
+
+ # Creates a team on the linked Mattermost instance, the team admin will be the
+ # `current_user` passed to the Mattermost::Client instance
+ def create(name:, display_name:, type:)
+ session_post('/api/v3/teams/create', body: {
+ name: name,
+ display_name: display_name,
+ type: type
+ }.to_json)
end
end
end
diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl
index 5661394058d..330031aaddc 100644
--- a/lib/support/nginx/gitlab-ssl
+++ b/lib/support/nginx/gitlab-ssl
@@ -82,6 +82,9 @@ server {
##
# ssl_dhparam /etc/ssl/certs/dhparam.pem;
+ ## [Optional] Enable HTTP Strict Transport Security
+ # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
+
## Individual nginx logs for this GitLab vhost
access_log /var/log/nginx/gitlab_access.log;
error_log /var/log/nginx/gitlab_error.log;
diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake
index 993112aee3b..1f93b5a4dd2 100644
--- a/lib/tasks/gemojione.rake
+++ b/lib/tasks/gemojione.rake
@@ -5,29 +5,29 @@ namespace :gemojione do
require 'json'
dir = Gemojione.images_path
- digests = []
- aliases = Hash.new { |hash, key| hash[key] = [] }
- aliases_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json')
-
- JSON.parse(File.read(aliases_path)).each do |alias_name, real_name|
- aliases[real_name] << alias_name
- end
-
- Gitlab::AwardEmoji.emojis.map do |name, emoji_hash|
- fpath = File.join(dir, "#{emoji_hash['unicode']}.png")
- digest = Digest::SHA256.file(fpath).hexdigest
-
- digests << { name: name, unicode: emoji_hash['unicode'], digest: digest }
+ resultant_emoji_map = {}
+
+ Gitlab::Emoji.emojis.each do |name, emoji_hash|
+ # Ignore aliases
+ unless Gitlab::Emoji.emojis_aliases.key?(name)
+ fpath = File.join(dir, "#{emoji_hash['unicode']}.png")
+ hash_digest = Digest::SHA256.file(fpath).hexdigest
+
+ entry = {
+ category: emoji_hash['category'],
+ moji: emoji_hash['moji'],
+ unicodeVersion: Gitlab::Emoji.emoji_unicode_version(name),
+ digest: hash_digest,
+ }
- aliases[name].each do |alias_name|
- digests << { name: alias_name, unicode: emoji_hash['unicode'], digest: digest }
+ resultant_emoji_map[name] = entry
end
end
out = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
File.open(out, 'w') do |handle|
- handle.write(JSON.pretty_generate(digests))
+ handle.write(JSON.pretty_generate(resultant_emoji_map))
end
end
@@ -55,21 +55,40 @@ namespace :gemojione do
SPRITESHEET_WIDTH = 860
SPRITESHEET_HEIGHT = 840
+ # Setup a map to rename image files
+ emoji_unicode_string_to_name_map = {}
+ Gitlab::Emoji.emojis.each do |name, emoji_hash|
+ # Ignore aliases
+ unless Gitlab::Emoji.emojis_aliases.key?(name)
+ emoji_unicode_string_to_name_map[emoji_hash['unicode']] = name
+ end
+ end
+
+ # Copy the Gemojione assets to the temporary folder for renaming
+ emoji_dir = "app/assets/images/emoji"
+ FileUtils.rm_rf(emoji_dir)
+ FileUtils.mkdir_p(emoji_dir, mode: 0700)
+ FileUtils.cp_r(File.join(Gemojione.images_path, '.'), emoji_dir)
+ Dir[File.join(emoji_dir, "**/*.png")].each do |png|
+ image_path = png
+ rename_to_named_emoji_image!(emoji_unicode_string_to_name_map, image_path)
+ end
+
Dir.mktmpdir do |tmpdir|
- # Copy the Gemojione assets to the temporary folder for resizing
- FileUtils.cp_r(Gemojione.images_path, tmpdir)
+ FileUtils.cp_r(File.join(emoji_dir, '.'), tmpdir)
Dir.chdir(tmpdir) do
Dir["**/*.png"].each do |png|
- resize!(File.join(tmpdir, png), SIZE)
+ tmp_image_path = File.join(tmpdir, png)
+ resize!(tmp_image_path, SIZE)
end
end
- style_path = Rails.root.join(*%w(app assets stylesheets pages emojis.scss))
+ style_path = Rails.root.join(*%w(app assets stylesheets framework emoji-sprites.scss))
# Combine the resized assets into a packed sprite and re-generate the SCSS
SpriteFactory.cssurl = "image-url('$IMAGE')"
- SpriteFactory.run!(File.join(tmpdir, 'png'), {
+ SpriteFactory.run!(tmpdir, {
output_style: style_path,
output_image: "app/assets/images/emoji.png",
selector: '.emoji-',
@@ -83,7 +102,7 @@ namespace :gemojione do
# let's simplify it
system(%Q(sed -i '' "s/width: #{SIZE}px; height: #{SIZE}px; background: image-url('emoji.png')/background-position:/" #{style_path}))
system(%Q(sed -i '' "s/ no-repeat//" #{style_path}))
- system(%Q(sed -i '' "s/ 0px/ 0/" #{style_path}))
+ system(%Q(sed -i '' "s/ 0px/ 0/g" #{style_path}))
# Append a generic rule that applies to all Emojis
File.open(style_path, 'a') do |f|
@@ -92,6 +111,8 @@ namespace :gemojione do
.emoji-icon {
background-image: image-url('emoji.png');
background-repeat: no-repeat;
+ color: transparent;
+ text-indent: -99em;
height: #{SIZE}px;
width: #{SIZE}px;
@@ -112,16 +133,17 @@ namespace :gemojione do
# Now do it again but for Retina
Dir.mktmpdir do |tmpdir|
# Copy the Gemojione assets to the temporary folder for resizing
- FileUtils.cp_r(Gemojione.images_path, tmpdir)
+ FileUtils.cp_r(File.join(emoji_dir, '.'), tmpdir)
Dir.chdir(tmpdir) do
Dir["**/*.png"].each do |png|
- resize!(File.join(tmpdir, png), RETINA)
+ tmp_image_path = File.join(tmpdir, png)
+ resize!(tmp_image_path, RETINA)
end
end
# Combine the resized assets into a packed sprite and re-generate the SCSS
- SpriteFactory.run!(File.join(tmpdir), {
+ SpriteFactory.run!(tmpdir, {
output_image: "app/assets/images/emoji@2x.png",
style: false,
nocomments: true,
@@ -155,4 +177,20 @@ namespace :gemojione do
image.write(image_path) { self.quality = 100 }
image.destroy!
end
+
+ EMOJI_IMAGE_PATH_RE = /(.*?)(([0-9a-f]-?)+)\.png$/i
+ def rename_to_named_emoji_image!(emoji_unicode_string_to_name_map, image_path)
+ # Rename file from unicode to emoji name
+ matches = EMOJI_IMAGE_PATH_RE.match(image_path)
+ preceding_path = matches[1]
+ unicode_string = matches[2]
+ name = emoji_unicode_string_to_name_map[unicode_string]
+ if name
+ new_png_path = File.join(preceding_path, "#{name}.png")
+ FileUtils.mv(image_path, new_png_path)
+ new_png_path
+ else
+ puts "Warning: emoji_unicode_string_to_name_map missing entry for #{unicode_string}. Full path: #{image_path}"
+ end
+ end
end