Read about our upcoming Code of Conduct on this issue

projects.rb 18.4 KB
Newer Older
1
2
3
4
5
6
7
module API
  module V3
    class Projects < Grape::API
      include PaginationParams

      before { authenticate_non_get! }

8
9
10
11
      after_validation do
        set_only_allow_merge_if_pipeline_succeeds!
      end

12
13
14
15
16
17
18
19
20
      helpers do
        params :optional_params do
          optional :description, type: String, desc: 'The description of the project'
          optional :issues_enabled, type: Boolean, desc: 'Flag indication if the issue tracker is enabled'
          optional :merge_requests_enabled, type: Boolean, desc: 'Flag indication if merge requests are enabled'
          optional :wiki_enabled, type: Boolean, desc: 'Flag indication if the wiki is enabled'
          optional :builds_enabled, type: Boolean, desc: 'Flag indication if builds are enabled'
          optional :snippets_enabled, type: Boolean, desc: 'Flag indication if snippets are enabled'
          optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project'
21
          optional :resolve_outdated_diff_discussions, type: Boolean, desc: 'Automatically resolve merge request diffs discussions on lines changed with a push'
22
23
24
25
26
27
          optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project'
          optional :lfs_enabled, type: Boolean, desc: 'Flag indication if Git LFS is enabled for that project'
          optional :public, type: Boolean, desc: 'Create a public project. The same as visibility_level = 20.'
          optional :visibility_level, type: Integer, values: [
            Gitlab::VisibilityLevel::PRIVATE,
            Gitlab::VisibilityLevel::INTERNAL,
28
29
            Gitlab::VisibilityLevel::PUBLIC
          ], desc: 'Create a public project. The same as visibility_level = 20.'
30
31
32
          optional :public_builds, type: Boolean, desc: 'Perform public builds'
          optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
          optional :only_allow_merge_if_build_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
33
          optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
34
35
36
37
38
39
40
41
42
43
44
45
          optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved'
        end

        def map_public_to_visibility_level(attrs)
          publik = attrs.delete(:public)
          if !publik.nil? && !attrs[:visibility_level].present?
            # Since setting the public attribute to private could mean either
            # private or internal, use the more conservative option, private.
            attrs[:visibility_level] = (publik == true) ? Gitlab::VisibilityLevel::PUBLIC : Gitlab::VisibilityLevel::PRIVATE
          end
          attrs
        end
46
47

        def set_only_allow_merge_if_pipeline_succeeds!
48
          if params.key?(:only_allow_merge_if_build_succeeds)
49
50
51
            params[:only_allow_merge_if_pipeline_succeeds] = params.delete(:only_allow_merge_if_build_succeeds)
          end
        end
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
      end

      resource :projects do
        helpers do
          params :collection_params do
            use :sort_params
            use :filter_params
            use :pagination

            optional :simple, type: Boolean, default: false,
                              desc: 'Return only the ID, URL, name, and path of each project'
          end

          params :sort_params do
            optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at],
                                default: 'created_at', desc: 'Return projects ordered by field'
            optional :sort, type: String, values: %w[asc desc], default: 'desc',
                            desc: 'Return projects sorted in ascending and descending order'
          end

          params :filter_params do
73
            optional :archived, type: Boolean, default: nil, desc: 'Limit by archived status'
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
            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'
          end

          params :statistics_params do
            optional :statistics, type: Boolean, default: false, desc: 'Include project statistics'
          end

          params :create_params do
            optional :namespace_id, type: Integer, desc: 'Namespace ID for the new project. Default to the user namespace.'
            optional :import_url, type: String, desc: 'URL from which the project is imported'
          end

          def present_projects(projects, options = {})
            options = options.reverse_merge(
90
              with: ::API::V3::Entities::Project,
91
              current_user: current_user,
92
              simple: params[:simple]
93
94
95
96
            )

            projects = filter_projects(projects)
            projects = projects.with_statistics if options[:statistics]
97
            options[:with] = ::API::Entities::BasicProjectDetails if options[:simple]
98
99
100
101
102
103

            present paginate(projects), options
          end
        end

        desc 'Get a list of visible projects for authenticated user' do
104
          success ::API::Entities::BasicProjectDetails
105
106
107
108
109
        end
        params do
          use :collection_params
        end
        get '/visible' do
110
          entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails
111
          present_projects ProjectsFinder.new(current_user: current_user).execute, with: entity
112
113
114
        end

        desc 'Get a projects list for authenticated user' do
115
          success ::API::Entities::BasicProjectDetails
116
117
118
119
120
121
122
123
        end
        params do
          use :collection_params
        end
        get do
          authenticate!

          present_projects current_user.authorized_projects,
124
            with: ::API::V3::Entities::ProjectWithAccess
125
126
127
        end

        desc 'Get an owned projects list for authenticated user' do
128
          success ::API::Entities::BasicProjectDetails
129
130
131
132
133
134
135
136
137
        end
        params do
          use :collection_params
          use :statistics_params
        end
        get '/owned' do
          authenticate!

          present_projects current_user.owned_projects,
138
            with: ::API::V3::Entities::ProjectWithAccess,
139
140
141
142
            statistics: params[:statistics]
        end

        desc 'Gets starred project for the authenticated user' do
143
          success ::API::Entities::BasicProjectDetails
144
145
146
147
148
149
150
        end
        params do
          use :collection_params
        end
        get '/starred' do
          authenticate!

Toon Claes's avatar
Toon Claes committed
151
          present_projects ProjectsFinder.new(current_user: current_user, params: { starred: true }).execute
152
153
154
        end

        desc 'Get all projects for admin user' do
155
          success ::API::Entities::BasicProjectDetails
156
157
158
159
160
161
162
163
        end
        params do
          use :collection_params
          use :statistics_params
        end
        get '/all' do
          authenticated_as_admin!

164
          present_projects Project.all, with: ::API::V3::Entities::ProjectWithAccess, statistics: params[:statistics]
165
166
167
        end

        desc 'Search for projects the current user has access to' do
168
          success ::API::V3::Entities::Project
169
170
171
172
173
174
175
176
177
178
179
        end
        params do
          requires :query, type: String, desc: 'The project name to be searched'
          use :sort_params
          use :pagination
        end
        get "/search/:query", requirements: { query: /[^\/]+/ } do
          search_service = Search::GlobalService.new(current_user, search: params[:query]).execute
          projects = search_service.objects('projects', params[:page])
          projects = projects.reorder(params[:order_by] => params[:sort])

180
          present paginate(projects), with: ::API::V3::Entities::Project
181
182
183
        end

        desc 'Create new project' do
184
          success ::API::V3::Entities::Project
185
186
        end
        params do
187
          optional :name, type: String, desc: 'The name of the project'
188
          optional :path, type: String, desc: 'The path of the repository'
189
          at_least_one_of :name, :path
190
191
192
193
194
195
196
197
          use :optional_params
          use :create_params
        end
        post do
          attrs = map_public_to_visibility_level(declared_params(include_missing: false))
          project = ::Projects::CreateService.new(current_user, attrs).execute

          if project.saved?
198
            present project, with: ::API::V3::Entities::Project,
199
200
201
202
203
204
205
206
207
208
                             user_can_admin_project: can?(current_user, :admin_project, project)
          else
            if project.errors[:limit_reached].present?
              error!(project.errors[:limit_reached], 403)
            end
            render_validation_error!(project)
          end
        end

        desc 'Create new project for a specified user. Only available to admin users.' do
209
          success ::API::V3::Entities::Project
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
        end
        params do
          requires :name, type: String, desc: 'The name of the project'
          requires :user_id, type: Integer, desc: 'The ID of a user'
          optional :default_branch, type: String, desc: 'The default branch of the project'
          use :optional_params
          use :create_params
        end
        post "user/:user_id" do
          authenticated_as_admin!
          user = User.find_by(id: params.delete(:user_id))
          not_found!('User') unless user

          attrs = map_public_to_visibility_level(declared_params(include_missing: false))
          project = ::Projects::CreateService.new(user, attrs).execute

          if project.saved?
227
            present project, with: ::API::V3::Entities::Project,
228
229
230
231
232
233
234
235
236
237
                             user_can_admin_project: can?(current_user, :admin_project, project)
          else
            render_validation_error!(project)
          end
        end
      end

      params do
        requires :id, type: String, desc: 'The ID of a project'
      end
238
      resource :projects, requirements: { id: %r{[^/]+} } do
239
        desc 'Get a single project' do
240
          success ::API::V3::Entities::ProjectWithAccess
241
242
        end
        get ":id" do
243
          entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails
244
245
246
247
248
          present user_project, with: entity, current_user: current_user,
                                user_can_admin_project: can?(current_user, :admin_project, user_project)
        end

        desc 'Get events for a single project' do
249
          success ::API::V3::Entities::Event
250
251
252
253
254
        end
        params do
          use :pagination
        end
        get ":id/events" do
255
          present paginate(user_project.events.recent), with: ::API::V3::Entities::Event
256
257
258
        end

        desc 'Fork new project for the current user or provided namespace.' do
259
          success ::API::V3::Entities::Project
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
        end
        params do
          optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be forked into'
        end
        post 'fork/:id' do
          fork_params = declared_params(include_missing: false)
          namespace_id = fork_params[:namespace]

          if namespace_id.present?
            fork_params[:namespace] = if namespace_id =~ /^\d+$/
                                        Namespace.find_by(id: namespace_id)
                                      else
                                        Namespace.find_by_path_or_name(namespace_id)
                                      end

            unless fork_params[:namespace] && can?(current_user, :create_projects, fork_params[:namespace])
              not_found!('Target Namespace')
            end
          end

          forked_project = ::Projects::ForkService.new(user_project, current_user, fork_params).execute

          if forked_project.errors.any?
            conflict!(forked_project.errors.messages)
          else
285
            present forked_project, with: ::API::V3::Entities::Project,
286
287
288
289
290
                                    user_can_admin_project: can?(current_user, :admin_project, forked_project)
          end
        end

        desc 'Update an existing project' do
291
          success ::API::V3::Entities::Project
292
293
294
295
296
297
298
299
        end
        params do
          optional :name, type: String, desc: 'The name of the project'
          optional :default_branch, type: String, desc: 'The default branch of the project'
          optional :path, type: String, desc: 'The path of the repository'
          use :optional_params
          at_least_one_of :name, :description, :issues_enabled, :merge_requests_enabled,
            :wiki_enabled, :builds_enabled, :snippets_enabled,
300
            :shared_runners_enabled, :resolve_outdated_diff_discussions,
301
302
            :container_registry_enabled, :lfs_enabled, :public, :visibility_level,
            :public_builds, :request_access_enabled, :only_allow_merge_if_build_succeeds,
303
304
305
306
307
308
309
310
311
312
313
314
            :only_allow_merge_if_all_discussions_are_resolved, :path,
            :default_branch
        end
        put ':id' do
          authorize_admin_project
          attrs = map_public_to_visibility_level(declared_params(include_missing: false))
          authorize! :rename_project, user_project if attrs[:name].present?
          authorize! :change_visibility_level, user_project if attrs[:visibility_level].present?

          result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute

          if result[:status] == :success
315
            present user_project, with: ::API::V3::Entities::Project,
316
317
318
319
320
321
322
                                  user_can_admin_project: can?(current_user, :admin_project, user_project)
          else
            render_validation_error!(user_project)
          end
        end

        desc 'Archive a project' do
323
          success ::API::V3::Entities::Project
324
325
326
327
328
329
        end
        post ':id/archive' do
          authorize!(:archive_project, user_project)

          user_project.archive!

330
          present user_project, with: ::API::V3::Entities::Project
331
332
333
        end

        desc 'Unarchive a project' do
334
          success ::API::V3::Entities::Project
335
336
337
338
339
340
        end
        post ':id/unarchive' do
          authorize!(:archive_project, user_project)

          user_project.unarchive!

341
          present user_project, with: ::API::V3::Entities::Project
342
343
344
        end

        desc 'Star a project' do
345
          success ::API::V3::Entities::Project
346
347
348
349
350
351
352
353
        end
        post ':id/star' do
          if current_user.starred?(user_project)
            not_modified!
          else
            current_user.toggle_star(user_project)
            user_project.reload

354
            present user_project, with: ::API::V3::Entities::Project
355
356
357
358
          end
        end

        desc 'Unstar a project' do
359
          success ::API::V3::Entities::Project
360
361
362
363
364
365
        end
        delete ':id/star' do
          if current_user.starred?(user_project)
            current_user.toggle_star(user_project)
            user_project.reload

366
            present user_project, with: ::API::V3::Entities::Project
367
368
369
370
371
372
373
374
          else
            not_modified!
          end
        end

        desc 'Remove a project'
        delete ":id" do
          authorize! :remove_project, user_project
Robert Schilling's avatar
Robert Schilling committed
375
376

          status(200)
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
          ::Projects::DestroyService.new(user_project, current_user, {}).async_execute
        end

        desc 'Mark this project as forked from another'
        params do
          requires :forked_from_id, type: String, desc: 'The ID of the project it was forked from'
        end
        post ":id/fork/:forked_from_id" do
          authenticated_as_admin!

          forked_from_project = find_project!(params[:forked_from_id])
          not_found!("Source Project") unless forked_from_project

          if user_project.forked_from_project.nil?
            user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id)
392
393

            ::Projects::ForksCountService.new(forked_from_project).refresh_cache
394
395
396
397
398
399
400
401
402
403
          else
            render_api_error!("Project already forked", 409)
          end
        end

        desc 'Remove a forked_from relationship'
        delete ":id/fork" do
          authorize! :remove_fork_project, user_project

          if user_project.forked?
Robert Schilling's avatar
Robert Schilling committed
404
            status(200)
405
406
407
408
409
410
411
            user_project.forked_project_link.destroy
          else
            not_modified!
          end
        end

        desc 'Share the project with a group' do
412
          success ::API::Entities::ProjectGroupLink
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
        end
        params do
          requires :group_id, type: Integer, desc: 'The ID of a group'
          requires :group_access, type: Integer, values: Gitlab::Access.values, desc: 'The group access level'
          optional :expires_at, type: Date, desc: 'Share expiration date'
        end
        post ":id/share" do
          authorize! :admin_project, user_project
          group = Group.find_by_id(params[:group_id])

          unless group && can?(current_user, :read_group, group)
            not_found!('Group')
          end

          unless user_project.allowed_to_share_with_group?
            return render_api_error!("The project sharing with group is disabled", 400)
          end

          link = user_project.project_group_links.new(declared_params(include_missing: false))

          if link.save
434
            present link, with: ::API::Entities::ProjectGroupLink
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
          else
            render_api_error!(link.errors.full_messages.first, 409)
          end
        end

        params do
          requires :group_id, type: Integer, desc: 'The ID of the group'
        end
        delete ":id/share/:group_id" do
          authorize! :admin_project, user_project

          link = user_project.project_group_links.find_by(group_id: params[:group_id])
          not_found!('Group Link') unless link

          link.destroy
          no_content!
        end

        desc 'Upload a file'
        params do
          requires :file, type: File, desc: 'The file to be uploaded'
        end
        post ":id/uploads" do
458
          UploadService.new(user_project, params[:file]).execute
459
460
461
        end

        desc 'Get the users list of a project' do
462
          success ::API::Entities::UserBasic
463
464
465
466
467
468
469
470
471
        end
        params do
          optional :search, type: String, desc: 'Return list of users matching the search criteria'
          use :pagination
        end
        get ':id/users' do
          users = user_project.team.users
          users = users.search(params[:search]) if params[:search].present?

472
          present paginate(users), with: ::API::Entities::UserBasic
473
474
475
476
477
        end
      end
    end
  end
end