BigW Consortium Gitlab

Removed API endpoint and specs

parent 439adb96
......@@ -8,8 +8,6 @@ engines:
languages:
- ruby
- javascript
exclude_paths:
- "lib/api/v3/*"
ratings:
paths:
- Gemfile.lock
......
......@@ -173,7 +173,6 @@ Lint/UriEscapeUnescape:
- 'spec/requests/api/files_spec.rb'
- 'spec/requests/api/internal_spec.rb'
- 'spec/requests/api/issues_spec.rb'
- 'spec/requests/api/v3/issues_spec.rb'
# Offense count: 1
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
......@@ -333,8 +332,6 @@ RSpec/ScatteredSetup:
- 'spec/lib/gitlab/bitbucket_import/importer_spec.rb'
- 'spec/lib/gitlab/git/env_spec.rb'
- 'spec/requests/api/jobs_spec.rb'
- 'spec/requests/api/v3/builds_spec.rb'
- 'spec/requests/api/v3/projects_spec.rb'
- 'spec/services/projects/create_service_spec.rb'
# Offense count: 1
......@@ -618,7 +615,6 @@ Style/OrAssignment:
Exclude:
- 'app/models/concerns/token_authenticatable.rb'
- 'lib/api/commit_statuses.rb'
- 'lib/api/v3/members.rb'
- 'lib/gitlab/project_transfer.rb'
# Offense count: 50
......@@ -781,7 +777,6 @@ Style/TernaryParentheses:
- 'app/finders/projects_finder.rb'
- 'app/helpers/namespaces_helper.rb'
- 'features/support/capybara.rb'
- 'lib/api/v3/projects.rb'
- 'lib/gitlab/ci/build/artifacts/metadata/entry.rb'
- 'spec/requests/api/pipeline_schedules_spec.rb'
- 'spec/support/capybara.rb'
......
......@@ -360,17 +360,6 @@ class ApplicationSetting < ActiveRecord::Base
Array(read_attribute(:repository_storages))
end
# DEPRECATED
# repository_storage is still required in the API. Remove in 9.0
# Still used in API v3
def repository_storage
repository_storages.first
end
def repository_storage=(value)
self.repository_storages = [value]
end
def default_project_visibility=(level)
super(Gitlab::VisibilityLevel.level_value(level))
end
......
# Makes api V3 compatible with old project features permissions methods
# Makes api V4 compatible with old project features permissions methods
#
# After migrating issues_enabled merge_requests_enabled builds_enabled snippets_enabled and wiki_enabled
# fields to a new table "project_features", support for the old fields is still needed in the API.
......
---
title: Removed API v3 from the codebase
merge_request: 18970
author:
type: removed
......@@ -90,24 +90,23 @@ specification.
## Compatibility Guidelines
The HTTP API is versioned using a single number, the current one being 4. This
number symbolises the same as the major version number as described by
number symbolises the same as the major version number as described by
[SemVer](https://semver.org/). This mean that backward incompatible changes
will require this version number to change. However, the minor version is
not explicit. This allows for a stable API endpoint, but also means new
not explicit. This allows for a stable API endpoint, but also means new
features can be added to the API in the same version number.
New features and bug fixes are released in tandem with a new GitLab, and apart
from incidental patch and security releases, are released on the 22nd each
month. Backward incompatible changes (e.g. endpoints removal, parameters
removal etc.), as well as removal of entire API versions are done in tandem
with a major point release of GitLab itself. All deprecations and changes
between two versions should be listed in the documentation. For the changes
month. Backward incompatible changes (e.g. endpoints removal, parameters
removal etc.), as well as removal of entire API versions are done in tandem
with a major point release of GitLab itself. All deprecations and changes
between two versions should be listed in the documentation. For the changes
between v3 and v4; please read the [v3 to v4 documentation](v3_to_v4.md)
#### Current status
Currently two API versions are available, v3 and v4. v3 is deprecated and
will soon be removed. Deletion is scheduled for
Currently only API version v4 is available. Version v3 was removed in
[GitLab 11.0](https://gitlab.com/gitlab-org/gitlab-ce/issues/36819).
## Basic usage
......
......@@ -23,7 +23,7 @@ POST /applications
| `scopes` | string | yes | The scopes of the application |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "name=MyApplication&redirect_uri=http://redirect.uri&scopes=" https://gitlab.example.com/api/v3/applications
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "name=MyApplication&redirect_uri=http://redirect.uri&scopes=" https://gitlab.example.com/api/v4/applications
```
Example response:
......
......@@ -123,7 +123,7 @@ POST /projects/:id/environments/:environment_id/stop
| `environment_id` | integer | yes | The ID of the environment |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments/1/stop"
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/environments/1/stop"
```
Example response:
......
......@@ -1169,7 +1169,7 @@ The `file=` parameter must point to a file on your filesystem and be preceded
by `@`. For example:
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "file=@dk.png" https://gitlab.example.com/api/v3/projects/5/uploads
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "file=@dk.png" https://gitlab.example.com/api/v4/projects/5/uploads
```
Returned object:
......
......@@ -2,10 +2,9 @@
Since GitLab 9.0, API V4 is the preferred version to be used.
API V3 will be unsupported from GitLab 9.5, to be released on August
22, 2017. It will be removed in GitLab 9.5 or later. In the meantime, we advise
you to make any necessary changes to applications that use V3. The V3 API
documentation is still
API V3 was unsupported from GitLab 9.5, released on August
22, 2017. API v3 was removed in [GitLab 11.0](https://gitlab.com/gitlab-org/gitlab-ce/issues/36819).
The V3 API documentation is still
[available](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable/doc/api/README.md).
Below are the changes made between V3 and V4.
......
......@@ -107,7 +107,7 @@ you will not get a shibboleth session!
RewriteEngine on
#Don't escape encoded characters in api requests
RewriteCond %{REQUEST_URI} ^/api/v3/.*
RewriteCond %{REQUEST_URI} ^/api/v4/.*
RewriteCond %{REQUEST_URI} !/Shibboleth.sso
RewriteCond %{REQUEST_URI} !/shibboleth-sp
RewriteRule .* http://127.0.0.1:8181%{REQUEST_URI} [P,QSA,NE]
......
......@@ -22,48 +22,14 @@ module API
allow_access_with_scope :api
prefix :api
version %w(v3 v4), using: :path
version 'v3', using: :path do
helpers ::API::V3::Helpers
helpers ::API::Helpers::CommonHelpers
mount ::API::V3::AwardEmoji
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
mount ::API::V3::Files
mount ::API::V3::Groups
mount ::API::V3::Issues
mount ::API::V3::Labels
mount ::API::V3::Members
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
mount ::API::V3::Templates
mount ::API::V3::Todos
mount ::API::V3::Triggers
mount ::API::V3::Users
mount ::API::V3::Variables
route :any, '*path' do
error!('API V3 is no longer supported. Use API V4 instead.', 410)
end
end
version 'v4', using: :path
before do
header['X-Frame-Options'] = 'SAMEORIGIN'
header['X-Content-Type-Options'] = 'nosniff'
......
module API
module V3
class AwardEmoji < Grape::API
include PaginationParams
before { authenticate! }
AWARDABLES = %w[issue merge_request snippet].freeze
resource :projects, requirements: { id: %r{[^/]+} } do
AWARDABLES.each do |awardable_type|
awardable_string = awardable_type.pluralize
awardable_id_string = "#{awardable_type}_id"
params do
requires :id, type: String, desc: 'The ID of a project'
requires :"#{awardable_id_string}", type: Integer, desc: "The ID of an Issue, Merge Request or Snippet"
end
[
":id/#{awardable_string}/:#{awardable_id_string}/award_emoji",
":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji"
].each do |endpoint|
desc 'Get a list of project +awardable+ award emoji' do
detail 'This feature was introduced in 8.9'
success Entities::AwardEmoji
end
params do
use :pagination
end
get endpoint do
if can_read_awardable?
awards = awardable.award_emoji
present paginate(awards), with: Entities::AwardEmoji
else
not_found!("Award Emoji")
end
end
desc 'Get a specific award emoji' do
detail 'This feature was introduced in 8.9'
success Entities::AwardEmoji
end
params do
requires :award_id, type: Integer, desc: 'The ID of the award'
end
get "#{endpoint}/:award_id" do
if can_read_awardable?
present awardable.award_emoji.find(params[:award_id]), with: Entities::AwardEmoji
else
not_found!("Award Emoji")
end
end
desc 'Award a new Emoji' do
detail 'This feature was introduced in 8.9'
success Entities::AwardEmoji
end
params do
requires :name, type: String, desc: 'The name of a award_emoji (without colons)'
end
post endpoint do
not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable?
award = awardable.create_award_emoji(params[:name], current_user)
if award.persisted?
present award, with: Entities::AwardEmoji
else
not_found!("Award Emoji #{award.errors.messages}")
end
end
desc 'Delete a +awardables+ award emoji' do
detail 'This feature was introduced in 8.9'
success Entities::AwardEmoji
end
params do
requires :award_id, type: Integer, desc: 'The ID of an award emoji'
end
delete "#{endpoint}/:award_id" do
award = awardable.award_emoji.find(params[:award_id])
unauthorized! unless award.user == current_user || current_user.admin?
award.destroy
present award, with: Entities::AwardEmoji
end
end
end
end
helpers do
def can_read_awardable?
can?(current_user, read_ability(awardable), awardable)
end
def can_award_awardable?
awardable.user_can_award?(current_user, params[:name])
end
def awardable
@awardable ||=
begin
if params.include?(:note_id)
note_id = params.delete(:note_id)
awardable.notes.find(note_id)
elsif params.include?(:issue_id)
user_project.issues.find(params[:issue_id])
elsif params.include?(:merge_request_id)
user_project.merge_requests.find(params[:merge_request_id])
else
user_project.snippets.find(params[:snippet_id])
end
end
end
def read_ability(awardable)
case awardable
when Note
read_ability(awardable.noteable)
else
:"read_#{awardable.class.to_s.underscore}"
end
end
end
end
end
end
module API
module V3
class Boards < Grape::API
before { authenticate! }
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get all project boards' do
detail 'This feature was introduced in 8.13'
success ::API::Entities::Board
end
get ':id/boards' do
authorize!(:read_board, user_project)
present user_project.boards, with: ::API::Entities::Board
end
params do
requires :board_id, type: Integer, desc: 'The ID of a board'
end
segment ':id/boards/:board_id' do
helpers do
def project_board
board = user_project.boards.first
if params[:board_id] == board.id
board
else
not_found!('Board')
end
end
def board_lists
project_board.lists.destroyable
end
end
desc 'Get the lists of a project board' do
detail 'Does not include `done` list. This feature was introduced in 8.13'
success ::API::Entities::List
end
get '/lists' do
authorize!(:read_board, user_project)
present board_lists, with: ::API::Entities::List
end
desc 'Delete a board list' do
detail 'This feature was introduced in 8.13'
success ::API::Entities::List
end
params do
requires :list_id, type: Integer, desc: 'The ID of a board list'
end
delete "/lists/:list_id" do
authorize!(:admin_list, user_project)
list = board_lists.find(params[:list_id])
service = ::Boards::Lists::DestroyService.new(user_project, current_user)
if service.execute(list)
present list, with: ::API::Entities::List
else
render_api_error!({ error: 'List could not be deleted!' }, 400)
end
end
end
end
end
end
end
require 'mime/types'
module API
module V3
class Branches < Grape::API
before { authenticate! }
before { authorize! :download_code, user_project }
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository branches' do
success ::API::Entities::Branch
end
get ":id/repository/branches" do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42276')
repository = user_project.repository
branches = repository.branches.sort_by(&:name)
merged_branch_names = repository.merged_branch_names(branches.map(&:name))
present branches, with: ::API::Entities::Branch, project: user_project, merged_branch_names: merged_branch_names
end
desc 'Delete a branch'
params do
requires :branch, type: String, desc: 'The name of the branch'
end
delete ":id/repository/branches/:branch", requirements: { branch: /.+/ } do
authorize_push_project
result = DeleteBranchService.new(user_project, current_user)
.execute(params[:branch])
if result[:status] == :success
status(200)
{
branch_name: params[:branch]
}
else
render_api_error!(result[:message], result[:return_code])
end
end
desc 'Delete all merged branches'
delete ":id/repository/merged_branches" do
DeleteMergedBranchesService.new(user_project, current_user).async_execute
status(200)
end
desc 'Create branch' do
success ::API::Entities::Branch
end
params do
requires :branch_name, type: String, desc: 'The name of the branch'
requires :ref, type: String, desc: 'Create branch from commit sha or existing branch'
end
post ":id/repository/branches" do
authorize_push_project
result = CreateBranchService.new(user_project, current_user)
.execute(params[:branch_name], params[:ref])
if result[:status] == :success
present result[:branch],
with: ::API::Entities::Branch,
project: user_project
else
render_api_error!(result[:message], 400)
end
end
end
end
end
end
module API
module V3
class BroadcastMessages < Grape::API
include PaginationParams
before { authenticate! }
before { authenticated_as_admin! }
resource :broadcast_messages do
helpers do
def find_message
BroadcastMessage.find(params[:id])
end
end
desc 'Delete a broadcast message' do
detail 'This feature was introduced in GitLab 8.12.'
success ::API::Entities::BroadcastMessage
end
params do
requires :id, type: Integer, desc: 'Broadcast message ID'
end
delete ':id' do
message = find_message
present message.destroy, with: ::API::Entities::BroadcastMessage
end
end
end
end
end
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, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS 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?(::Hash)
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])
builds = builds.preload(:user, :job_artifacts_archive, :runner, pipeline: :project)
present paginate(builds), with: ::API::V3::Entities::Build
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!
break 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
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
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_carrierwave_file!(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_carrierwave_file!(latest_build.artifacts_file)
end
# TODO: We should use `present_disk_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.raw
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])
authorize!(:update_build, build)
build.cancel
present build, with: ::API::V3::Entities::Build
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])
authorize!(:update_build, build)
break forbidden!('Build is not retryable') unless build.retryable?
build = Ci::Build.retry(build, current_user)
present build, with: ::API::V3::Entities::Build
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])
authorize!(:erase_build, build)
break forbidden!('Build is not erasable!') unless build.erasable?
build.erase(erased_by: current_user)
present build, with: ::API::V3::Entities::Build
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])
authorize!(:update_build, build)
break not_found!(build) unless build.artifacts?
build.keep_artifacts!
status 200
present build, with: ::API::V3::Entities::Build
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])
authorize!(:update_build, build)
bad_request!("Unplayable Job") unless build.playable?
build.play(current_user)
status 200
present build, with: ::API::V3::Entities::Build
end
end
helpers do
def find_build(id)
user_project.builds.find_by(id: id.to_i)
end
def get_build!(id)
find_build(id) || not_found!
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
require 'mime/types'
module API
module V3
class Commits < Grape::API
include PaginationParams
before { authenticate! }
before { authorize! :download_code, user_project }
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get a project repository commits' do
success ::API::Entities::Commit
end
params do
optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
optional :since, type: DateTime, desc: 'Only commits after or in this date will be returned'
optional :until, type: DateTime, desc: 'Only commits before or in this date will be returned'
optional :page, type: Integer, default: 0, desc: 'The page for pagination'
optional :per_page, type: Integer, default: 20, desc: 'The number of results per page'
optional :path, type: String, desc: 'The file path'
end
get ":id/repository/commits" do
ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
offset = params[:page] * params[:per_page]
commits = user_project.repository.commits(ref,
path: params[:path],
limit: params[:per_page],
offset: offset,
after: params[:since],
before: params[:until])
present commits, with: ::API::Entities::Commit
end
desc 'Commit multiple file changes as one commit' do
success ::API::Entities::CommitDetail
detail 'This feature was introduced in GitLab 8.13'
end
params do
requires :branch_name, type: String, desc: 'The name of branch'
requires :commit_message, type: String, desc: 'Commit message'
requires :actions, type: Array[Hash], desc: 'Actions to perform in commit'
optional :author_email, type: String, desc: 'Author email for commit'
optional :author_name, type: String, desc: 'Author name for commit'
end
post ":id/repository/commits" do
authorize! :push_code, user_project
attrs = declared_params.dup
branch = attrs.delete(:branch_name)
attrs.merge!(start_branch: branch, branch_name: branch)
result = ::Files::MultiService.new(user_project, current_user, attrs).execute
if result[:status] == :success
commit_detail = user_project.repository.commits(result[:result], limit: 1).first
present commit_detail, with: ::API::Entities::CommitDetail
else
render_api_error!(result[:message], 400)
end
end
desc 'Get a specific commit of a project' do
success ::API::Entities::CommitDetail
failure [[404, 'Not Found']]
end
params do
requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
optional :stats, type: Boolean, default: true, desc: 'Include commit stats'
end
get ":id/repository/commits/:sha", requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
commit = user_project.commit(params[:sha])
not_found! "Commit" unless commit
present commit, with: ::API::Entities::CommitDetail, stats: params[:stats]
end
desc 'Get the diff for a specific commit of a project' do
failure [[404, 'Not Found']]
end
params do
requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
end
get ":id/repository/commits/:sha/diff", requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
commit = user_project.commit(params[:sha])
not_found! "Commit" unless commit
commit.raw_diffs.to_a
end
desc "Get a commit's comments" do
success ::API::Entities::CommitNote
failure [[404, 'Not Found']]
end
params do
use :pagination
requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
end
get ':id/repository/commits/:sha/comments', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
commit = user_project.commit(params[:sha])
not_found! 'Commit' unless commit
notes = commit.notes.order(:created_at)
present paginate(notes), with: ::API::Entities::CommitNote
end
desc 'Cherry pick commit into a branch' do
detail 'This feature was introduced in GitLab 8.15'
success ::API::Entities::Commit
end
params do
requires :sha, type: String, desc: 'A commit sha to be cherry picked'
requires :branch, type: String, desc: 'The name of the branch'
end
post ':id/repository/commits/:sha/cherry_pick', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
authorize! :push_code, user_project
commit = user_project.commit(params[:sha])
not_found!('Commit') unless commit
branch = user_project.repository.find_branch(params[:branch])
not_found!('Branch') unless branch
commit_params = {
commit: commit,
start_branch: params[:branch],
branch_name: params[:branch]
}
result = ::Commits::CherryPickService.new(user_project, current_user, commit_params).execute
if result[:status] == :success
branch = user_project.repository.find_branch(params[:branch])
present user_project.repository.commit(branch.dereferenced_target), with: ::API::Entities::Commit
else
render_api_error!(result[:message], 400)
end
end
desc 'Post comment to commit' do
success ::API::Entities::CommitNote
end
params do
requires :sha, type: String, regexp: /\A\h{6,40}\z/, desc: "The commit's SHA"
requires :note, type: String, desc: 'The text of the comment'
optional :path, type: String, desc: 'The file path'
given :path do
requires :line, type: Integer, desc: 'The line number'
requires :line_type, type: String, values: %w(new old), default: 'new', desc: 'The type of the line'
end
end
post ':id/repository/commits/:sha/comments', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
commit = user_project.commit(params[:sha])
not_found! 'Commit' unless commit
opts = {
note: params[:note],
noteable_type: 'Commit',
commit_id: commit.id
}
if params[:path]
commit.raw_diffs(limits: false).each do |diff|
next unless diff.new_path == params[:path]
lines = Gitlab::Diff::Parser.new.parse(diff.diff.each_line)
lines.each do |line|
next unless line.new_pos == params[:line] && line.type == params[:line_type]
break opts[:line_code] = Gitlab::Git.diff_line_code(diff.new_path, line.new_pos, line.old_pos)
end
break if opts[:line_code]
end
opts[:type] = LegacyDiffNote.name if opts[:line_code]
end
note = ::Notes::CreateService.new(user_project, current_user, opts).execute
if note.save
present note, with: ::API::Entities::CommitNote
else
render_api_error!("Failed to save note #{note.errors.messages}", 400)
end
end
end
end
end
end
module API
module V3
class DeployKeys < Grape::API
before { authenticate! }
helpers do
def add_deploy_keys_project(project, attrs = {})
project.deploy_keys_projects.create(attrs)
end
def find_by_deploy_key(project, key_id)
project.deploy_keys_projects.find_by!(deploy_key: key_id)
end
end
get "deploy_keys" do
authenticated_as_admin!
keys = DeployKey.all
present keys, with: ::API::Entities::SSHKey
end
params do
requires :id, type: String, desc: 'The ID of the project'
end
resource :projects, requirements: { id: %r{[^/]+} } do
before { authorize_admin_project }
%w(keys deploy_keys).each do |path|
desc "Get a specific project's deploy keys" do
success ::API::Entities::DeployKeysProject
end
get ":id/#{path}" do
keys = user_project.deploy_keys_projects.preload(:deploy_key)
present keys, with: ::API::Entities::DeployKeysProject
end
desc 'Get single deploy key' do
success ::API::Entities::DeployKeysProject
end
params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end
get ":id/#{path}/:key_id" do
key = find_by_deploy_key(user_project, params[:key_id])
present key, with: ::API::Entities::DeployKeysProject
end
desc 'Add new deploy key to currently authenticated user' do
success ::API::Entities::DeployKeysProject
end
params do
requires :key, type: String, desc: 'The new deploy key'
requires :title, type: String, desc: 'The name of the deploy key'
optional :can_push, type: Boolean, desc: "Can deploy key push to the project's repository"
end
post ":id/#{path}" do
params[:key].strip!
# Check for an existing key joined to this project
key = user_project.deploy_keys_projects
.joins(:deploy_key)
.find_by(keys: { key: params[:key] })
if key
present key, with: ::API::Entities::DeployKeysProject
break
end
# Check for available deploy keys in other projects
key = current_user.accessible_deploy_keys.find_by(key: params[:key])
if key
added_key = add_deploy_keys_project(user_project, deploy_key: key, can_push: !!params[:can_push])
present added_key, with: ::API::Entities::DeployKeysProject
break
end
# Create a new deploy key
key_attributes = { can_push: !!params[:can_push],
deploy_key_attributes: declared_params.except(:can_push) }
key = add_deploy_keys_project(user_project, key_attributes)
if key.valid?
present key, with: ::API::Entities::DeployKeysProject
else
render_validation_error!(key)
end
end
desc 'Enable a deploy key for a project' do
detail 'This feature was added in GitLab 8.11'
success ::API::Entities::SSHKey
end
params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end
post ":id/#{path}/:key_id/enable" do
key = ::Projects::EnableDeployKeyService.new(user_project,
current_user, declared_params).execute
if key
present key, with: ::API::Entities::SSHKey
else
not_found!('Deploy Key')
end
end
desc 'Disable a deploy key for a project' do
detail 'This feature was added in GitLab 8.11'
success ::API::Entities::SSHKey
end
params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end
delete ":id/#{path}/:key_id/disable" do
key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
key.destroy
present key.deploy_key, with: ::API::Entities::SSHKey
end
desc 'Delete deploy key for a project' do
success Key
end
params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end
delete ":id/#{path}/:key_id" do
key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
if key
key.destroy
else
not_found!('Deploy Key')
end
end
end
end
end
end
end
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, requirements: { id: %r{[^/]+} } 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
module API
module V3
class Environments < Grape::API
include ::API::Helpers::CustomValidators
include PaginationParams
before { authenticate! }
params do
requires :id, type: String, desc: 'The project ID'
end
resource :projects, requirements: { id: %r{[^/]+} } 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 Entities::Environment
end
params do
requires :environment_id, type: Integer, desc: 'The environment ID'
end
delete ':id/environments/:environment_id' do
authorize! :update_environment, user_project
environment = user_project.environments.find(params[:environment_id])
present environment.destroy, with: Entities::Environment
end
end
end
end
end
module API
module V3
class Files < Grape::API
helpers do
def commit_params(attrs)
{
file_path: attrs[:file_path],
start_branch: attrs[:branch],
branch_name: attrs[:branch],
commit_message: attrs[:commit_message],
file_content: attrs[:content],
file_content_encoding: attrs[:encoding],
author_email: attrs[:author_email],
author_name: attrs[:author_name]
}
end
def commit_response(attrs)
{
file_path: attrs[:file_path],
branch: attrs[:branch]
}
end
params :simple_file_params do
requires :file_path, type: String, desc: 'The path to new file. Ex. lib/class.rb'
requires :branch_name, type: String, desc: 'The name of branch'
requires :commit_message, type: String, desc: 'Commit Message'
optional :author_email, type: String, desc: 'The email of the author'
optional :author_name, type: String, desc: 'The name of the author'
end
params :extended_file_params do
use :simple_file_params
requires :content, type: String, desc: 'File content'
optional :encoding, type: String, values: %w[base64], desc: 'File encoding'
end
end
params do
requires :id, type: String, desc: 'The project ID'
end
resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a file from repository'
params do
requires :file_path, type: String, desc: 'The path to the file. Ex. lib/class.rb'
requires :ref, type: String, desc: 'The name of branch, tag, or commit'
end
get ":id/repository/files" do
authorize! :download_code, user_project
commit = user_project.commit(params[:ref])
not_found!('Commit') unless commit
repo = user_project.repository
blob = repo.blob_at(commit.sha, params[:file_path])
not_found!('File') unless blob
blob.load_all_data!
status(200)
{
file_name: blob.name,
file_path: blob.path,
size: blob.size,
encoding: "base64",
content: Base64.strict_encode64(blob.data),
ref: params[:ref],
blob_id: blob.id,
commit_id: commit.id,
last_commit_id: repo.last_commit_id_for_path(commit.sha, params[:file_path])
}
end
desc 'Create new file in repository'
params do
use :extended_file_params
end
post ":id/repository/files" do
authorize! :push_code, user_project
file_params = declared_params(include_missing: false)
file_params[:branch] = file_params.delete(:branch_name)
result = ::Files::CreateService.new(user_project, current_user, commit_params(file_params)).execute
if result[:status] == :success
status(201)
commit_response(file_params)
else
render_api_error!(result[:message], 400)
end
end
desc 'Update existing file in repository'
params do
use :extended_file_params
end
put ":id/repository/files" do
authorize! :push_code, user_project
file_params = declared_params(include_missing: false)
file_params[:branch] = file_params.delete(:branch_name)
result = ::Files::UpdateService.new(user_project, current_user, commit_params(file_params)).execute
if result[:status] == :success
status(200)
commit_response(file_params)
else
http_status = result[:http_status] || 400
render_api_error!(result[:message], http_status)
end
end
desc 'Delete an existing file in repository'
params do
use :simple_file_params
end
delete ":id/repository/files" do
authorize! :push_code, user_project
file_params = declared_params(include_missing: false)
file_params[:branch] = file_params.delete(:branch_name)
result = ::Files::DeleteService.new(user_project, current_user, commit_params(file_params)).execute
if result[:status] == :success
status(200)
commit_response(file_params)
else
render_api_error!(result[:message], 400)
end
end
end
end
end
end
module API
module V3
class Groups < Grape::API
include PaginationParams
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: Entities::Group,
current_user: current_user
)
groups = groups.with_statistics if options[:statistics]
present paginate(groups), options
end
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(current_user).execute
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.admin?
end
desc 'Get list of owned groups for authenticated user' do
success Entities::Group
end
params do
use :pagination
use :statistics_params
end
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'
if ::Group.supports_nested_groups?
optional :parent_id, type: Integer, desc: 'The parent group id for creating nested group'
end
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, requirements: { id: %r{[^/]+} } 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
::Groups::DestroyService.new(group, current_user).async_execute
accepted!
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: group, current_user: current_user).execute
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", requirements: { 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
end
module API
module V3
module Helpers
def find_project_issue(id)
IssuesFinder.new(current_user, project_id: user_project.id).find(id)
end
def find_project_merge_request(id)
MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id)
end
def find_merge_request_with_access(id, access_level = :read_merge_request)
merge_request = user_project.merge_requests.find(id)
authorize! access_level, merge_request
merge_request
end
# 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
if params[:starred]
projects = projects.merge(current_user.starred_projects)
end
if params[:search].present?
projects = projects.search(params[:search])
end
if params[:visibility].present?
projects = projects.where(visibility_level: Gitlab::VisibilityLevel.level_value(params[:visibility]))
end
unless params[:archived].nil?
projects = projects.where(archived: to_boolean(params[:archived]))
end
projects.reorder(params[:order_by] => params[:sort])
end
end
end
end
module API
module V3
class Issues < Grape::API
include PaginationParams
before { authenticate! }
helpers do
def find_issues(args = {})
args = params.merge(args)
args = convert_parameters_from_legacy_format(args)
args.delete(:id)
args[:milestone_title] = args.delete(:milestone)
match_all_labels = args.delete(:match_all_labels)
labels = args.delete(:labels)
args[:label_name] = labels if match_all_labels
# IssuesFinder expects iids
args[:iids] = args.delete(:iid) if args.key?(:iid)
issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations
if !match_all_labels && labels.present?
issues = issues.includes(:labels).where('labels.title' => labels.split(','))
end
issues.reorder(args[:order_by] => args[:sort])
end
params :issues_params do
optional :labels, type: String, desc: 'Comma-separated list of label names'
optional :milestone, type: String, desc: 'Milestone title'
optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
desc: 'Return issues ordered by `created_at` or `updated_at` fields.'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return issues sorted in `asc` or `desc` order.'
optional :milestone, type: String, desc: 'Return issues for a specific milestone'
use :pagination
end
params :issue_params do
optional :description, type: String, desc: 'The description of an issue'
optional :assignee_id, type: Integer, desc: 'The ID of a user to assign issue'
optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign issue'
optional :labels, type: String, desc: 'Comma-separated list of label names'
optional :due_date, type: String, desc: 'Date time string in the format YEAR-MONTH-DAY'
optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential'
end
end
resource :issues do
desc "Get currently authenticated user's issues" do
success ::API::V3::Entities::Issue
end
params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues'
use :issues_params
end
get do
issues = find_issues(scope: 'authored')
present paginate(issues), with: ::API::V3::Entities::Issue, current_user: current_user
end
end
params do
requires :id, type: String, desc: 'The ID of a group'
end
resource :groups, requirements: { id: %r{[^/]+} } do
desc 'Get a list of group issues' do
success ::API::V3::Entities::Issue
end
params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues'
use :issues_params
end
get ":id/issues" do
group = find_group!(params[:id])
issues = find_issues(group_id: group.id, match_all_labels: true)
present paginate(issues), with: ::API::V3::Entities::Issue, current_user: current_user
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: { id: %r{[^/]+} } do
include TimeTrackingEndpoints
desc 'Get a list of project issues' do
detail 'iid filter is deprecated have been removed on V4'
success ::API::V3::Entities::Issue
end
params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues'
optional :iid, type: Integer, desc: 'Return the issue having the given `iid`'
use :issues_params
end
get ":id/issues" do
project = find_project!(params[:id])
issues = find_issues(project_id: project.id)
present paginate(issues), with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project
end
desc 'Get a single project issue' do
success ::API::V3::Entities::Issue
end
params do
requires :issue_id, type: Integer, desc: 'The ID of a project issue'
end
get ":id/issues/:issue_id" do
issue = find_project_issue(params[:issue_id])
present issue, with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project
end
desc 'Create a new project issue' do
success ::API::V3::Entities::Issue
end
params do
requires :title, type: String, desc: 'The title of an issue'
optional :created_at, type: DateTime,
desc: 'Date time when the issue was created. Available only for admins and project owners.'
optional :merge_request_for_resolving_discussions, type: Integer,
desc: 'The IID of a merge request for which to resolve discussions'
use :issue_params
end
post ':id/issues' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42131')
# Setting created_at time only allowed for admins and project owners
unless current_user.admin? || user_project.owner == current_user
params.delete(:created_at)
end
issue_params = declared_params(include_missing: false)
issue_params = issue_params.merge(merge_request_to_resolve_discussions_of: issue_params.delete(:merge_request_for_resolving_discussions))
issue_params = convert_parameters_from_legacy_format(issue_params)
issue = ::Issues::CreateService.new(user_project,
current_user,
issue_params.merge(request: request, api: true)).execute
render_spam_error! if issue.spam?
if issue.valid?
present issue, with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project
else
render_validation_error!(issue)
end
end
desc 'Update an existing issue' do
success ::API::V3::Entities::Issue
end
params do
requires :issue_id, type: Integer, desc: 'The ID of a project issue'
optional :title, type: String, desc: 'The title of an issue'
optional :updated_at, type: DateTime,
desc: 'Date time when the issue was updated. Available only for admins and project owners.'
optional :state_event, type: String, values: %w[reopen close], desc: 'State of the issue'
use :issue_params
at_least_one_of :title, :description, :assignee_id, :milestone_id,
:labels, :created_at, :due_date, :confidential, :state_event
end
put ':id/issues/:issue_id' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42132')
issue = user_project.issues.find(params.delete(:issue_id))
authorize! :update_issue, issue
# Setting created_at time only allowed for admins and project owners
unless current_user.admin? || user_project.owner == current_user
params.delete(:updated_at)
end
update_params = declared_params(include_missing: false).merge(request: request, api: true)
update_params = convert_parameters_from_legacy_format(update_params)
issue = ::Issues::UpdateService.new(user_project,
current_user,
update_params).execute(issue)
render_spam_error! if issue.spam?
if issue.valid?
present issue, with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project
else
render_validation_error!(issue)
end
end
desc 'Move an existing issue' do
success ::API::V3::Entities::Issue
end
params do
requires :issue_id, type: Integer, desc: 'The ID of a project issue'
requires :to_project_id, type: Integer, desc: 'The ID of the new project'
end
post ':id/issues/:issue_id/move' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42133')
issue = user_project.issues.find_by(id: params[:issue_id])
not_found!('Issue') unless issue
new_project = Project.find_by(id: params[:to_project_id])
not_found!('Project') unless new_project
begin
issue = ::Issues::MoveService.new(user_project, current_user).execute(issue, new_project)
present issue, with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project
rescue ::Issues::MoveService::MoveError => error
render_api_error!(error.message, 400)
end
end
desc 'Delete a project issue'
params do
requires :issue_id, type: Integer, desc: 'The ID of a project issue'
end
delete ":id/issues/:issue_id" do
issue = user_project.issues.find_by(id: params[:issue_id])
not_found!('Issue') unless issue
authorize!(:destroy_issue, issue)
status(200)
issue.destroy
end
end
end
end
end
module API
module V3
class Labels < Grape::API
before { authenticate! }
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get all labels of the project' do
success ::API::Entities::Label
end
get ':id/labels' do
present available_labels_for(user_project), with: ::API::Entities::Label, current_user: current_user, project: user_project
end
desc 'Delete an existing label' do
success ::API::Entities::Label
end
params do
requires :name, type: String, desc: 'The name of the label to be deleted'
end
delete ':id/labels' do
authorize! :admin_label, user_project
label = user_project.labels.find_by(title: params[:name])
not_found!('Label') unless label
present label.destroy, with: ::API::Entities::Label, current_user: current_user, project: user_project
end
end
end
end
end
module API
module V3
class Members < Grape::API
include PaginationParams
before { authenticate! }
helpers ::API::Helpers::MembersHelpers
%w[group project].each do |source_type|
params do
requires :id, type: String, desc: "The #{source_type} ID"
end
resource source_type.pluralize, requirements: { id: %r{[^/]+} } do
desc 'Gets a list of group or project members viewable by the authenticated user.' do
success ::API::Entities::Member
end
params do
optional :query, type: String, desc: 'A query string to search for members'
use :pagination
end
get ":id/members" do
source = find_source(source_type, params[:id])
members = source.members.where.not(user_id: nil).includes(:user)
members = members.joins(:user).merge(User.search(params[:query])) if params[:query].present?
members = paginate(members)
present members, with: ::API::Entities::Member
end
desc 'Gets a member of a group or project.' do
success ::API::Entities::Member
end
params do
requires :user_id, type: Integer, desc: 'The user ID of the member'
end
get ":id/members/:user_id" do
source = find_source(source_type, params[:id])
members = source.members
member = members.find_by!(user_id: params[:user_id])
present member, with: ::API::Entities::Member
end
desc 'Adds a member to a group or project.' do
success ::API::Entities::Member
end
params do
requires :user_id, type: Integer, desc: 'The user ID of the new member'
requires :access_level, type: Integer, desc: 'A valid access level (defaults: `30`, developer access level)'
optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
end
post ":id/members" do
source = find_source(source_type, params[:id])
authorize_admin_source!(source_type, source)
member = source.members.find_by(user_id: params[:user_id])
# We need this explicit check because `source.add_user` doesn't
# currently return the member created so it would return 201 even if
# the member already existed...
# The `source_type == 'group'` check is to ensure back-compatibility
# but 409 behavior should be used for both project and group members in 9.0!
conflict!('Member already exists') if source_type == 'group' && member
unless member
member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
end
if member.persisted? && member.valid?
present member, with: ::API::Entities::Member
else
# This is to ensure back-compatibility but 400 behavior should be used
# for all validation errors in 9.0!
render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
render_validation_error!(member)
end
end
desc 'Updates a member of a group or project.' do
success ::API::Entities::Member
end
params do
requires :user_id, type: Integer, desc: 'The user ID of the new member'
requires :access_level, type: Integer, desc: 'A valid access level'
optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
end
put ":id/members/:user_id" do
source = find_source(source_type, params.delete(:id))
authorize_admin_source!(source_type, source)
member = source.members.find_by!(user_id: params.delete(:user_id))
if member.update_attributes(declared_params(include_missing: false))
present member, with: ::API::Entities::Member
else
# This is to ensure back-compatibility but 400 behavior should be used
# for all validation errors in 9.0!
render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
render_validation_error!(member)
end
end
desc 'Removes a user from a group or project.'
params do
requires :user_id, type: Integer, desc: 'The user ID of the member'
end
delete ":id/members/:user_id" do
source = find_source(source_type, params[:id])
# This is to ensure back-compatibility but find_by! should be used
# in that casse in 9.0!
member = source.members.find_by(user_id: params[:user_id])
# This is to ensure back-compatibility but this should be removed in
# favor of find_by! in 9.0!
not_found!("Member: user_id:#{params[:user_id]}") if source_type == 'group' && member.nil?
# This is to ensure back-compatibility but 204 behavior should be used
# for all DELETE endpoints in 9.0!
if member.nil?
status(200 )
{ message: "Access revoked", id: params[:user_id].to_i }
else
::Members::DestroyService.new(current_user).execute(member)
present member, with: ::API::Entities::Member
end
end
end
end
end
end
end
module API
module V3
# MergeRequestDiff API
class MergeRequestDiffs < Grape::API
before { authenticate! }
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: { id: %r{[^/]+} } 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 :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.order_id_desc, 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 :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
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, requirements: { id: %r{[^/]+} } 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?
milestones = milestones.order_id_desc
present paginate(milestones), with: ::API::Entities::Milestone
end
desc 'Get all issues for a single project milestone' do
success ::API::V3::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::V3::Entities::Issue, current_user: current_user, project: user_project
end
end
end
end
end
module API
module V3
class Notes < Grape::API
include PaginationParams
before { authenticate! }
NOTEABLE_TYPES = [Issue, MergeRequest, Snippet].freeze
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: { id: %r{[^/]+} } do
NOTEABLE_TYPES.each do |noteable_type|
noteables_str = noteable_type.to_s.underscore.pluralize
desc 'Get a list of project +noteable+ notes' do
success ::API::V3::Entities::Note
end
params do
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
use :pagination
end
get ":id/#{noteables_str}/:noteable_id/notes" do
noteable = user_project.public_send(noteables_str.to_sym).find(params[:noteable_id]) # rubocop:disable GitlabSecurity/PublicSend
if can?(current_user, noteable_read_ability_name(noteable), noteable)
# We exclude notes that are cross-references and that cannot be viewed
# by the current user. By doing this exclusion at this level and not
# at the DB query level (which we cannot in that case), the current
# page can have less elements than :per_page even if
# there's more than one page.
notes =
# paginate() only works with a relation. This could lead to a
# mismatch between the pagination headers info and the actual notes
# array returned, but this is really a edge-case.
paginate(noteable.notes)
.reject { |n| n.cross_reference_not_visible_for?(current_user) }
present notes, with: ::API::V3::Entities::Note
else
not_found!("Notes")
end
end
desc 'Get a single +noteable+ note' do
success ::API::V3::Entities::Note
end
params do
requires :note_id, type: Integer, desc: 'The ID of a note'
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
end
get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
noteable = user_project.public_send(noteables_str.to_sym).find(params[:noteable_id]) # rubocop:disable GitlabSecurity/PublicSend
note = noteable.notes.find(params[:note_id])
can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user)
if can_read_note
present note, with: ::API::V3::Entities::Note
else
not_found!("Note")
end
end
desc 'Create a new +noteable+ note' do
success ::API::V3::Entities::Note
end
params do
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
requires :body, type: String, desc: 'The content of a note'
optional :created_at, type: String, desc: 'The creation date of the note'
end
post ":id/#{noteables_str}/:noteable_id/notes" do
opts = {
note: params[:body],
noteable_type: noteables_str.classify,
noteable_id: params[:noteable_id]
}
noteable = user_project.public_send(noteables_str.to_sym).find(params[:noteable_id]) # rubocop:disable GitlabSecurity/PublicSend
if can?(current_user, noteable_read_ability_name(noteable), noteable)
if params[:created_at] && (current_user.admin? || user_project.owner == current_user)
opts[:created_at] = params[:created_at]
end
note = ::Notes::CreateService.new(user_project, current_user, opts).execute
if note.valid?
present note, with: ::API::V3::Entities.const_get(note.class.name)
else
not_found!("Note #{note.errors.messages}")
end
else
not_found!("Note")
end
end
desc 'Update an existing +noteable+ note' do
success ::API::V3::Entities::Note
end
params do
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
requires :note_id, type: Integer, desc: 'The ID of a note'
requires :body, type: String, desc: 'The content of a note'
end
put ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
note = user_project.notes.find(params[:note_id])
authorize! :admin_note, note
opts = {
note: params[:body]
}
note = ::Notes::UpdateService.new(user_project, current_user, opts).execute(note)
if note.valid?
present note, with: ::API::V3::Entities::Note
else
render_api_error!("Failed to save note #{note.errors.messages}", 400)
end
end
desc 'Delete a +noteable+ note' do
success ::API::V3::Entities::Note
end
params do
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
requires :note_id, type: Integer, desc: 'The ID of a note'
end
delete ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
note = user_project.notes.find(params[:note_id])
authorize! :admin_note, note
::Notes::DestroyService.new(user_project, current_user).execute(note)
present note, with: ::API::V3::Entities::Note
end
end
end
helpers do
def noteable_read_ability_name(noteable)
"read_#{noteable.class.to_s.underscore}".to_sym
end
end
end
end
end
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, requirements: { id: %r{[^/]+} } 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
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42123')
authorize! :read_pipeline, user_project
pipelines = PipelinesFinder.new(user_project, scope: params[:scope]).execute
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
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 :confidential_issues_events, type: Boolean, desc: "Trigger hook on confidential 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, requirements: { id: %r{[^/]+} } 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
attrs = declared_params(include_missing: false)
attrs[:job_events] = attrs.delete(:build_events) if attrs.key?(:build_events)
hook = user_project.hooks.new(attrs)
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))
attrs = declared_params(include_missing: false)
attrs[:job_events] = attrs.delete(:build_events) if attrs.key?(:build_events)
if hook.update_attributes(attrs)
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
module API
module V3
class ProjectSnippets < Grape::API
include PaginationParams
before { authenticate! }
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: { id: %r{[^/]+} } do
helpers do
def handle_project_member_errors(errors)
if errors[:project_access].any?
error!(errors[:project_access], 422)
end
not_found!
end
def snippets_for_current_user
SnippetsFinder.new(current_user, project: user_project).execute
end
end
desc 'Get all project snippets' do
success ::API::V3::Entities::ProjectSnippet
end
params do
use :pagination
end
get ":id/snippets" do
present paginate(snippets_for_current_user), with: ::API::V3::Entities::ProjectSnippet
end
desc 'Get a single project snippet' do
success ::API::V3::Entities::ProjectSnippet
end
params do
requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
end
get ":id/snippets/:snippet_id" do
snippet = snippets_for_current_user.find(params[:snippet_id])
present snippet, with: ::API::V3::Entities::ProjectSnippet
end
desc 'Create a new project snippet' do
success ::API::V3::Entities::ProjectSnippet
end
params do
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'
end
post ":id/snippets" do
authorize! :create_project_snippet, user_project
snippet_params = declared_params.merge(request: request, api: true)
snippet_params[:content] = snippet_params.delete(:code)
snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute
render_spam_error! if snippet.spam?
if snippet.persisted?
present snippet, with: ::API::V3::Entities::ProjectSnippet
else
render_validation_error!(snippet)
end
end
desc 'Update an existing project snippet' do
success ::API::V3::Entities::ProjectSnippet
end
params do
requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
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'
at_least_one_of :title, :file_name, :code, :visibility_level
end
put ":id/snippets/:snippet_id" do
snippet = snippets_for_current_user.find_by(id: params.delete(:snippet_id))
not_found!('Snippet') unless snippet
authorize! :update_project_snippet, snippet
snippet_params = declared_params(include_missing: false)
.merge(request: request, api: true)
snippet_params[:content] = snippet_params.delete(:code) if snippet_params[:code].present?
UpdateSnippetService.new(user_project, current_user, snippet,
snippet_params).execute
render_spam_error! if snippet.spam?
if snippet.valid?
present snippet, with: ::API::V3::Entities::ProjectSnippet
else
render_validation_error!(snippet)
end
end
desc 'Delete a project snippet'
params do
requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
end
delete ":id/snippets/:snippet_id" do
snippet = snippets_for_current_user.find_by(id: params[:snippet_id])
not_found!('Snippet') unless snippet
authorize! :admin_project_snippet, snippet
snippet.destroy
status(200)
end
desc 'Get a raw project snippet'
params do
requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
end
get ":id/snippets/:snippet_id/raw" do
snippet = snippets_for_current_user.find_by(id: params[:snippet_id])
not_found!('Snippet') unless snippet
env['api.format'] = :txt
content_type 'text/plain'
present snippet.content
end
end
end
end
end
require 'mime/types'
module API
module V3
class Repositories < Grape::API
before { authorize! :download_code, user_project }
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
helpers do
def handle_project_member_errors(errors)
if errors[:project_access].any?
error!(errors[:project_access], 422)
end
not_found!
end
end
desc 'Get a project repository tree' do
success ::API::Entities::TreeObject
end
params do
optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
optional :path, type: String, desc: 'The path of the tree'
optional :recursive, type: Boolean, default: false, desc: 'Used to get a recursive tree'
end
get ':id/repository/tree' do
ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
path = params[:path] || nil
commit = user_project.commit(ref)
not_found!('Tree') unless commit
tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive])
present tree.sorted_entries, with: ::API::Entities::TreeObject
end
desc 'Get a raw file contents'
params do
requires :sha, type: String, desc: 'The commit, branch name, or tag name'
requires :filepath, type: String, desc: 'The path to the file to display'
end
get [":id/repository/blobs/:sha", ":id/repository/commits/:sha/blob"], requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
repo = user_project.repository
commit = repo.commit(params[:sha])
not_found! "Commit" unless commit
blob = Gitlab::Git::Blob.find(repo, commit.id, params[:filepath])
not_found! "File" unless blob
send_git_blob repo, blob
end
desc 'Get a raw blob contents by blob sha'
params do
requires :sha, type: String, desc: 'The commit, branch name, or tag name'
end
get ':id/repository/raw_blobs/:sha', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
repo = user_project.repository
begin
blob = Gitlab::Git::Blob.raw(repo, params[:sha])
rescue
not_found! 'Blob'
end
not_found! 'Blob' unless blob
send_git_blob repo, blob
end
desc 'Get an archive of the repository'
params do
optional :sha, type: String, desc: 'The commit sha of the archive to be downloaded'
optional :format, type: String, desc: 'The archive format'
end
get ':id/repository/archive', requirements: { format: Gitlab::PathRegex.archive_formats_regex } do
begin
send_git_archive user_project.repository, ref: params[:sha], format: params[:format], append_sha: true
rescue
not_found!('File')
end
end
desc 'Compare two branches, tags, or commits' do
success ::API::Entities::Compare
end
params do
requires :from, type: String, desc: 'The commit, branch name, or tag name to start comparison'
requires :to, type: String, desc: 'The commit, branch name, or tag name to stop comparison'
end
get ':id/repository/compare' do
compare = Gitlab::Git::Compare.new(user_project.repository.raw_repository, params[:from], params[:to])
present compare, with: ::API::Entities::Compare
end
desc 'Get repository contributors' do
success ::API::Entities::Contributor
end
get ':id/repository/contributors' do
begin
present user_project.repository.contributors,
with: ::API::Entities::Contributor
rescue
not_found!
end
end
end
end
end
end
module API
module V3
class Runners < Grape::API
include PaginationParams
before { authenticate! }
resource :runners do
desc 'Remove a runner' do
success ::API::Entities::Runner
end
params do
requires :id, type: Integer, desc: 'The ID of the runner'
end
delete ':id' do
runner = Ci::Runner.find(params[:id])
not_found!('Runner') unless runner
authenticate_delete_runner!(runner)
status(200)
runner.destroy
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: { id: %r{[^/]+} } do
before { authorize_admin_project }
desc "Disable project's runner" do
success ::API::Entities::Runner
end
params do
requires :runner_id, type: Integer, desc: 'The ID of the runner'
end
delete ':id/runners/:runner_id' do
runner_project = user_project.runner_projects.find_by(runner_id: params[:runner_id])
not_found!('Runner') unless runner_project
runner = runner_project.runner
forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.projects.count == 1
runner_project.destroy
present runner, with: ::API::Entities::Runner
end
end
helpers do
def authenticate_delete_runner!(runner)
return if current_user.admin?
forbidden!("Runner is shared") if runner.is_shared?
forbidden!("Runner associated with more than one project") if runner.projects.count > 1
forbidden!("No access granted") unless user_can_access_runner?(runner)
end
def user_can_access_runner?(runner)
current_user.ci_owned_runners.exists?(runner.id)
end
end
end
end
end
module API
module V3
class Snippets < Grape::API
include PaginationParams
before { authenticate! }
resource :snippets do
helpers do
def snippets_for_current_user
SnippetsFinder.new(current_user, author: current_user).execute
end
def public_snippets
SnippetsFinder.new(current_user, visibility: Snippet::PUBLIC).execute
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))
break 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))
break 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))
break not_found!('Snippet') unless snippet
env['api.format'] = :txt
content_type 'text/plain'
present snippet.content
end
end
end
end
end
module API
module V3
class Subscriptions < Grape::API
before { authenticate! }
subscribable_types = {
'merge_request' => proc { |id| find_merge_request_with_access(id, :update_merge_request) },
'merge_requests' => proc { |id| find_merge_request_with_access(id, :update_merge_request) },
'issues' => proc { |id| find_project_issue(id) },
'labels' => proc { |id| find_project_label(id) }
}
params do
requires :id, type: String, desc: 'The ID of a project'
requires :subscribable_id, type: String, desc: 'The ID of a resource'
end
resource :projects, requirements: { id: %r{[^/]+} } do
subscribable_types.each do |type, finder|
type_singularized = type.singularize
entity_class = ::API::Entities.const_get(type_singularized.camelcase)
desc 'Subscribe to a resource' do
success entity_class
end
post ":id/#{type}/:subscribable_id/subscription" do
resource = instance_exec(params[:subscribable_id], &finder)
if resource.subscribed?(current_user, user_project)
not_modified!
else
resource.subscribe(current_user, user_project)
present resource, with: entity_class, current_user: current_user, project: user_project
end
end
desc 'Unsubscribe from a resource' do
success entity_class
end
delete ":id/#{type}/:subscribable_id/subscription" do
resource = instance_exec(params[:subscribable_id], &finder)
if !resource.subscribed?(current_user, user_project)
not_modified!
else
resource.unsubscribe(current_user, user_project)
present resource, with: entity_class, current_user: current_user, project: user_project
end
end
end
end
end
end
end
module API
module V3
class SystemHooks < Grape::API
before do
authenticate!
authenticated_as_admin!
end
resource :hooks do
desc 'Get the list of system hooks' do
success ::API::Entities::Hook
end
get do
present SystemHook.all, with: ::API::Entities::Hook
end
desc 'Delete a hook' do
success ::API::Entities::Hook
end
params do
requires :id, type: Integer, desc: 'The ID of the system hook'
end
delete ":id" do
hook = SystemHook.find_by(id: params[:id])
not_found!('System hook') unless hook
present hook.destroy, with: ::API::Entities::Hook
end
end
end
end
end
module API
module V3
class Tags < Grape::API
before { authorize! :download_code, user_project }
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository tags' do
success ::API::Entities::Tag
end
get ":id/repository/tags" do
tags = user_project.repository.tags.sort_by(&:name).reverse
present tags, with: ::API::Entities::Tag, project: user_project
end
desc 'Delete a repository tag'
params do
requires :tag_name, type: String, desc: 'The name of the tag'
end
delete ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do
authorize_push_project
result = ::Tags::DestroyService.new(user_project, current_user)
.execute(params[:tag_name])
if result[:status] == :success
status(200)
{
tag_name: params[:tag_name]
}
else
render_api_error!(result[:message], result[:return_code])
end
end
end
end
end
end
module API
module V3
class Templates < Grape::API
GLOBAL_TEMPLATE_TYPES = {
gitignores: {
klass: Gitlab::Template::GitignoreTemplate,
gitlab_version: 8.8
},
gitlab_ci_ymls: {
klass: Gitlab::Template::GitlabCiYmlTemplate,
gitlab_version: 8.9
},
dockerfiles: {
klass: Gitlab::Template::DockerfileTemplate,
gitlab_version: 8.15
}
}.freeze
PROJECT_TEMPLATE_REGEX =
%r{[\<\{\[]
(project|description|
one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here
[\>\}\]]}xi.freeze
YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze
FULLNAME_TEMPLATE_REGEX =
%r{[\<\{\[]
(fullname|name\sof\s(author|copyright\sowner))
[\>\}\]]}xi.freeze
DEPRECATION_MESSAGE = ' This endpoint is deprecated and has been removed in V4.'.freeze
helpers do
def parsed_license_template
# We create a fresh Licensee::License object since we'll modify its
# content in place below.
template = Licensee::License.new(params[:name])
template.content.gsub!(YEAR_TEMPLATE_REGEX, Time.now.year.to_s)
template.content.gsub!(PROJECT_TEMPLATE_REGEX, params[:project]) if params[:project].present?
fullname = params[:fullname].presence || current_user.try(:name)
template.content.gsub!(FULLNAME_TEMPLATE_REGEX, fullname) if fullname
template
end
def render_response(template_type, template)
not_found!(template_type.to_s.singularize) unless template
present template, with: ::API::Entities::Template
end
end
{ "licenses" => :deprecated, "templates/licenses" => :ok }.each do |route, status|
desc 'Get the list of the available license template' do
detailed_desc = 'This feature was introduced in GitLab 8.7.'
detailed_desc << DEPRECATION_MESSAGE unless status == :ok
detail detailed_desc
success ::API::Entities::License
end
params do
optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses'
end
get route do
options = {
featured: declared(params)[:popular].present? ? true : nil
}
present Licensee::License.all(options), with: ::API::Entities::License
end
end
{ "licenses/:name" => :deprecated, "templates/licenses/:name" => :ok }.each do |route, status|
desc 'Get the text for a specific license' do
detailed_desc = 'This feature was introduced in GitLab 8.7.'
detailed_desc << DEPRECATION_MESSAGE unless status == :ok
detail detailed_desc
success ::API::Entities::License
end
params do
requires :name, type: String, desc: 'The name of the template'
end
get route, requirements: { name: /[\w\.-]+/ } do
not_found!('License') unless Licensee::License.find(declared(params)[:name])
template = parsed_license_template
present template, with: ::API::Entities::License
end
end
GLOBAL_TEMPLATE_TYPES.each do |template_type, properties|
klass = properties[:klass]
gitlab_version = properties[:gitlab_version]
{ template_type => :deprecated, "templates/#{template_type}" => :ok }.each do |route, status|
desc 'Get the list of the available template' do
detailed_desc = "This feature was introduced in GitLab #{gitlab_version}."
detailed_desc << DEPRECATION_MESSAGE unless status == :ok
detail detailed_desc
success ::API::Entities::TemplatesList
end
get route do
present klass.all, with: ::API::Entities::TemplatesList
end
end
{ "#{template_type}/:name" => :deprecated, "templates/#{template_type}/:name" => :ok }.each do |route, status|
desc 'Get the text for a specific template present in local filesystem' do
detailed_desc = "This feature was introduced in GitLab #{gitlab_version}."
detailed_desc << DEPRECATION_MESSAGE unless status == :ok
detail detailed_desc
success ::API::Entities::Template
end
params do
requires :name, type: String, desc: 'The name of the template'
end
get route do
new_template = klass.find(declared(params)[:name])
render_response(template_type, new_template)
end
end
end
end
end
end
module API
module V3
module TimeTrackingEndpoints
extend ActiveSupport::Concern
included do
helpers do
def issuable_name
declared_params.key?(:issue_id) ? 'issue' : 'merge_request'
end
def issuable_key
"#{issuable_name}_id".to_sym
end
def update_issuable_key
"update_#{issuable_name}".to_sym
end
def read_issuable_key
"read_#{issuable_name}".to_sym
end
def load_issuable
@issuable ||= begin
case issuable_name
when 'issue'
find_project_issue(params.delete(issuable_key))
when 'merge_request'
find_project_merge_request(params.delete(issuable_key))
end
end
end
def update_issuable(attrs)
custom_params = declared_params(include_missing: false)
custom_params.merge!(attrs)
issuable = update_service.new(user_project, current_user, custom_params).execute(load_issuable)
if issuable.valid?
present issuable, with: ::API::Entities::IssuableTimeStats
else
render_validation_error!(issuable)
end
end
def update_service
issuable_name == 'issue' ? ::Issues::UpdateService : ::MergeRequests::UpdateService
end
end
issuable_name = name.end_with?('Issues') ? 'issue' : 'merge_request'
issuable_collection_name = issuable_name.pluralize
issuable_key = "#{issuable_name}_id".to_sym
desc "Set a time estimate for a project #{issuable_name}"
params do
requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
requires :duration, type: String, desc: 'The duration to be parsed'
end
post ":id/#{issuable_collection_name}/:#{issuable_key}/time_estimate" do
authorize! update_issuable_key, load_issuable
status :ok
update_issuable(time_estimate: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)))
end
desc "Reset the time estimate for a project #{issuable_name}"
params do
requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
end
post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_time_estimate" do
authorize! update_issuable_key, load_issuable
status :ok
update_issuable(time_estimate: 0)
end
desc "Add spent time for a project #{issuable_name}"
params do
requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
requires :duration, type: String, desc: 'The duration to be parsed'
end
post ":id/#{issuable_collection_name}/:#{issuable_key}/add_spent_time" do
authorize! update_issuable_key, load_issuable
update_issuable(spend_time: {
duration: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)),
user_id: current_user.id
})
end
desc "Reset spent time for a project #{issuable_name}"
params do
requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
end
post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_spent_time" do
authorize! update_issuable_key, load_issuable
status :ok
update_issuable(spend_time: { duration: :reset, user_id: current_user.id })
end
desc "Show time stats for a project #{issuable_name}"
params do
requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
end
get ":id/#{issuable_collection_name}/:#{issuable_key}/time_stats" do
authorize! read_issuable_key, load_issuable
present load_issuable, with: ::API::Entities::IssuableTimeStats
end
end
end
end
end
module API
module V3
class Todos < Grape::API
before { authenticate! }
resource :todos do
desc 'Mark a todo as done' do
success ::API::Entities::Todo
end
params do
requires :id, type: Integer, desc: 'The ID of the todo being marked as done'
end
delete ':id' do
TodoService.new.mark_todos_as_done_by_ids(params[:id], current_user)
todo = current_user.todos.find(params[:id])
present todo, with: ::API::Entities::Todo, current_user: current_user
end
desc 'Mark all todos as done'
delete do
status(200)
todos = TodosFinder.new(current_user, params).execute
TodoService.new.mark_todos_as_done(todos, current_user).size
end
end
end
end
end
module API
module V3
class Triggers < Grape::API
include PaginationParams
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: { id: %r{[^/]+} } 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", requirements: { ref: /.+/ } do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42121')
# validate variables
params[:variables] = params[:variables].to_h
unless params[: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
project = find_project(params[:id])
not_found! unless project
result = Ci::PipelineTriggerService.new(project, nil, params).execute
not_found! unless result
if result[:http_status]
render_api_error!(result[:message], result[:http_status])
else
pipeline = result[:pipeline]
# We switched to Ci::PipelineVariable from Ci::TriggerRequest.variables.
# Ci::TriggerRequest doesn't save variables anymore.
# Here is copying Ci::PipelineVariable to Ci::TriggerRequest.variables for presenting the variables.
# The same endpoint in v4 API pressents Pipeline instead of TriggerRequest, so it doesn't need such a process.
trigger_request = pipeline.trigger_requests.last
trigger_request.variables = params[:variables]
present trigger_request, with: ::API::V3::Entities::TriggerRequest
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)
break 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::V3::Entities::Trigger
end
params do
requires :token, type: String, desc: 'The unique token of trigger'
end
delete ':id/triggers/:token' do
authenticate!
authorize! :admin_build, user_project
trigger = user_project.triggers.find_by(token: params[:token].to_s)
break not_found!('Trigger') unless trigger
trigger.destroy
present trigger, with: ::API::V3::Entities::Trigger
end
end
end
end
end
module API
module V3
class Users < Grape::API
include PaginationParams
include APIGuard
allow_access_with_scope :read_user, if: -> (request) { request.get? }
before do
authenticate!
end
resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
helpers do
params :optional_attributes do
optional :skype, type: String, desc: 'The Skype username'
optional :linkedin, type: String, desc: 'The LinkedIn username'
optional :twitter, type: String, desc: 'The Twitter username'
optional :website_url, type: String, desc: 'The website of the user'
optional :organization, type: String, desc: 'The organization of the user'
optional :projects_limit, type: Integer, desc: 'The number of projects a user can create'
optional :extern_uid, type: String, desc: 'The external authentication provider UID'
optional :provider, type: String, desc: 'The external provider'
optional :bio, type: String, desc: 'The biography of the user'
optional :location, type: String, desc: 'The location of the user'
optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator'
optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups'
optional :confirm, type: Boolean, default: true, desc: 'Flag indicating the account needs to be confirmed'
optional :external, type: Boolean, desc: 'Flag indicating the user is an external user'
all_or_none_of :extern_uid, :provider
end
end
desc 'Create a user. Available only for admins.' do
success ::API::Entities::UserPublic
end
params do
requires :email, type: String, desc: 'The email of the user'
optional :password, type: String, desc: 'The password of the new user'
optional :reset_password, type: Boolean, desc: 'Flag indicating the user will be sent a password reset token'
at_least_one_of :password, :reset_password
requires :name, type: String, desc: 'The name of the user'
requires :username, type: String, desc: 'The username of the user'
use :optional_attributes
end
post do
authenticated_as_admin!
params = declared_params(include_missing: false)
user = ::Users::CreateService.new(current_user, params.merge!(skip_confirmation: !params[:confirm])).execute
if user.persisted?
present user, with: ::API::Entities::UserPublic
else
conflict!('Email has already been taken') if User
.where(email: user.email)
.count > 0
conflict!('Username has already been taken') if User
.where(username: user.username)
.count > 0
render_validation_error!(user)
end
end
desc 'Get the SSH keys of a specified user. Available only for admins.' do
success ::API::Entities::SSHKey
end
params do
requires :id, type: Integer, desc: 'The ID of the user'
use :pagination
end
get ':id/keys' do
authenticated_as_admin!
user = User.find_by(id: params[:id])
not_found!('User') unless user
present paginate(user.keys), with: ::API::Entities::SSHKey
end
desc 'Get the emails addresses of a specified user. Available only for admins.' do
success ::API::Entities::Email
end
params do
requires :id, type: Integer, desc: 'The ID of the user'
use :pagination
end
get ':id/emails' do
authenticated_as_admin!
user = User.find_by(id: params[:id])
not_found!('User') unless user
present user.emails, with: ::API::Entities::Email
end
desc 'Block a user. Available only for admins.'
params do
requires :id, type: Integer, desc: 'The ID of the user'
end
put ':id/block' do
authenticated_as_admin!
user = User.find_by(id: params[:id])
not_found!('User') unless user
if !user.ldap_blocked?
user.block
else
forbidden!('LDAP blocked users cannot be modified by the API')
end
end
desc 'Unblock a user. Available only for admins.'
params do
requires :id, type: Integer, desc: 'The ID of the user'
end
put ':id/unblock' do
authenticated_as_admin!
user = User.find_by(id: params[:id])
not_found!('User') unless user
if user.ldap_blocked?
forbidden!('LDAP blocked users cannot be unblocked by the API')
else
user.activate
end
end
desc 'Get the contribution events of a specified user' do
detail 'This feature was introduced in GitLab 8.13.'
success ::API::V3::Entities::Event
end
params do
requires :id, type: Integer, desc: 'The ID of the user'
use :pagination
end
get ':id/events' do
user = User.find_by(id: params[:id])
not_found!('User') unless user
events = user.events
.merge(ProjectsFinder.new(current_user: current_user).execute)
.references(:project)
.with_associations
.recent
present paginate(events), with: ::API::V3::Entities::Event
end
desc 'Delete an existing SSH key from a specified user. Available only for admins.' do
success ::API::Entities::SSHKey
end
params do
requires :id, type: Integer, desc: 'The ID of the user'
requires :key_id, type: Integer, desc: 'The ID of the SSH key'
end
delete ':id/keys/:key_id' do
authenticated_as_admin!
user = User.find_by(id: params[:id])
not_found!('User') unless user
key = user.keys.find_by(id: params[:key_id])
not_found!('Key') unless key
present key.destroy, with: ::API::Entities::SSHKey
end
end
resource :user do
desc "Get the currently authenticated user's SSH keys" do
success ::API::Entities::SSHKey
end
params do
use :pagination
end
get "keys" do
present current_user.keys, with: ::API::Entities::SSHKey
end
desc "Get the currently authenticated user's email addresses" do
success ::API::Entities::Email
end
get "emails" do
present current_user.emails, with: ::API::Entities::Email
end
desc 'Delete an SSH key from the currently authenticated user' do
success ::API::Entities::SSHKey
end
params do
requires :key_id, type: Integer, desc: 'The ID of the SSH key'
end
delete "keys/:key_id" do
key = current_user.keys.find_by(id: params[:key_id])
not_found!('Key') unless key
present key.destroy, with: ::API::Entities::SSHKey
end
end
end
end
end
module API
module V3
class Variables < Grape::API
include PaginationParams
before { authenticate! }
before { authorize! :admin_build, user_project }
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Delete an existing variable from a project' do
success ::API::Entities::Variable
end
params do
requires :key, type: String, desc: 'The key of the variable'
end
delete ':id/variables/:key' do
variable = user_project.variables.find_by(key: params[:key])
not_found!('Variable') unless variable
present variable.destroy, with: ::API::Entities::Variable
end
end
end
end
end
......@@ -29,28 +29,28 @@ module Gitlab
end
def user
api.get("/api/v3/user").parsed
api.get("/api/v4/user").parsed
end
def issues(project_identifier)
lazy_page_iterator(PER_PAGE) do |page|
api.get("/api/v3/projects/#{project_identifier}/issues?per_page=#{PER_PAGE}&page=#{page}").parsed
api.get("/api/v4/projects/#{project_identifier}/issues?per_page=#{PER_PAGE}&page=#{page}").parsed
end
end
def issue_comments(project_identifier, issue_id)
lazy_page_iterator(PER_PAGE) do |page|
api.get("/api/v3/projects/#{project_identifier}/issues/#{issue_id}/notes?per_page=#{PER_PAGE}&page=#{page}").parsed
api.get("/api/v4/projects/#{project_identifier}/issues/#{issue_id}/notes?per_page=#{PER_PAGE}&page=#{page}").parsed
end
end
def project(id)
api.get("/api/v3/projects/#{id}").parsed
api.get("/api/v4/projects/#{id}").parsed
end
def projects
lazy_page_iterator(PER_PAGE) do |page|
api.get("/api/v3/projects?per_page=#{PER_PAGE}&page=#{page}").parsed
api.get("/api/v4/projects?per_page=#{PER_PAGE}&page=#{page}").parsed
end
end
......
......@@ -25,7 +25,7 @@ module Gitlab
body = @formatter.author_line(issue["author"]["name"])
body += issue["description"]
comments = client.issue_comments(project_identifier, issue["id"])
comments = client.issue_comments(project_identifier, issue["iid"])
if comments.any?
body += @formatter.comments_header
......
......@@ -17,7 +17,7 @@ module Gitlab
path: repo["path"],
description: repo["description"],
namespace_id: namespace.id,
visibility_level: repo["visibility_level"],
visibility_level: Gitlab::VisibilityLevel.level_value(repo["visibility"]),
import_type: "gitlab",
import_source: repo["path_with_namespace"],
import_url: repo["http_url_to_repo"].sub("://", "://oauth2:#{@session_data[:gitlab_access_token]}@")
......
......@@ -36,7 +36,7 @@ describe QA::Runtime::API::Request do
end
it 'uses a different api version' do
expect(request.request_path('/users', version: 'v3')).to eq '/api/v3/users'
expect(request.request_path('/users', version: 'other_version')).to eq '/api/other_version/users'
end
end
end
{
"type": "array",
"items": {
"type": "object",
"properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"project_id": { "type": "integer" },
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"state": { "type": "string" },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"milestone": {
"type": "object",
"properties": {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"project_id": { "type": ["integer", "null"] },
"group_id": { "type": ["integer", "null"] },
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"state": { "type": "string" },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
"due_date": { "type": "date" },
"start_date": { "type": "date" }
},
"additionalProperties": false
},
"assignee": {
"type": ["object", "null"],
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"additionalProperties": false
},
"author": {
"type": "object",
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"additionalProperties": false
},
"user_notes_count": { "type": "integer" },
"upvotes": { "type": "integer" },
"downvotes": { "type": "integer" },
"due_date": { "type": ["date", "null"] },
"confidential": { "type": "boolean" },
"web_url": { "type": "uri" },
"subscribed": { "type": ["boolean"] }
},
"required": [
"id", "iid", "project_id", "title", "description",
"state", "created_at", "updated_at", "labels",
"milestone", "assignee", "author", "user_notes_count",
"upvotes", "downvotes", "due_date", "confidential",
"web_url", "subscribed"
],
"additionalProperties": false
}
}
{
"type": "array",
"items": {
"type": "object",
"properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"project_id": { "type": "integer" },
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"state": { "type": "string" },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
"target_branch": { "type": "string" },
"source_branch": { "type": "string" },
"upvotes": { "type": "integer" },
"downvotes": { "type": "integer" },
"author": {
"type": "object",
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"additionalProperties": false
},
"assignee": {
"type": "object",
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"additionalProperties": false
},
"source_project_id": { "type": "integer" },
"target_project_id": { "type": "integer" },
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"work_in_progress": { "type": "boolean" },
"milestone": {
"type": ["object", "null"],
"properties": {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"project_id": { "type": ["integer", "null"] },
"group_id": { "type": ["integer", "null"] },
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"state": { "type": "string" },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
"due_date": { "type": "date" },
"start_date": { "type": "date" }
},
"additionalProperties": false
},
"merge_when_build_succeeds": { "type": "boolean" },
"merge_status": { "type": "string" },
"sha": { "type": "string" },
"merge_commit_sha": { "type": ["string", "null"] },
"user_notes_count": { "type": "integer" },
"should_remove_source_branch": { "type": ["boolean", "null"] },
"force_remove_source_branch": { "type": ["boolean", "null"] },
"web_url": { "type": "uri" },
"subscribed": { "type": ["boolean"] },
"squash": { "type": "boolean" }
},
"required": [
"id", "iid", "project_id", "title", "description",
"state", "created_at", "updated_at", "target_branch",
"source_branch", "upvotes", "downvotes", "author",
"assignee", "source_project_id", "target_project_id",
"labels", "work_in_progress", "milestone", "merge_when_build_succeeds",
"merge_status", "sha", "merge_commit_sha", "user_notes_count",
"should_remove_source_branch", "force_remove_source_branch",
"web_url", "subscribed", "squash"
],
"additionalProperties": false
}
}
......@@ -20,7 +20,7 @@ describe Gitlab::GitlabImport::Importer do
}
}
])
stub_request('issues/2579857/notes', [])
stub_request('issues/3/notes', [])
end
it 'persists issues' do
......@@ -43,7 +43,7 @@ describe Gitlab::GitlabImport::Importer do
end
def stub_request(path, body)
url = "https://gitlab.com/api/v3/projects/asd%2Fvim/#{path}?page=1&per_page=100"
url = "https://gitlab.com/api/v4/projects/asd%2Fvim/#{path}?page=1&per_page=100"
WebMock.stub_request(:get, url)
.to_return(
......
......@@ -195,22 +195,6 @@ describe ApplicationSetting do
expect(setting.pick_repository_storage).to eq('random')
end
describe '#repository_storage' do
it 'returns the first storage' do
setting.repository_storages = %w(good bad)
expect(setting.repository_storage).to eq('good')
end
end
describe '#repository_storage=' do
it 'overwrites repository_storages' do
setting.repository_storage = 'overwritten'
expect(setting.repository_storages).to eq(['overwritten'])
end
end
end
end
......
require 'spec_helper'
describe API::V3::Boards do
set(:user) { create(:user) }
set(:guest) { create(:user) }
set(:non_member) { create(:user) }
set(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
set(:dev_label) do
create(:label, title: 'Development', color: '#FFAABB', project: project)
end
set(:test_label) do
create(:label, title: 'Testing', color: '#FFAACC', project: project)
end
set(:dev_list) do
create(:list, label: dev_label, position: 1)
end
set(:test_list) do
create(:list, label: test_label, position: 2)
end
set(:board) do
create(:board, project: project, lists: [dev_list, test_list])
end
before do
project.add_reporter(user)
project.add_guest(guest)
end
describe "GET /projects/:id/boards" do
let(:base_url) { "/projects/#{project.id}/boards" }
context "when unauthenticated" do
it "returns authentication error" do
get v3_api(base_url)
expect(response).to have_gitlab_http_status(401)
end
end
context "when authenticated" do
it "returns the project issue board" do
get v3_api(base_url, user)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(board.id)
expect(json_response.first['lists']).to be_an Array
expect(json_response.first['lists'].length).to eq(2)
expect(json_response.first['lists'].last).to have_key('position')
end
end
end
describe "GET /projects/:id/boards/:board_id/lists" do
let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
it 'returns issue board lists' do
get v3_api(base_url, user)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['label']['name']).to eq(dev_label.title)
end
it 'returns 404 if board not found' do
get v3_api("/projects/#{project.id}/boards/22343/lists", user)
expect(response).to have_gitlab_http_status(404)
end
end
describe "DELETE /projects/:id/board/lists/:list_id" do
let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
it "rejects a non member from deleting a list" do
delete v3_api("#{base_url}/#{dev_list.id}", non_member)
expect(response).to have_gitlab_http_status(403)
end
it "rejects a user with guest role from deleting a list" do
delete v3_api("#{base_url}/#{dev_list.id}", guest)
expect(response).to have_gitlab_http_status(403)
end
it "returns 404 error if list id not found" do
delete v3_api("#{base_url}/44444", user)
expect(response).to have_gitlab_http_status(404)
end
context "when the user is project owner" do
set(:owner) { create(:user) }
before do
project.update(namespace: owner.namespace)
end
it "deletes the list if an admin requests it" do
delete v3_api("#{base_url}/#{dev_list.id}", owner)
expect(response).to have_gitlab_http_status(200)
end
end
end
end
require 'spec_helper'
require 'mime/types'
describe API::V3::Branches do
set(:user) { create(:user) }
set(:user2) { create(:user) }
set(:project) { create(:project, :repository, creator: user) }
set(:master) { create(:project_member, :master, user: user, project: project) }
set(:guest) { create(:project_member, :guest, user: user2, project: project) }
let!(:branch_name) { 'feature' }
let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") }
describe "GET /projects/:id/repository/branches" do
it "returns an array of project branches" do
project.repository.expire_all_method_caches
get v3_api("/projects/#{project.id}/repository/branches", user), per_page: 100
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
branch_names = json_response.map { |x| x['name'] }
expect(branch_names).to match_array(project.repository.branch_names)
end
end
describe "DELETE /projects/:id/repository/branches/:branch" do
before do
allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true)
end
it "removes branch" do
delete v3_api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['branch_name']).to eq(branch_name)
end
it "removes a branch with dots in the branch name" do
delete v3_api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['branch_name']).to eq("with.1.2.3")
end
it 'returns 404 if branch not exists' do
delete v3_api("/projects/#{project.id}/repository/branches/foobar", user)
expect(response).to have_gitlab_http_status(404)
end
end
describe "DELETE /projects/:id/repository/merged_branches" do
before do
allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true)
end
it 'returns 200' do
delete v3_api("/projects/#{project.id}/repository/merged_branches", user)
expect(response).to have_gitlab_http_status(200)
end
it 'returns a 403 error if guest' do
delete v3_api("/projects/#{project.id}/repository/merged_branches", user2)
expect(response).to have_gitlab_http_status(403)
end
end
describe "POST /projects/:id/repository/branches" do
it "creates a new branch" do
post v3_api("/projects/#{project.id}/repository/branches", user),
branch_name: 'feature1',
ref: branch_sha
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq('feature1')
expect(json_response['commit']['id']).to eq(branch_sha)
end
it "denies for user without push access" do
post v3_api("/projects/#{project.id}/repository/branches", user2),
branch_name: branch_name,
ref: branch_sha
expect(response).to have_gitlab_http_status(403)
end
it 'returns 400 if branch name is invalid' do
post v3_api("/projects/#{project.id}/repository/branches", user),
branch_name: 'new design',
ref: branch_sha
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq('Branch name is invalid')
end
it 'returns 400 if branch already exists' do
post v3_api("/projects/#{project.id}/repository/branches", user),
branch_name: 'new_design1',
ref: branch_sha
expect(response).to have_gitlab_http_status(201)
post v3_api("/projects/#{project.id}/repository/branches", user),
branch_name: 'new_design1',
ref: branch_sha
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq('Branch already exists')
end
it 'returns 400 if ref name is invalid' do
post v3_api("/projects/#{project.id}/repository/branches", user),
branch_name: 'new_design3',
ref: 'foo'
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq('Invalid reference name')
end
end
end
require 'spec_helper'
describe API::V3::BroadcastMessages do
set(:user) { create(:user) }
set(:admin) { create(:admin) }
describe 'DELETE /broadcast_messages/:id' do
set(:message) { create(:broadcast_message) }
it 'returns a 401 for anonymous users' do
delete v3_api("/broadcast_messages/#{message.id}"),
attributes_for(:broadcast_message)
expect(response).to have_gitlab_http_status(401)
end
it 'returns a 403 for users' do
delete v3_api("/broadcast_messages/#{message.id}", user),
attributes_for(:broadcast_message)
expect(response).to have_gitlab_http_status(403)
end
it 'deletes the broadcast message for admins' do
expect do
delete v3_api("/broadcast_messages/#{message.id}", admin)
expect(response).to have_gitlab_http_status(200)
end.to change { BroadcastMessage.count }.by(-1)
end
end
end
require 'spec_helper'
describe API::V3::DeployKeys do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:project) { create(:project, creator_id: user.id) }
let(:project2) { create(:project, creator_id: user.id) }
let(:deploy_key) { create(:deploy_key, public: true) }
let!(:deploy_keys_project) do
create(:deploy_keys_project, project: project, deploy_key: deploy_key)
end
describe 'GET /deploy_keys' do
context 'when unauthenticated' do
it 'should return authentication error' do
get v3_api('/deploy_keys')
expect(response.status).to eq(401)
end
end
context 'when authenticated as non-admin user' do
it 'should return a 403 error' do
get v3_api('/deploy_keys', user)
expect(response.status).to eq(403)
end
end
context 'when authenticated as admin' do
it 'should return all deploy keys' do
get v3_api('/deploy_keys', admin)
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id)
end
end
end
%w(deploy_keys keys).each do |path|
describe "GET /projects/:id/#{path}" do
before { deploy_key }
it 'should return array of ssh keys' do
get v3_api("/projects/#{project.id}/#{path}", admin)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(deploy_key.title)
end
end
describe "GET /projects/:id/#{path}/:key_id" do
it 'should return a single key' do
get v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}", admin)
expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(deploy_key.title)
end
it 'should return 404 Not Found with invalid ID' do
get v3_api("/projects/#{project.id}/#{path}/404", admin)
expect(response).to have_gitlab_http_status(404)
end
end
describe "POST /projects/:id/deploy_keys" do
it 'should not create an invalid ssh key' do
post v3_api("/projects/#{project.id}/#{path}", admin), { title: 'invalid key' }
expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('key is missing')
end
it 'should not create a key without title' do
post v3_api("/projects/#{project.id}/#{path}", admin), key: 'some key'
expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('title is missing')
end
it 'should create new ssh key' do
key_attrs = attributes_for :another_key
expect do
post v3_api("/projects/#{project.id}/#{path}", admin), key_attrs
end.to change { project.deploy_keys.count }.by(1)
end
it 'returns an existing ssh key when attempting to add a duplicate' do
expect do
post v3_api("/projects/#{project.id}/#{path}", admin), { key: deploy_key.key, title: deploy_key.title }
end.not_to change { project.deploy_keys.count }
expect(response).to have_gitlab_http_status(201)
end
it 'joins an existing ssh key to a new project' do
expect do
post v3_api("/projects/#{project2.id}/#{path}", admin), { key: deploy_key.key, title: deploy_key.title }
end.to change { project2.deploy_keys.count }.by(1)
expect(response).to have_gitlab_http_status(201)
end
it 'accepts can_push parameter' do
key_attrs = attributes_for(:another_key).merge(can_push: true)
post v3_api("/projects/#{project.id}/#{path}", admin), key_attrs
expect(response).to have_gitlab_http_status(201)
expect(json_response['can_push']).to eq(true)
end
end
describe "DELETE /projects/:id/#{path}/:key_id" do
before { deploy_key }
it 'should delete existing key' do
expect do
delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}", admin)
end.to change { project.deploy_keys.count }.by(-1)
end
it 'should return 404 Not Found with invalid ID' do
delete v3_api("/projects/#{project.id}/#{path}/404", admin)
expect(response).to have_gitlab_http_status(404)
end
end
describe "POST /projects/:id/#{path}/:key_id/enable" do
let(:project2) { create(:project) }
context 'when the user can admin the project' do
it 'enables the key' do
expect do
post v3_api("/projects/#{project2.id}/#{path}/#{deploy_key.id}/enable", admin)
end.to change { project2.deploy_keys.count }.from(0).to(1)
expect(response).to have_gitlab_http_status(201)
expect(json_response['id']).to eq(deploy_key.id)
end
end
context 'when authenticated as non-admin user' do
it 'should return a 404 error' do
post v3_api("/projects/#{project2.id}/#{path}/#{deploy_key.id}/enable", user)
expect(response).to have_gitlab_http_status(404)
end
end
end
describe "DELETE /projects/:id/deploy_keys/:key_id/disable" do
context 'when the user can admin the project' do
it 'disables the key' do
expect do
delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}/disable", admin)
end.to change { project.deploy_keys.count }.from(1).to(0)
expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(deploy_key.id)
end
end
context 'when authenticated as non-admin user' do
it 'should return a 404 error' do
delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}/disable", user)
expect(response).to have_gitlab_http_status(404)
end
end
end
end
end
require 'spec_helper'
describe API::V3::Deployments do
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:project) { deployment.environment.project }
let!(:deployment) { create(:deployment) }
before do
project.add_master(user)
end
shared_examples 'a paginated resources' do
before do
# Fires the request
request
end
it 'has pagination headers' do
expect(response).to include_pagination_headers
end
end
describe 'GET /projects/:id/deployments' do
context 'as member of the project' do
it_behaves_like 'a paginated resources' do
let(:request) { get v3_api("/projects/#{project.id}/deployments", user) }
end
it 'returns projects deployments' do
get v3_api("/projects/#{project.id}/deployments", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
expect(json_response.first['iid']).to eq(deployment.iid)
expect(json_response.first['sha']).to match /\A\h{40}\z/
end
end
context 'as non member' do
it 'returns a 404 status code' do
get v3_api("/projects/#{project.id}/deployments", non_member)
expect(response).to have_gitlab_http_status(404)
end
end
end
describe 'GET /projects/:id/deployments/:deployment_id' do
context 'as a member of the project' do
it 'returns the projects deployment' do
get v3_api("/projects/#{project.id}/deployments/#{deployment.id}", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['sha']).to match /\A\h{40}\z/
expect(json_response['id']).to eq(deployment.id)
end
end
context 'as non member' do
it 'returns a 404 status code' do
get v3_api("/projects/#{project.id}/deployments/#{deployment.id}", non_member)
expect(response).to have_gitlab_http_status(404)
end
end
end
end
require 'spec_helper'
describe API::V3::Environments do
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:project) { create(:project, :private, namespace: user.namespace) }
let!(:environment) { create(:environment, project: project) }
before do
project.add_master(user)
end
shared_examples 'a paginated resources' do
before do
# Fires the request
request
end
it 'has pagination headers' do
expect(response.headers).to include('X-Total')
expect(response.headers).to include('X-Total-Pages')
expect(response.headers).to include('X-Per-Page')
expect(response.headers).to include('X-Page')
expect(response.headers).to include('X-Next-Page')
expect(response.headers).to include('X-Prev-Page')
expect(response.headers).to include('Link')
end
end
describe 'GET /projects/:id/environments' do
context 'as member of the project' do
it_behaves_like 'a paginated resources' do
let(:request) { get v3_api("/projects/#{project.id}/environments", user) }
end
it 'returns project environments' do
get v3_api("/projects/#{project.id}/environments", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
expect(json_response.first['name']).to eq(environment.name)
expect(json_response.first['external_url']).to eq(environment.external_url)
expect(json_response.first['project']['id']).to eq(project.id)
expect(json_response.first['project']['visibility_level']).to be_present
end
end
context 'as non member' do
it 'returns a 404 status code' do
get v3_api("/projects/#{project.id}/environments", non_member)
expect(response).to have_gitlab_http_status(404)
end
end
end
describe 'POST /projects/:id/environments' do
context 'as a member' do
it 'creates a environment with valid params' do
post v3_api("/projects/#{project.id}/environments", user), name: "mepmep"
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq('mepmep')
expect(json_response['slug']).to eq('mepmep')
expect(json_response['external']).to be nil
end
it 'requires name to be passed' do
post v3_api("/projects/#{project.id}/environments", user), external_url: 'test.gitlab.com'
expect(response).to have_gitlab_http_status(400)
end
it 'returns a 400 if environment already exists' do
post v3_api("/projects/#{project.id}/environments", user), name: environment.name
expect(response).to have_gitlab_http_status(400)
end
it 'returns a 400 if slug is specified' do
post v3_api("/projects/#{project.id}/environments", user), name: "foo", slug: "foo"
expect(response).to have_gitlab_http_status(400)
expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed")
end
end
context 'a non member' do
it 'rejects the request' do
post v3_api("/projects/#{project.id}/environments", non_member), name: 'gitlab.com'
expect(response).to have_gitlab_http_status(404)
end
it 'returns a 400 when the required params are missing' do
post v3_api("/projects/12345/environments", non_member), external_url: 'http://env.git.com'
end
end
end
describe 'PUT /projects/:id/environments/:environment_id' do
it 'returns a 200 if name and external_url are changed' do
url = 'https://mepmep.whatever.ninja'
put v3_api("/projects/#{project.id}/environments/#{environment.id}", user),
name: 'Mepmep', external_url: url
expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq('Mepmep')
expect(json_response['external_url']).to eq(url)
end
it "won't allow slug to be changed" do
slug = environment.slug
api_url = v3_api("/projects/#{project.id}/environments/#{environment.id}", user)
put api_url, slug: slug + "-foo"
expect(response).to have_gitlab_http_status(400)
expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed")
end
it "won't update the external_url if only the name is passed" do
url = environment.external_url
put v3_api("/projects/#{project.id}/environments/#{environment.id}", user),
name: 'Mepmep'
expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq('Mepmep')
expect(json_response['external_url']).to eq(url)
end
it 'returns a 404 if the environment does not exist' do
put v3_api("/projects/#{project.id}/environments/12345", user)
expect(response).to have_gitlab_http_status(404)
end
end
describe 'DELETE /projects/:id/environments/:environment_id' do
context 'as a master' do
it 'returns a 200 for an existing environment' do
delete v3_api("/projects/#{project.id}/environments/#{environment.id}", user)
expect(response).to have_gitlab_http_status(200)
end
it 'returns a 404 for non existing id' do
delete v3_api("/projects/#{project.id}/environments/12345", user)
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
end
context 'a non member' do
it 'rejects the request' do
delete v3_api("/projects/#{project.id}/environments/#{environment.id}", non_member)
expect(response).to have_gitlab_http_status(404)
end
end
end
end
require 'spec_helper'
describe API::V3::Files do
# I have to remove periods from the end of the name
# This happened when the user's name had a suffix (i.e. "Sr.")
# This seems to be what git does under the hood. For example, this commit:
#
# $ git commit --author='Foo Sr. <foo@example.com>' -m 'Where's my trailing period?'
#
# results in this:
#
# $ git show --pretty
# ...
# Author: Foo Sr <foo@example.com>
# ...
let(:user) { create(:user) }
let!(:project) { create(:project, :repository, namespace: user.namespace ) }
let(:guest) { create(:user) { |u| project.add_guest(u) } }
let(:file_path) { 'files/ruby/popen.rb' }
let(:params) do
{
file_path: file_path,
ref: 'master'
}
end
let(:author_email) { 'user@example.org' }
let(:author_name) { 'John Doe' }
before { project.add_developer(user) }
describe "GET /projects/:id/repository/files" do
let(:route) { "/projects/#{project.id}/repository/files" }
shared_examples_for 'repository files' do
it "returns file info" do
get v3_api(route, current_user), params
expect(response).to have_gitlab_http_status(200)
expect(json_response['file_path']).to eq(file_path)
expect(json_response['file_name']).to eq('popen.rb')
expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n")
end
context 'when no params are given' do
it_behaves_like '400 response' do
let(:request) { get v3_api(route, current_user) }
end
end
context 'when file_path does not exist' do
let(:params) do
{
file_path: 'app/models/application.rb',
ref: 'master'
}
end
it_behaves_like '404 response' do
let(:request) { get v3_api(route, current_user), params }
let(:message) { '404 File Not Found' }
end
end
context 'when repository is disabled' do
include_context 'disabled repository'
it_behaves_like '403 response' do
let(:request) { get v3_api(route, current_user), params }
end
end
end
context 'when unauthenticated', 'and project is public' do
it_behaves_like 'repository files' do
let(:project) { create(:project, :public, :repository) }
let(:current_user) { nil }
end
end
context 'when unauthenticated', 'and project is private' do
it_behaves_like '404 response' do
let(:request) { get v3_api(route), params }
let(:message) { '404 Project Not Found' }
end
end
context 'when authenticated', 'as a developer' do
it_behaves_like 'repository files' do
let(:current_user) { user }
end
end
context 'when authenticated', 'as a guest' do
it_behaves_like '403 response' do
let(:request) { get v3_api(route, guest), params }
end
end
end
describe "POST /projects/:id/repository/files" do
let(:valid_params) do
{
file_path: 'newfile.rb',
branch_name: 'master',
content: 'puts 8',
commit_message: 'Added newfile'
}
end
it "creates a new file in project repo" do
post v3_api("/projects/#{project.id}/repository/files", user), valid_params
expect(response).to have_gitlab_http_status(201)
expect(json_response['file_path']).to eq('newfile.rb')
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(user.email)
expect(last_commit.author_name).to eq(user.name)
end
it "returns a 400 bad request if no params given" do
post v3_api("/projects/#{project.id}/repository/files", user)
expect(response).to have_gitlab_http_status(400)
end
it "returns a 400 if editor fails to create file" do
allow_any_instance_of(Repository).to receive(:create_file)
.and_raise(Gitlab::Git::CommitError, 'Cannot create file')
post v3_api("/projects/#{project.id}/repository/files", user), valid_params
expect(response).to have_gitlab_http_status(400)
end
context "when specifying an author" do
it "creates a new file with the specified author" do
valid_params.merge!(author_email: author_email, author_name: author_name)
post v3_api("/projects/#{project.id}/repository/files", user), valid_params
expect(response).to have_gitlab_http_status(201)
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(author_email)
expect(last_commit.author_name).to eq(author_name)
end
end
context 'when the repo is empty' do
let!(:project) { create(:project_empty_repo, namespace: user.namespace ) }
it "creates a new file in project repo" do
post v3_api("/projects/#{project.id}/repository/files", user), valid_params
expect(response).to have_gitlab_http_status(201)
expect(json_response['file_path']).to eq('newfile.rb')
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(user.email)
expect(last_commit.author_name).to eq(user.name)
end
end
end
describe "PUT /projects/:id/repository/files" do
let(:valid_params) do
{
file_path: file_path,
branch_name: 'master',
content: 'puts 8',
commit_message: 'Changed file'
}
end
it "updates existing file in project repo" do
put v3_api("/projects/#{project.id}/repository/files", user), valid_params
expect(response).to have_gitlab_http_status(200)
expect(json_response['file_path']).to eq(file_path)
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(user.email)
expect(last_commit.author_name).to eq(user.name)
end
it "returns a 400 bad request if no params given" do
put v3_api("/projects/#{project.id}/repository/files", user)
expect(response).to have_gitlab_http_status(400)
end
context "when specifying an author" do
it "updates a file with the specified author" do
valid_params.merge!(author_email: author_email, author_name: author_name, content: "New content")
put v3_api("/projects/#{project.id}/repository/files", user), valid_params
expect(response).to have_gitlab_http_status(200)
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(author_email)
expect(last_commit.author_name).to eq(author_name)
end
end
end
describe "DELETE /projects/:id/repository/files" do
let(:valid_params) do
{
file_path: file_path,
branch_name: 'master',
commit_message: 'Changed file'
}
end
it "deletes existing file in project repo" do
delete v3_api("/projects/#{project.id}/repository/files", user), valid_params
expect(response).to have_gitlab_http_status(200)
expect(json_response['file_path']).to eq(file_path)
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(user.email)
expect(last_commit.author_name).to eq(user.name)
end
it "returns a 400 bad request if no params given" do
delete v3_api("/projects/#{project.id}/repository/files", user)
expect(response).to have_gitlab_http_status(400)
end
it "returns a 400 if fails to delete file" do
allow_any_instance_of(Repository).to receive(:delete_file).and_raise(Gitlab::Git::CommitError, 'Cannot delete file')
delete v3_api("/projects/#{project.id}/repository/files", user), valid_params
expect(response).to have_gitlab_http_status(400)
end
context "when specifying an author" do
it "removes a file with the specified author" do
valid_params.merge!(author_email: author_email, author_name: author_name)
delete v3_api("/projects/#{project.id}/repository/files", user), valid_params
expect(response).to have_gitlab_http_status(200)
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(author_email)
expect(last_commit.author_name).to eq(author_name)
end
end
end
describe "POST /projects/:id/repository/files with binary file" do
let(:file_path) { 'test.bin' }
let(:put_params) do
{
file_path: file_path,
branch_name: 'master',
content: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=',
commit_message: 'Binary file with a \n should not be touched',
encoding: 'base64'
}
end
let(:get_params) do
{
file_path: file_path,
ref: 'master'
}
end
before do
post v3_api("/projects/#{project.id}/repository/files", user), put_params
end
it "remains unchanged" do
get v3_api("/projects/#{project.id}/repository/files", user), get_params
expect(response).to have_gitlab_http_status(200)
expect(json_response['file_path']).to eq(file_path)
expect(json_response['file_name']).to eq(file_path)
expect(json_response['content']).to eq(put_params[:content])
end
end
end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment