summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorLuke "Jared" Bennett <lbennett@gitlab.com>2017-04-04 14:56:27 +0100
committerLuke "Jared" Bennett <lbennett@gitlab.com>2017-04-04 14:56:27 +0100
commitccca73d779ae0e22a86c9586fd52b15a8600b9f3 (patch)
tree7385d9a2019bfa98affe9bc83c0afd7da2fd730d /lib
parentc252c03401881fd7dbf7fab984285c402eb31d5f (diff)
parent5f7ebfb9aa023849b8b296bf9cf0f472b886d072 (diff)
downloadgitlab-ce-ccca73d779ae0e22a86c9586fd52b15a8600b9f3.tar.gz
Merge branch 'master' into add-sentry-js-again-with-vue
Diffstat (limited to 'lib')
-rw-r--r--lib/additional_email_headers_interceptor.rb8
-rw-r--r--lib/api/access_requests.rb2
-rw-r--r--lib/api/api.rb49
-rw-r--r--lib/api/api_guard.rb19
-rw-r--r--lib/api/award_emoji.rb36
-rw-r--r--lib/api/boards.rb21
-rw-r--r--lib/api/branches.rb35
-rw-r--r--lib/api/broadcast_messages.rb2
-rw-r--r--lib/api/commit_statuses.rb18
-rw-r--r--lib/api/commits.rb57
-rw-r--r--lib/api/deploy_keys.rb171
-rw-r--r--lib/api/deployments.rb4
-rw-r--r--lib/api/entities.rb233
-rw-r--r--lib/api/environments.rb21
-rw-r--r--lib/api/files.rb80
-rw-r--r--lib/api/groups.rb32
-rw-r--r--lib/api/helpers.rb96
-rw-r--r--lib/api/helpers/internal_helpers.rb10
-rw-r--r--lib/api/helpers/pagination.rb2
-rw-r--r--lib/api/helpers/runner.rb69
-rw-r--r--lib/api/internal.rb12
-rw-r--r--lib/api/issues.rb78
-rw-r--r--lib/api/jobs.rb (renamed from lib/api/builds.rb)143
-rw-r--r--lib/api/labels.rb21
-rw-r--r--lib/api/members.rb47
-rw-r--r--lib/api/merge_request_diffs.rb29
-rw-r--r--lib/api/merge_requests.rb275
-rw-r--r--lib/api/milestones.rb41
-rw-r--r--lib/api/notes.rb42
-rw-r--r--lib/api/notification_settings.rb9
-rw-r--r--lib/api/pagination_params.rb4
-rw-r--r--lib/api/pipelines.rb12
-rw-r--r--lib/api/project_hooks.rb15
-rw-r--r--lib/api/project_snippets.rb28
-rw-r--r--lib/api/projects.rb135
-rw-r--r--lib/api/repositories.rb73
-rw-r--r--lib/api/runner.rb264
-rw-r--r--lib/api/runners.rb12
-rw-r--r--lib/api/services.rb117
-rw-r--r--lib/api/settings.rb22
-rw-r--r--lib/api/snippets.rb28
-rw-r--r--lib/api/subscriptions.rb9
-rw-r--r--lib/api/system_hooks.rb12
-rw-r--r--lib/api/tags.rb22
-rw-r--r--lib/api/templates.rb103
-rw-r--r--lib/api/time_tracking_endpoints.rb6
-rw-r--r--lib/api/todos.rb18
-rw-r--r--lib/api/triggers.rb91
-rw-r--r--lib/api/users.rb126
-rw-r--r--lib/api/v3/award_emoji.rb130
-rw-r--r--lib/api/v3/boards.rb72
-rw-r--r--lib/api/v3/branches.rb72
-rw-r--r--lib/api/v3/broadcast_messages.rb31
-rw-r--r--lib/api/v3/builds.rb255
-rw-r--r--lib/api/v3/commits.rb196
-rw-r--r--lib/api/v3/deploy_keys.rb122
-rw-r--r--lib/api/v3/deployments.rb43
-rw-r--r--lib/api/v3/entities.rb253
-rw-r--r--lib/api/v3/environments.rb87
-rw-r--r--lib/api/v3/files.rb138
-rw-r--r--lib/api/v3/groups.rb181
-rw-r--r--lib/api/v3/helpers.rb19
-rw-r--r--lib/api/v3/issues.rb231
-rw-r--r--lib/api/v3/labels.rb34
-rw-r--r--lib/api/v3/members.rb134
-rw-r--r--lib/api/v3/merge_request_diffs.rb44
-rw-r--r--lib/api/v3/merge_requests.rb290
-rw-r--r--lib/api/v3/milestones.rb64
-rw-r--r--lib/api/v3/notes.rb148
-rw-r--r--lib/api/v3/pipelines.rb36
-rw-r--r--lib/api/v3/project_hooks.rb106
-rw-r--r--lib/api/v3/project_snippets.rb143
-rw-r--r--lib/api/v3/projects.rb474
-rw-r--r--lib/api/v3/repositories.rb109
-rw-r--r--lib/api/v3/runners.rb65
-rw-r--r--lib/api/v3/services.rb644
-rw-r--r--lib/api/v3/settings.rb137
-rw-r--r--lib/api/v3/snippets.rb138
-rw-r--r--lib/api/v3/subscriptions.rb53
-rw-r--r--lib/api/v3/system_hooks.rb32
-rw-r--r--lib/api/v3/tags.rb40
-rw-r--r--lib/api/v3/templates.rb122
-rw-r--r--lib/api/v3/time_tracking_endpoints.rb116
-rw-r--r--lib/api/v3/todos.rb30
-rw-r--r--lib/api/v3/triggers.rb103
-rw-r--r--lib/api/v3/users.rb202
-rw-r--r--lib/api/v3/variables.rb29
-rw-r--r--lib/api/variables.rb8
-rw-r--r--lib/backup/database.rb70
-rw-r--r--lib/backup/files.rb21
-rw-r--r--lib/backup/manager.rb27
-rw-r--r--lib/backup/pages.rb13
-rw-r--r--lib/backup/repository.rb27
-rw-r--r--lib/backup/uploads.rb1
-rw-r--r--lib/banzai/cross_project_reference.rb2
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb32
-rw-r--r--lib/banzai/filter/autolink_filter.rb2
-rw-r--r--lib/banzai/filter/emoji_filter.rb64
-rw-r--r--lib/banzai/filter/gollum_tags_filter.rb11
-rw-r--r--lib/banzai/filter/image_link_filter.rb13
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb13
-rw-r--r--lib/banzai/filter/merge_request_reference_filter.rb29
-rw-r--r--lib/banzai/filter/plantuml_filter.rb39
-rw-r--r--lib/banzai/filter/reference_filter.rb4
-rw-r--r--lib/banzai/filter/sanitization_filter.rb4
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb19
-rw-r--r--lib/banzai/filter/user_reference_filter.rb15
-rw-r--r--lib/banzai/filter/video_link_filter.rb4
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb7
-rw-r--r--lib/banzai/querying.rb56
-rw-r--r--lib/banzai/reference_extractor.rb5
-rw-r--r--lib/banzai/reference_parser/base_parser.rb11
-rw-r--r--lib/banzai/reference_parser/directly_addressed_user_parser.rb8
-rw-r--r--lib/banzai/renderer.rb4
-rw-r--r--lib/bitbucket/connection.rb10
-rw-r--r--lib/bitbucket/error/unauthorized.rb3
-rw-r--r--lib/bitbucket/representation/repo.rb2
-rw-r--r--lib/ci/ansi2html.rb52
-rw-r--r--lib/ci/api/builds.rb31
-rw-r--r--lib/ci/api/helpers.rb6
-rw-r--r--lib/ci/api/runners.rb44
-rw-r--r--lib/ci/api/triggers.rb43
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb5
-rw-r--r--lib/constraints/project_url_constrainer.rb2
-rw-r--r--lib/container_registry/client.rb2
-rw-r--r--lib/extracts_path.rb4
-rw-r--r--lib/file_size_validator.rb4
-rw-r--r--lib/gitlab/access.rb6
-rw-r--r--lib/gitlab/allowable.rb2
-rw-r--r--lib/gitlab/asciidoc.rb3
-rw-r--r--lib/gitlab/auth.rb67
-rw-r--r--lib/gitlab/auth/too_many_ips.rb17
-rw-r--r--lib/gitlab/auth/unique_ips_limiter.rb43
-rw-r--r--lib/gitlab/award_emoji.rb83
-rw-r--r--lib/gitlab/badge/build/template.rb2
-rw-r--r--lib/gitlab/badge/coverage/template.rb2
-rw-r--r--lib/gitlab/badge/metadata.rb4
-rw-r--r--lib/gitlab/bitbucket_import/importer.rb9
-rw-r--r--lib/gitlab/changes_list.rb2
-rw-r--r--lib/gitlab/chat_commands/base_command.rb4
-rw-r--r--lib/gitlab/chat_commands/command.rb35
-rw-r--r--lib/gitlab/chat_commands/deploy.rb24
-rw-r--r--lib/gitlab/chat_commands/help.rb28
-rw-r--r--lib/gitlab/chat_commands/issue_new.rb (renamed from lib/gitlab/chat_commands/issue_create.rb)20
-rw-r--r--lib/gitlab/chat_commands/issue_search.rb8
-rw-r--r--lib/gitlab/chat_commands/issue_show.rb8
-rw-r--r--lib/gitlab/chat_commands/presenter.rb131
-rw-r--r--lib/gitlab/chat_commands/presenters/access.rb40
-rw-r--r--lib/gitlab/chat_commands/presenters/base.rb77
-rw-r--r--lib/gitlab/chat_commands/presenters/deploy.rb21
-rw-r--r--lib/gitlab/chat_commands/presenters/help.rb27
-rw-r--r--lib/gitlab/chat_commands/presenters/issue_base.rb43
-rw-r--r--lib/gitlab/chat_commands/presenters/issue_new.rb50
-rw-r--r--lib/gitlab/chat_commands/presenters/issue_search.rb47
-rw-r--r--lib/gitlab/chat_commands/presenters/issue_show.rb61
-rw-r--r--lib/gitlab/checks/change_access.rb8
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata.rb2
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata/entry.rb6
-rw-r--r--lib/gitlab/ci/build/image.rb33
-rw-r--r--lib/gitlab/ci/build/step.rb41
-rw-r--r--lib/gitlab/ci/config/entry/artifacts.rb2
-rw-r--r--lib/gitlab/ci/config/entry/cache.rb8
-rw-r--r--lib/gitlab/ci/config/entry/configurable.rb2
-rw-r--r--lib/gitlab/ci/config/entry/coverage.rb22
-rw-r--r--lib/gitlab/ci/config/entry/environment.rb8
-rw-r--r--lib/gitlab/ci/config/entry/factory.rb2
-rw-r--r--lib/gitlab/ci/config/entry/job.rb19
-rw-r--r--lib/gitlab/ci/config/entry/key.rb4
-rw-r--r--lib/gitlab/ci/config/entry/legacy_validation_helpers.rb10
-rw-r--r--lib/gitlab/ci/config/entry/node.rb8
-rw-r--r--lib/gitlab/ci/config/entry/trigger.rb10
-rw-r--r--lib/gitlab/ci/config/entry/undefined.rb4
-rw-r--r--lib/gitlab/ci/config/entry/validators.rb45
-rw-r--r--lib/gitlab/ci/config/loader.rb2
-rw-r--r--lib/gitlab/ci/status/build/cancelable.rb2
-rw-r--r--lib/gitlab/ci/status/build/factory.rb7
-rw-r--r--lib/gitlab/ci/status/build/failed_allowed.rb27
-rw-r--r--lib/gitlab/ci/status/build/play.rb18
-rw-r--r--lib/gitlab/ci/status/build/retryable.rb2
-rw-r--r--lib/gitlab/ci/status/build/stop.rb14
-rw-r--r--lib/gitlab/ci/status/canceled.rb4
-rw-r--r--lib/gitlab/ci/status/core.rb7
-rw-r--r--lib/gitlab/ci/status/created.rb4
-rw-r--r--lib/gitlab/ci/status/factory.rb43
-rw-r--r--lib/gitlab/ci/status/failed.rb4
-rw-r--r--lib/gitlab/ci/status/manual.rb23
-rw-r--r--lib/gitlab/ci/status/pending.rb4
-rw-r--r--lib/gitlab/ci/status/pipeline/blocked.rb23
-rw-r--r--lib/gitlab/ci/status/pipeline/factory.rb3
-rw-r--r--lib/gitlab/ci/status/pipeline/success_with_warnings.rb31
-rw-r--r--lib/gitlab/ci/status/running.rb4
-rw-r--r--lib/gitlab/ci/status/skipped.rb4
-rw-r--r--lib/gitlab/ci/status/stage/factory.rb4
-rw-r--r--lib/gitlab/ci/status/success.rb4
-rw-r--r--lib/gitlab/ci/status/success_warning.rb33
-rw-r--r--lib/gitlab/ci/trace_reader.rb1
-rw-r--r--lib/gitlab/conflict/file.rb14
-rw-r--r--lib/gitlab/conflict/file_collection.rb3
-rw-r--r--lib/gitlab/conflict/parser.rb30
-rw-r--r--lib/gitlab/conflict/resolution_error.rb3
-rw-r--r--lib/gitlab/contributions_calendar.rb6
-rw-r--r--lib/gitlab/current_settings.rb46
-rw-r--r--lib/gitlab/cycle_analytics/base_event_fetcher.rb4
-rw-r--r--lib/gitlab/cycle_analytics/base_stage.rb2
-rw-r--r--lib/gitlab/cycle_analytics/code_event_fetcher.rb2
-rw-r--r--lib/gitlab/cycle_analytics/code_stage.rb4
-rw-r--r--lib/gitlab/cycle_analytics/issue_event_fetcher.rb2
-rw-r--r--lib/gitlab/cycle_analytics/issue_stage.rb4
-rw-r--r--lib/gitlab/cycle_analytics/plan_event_fetcher.rb2
-rw-r--r--lib/gitlab/cycle_analytics/plan_stage.rb4
-rw-r--r--lib/gitlab/cycle_analytics/production_stage.rb4
-rw-r--r--lib/gitlab/cycle_analytics/review_event_fetcher.rb2
-rw-r--r--lib/gitlab/cycle_analytics/review_stage.rb4
-rw-r--r--lib/gitlab/cycle_analytics/stage_summary.rb2
-rw-r--r--lib/gitlab/cycle_analytics/staging_event_fetcher.rb2
-rw-r--r--lib/gitlab/cycle_analytics/staging_stage.rb4
-rw-r--r--lib/gitlab/cycle_analytics/test_stage.rb4
-rw-r--r--lib/gitlab/data_builder/build.rb10
-rw-r--r--lib/gitlab/data_builder/pipeline.rb2
-rw-r--r--lib/gitlab/database.rb54
-rw-r--r--lib/gitlab/database/median.rb1
-rw-r--r--lib/gitlab/database/migration_helpers.rb59
-rw-r--r--lib/gitlab/diff/highlight.rb2
-rw-r--r--lib/gitlab/diff/inline_diff_marker.rb2
-rw-r--r--lib/gitlab/diff/line.rb6
-rw-r--r--lib/gitlab/diff/parser.rb10
-rw-r--r--lib/gitlab/diff/position.rb19
-rw-r--r--lib/gitlab/downtime_check/message.rb4
-rw-r--r--lib/gitlab/ee_compat_check.rb299
-rw-r--r--lib/gitlab/email/handler.rb3
-rw-r--r--lib/gitlab/email/handler/base_handler.rb43
-rw-r--r--lib/gitlab/email/handler/create_issue_handler.rb3
-rw-r--r--lib/gitlab/email/handler/create_note_handler.rb7
-rw-r--r--lib/gitlab/email/handler/reply_processing.rb54
-rw-r--r--lib/gitlab/email/handler/unsubscribe_handler.rb32
-rw-r--r--lib/gitlab/email/message/repository_push.rb4
-rw-r--r--lib/gitlab/email/receiver.rb47
-rw-r--r--lib/gitlab/email/reply_parser.rb11
-rw-r--r--lib/gitlab/emoji.rb33
-rw-r--r--lib/gitlab/etag_caching/middleware.rb73
-rw-r--r--lib/gitlab/etag_caching/store.rb32
-rw-r--r--lib/gitlab/exclusive_lease.rb2
-rw-r--r--lib/gitlab/file_detector.rb2
-rw-r--r--lib/gitlab/git.rb2
-rw-r--r--lib/gitlab/git/blob.rb157
-rw-r--r--lib/gitlab/git/blob_snippet.rb2
-rw-r--r--lib/gitlab/git/commit.rb6
-rw-r--r--lib/gitlab/git/diff.rb23
-rw-r--r--lib/gitlab/git/diff_collection.rb4
-rw-r--r--lib/gitlab/git/index.rb126
-rw-r--r--lib/gitlab/git/repository.rb166
-rw-r--r--lib/gitlab/git/tree.rb2
-rw-r--r--lib/gitlab/git_access.rb10
-rw-r--r--lib/gitlab/git_post_receive.rb4
-rw-r--r--lib/gitlab/git_ref_validator.rb3
-rw-r--r--lib/gitlab/gitaly_client.rb54
-rw-r--r--lib/gitlab/gitaly_client/commit.rb41
-rw-r--r--lib/gitlab/gitaly_client/notifications.rb22
-rw-r--r--lib/gitlab/github_import/base_formatter.rb18
-rw-r--r--lib/gitlab/github_import/branch_formatter.rb10
-rw-r--r--lib/gitlab/github_import/client.rb8
-rw-r--r--lib/gitlab/github_import/comment_formatter.rb10
-rw-r--r--lib/gitlab/github_import/importer.rb25
-rw-r--r--lib/gitlab/github_import/issuable_formatter.rb30
-rw-r--r--lib/gitlab/github_import/project_creator.rb4
-rw-r--r--lib/gitlab/github_import/pull_request_formatter.rb42
-rw-r--r--lib/gitlab/github_import/user_formatter.rb45
-rw-r--r--lib/gitlab/gon_helper.rb7
-rw-r--r--lib/gitlab/google_code_import/importer.rb2
-rw-r--r--lib/gitlab/highlight.rb4
-rw-r--r--lib/gitlab/import_export.rb4
-rw-r--r--lib/gitlab/import_export/command_line_util.rb12
-rw-r--r--lib/gitlab/import_export/error.rb2
-rw-r--r--lib/gitlab/import_export/import_export.yml5
-rw-r--r--lib/gitlab/import_export/importer.rb2
-rw-r--r--lib/gitlab/import_export/members_mapper.rb6
-rw-r--r--lib/gitlab/import_export/project_tree_saver.rb33
-rw-r--r--lib/gitlab/import_export/reader.rb4
-rw-r--r--lib/gitlab/import_export/relation_factory.rb34
-rw-r--r--lib/gitlab/import_export/repo_restorer.rb21
-rw-r--r--lib/gitlab/import_sources.rb2
-rw-r--r--lib/gitlab/incoming_email.rb21
-rw-r--r--lib/gitlab/job_waiter.rb27
-rw-r--r--lib/gitlab/kubernetes.rb4
-rw-r--r--lib/gitlab/ldap/config.rb4
-rw-r--r--lib/gitlab/ldap/person.rb4
-rw-r--r--lib/gitlab/ldap/user.rb2
-rw-r--r--lib/gitlab/metrics.rb6
-rw-r--r--lib/gitlab/metrics/instrumentation.rb11
-rw-r--r--lib/gitlab/metrics/rack_middleware.rb6
-rw-r--r--lib/gitlab/metrics/subscribers/action_view.rb2
-rw-r--r--lib/gitlab/metrics/system.rb2
-rw-r--r--lib/gitlab/metrics/transaction.rb2
-rw-r--r--lib/gitlab/middleware/go.rb66
-rw-r--r--lib/gitlab/middleware/multipart.rb2
-rw-r--r--lib/gitlab/middleware/webpack_proxy.rb24
-rw-r--r--lib/gitlab/o_auth/user.rb19
-rw-r--r--lib/gitlab/optimistic_locking.rb6
-rw-r--r--lib/gitlab/other_markup.rb3
-rw-r--r--lib/gitlab/pages_transfer.rb7
-rw-r--r--lib/gitlab/polling_interval.rb22
-rw-r--r--lib/gitlab/project_search_results.rb28
-rw-r--r--lib/gitlab/project_transfer.rb35
-rw-r--r--lib/gitlab/prometheus.rb70
-rw-r--r--lib/gitlab/recaptcha.rb4
-rw-r--r--lib/gitlab/redis.rb29
-rw-r--r--lib/gitlab/reference_extractor.rb8
-rw-r--r--lib/gitlab/regex.rb13
-rw-r--r--lib/gitlab/repo_path.rb23
-rw-r--r--lib/gitlab/request_context.rb21
-rw-r--r--lib/gitlab/request_profiler.rb2
-rw-r--r--lib/gitlab/request_profiler/middleware.rb3
-rw-r--r--lib/gitlab/route_map.rb50
-rw-r--r--lib/gitlab/saml/user.rb11
-rw-r--r--lib/gitlab/sanitizers/svg/whitelist.rb25
-rw-r--r--lib/gitlab/search_results.rb26
-rw-r--r--lib/gitlab/seeder.rb19
-rw-r--r--lib/gitlab/serializer/ci/variables.rb (renamed from lib/gitlab/serialize/ci/variables.rb)2
-rw-r--r--lib/gitlab/serializer/pagination.rb36
-rw-r--r--lib/gitlab/shell.rb (renamed from lib/gitlab/backend/shell.rb)35
-rw-r--r--lib/gitlab/shell_adapter.rb (renamed from lib/gitlab/backend/shell_adapter.rb)0
-rw-r--r--lib/gitlab/sherlock/query.rb11
-rw-r--r--lib/gitlab/sidekiq_status.rb89
-rw-r--r--lib/gitlab/sidekiq_status/client_middleware.rb10
-rw-r--r--lib/gitlab/sidekiq_status/server_middleware.rb13
-rw-r--r--lib/gitlab/slash_commands/extractor.rb2
-rw-r--r--lib/gitlab/snippet_search_results.rb4
-rw-r--r--lib/gitlab/template/finders/repo_template_finder.rb2
-rw-r--r--lib/gitlab/template/gitlab_ci_yml_template.rb2
-rw-r--r--lib/gitlab/testing/request_blocker_middleware.rb61
-rw-r--r--lib/gitlab/themes.rb87
-rw-r--r--lib/gitlab/update_path_error.rb2
-rw-r--r--lib/gitlab/upgrader.rb15
-rw-r--r--lib/gitlab/uploads_transfer.rb32
-rw-r--r--lib/gitlab/url_blocker.rb59
-rw-r--r--lib/gitlab/url_sanitizer.rb10
-rw-r--r--lib/gitlab/user_access.rb18
-rw-r--r--lib/gitlab/view/presenter/base.rb2
-rw-r--r--lib/gitlab/view/presenter/delegated.rb4
-rw-r--r--lib/gitlab/visibility_level.rb58
-rw-r--r--lib/gitlab/workhorse.rb57
-rw-r--r--lib/mattermost/client.rb24
-rw-r--r--lib/mattermost/command.rb2
-rw-r--r--lib/mattermost/error.rb2
-rw-r--r--lib/mattermost/session.rb4
-rw-r--r--lib/mattermost/team.rb13
-rw-r--r--lib/omni_auth/strategies/bitbucket.rb (renamed from lib/omniauth/strategies/bitbucket.rb)0
-rw-r--r--lib/rouge/formatters/html_gitlab.rb10
-rw-r--r--lib/rouge/lexers/plantuml.rb21
-rwxr-xr-xlib/support/deploy/deploy.sh4
-rwxr-xr-xlib/support/init.d/gitlab119
-rw-r--r--[-rwxr-xr-x]lib/support/init.d/gitlab.default.example28
-rw-r--r--lib/support/nginx/gitlab7
-rw-r--r--lib/support/nginx/gitlab-pages28
-rw-r--r--lib/support/nginx/gitlab-pages-ssl77
-rw-r--r--lib/support/nginx/gitlab-ssl10
-rw-r--r--lib/tasks/brakeman.rake2
-rw-r--r--lib/tasks/cache.rake2
-rw-r--r--lib/tasks/config_lint.rake25
-rw-r--r--lib/tasks/dev.rake2
-rw-r--r--lib/tasks/downtime_check.rake10
-rw-r--r--lib/tasks/eslint.rake7
-rw-r--r--lib/tasks/flay.rake2
-rw-r--r--lib/tasks/gemojione.rake93
-rw-r--r--lib/tasks/gitlab/assets.rake48
-rw-r--r--lib/tasks/gitlab/backup.rake21
-rw-r--r--lib/tasks/gitlab/check.rake81
-rw-r--r--lib/tasks/gitlab/cleanup.rake19
-rw-r--r--lib/tasks/gitlab/db.rake8
-rw-r--r--lib/tasks/gitlab/dev.rake2
-rw-r--r--lib/tasks/gitlab/git.rake2
-rw-r--r--lib/tasks/gitlab/gitaly.rake23
-rw-r--r--lib/tasks/gitlab/import.rake9
-rw-r--r--lib/tasks/gitlab/import_export.rake2
-rw-r--r--lib/tasks/gitlab/info.rake29
-rw-r--r--lib/tasks/gitlab/shell.rake6
-rw-r--r--lib/tasks/gitlab/sidekiq.rake8
-rw-r--r--lib/tasks/gitlab/task_helpers.rb57
-rw-r--r--lib/tasks/gitlab/test.rake12
-rw-r--r--lib/tasks/gitlab/track_deployment.rake4
-rw-r--r--lib/tasks/gitlab/update_templates.rake2
-rw-r--r--lib/tasks/gitlab/web_hook.rake6
-rw-r--r--lib/tasks/grape.rake6
-rw-r--r--lib/tasks/karma.rake21
-rw-r--r--lib/tasks/lint.rake1
-rw-r--r--lib/tasks/migrate/migrate_iids.rake2
-rw-r--r--lib/tasks/migrate/setup_postgresql.rake2
-rw-r--r--lib/tasks/services.rake10
-rw-r--r--lib/tasks/sidekiq.rake8
-rw-r--r--lib/tasks/spec.rake30
-rw-r--r--lib/tasks/spinach.rake2
-rw-r--r--lib/tasks/teaspoon.rake25
-rw-r--r--lib/tasks/test.rake2
-rw-r--r--lib/tasks/tokens.rake38
-rw-r--r--lib/tasks/yarn.rake40
395 files changed, 11142 insertions, 2973 deletions
diff --git a/lib/additional_email_headers_interceptor.rb b/lib/additional_email_headers_interceptor.rb
new file mode 100644
index 00000000000..2358fa6bbfd
--- /dev/null
+++ b/lib/additional_email_headers_interceptor.rb
@@ -0,0 +1,8 @@
+class AdditionalEmailHeadersInterceptor
+ def self.delivering_email(message)
+ message.headers(
+ 'Auto-Submitted' => 'auto-generated',
+ 'X-Auto-Response-Suppress' => 'All'
+ )
+ end
+end
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb
index 789f45489eb..a5c9f0b509c 100644
--- a/lib/api/access_requests.rb
+++ b/lib/api/access_requests.rb
@@ -10,7 +10,7 @@ module API
params do
requires :id, type: String, desc: "The #{source_type} ID"
end
- resource source_type.pluralize do
+ resource source_type.pluralize, requirements: { id: %r{[^/]+} } do
desc "Gets a list of access requests for a #{source_type}." do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::AccessRequester
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 6cf6b501021..1bf20f76ad6 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -1,7 +1,47 @@
module API
class API < Grape::API
include APIGuard
- version 'v3', using: :path
+
+ version %w(v3 v4), using: :path
+
+ version 'v3', using: :path do
+ helpers ::API::V3::Helpers
+
+ 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
+ end
before { allow_access_with_scope :api }
@@ -23,6 +63,10 @@ module API
error! e.message, e.status, e.headers
end
+ rescue_from Gitlab::Auth::TooManyIps do |e|
+ rack_response({ 'message' => '403 Forbidden' }.to_json, 403)
+ end
+
rescue_from :all do |exception|
handle_api_exception(exception)
end
@@ -40,7 +84,6 @@ module API
mount ::API::Boards
mount ::API::Branches
mount ::API::BroadcastMessages
- mount ::API::Builds
mount ::API::Commits
mount ::API::CommitStatuses
mount ::API::DeployKeys
@@ -50,6 +93,7 @@ module API
mount ::API::Groups
mount ::API::Internal
mount ::API::Issues
+ mount ::API::Jobs
mount ::API::Keys
mount ::API::Labels
mount ::API::Lint
@@ -65,6 +109,7 @@ module API
mount ::API::Projects
mount ::API::ProjectSnippets
mount ::API::Repositories
+ mount ::API::Runner
mount ::API::Runners
mount ::API::Services
mount ::API::Session
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index df6db140d0e..9fcf04efa38 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -6,7 +6,7 @@ module API
module APIGuard
extend ActiveSupport::Concern
- PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
+ PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN".freeze
PRIVATE_TOKEN_PARAM = :private_token
included do |base|
@@ -114,14 +114,14 @@ module API
private
def install_error_responders(base)
- error_classes = [ MissingTokenError, TokenNotFoundError,
- ExpiredError, RevokedError, InsufficientScopeError]
+ error_classes = [MissingTokenError, TokenNotFoundError,
+ ExpiredError, RevokedError, InsufficientScopeError]
base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler
end
def oauth2_bearer_token_error_handler
- Proc.new do |e|
+ proc do |e|
response =
case e
when MissingTokenError
@@ -160,13 +160,10 @@ module API
# Exceptions
#
- class MissingTokenError < StandardError; end
-
- class TokenNotFoundError < StandardError; end
-
- class ExpiredError < StandardError; end
-
- class RevokedError < StandardError; end
+ MissingTokenError = Class.new(StandardError)
+ TokenNotFoundError = Class.new(StandardError)
+ ExpiredError = Class.new(StandardError)
+ RevokedError = Class.new(StandardError)
class InsufficientScopeError < StandardError
attr_reader :scopes
diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb
index 58a4df54bea..56f19f89642 100644
--- a/lib/api/award_emoji.rb
+++ b/lib/api/award_emoji.rb
@@ -3,19 +3,26 @@ module API
include PaginationParams
before { authenticate! }
- AWARDABLES = %w[issue merge_request snippet]
-
- resource :projects do
- AWARDABLES.each do |awardable_type|
- awardable_string = awardable_type.pluralize
- awardable_id_string = "#{awardable_type}_id"
+ AWARDABLES = [
+ { type: 'issue', find_by: :iid },
+ { type: 'merge_request', find_by: :iid },
+ { type: 'snippet', find_by: :id }
+ ].freeze
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ AWARDABLES.each do |awardable_params|
+ awardable_string = awardable_params[:type].pluralize
+ awardable_id_string = "#{awardable_params[:type]}_#{awardable_params[:find_by]}"
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}/award_emoji",
":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji"
].each do |endpoint|
@@ -28,8 +35,8 @@ module API
end
get endpoint do
if can_read_awardable?
- awards = paginate(awardable.award_emoji)
- present awards, with: Entities::AwardEmoji
+ awards = awardable.award_emoji
+ present paginate(awards), with: Entities::AwardEmoji
else
not_found!("Award Emoji")
end
@@ -82,7 +89,6 @@ module API
unauthorized! unless award.user == current_user || current_user.admin?
award.destroy
- present award, with: Entities::AwardEmoji
end
end
end
@@ -104,10 +110,10 @@ module API
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])
+ elsif params.include?(:issue_iid)
+ user_project.issues.find_by!(iid: params[:issue_iid])
+ elsif params.include?(:merge_request_iid)
+ user_project.merge_requests.find_by!(iid: params[:merge_request_iid])
else
user_project.snippets.find(params[:snippet_id])
end
diff --git a/lib/api/boards.rb b/lib/api/boards.rb
index 4ac491edc1b..5a2d7a681e3 100644
--- a/lib/api/boards.rb
+++ b/lib/api/boards.rb
@@ -1,19 +1,23 @@
module API
- # Boards API
class Boards < Grape::API
+ include PaginationParams
+
before { authenticate! }
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get all project boards' do
detail 'This feature was introduced in 8.13'
success Entities::Board
end
+ params do
+ use :pagination
+ end
get ':id/boards' do
authorize!(:read_board, user_project)
- present user_project.boards, with: Entities::Board
+ present paginate(user_project.boards), with: Entities::Board
end
params do
@@ -37,12 +41,15 @@ module API
end
desc 'Get the lists of a project board' do
- detail 'Does not include `backlog` and `done` lists. This feature was introduced in 8.13'
+ detail 'Does not include `done` list. This feature was introduced in 8.13'
success Entities::List
end
+ params do
+ use :pagination
+ end
get '/lists' do
authorize!(:read_board, user_project)
- present board_lists, with: Entities::List
+ present paginate(board_lists), with: Entities::List
end
desc 'Get a list of a project board' do
@@ -120,9 +127,7 @@ module API
service = ::Boards::Lists::DestroyService.new(user_project, current_user)
- if service.execute(list)
- present list, with: Entities::List
- else
+ unless service.execute(list)
render_api_error!({ error: 'List could not be deleted!' }, 400)
end
end
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 0950c3d2e88..f35084a582a 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -1,22 +1,25 @@
require 'mime/types'
module API
- # Projects API
class Branches < Grape::API
- before { authenticate! }
+ include PaginationParams
+
before { authorize! :download_code, user_project }
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository branches' do
success Entities::RepoBranch
end
+ params do
+ use :pagination
+ end
get ":id/repository/branches" do
- branches = user_project.repository.branches.sort_by(&:name)
+ branches = ::Kaminari.paginate_array(user_project.repository.branches.sort_by(&:name))
- present branches, with: Entities::RepoBranch, project: user_project
+ present paginate(branches), with: Entities::RepoBranch, project: user_project
end
desc 'Get a single branch' do
@@ -84,7 +87,7 @@ module API
branch = user_project.repository.find_branch(params[:branch])
not_found!("Branch") unless branch
protected_branch = user_project.protected_branches.find_by(name: branch.name)
- protected_branch.destroy if protected_branch
+ protected_branch&.destroy
present branch, with: Entities::RepoBranch, project: user_project
end
@@ -93,13 +96,14 @@ module API
success Entities::RepoBranch
end
params do
- requires :branch_name, type: String, desc: 'The name of the branch'
+ requires :branch, 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])
+ execute(params[:branch], params[:ref])
if result[:status] == :success
present result[:branch],
@@ -120,25 +124,16 @@ module API
result = DeleteBranchService.new(user_project, current_user).
execute(params[:branch])
- if result[:status] == :success
- {
- branch_name: params[:branch]
- }
- else
+ if result[:status] != :success
render_api_error!(result[:message], result[:return_code])
end
end
- # Delete all merged branches
- #
- # Parameters:
- # id (required) - The ID of a project
- # Example Request:
- # DELETE /projects/:id/repository/branches/delete_merged
+ desc 'Delete all merged branches'
delete ":id/repository/merged_branches" do
DeleteMergedBranchesService.new(user_project, current_user).async_execute
- status(200)
+ accepted!
end
end
end
diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb
index 1217002bf8e..395c401203c 100644
--- a/lib/api/broadcast_messages.rb
+++ b/lib/api/broadcast_messages.rb
@@ -91,7 +91,7 @@ module API
delete ':id' do
message = find_message
- present message.destroy, with: Entities::BroadcastMessage
+ message.destroy
end
end
end
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index b6e6820c3f4..827a38d33da 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -2,7 +2,10 @@ require 'mime/types'
module API
class CommitStatuses < Grape::API
- resource :projects do
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
include PaginationParams
before { authenticate! }
@@ -11,7 +14,6 @@ module API
success Entities::CommitStatus
end
params do
- requires :id, type: String, desc: 'The ID of a project'
requires :sha, type: String, desc: 'The commit hash'
optional :ref, type: String, desc: 'The ref'
optional :stage, type: String, desc: 'The stage'
@@ -37,15 +39,15 @@ module API
success Entities::CommitStatus
end
params do
- requires :id, type: String, desc: 'The ID of a project'
requires :sha, type: String, desc: 'The commit hash'
requires :state, type: String, desc: 'The state of the status',
- values: ['pending', 'running', 'success', 'failed', 'canceled']
+ values: %w(pending running success failed canceled)
optional :ref, type: String, desc: 'The ref'
optional :target_url, type: String, desc: 'The target URL to associate with this status'
optional :description, type: String, desc: 'A short description of the status'
optional :name, type: String, desc: 'A string label to differentiate this status from the status of other systems. Default: "default"'
optional :context, type: String, desc: 'A string label to differentiate this status from the status of other systems. Default: "default"'
+ optional :coverage, type: Float, desc: 'The total code coverage'
end
post ':id/statuses/:sha' do
authorize! :create_commit_status, user_project
@@ -71,13 +73,15 @@ module API
status = GenericCommitStatus.running_or_pending.find_or_initialize_by(
project: @project,
pipeline: pipeline,
- user: current_user,
name: name,
ref: ref,
- target_url: params[:target_url],
- description: params[:description]
+ user: current_user
)
+ optional_attributes =
+ attributes_for_keys(%w[target_url description coverage])
+
+ status.update(optional_attributes) if optional_attributes.any?
render_validation_error!(status) if status.invalid?
begin
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index e6d707f3c3d..66b37fd2bcc 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -10,33 +10,42 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository commits' do
success Entities::RepoCommit
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: String, desc: 'Only commits after or in this date will be returned'
- optional :until, type: String, 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 :since, type: DateTime, desc: 'Only commits after or on this date will be returned'
+ optional :until, type: DateTime, desc: 'Only commits before or on this date will be returned'
optional :path, type: String, desc: 'The file path'
+ use :pagination
end
get ":id/repository/commits" do
- # TODO remove the next line for 9.0, use DateTime type in the params block
- datetime_attributes! :since, :until
-
- ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
- offset = params[:page] * params[:per_page]
+ path = params[:path]
+ before = params[:until]
+ after = params[:since]
+ ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
+ offset = (params[:page] - 1) * params[:per_page]
commits = user_project.repository.commits(ref,
- path: params[:path],
+ path: path,
limit: params[:per_page],
offset: offset,
- after: params[:since],
- before: params[:until])
+ before: before,
+ after: after)
+
+ commit_count =
+ if path || before || after
+ user_project.repository.count_commits(ref: ref, path: path, before: before, after: after)
+ else
+ # Cacheable commit count.
+ user_project.repository.commit_count_for_ref(ref)
+ end
- present commits, with: Entities::RepoCommit
+ paginated_commits = Kaminari.paginate_array(commits, total_count: commit_count)
+
+ present paginate(paginated_commits), with: Entities::RepoCommit
end
desc 'Commit multiple file changes as one commit' do
@@ -44,7 +53,7 @@ module API
detail 'This feature was introduced in GitLab 8.13'
end
params do
- requires :branch_name, type: String, desc: 'The name of branch'
+ requires :branch, 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'
@@ -53,15 +62,7 @@ module API
post ":id/repository/commits" do
authorize! :push_code, user_project
- attrs = declared_params
- attrs[:source_branch] = attrs[:branch_name]
- attrs[:target_branch] = attrs[:branch_name]
- attrs[:actions].map! do |action|
- action[:action] = action[:action].to_sym
- action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/')
- action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/')
- action
- end
+ attrs = declared_params.merge(start_branch: declared_params[:branch], target_branch: declared_params[:branch])
result = ::Files::MultiService.new(user_project, current_user, attrs).execute
@@ -114,7 +115,7 @@ module API
commit = user_project.commit(params[:sha])
not_found! 'Commit' unless commit
- notes = Note.where(commit_id: commit.id).order(:created_at)
+ notes = user_project.notes.where(commit_id: commit.id).order(:created_at)
present paginate(notes), with: Entities::CommitNote
end
@@ -138,9 +139,7 @@ module API
commit_params = {
commit: commit,
- create_merge_request: false,
- source_project: user_project,
- source_branch: commit.cherry_pick_branch_name,
+ start_branch: params[:branch],
target_branch: params[:branch]
}
@@ -163,7 +162,7 @@ module API
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: ['new', 'old'], default: 'new', desc: 'The type of the line'
+ 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' do
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index 85360730841..b888ede6fe8 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -1,120 +1,109 @@
module API
- # Projects API
class DeployKeys < Grape::API
+ include PaginationParams
+
before { authenticate! }
+ desc 'Return all deploy keys'
+ params do
+ use :pagination
+ end
get "deploy_keys" do
authenticated_as_admin!
- keys = DeployKey.all
- present keys, with: Entities::SSHKey
+ present paginate(DeployKey.all), with: Entities::SSHKey
end
params do
requires :id, type: String, desc: 'The ID of the project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
before { authorize_admin_project }
- # Routing "projects/:id/keys/..." is DEPRECATED and WILL BE REMOVED in version 9.0
- # Use "projects/:id/deploy_keys/..." instead.
- #
- %w(keys deploy_keys).each do |path|
- desc "Get a specific project's deploy keys" do
- success Entities::SSHKey
- end
- get ":id/#{path}" do
- present user_project.deploy_keys, with: Entities::SSHKey
- end
-
- desc 'Get single deploy key' do
- success Entities::SSHKey
- end
- params do
- requires :key_id, type: Integer, desc: 'The ID of the deploy key'
- end
- get ":id/#{path}/:key_id" do
- key = user_project.deploy_keys.find params[:key_id]
- present key, with: Entities::SSHKey
- end
-
- # TODO: for 9.0 we should check if params are there with the params block
- # grape provides, at this point we'd change behaviour so we can't
- # Behaviour now if you don't provide all required params: it renders a
- # validation error or two.
- desc 'Add new deploy key to currently authenticated user' do
- success Entities::SSHKey
- end
- post ":id/#{path}" do
- attrs = attributes_for_keys [:title, :key]
- attrs[:key].strip! if attrs[:key]
+ desc "Get a specific project's deploy keys" do
+ success Entities::SSHKey
+ end
+ params do
+ use :pagination
+ end
+ get ":id/deploy_keys" do
+ present paginate(user_project.deploy_keys), with: Entities::SSHKey
+ end
- # Check for an existing key joined to this project
- key = user_project.deploy_keys.find_by(key: attrs[:key])
- if key
- present key, with: Entities::SSHKey
- break
- end
+ desc 'Get single deploy key' do
+ success Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ get ":id/deploy_keys/:key_id" do
+ key = user_project.deploy_keys.find params[:key_id]
+ present key, with: Entities::SSHKey
+ end
- # Check for available deploy keys in other projects
- key = current_user.accessible_deploy_keys.find_by(key: attrs[:key])
- if key
- user_project.deploy_keys << key
- present key, with: Entities::SSHKey
- break
- end
+ desc 'Add new deploy key to currently authenticated user' do
+ success Entities::SSHKey
+ end
+ params do
+ requires :key, type: String, desc: 'The new deploy key'
+ requires :title, type: String, desc: 'The name of the deploy key'
+ end
+ post ":id/deploy_keys" do
+ params[:key].strip!
- # Create a new deploy key
- key = DeployKey.new attrs
- if key.valid? && user_project.deploy_keys << key
- present key, with: Entities::SSHKey
- else
- render_validation_error!(key)
- end
+ # Check for an existing key joined to this project
+ key = user_project.deploy_keys.find_by(key: params[:key])
+ if key
+ present key, with: Entities::SSHKey
+ break
end
- desc 'Enable a deploy key for a project' do
- detail 'This feature was added in GitLab 8.11'
- success Entities::SSHKey
- end
- params do
- requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ # Check for available deploy keys in other projects
+ key = current_user.accessible_deploy_keys.find_by(key: params[:key])
+ if key
+ user_project.deploy_keys << key
+ present key, with: Entities::SSHKey
+ break
end
- post ":id/#{path}/:key_id/enable" do
- key = ::Projects::EnableDeployKeyService.new(user_project,
- current_user, declared_params).execute
- if key
- present key, with: Entities::SSHKey
- else
- not_found!('Deploy Key')
- end
+ # Create a new deploy key
+ key = DeployKey.new(declared_params(include_missing: false))
+ if key.valid? && user_project.deploy_keys << key
+ present key, with: Entities::SSHKey
+ else
+ render_validation_error!(key)
end
+ end
- desc 'Disable a deploy key for a project' do
- detail 'This feature was added in GitLab 8.11'
- success 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
+ desc 'Enable a deploy key for a project' do
+ detail 'This feature was added in GitLab 8.11'
+ success Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ post ":id/deploy_keys/:key_id/enable" do
+ key = ::Projects::EnableDeployKeyService.new(user_project,
+ current_user, declared_params).execute
- present key.deploy_key, with: Entities::SSHKey
+ if key
+ present key, with: Entities::SSHKey
+ else
+ not_found!('Deploy Key')
end
+ end
- desc 'Delete existing deploy key of currently authenticated user' 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.find(params[:key_id])
- key.destroy
- 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/deploy_keys/:key_id" do
+ key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
+ not_found!('Deploy Key') unless key
+
+ key.destroy
end
end
end
diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb
index c5feb49b22f..46b936897f6 100644
--- a/lib/api/deployments.rb
+++ b/lib/api/deployments.rb
@@ -1,5 +1,5 @@
module API
- # Deployments RESTfull API endpoints
+ # Deployments RESTful API endpoints
class Deployments < Grape::API
include PaginationParams
@@ -8,7 +8,7 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get all deployments of the project' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Deployment
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 9f59939e9ae..00d44821e3f 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -26,7 +26,7 @@ module API
expose :last_sign_in_at
expose :confirmed_at
expose :email
- expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at
+ expose :color_scheme_id, :projects_limit, :current_sign_in_at
expose :identities, using: Entities::Identity
expose :can_create_group?, as: :can_create_group
expose :can_create_project?, as: :can_create_project
@@ -49,7 +49,8 @@ module API
class ProjectHook < Hook
expose :project_id, :issues_events, :merge_requests_events
- expose :note_events, :build_events, :pipeline_events, :wiki_page_events
+ expose :note_events, :pipeline_events, :wiki_page_events
+ expose :build_events, as: :job_events
end
class BasicProjectDetails < Grape::Entity
@@ -69,9 +70,8 @@ module API
class Project < Grape::Entity
expose :id, :description, :default_branch, :tag_list
- expose :public?, as: :public
expose :archived?, as: :archived
- expose :visibility_level, :ssh_url_to_repo, :http_url_to_repo, :web_url
+ expose :visibility, :ssh_url_to_repo, :http_url_to_repo, :web_url
expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group }
expose :name, :name_with_namespace
expose :path, :path_with_namespace
@@ -81,7 +81,7 @@ module API
expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) }
expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) }
expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) }
- expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) }
+ expose(:jobs_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) }
expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) }
expose :created_at, :last_activity_at
@@ -94,11 +94,11 @@ module API
expose :star_count, :forks_count
expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) && project.default_issues_tracker? }
expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
- expose :public_builds
+ expose :public_builds, as: :public_jobs
expose :shared_with_groups do |project, options|
SharedGroup.represent(project.project_group_links.all, options)
end
- expose :only_allow_merge_if_build_succeeds
+ expose :only_allow_merge_if_pipeline_succeeds
expose :request_access_enabled
expose :only_allow_merge_if_all_discussions_are_resolved
@@ -110,7 +110,7 @@ module API
expose :storage_size
expose :repository_size
expose :lfs_objects_size
- expose :build_artifacts_size
+ expose :build_artifacts_size, as: :job_artifacts_size
end
class Member < UserBasic
@@ -132,18 +132,20 @@ module API
end
class Group < Grape::Entity
- expose :id, :name, :path, :description, :visibility_level
+ expose :id, :name, :path, :description, :visibility
expose :lfs_enabled?, as: :lfs_enabled
expose :avatar_url
expose :web_url
expose :request_access_enabled
+ expose :full_name, :full_path
+ expose :parent_id
expose :statistics, if: :statistics do
with_options format_with: -> (value) { value.to_i } do
expose :storage_size
expose :repository_size
expose :lfs_objects_size
- expose :build_artifacts_size
+ expose :build_artifacts_size, as: :job_artifacts_size
end
end
end
@@ -153,10 +155,27 @@ module API
expose :shared_projects, using: Entities::Project
end
+ class RepoCommit < Grape::Entity
+ expose :id, :short_id, :title, :created_at
+ expose :parent_ids
+ expose :safe_message, as: :message
+ expose :author_name, :author_email, :authored_date
+ expose :committer_name, :committer_email, :committed_date
+ end
+
+ class RepoCommitStats < Grape::Entity
+ expose :additions, :deletions, :total
+ end
+
+ class RepoCommitDetail < RepoCommit
+ expose :stats, using: Entities::RepoCommitStats
+ expose :status
+ end
+
class RepoBranch < Grape::Entity
expose :name
- expose :commit do |repo_branch, options|
+ expose :commit, using: Entities::RepoCommit do |repo_branch, options|
options[:project].repository.commit(repo_branch.dereferenced_target)
end
@@ -185,36 +204,17 @@ module API
expose :id, :name, :type, :path
expose :mode do |obj, options|
- filemode = obj.mode.to_s(8)
+ filemode = obj.mode
filemode = "0" + filemode if filemode.length < 6
filemode
end
end
- class RepoCommit < Grape::Entity
- expose :id, :short_id, :title, :author_name, :author_email, :created_at
- expose :committer_name, :committer_email
- expose :safe_message, as: :message
- end
-
- class RepoCommitStats < Grape::Entity
- expose :additions, :deletions, :total
- end
-
- class RepoCommitDetail < RepoCommit
- expose :parent_ids, :committed_date, :authored_date
- expose :stats, using: Entities::RepoCommitStats
- expose :status
- end
-
class ProjectSnippet < Grape::Entity
expose :id, :title, :file_name
expose :author, using: Entities::UserBasic
expose :updated_at, :created_at
- # TODO (rspeicher): Deprecated; remove in 9.0
- expose(:expires_at) { |snippet| nil }
-
expose :web_url do |snippet, options|
Gitlab::UrlBuilder.build(snippet)
end
@@ -250,14 +250,11 @@ module API
expose :start_date
end
- class Issue < ProjectEntity
+ class IssueBasic < ProjectEntity
expose :label_names, as: :labels
expose :milestone, using: Entities::Milestone
expose :assignee, :author, using: Entities::UserBasic
- expose :subscribed do |issue, options|
- issue.subscribed?(options[:current_user], options[:project] || issue.project)
- end
expose :user_notes_count
expose :upvotes, :downvotes
expose :due_date
@@ -268,6 +265,12 @@ module API
end
end
+ class Issue < IssueBasic
+ expose :subscribed do |issue, options|
+ issue.subscribed?(options[:current_user], options[:project] || issue.project)
+ end
+ end
+
class IssuableTimeStats < Grape::Entity
expose :time_estimate
expose :total_time_spent
@@ -280,7 +283,7 @@ module API
expose :id
end
- class MergeRequest < ProjectEntity
+ class MergeRequestBasic < ProjectEntity
expose :target_branch, :source_branch
expose :upvotes, :downvotes
expose :author, :assignee, using: Entities::UserBasic
@@ -288,13 +291,10 @@ module API
expose :label_names, as: :labels
expose :work_in_progress?, as: :work_in_progress
expose :milestone, using: Entities::Milestone
- expose :merge_when_build_succeeds
+ expose :merge_when_pipeline_succeeds
expose :merge_status
expose :diff_head_sha, as: :sha
expose :merge_commit_sha
- expose :subscribed do |merge_request, options|
- merge_request.subscribed?(options[:current_user], options[:project])
- end
expose :user_notes_count
expose :should_remove_source_branch?, as: :should_remove_source_branch
expose :force_remove_source_branch?, as: :force_remove_source_branch
@@ -304,6 +304,12 @@ module API
end
end
+ class MergeRequest < MergeRequestBasic
+ expose :subscribed do |merge_request, options|
+ merge_request.subscribed?(options[:current_user], options[:project])
+ end
+ end
+
class MergeRequestChanges < MergeRequest
expose :diffs, as: :changes, using: Entities::RepoDiff do |compare, _|
compare.raw_diffs(all_diffs: true).to_a
@@ -339,9 +345,6 @@ module API
expose :created_at, :updated_at
expose :system?, as: :system
expose :noteable_id, :noteable_type
- # upvote? and downvote? are deprecated, always return false
- expose(:upvote?) { |note| false }
- expose(:downvote?) { |note| false }
end
class AwardEmoji < Grape::Entity
@@ -368,7 +371,7 @@ module API
class CommitStatus < Grape::Entity
expose :id, :sha, :ref, :status, :name, :target_url, :description,
- :created_at, :started_at, :finished_at, :allow_failure
+ :created_at, :started_at, :finished_at, :allow_failure, :coverage
expose :author, using: Entities::UserBasic
end
@@ -381,9 +384,7 @@ module API
expose :author, using: Entities::UserBasic, if: ->(event, options) { event.author }
expose :author_username do |event, options|
- if event.author
- event.author.username
- end
+ event.author&.username
end
end
@@ -399,7 +400,8 @@ module API
expose :target_type
expose :target do |todo, options|
- Entities.const_get(todo.target_type).represent(todo.target, options)
+ target = todo.target_type == 'Commit' ? 'RepoCommit' : todo.target_type
+ Entities.const_get(target).represent(todo.target, options)
end
expose :target_url do |todo, options|
@@ -417,7 +419,7 @@ module API
end
class Namespace < Grape::Entity
- expose :id, :name, :path, :kind
+ expose :id, :name, :path, :kind, :full_path
end
class MemberAccess < Grape::Entity
@@ -453,7 +455,8 @@ module API
class ProjectService < Grape::Entity
expose :id, :title, :created_at, :updated_at, :active
expose :push_events, :issues_events, :merge_requests_events
- expose :tag_push_events, :note_events, :build_events, :pipeline_events
+ expose :tag_push_events, :note_events, :pipeline_events
+ expose :build_events, as: :job_events
# Expose serialized properties
expose :properties do |service, options|
field_names = service.fields.
@@ -556,12 +559,15 @@ module API
expose :updated_at
expose :home_page_url
expose :default_branch_protection
- expose :restricted_visibility_levels
+ expose(:restricted_visibility_levels) do |setting, _options|
+ setting.restricted_visibility_levels.map { |level| Gitlab::VisibilityLevel.string_level(level) }
+ end
expose :max_attachment_size
expose :session_expire_delay
- expose :default_project_visibility
- expose :default_snippet_visibility
- expose :default_group_visibility
+ expose(:default_project_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_project_visibility) }
+ expose(:default_snippet_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_snippet_visibility) }
+ expose(:default_group_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_group_visibility) }
+ expose :default_artifacts_expire_in
expose :domain_whitelist
expose :domain_blacklist_enabled
expose :domain_blacklist
@@ -574,6 +580,8 @@ module API
expose :koding_url
expose :plantuml_enabled
expose :plantuml_url
+ expose :terminal_max_session_time
+ expose :polling_interval_multiplier
end
class Release < Grape::Entity
@@ -593,10 +601,6 @@ module API
end
end
- class TriggerRequest < Grape::Entity
- expose :id, :variables
- end
-
class Runner < Grape::Entity
expose :id
expose :description
@@ -621,7 +625,11 @@ module API
end
end
- class BuildArtifactFile < Grape::Entity
+ class RunnerRegistrationDetails < Grape::Entity
+ expose :id, :token
+ end
+
+ class JobArtifactFile < Grape::Entity
expose :filename, :size
end
@@ -629,18 +637,21 @@ module API
expose :id, :sha, :ref, :status
end
- class Build < Grape::Entity
+ class Job < Grape::Entity
expose :id, :status, :stage, :name, :ref, :tag, :coverage
expose :created_at, :started_at, :finished_at
expose :user, with: User
- expose :artifacts_file, using: BuildArtifactFile, if: -> (build, opts) { build.artifacts? }
+ expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? }
expose :commit, with: RepoCommit
expose :runner, with: Runner
expose :pipeline, with: PipelineBasic
end
class Trigger < Grape::Entity
- expose :token, :created_at, :updated_at, :deleted_at, :last_used
+ expose :id
+ expose :token, :description
+ expose :created_at, :updated_at, :deleted_at, :last_used
+ expose :owner, using: Entities::UserBasic
end
class Variable < Grape::Entity
@@ -661,14 +672,14 @@ module API
end
class Environment < EnvironmentBasic
- expose :project, using: Entities::Project
+ expose :project, using: Entities::BasicProjectDetails
end
class Deployment < Grape::Entity
expose :id, :iid, :ref, :sha, :created_at
expose :user, using: Entities::UserBasic
expose :environment, using: Entities::EnvironmentBasic
- expose :deployable, using: Entities::Build
+ expose :deployable, using: Entities::Job
end
class RepoLicense < Grape::Entity
@@ -695,5 +706,99 @@ module API
expose :id, :message, :starts_at, :ends_at, :color, :font
expose :active?, as: :active
end
+
+ class PersonalAccessToken < Grape::Entity
+ expose :id, :name, :revoked, :created_at, :scopes
+ expose :active?, as: :active
+ expose :expires_at do |personal_access_token|
+ personal_access_token.expires_at ? personal_access_token.expires_at.strftime("%Y-%m-%d") : nil
+ end
+ end
+
+ class PersonalAccessTokenWithToken < PersonalAccessToken
+ expose :token
+ end
+
+ class ImpersonationToken < PersonalAccessTokenWithToken
+ expose :impersonation
+ end
+
+ module JobRequest
+ class JobInfo < Grape::Entity
+ expose :name, :stage
+ expose :project_id, :project_name
+ end
+
+ class GitInfo < Grape::Entity
+ expose :repo_url, :ref, :sha, :before_sha
+ expose :ref_type do |model|
+ if model.tag
+ 'tag'
+ else
+ 'branch'
+ end
+ end
+ end
+
+ class RunnerInfo < Grape::Entity
+ expose :timeout
+ end
+
+ class Step < Grape::Entity
+ expose :name, :script, :timeout, :when, :allow_failure
+ end
+
+ class Image < Grape::Entity
+ expose :name
+ end
+
+ class Artifacts < Grape::Entity
+ expose :name, :untracked, :paths, :when, :expire_in
+ end
+
+ class Cache < Grape::Entity
+ expose :key, :untracked, :paths
+ end
+
+ class Credentials < Grape::Entity
+ expose :type, :url, :username, :password
+ end
+
+ class ArtifactFile < Grape::Entity
+ expose :filename, :size
+ end
+
+ class Dependency < Grape::Entity
+ expose :id, :name, :token
+ expose :artifacts_file, using: ArtifactFile, if: ->(job, _) { job.artifacts? }
+ end
+
+ class Response < Grape::Entity
+ expose :id
+ expose :token
+ expose :allow_git_fetch
+
+ expose :job_info, using: JobInfo do |model|
+ model
+ end
+
+ expose :git_info, using: GitInfo do |model|
+ model
+ end
+
+ expose :runner_info, using: RunnerInfo do |model|
+ model
+ end
+
+ expose :variables
+ expose :steps, using: Step
+ expose :image, using: Image
+ expose :services, using: Image
+ expose :artifacts, using: Artifacts
+ expose :cache, using: Cache
+ expose :credentials, using: Credentials
+ expose :dependencies, using: Dependency
+ end
+ end
end
end
diff --git a/lib/api/environments.rb b/lib/api/environments.rb
index 1a7e68f0528..945771d46f3 100644
--- a/lib/api/environments.rb
+++ b/lib/api/environments.rb
@@ -9,7 +9,7 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects do
+ 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
@@ -79,7 +79,24 @@ module API
environment = user_project.environments.find(params[:environment_id])
- present environment.destroy, with: Entities::Environment
+ environment.destroy
+ end
+
+ desc 'Stops an existing environment' do
+ success Entities::Environment
+ end
+ params do
+ requires :environment_id, type: Integer, desc: 'The environment ID'
+ end
+ post ':id/environments/:environment_id/stop' do
+ authorize! :create_deployment, user_project
+
+ environment = user_project.environments.find(params[:environment_id])
+
+ environment.stop_with_action!(current_user)
+
+ status 200
+ present environment, with: Entities::Environment
end
end
end
diff --git a/lib/api/files.rb b/lib/api/files.rb
index 2e79e22e649..33fc970dc09 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -1,12 +1,11 @@
module API
- # Projects API
class Files < Grape::API
helpers do
def commit_params(attrs)
{
file_path: attrs[:file_path],
- source_branch: attrs[:branch_name],
- target_branch: attrs[:branch_name],
+ start_branch: attrs[:branch],
+ target_branch: attrs[:branch],
commit_message: attrs[:commit_message],
file_content: attrs[:content],
file_content_encoding: attrs[:encoding],
@@ -15,16 +14,29 @@ module API
}
end
+ def assign_file_vars!
+ 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!(@repo)
+ end
+
def commit_response(attrs)
{
file_path: attrs[:file_path],
- branch_name: attrs[:branch_name]
+ 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 :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb'
+ requires :branch, 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'
@@ -40,35 +52,36 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects do
- desc 'Get a file from repository'
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ desc 'Get raw file contents from the 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'
+ requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb'
+ requires :ref, type: String, desc: 'The name of branch, tag commit'
end
- get ":id/repository/files" do
- authorize! :download_code, user_project
-
- commit = user_project.commit(params[:ref])
- not_found!('Commit') unless commit
+ get ":id/repository/files/:file_path/raw" do
+ assign_file_vars!
- repo = user_project.repository
- blob = repo.blob_at(commit.sha, params[:file_path])
- not_found!('File') unless blob
+ send_git_blob @repo, @blob
+ end
- blob.load_all_data!(repo)
- status(200)
+ desc 'Get a file from the repository'
+ params do
+ requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb'
+ requires :ref, type: String, desc: 'The name of branch, tag or commit'
+ end
+ get ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do
+ assign_file_vars!
{
- file_name: blob.name,
- file_path: blob.path,
- size: blob.size,
+ file_name: @blob.name,
+ file_path: @blob.path,
+ size: @blob.size,
encoding: "base64",
- content: Base64.strict_encode64(blob.data),
+ 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])
+ blob_id: @blob.id,
+ commit_id: @commit.id,
+ last_commit_id: @repo.last_commit_id_for_path(@commit.sha, params[:file_path])
}
end
@@ -76,7 +89,7 @@ module API
params do
use :extended_file_params
end
- post ":id/repository/files" do
+ post ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do
authorize! :push_code, user_project
file_params = declared_params(include_missing: false)
@@ -94,7 +107,7 @@ module API
params do
use :extended_file_params
end
- put ":id/repository/files" do
+ put ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do
authorize! :push_code, user_project
file_params = declared_params(include_missing: false)
@@ -113,16 +126,13 @@ module API
params do
use :simple_file_params
end
- delete ":id/repository/files" do
+ delete ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do
authorize! :push_code, user_project
file_params = declared_params(include_missing: false)
- result = ::Files::DeleteService.new(user_project, current_user, commit_params(file_params)).execute
+ result = ::Files::DestroyService.new(user_project, current_user, commit_params(file_params)).execute
- if result[:status] == :success
- status(200)
- commit_response(file_params)
- else
+ if result[:status] != :success
render_api_error!(result[:message], 400)
end
end
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 7682d286866..8f3799417e3 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -7,7 +7,7 @@ module API
helpers do
params :optional_params do
optional :description, type: String, desc: 'The description of the group'
- optional :visibility_level, type: Integer, desc: 'The visibility level of the group'
+ optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the group'
optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group'
optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
end
@@ -36,12 +36,15 @@ module API
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 :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user'
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
+ groups = if params[:owned]
+ current_user.owned_groups
+ elsif current_user.admin
Group.all
elsif params[:all_available]
GroupsFinder.new.execute(current_user)
@@ -56,23 +59,13 @@ module API
present_groups groups, statistics: params[:statistics] && current_user.is_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'
+ optional :parent_id, type: Integer, desc: 'The parent group id for creating nested group'
use :optional_params
end
post do
@@ -91,7 +84,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a group'
end
- resource :groups do
+ resource :groups, requirements: { id: %r{[^/]+} } do
desc 'Update a group. Available only for users who can administrate groups.' do
success Entities::Group
end
@@ -99,7 +92,7 @@ module API
optional :name, type: String, desc: 'The name of the group'
optional :path, type: String, desc: 'The path of the group'
use :optional_params
- at_least_one_of :name, :path, :description, :visibility_level,
+ at_least_one_of :name, :path, :description, :visibility,
:lfs_enabled, :request_access_enabled
end
put ':id' do
@@ -125,7 +118,7 @@ module API
delete ":id" do
group = find_group!(params[:id])
authorize! :admin_group, group
- DestroyGroupService.new(group, current_user).execute
+ ::Groups::DestroyService.new(group, current_user).execute
end
desc 'Get a list of projects in this group.' do
@@ -133,7 +126,7 @@ module API
end
params do
optional :archived, type: Boolean, default: false, desc: 'Limit by archived status'
- optional :visibility, type: String, values: %w[public internal private],
+ optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values,
desc: 'Limit by visibility'
optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at],
@@ -142,6 +135,9 @@ module API
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
@@ -158,7 +154,7 @@ module API
params do
requires :project_id, type: String, desc: 'The ID or path of the project'
end
- post ":id/projects/:project_id" do
+ post ":id/projects/:project_id", requirements: { project_id: /.+/ } do
authenticated_as_admin!
group = find_group!(params[:id])
project = find_project!(params[:project_id])
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 49c5f0652ab..61527c1e20b 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -3,7 +3,7 @@ module API
include Gitlab::Utils
include Helpers::Pagination
- SUDO_HEADER = "HTTP_SUDO"
+ SUDO_HEADER = "HTTP_SUDO".freeze
SUDO_PARAM = :sudo
def declared_params(options = {})
@@ -45,7 +45,7 @@ module API
if id =~ /^\d+$/
Project.find_by(id: id)
else
- Project.find_with_namespace(id)
+ Project.find_by_full_path(id)
end
end
@@ -82,16 +82,27 @@ module API
label || not_found!('Label')
end
- def find_project_issue(id)
- IssuesFinder.new(current_user, project_id: user_project.id).find(id)
+ def find_project_issue(iid)
+ IssuesFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid)
end
- def find_project_merge_request(id)
- MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id)
+ def find_project_merge_request(iid)
+ MergeRequestsFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid)
+ end
+
+ def find_project_snippet(id)
+ finder_params = { filter: :by_project, project: user_project }
+ SnippetsFinder.new.execute(current_user, finder_params).find(id)
+ end
+
+ def find_merge_request_with_access(iid, access_level = :read_merge_request)
+ merge_request = user_project.merge_requests.find_by!(iid: iid)
+ authorize! access_level, merge_request
+ merge_request
end
def authenticate!
- unauthorized! unless current_user
+ unauthorized! unless current_user && can?(current_user, :access_api)
end
def authenticate_non_get!
@@ -110,7 +121,7 @@ module API
forbidden! unless current_user.is_admin?
end
- def authorize!(action, subject = nil)
+ def authorize!(action, subject = :global)
forbidden! unless can?(current_user, action, subject)
end
@@ -128,7 +139,7 @@ module API
end
end
- def can?(object, action, subject)
+ def can?(object, action, subject = :global)
Ability.allowed?(object, action, subject)
end
@@ -147,33 +158,21 @@ module API
params_hash = custom_params || params
attrs = {}
keys.each do |key|
- if params_hash[key].present? or (params_hash.has_key?(key) and params_hash[key] == false)
+ if params_hash[key].present? || (params_hash.has_key?(key) && params_hash[key] == false)
attrs[key] = params_hash[key]
end
end
ActionController::Parameters.new(attrs).permit!
end
- # Checks the occurrences of datetime attributes, each attribute if present in the params hash must be in ISO 8601
- # format (YYYY-MM-DDTHH:MM:SSZ) or a Bad Request error is invoked.
- #
- # Parameters:
- # keys (required) - An array consisting of elements that must be parseable as dates from the params hash
- def datetime_attributes!(*keys)
- keys.each do |key|
- begin
- params[key] = Time.xmlschema(params[key]) if params[key].present?
- rescue ArgumentError
- message = "\"" + key.to_s + "\" must be a timestamp in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ"
- render_api_error!(message, 400)
- end
- end
- end
-
def filter_by_iid(items, iid)
items.where(iid: iid)
end
+ def filter_by_search(items, text)
+ items.search(text)
+ end
+
# error helpers
def forbidden!(reason = nil)
@@ -219,12 +218,20 @@ module API
render_api_error!('204 No Content', 204)
end
+ def accepted!
+ render_api_error!('202 Accepted', 202)
+ end
+
def render_validation_error!(model)
if model.errors.any?
render_api_error!(model.errors.messages || '400 Bad Request', 400)
end
end
+ def render_spam_error!
+ render_api_error!({ error: 'Spam detected' }, 400)
+ end
+
def render_api_error!(message, status)
error!({ 'message' => message }, status, header)
end
@@ -250,6 +257,18 @@ module API
# project helpers
def filter_projects(projects)
+ if params[:membership]
+ projects = projects.merge(current_user.authorized_projects)
+ end
+
+ if params[:owned]
+ projects = projects.merge(current_user.owned_projects)
+ end
+
+ if params[:starred]
+ projects = projects.merge(current_user.starred_projects)
+ end
+
if params[:search].present?
projects = projects.search(params[:search])
end
@@ -298,7 +317,7 @@ module API
header['X-Sendfile'] = path
body
else
- path
+ file path
end
end
@@ -322,16 +341,17 @@ module API
def initial_current_user
return @initial_current_user if defined?(@initial_current_user)
+ Gitlab::Auth::UniqueIpsLimiter.limit_user! do
+ @initial_current_user ||= find_user_by_private_token(scopes: @scopes)
+ @initial_current_user ||= doorkeeper_guard(scopes: @scopes)
+ @initial_current_user ||= find_user_from_warden
- @initial_current_user ||= find_user_by_private_token(scopes: @scopes)
- @initial_current_user ||= doorkeeper_guard(scopes: @scopes)
- @initial_current_user ||= find_user_from_warden
+ unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed?
+ @initial_current_user = nil
+ end
- unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed?
- @initial_current_user = nil
+ @initial_current_user
end
-
- @initial_current_user
end
def sudo!
@@ -374,14 +394,6 @@ module API
header(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format))
end
- def issue_entity(project)
- if project.has_external_issue_tracker?
- Entities::ExternalIssue
- else
- Entities::Issue
- end
- end
-
# The Grape Error Middleware only has access to env but no params. We workaround this by
# defining a method that returns the right value.
def define_params_for_grape_middleware
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index e8975eb57e0..2135a787b11 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -9,11 +9,11 @@ module API
# In addition, they may have a '.git' extension and multiple namespaces
#
# Transform all these cases to 'namespace/project'
- def clean_project_path(project_path, storage_paths = Repository.storages.values)
+ def clean_project_path(project_path, storages = Gitlab.config.repositories.storages.values)
project_path = project_path.sub(/\.git\z/, '')
- storage_paths.each do |storage_path|
- storage_path = File.expand_path(storage_path)
+ storages.each do |storage|
+ storage_path = File.expand_path(storage['path'])
if project_path.start_with?(storage_path)
project_path = project_path.sub(storage_path, '')
@@ -30,7 +30,7 @@ module API
def wiki?
@wiki ||= project_path.end_with?('.wiki') &&
- !Project.find_with_namespace(project_path)
+ !Project.find_by_full_path(project_path)
end
def project
@@ -41,7 +41,7 @@ module API
# the wiki repository as well.
project_path.chomp!('.wiki') if wiki?
- Project.find_with_namespace(project_path)
+ Project.find_by_full_path(project_path)
end
end
diff --git a/lib/api/helpers/pagination.rb b/lib/api/helpers/pagination.rb
index 2199eea7e5f..0764b58fb4c 100644
--- a/lib/api/helpers/pagination.rb
+++ b/lib/api/helpers/pagination.rb
@@ -2,7 +2,7 @@ module API
module Helpers
module Pagination
def paginate(relation)
- relation.page(params[:page]).per(params[:per_page].to_i).tap do |data|
+ relation.page(params[:page]).per(params[:per_page]).tap do |data|
add_pagination_headers(data)
end
end
diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb
new file mode 100644
index 00000000000..74848a6e144
--- /dev/null
+++ b/lib/api/helpers/runner.rb
@@ -0,0 +1,69 @@
+module API
+ module Helpers
+ module Runner
+ JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN'.freeze
+ JOB_TOKEN_PARAM = :token
+ UPDATE_RUNNER_EVERY = 10 * 60
+
+ def runner_registration_token_valid?
+ ActiveSupport::SecurityUtils.variable_size_secure_compare(params[:token],
+ current_application_settings.runners_registration_token)
+ end
+
+ def get_runner_version_from_params
+ return unless params['info'].present?
+ attributes_for_keys(%w(name version revision platform architecture), params['info'])
+ end
+
+ def authenticate_runner!
+ forbidden! unless current_runner
+ end
+
+ def current_runner
+ @runner ||= ::Ci::Runner.find_by_token(params[:token].to_s)
+ end
+
+ def update_runner_info
+ return unless update_runner?
+
+ current_runner.contacted_at = Time.now
+ current_runner.assign_attributes(get_runner_version_from_params)
+ current_runner.save if current_runner.changed?
+ end
+
+ def update_runner?
+ # Use a random threshold to prevent beating DB updates.
+ # It generates a distribution between [40m, 80m].
+ #
+ contacted_at_max_age = UPDATE_RUNNER_EVERY + Random.rand(UPDATE_RUNNER_EVERY)
+
+ current_runner.contacted_at.nil? ||
+ (Time.now - current_runner.contacted_at) >= contacted_at_max_age
+ end
+
+ def validate_job!(job)
+ not_found! unless job
+
+ yield if block_given?
+
+ forbidden!('Project has been deleted!') unless job.project
+ forbidden!('Job has been erased!') if job.erased?
+ end
+
+ def authenticate_job!(job)
+ validate_job!(job) do
+ forbidden! unless job_token_valid?(job)
+ end
+ end
+
+ def job_token_valid?(job)
+ token = (params[JOB_TOKEN_PARAM] || env[JOB_TOKEN_HEADER]).to_s
+ token && job.valid_token?(token)
+ end
+
+ def max_artifacts_size
+ current_application_settings.max_artifacts_size.megabytes.to_i
+ end
+ end
+ end
+end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index d235977fbd8..523f38d129e 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -132,6 +132,18 @@ module API
{ success: true, recovery_codes: codes }
end
+
+ post "/notify_post_receive" do
+ status 200
+
+ return unless Gitlab::GitalyClient.enabled?
+
+ begin
+ Gitlab::GitalyClient::Notifications.new(params[:repo_path]).post_receive
+ rescue GRPC::Unavailable => e
+ render_api_error(e, 500)
+ end
+ end
end
end
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index fe016c1ec0a..4dce5dd130a 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -10,19 +10,9 @@ module API
args.delete(:id)
args[:milestone_title] = args.delete(:milestone)
+ args[:label_name] = args.delete(:labels)
- match_all_labels = args.delete(:match_all_labels)
- labels = args.delete(:labels)
- args[:label_name] = labels if match_all_labels
-
- args[:search] = "#{Issue.reference_prefix}#{args.delete(:iid)}" if args.key?(:iid)
-
- issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations
-
- # TODO: Remove in 9.0 pass `label_name: args.delete(:labels)` to IssuesFinder
- if !match_all_labels && labels.present?
- issues = issues.includes(:labels).where('labels.title' => labels.split(','))
- end
+ issues = IssuesFinder.new(current_user, args).execute
issues.reorder(args[:order_by] => args[:sort])
end
@@ -35,6 +25,7 @@ module API
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'
+ optional :iids, type: Array[Integer], desc: 'The IID array of issues'
use :pagination
end
@@ -50,7 +41,7 @@ module API
resource :issues do
desc "Get currently authenticated user's issues" do
- success Entities::Issue
+ success Entities::IssueBasic
end
params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
@@ -60,62 +51,61 @@ module API
get do
issues = find_issues(scope: 'authored')
- present paginate(issues), with: Entities::Issue, current_user: current_user
+ present paginate(issues), with: Entities::IssueBasic, current_user: current_user
end
end
params do
requires :id, type: String, desc: 'The ID of a group'
end
- resource :groups do
+ resource :groups, requirements: { id: %r{[^/]+} } do
desc 'Get a list of group issues' do
- success Entities::Issue
+ success Entities::IssueBasic
end
params do
- optional :state, type: String, values: %w[opened closed all], default: 'opened',
+ 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, state: params[:state] || 'opened', match_all_labels: true)
+ issues = find_issues(group_id: group.id)
- present paginate(issues), with: Entities::Issue, current_user: current_user
+ present paginate(issues), with: Entities::IssueBasic, current_user: current_user
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
include TimeTrackingEndpoints
desc 'Get a list of project issues' do
- success Entities::Issue
+ success Entities::IssueBasic
end
params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
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])
+ project = find_project!(params[:id])
issues = find_issues(project_id: project.id)
- present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project
+ present paginate(issues), with: Entities::IssueBasic, current_user: current_user, project: user_project
end
desc 'Get a single project issue' do
success Entities::Issue
end
params do
- requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+ requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
end
- get ":id/issues/:issue_id" do
- issue = find_project_issue(params[:issue_id])
+ get ":id/issues/:issue_iid" do
+ issue = find_project_issue(params[:issue_iid])
present issue, with: Entities::Issue, current_user: current_user, project: user_project
end
@@ -126,8 +116,10 @@ module API
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,
+ optional :merge_request_to_resolve_discussions_of, type: Integer,
desc: 'The IID of a merge request for which to resolve discussions'
+ optional :discussion_to_resolve, type: String,
+ desc: 'The ID of a discussion to resolve, also pass `merge_request_to_resolve_discussions_of`'
use :issue_params
end
post ':id/issues' do
@@ -138,12 +130,6 @@ module API
issue_params = declared_params(include_missing: false)
- if merge_request_iid = params[:merge_request_for_resolving_discussions]
- issue_params[:merge_request_for_resolving_discussions] = MergeRequestsFinder.new(current_user, project_id: user_project.id).
- execute.
- find_by(iid: merge_request_iid)
- end
-
issue = ::Issues::CreateService.new(user_project,
current_user,
issue_params.merge(request: request, api: true)).execute
@@ -162,7 +148,7 @@ module API
success Entities::Issue
end
params do
- requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+ requires :issue_iid, type: Integer, desc: 'The internal 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.'
@@ -171,8 +157,8 @@ module API
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
- issue = user_project.issues.find(params.delete(:issue_id))
+ put ':id/issues/:issue_iid' do
+ issue = user_project.issues.find_by!(iid: params.delete(:issue_iid))
authorize! :update_issue, issue
# Setting created_at time only allowed for admins and project owners
@@ -180,9 +166,13 @@ module API
params.delete(:updated_at)
end
+ update_params = declared_params(include_missing: false).merge(request: request, api: true)
+
issue = ::Issues::UpdateService.new(user_project,
current_user,
- declared_params(include_missing: false)).execute(issue)
+ update_params).execute(issue)
+
+ render_spam_error! if issue.spam?
if issue.valid?
present issue, with: Entities::Issue, current_user: current_user, project: user_project
@@ -195,11 +185,11 @@ module API
success Entities::Issue
end
params do
- requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+ requires :issue_iid, type: Integer, desc: 'The internal 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
- issue = user_project.issues.find_by(id: params[:issue_id])
+ post ':id/issues/:issue_iid/move' do
+ issue = user_project.issues.find_by(iid: params[:issue_iid])
not_found!('Issue') unless issue
new_project = Project.find_by(id: params[:to_project_id])
@@ -215,10 +205,10 @@ module API
desc 'Delete a project issue'
params do
- requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+ requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
end
- delete ":id/issues/:issue_id" do
- issue = user_project.issues.find_by(id: params[:issue_id])
+ delete ":id/issues/:issue_iid" do
+ issue = user_project.issues.find_by(iid: params[:issue_iid])
not_found!('Issue') unless issue
authorize!(:destroy_issue, issue)
diff --git a/lib/api/builds.rb b/lib/api/jobs.rb
index af61be343be..ffab0aafe59 100644
--- a/lib/api/builds.rb
+++ b/lib/api/jobs.rb
@@ -1,5 +1,5 @@
module API
- class Builds < Grape::API
+ class Jobs < Grape::API
include PaginationParams
before { authenticate! }
@@ -7,16 +7,19 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
helpers do
params :optional_scope do
optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show',
- values: ['pending', 'running', 'failed', 'success', 'canceled'],
+ values: ::CommitStatus::AVAILABLE_STATUSES,
coerce_with: ->(scope) {
- if scope.is_a?(String)
+ case scope
+ when String
[scope]
- elsif scope.is_a?(Hashie::Mash)
+ when Hashie::Mash
scope.values
+ when Hashie::Array
+ scope
else
['unknown']
end
@@ -24,79 +27,72 @@ module API
end
end
- desc 'Get a project builds' do
- success Entities::Build
+ desc 'Get a projects jobs' do
+ success Entities::Job
end
params do
use :optional_scope
use :pagination
end
- get ':id/builds' do
+ get ':id/jobs' do
builds = user_project.builds.order('id DESC')
builds = filter_builds(builds, params[:scope])
- present paginate(builds), with: Entities::Build,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ present paginate(builds), with: Entities::Job
end
- desc 'Get builds for a specific commit of a project' do
- success Entities::Build
+ desc 'Get pipeline jobs' do
+ success Entities::Job
end
params do
- requires :sha, type: String, desc: 'The SHA id of a commit'
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
use :optional_scope
use :pagination
end
- get ':id/repository/commits/:sha/builds' do
- authorize_read_builds!
-
- return not_found! unless user_project.commit(params[:sha])
-
- pipelines = user_project.pipelines.where(sha: params[:sha])
- builds = user_project.builds.where(pipeline: pipelines).order('id DESC')
+ get ':id/pipelines/:pipeline_id/jobs' do
+ pipeline = user_project.pipelines.find(params[:pipeline_id])
+ builds = pipeline.builds
builds = filter_builds(builds, params[:scope])
- present paginate(builds), with: Entities::Build,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ present paginate(builds), with: Entities::Job
end
- desc 'Get a specific build of a project' do
- success Entities::Build
+ desc 'Get a specific job of a project' do
+ success Entities::Job
end
params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
+ requires :job_id, type: Integer, desc: 'The ID of a job'
end
- get ':id/builds/:build_id' do
+ get ':id/jobs/:job_id' do
authorize_read_builds!
- build = get_build!(params[:build_id])
+ build = get_build!(params[:job_id])
- present build, with: Entities::Build,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ present build, with: Entities::Job
end
- desc 'Download the artifacts file from build' do
+ desc 'Download the artifacts file from a job' do
detail 'This feature was introduced in GitLab 8.5'
end
params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
+ requires :job_id, type: Integer, desc: 'The ID of a job'
end
- get ':id/builds/:build_id/artifacts' do
+ get ':id/jobs/:job_id/artifacts' do
authorize_read_builds!
- build = get_build!(params[:build_id])
+ build = get_build!(params[:job_id])
present_artifacts!(build.artifacts_file)
end
- desc 'Download the artifacts file from build' do
+ desc 'Download the artifacts file from a job' do
detail 'This feature was introduced in GitLab 8.10'
end
params do
requires :ref_name, type: String, desc: 'The ref from repository'
- requires :job, type: String, desc: 'The name for the build'
+ requires :job, type: String, desc: 'The name for the job'
end
- get ':id/builds/artifacts/:ref_name/download',
+ get ':id/jobs/artifacts/:ref_name/download',
requirements: { ref_name: /.+/ } do
authorize_read_builds!
@@ -109,14 +105,14 @@ module API
# TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace
# is saved in the DB instead of file). But before that, we need to consider how to replace the value of
# `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse.
- desc 'Get a trace of a specific build of a project'
+ desc 'Get a trace of a specific job of a project'
params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
+ requires :job_id, type: Integer, desc: 'The ID of a job'
end
- get ':id/builds/:build_id/trace' do
+ get ':id/jobs/:job_id/trace' do
authorize_read_builds!
- build = get_build!(params[:build_id])
+ build = get_build!(params[:job_id])
header 'Content-Disposition', "infile; filename=\"#{build.id}.log\""
content_type 'text/plain'
@@ -126,96 +122,91 @@ module API
body trace
end
- desc 'Cancel a specific build of a project' do
- success Entities::Build
+ desc 'Cancel a specific job of a project' do
+ success Entities::Job
end
params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
+ requires :job_id, type: Integer, desc: 'The ID of a job'
end
- post ':id/builds/:build_id/cancel' do
+ post ':id/jobs/:job_id/cancel' do
authorize_update_builds!
- build = get_build!(params[:build_id])
+ build = get_build!(params[:job_id])
build.cancel
- present build, with: Entities::Build,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ present build, with: Entities::Job
end
desc 'Retry a specific build of a project' do
- success Entities::Build
+ success Entities::Job
end
params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
+ requires :job_id, type: Integer, desc: 'The ID of a build'
end
- post ':id/builds/:build_id/retry' do
+ post ':id/jobs/:job_id/retry' do
authorize_update_builds!
- build = get_build!(params[:build_id])
- return forbidden!('Build is not retryable') unless build.retryable?
+ build = get_build!(params[:job_id])
+ return forbidden!('Job is not retryable') unless build.retryable?
build = Ci::Build.retry(build, current_user)
- present build, with: Entities::Build,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ present build, with: Entities::Job
end
- desc 'Erase build (remove artifacts and build trace)' do
- success Entities::Build
+ desc 'Erase job (remove artifacts and the trace)' do
+ success Entities::Job
end
params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
+ requires :job_id, type: Integer, desc: 'The ID of a build'
end
- post ':id/builds/:build_id/erase' do
+ post ':id/jobs/:job_id/erase' do
authorize_update_builds!
- build = get_build!(params[:build_id])
- return forbidden!('Build is not erasable!') unless build.erasable?
+ build = get_build!(params[:job_id])
+ return forbidden!('Job is not erasable!') unless build.erasable?
build.erase(erased_by: current_user)
- present build, with: Entities::Build,
- user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
+ present build, with: Entities::Job
end
desc 'Keep the artifacts to prevent them from being deleted' do
- success Entities::Build
+ success Entities::Job
end
params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
+ requires :job_id, type: Integer, desc: 'The ID of a job'
end
- post ':id/builds/:build_id/artifacts/keep' do
+ post ':id/jobs/:job_id/artifacts/keep' do
authorize_update_builds!
- build = get_build!(params[:build_id])
+ build = get_build!(params[:job_id])
return not_found!(build) unless build.artifacts?
build.keep_artifacts!
status 200
- present build, with: Entities::Build,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ present build, with: Entities::Job
end
- desc 'Trigger a manual build' do
- success Entities::Build
+ desc 'Trigger a manual job' do
+ success Entities::Job
detail 'This feature was added in GitLab 8.11'
end
params do
- requires :build_id, type: Integer, desc: 'The ID of a Build'
+ requires :job_id, type: Integer, desc: 'The ID of a Job'
end
- post ":id/builds/:build_id/play" do
+ post ":id/jobs/:job_id/play" do
authorize_read_builds!
- build = get_build!(params[:build_id])
+ build = get_build!(params[:job_id])
- bad_request!("Unplayable Build") unless build.playable?
+ bad_request!("Unplayable Job") unless build.playable?
build.play(current_user)
status 200
- present build, with: Entities::Build,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ present build, with: Entities::Job
end
end
diff --git a/lib/api/labels.rb b/lib/api/labels.rb
index 652786d4e3e..20b25529d0c 100644
--- a/lib/api/labels.rb
+++ b/lib/api/labels.rb
@@ -1,17 +1,21 @@
module API
- # Labels API
class Labels < Grape::API
+ include PaginationParams
+
before { authenticate! }
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get all labels of the project' do
success Entities::Label
end
+ params do
+ use :pagination
+ end
get ':id/labels' do
- present available_labels, with: Entities::Label, current_user: current_user, project: user_project
+ present paginate(available_labels), with: Entities::Label, current_user: current_user, project: user_project
end
desc 'Create a new label' do
@@ -19,7 +23,7 @@ module API
end
params do
requires :name, type: String, desc: 'The name of the label to be created'
- requires :color, type: String, desc: "The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB)"
+ requires :color, type: String, desc: "The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the allowed CSS color names"
optional :description, type: String, desc: 'The description of label to be created'
optional :priority, type: Integer, desc: 'The priority of the label', allow_blank: true
end
@@ -30,7 +34,7 @@ module API
conflict!('Label already exists') if label
priority = params.delete(:priority)
- label = user_project.labels.create(declared_params(include_missing: false))
+ label = ::Labels::CreateService.new(declared_params(include_missing: false)).execute(project: user_project)
if label.valid?
label.prioritize!(user_project, priority) if priority
@@ -52,7 +56,7 @@ module API
label = user_project.labels.find_by(title: params[:name])
not_found!('Label') unless label
- present label.destroy, with: Entities::Label, current_user: current_user, project: user_project
+ label.destroy
end
desc 'Update an existing label. At least one optional parameter is required.' do
@@ -61,7 +65,7 @@ module API
params do
requires :name, type: String, desc: 'The name of the label to be updated'
optional :new_name, type: String, desc: 'The new name of the label'
- optional :color, type: String, desc: "The new color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB)"
+ optional :color, type: String, desc: "The new color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the allowed CSS color names"
optional :description, type: String, desc: 'The new description of label'
optional :priority, type: Integer, desc: 'The priority of the label', allow_blank: true
at_least_one_of :new_name, :color, :description, :priority
@@ -78,7 +82,8 @@ module API
# Rename new name to the actual label attribute name
label_params[:name] = label_params.delete(:new_name) if label_params.key?(:new_name)
- render_validation_error!(label) unless label.update(label_params)
+ label = ::Labels::UpdateService.new(label_params).execute(label)
+ render_validation_error!(label) unless label.valid?
if update_priority
if priority.nil?
diff --git a/lib/api/members.rb b/lib/api/members.rb
index d85f1f78cd6..c200e46a328 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -10,7 +10,7 @@ module API
params do
requires :id, type: String, desc: "The #{source_type} ID"
end
- resource source_type.pluralize do
+ resource source_type.pluralize, requirements: { id: %r{[^/]+} } do
desc 'Gets a list of group or project members viewable by the authenticated user.' do
success Entities::Member
end
@@ -55,24 +55,13 @@ module API
authorize_admin_source!(source_type, source)
member = source.members.find_by(user_id: params[:user_id])
+ conflict!('Member already exists') if member
- # 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
+ member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
if member.persisted? && member.valid?
present member.user, with: Entities::Member, member: 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
@@ -86,18 +75,14 @@ module API
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[:id])
+ source = find_source(source_type, params.delete(:id))
authorize_admin_source!(source_type, source)
- member = source.members.find_by!(user_id: params[:user_id])
- attrs = attributes_for_keys [:access_level, :expires_at]
+ member = source.members.find_by!(user_id: params.delete(:user_id))
- if member.update_attributes(attrs)
+ if member.update_attributes(declared_params(include_missing: false))
present member.user, with: Entities::Member, member: 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
@@ -108,24 +93,10 @@ module API
end
delete ":id/members/:user_id" do
source = find_source(source_type, params[:id])
+ # Ensure that memeber exists
+ source.members.find_by!(user_id: params[:user_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?
- { message: "Access revoked", id: params[:user_id].to_i }
- else
- ::Members::DestroyService.new(source, current_user, declared_params).execute
-
- present member.user, with: Entities::Member, member: member
- end
+ ::Members::DestroyService.new(source, current_user, declared_params).execute
end
end
end
diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb
index 07435d78468..4b79eac2b8b 100644
--- a/lib/api/merge_request_diffs.rb
+++ b/lib/api/merge_request_diffs.rb
@@ -1,25 +1,27 @@
module API
# MergeRequestDiff API
class MergeRequestDiffs < Grape::API
+ include PaginationParams
+
before { authenticate! }
- resource :projects do
+ 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 Entities::MergeRequestDiff
end
params do
- requires :id, type: String, desc: 'The ID of a project'
- requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
+ use :pagination
end
+ get ":id/merge_requests/:merge_request_iid/versions" do
+ merge_request = find_merge_request_with_access(params[:merge_request_iid])
- get ":id/merge_requests/:merge_request_id/versions" do
- merge_request = user_project.merge_requests.
- find(params[:merge_request_id])
-
- authorize! :read_merge_request, merge_request
- present merge_request.merge_request_diffs, with: Entities::MergeRequestDiff
+ present paginate(merge_request.merge_request_diffs), with: Entities::MergeRequestDiff
end
desc 'Get a single merge request diff version' do
@@ -28,16 +30,13 @@ module API
end
params do
- requires :id, type: String, desc: 'The ID of a project'
- requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ requires :merge_request_iid, type: Integer, desc: 'The IID 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 = user_project.merge_requests.
- find(params[:merge_request_id])
+ get ":id/merge_requests/:merge_request_iid/versions/:version_id" do
+ merge_request = find_merge_request_with_access(params[:merge_request_iid])
- authorize! :read_merge_request, merge_request
present merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull
end
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index e77af4b7a0d..c8033664133 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -2,14 +2,12 @@ module API
class MergeRequests < Grape::API
include PaginationParams
- DEPRECATION_MESSAGE = 'This endpoint is deprecated and will be removed in GitLab 9.0.'.freeze
-
before { authenticate! }
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
include TimeTrackingEndpoints
helpers do
@@ -27,6 +25,14 @@ module API
render_api_error!(errors, 400)
end
+ def issue_entity(project)
+ if project.has_external_issue_tracker?
+ Entities::ExternalIssue
+ else
+ Entities::IssueBasic
+ end
+ end
+
params :optional_params do
optional :description, type: String, desc: 'The description of the merge request'
optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request'
@@ -37,7 +43,7 @@ module API
end
desc 'List merge requests' do
- success Entities::MergeRequest
+ success Entities::MergeRequestBasic
end
params do
optional :state, type: String, values: %w[opened closed merged all], default: 'all',
@@ -46,14 +52,14 @@ module API
desc: 'Return merge requests ordered by `created_at` or `updated_at` fields.'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return merge requests sorted in `asc` or `desc` order.'
- optional :iid, type: Array[Integer], desc: 'The IID of the merge requests'
+ optional :iids, type: Array[Integer], desc: 'The IID array of merge requests'
use :pagination
end
get ":id/merge_requests" do
authorize! :read_merge_request, user_project
merge_requests = user_project.merge_requests.inc_notes_with_associations
- merge_requests = filter_by_iid(merge_requests, params[:iid]) if params[:iid].present?
+ merge_requests = filter_by_iid(merge_requests, params[:iids]) if params[:iids].present?
merge_requests =
case params[:state]
@@ -64,7 +70,7 @@ module API
end
merge_requests = merge_requests.reorder(params[:order_by] => params[:sort])
- present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user, project: user_project
+ present paginate(merge_requests), with: Entities::MergeRequestBasic, current_user: current_user, project: user_project
end
desc 'Create a merge request' do
@@ -95,190 +101,141 @@ module API
desc 'Delete a merge request'
params do
- requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
end
- delete ":id/merge_requests/:merge_request_id" do
- merge_request = find_project_merge_request(params[:merge_request_id])
+ delete ":id/merge_requests/:merge_request_iid" do
+ merge_request = find_project_merge_request(params[:merge_request_iid])
authorize!(:destroy_merge_request, merge_request)
merge_request.destroy
end
- # Routing "merge_request/:merge_request_id/..." is DEPRECATED and WILL BE REMOVED in version 9.0
- # Use "merge_requests/:merge_request_id/..." instead.
- #
params do
- requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
end
- { ":id/merge_request/:merge_request_id" => :deprecated, ":id/merge_requests/:merge_request_id" => :ok }.each do |path, status|
- desc 'Get a single merge request' do
- if status == :deprecated
- detail DEPRECATION_MESSAGE
- end
- success Entities::MergeRequest
- end
- get path do
- merge_request = find_project_merge_request(params[:merge_request_id])
- authorize! :read_merge_request, merge_request
- present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
- end
-
- desc 'Get the commits of a merge request' do
- success Entities::RepoCommit
- end
- get "#{path}/commits" do
- merge_request = find_project_merge_request(params[:merge_request_id])
- authorize! :read_merge_request, merge_request
- present merge_request.commits, with: Entities::RepoCommit
- end
-
- desc 'Show the merge request changes' do
- success Entities::MergeRequestChanges
- end
- get "#{path}/changes" do
- merge_request = find_project_merge_request(params[:merge_request_id])
- authorize! :read_merge_request, merge_request
- present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
- end
-
- desc 'Update a merge request' do
- success Entities::MergeRequest
- end
- params do
- optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
- optional :target_branch, type: String, allow_blank: false, desc: 'The target branch'
- optional :state_event, type: String, values: %w[close reopen merge],
- desc: 'Status of the merge request'
- use :optional_params
- at_least_one_of :title, :target_branch, :description, :assignee_id,
- :milestone_id, :labels, :state_event,
- :remove_source_branch
- end
- put path do
- merge_request = find_project_merge_request(params.delete(:merge_request_id))
- authorize! :update_merge_request, merge_request
-
- mr_params = declared_params(include_missing: false)
- mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
-
- merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request)
+ desc 'Get a single merge request' do
+ success Entities::MergeRequest
+ end
+ get ':id/merge_requests/:merge_request_iid' do
+ merge_request = find_merge_request_with_access(params[:merge_request_iid])
- if merge_request.valid?
- present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
- else
- handle_merge_request_errors! merge_request.errors
- end
- end
+ present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
+ end
- desc 'Merge a merge request' do
- success Entities::MergeRequest
- end
- params do
- optional :merge_commit_message, type: String, desc: 'Custom merge commit message'
- optional :should_remove_source_branch, type: Boolean,
- desc: 'When true, the source branch will be deleted if possible'
- optional :merge_when_build_succeeds, type: Boolean,
- desc: 'When true, this merge request will be merged when the pipeline succeeds'
- optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
- end
- put "#{path}/merge" do
- merge_request = find_project_merge_request(params[:merge_request_id])
+ desc 'Get the commits of a merge request' do
+ success Entities::RepoCommit
+ end
+ get ':id/merge_requests/:merge_request_iid/commits' do
+ merge_request = find_merge_request_with_access(params[:merge_request_iid])
+ commits = ::Kaminari.paginate_array(merge_request.commits)
- # Merge request can not be merged
- # because user dont have permissions to push into target branch
- unauthorized! unless merge_request.can_be_merged_by?(current_user)
+ present paginate(commits), with: Entities::RepoCommit
+ end
- not_allowed! unless merge_request.mergeable_state?
+ desc 'Show the merge request changes' do
+ success Entities::MergeRequestChanges
+ end
+ get ':id/merge_requests/:merge_request_iid/changes' do
+ merge_request = find_merge_request_with_access(params[:merge_request_iid])
- render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable?
+ present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
+ end
- if params[:sha] && merge_request.diff_head_sha != params[:sha]
- render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409)
- end
+ desc 'Update a merge request' do
+ success Entities::MergeRequest
+ end
+ params do
+ optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
+ optional :target_branch, type: String, allow_blank: false, desc: 'The target branch'
+ optional :state_event, type: String, values: %w[close reopen],
+ desc: 'Status of the merge request'
+ use :optional_params
+ at_least_one_of :title, :target_branch, :description, :assignee_id,
+ :milestone_id, :labels, :state_event,
+ :remove_source_branch
+ end
+ put ':id/merge_requests/:merge_request_iid' do
+ merge_request = find_merge_request_with_access(params.delete(:merge_request_iid), :update_merge_request)
- merge_params = {
- commit_message: params[:merge_commit_message],
- should_remove_source_branch: params[:should_remove_source_branch]
- }
+ mr_params = declared_params(include_missing: false)
+ mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
- if params[:merge_when_build_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active?
- ::MergeRequests::MergeWhenPipelineSucceedsService
- .new(merge_request.target_project, current_user, merge_params)
- .execute(merge_request)
- else
- ::MergeRequests::MergeService
- .new(merge_request.target_project, current_user, merge_params)
- .execute(merge_request)
- end
+ merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request)
+ if merge_request.valid?
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
+ else
+ handle_merge_request_errors! merge_request.errors
end
+ end
- desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do
- success Entities::MergeRequest
- end
- post "#{path}/cancel_merge_when_build_succeeds" do
- merge_request = find_project_merge_request(params[:merge_request_id])
+ desc 'Merge a merge request' do
+ success Entities::MergeRequest
+ end
+ params do
+ optional :merge_commit_message, type: String, desc: 'Custom merge commit message'
+ optional :should_remove_source_branch, type: Boolean,
+ desc: 'When true, the source branch will be deleted if possible'
+ optional :merge_when_pipeline_succeeds, type: Boolean,
+ desc: 'When true, this merge request will be merged when the pipeline succeeds'
+ optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
+ end
+ put ':id/merge_requests/:merge_request_iid/merge' do
+ merge_request = find_project_merge_request(params[:merge_request_iid])
- unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user)
+ # Merge request can not be merged
+ # because user dont have permissions to push into target branch
+ unauthorized! unless merge_request.can_be_merged_by?(current_user)
- ::MergeRequest::MergeWhenPipelineSucceedsService
- .new(merge_request.target_project, current_user)
- .cancel(merge_request)
- end
+ not_allowed! unless merge_request.mergeable_state?
- desc 'Get the comments of a merge request' do
- detail 'Duplicate. DEPRECATED and WILL BE REMOVED in 9.0'
- success Entities::MRNote
- end
- params do
- use :pagination
+ render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable?
+
+ if params[:sha] && merge_request.diff_head_sha != params[:sha]
+ render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409)
end
- get "#{path}/comments" do
- merge_request = find_project_merge_request(params[:merge_request_id])
- authorize! :read_merge_request, merge_request
+ merge_params = {
+ commit_message: params[:merge_commit_message],
+ should_remove_source_branch: params[:should_remove_source_branch]
+ }
- present paginate(merge_request.notes.fresh), with: Entities::MRNote
+ if params[:merge_when_pipeline_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active?
+ ::MergeRequests::MergeWhenPipelineSucceedsService
+ .new(merge_request.target_project, current_user, merge_params)
+ .execute(merge_request)
+ else
+ ::MergeRequests::MergeService
+ .new(merge_request.target_project, current_user, merge_params)
+ .execute(merge_request)
end
- desc 'Post a comment to a merge request' do
- detail 'Duplicate. DEPRECATED and WILL BE REMOVED in 9.0'
- success Entities::MRNote
- end
- params do
- requires :note, type: String, desc: 'The text of the comment'
- end
- post "#{path}/comments" do
- merge_request = find_project_merge_request(params[:merge_request_id])
- authorize! :create_note, merge_request
+ present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
+ end
- opts = {
- note: params[:note],
- noteable_type: 'MergeRequest',
- noteable_id: merge_request.id
- }
+ desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do
+ success Entities::MergeRequest
+ end
+ post ':id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds' do
+ merge_request = find_project_merge_request(params[:merge_request_iid])
- note = ::Notes::CreateService.new(user_project, current_user, opts).execute
+ unauthorized! unless merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user)
- if note.save
- present note, with: Entities::MRNote
- else
- render_api_error!("Failed to save note #{note.errors.messages}", 400)
- end
- end
+ ::MergeRequest::MergeWhenPipelineSucceedsService
+ .new(merge_request.target_project, current_user)
+ .cancel(merge_request)
+ end
- desc 'List issues that will be closed on merge' do
- success Entities::MRNote
- end
- params do
- use :pagination
- end
- get "#{path}/closes_issues" do
- merge_request = find_project_merge_request(params[:merge_request_id])
- issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
- present paginate(issues), with: issue_entity(user_project), current_user: current_user
- end
+ desc 'List issues that will be closed on merge' do
+ success Entities::MRNote
+ end
+ params do
+ use :pagination
+ end
+ get ':id/merge_requests/:merge_request_iid/closes_issues' do
+ merge_request = find_merge_request_with_access(params[:merge_request_iid])
+ issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
+ present paginate(issues), with: issue_entity(user_project), current_user: current_user
end
end
end
diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb
index 3c373a84ec5..e7ab82f08db 100644
--- a/lib/api/milestones.rb
+++ b/lib/api/milestones.rb
@@ -23,14 +23,15 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a list of project milestones' do
success 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'
+ optional :iids, type: Array[Integer], desc: 'The IIDs of the milestones'
+ optional :search, type: String, desc: 'The search criteria for the title or description of the milestone'
use :pagination
end
get ":id/milestones" do
@@ -38,7 +39,8 @@ module API
milestones = user_project.milestones
milestones = filter_milestones_state(milestones, params[:state])
- milestones = filter_by_iid(milestones, params[:iid]) if params[:iid].present?
+ milestones = filter_by_iid(milestones, params[:iids]) if params[:iids].present?
+ milestones = filter_by_search(milestones, params[:search]) if params[:search]
present paginate(milestones), with: Entities::Milestone
end
@@ -101,7 +103,7 @@ module API
end
desc 'Get all issues for a single project milestone' do
- success Entities::Issue
+ success Entities::IssueBasic
end
params do
requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
@@ -114,11 +116,38 @@ module API
finder_params = {
project_id: user_project.id,
- milestone_title: milestone.title
+ milestone_title: milestone.title,
+ sort: 'position_asc'
}
issues = IssuesFinder.new(current_user, finder_params).execute
- present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project
+ present paginate(issues), with: Entities::IssueBasic, current_user: current_user, project: user_project
+ end
+
+ desc 'Get all merge requests for a single project milestone' do
+ detail 'This feature was introduced in GitLab 9.'
+ success Entities::MergeRequestBasic
+ end
+ params do
+ requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
+ use :pagination
+ end
+ get ':id/milestones/:milestone_id/merge_requests' do
+ authorize! :read_milestone, user_project
+
+ milestone = user_project.milestones.find(params[:milestone_id])
+
+ finder_params = {
+ project_id: user_project.id,
+ milestone_id: milestone.id,
+ sort: 'position_asc'
+ }
+
+ merge_requests = MergeRequestsFinder.new(current_user, finder_params).execute
+ present paginate(merge_requests),
+ with: Entities::MergeRequestBasic,
+ current_user: current_user,
+ project: user_project
end
end
end
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 284e4cf549a..de39e579ac3 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -4,12 +4,12 @@ module API
before { authenticate! }
- NOTEABLE_TYPES = [Issue, MergeRequest, Snippet]
+ NOTEABLE_TYPES = [Issue, MergeRequest, Snippet].freeze
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
NOTEABLE_TYPES.each do |noteable_type|
noteables_str = noteable_type.to_s.underscore.pluralize
@@ -21,7 +21,7 @@ module API
use :pagination
end
get ":id/#{noteables_str}/:noteable_id/notes" do
- noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
+ noteable = find_project_noteable(noteables_str, params[:noteable_id])
if can?(current_user, noteable_read_ability_name(noteable), noteable)
# We exclude notes that are cross-references and that cannot be viewed
@@ -49,7 +49,7 @@ module API
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.send(noteables_str.to_sym).find(params[:noteable_id])
+ noteable = find_project_noteable(noteables_str, params[:noteable_id])
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)
@@ -69,22 +69,28 @@ module API
optional :created_at, type: String, desc: 'The creation date of the note'
end
post ":id/#{noteables_str}/:noteable_id/notes" do
+ noteable = find_project_noteable(noteables_str, params[:noteable_id])
+
opts = {
- note: params[:body],
- noteable_type: noteables_str.classify,
- noteable_id: params[:noteable_id]
+ note: params[:body],
+ noteable_type: noteables_str.classify,
+ noteable_id: noteable.id
}
- if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user)
- opts[:created_at] = params[:created_at]
- end
+ if can?(current_user, noteable_read_ability_name(noteable), noteable)
+ if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user)
+ opts[:created_at] = params[:created_at]
+ end
- note = ::Notes::CreateService.new(user_project, current_user, opts).execute
+ note = ::Notes::CreateService.new(user_project, current_user, opts).execute
- if note.valid?
- present note, with: Entities::const_get(note.class.name)
+ if note.valid?
+ present note, with: Entities.const_get(note.class.name)
+ else
+ not_found!("Note #{note.errors.messages}")
+ end
else
- not_found!("Note #{note.errors.messages}")
+ not_found!("Note")
end
end
@@ -125,14 +131,16 @@ module API
note = user_project.notes.find(params[:note_id])
authorize! :admin_note, note
- ::Notes::DeleteService.new(user_project, current_user).execute(note)
-
- present note, with: Entities::Note
+ ::Notes::DestroyService.new(user_project, current_user).execute(note)
end
end
end
helpers do
+ def find_project_noteable(noteables_str, noteable_id)
+ public_send("find_project_#{noteables_str.singularize}", noteable_id)
+ end
+
def noteable_read_ability_name(noteable)
"read_#{noteable.class.to_s.underscore}".to_sym
end
diff --git a/lib/api/notification_settings.rb b/lib/api/notification_settings.rb
index c5e9b3ad69b..992ea5dc24d 100644
--- a/lib/api/notification_settings.rb
+++ b/lib/api/notification_settings.rb
@@ -48,14 +48,14 @@ module API
end
%w[group project].each do |source_type|
- resource source_type.pluralize do
+ params do
+ requires :id, type: String, desc: "The #{source_type} ID"
+ end
+ resource source_type.pluralize, requirements: { id: %r{[^/]+} } do
desc "Get #{source_type} level notification level settings, defaults to Global" do
detail 'This feature was introduced in GitLab 8.12'
success Entities::NotificationSetting
end
- params do
- requires :id, type: String, desc: 'The group ID or project ID or project NAMESPACE/PROJECT_NAME'
- end
get ":id/notification_settings" do
source = find_source(source_type, params[:id])
@@ -69,7 +69,6 @@ module API
success Entities::NotificationSetting
end
params do
- requires :id, type: String, desc: 'The group ID or project ID or project NAMESPACE/PROJECT_NAME'
optional :level, type: String, desc: "The #{source_type} notification level"
NotificationSetting::EMAIL_EVENTS.each do |event|
optional event, type: Boolean, desc: 'Enable/disable this notification'
diff --git a/lib/api/pagination_params.rb b/lib/api/pagination_params.rb
index 8c1e4381a74..f566eb3ed2b 100644
--- a/lib/api/pagination_params.rb
+++ b/lib/api/pagination_params.rb
@@ -15,8 +15,8 @@ module API
included do
helpers do
params :pagination do
- optional :page, type: Integer, desc: 'Current page number'
- optional :per_page, type: Integer, desc: 'Number of items per page'
+ optional :page, type: Integer, default: 1, desc: 'Current page number'
+ optional :per_page, type: Integer, default: 20, desc: 'Number of items per page'
end
end
end
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
index b634b1d0222..754c3d85a04 100644
--- a/lib/api/pipelines.rb
+++ b/lib/api/pipelines.rb
@@ -7,23 +7,23 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get all Pipelines of the project' do
detail 'This feature was introduced in GitLab 8.11.'
- success Entities::Pipeline
+ success Entities::PipelineBasic
end
params do
use :pagination
- optional :scope, type: String, values: ['running', 'branches', 'tags'],
+ optional :scope, type: String, values: %w(running branches tags),
desc: 'Either running, branches, or tags'
end
get ':id/pipelines' do
authorize! :read_pipeline, user_project
pipelines = PipelinesFinder.new(user_project).execute(scope: params[:scope])
- present paginate(pipelines), with: Entities::Pipeline
+ present paginate(pipelines), with: Entities::PipelineBasic
end
-
+
desc 'Create a new pipeline' do
detail 'This feature was introduced in GitLab 8.14'
success Entities::Pipeline
@@ -58,7 +58,7 @@ module API
present pipeline, with: Entities::Pipeline
end
- desc 'Retry failed builds in the pipeline' do
+ desc 'Retry builds in the pipeline' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Pipeline
end
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index cb679e6658a..53791166c33 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -24,7 +24,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get project hooks' do
success Entities::ProjectHook
end
@@ -32,9 +32,7 @@ module API
use :pagination
end
get ":id/hooks" do
- hooks = paginate user_project.hooks
-
- present hooks, with: Entities::ProjectHook
+ present paginate(user_project.hooks), with: Entities::ProjectHook
end
desc 'Get a project hook' do
@@ -92,12 +90,9 @@ module API
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: Entities::ProjectHook
- rescue
- # ProjectHook can raise Error if hook_id not found
- not_found!("Error deleting hook #{params[:hook_id]}")
- end
+ hook = user_project.hooks.find(params.delete(:hook_id))
+
+ hook.destroy
end
end
end
diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb
index 9d8c5b63685..cfee38a9baf 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -7,7 +7,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
helpers do
def handle_project_member_errors(errors)
if errors[:project_access].any?
@@ -50,19 +50,19 @@ module API
requires :title, type: String, desc: 'The title of the snippet'
requires :file_name, type: String, desc: 'The file name of the snippet'
requires :code, type: String, desc: 'The content of the snippet'
- requires :visibility_level, type: Integer,
- values: [Gitlab::VisibilityLevel::PRIVATE,
- Gitlab::VisibilityLevel::INTERNAL,
- Gitlab::VisibilityLevel::PUBLIC],
- desc: 'The visibility level of the snippet'
+ requires :visibility, type: String,
+ values: Gitlab::VisibilityLevel.string_values,
+ desc: 'The visibility of the snippet'
end
post ":id/snippets" do
authorize! :create_project_snippet, user_project
- snippet_params = declared_params
+ 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: Entities::ProjectSnippet
else
@@ -78,11 +78,9 @@ module API
optional :title, type: String, desc: 'The title of the snippet'
optional :file_name, type: String, desc: 'The file name of the snippet'
optional :code, type: String, desc: 'The content of the snippet'
- optional :visibility_level, type: Integer,
- values: [Gitlab::VisibilityLevel::PRIVATE,
- Gitlab::VisibilityLevel::INTERNAL,
- Gitlab::VisibilityLevel::PUBLIC],
- desc: 'The visibility level of the snippet'
+ optional :visibility, type: String,
+ values: Gitlab::VisibilityLevel.string_values,
+ desc: 'The visibility of the snippet'
at_least_one_of :title, :file_name, :code, :visibility_level
end
put ":id/snippets/:snippet_id" do
@@ -92,12 +90,16 @@ module API
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
- if snippet.persisted?
+ render_spam_error! if snippet.spam?
+
+ if snippet.valid?
present snippet, with: Entities::ProjectSnippet
else
render_validation_error!(snippet)
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 941f47114a4..0fbe1669d45 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -16,26 +16,12 @@ module API
optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project'
optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project'
optional :lfs_enabled, type: Boolean, desc: 'Flag indication if Git LFS is enabled for that project'
- optional :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,
- Gitlab::VisibilityLevel::PUBLIC ], desc: 'Create a public project. The same as visibility_level = 20.'
+ optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the project.'
optional :public_builds, type: Boolean, desc: 'Perform public builds'
optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
- optional :only_allow_merge_if_build_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
+ optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
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
end
resource :projects do
@@ -58,9 +44,12 @@ module API
params :filter_params do
optional :archived, type: Boolean, default: false, desc: 'Limit by archived status'
- optional :visibility, type: String, values: %w[public internal private],
+ optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values,
desc: 'Limit by visibility'
- optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
+ optional :search, type: String, desc: 'Return list of projects matching the search criteria'
+ optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user'
+ optional :starred, type: Boolean, default: false, desc: 'Limit by starred status'
+ optional :membership, type: Boolean, default: false, desc: 'Limit by projects that the current user is a member of'
end
params :statistics_params do
@@ -93,91 +82,23 @@ module API
params do
use :collection_params
end
- get '/visible' do
- entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails
- present_projects ProjectsFinder.new.execute(current_user), with: entity
- end
-
- desc 'Get a projects list for authenticated user' do
- success Entities::BasicProjectDetails
- end
- params do
- use :collection_params
- end
get do
- authenticate!
-
- present_projects current_user.authorized_projects,
- with: Entities::ProjectWithAccess
- end
-
- desc 'Get an owned projects list for authenticated user' do
- success Entities::BasicProjectDetails
- end
- params do
- use :collection_params
- use :statistics_params
- end
- get '/owned' do
- authenticate!
-
- present_projects current_user.owned_projects,
- with: Entities::ProjectWithAccess,
- statistics: params[:statistics]
- end
-
- desc 'Gets starred project for the authenticated user' do
- success Entities::BasicProjectDetails
- end
- params do
- use :collection_params
- end
- get '/starred' do
- authenticate!
-
- present_projects current_user.viewable_starred_projects
- end
-
- desc 'Get all projects for admin user' do
- success Entities::BasicProjectDetails
- end
- params do
- use :collection_params
- use :statistics_params
- end
- get '/all' do
- authenticated_as_admin!
-
- present_projects Project.all, with: Entities::ProjectWithAccess, statistics: params[:statistics]
- end
-
- desc 'Search for projects the current user has access to' do
- success Entities::Project
- 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])
-
- present paginate(projects), with: Entities::Project
+ entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails
+ present_projects ProjectsFinder.new.execute(current_user), with: entity, statistics: params[:statistics]
end
desc 'Create new project' do
success Entities::Project
end
params do
- requires :name, type: String, desc: 'The name of the project'
+ optional :name, type: String, desc: 'The name of the project'
optional :path, type: String, desc: 'The path of the repository'
+ at_least_one_of :name, :path
use :optional_params
use :create_params
end
post do
- attrs = map_public_to_visibility_level(declared_params(include_missing: false))
+ attrs = declared_params(include_missing: false)
project = ::Projects::CreateService.new(current_user, attrs).execute
if project.saved?
@@ -206,7 +127,7 @@ module API
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))
+ attrs = declared_params(include_missing: false)
project = ::Projects::CreateService.new(user, attrs).execute
if project.saved?
@@ -221,7 +142,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: /[^\/]+/ } do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a single project' do
success Entities::ProjectWithAccess
end
@@ -247,7 +168,7 @@ module API
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
+ post ':id/fork' do
fork_params = declared_params(include_missing: false)
namespace_id = fork_params[:namespace]
@@ -284,16 +205,16 @@ module API
at_least_one_of :name, :description, :issues_enabled, :merge_requests_enabled,
:wiki_enabled, :builds_enabled, :snippets_enabled,
:shared_runners_enabled, :container_registry_enabled,
- :lfs_enabled, :public, :visibility_level, :public_builds,
- :request_access_enabled, :only_allow_merge_if_build_succeeds,
+ :lfs_enabled, :visibility, :public_builds,
+ :request_access_enabled, :only_allow_merge_if_pipeline_succeeds,
:only_allow_merge_if_all_discussions_are_resolved, :path,
:default_branch
end
put ':id' do
authorize_admin_project
- attrs = map_public_to_visibility_level(declared_params(include_missing: false))
+ attrs = declared_params(include_missing: false)
authorize! :rename_project, user_project if attrs[:name].present?
- authorize! :change_visibility_level, user_project if attrs[:visibility_level].present?
+ authorize! :change_visibility_level, user_project if attrs[:visibility].present?
result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
@@ -344,7 +265,7 @@ module API
desc 'Unstar a project' do
success Entities::Project
end
- delete ':id/star' do
+ post ':id/unstar' do
if current_user.starred?(user_project)
current_user.toggle_star(user_project)
user_project.reload
@@ -359,6 +280,8 @@ module API
delete ":id" do
authorize! :remove_project, user_project
::Projects::DestroyService.new(user_project, current_user, {}).async_execute
+
+ accepted!
end
desc 'Mark this project as forked from another'
@@ -428,7 +351,6 @@ module API
not_found!('Group Link') unless link
link.destroy
- no_content!
end
desc 'Upload a file'
@@ -452,6 +374,19 @@ module API
present paginate(users), with: Entities::UserBasic
end
+
+ desc 'Start the housekeeping task for a project' do
+ detail 'This feature was introduced in GitLab 9.0.'
+ end
+ post ':id/housekeeping' do
+ authorize_admin_project
+
+ begin
+ ::Projects::HousekeepingService.new(user_project).execute
+ rescue ::Projects::HousekeepingService::LeaseTaken => error
+ conflict!(error.message)
+ end
+ end
end
end
end
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index 4ca6646a6f1..8f16e532ecb 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -2,12 +2,14 @@ require 'mime/types'
module API
class Repositories < Grape::API
+ include PaginationParams
+
before { authorize! :download_code, user_project }
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
helpers do
def handle_project_member_errors(errors)
if errors[:project_access].any?
@@ -15,61 +17,67 @@ module API
end
not_found!
end
+
+ def assign_blob_vars!
+ authorize! :download_code, user_project
+
+ @repo = user_project.repository
+
+ begin
+ @blob = Gitlab::Git::Blob.raw(@repo, params[:sha])
+ @blob.load_all_data!(@repo)
+ rescue
+ not_found! 'Blob'
+ end
+
+ not_found! 'Blob' unless @blob
+ end
end
desc 'Get a project repository tree' do
success Entities::RepoTreeObject
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 :ref, 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'
+ use :pagination
end
get ':id/repository/tree' do
- ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
+ ref = params[:ref] || 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: Entities::RepoTreeObject
+ entries = ::Kaminari.paginate_array(tree.sorted_entries)
+ present paginate(entries), with: Entities::RepoTreeObject
end
- desc 'Get a raw file contents'
+ desc 'Get raw blob contents from the repository'
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" ] do
- repo = user_project.repository
-
- commit = repo.commit(params[:sha])
- not_found! "Commit" unless commit
+ get ':id/repository/blobs/:sha/raw' do
+ assign_blob_vars!
- blob = Gitlab::Git::Blob.find(repo, commit.id, params[:filepath])
- not_found! "File" unless blob
-
- send_git_blob repo, blob
+ send_git_blob @repo, @blob
end
- desc 'Get a raw blob contents by blob sha'
+ desc 'Get a blob from the repository'
params do
requires :sha, type: String, desc: 'The commit, branch name, or tag name'
end
- get ':id/repository/raw_blobs/:sha' 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
+ get ':id/repository/blobs/:sha' do
+ assign_blob_vars!
+
+ {
+ size: @blob.size,
+ encoding: "base64",
+ content: Base64.strict_encode64(@blob.data),
+ sha: @blob.id
+ }
end
desc 'Get an archive of the repository'
@@ -100,10 +108,13 @@ module API
desc 'Get repository contributors' do
success Entities::Contributor
end
+ params do
+ use :pagination
+ end
get ':id/repository/contributors' do
begin
- present user_project.repository.contributors,
- with: Entities::Contributor
+ contributors = ::Kaminari.paginate_array(user_project.repository.contributors)
+ present paginate(contributors), with: Entities::Contributor
rescue
not_found!
end
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
new file mode 100644
index 00000000000..4c9db2c8716
--- /dev/null
+++ b/lib/api/runner.rb
@@ -0,0 +1,264 @@
+module API
+ class Runner < Grape::API
+ helpers ::API::Helpers::Runner
+
+ resource :runners do
+ desc 'Registers a new Runner' do
+ success Entities::RunnerRegistrationDetails
+ http_codes [[201, 'Runner was created'], [403, 'Forbidden']]
+ end
+ params do
+ requires :token, type: String, desc: 'Registration token'
+ optional :description, type: String, desc: %q(Runner's description)
+ optional :info, type: Hash, desc: %q(Runner's metadata)
+ optional :locked, type: Boolean, desc: 'Should Runner be locked for current project'
+ optional :run_untagged, type: Boolean, desc: 'Should Runner handle untagged jobs'
+ optional :tag_list, type: Array[String], desc: %q(List of Runner's tags)
+ end
+ post '/' do
+ attributes = attributes_for_keys [:description, :locked, :run_untagged, :tag_list]
+
+ runner =
+ if runner_registration_token_valid?
+ # Create shared runner. Requires admin access
+ Ci::Runner.create(attributes.merge(is_shared: true))
+ elsif project = Project.find_by(runners_token: params[:token])
+ # Create a specific runner for project.
+ project.runners.create(attributes)
+ end
+
+ return forbidden! unless runner
+
+ if runner.id
+ runner.update(get_runner_version_from_params)
+ present runner, with: Entities::RunnerRegistrationDetails
+ else
+ not_found!
+ end
+ end
+
+ desc 'Deletes a registered Runner' do
+ http_codes [[204, 'Runner was deleted'], [403, 'Forbidden']]
+ end
+ params do
+ requires :token, type: String, desc: %q(Runner's authentication token)
+ end
+ delete '/' do
+ authenticate_runner!
+ Ci::Runner.find_by_token(params[:token]).destroy
+ end
+
+ desc 'Validates authentication credentials' do
+ http_codes [[200, 'Credentials are valid'], [403, 'Forbidden']]
+ end
+ params do
+ requires :token, type: String, desc: %q(Runner's authentication token)
+ end
+ post '/verify' do
+ authenticate_runner!
+ status 200
+ end
+ end
+
+ resource :jobs do
+ desc 'Request a job' do
+ success Entities::JobRequest::Response
+ http_codes [[201, 'Job was scheduled'],
+ [204, 'No job for Runner'],
+ [403, 'Forbidden']]
+ end
+ params do
+ requires :token, type: String, desc: %q(Runner's authentication token)
+ optional :last_update, type: String, desc: %q(Runner's queue last_update token)
+ optional :info, type: Hash, desc: %q(Runner's metadata)
+ end
+ post '/request' do
+ authenticate_runner!
+ no_content! unless current_runner.active?
+ update_runner_info
+
+ if current_runner.is_runner_queue_value_latest?(params[:last_update])
+ header 'X-GitLab-Last-Update', params[:last_update]
+ Gitlab::Metrics.add_event(:build_not_found_cached)
+ return no_content!
+ end
+
+ new_update = current_runner.ensure_runner_queue_value
+ result = ::Ci::RegisterJobService.new(current_runner).execute
+
+ if result.valid?
+ if result.build
+ Gitlab::Metrics.add_event(:build_found,
+ project: result.build.project.path_with_namespace)
+ present result.build, with: Entities::JobRequest::Response
+ else
+ Gitlab::Metrics.add_event(:build_not_found)
+ header 'X-GitLab-Last-Update', new_update
+ no_content!
+ end
+ else
+ # We received build that is invalid due to concurrency conflict
+ Gitlab::Metrics.add_event(:build_invalid)
+ conflict!
+ end
+ end
+
+ desc 'Updates a job' do
+ http_codes [[200, 'Job was updated'], [403, 'Forbidden']]
+ end
+ params do
+ requires :token, type: String, desc: %q(Runners's authentication token)
+ requires :id, type: Integer, desc: %q(Job's ID)
+ optional :trace, type: String, desc: %q(Job's full trace)
+ optional :state, type: String, desc: %q(Job's status: success, failed)
+ end
+ put '/:id' do
+ job = Ci::Build.find_by_id(params[:id])
+ authenticate_job!(job)
+
+ job.update_attributes(trace: params[:trace]) if params[:trace]
+
+ Gitlab::Metrics.add_event(:update_build,
+ project: job.project.path_with_namespace)
+
+ case params[:state].to_s
+ when 'success'
+ job.success
+ when 'failed'
+ job.drop
+ end
+ end
+
+ desc 'Appends a patch to the job trace' do
+ http_codes [[202, 'Trace was patched'],
+ [400, 'Missing Content-Range header'],
+ [403, 'Forbidden'],
+ [416, 'Range not satisfiable']]
+ end
+ params do
+ requires :id, type: Integer, desc: %q(Job's ID)
+ optional :token, type: String, desc: %q(Job's authentication token)
+ end
+ patch '/:id/trace' do
+ job = Ci::Build.find_by_id(params[:id])
+ authenticate_job!(job)
+
+ error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range')
+ content_range = request.headers['Content-Range']
+ content_range = content_range.split('-')
+
+ current_length = job.trace_length
+ unless current_length == content_range[0].to_i
+ return error!('416 Range Not Satisfiable', 416, { 'Range' => "0-#{current_length}" })
+ end
+
+ job.append_trace(request.body.read, content_range[0].to_i)
+
+ status 202
+ header 'Job-Status', job.status
+ header 'Range', "0-#{job.trace_length}"
+ end
+
+ desc 'Authorize artifacts uploading for job' do
+ http_codes [[200, 'Upload allowed'],
+ [403, 'Forbidden'],
+ [405, 'Artifacts support not enabled'],
+ [413, 'File too large']]
+ end
+ params do
+ requires :id, type: Integer, desc: %q(Job's ID)
+ optional :token, type: String, desc: %q(Job's authentication token)
+ optional :filesize, type: Integer, desc: %q(Artifacts filesize)
+ end
+ post '/:id/artifacts/authorize' do
+ not_allowed! unless Gitlab.config.artifacts.enabled
+ require_gitlab_workhorse!
+ Gitlab::Workhorse.verify_api_request!(headers)
+
+ job = Ci::Build.find_by_id(params[:id])
+ authenticate_job!(job)
+ forbidden!('Job is not running') unless job.running?
+
+ if params[:filesize]
+ file_size = params[:filesize].to_i
+ file_to_large! unless file_size < max_artifacts_size
+ end
+
+ status 200
+ content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
+ Gitlab::Workhorse.artifact_upload_ok
+ end
+
+ desc 'Upload artifacts for job' do
+ success Entities::JobRequest::Response
+ http_codes [[201, 'Artifact uploaded'],
+ [400, 'Bad request'],
+ [403, 'Forbidden'],
+ [405, 'Artifacts support not enabled'],
+ [413, 'File too large']]
+ end
+ params do
+ requires :id, type: Integer, desc: %q(Job's ID)
+ optional :token, type: String, desc: %q(Job's authentication token)
+ optional :expire_in, type: String, desc: %q(Specify when artifacts should expire)
+ optional :file, type: File, desc: %q(Artifact's file)
+ optional 'file.path', type: String, desc: %q(path to locally stored body (generated by Workhorse))
+ optional 'file.name', type: String, desc: %q(real filename as send in Content-Disposition (generated by Workhorse))
+ optional 'file.type', type: String, desc: %q(real content type as send in Content-Type (generated by Workhorse))
+ optional 'metadata.path', type: String, desc: %q(path to locally stored body (generated by Workhorse))
+ optional 'metadata.name', type: String, desc: %q(filename (generated by Workhorse))
+ end
+ post '/:id/artifacts' do
+ not_allowed! unless Gitlab.config.artifacts.enabled
+ require_gitlab_workhorse!
+
+ job = Ci::Build.find_by_id(params[:id])
+ authenticate_job!(job)
+ forbidden!('Job is not running!') unless job.running?
+
+ artifacts_upload_path = ArtifactUploader.artifacts_upload_path
+ artifacts = uploaded_file(:file, artifacts_upload_path)
+ metadata = uploaded_file(:metadata, artifacts_upload_path)
+
+ bad_request!('Missing artifacts file!') unless artifacts
+ file_to_large! unless artifacts.size < max_artifacts_size
+
+ job.artifacts_file = artifacts
+ job.artifacts_metadata = metadata
+ job.artifacts_expire_in = params['expire_in'] ||
+ Gitlab::CurrentSettings.current_application_settings.default_artifacts_expire_in
+
+ if job.save
+ present job, with: Entities::JobRequest::Response
+ else
+ render_validation_error!(job)
+ end
+ end
+
+ desc 'Download the artifacts file for job' do
+ http_codes [[200, 'Upload allowed'],
+ [403, 'Forbidden'],
+ [404, 'Artifact not found']]
+ end
+ params do
+ requires :id, type: Integer, desc: %q(Job's ID)
+ optional :token, type: String, desc: %q(Job's authentication token)
+ end
+ get '/:id/artifacts' do
+ job = Ci::Build.find_by_id(params[:id])
+ authenticate_job!(job)
+
+ artifacts_file = job.artifacts_file
+ unless artifacts_file.file_storage?
+ return redirect_to job.artifacts_file.url
+ end
+
+ unless artifacts_file.exists?
+ not_found!
+ end
+
+ present_file!(artifacts_file.path, artifacts_file.filename)
+ end
+ end
+ end
+end
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
index 4816b5ed1b7..a77c876a749 100644
--- a/lib/api/runners.rb
+++ b/lib/api/runners.rb
@@ -14,7 +14,7 @@ module API
use :pagination
end
get do
- runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: ['specific', 'shared'])
+ runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: %w(specific shared))
present paginate(runners), with: Entities::Runner
end
@@ -60,8 +60,9 @@ module API
put ':id' do
runner = get_runner(params.delete(:id))
authenticate_update_runner!(runner)
+ update_service = Ci::UpdateRunnerService.new(runner)
- if runner.update(declared_params(include_missing: false))
+ if update_service.update(declared_params(include_missing: false))
present runner, with: Entities::RunnerDetails, current_user: current_user
else
render_validation_error!(runner)
@@ -77,16 +78,15 @@ module API
delete ':id' do
runner = get_runner(params[:id])
authenticate_delete_runner!(runner)
- runner.destroy!
- present runner, with: Entities::Runner
+ runner.destroy!
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
before { authorize_admin_project }
desc 'Get runners available for project' do
@@ -135,8 +135,6 @@ module API
forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.projects.count == 1
runner_project.destroy
-
- present runner, with: Entities::Runner
end
end
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 3a9dfbb237c..4e0c9cb1f63 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -107,26 +107,6 @@ module API
desc: 'Enable SSL verification for communication'
}
],
- 'builds-email' => [
- {
- required: true,
- name: :recipients,
- type: String,
- desc: 'Comma-separated list of recipient email addresses'
- },
- {
- required: false,
- name: :add_pusher,
- type: Boolean,
- desc: 'Add pusher to recipients list'
- },
- {
- required: false,
- name: :notify_only_broken_builds,
- type: Boolean,
- desc: 'Notify only broken builds'
- }
- ],
'campfire' => [
{
required: true,
@@ -145,7 +125,7 @@ module API
name: :room,
type: String,
desc: 'Campfire room'
- },
+ }
],
'custom-issue-tracker' => [
{
@@ -403,9 +383,9 @@ module API
},
{
required: false,
- name: :notify_only_broken_builds,
+ name: :notify_only_broken_pipelines,
type: Boolean,
- desc: 'Notify only broken builds'
+ desc: 'Notify only broken pipelines'
}
],
'pivotaltracker' => [
@@ -422,6 +402,14 @@ module API
desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.'
}
],
+ 'prometheus' => [
+ {
+ required: true,
+ name: :api_url,
+ type: String,
+ desc: 'Prometheus API Base URL, like http://prometheus.example.com/'
+ }
+ ],
'pushover' => [
{
required: true,
@@ -534,7 +522,49 @@ module API
desc: 'The password of the user'
}
]
- }.freeze
+ }
+
+ service_classes = [
+ AsanaService,
+ AssemblaService,
+ BambooService,
+ BugzillaService,
+ BuildkiteService,
+ CampfireService,
+ CustomIssueTrackerService,
+ DroneCiService,
+ EmailsOnPushService,
+ ExternalWikiService,
+ FlowdockService,
+ GemnasiumService,
+ HipchatService,
+ IrkerService,
+ JiraService,
+ KubernetesService,
+ MattermostSlashCommandsService,
+ SlackSlashCommandsService,
+ PipelinesEmailService,
+ PivotaltrackerService,
+ PrometheusService,
+ PushoverService,
+ RedmineService,
+ SlackService,
+ MattermostService,
+ TeamcityService,
+ ]
+
+ if Rails.env.development?
+ services['mock-ci'] = [
+ {
+ required: true,
+ name: :mock_service_url,
+ type: String,
+ desc: 'URL to the mock service'
+ }
+ ]
+
+ service_classes << MockCiService
+ end
trigger_services = {
'mattermost-slash-commands' => [
@@ -553,7 +583,10 @@ module API
]
}.freeze
- resource :projects do
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
before { authenticate! }
before { authorize_admin_project }
@@ -568,6 +601,19 @@ module API
services.each do |service_slug, settings|
desc "Set #{service_slug} service for project"
params do
+ service_classes.each do |service|
+ event_names = service.try(:event_names) || next
+ event_names.each do |event_name|
+ services[service.to_param.tr("_", "-")] << {
+ required: false,
+ name: event_name.to_sym,
+ type: String,
+ desc: ServicesHelper.service_event_description(event_name)
+ }
+ end
+ end
+ services.freeze
+
settings.each do |setting|
if setting[:required]
requires setting[:name], type: setting[:type], desc: setting[:desc]
@@ -581,7 +627,7 @@ module API
service_params = declared_params(include_missing: false).merge(active: true)
if service.update_attributes(service_params)
- true
+ present service, with: Entities::ProjectService, include_passwords: current_user.is_admin?
else
render_api_error!('400 Bad Request', 400)
end
@@ -599,9 +645,7 @@ module API
hash.merge!(key => nil)
end
- if service.update_attributes(attrs.merge(active: false))
- true
- else
+ unless service.update_attributes(attrs.merge(active: false))
render_api_error!('400 Bad Request', 400)
end
end
@@ -619,10 +663,18 @@ module API
end
trigger_services.each do |service_slug, settings|
+ helpers do
+ def chat_command_service(project, service_slug, params)
+ project.services.active.where(template: false).find do |service|
+ service.try(:token) == params[:token] && service.to_param == service_slug.underscore
+ end
+ end
+ end
+
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc "Trigger a slash command for #{service_slug}" do
detail 'Added in GitLab 8.13'
end
@@ -637,9 +689,8 @@ module API
# This is not accurate, but done to prevent leakage of the project names
not_found!('Service') unless project
- service = project.find_or_initialize_service(service_slug.underscore)
-
- result = service.try(:active?) && service.try(:trigger, params)
+ service = chat_command_service(project, service_slug, params)
+ result = service.try(:trigger, params)
if result
status result[:status] || 200
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index c5eff16a5de..c7f97ad2aab 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -21,9 +21,9 @@ module API
end
params do
optional :default_branch_protection, type: Integer, values: [0, 1, 2], desc: 'Determine if developers can push to master'
- optional :default_project_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default project visibility'
- optional :default_snippet_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default snippet visibility'
- optional :default_group_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default group visibility'
+ optional :default_project_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default project visibility'
+ optional :default_snippet_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default snippet visibility'
+ optional :default_group_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default group visibility'
optional :restricted_visibility_levels, type: Array[String], desc: 'Selected levels cannot be used by non-admin users for projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.'
optional :import_sources, type: Array[String], values: %w[github bitbucket gitlab google_code fogbugz git gitlab_project],
desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com'
@@ -56,7 +56,9 @@ module API
given shared_runners_enabled: ->(val) { val } do
requires :shared_runners_text, type: String, desc: 'Shared runners text '
end
- optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size each build's artifacts can have"
+ optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size for each job's artifacts"
+ optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts"
+ optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB'
optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)'
optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics'
given metrics_enabled: ->(val) { val } do
@@ -107,6 +109,8 @@ module API
requires :housekeeping_full_repack_period, type: Integer, desc: "Number of Git pushes after which a full 'git repack' is run."
requires :housekeeping_gc_period, type: Integer, desc: "Number of Git pushes after which 'git gc' is run."
end
+ optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.'
+ optional :polling_interval_multiplier, type: BigDecimal, desc: 'Interval multiplier used by endpoints that perform polling. Set to 0 to disable polling.'
at_least_one_of :default_branch_protection, :default_project_visibility, :default_snippet_visibility,
:default_group_visibility, :restricted_visibility_levels, :import_sources,
:enabled_git_access_protocol, :gravatar_enabled, :default_projects_limit,
@@ -115,15 +119,19 @@ module API
:send_user_confirmation_email, :domain_whitelist, :domain_blacklist_enabled,
:after_sign_up_text, :signin_enabled, :require_two_factor_authentication,
:home_page_url, :after_sign_out_path, :sign_in_text, :help_page_text,
- :shared_runners_enabled, :max_artifacts_size, :container_registry_token_expire_delay,
+ :shared_runners_enabled, :max_artifacts_size,
+ :default_artifacts_expire_in, :max_pages_size,
+ :container_registry_token_expire_delay,
:metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled,
:akismet_enabled, :admin_notification_email, :sentry_enabled,
:repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled,
:version_check_enabled, :email_author_in_body, :html_emails_enabled,
- :housekeeping_enabled
+ :housekeeping_enabled, :terminal_max_session_time, :polling_interval_multiplier
end
put "application/settings" do
- if current_settings.update_attributes(declared_params(include_missing: false))
+ attrs = declared_params(include_missing: false)
+
+ if current_settings.update_attributes(attrs)
present current_settings, with: Entities::ApplicationSetting
else
render_validation_error!(current_settings)
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
index e096e636806..b93fdc62808 100644
--- a/lib/api/snippets.rb
+++ b/lib/api/snippets.rb
@@ -58,15 +58,17 @@ module API
requires :title, type: String, desc: 'The title of a snippet'
requires :file_name, type: String, desc: 'The name of a snippet file'
requires :content, type: String, desc: 'The content of a snippet'
- optional :visibility_level, type: Integer,
- values: Gitlab::VisibilityLevel.values,
- default: Gitlab::VisibilityLevel::INTERNAL,
- desc: 'The visibility level of the snippet'
+ optional :visibility, type: String,
+ values: Gitlab::VisibilityLevel.string_values,
+ default: 'internal',
+ desc: 'The visibility of the snippet'
end
post do
- attrs = declared_params(include_missing: false)
+ attrs = declared_params(include_missing: false).merge(request: request, api: true)
snippet = CreateSnippetService.new(nil, current_user, attrs).execute
+ render_spam_error! if snippet.spam?
+
if snippet.persisted?
present snippet, with: Entities::PersonalSnippet
else
@@ -83,19 +85,22 @@ module API
optional :title, type: String, desc: 'The title of a snippet'
optional :file_name, type: String, desc: 'The name of a snippet file'
optional :content, type: String, desc: 'The content of a snippet'
- optional :visibility_level, type: Integer,
- values: Gitlab::VisibilityLevel.values,
- desc: 'The visibility level of the snippet'
- at_least_one_of :title, :file_name, :content, :visibility_level
+ optional :visibility, type: String,
+ values: Gitlab::VisibilityLevel.string_values,
+ desc: 'The visibility of the snippet'
+ at_least_one_of :title, :file_name, :content, :visibility
end
put ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
return not_found!('Snippet') unless snippet
authorize! :update_personal_snippet, snippet
- attrs = declared_params(include_missing: false)
+ attrs = declared_params(include_missing: false).merge(request: request, api: true)
UpdateSnippetService.new(nil, current_user, snippet, attrs).execute
+
+ render_spam_error! if snippet.spam?
+
if snippet.persisted?
present snippet, with: Entities::PersonalSnippet
else
@@ -113,9 +118,10 @@ module API
delete ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
return not_found!('Snippet') unless snippet
+
authorize! :destroy_personal_snippet, snippet
+
snippet.destroy
- no_content!
end
desc 'Get a raw snippet' do
diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb
index 10749b34004..dbe54d3cd31 100644
--- a/lib/api/subscriptions.rb
+++ b/lib/api/subscriptions.rb
@@ -3,8 +3,7 @@ module API
before { authenticate! }
subscribable_types = {
- 'merge_request' => proc { |id| user_project.merge_requests.find(id) },
- 'merge_requests' => proc { |id| user_project.merge_requests.find(id) },
+ '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) },
}
@@ -13,7 +12,7 @@ module API
requires :id, type: String, desc: 'The ID of a project'
requires :subscribable_id, type: String, desc: 'The ID of a resource'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
subscribable_types.each do |type, finder|
type_singularized = type.singularize
entity_class = Entities.const_get(type_singularized.camelcase)
@@ -21,7 +20,7 @@ module API
desc 'Subscribe to a resource' do
success entity_class
end
- post ":id/#{type}/:subscribable_id/subscription" do
+ post ":id/#{type}/:subscribable_id/subscribe" do
resource = instance_exec(params[:subscribable_id], &finder)
if resource.subscribed?(current_user, user_project)
@@ -35,7 +34,7 @@ module API
desc 'Unsubscribe from a resource' do
success entity_class
end
- delete ":id/#{type}/:subscribable_id/subscription" do
+ post ":id/#{type}/:subscribable_id/unsubscribe" do
resource = instance_exec(params[:subscribable_id], &finder)
if !resource.subscribed?(current_user, user_project)
diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb
index 708ec8cfe70..ed7b23b474a 100644
--- a/lib/api/system_hooks.rb
+++ b/lib/api/system_hooks.rb
@@ -1,6 +1,7 @@
module API
- # Hooks API
class SystemHooks < Grape::API
+ include PaginationParams
+
before do
authenticate!
authenticated_as_admin!
@@ -10,10 +11,11 @@ module API
desc 'Get the list of system hooks' do
success Entities::Hook
end
+ params do
+ use :pagination
+ end
get do
- hooks = SystemHook.all
-
- present hooks, with: Entities::Hook
+ present paginate(SystemHook.all), with: Entities::Hook
end
desc 'Create a new system hook' do
@@ -64,7 +66,7 @@ module API
hook = SystemHook.find_by(id: params[:id])
not_found!('System hook') unless hook
- present hook.destroy, with: Entities::Hook
+ hook.destroy
end
end
end
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index 5b345db3a41..c7b1efe0bfa 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -1,18 +1,22 @@
module API
- # Git Tags API
class Tags < Grape::API
+ include PaginationParams
+
before { authorize! :download_code, user_project }
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository tags' do
success Entities::RepoTag
end
+ params do
+ use :pagination
+ end
get ":id/repository/tags" do
- present user_project.repository.tags.sort_by(&:name).reverse,
- with: Entities::RepoTag, project: user_project
+ tags = ::Kaminari.paginate_array(user_project.repository.tags.sort_by(&:name).reverse)
+ present paginate(tags), with: Entities::RepoTag, project: user_project
end
desc 'Get a single repository tag' do
@@ -40,7 +44,7 @@ module API
post ':id/repository/tags' do
authorize_push_project
- result = CreateTagService.new(user_project, current_user).
+ result = ::Tags::CreateService.new(user_project, current_user).
execute(params[:tag_name], params[:ref], params[:message], params[:release_description])
if result[:status] == :success
@@ -59,14 +63,10 @@ module API
delete ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do
authorize_push_project
- result = DeleteTagService.new(user_project, current_user).
+ result = ::Tags::DestroyService.new(user_project, current_user).
execute(params[:tag_name])
- if result[:status] == :success
- {
- tag_name: params[:tag_name]
- }
- else
+ if result[:status] != :success
render_api_error!(result[:message], result[:return_code])
end
end
diff --git a/lib/api/templates.rb b/lib/api/templates.rb
index e23f99256a5..0fc13b35d5b 100644
--- a/lib/api/templates.rb
+++ b/lib/api/templates.rb
@@ -1,5 +1,7 @@
module API
class Templates < Grape::API
+ include PaginationParams
+
GLOBAL_TEMPLATE_TYPES = {
gitignores: {
klass: Gitlab::Template::GitignoreTemplate,
@@ -24,7 +26,6 @@ module API
/[\<\{\[]
(fullname|name\sof\s(author|copyright\sowner))
[\>\}\]]/xi.freeze
- DEPRECATION_MESSAGE = ' This endpoint is deprecated and will be removed in GitLab 9.0.'.freeze
helpers do
def parsed_license_template
@@ -46,74 +47,64 @@ module API
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 Entities::RepoLicense
- 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: Entities::RepoLicense
- end
+ desc 'Get the list of the available license template' do
+ detail 'This feature was introduced in GitLab 8.7.'
+ success ::API::Entities::RepoLicense
+ end
+ params do
+ optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses'
+ use :pagination
+ end
+ get "templates/licenses" do
+ options = {
+ featured: declared(params).popular.present? ? true : nil
+ }
+ licences = ::Kaminari.paginate_array(Licensee::License.all(options))
+ present paginate(licences), with: Entities::RepoLicense
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 Entities::RepoLicense
- 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)
+ desc 'Get the text for a specific license' do
+ detail 'This feature was introduced in GitLab 8.7.'
+ success ::API::Entities::RepoLicense
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the template'
+ end
+ get "templates/licenses/:name", requirements: { name: /[\w\.-]+/ } do
+ not_found!('License') unless Licensee::License.find(declared(params).name)
- template = parsed_license_template
+ template = parsed_license_template
- present template, with: Entities::RepoLicense
- end
+ present template, with: ::API::Entities::RepoLicense
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 Entities::TemplatesList
- end
- get route do
- present klass.all, with: Entities::TemplatesList
- end
+ desc 'Get the list of the available template' do
+ detail "This feature was introduced in GitLab #{gitlab_version}."
+ success Entities::TemplatesList
+ end
+ params do
+ use :pagination
+ end
+ get "templates/#{template_type}" do
+ templates = ::Kaminari.paginate_array(klass.all)
+ present paginate(templates), with: Entities::TemplatesList
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 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)
+ desc 'Get the text for a specific template present in local filesystem' do
+ detail "This feature was introduced in GitLab #{gitlab_version}."
+ success Entities::Template
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the template'
+ end
+ get "templates/#{template_type}/:name" do
+ new_template = klass.find(declared(params).name)
- render_response(template_type, new_template)
- end
+ render_response(template_type, new_template)
end
end
end
diff --git a/lib/api/time_tracking_endpoints.rb b/lib/api/time_tracking_endpoints.rb
index 85b5f7d98b8..05b4b490e27 100644
--- a/lib/api/time_tracking_endpoints.rb
+++ b/lib/api/time_tracking_endpoints.rb
@@ -5,11 +5,11 @@ module API
included do
helpers do
def issuable_name
- declared_params.has_key?(:issue_id) ? 'issue' : 'merge_request'
+ declared_params.has_key?(:issue_iid) ? 'issue' : 'merge_request'
end
def issuable_key
- "#{issuable_name}_id".to_sym
+ "#{issuable_name}_iid".to_sym
end
def update_issuable_key
@@ -50,7 +50,7 @@ module API
issuable_name = name.end_with?('Issues') ? 'issue' : 'merge_request'
issuable_collection_name = issuable_name.pluralize
- issuable_key = "#{issuable_name}_id".to_sym
+ issuable_key = "#{issuable_name}_iid".to_sym
desc "Set a time estimate for a project #{issuable_name}"
params do
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index ed8f48aa1e3..d1f7e364029 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -5,22 +5,22 @@ module API
before { authenticate! }
ISSUABLE_TYPES = {
- 'merge_requests' => ->(id) { user_project.merge_requests.find(id) },
- 'issues' => ->(id) { find_project_issue(id) }
- }
+ 'merge_requests' => ->(iid) { find_merge_request_with_access(iid) },
+ 'issues' => ->(iid) { find_project_issue(iid) }
+ }.freeze
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
ISSUABLE_TYPES.each do |type, finder|
- type_id_str = "#{type.singularize}_id".to_sym
+ type_id_str = "#{type.singularize}_iid".to_sym
desc 'Create a todo on an issuable' do
success Entities::Todo
end
params do
- requires type_id_str, type: Integer, desc: 'The ID of an issuable'
+ requires type_id_str, type: Integer, desc: 'The IID of an issuable'
end
post ":id/#{type}/:#{type_id_str}/todo" do
issuable = instance_exec(params[type_id_str], &finder)
@@ -58,7 +58,7 @@ module API
params do
requires :id, type: Integer, desc: 'The ID of the todo being marked as done'
end
- delete ':id' do
+ post ':id/mark_as_done' do
todo = current_user.todos.find(params[:id])
TodoService.new.mark_todos_as_done([todo], current_user)
@@ -66,9 +66,11 @@ module API
end
desc 'Mark all todos as done'
- delete do
+ post '/mark_as_done' do
todos = find_todos
TodoService.new.mark_todos_as_done(todos, current_user)
+
+ no_content!
end
end
end
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index 87a717ba751..a9f2ca2608e 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -5,38 +5,33 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
- desc 'Trigger a GitLab project build' do
- success Entities::TriggerRequest
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ desc 'Trigger a GitLab project pipeline' do
+ success Entities::Pipeline
end
params do
requires :ref, type: String, desc: 'The commit sha or name of a branch or tag'
requires :token, type: String, desc: 'The unique token of trigger'
optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
end
- post ":id/(ref/:ref/)trigger/builds" do
+ post ":id/(ref/:ref/)trigger/pipeline", requirements: { ref: /.+/ } do
project = find_project(params[:id])
trigger = Ci::Trigger.find_by_token(params[:token].to_s)
not_found! unless project && trigger
unauthorized! unless trigger.project == project
# validate variables
- variables = params[:variables]
- if variables
- unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
- render_api_error!('variables needs to be a map of key-valued strings', 400)
- end
-
- # convert variables from Mash to Hash
- variables = variables.to_h
+ variables = params[:variables].to_h
+ unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
+ render_api_error!('variables needs to be a map of key-valued strings', 400)
end
# create request and trigger builds
trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables)
if trigger_request
- present trigger_request, with: Entities::TriggerRequest
+ present trigger_request.pipeline, with: Entities::Pipeline
else
- errors = 'No builds created'
+ errors = 'No pipeline created'
render_api_error!(errors, 400)
end
end
@@ -60,13 +55,13 @@ module API
success Entities::Trigger
end
params do
- requires :token, type: String, desc: 'The unique token of trigger'
+ requires :trigger_id, type: Integer, desc: 'The trigger ID'
end
- get ':id/triggers/:token' do
+ get ':id/triggers/:trigger_id' do
authenticate!
authorize! :admin_build, user_project
- trigger = user_project.triggers.find_by(token: params[:token].to_s)
+ trigger = user_project.triggers.find(params.delete(:trigger_id))
return not_found!('Trigger') unless trigger
present trigger, with: Entities::Trigger
@@ -75,31 +70,79 @@ module API
desc 'Create a trigger' do
success Entities::Trigger
end
+ params do
+ requires :description, type: String, desc: 'The trigger description'
+ end
post ':id/triggers' do
authenticate!
authorize! :admin_build, user_project
- trigger = user_project.triggers.create
+ trigger = user_project.triggers.create(
+ declared_params(include_missing: false).merge(owner: current_user))
- present trigger, with: Entities::Trigger
+ if trigger.valid?
+ present trigger, with: Entities::Trigger
+ else
+ render_validation_error!(trigger)
+ end
+ end
+
+ desc 'Update a trigger' do
+ success Entities::Trigger
+ end
+ params do
+ requires :trigger_id, type: Integer, desc: 'The trigger ID'
+ optional :description, type: String, desc: 'The trigger description'
+ end
+ put ':id/triggers/:trigger_id' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ trigger = user_project.triggers.find(params.delete(:trigger_id))
+ return not_found!('Trigger') unless trigger
+
+ if trigger.update(declared_params(include_missing: false))
+ present trigger, with: Entities::Trigger
+ else
+ render_validation_error!(trigger)
+ end
+ end
+
+ desc 'Take ownership of trigger' do
+ success Entities::Trigger
+ end
+ params do
+ requires :trigger_id, type: Integer, desc: 'The trigger ID'
+ end
+ post ':id/triggers/:trigger_id/take_ownership' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ trigger = user_project.triggers.find(params.delete(:trigger_id))
+ return not_found!('Trigger') unless trigger
+
+ if trigger.update(owner: current_user)
+ status :ok
+ present trigger, with: Entities::Trigger
+ else
+ render_validation_error!(trigger)
+ end
end
desc 'Delete a trigger' do
success Entities::Trigger
end
params do
- requires :token, type: String, desc: 'The unique token of trigger'
+ requires :trigger_id, type: Integer, desc: 'The trigger ID'
end
- delete ':id/triggers/:token' do
+ delete ':id/triggers/:trigger_id' do
authenticate!
authorize! :admin_build, user_project
- trigger = user_project.triggers.find_by(token: params[:token].to_s)
+ trigger = user_project.triggers.find(params.delete(:trigger_id))
return not_found!('Trigger') unless trigger
trigger.destroy
-
- present trigger, with: Entities::Trigger
end
end
end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 11a7368b4c0..530ca0b5235 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -9,6 +9,11 @@ module API
resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
helpers do
+ def find_user(params)
+ id = params[:user_id] || params[:id]
+ User.find_by(id: id) || not_found!('User')
+ end
+
params :optional_attributes do
optional :skype, type: String, desc: 'The Skype username'
optional :linkedin, type: String, desc: 'The LinkedIn username'
@@ -22,7 +27,7 @@ module API
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, desc: 'Flag indicating the account needs to be confirmed'
+ optional :skip_confirmation, type: Boolean, default: false, desc: 'Flag indicating the account is confirmed'
optional :external, type: Boolean, desc: 'Flag indicating the user is an external user'
all_or_none_of :extern_uid, :provider
end
@@ -40,7 +45,7 @@ module API
use :pagination
end
get do
- unless can?(current_user, :read_users_list, nil)
+ unless can?(current_user, :read_users_list)
render_api_error!("Not authorized.", 403)
end
@@ -82,7 +87,9 @@ module API
end
params do
requires :email, type: String, desc: 'The email of the user'
- requires :password, type: String, desc: 'The password of the new 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
@@ -90,19 +97,10 @@ module API
post do
authenticated_as_admin!
- # Filter out params which are used later
- user_params = declared_params(include_missing: false)
- identity_attrs = user_params.slice(:provider, :extern_uid)
- confirm = user_params.delete(:confirm)
-
- user = User.new(user_params.except(:extern_uid, :provider))
- user.skip_confirmation! unless confirm
+ params = declared_params(include_missing: false)
+ user = ::Users::CreateService.new(current_user, params).execute
- if identity_attrs.any?
- user.identities.build(identity_attrs)
- end
-
- if user.save
+ if user.persisted?
present user, with: Entities::UserPublic
else
conflict!('Email has already been taken') if User.
@@ -160,6 +158,8 @@ module API
end
end
+ user_params[:password_expires_at] = Time.now if user_params[:password].present?
+
if user.update_attributes(user_params.except(:extern_uid, :provider))
present user, with: Entities::UserPublic
else
@@ -195,6 +195,7 @@ module API
end
params do
requires :id, type: Integer, desc: 'The ID of the user'
+ use :pagination
end
get ':id/keys' do
authenticated_as_admin!
@@ -202,7 +203,7 @@ module API
user = User.find_by(id: params[:id])
not_found!('User') unless user
- present user.keys, with: Entities::SSHKey
+ present paginate(user.keys), with: Entities::SSHKey
end
desc 'Delete an existing SSH key from a specified user. Available only for admins.' do
@@ -221,7 +222,7 @@ module API
key = user.keys.find_by(id: params[:key_id])
not_found!('Key') unless key
- present key.destroy, with: Entities::SSHKey
+ key.destroy
end
desc 'Add an email address to a specified user. Available only for admins.' do
@@ -252,13 +253,14 @@ module API
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: Entities::Email
+ present paginate(user.emails), with: Entities::Email
end
desc 'Delete an email address of a specified user. Available only for admins.' do
@@ -291,14 +293,14 @@ module API
user = User.find_by(id: params[:id])
not_found!('User') unless user
- DeleteUserService.new(current_user).execute(user)
+ DeleteUserWorker.perform_async(current_user.id, user.id)
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
+ post ':id/block' do
authenticated_as_admin!
user = User.find_by(id: params[:id])
not_found!('User') unless user
@@ -314,7 +316,7 @@ module API
params do
requires :id, type: Integer, desc: 'The ID of the user'
end
- put ':id/unblock' do
+ post ':id/unblock' do
authenticated_as_admin!
user = User.find_by(id: params[:id])
not_found!('User') unless user
@@ -346,6 +348,76 @@ module API
present paginate(events), with: Entities::Event
end
+
+ params do
+ requires :user_id, type: Integer, desc: 'The ID of the user'
+ end
+ segment ':user_id' do
+ resource :impersonation_tokens do
+ helpers do
+ def finder(options = {})
+ user = find_user(params)
+ PersonalAccessTokensFinder.new({ user: user, impersonation: true }.merge(options))
+ end
+
+ def find_impersonation_token
+ finder.find_by(id: declared_params[:impersonation_token_id]) || not_found!('Impersonation Token')
+ end
+ end
+
+ before { authenticated_as_admin! }
+
+ desc 'Retrieve impersonation tokens. Available only for admins.' do
+ detail 'This feature was introduced in GitLab 9.0'
+ success Entities::ImpersonationToken
+ end
+ params do
+ use :pagination
+ optional :state, type: String, default: 'all', values: %w[all active inactive], desc: 'Filters (all|active|inactive) impersonation_tokens'
+ end
+ get { present paginate(finder(declared_params(include_missing: false)).execute), with: Entities::ImpersonationToken }
+
+ desc 'Create a impersonation token. Available only for admins.' do
+ detail 'This feature was introduced in GitLab 9.0'
+ success Entities::ImpersonationToken
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the impersonation token'
+ optional :expires_at, type: Date, desc: 'The expiration date in the format YEAR-MONTH-DAY of the impersonation token'
+ optional :scopes, type: Array, desc: 'The array of scopes of the impersonation token'
+ end
+ post do
+ impersonation_token = finder.build(declared_params(include_missing: false))
+
+ if impersonation_token.save
+ present impersonation_token, with: Entities::ImpersonationToken
+ else
+ render_validation_error!(impersonation_token)
+ end
+ end
+
+ desc 'Retrieve impersonation token. Available only for admins.' do
+ detail 'This feature was introduced in GitLab 9.0'
+ success Entities::ImpersonationToken
+ end
+ params do
+ requires :impersonation_token_id, type: Integer, desc: 'The ID of the impersonation token'
+ end
+ get ':impersonation_token_id' do
+ present find_impersonation_token, with: Entities::ImpersonationToken
+ end
+
+ desc 'Revoke a impersonation token. Available only for admins.' do
+ detail 'This feature was introduced in GitLab 9.0'
+ end
+ params do
+ requires :impersonation_token_id, type: Integer, desc: 'The ID of the impersonation token'
+ end
+ delete ':impersonation_token_id' do
+ find_impersonation_token.revoke!
+ end
+ end
+ end
end
resource :user do
@@ -359,8 +431,11 @@ module API
desc "Get the currently authenticated user's SSH keys" do
success Entities::SSHKey
end
+ params do
+ use :pagination
+ end
get "keys" do
- present current_user.keys, with: Entities::SSHKey
+ present paginate(current_user.keys), with: Entities::SSHKey
end
desc 'Get a single key owned by currently authenticated user' do
@@ -403,14 +478,17 @@ module API
key = current_user.keys.find_by(id: params[:key_id])
not_found!('Key') unless key
- present key.destroy, with: Entities::SSHKey
+ key.destroy
end
desc "Get the currently authenticated user's email addresses" do
success Entities::Email
end
+ params do
+ use :pagination
+ end
get "emails" do
- present current_user.emails, with: Entities::Email
+ present paginate(current_user.emails), with: Entities::Email
end
desc 'Get a single email address owned by the currently authenticated user' do
diff --git a/lib/api/v3/award_emoji.rb b/lib/api/v3/award_emoji.rb
new file mode 100644
index 00000000000..b96b2d70b12
--- /dev/null
+++ b/lib/api/v3/award_emoji.rb
@@ -0,0 +1,130 @@
+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
diff --git a/lib/api/v3/boards.rb b/lib/api/v3/boards.rb
new file mode 100644
index 00000000000..94acc67171e
--- /dev/null
+++ b/lib/api/v3/boards.rb
@@ -0,0 +1,72 @@
+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
diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb
new file mode 100644
index 00000000000..0a877b960f6
--- /dev/null
+++ b/lib/api/v3/branches.rb
@@ -0,0 +1,72 @@
+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::RepoBranch
+ end
+ get ":id/repository/branches" do
+ branches = user_project.repository.branches.sort_by(&:name)
+
+ present branches, with: ::API::Entities::RepoBranch, project: user_project
+ 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::RepoBranch
+ 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::RepoBranch,
+ project: user_project
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/broadcast_messages.rb b/lib/api/v3/broadcast_messages.rb
new file mode 100644
index 00000000000..417e4ad0b26
--- /dev/null
+++ b/lib/api/v3/broadcast_messages.rb
@@ -0,0 +1,31 @@
+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
diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb
new file mode 100644
index 00000000000..6f97102c6ef
--- /dev/null
+++ b/lib/api/v3/builds.rb
@@ -0,0 +1,255 @@
+module API
+ module V3
+ class Builds < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ helpers do
+ params :optional_scope do
+ optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show',
+ values: %w(pending running failed success canceled skipped),
+ coerce_with: ->(scope) {
+ if scope.is_a?(String)
+ [scope]
+ elsif scope.is_a?(Hashie::Mash)
+ scope.values
+ else
+ ['unknown']
+ end
+ }
+ end
+ end
+
+ desc 'Get a project builds' do
+ success ::API::V3::Entities::Build
+ end
+ params do
+ use :optional_scope
+ use :pagination
+ end
+ get ':id/builds' do
+ builds = user_project.builds.order('id DESC')
+ builds = filter_builds(builds, params[:scope])
+
+ present paginate(builds), with: ::API::V3::Entities::Build
+ end
+
+ desc 'Get builds for a specific commit of a project' do
+ success ::API::V3::Entities::Build
+ end
+ params do
+ requires :sha, type: String, desc: 'The SHA id of a commit'
+ use :optional_scope
+ use :pagination
+ end
+ get ':id/repository/commits/:sha/builds' do
+ authorize_read_builds!
+
+ return not_found! unless user_project.commit(params[:sha])
+
+ pipelines = user_project.pipelines.where(sha: params[:sha])
+ builds = user_project.builds.where(pipeline: pipelines).order('id DESC')
+ builds = filter_builds(builds, params[:scope])
+
+ present paginate(builds), with: ::API::V3::Entities::Build
+ 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_artifacts!(build.artifacts_file)
+ end
+
+ desc 'Download the artifacts file from build' do
+ detail 'This feature was introduced in GitLab 8.10'
+ end
+ params do
+ requires :ref_name, type: String, desc: 'The ref from repository'
+ requires :job, type: String, desc: 'The name for the build'
+ end
+ get ':id/builds/artifacts/:ref_name/download',
+ requirements: { ref_name: /.+/ } do
+ authorize_read_builds!
+
+ builds = user_project.latest_successful_builds_for(params[:ref_name])
+ latest_build = builds.find_by!(name: params[:job])
+
+ present_artifacts!(latest_build.artifacts_file)
+ end
+
+ # TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace
+ # is saved in the DB instead of file). But before that, we need to consider how to replace the value of
+ # `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse.
+ desc 'Get a trace of a specific build of a project'
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ get ':id/builds/:build_id/trace' do
+ authorize_read_builds!
+
+ build = get_build!(params[:build_id])
+
+ header 'Content-Disposition', "infile; filename=\"#{build.id}.log\""
+ content_type 'text/plain'
+ env['api.format'] = :binary
+
+ trace = build.trace
+ body trace
+ end
+
+ desc 'Cancel a specific build of a project' do
+ success ::API::V3::Entities::Build
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ post ':id/builds/:build_id/cancel' do
+ authorize_update_builds!
+
+ build = get_build!(params[:build_id])
+
+ build.cancel
+
+ present build, with: ::API::V3::Entities::Build
+ end
+
+ desc 'Retry a specific build of a project' do
+ success ::API::V3::Entities::Build
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ post ':id/builds/:build_id/retry' do
+ authorize_update_builds!
+
+ build = get_build!(params[:build_id])
+ return forbidden!('Build is not retryable') unless build.retryable?
+
+ build = Ci::Build.retry(build, current_user)
+
+ present build, with: ::API::V3::Entities::Build
+ end
+
+ desc 'Erase build (remove artifacts and build trace)' do
+ success ::API::V3::Entities::Build
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ post ':id/builds/:build_id/erase' do
+ authorize_update_builds!
+
+ build = get_build!(params[:build_id])
+ return forbidden!('Build is not erasable!') unless build.erasable?
+
+ build.erase(erased_by: current_user)
+ present build, with: ::API::V3::Entities::Build
+ end
+
+ desc 'Keep the artifacts to prevent them from being deleted' do
+ success ::API::V3::Entities::Build
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ post ':id/builds/:build_id/artifacts/keep' do
+ authorize_update_builds!
+
+ build = get_build!(params[:build_id])
+ return not_found!(build) unless build.artifacts?
+
+ build.keep_artifacts!
+
+ status 200
+ present build, with: ::API::V3::Entities::Build
+ end
+
+ desc 'Trigger a manual build' do
+ success ::API::V3::Entities::Build
+ detail 'This feature was added in GitLab 8.11'
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a Build'
+ end
+ post ":id/builds/:build_id/play" do
+ authorize_read_builds!
+
+ build = get_build!(params[:build_id])
+
+ bad_request!("Unplayable Job") unless build.playable?
+
+ build.play(current_user)
+
+ status 200
+ present build, with: ::API::V3::Entities::Build
+ end
+ end
+
+ helpers do
+ def get_build(id)
+ user_project.builds.find_by(id: id.to_i)
+ end
+
+ def get_build!(id)
+ get_build(id) || not_found!
+ end
+
+ def present_artifacts!(artifacts_file)
+ if !artifacts_file.file_storage?
+ redirect_to(build.artifacts_file.url)
+ elsif artifacts_file.exists?
+ present_file!(artifacts_file.path, artifacts_file.filename)
+ else
+ not_found!
+ end
+ end
+
+ def filter_builds(builds, scope)
+ return builds if scope.nil? || scope.empty?
+
+ available_statuses = ::CommitStatus::AVAILABLE_STATUSES
+
+ unknown = scope - available_statuses
+ render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty?
+
+ builds.where(status: available_statuses && scope)
+ end
+
+ def authorize_read_builds!
+ authorize! :read_build, user_project
+ end
+
+ def authorize_update_builds!
+ authorize! :update_build, user_project
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb
new file mode 100644
index 00000000000..3414a2883e5
--- /dev/null
+++ b/lib/api/v3/commits.rb
@@ -0,0 +1,196 @@
+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: { id: %r{[^/]+} } do
+ desc 'Get a project repository commits' do
+ success ::API::Entities::RepoCommit
+ 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::RepoCommit
+ end
+
+ desc 'Commit multiple file changes as one commit' do
+ success ::API::Entities::RepoCommitDetail
+ 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!(branch: branch, start_branch: branch, target_branch: 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::RepoCommitDetail
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
+
+ desc 'Get a specific commit of a project' do
+ success ::API::Entities::RepoCommitDetail
+ 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" do
+ commit = user_project.commit(params[:sha])
+
+ not_found! "Commit" unless commit
+
+ present commit, with: ::API::Entities::RepoCommitDetail
+ 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" 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' do
+ commit = user_project.commit(params[:sha])
+
+ not_found! 'Commit' unless commit
+ notes = Note.where(commit_id: commit.id).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::RepoCommit
+ 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' 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],
+ target_branch: 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::RepoCommit
+ 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' 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(all_diffs: true).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::Diff::LineCode.generate(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
diff --git a/lib/api/v3/deploy_keys.rb b/lib/api/v3/deploy_keys.rb
new file mode 100644
index 00000000000..bbb174b6003
--- /dev/null
+++ b/lib/api/v3/deploy_keys.rb
@@ -0,0 +1,122 @@
+module API
+ module V3
+ class DeployKeys < Grape::API
+ before { authenticate! }
+
+ 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::SSHKey
+ end
+ get ":id/#{path}" do
+ present user_project.deploy_keys, with: ::API::Entities::SSHKey
+ end
+
+ desc 'Get single deploy key' do
+ success ::API::Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ get ":id/#{path}/:key_id" do
+ key = user_project.deploy_keys.find params[:key_id]
+ present key, with: ::API::Entities::SSHKey
+ end
+
+ desc 'Add new deploy key to currently authenticated user' do
+ success ::API::Entities::SSHKey
+ end
+ params do
+ requires :key, type: String, desc: 'The new deploy key'
+ requires :title, type: String, desc: 'The name of the deploy key'
+ end
+ post ":id/#{path}" do
+ params[:key].strip!
+
+ # Check for an existing key joined to this project
+ key = user_project.deploy_keys.find_by(key: params[:key])
+ if key
+ present key, with: ::API::Entities::SSHKey
+ break
+ end
+
+ # Check for available deploy keys in other projects
+ key = current_user.accessible_deploy_keys.find_by(key: params[:key])
+ if key
+ user_project.deploy_keys << key
+ present key, with: ::API::Entities::SSHKey
+ break
+ end
+
+ # Create a new deploy key
+ key = DeployKey.new(declared_params(include_missing: false))
+ if key.valid? && user_project.deploy_keys << key
+ present key, with: ::API::Entities::SSHKey
+ 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
diff --git a/lib/api/v3/deployments.rb b/lib/api/v3/deployments.rb
new file mode 100644
index 00000000000..1d4972eda26
--- /dev/null
+++ b/lib/api/v3/deployments.rb
@@ -0,0 +1,43 @@
+module API
+ module V3
+ # Deployments RESTful API endpoints
+ class Deployments < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The project ID'
+ end
+ resource :projects, 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
diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb
new file mode 100644
index 00000000000..832b4bdeb4f
--- /dev/null
+++ b/lib/api/v3/entities.rb
@@ -0,0 +1,253 @@
+module API
+ module V3
+ module Entities
+ class ProjectSnippet < Grape::Entity
+ expose :id, :title, :file_name
+ expose :author, using: ::API::Entities::UserBasic
+ expose :updated_at, :created_at
+ expose(:expires_at) { |snippet| nil }
+
+ expose :web_url do |snippet, options|
+ Gitlab::UrlBuilder.build(snippet)
+ end
+ end
+
+ class Note < Grape::Entity
+ expose :id
+ expose :note, as: :body
+ expose :attachment_identifier, as: :attachment
+ expose :author, using: ::API::Entities::UserBasic
+ expose :created_at, :updated_at
+ expose :system?, as: :system
+ expose :noteable_id, :noteable_type
+ # upvote? and downvote? are deprecated, always return false
+ expose(:upvote?) { |note| false }
+ expose(:downvote?) { |note| false }
+ end
+
+ class Event < Grape::Entity
+ expose :title, :project_id, :action_name
+ expose :target_id, :target_type, :author_id
+ expose :data, :target_title
+ expose :created_at
+ expose :note, using: Entities::Note, if: ->(event, options) { event.note? }
+ expose :author, using: ::API::Entities::UserBasic, if: ->(event, options) { event.author }
+
+ expose :author_username do |event, options|
+ event.author&.username
+ end
+ end
+
+ class AwardEmoji < Grape::Entity
+ expose :id
+ expose :name
+ expose :user, using: ::API::Entities::UserBasic
+ expose :created_at, :updated_at
+ expose :awardable_id, :awardable_type
+ end
+
+ class Project < Grape::Entity
+ expose :id, :description, :default_branch, :tag_list
+ expose :public?, as: :public
+ expose :archived?, as: :archived
+ expose :visibility_level, :ssh_url_to_repo, :http_url_to_repo, :web_url
+ expose :owner, using: ::API::Entities::UserBasic, unless: ->(project, options) { project.group }
+ expose :name, :name_with_namespace
+ expose :path, :path_with_namespace
+ expose :container_registry_enabled
+
+ # Expose old field names with the new permissions methods to keep API compatible
+ expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) }
+ expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) }
+ expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) }
+ expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) }
+ expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) }
+
+ expose :created_at, :last_activity_at
+ expose :shared_runners_enabled
+ expose :lfs_enabled?, as: :lfs_enabled
+ expose :creator_id
+ expose :namespace, using: 'API::Entities::Namespace'
+ expose :forked_from_project, using: ::API::Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? }
+ expose :avatar_url
+ expose :star_count, :forks_count
+ expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) && project.default_issues_tracker? }
+ expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
+ expose :public_builds
+ expose :shared_with_groups do |project, options|
+ ::API::Entities::SharedGroup.represent(project.project_group_links.all, options)
+ end
+ expose :only_allow_merge_if_pipeline_succeeds, as: :only_allow_merge_if_build_succeeds
+ expose :request_access_enabled
+ expose :only_allow_merge_if_all_discussions_are_resolved
+
+ expose :statistics, using: '::API::V3::Entities::ProjectStatistics', if: :statistics
+ end
+
+ class ProjectWithAccess < Project
+ expose :permissions do
+ expose :project_access, using: ::API::Entities::ProjectAccess do |project, options|
+ project.project_members.find_by(user_id: options[:current_user].id)
+ end
+
+ expose :group_access, using: ::API::Entities::GroupAccess do |project, options|
+ if project.group
+ project.group.group_members.find_by(user_id: options[:current_user].id)
+ end
+ end
+ end
+ end
+
+ class MergeRequest < Grape::Entity
+ expose :id, :iid
+ expose(:project_id) { |entity| entity.project.id }
+ expose :title, :description
+ expose :state, :created_at, :updated_at
+ expose :target_branch, :source_branch
+ expose :upvotes, :downvotes
+ expose :author, :assignee, using: ::API::Entities::UserBasic
+ expose :source_project_id, :target_project_id
+ expose :label_names, as: :labels
+ expose :work_in_progress?, as: :work_in_progress
+ expose :milestone, using: ::API::Entities::Milestone
+ expose :merge_when_pipeline_succeeds, as: :merge_when_build_succeeds
+ expose :merge_status
+ expose :diff_head_sha, as: :sha
+ expose :merge_commit_sha
+ expose :subscribed do |merge_request, options|
+ merge_request.subscribed?(options[:current_user], options[:project])
+ end
+ expose :user_notes_count
+ expose :should_remove_source_branch?, as: :should_remove_source_branch
+ expose :force_remove_source_branch?, as: :force_remove_source_branch
+
+ expose :web_url do |merge_request, options|
+ Gitlab::UrlBuilder.build(merge_request)
+ end
+ end
+
+ class Group < Grape::Entity
+ expose :id, :name, :path, :description, :visibility_level
+ expose :lfs_enabled?, as: :lfs_enabled
+ expose :avatar_url
+ expose :web_url
+ expose :request_access_enabled
+ expose :full_name, :full_path
+ expose :parent_id
+
+ expose :statistics, if: :statistics do
+ with_options format_with: -> (value) { value.to_i } do
+ expose :storage_size
+ expose :repository_size
+ expose :lfs_objects_size
+ expose :build_artifacts_size
+ end
+ end
+ end
+
+ class GroupDetail < Group
+ expose :projects, using: Entities::Project
+ expose :shared_projects, using: Entities::Project
+ end
+
+ class ApplicationSetting < Grape::Entity
+ expose :id
+ expose :default_projects_limit
+ expose :signup_enabled
+ expose :signin_enabled
+ expose :gravatar_enabled
+ expose :sign_in_text
+ expose :after_sign_up_text
+ expose :created_at
+ expose :updated_at
+ expose :home_page_url
+ expose :default_branch_protection
+ expose :restricted_visibility_levels
+ expose :max_attachment_size
+ expose :session_expire_delay
+ expose :default_project_visibility
+ expose :default_snippet_visibility
+ expose :default_group_visibility
+ expose :domain_whitelist
+ expose :domain_blacklist_enabled
+ expose :domain_blacklist
+ expose :user_oauth_applications
+ expose :after_sign_out_path
+ expose :container_registry_token_expire_delay
+ expose :repository_storage
+ expose :repository_storages
+ expose :koding_enabled
+ expose :koding_url
+ expose :plantuml_enabled
+ expose :plantuml_url
+ expose :terminal_max_session_time
+ end
+
+ class Environment < ::API::Entities::EnvironmentBasic
+ expose :project, using: Entities::Project
+ end
+
+ class Trigger < Grape::Entity
+ expose :token, :created_at, :updated_at, :deleted_at, :last_used
+ expose :owner, using: ::API::Entities::UserBasic
+ end
+
+ class TriggerRequest < Grape::Entity
+ expose :id, :variables
+ end
+
+ class Build < Grape::Entity
+ expose :id, :status, :stage, :name, :ref, :tag, :coverage
+ expose :created_at, :started_at, :finished_at
+ expose :user, with: ::API::Entities::User
+ expose :artifacts_file, using: ::API::Entities::JobArtifactFile, if: -> (build, opts) { build.artifacts? }
+ expose :commit, with: ::API::Entities::RepoCommit
+ expose :runner, with: ::API::Entities::Runner
+ expose :pipeline, with: ::API::Entities::PipelineBasic
+ end
+
+ class BuildArtifactFile < Grape::Entity
+ expose :filename, :size
+ end
+
+ class Deployment < Grape::Entity
+ expose :id, :iid, :ref, :sha, :created_at
+ expose :user, using: ::API::Entities::UserBasic
+ expose :environment, using: ::API::Entities::EnvironmentBasic
+ expose :deployable, using: Entities::Build
+ end
+
+ class MergeRequestChanges < MergeRequest
+ expose :diffs, as: :changes, using: ::API::Entities::RepoDiff do |compare, _|
+ compare.raw_diffs(all_diffs: true).to_a
+ end
+ end
+
+ class ProjectStatistics < Grape::Entity
+ expose :commit_count
+ expose :storage_size
+ expose :repository_size
+ expose :lfs_objects_size
+ expose :build_artifacts_size
+ end
+
+ class ProjectService < Grape::Entity
+ expose :id, :title, :created_at, :updated_at, :active
+ expose :push_events, :issues_events, :merge_requests_events
+ expose :tag_push_events, :note_events, :build_events, :pipeline_events
+ # Expose serialized properties
+ expose :properties do |service, options|
+ field_names = service.fields.
+ select { |field| options[:include_passwords] || field[:type] != 'password' }.
+ map { |field| field[:name] }
+ service.properties.slice(*field_names)
+ end
+ end
+
+ class ProjectHook < ::API::Entities::Hook
+ expose :project_id, :issues_events, :merge_requests_events
+ expose :note_events, :build_events, :pipeline_events, :wiki_page_events
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/environments.rb b/lib/api/v3/environments.rb
new file mode 100644
index 00000000000..6bb4e016a01
--- /dev/null
+++ b/lib/api/v3/environments.rb
@@ -0,0 +1,87 @@
+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
diff --git a/lib/api/v3/files.rb b/lib/api/v3/files.rb
new file mode 100644
index 00000000000..13542b0c71c
--- /dev/null
+++ b/lib/api/v3/files.rb
@@ -0,0 +1,138 @@
+module API
+ module V3
+ class Files < Grape::API
+ helpers do
+ def commit_params(attrs)
+ {
+ file_path: attrs[:file_path],
+ start_branch: attrs[:branch],
+ target_branch: 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!(repo)
+ 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::DestroyService.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
diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb
new file mode 100644
index 00000000000..c5b37622d79
--- /dev/null
+++ b/lib/api/v3/groups.rb
@@ -0,0 +1,181 @@
+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.execute(current_user)
+ else
+ current_user.groups
+ end
+
+ groups = groups.search(params[:search]) if params[:search].present?
+ groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
+ groups = groups.reorder(params[:order_by] => params[:sort])
+
+ present_groups groups, statistics: params[:statistics] && current_user.is_admin?
+ end
+
+ desc 'Get list of owned groups for authenticated user' do
+ success 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'
+ optional :parent_id, type: Integer, desc: 'The parent group id for creating nested group'
+ use :optional_params
+ end
+ post do
+ authorize! :create_group
+
+ group = ::Groups::CreateService.new(current_user, declared_params(include_missing: false)).execute
+
+ if group.persisted?
+ present group, with: Entities::Group, current_user: current_user
+ else
+ render_api_error!("Failed to save group #{group.errors.messages}", 400)
+ end
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a group'
+ end
+ resource :groups, 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
+ present ::Groups::DestroyService.new(group, current_user).execute, with: Entities::GroupDetail, current_user: current_user
+ end
+
+ desc 'Get a list of projects in this group.' do
+ success Entities::Project
+ end
+ params do
+ optional :archived, type: Boolean, default: false, desc: 'Limit by archived status'
+ optional :visibility, type: String, values: %w[public internal private],
+ desc: 'Limit by visibility'
+ optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
+ optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at],
+ default: 'created_at', desc: 'Return projects ordered by field'
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return projects sorted in ascending and descending order'
+ optional :simple, type: Boolean, default: false,
+ desc: 'Return only the ID, URL, name, and path of each project'
+ optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user'
+ optional :starred, type: Boolean, default: false, desc: 'Limit by starred status'
+
+ use :pagination
+ end
+ get ":id/projects" do
+ group = find_group!(params[:id])
+ projects = GroupProjectsFinder.new(group).execute(current_user)
+ projects = filter_projects(projects)
+ entity = params[:simple] ? ::API::Entities::BasicProjectDetails : Entities::Project
+ present paginate(projects), with: entity, current_user: current_user
+ end
+
+ desc 'Transfer a project to the group namespace. Available only for admin.' do
+ success Entities::GroupDetail
+ end
+ params do
+ requires :project_id, type: String, desc: 'The ID or path of the project'
+ end
+ post ":id/projects/:project_id", 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
diff --git a/lib/api/v3/helpers.rb b/lib/api/v3/helpers.rb
new file mode 100644
index 00000000000..0f234d4cdad
--- /dev/null
+++ b/lib/api/v3/helpers.rb
@@ -0,0 +1,19 @@
+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
+ end
+ end
+end
diff --git a/lib/api/v3/issues.rb b/lib/api/v3/issues.rb
new file mode 100644
index 00000000000..715083fc4f8
--- /dev/null
+++ b/lib/api/v3/issues.rb
@@ -0,0 +1,231 @@
+module API
+ module V3
+ class Issues < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ helpers do
+ def find_issues(args = {})
+ args = params.merge(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::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::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::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::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::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::Entities::Issue, current_user: current_user, project: user_project
+ end
+
+ desc 'Get a single project issue' do
+ success ::API::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::Entities::Issue, current_user: current_user, project: user_project
+ end
+
+ desc 'Create a new project issue' do
+ success ::API::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
+ # 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 = ::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::Entities::Issue, current_user: current_user, project: user_project
+ else
+ render_validation_error!(issue)
+ end
+ end
+
+ desc 'Update an existing issue' do
+ success ::API::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
+ 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)
+
+ 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::Entities::Issue, current_user: current_user, project: user_project
+ else
+ render_validation_error!(issue)
+ end
+ end
+
+ desc 'Move an existing issue' do
+ success ::API::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
+ 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::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
diff --git a/lib/api/v3/labels.rb b/lib/api/v3/labels.rb
new file mode 100644
index 00000000000..bd5eb2175e8
--- /dev/null
+++ b/lib/api/v3/labels.rb
@@ -0,0 +1,34 @@
+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, 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
diff --git a/lib/api/v3/members.rb b/lib/api/v3/members.rb
new file mode 100644
index 00000000000..684860b553e
--- /dev/null
+++ b/lib/api/v3/members.rb
@@ -0,0 +1,134 @@
+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])
+
+ users = source.users
+ users = users.merge(User.search(params[:query])) if params[:query]
+
+ present paginate(users), with: ::API::Entities::Member, source: source
+ 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.user, with: ::API::Entities::Member, member: 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.user, with: ::API::Entities::Member, member: 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.user, with: ::API::Entities::Member, member: 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(source, current_user, declared_params).execute
+
+ present member.user, with: ::API::Entities::Member, member: member
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/merge_request_diffs.rb b/lib/api/v3/merge_request_diffs.rb
new file mode 100644
index 00000000000..35f462e907b
--- /dev/null
+++ b/lib/api/v3/merge_request_diffs.rb
@@ -0,0 +1,44 @@
+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, 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
diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb
new file mode 100644
index 00000000000..3077240e650
--- /dev/null
+++ b/lib/api/v3/merge_requests.rb
@@ -0,0 +1,290 @@
+module API
+ module V3
+ class MergeRequests < Grape::API
+ include PaginationParams
+
+ DEPRECATION_MESSAGE = 'This endpoint is deprecated and has been removed on V4'.freeze
+
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ include TimeTrackingEndpoints
+
+ helpers do
+ def handle_merge_request_errors!(errors)
+ if errors[:project_access].any?
+ error!(errors[:project_access], 422)
+ elsif errors[:branch_conflict].any?
+ error!(errors[:branch_conflict], 422)
+ elsif errors[:validate_fork].any?
+ error!(errors[:validate_fork], 422)
+ elsif errors[:validate_branches].any?
+ conflict!(errors[:validate_branches])
+ end
+
+ render_api_error!(errors, 400)
+ end
+
+ def issue_entity(project)
+ if project.has_external_issue_tracker?
+ ::API::Entities::ExternalIssue
+ else
+ ::API::Entities::Issue
+ end
+ end
+
+ params :optional_params do
+ optional :description, type: String, desc: 'The description of the merge request'
+ optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request'
+ optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request'
+ optional :labels, type: String, desc: 'Comma-separated list of label names'
+ optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging'
+ end
+ end
+
+ desc 'List merge requests' do
+ detail 'iid filter is deprecated have been removed on V4'
+ success ::API::V3::Entities::MergeRequest
+ end
+ params do
+ optional :state, type: String, values: %w[opened closed merged all], default: 'all',
+ desc: 'Return opened, closed, merged, or all merge requests'
+ optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
+ desc: 'Return merge requests ordered by `created_at` or `updated_at` fields.'
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return merge requests sorted in `asc` or `desc` order.'
+ optional :iid, type: Array[Integer], desc: 'The IID of the merge requests'
+ use :pagination
+ end
+ get ":id/merge_requests" do
+ authorize! :read_merge_request, user_project
+
+ merge_requests = user_project.merge_requests.inc_notes_with_associations
+ merge_requests = filter_by_iid(merge_requests, params[:iid]) if params[:iid].present?
+
+ merge_requests =
+ case params[:state]
+ when 'opened' then merge_requests.opened
+ when 'closed' then merge_requests.closed
+ when 'merged' then merge_requests.merged
+ else merge_requests
+ end
+
+ merge_requests = merge_requests.reorder(params[:order_by] => params[:sort])
+ present paginate(merge_requests), with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project
+ end
+
+ desc 'Create a merge request' do
+ success ::API::V3::Entities::MergeRequest
+ end
+ params do
+ requires :title, type: String, desc: 'The title of the merge request'
+ requires :source_branch, type: String, desc: 'The source branch'
+ requires :target_branch, type: String, desc: 'The target branch'
+ optional :target_project_id, type: Integer,
+ desc: 'The target project of the merge request defaults to the :id of the project'
+ use :optional_params
+ end
+ post ":id/merge_requests" do
+ authorize! :create_merge_request, user_project
+
+ mr_params = declared_params(include_missing: false)
+ mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
+
+ merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute
+
+ if merge_request.valid?
+ present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project
+ else
+ handle_merge_request_errors! merge_request.errors
+ end
+ end
+
+ desc 'Delete a merge request'
+ params do
+ requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ end
+ delete ":id/merge_requests/:merge_request_id" do
+ merge_request = find_project_merge_request(params[:merge_request_id])
+
+ authorize!(:destroy_merge_request, merge_request)
+
+ status(200)
+ merge_request.destroy
+ end
+
+ params do
+ requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ end
+ { ":id/merge_request/:merge_request_id" => :deprecated, ":id/merge_requests/:merge_request_id" => :ok }.each do |path, status|
+ desc 'Get a single merge request' do
+ if status == :deprecated
+ detail DEPRECATION_MESSAGE
+ end
+ success ::API::V3::Entities::MergeRequest
+ end
+ get path do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+
+ present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project
+ end
+
+ desc 'Get the commits of a merge request' do
+ success ::API::Entities::RepoCommit
+ end
+ get "#{path}/commits" do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+
+ present merge_request.commits, with: ::API::Entities::RepoCommit
+ end
+
+ desc 'Show the merge request changes' do
+ success ::API::Entities::MergeRequestChanges
+ end
+ get "#{path}/changes" do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+
+ present merge_request, with: ::API::Entities::MergeRequestChanges, current_user: current_user
+ end
+
+ desc 'Update a merge request' do
+ success ::API::V3::Entities::MergeRequest
+ end
+ params do
+ optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
+ optional :target_branch, type: String, allow_blank: false, desc: 'The target branch'
+ optional :state_event, type: String, values: %w[close reopen merge],
+ desc: 'Status of the merge request'
+ use :optional_params
+ at_least_one_of :title, :target_branch, :description, :assignee_id,
+ :milestone_id, :labels, :state_event,
+ :remove_source_branch
+ end
+ put path do
+ merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request)
+
+ mr_params = declared_params(include_missing: false)
+ mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
+
+ merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request)
+
+ if merge_request.valid?
+ present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project
+ else
+ handle_merge_request_errors! merge_request.errors
+ end
+ end
+
+ desc 'Merge a merge request' do
+ success ::API::V3::Entities::MergeRequest
+ end
+ params do
+ optional :merge_commit_message, type: String, desc: 'Custom merge commit message'
+ optional :should_remove_source_branch, type: Boolean,
+ desc: 'When true, the source branch will be deleted if possible'
+ optional :merge_when_build_succeeds, type: Boolean,
+ desc: 'When true, this merge request will be merged when the build succeeds'
+ optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
+ end
+ put "#{path}/merge" do
+ merge_request = find_project_merge_request(params[:merge_request_id])
+
+ # Merge request can not be merged
+ # because user dont have permissions to push into target branch
+ unauthorized! unless merge_request.can_be_merged_by?(current_user)
+
+ not_allowed! unless merge_request.mergeable_state?
+
+ render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable?
+
+ if params[:sha] && merge_request.diff_head_sha != params[:sha]
+ render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409)
+ end
+
+ merge_params = {
+ commit_message: params[:merge_commit_message],
+ should_remove_source_branch: params[:should_remove_source_branch]
+ }
+
+ if params[:merge_when_build_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active?
+ ::MergeRequests::MergeWhenPipelineSucceedsService
+ .new(merge_request.target_project, current_user, merge_params)
+ .execute(merge_request)
+ else
+ ::MergeRequests::MergeService
+ .new(merge_request.target_project, current_user, merge_params)
+ .execute(merge_request)
+ end
+
+ present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project
+ end
+
+ desc 'Cancel merge if "Merge When Build succeeds" is enabled' do
+ success ::API::V3::Entities::MergeRequest
+ end
+ post "#{path}/cancel_merge_when_build_succeeds" do
+ merge_request = find_project_merge_request(params[:merge_request_id])
+
+ unauthorized! unless merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user)
+
+ ::MergeRequest::MergeWhenPipelineSucceedsService
+ .new(merge_request.target_project, current_user)
+ .cancel(merge_request)
+ end
+
+ desc 'Get the comments of a merge request' do
+ detail 'Duplicate. DEPRECATED and HAS BEEN REMOVED in V4'
+ success ::API::Entities::MRNote
+ end
+ params do
+ use :pagination
+ end
+ get "#{path}/comments" do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+ present paginate(merge_request.notes.fresh), with: ::API::Entities::MRNote
+ end
+
+ desc 'Post a comment to a merge request' do
+ detail 'Duplicate. DEPRECATED and HAS BEEN REMOVED in V4'
+ success ::API::Entities::MRNote
+ end
+ params do
+ requires :note, type: String, desc: 'The text of the comment'
+ end
+ post "#{path}/comments" do
+ merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note)
+
+ opts = {
+ note: params[:note],
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id
+ }
+
+ note = ::Notes::CreateService.new(user_project, current_user, opts).execute
+
+ if note.save
+ present note, with: ::API::Entities::MRNote
+ else
+ render_api_error!("Failed to save note #{note.errors.messages}", 400)
+ end
+ end
+
+ desc 'List issues that will be closed on merge' do
+ success ::API::Entities::MRNote
+ end
+ params do
+ use :pagination
+ end
+ get "#{path}/closes_issues" do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+ issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
+ present paginate(issues), with: issue_entity(user_project), current_user: current_user
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/milestones.rb b/lib/api/v3/milestones.rb
new file mode 100644
index 00000000000..be90cec4afc
--- /dev/null
+++ b/lib/api/v3/milestones.rb
@@ -0,0 +1,64 @@
+module API
+ module V3
+ class Milestones < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ helpers do
+ def filter_milestones_state(milestones, state)
+ case state
+ when 'active' then milestones.active
+ when 'closed' then milestones.closed
+ else milestones
+ end
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, 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?
+
+ present paginate(milestones), with: ::API::Entities::Milestone
+ end
+
+ desc 'Get all issues for a single project milestone' do
+ success ::API::Entities::Issue
+ end
+ params do
+ requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
+ use :pagination
+ end
+ get ':id/milestones/:milestone_id/issues' do
+ authorize! :read_milestone, user_project
+
+ milestone = user_project.milestones.find(params[:milestone_id])
+
+ finder_params = {
+ project_id: user_project.id,
+ milestone_title: milestone.title
+ }
+
+ issues = IssuesFinder.new(current_user, finder_params).execute
+ present paginate(issues), with: ::API::Entities::Issue, current_user: current_user, project: user_project
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/notes.rb b/lib/api/v3/notes.rb
new file mode 100644
index 00000000000..4f8e0eff4ff
--- /dev/null
+++ b/lib/api/v3/notes.rb
@@ -0,0 +1,148 @@
+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.send(noteables_str.to_sym).find(params[:noteable_id])
+
+ 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.send(noteables_str.to_sym).find(params[:noteable_id])
+ 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.send(noteables_str.to_sym).find(params[:noteable_id])
+
+ if can?(current_user, noteable_read_ability_name(noteable), noteable)
+ if params[:created_at] && (current_user.is_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
diff --git a/lib/api/v3/pipelines.rb b/lib/api/v3/pipelines.rb
new file mode 100644
index 00000000000..82827249244
--- /dev/null
+++ b/lib/api/v3/pipelines.rb
@@ -0,0 +1,36 @@
+module API
+ module V3
+ class Pipelines < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The project ID'
+ end
+ resource :projects, 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
+ authorize! :read_pipeline, user_project
+
+ pipelines = PipelinesFinder.new(user_project).execute(scope: params[:scope])
+ present paginate(pipelines), with: ::API::Entities::Pipeline
+ end
+ end
+
+ helpers do
+ def pipeline
+ @pipeline ||= user_project.pipelines.find(params[:pipeline_id])
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/project_hooks.rb b/lib/api/v3/project_hooks.rb
new file mode 100644
index 00000000000..94614bfc8b6
--- /dev/null
+++ b/lib/api/v3/project_hooks.rb
@@ -0,0 +1,106 @@
+module API
+ module V3
+ class ProjectHooks < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+ before { authorize_admin_project }
+
+ helpers do
+ params :project_hook_properties do
+ requires :url, type: String, desc: "The URL to send the request to"
+ optional :push_events, type: Boolean, desc: "Trigger hook on push events"
+ optional :issues_events, type: Boolean, desc: "Trigger hook on issues events"
+ optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events"
+ optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events"
+ optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events"
+ optional :build_events, type: Boolean, desc: "Trigger hook on build events"
+ optional :pipeline_events, type: Boolean, desc: "Trigger hook on pipeline events"
+ optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events"
+ optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook"
+ optional :token, type: String, desc: "Secret token to validate received payloads; this will not be returned in the response"
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, 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
+ hook = user_project.hooks.new(declared_params(include_missing: false))
+
+ if hook.save
+ present hook, with: ::API::V3::Entities::ProjectHook
+ else
+ error!("Invalid url given", 422) if hook.errors[:url].present?
+
+ not_found!("Project hook #{hook.errors.messages}")
+ end
+ end
+
+ desc 'Update an existing project hook' do
+ success ::API::V3::Entities::ProjectHook
+ end
+ params do
+ requires :hook_id, type: Integer, desc: "The ID of the hook to update"
+ use :project_hook_properties
+ end
+ put ":id/hooks/:hook_id" do
+ hook = user_project.hooks.find(params.delete(:hook_id))
+
+ if hook.update_attributes(declared_params(include_missing: false))
+ present hook, with: ::API::V3::Entities::ProjectHook
+ else
+ error!("Invalid url given", 422) if hook.errors[:url].present?
+
+ not_found!("Project hook #{hook.errors.messages}")
+ end
+ end
+
+ desc 'Deletes project hook' do
+ success ::API::V3::Entities::ProjectHook
+ end
+ params do
+ requires :hook_id, type: Integer, desc: 'The ID of the hook to delete'
+ end
+ delete ":id/hooks/:hook_id" do
+ begin
+ present user_project.hooks.destroy(params[:hook_id]), with: ::API::V3::Entities::ProjectHook
+ rescue
+ # ProjectHook can raise Error if hook_id not found
+ not_found!("Error deleting hook #{params[:hook_id]}")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/project_snippets.rb b/lib/api/v3/project_snippets.rb
new file mode 100644
index 00000000000..fc065a22d74
--- /dev/null
+++ b/lib/api/v3/project_snippets.rb
@@ -0,0 +1,143 @@
+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
+ finder_params = { filter: :by_project, project: user_project }
+ SnippetsFinder.new.execute(current_user, finder_params)
+ 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
diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb
new file mode 100644
index 00000000000..b753dbab381
--- /dev/null
+++ b/lib/api/v3/projects.rb
@@ -0,0 +1,474 @@
+module API
+ module V3
+ class Projects < Grape::API
+ include PaginationParams
+
+ before { authenticate_non_get! }
+
+ after_validation do
+ set_only_allow_merge_if_pipeline_succeeds!
+ end
+
+ 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'
+ 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,
+ Gitlab::VisibilityLevel::PUBLIC
+ ], desc: 'Create a public project. The same as visibility_level = 20.'
+ 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'
+ optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
+ 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
+
+ def set_only_allow_merge_if_pipeline_succeeds!
+ if params.has_key?(:only_allow_merge_if_build_succeeds)
+ params[:only_allow_merge_if_pipeline_succeeds] = params.delete(:only_allow_merge_if_build_succeeds)
+ end
+ end
+ 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
+ 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'
+ end
+
+ params :statistics_params do
+ optional :statistics, type: Boolean, default: false, desc: 'Include project statistics'
+ end
+
+ params :create_params do
+ optional :namespace_id, type: Integer, desc: 'Namespace ID for the new project. Default to the user namespace.'
+ optional :import_url, type: String, desc: 'URL from which the project is imported'
+ end
+
+ def present_projects(projects, options = {})
+ options = options.reverse_merge(
+ with: ::API::V3::Entities::Project,
+ current_user: current_user,
+ simple: params[:simple],
+ )
+
+ projects = filter_projects(projects)
+ projects = projects.with_statistics if options[:statistics]
+ options[:with] = ::API::Entities::BasicProjectDetails if options[:simple]
+
+ present paginate(projects), options
+ end
+ end
+
+ desc 'Get a list of visible projects for authenticated user' do
+ success ::API::Entities::BasicProjectDetails
+ end
+ params do
+ use :collection_params
+ end
+ get '/visible' do
+ entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails
+ present_projects ProjectsFinder.new.execute(current_user), with: entity
+ end
+
+ desc 'Get a projects list for authenticated user' do
+ success ::API::Entities::BasicProjectDetails
+ end
+ params do
+ use :collection_params
+ end
+ get do
+ authenticate!
+
+ present_projects current_user.authorized_projects,
+ with: ::API::V3::Entities::ProjectWithAccess
+ end
+
+ desc 'Get an owned projects list for authenticated user' do
+ success ::API::Entities::BasicProjectDetails
+ end
+ params do
+ use :collection_params
+ use :statistics_params
+ end
+ get '/owned' do
+ authenticate!
+
+ present_projects current_user.owned_projects,
+ with: ::API::V3::Entities::ProjectWithAccess,
+ statistics: params[:statistics]
+ end
+
+ desc 'Gets starred project for the authenticated user' do
+ success ::API::Entities::BasicProjectDetails
+ end
+ params do
+ use :collection_params
+ end
+ get '/starred' do
+ authenticate!
+
+ present_projects current_user.viewable_starred_projects
+ end
+
+ desc 'Get all projects for admin user' do
+ success ::API::Entities::BasicProjectDetails
+ end
+ params do
+ use :collection_params
+ use :statistics_params
+ end
+ get '/all' do
+ authenticated_as_admin!
+
+ present_projects Project.all, with: ::API::V3::Entities::ProjectWithAccess, statistics: params[:statistics]
+ end
+
+ desc 'Search for projects the current user has access to' do
+ success ::API::V3::Entities::Project
+ 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])
+
+ present paginate(projects), with: ::API::V3::Entities::Project
+ end
+
+ desc 'Create new project' do
+ success ::API::V3::Entities::Project
+ end
+ params do
+ optional :name, type: String, desc: 'The name of the project'
+ optional :path, type: String, desc: 'The path of the repository'
+ at_least_one_of :name, :path
+ 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?
+ present project, with: ::API::V3::Entities::Project,
+ 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
+ success ::API::V3::Entities::Project
+ 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?
+ present project, with: ::API::V3::Entities::Project,
+ 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
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ desc 'Get a single project' do
+ success ::API::V3::Entities::ProjectWithAccess
+ end
+ get ":id" do
+ entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails
+ 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
+ success ::API::V3::Entities::Event
+ end
+ params do
+ use :pagination
+ end
+ get ":id/events" do
+ present paginate(user_project.events.recent), with: ::API::V3::Entities::Event
+ end
+
+ desc 'Fork new project for the current user or provided namespace.' do
+ success ::API::V3::Entities::Project
+ 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
+ present forked_project, with: ::API::V3::Entities::Project,
+ user_can_admin_project: can?(current_user, :admin_project, forked_project)
+ end
+ end
+
+ desc 'Update an existing project' do
+ success ::API::V3::Entities::Project
+ 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,
+ :shared_runners_enabled, :container_registry_enabled,
+ :lfs_enabled, :public, :visibility_level, :public_builds,
+ :request_access_enabled, :only_allow_merge_if_build_succeeds,
+ :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
+ present user_project, with: ::API::V3::Entities::Project,
+ user_can_admin_project: can?(current_user, :admin_project, user_project)
+ else
+ render_validation_error!(user_project)
+ end
+ end
+
+ desc 'Archive a project' do
+ success ::API::V3::Entities::Project
+ end
+ post ':id/archive' do
+ authorize!(:archive_project, user_project)
+
+ user_project.archive!
+
+ present user_project, with: ::API::V3::Entities::Project
+ end
+
+ desc 'Unarchive a project' do
+ success ::API::V3::Entities::Project
+ end
+ post ':id/unarchive' do
+ authorize!(:archive_project, user_project)
+
+ user_project.unarchive!
+
+ present user_project, with: ::API::V3::Entities::Project
+ end
+
+ desc 'Star a project' do
+ success ::API::V3::Entities::Project
+ end
+ post ':id/star' do
+ if current_user.starred?(user_project)
+ not_modified!
+ else
+ current_user.toggle_star(user_project)
+ user_project.reload
+
+ present user_project, with: ::API::V3::Entities::Project
+ end
+ end
+
+ desc 'Unstar a project' do
+ success ::API::V3::Entities::Project
+ end
+ delete ':id/star' do
+ if current_user.starred?(user_project)
+ current_user.toggle_star(user_project)
+ user_project.reload
+
+ present user_project, with: ::API::V3::Entities::Project
+ else
+ not_modified!
+ end
+ end
+
+ desc 'Remove a project'
+ delete ":id" do
+ authorize! :remove_project, user_project
+
+ status(200)
+ ::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)
+ 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?
+ status(200)
+ user_project.forked_project_link.destroy
+ else
+ not_modified!
+ end
+ end
+
+ desc 'Share the project with a group' do
+ success ::API::Entities::ProjectGroupLink
+ 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
+ present link, with: ::API::Entities::ProjectGroupLink
+ 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
+ ::Projects::UploadService.new(user_project, params[:file]).execute
+ end
+
+ desc 'Get the users list of a project' do
+ success ::API::Entities::UserBasic
+ 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?
+
+ present paginate(users), with: ::API::Entities::UserBasic
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/repositories.rb b/lib/api/v3/repositories.rb
new file mode 100644
index 00000000000..e4d14bc8168
--- /dev/null
+++ b/lib/api/v3/repositories.rb
@@ -0,0 +1,109 @@
+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: { 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
+ end
+
+ desc 'Get a project repository tree' do
+ success ::API::Entities::RepoTreeObject
+ 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::RepoTreeObject
+ 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"] 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' 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::Regex.archive_formats_regex } do
+ begin
+ send_git_archive user_project.repository, ref: params[:sha], format: params[:format]
+ 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
diff --git a/lib/api/v3/runners.rb b/lib/api/v3/runners.rb
new file mode 100644
index 00000000000..1934d6e578c
--- /dev/null
+++ b/lib/api/v3/runners.rb
@@ -0,0 +1,65 @@
+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.is_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_authorized_runners.exists?(runner.id)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/services.rb b/lib/api/v3/services.rb
new file mode 100644
index 00000000000..3bacaeee032
--- /dev/null
+++ b/lib/api/v3/services.rb
@@ -0,0 +1,644 @@
+module API
+ module V3
+ class Services < Grape::API
+ services = {
+ 'asana' => [
+ {
+ required: true,
+ name: :api_key,
+ type: String,
+ desc: 'User API token'
+ },
+ {
+ required: false,
+ name: :restrict_to_branch,
+ type: String,
+ desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches'
+ }
+ ],
+ 'assembla' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The authentication token'
+ },
+ {
+ required: false,
+ name: :subdomain,
+ type: String,
+ desc: 'Subdomain setting'
+ }
+ ],
+ 'bamboo' => [
+ {
+ required: true,
+ name: :bamboo_url,
+ type: String,
+ desc: 'Bamboo root URL like https://bamboo.example.com'
+ },
+ {
+ required: true,
+ name: :build_key,
+ type: String,
+ desc: 'Bamboo build plan key like'
+ },
+ {
+ required: true,
+ name: :username,
+ type: String,
+ desc: 'A user with API access, if applicable'
+ },
+ {
+ required: true,
+ name: :password,
+ type: String,
+ desc: 'Passord of the user'
+ }
+ ],
+ 'bugzilla' => [
+ {
+ required: true,
+ name: :new_issue_url,
+ type: String,
+ desc: 'New issue URL'
+ },
+ {
+ required: true,
+ name: :issues_url,
+ type: String,
+ desc: 'Issues URL'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'Project URL'
+ },
+ {
+ required: false,
+ name: :description,
+ type: String,
+ desc: 'Description'
+ },
+ {
+ required: false,
+ name: :title,
+ type: String,
+ desc: 'Title'
+ }
+ ],
+ 'buildkite' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Buildkite project GitLab token'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'The buildkite project URL'
+ },
+ {
+ required: false,
+ name: :enable_ssl_verification,
+ type: Boolean,
+ desc: 'Enable SSL verification for communication'
+ }
+ ],
+ 'builds-email' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Comma-separated list of recipient email addresses'
+ },
+ {
+ required: false,
+ name: :add_pusher,
+ type: Boolean,
+ desc: 'Add pusher to recipients list'
+ },
+ {
+ required: false,
+ name: :notify_only_broken_builds,
+ type: Boolean,
+ desc: 'Notify only broken builds'
+ }
+ ],
+ 'campfire' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Campfire token'
+ },
+ {
+ required: false,
+ name: :subdomain,
+ type: String,
+ desc: 'Campfire subdomain'
+ },
+ {
+ required: false,
+ name: :room,
+ type: String,
+ desc: 'Campfire room'
+ }
+ ],
+ 'custom-issue-tracker' => [
+ {
+ required: true,
+ name: :new_issue_url,
+ type: String,
+ desc: 'New issue URL'
+ },
+ {
+ required: true,
+ name: :issues_url,
+ type: String,
+ desc: 'Issues URL'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'Project URL'
+ },
+ {
+ required: false,
+ name: :description,
+ type: String,
+ desc: 'Description'
+ },
+ {
+ required: false,
+ name: :title,
+ type: String,
+ desc: 'Title'
+ }
+ ],
+ 'drone-ci' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Drone CI token'
+ },
+ {
+ required: true,
+ name: :drone_url,
+ type: String,
+ desc: 'Drone CI URL'
+ },
+ {
+ required: false,
+ name: :enable_ssl_verification,
+ type: Boolean,
+ desc: 'Enable SSL verification for communication'
+ }
+ ],
+ 'emails-on-push' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Comma-separated list of recipient email addresses'
+ },
+ {
+ required: false,
+ name: :disable_diffs,
+ type: Boolean,
+ desc: 'Disable code diffs'
+ },
+ {
+ required: false,
+ name: :send_from_committer_email,
+ type: Boolean,
+ desc: 'Send from committer'
+ }
+ ],
+ 'external-wiki' => [
+ {
+ required: true,
+ name: :external_wiki_url,
+ type: String,
+ desc: 'The URL of the external Wiki'
+ }
+ ],
+ 'flowdock' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Flowdock token'
+ }
+ ],
+ 'gemnasium' => [
+ {
+ required: true,
+ name: :api_key,
+ type: String,
+ desc: 'Your personal API key on gemnasium.com'
+ },
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: "The project's slug on gemnasium.com"
+ }
+ ],
+ 'hipchat' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The room token'
+ },
+ {
+ required: false,
+ name: :room,
+ type: String,
+ desc: 'The room name or ID'
+ },
+ {
+ required: false,
+ name: :color,
+ type: String,
+ desc: 'The room color'
+ },
+ {
+ required: false,
+ name: :notify,
+ type: Boolean,
+ desc: 'Enable notifications'
+ },
+ {
+ required: false,
+ name: :api_version,
+ type: String,
+ desc: 'Leave blank for default (v2)'
+ },
+ {
+ required: false,
+ name: :server,
+ type: String,
+ desc: 'Leave blank for default. https://hipchat.example.com'
+ }
+ ],
+ 'irker' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Recipients/channels separated by whitespaces'
+ },
+ {
+ required: false,
+ name: :default_irc_uri,
+ type: String,
+ desc: 'Default: irc://irc.network.net:6697'
+ },
+ {
+ required: false,
+ name: :server_host,
+ type: String,
+ desc: 'Server host. Default localhost'
+ },
+ {
+ required: false,
+ name: :server_port,
+ type: Integer,
+ desc: 'Server port. Default 6659'
+ },
+ {
+ required: false,
+ name: :colorize_messages,
+ type: Boolean,
+ desc: 'Colorize messages'
+ }
+ ],
+ 'jira' => [
+ {
+ required: true,
+ name: :url,
+ type: String,
+ desc: 'The URL to the JIRA project which is being linked to this GitLab project, e.g., https://jira.example.com'
+ },
+ {
+ required: true,
+ name: :project_key,
+ type: String,
+ desc: 'The short identifier for your JIRA project, all uppercase, e.g., PROJ'
+ },
+ {
+ required: false,
+ name: :username,
+ type: String,
+ desc: 'The username of the user created to be used with GitLab/JIRA'
+ },
+ {
+ required: false,
+ name: :password,
+ type: String,
+ desc: 'The password of the user created to be used with GitLab/JIRA'
+ },
+ {
+ required: false,
+ name: :jira_issue_transition_id,
+ type: Integer,
+ desc: 'The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`'
+ }
+ ],
+
+ 'kubernetes' => [
+ {
+ required: true,
+ name: :namespace,
+ type: String,
+ desc: 'The Kubernetes namespace to use'
+ },
+ {
+ required: true,
+ name: :api_url,
+ type: String,
+ desc: 'The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com'
+ },
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The service token to authenticate against the Kubernetes cluster with'
+ },
+ {
+ required: false,
+ name: :ca_pem,
+ type: String,
+ desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)'
+ },
+ ],
+ 'mattermost-slash-commands' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Mattermost token'
+ }
+ ],
+ 'slack-slash-commands' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Slack token'
+ }
+ ],
+ 'pipelines-email' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Comma-separated list of recipient email addresses'
+ },
+ {
+ required: false,
+ name: :notify_only_broken_builds,
+ type: Boolean,
+ desc: 'Notify only broken builds'
+ }
+ ],
+ 'pivotaltracker' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Pivotaltracker token'
+ },
+ {
+ required: false,
+ name: :restrict_to_branch,
+ type: String,
+ desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.'
+ }
+ ],
+ 'pushover' => [
+ {
+ required: true,
+ name: :api_key,
+ type: String,
+ desc: 'The application key'
+ },
+ {
+ required: true,
+ name: :user_key,
+ type: String,
+ desc: 'The user key'
+ },
+ {
+ required: true,
+ name: :priority,
+ type: String,
+ desc: 'The priority'
+ },
+ {
+ required: true,
+ name: :device,
+ type: String,
+ desc: 'Leave blank for all active devices'
+ },
+ {
+ required: true,
+ name: :sound,
+ type: String,
+ desc: 'The sound of the notification'
+ }
+ ],
+ 'redmine' => [
+ {
+ required: true,
+ name: :new_issue_url,
+ type: String,
+ desc: 'The new issue URL'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'The project URL'
+ },
+ {
+ required: true,
+ name: :issues_url,
+ type: String,
+ desc: 'The issues URL'
+ },
+ {
+ required: false,
+ name: :description,
+ type: String,
+ desc: 'The description of the tracker'
+ }
+ ],
+ 'slack' => [
+ {
+ required: true,
+ name: :webhook,
+ type: String,
+ desc: 'The Slack webhook. e.g. https://hooks.slack.com/services/...'
+ },
+ {
+ required: false,
+ name: :new_issue_url,
+ type: String,
+ desc: 'The user name'
+ },
+ {
+ required: false,
+ name: :channel,
+ type: String,
+ desc: 'The channel name'
+ }
+ ],
+ 'mattermost' => [
+ {
+ required: true,
+ name: :webhook,
+ type: String,
+ desc: 'The Mattermost webhook. e.g. http://mattermost_host/hooks/...'
+ }
+ ],
+ 'teamcity' => [
+ {
+ required: true,
+ name: :teamcity_url,
+ type: String,
+ desc: 'TeamCity root URL like https://teamcity.example.com'
+ },
+ {
+ required: true,
+ name: :build_type,
+ type: String,
+ desc: 'Build configuration ID'
+ },
+ {
+ required: true,
+ name: :username,
+ type: String,
+ desc: 'A user with permissions to trigger a manual build'
+ },
+ {
+ required: true,
+ name: :password,
+ type: String,
+ desc: 'The password of the user'
+ }
+ ]
+ }
+
+ trigger_services = {
+ 'mattermost-slash-commands' => [
+ {
+ name: :token,
+ type: String,
+ desc: 'The Mattermost token'
+ }
+ ],
+ 'slack-slash-commands' => [
+ {
+ name: :token,
+ type: String,
+ desc: 'The Slack token'
+ }
+ ]
+ }.freeze
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ before { authenticate! }
+ before { authorize_admin_project }
+
+ helpers do
+ def service_attributes(service)
+ service.fields.inject([]) do |arr, hash|
+ arr << hash[:name].to_sym
+ end
+ end
+ end
+
+ desc "Delete a service for project"
+ params do
+ requires :service_slug, type: String, values: services.keys, desc: 'The name of the service'
+ end
+ delete ":id/services/:service_slug" do
+ service = user_project.find_or_initialize_service(params[:service_slug].underscore)
+
+ attrs = service_attributes(service).inject({}) do |hash, key|
+ hash.merge!(key => nil)
+ end
+
+ if service.update_attributes(attrs.merge(active: false))
+ status(200)
+ true
+ else
+ render_api_error!('400 Bad Request', 400)
+ end
+ end
+
+ desc 'Get the service settings for project' do
+ success Entities::ProjectService
+ end
+ params do
+ requires :service_slug, type: String, values: services.keys, desc: 'The name of the service'
+ end
+ get ":id/services/:service_slug" do
+ service = user_project.find_or_initialize_service(params[:service_slug].underscore)
+ present service, with: Entities::ProjectService, include_passwords: current_user.is_admin?
+ end
+ end
+
+ trigger_services.each do |service_slug, settings|
+ helpers do
+ def chat_command_service(project, service_slug, params)
+ project.services.active.where(template: false).find do |service|
+ service.try(:token) == params[:token] && service.to_param == service_slug.underscore
+ end
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ desc "Trigger a slash command for #{service_slug}" do
+ detail 'Added in GitLab 8.13'
+ end
+ params do
+ settings.each do |setting|
+ requires setting[:name], type: setting[:type], desc: setting[:desc]
+ end
+ end
+ post ":id/services/#{service_slug.underscore}/trigger" do
+ project = find_project(params[:id])
+
+ # This is not accurate, but done to prevent leakage of the project names
+ not_found!('Service') unless project
+
+ service = chat_command_service(project, service_slug, params)
+ result = service.try(:trigger, params)
+
+ if result
+ status result[:status] || 200
+ present result
+ else
+ not_found!('Service')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/settings.rb b/lib/api/v3/settings.rb
new file mode 100644
index 00000000000..748d6b97d4f
--- /dev/null
+++ b/lib/api/v3/settings.rb
@@ -0,0 +1,137 @@
+module API
+ module V3
+ class Settings < Grape::API
+ before { authenticated_as_admin! }
+
+ helpers do
+ def current_settings
+ @current_setting ||=
+ (ApplicationSetting.current || ApplicationSetting.create_from_defaults)
+ end
+ end
+
+ desc 'Get the current application settings' do
+ success Entities::ApplicationSetting
+ end
+ get "application/settings" do
+ present current_settings, with: Entities::ApplicationSetting
+ end
+
+ desc 'Modify application settings' do
+ success Entities::ApplicationSetting
+ end
+ params do
+ optional :default_branch_protection, type: Integer, values: [0, 1, 2], desc: 'Determine if developers can push to master'
+ optional :default_project_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default project visibility'
+ optional :default_snippet_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default snippet visibility'
+ optional :default_group_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default group visibility'
+ optional :restricted_visibility_levels, type: Array[String], desc: 'Selected levels cannot be used by non-admin users for projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.'
+ optional :import_sources, type: Array[String], values: %w[github bitbucket gitlab google_code fogbugz git gitlab_project],
+ desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com'
+ optional :disabled_oauth_sign_in_sources, type: Array[String], desc: 'Disable certain OAuth sign-in sources'
+ optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.'
+ optional :gravatar_enabled, type: Boolean, desc: 'Flag indicating if the Gravatar service is enabled'
+ optional :default_projects_limit, type: Integer, desc: 'The maximum number of personal projects'
+ optional :max_attachment_size, type: Integer, desc: 'Maximum attachment size in MB'
+ optional :session_expire_delay, type: Integer, desc: 'Session duration in minutes. GitLab restart is required to apply changes.'
+ optional :user_oauth_applications, type: Boolean, desc: 'Allow users to register any application to use GitLab as an OAuth provider'
+ optional :user_default_external, type: Boolean, desc: 'Newly registered users will by default be external'
+ optional :signup_enabled, type: Boolean, desc: 'Flag indicating if sign up is enabled'
+ optional :send_user_confirmation_email, type: Boolean, desc: 'Send confirmation email on sign-up'
+ optional :domain_whitelist, type: String, desc: 'ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
+ optional :domain_blacklist_enabled, type: Boolean, desc: 'Enable domain blacklist for sign ups'
+ given domain_blacklist_enabled: ->(val) { val } do
+ requires :domain_blacklist, type: String, desc: 'Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
+ end
+ optional :after_sign_up_text, type: String, desc: 'Text shown after sign up'
+ optional :signin_enabled, type: Boolean, desc: 'Flag indicating if sign in is enabled'
+ optional :require_two_factor_authentication, type: Boolean, desc: 'Require all users to setup Two-factor authentication'
+ given require_two_factor_authentication: ->(val) { val } do
+ requires :two_factor_grace_period, type: Integer, desc: 'Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication'
+ end
+ optional :home_page_url, type: String, desc: 'We will redirect non-logged in users to this page'
+ optional :after_sign_out_path, type: String, desc: 'We will redirect users to this page after they sign out'
+ optional :sign_in_text, type: String, desc: 'The sign in text of the GitLab application'
+ optional :help_page_text, type: String, desc: 'Custom text displayed on the help page'
+ optional :shared_runners_enabled, type: Boolean, desc: 'Enable shared runners for new projects'
+ given shared_runners_enabled: ->(val) { val } do
+ requires :shared_runners_text, type: String, desc: 'Shared runners text '
+ end
+ optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size each build's artifacts can have"
+ optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB'
+ optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)'
+ optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics'
+ given metrics_enabled: ->(val) { val } do
+ requires :metrics_host, type: String, desc: 'The InfluxDB host'
+ requires :metrics_port, type: Integer, desc: 'The UDP port to use for connecting to InfluxDB'
+ requires :metrics_pool_size, type: Integer, desc: 'The amount of InfluxDB connections to open'
+ requires :metrics_timeout, type: Integer, desc: 'The amount of seconds after which an InfluxDB connection will time out'
+ requires :metrics_method_call_threshold, type: Integer, desc: 'A method call is only tracked when it takes longer to complete than the given amount of milliseconds.'
+ requires :metrics_sample_interval, type: Integer, desc: 'The sampling interval in seconds'
+ requires :metrics_packet_size, type: Integer, desc: 'The amount of points to store in a single UDP packet'
+ end
+ optional :sidekiq_throttling_enabled, type: Boolean, desc: 'Enable Sidekiq Job Throttling'
+ given sidekiq_throttling_enabled: ->(val) { val } do
+ requires :sidekiq_throttling_queus, type: Array[String], desc: 'Choose which queues you wish to throttle'
+ requires :sidekiq_throttling_factor, type: Float, desc: 'The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.'
+ end
+ optional :recaptcha_enabled, type: Boolean, desc: 'Helps prevent bots from creating accounts'
+ given recaptcha_enabled: ->(val) { val } do
+ requires :recaptcha_site_key, type: String, desc: 'Generate site key at http://www.google.com/recaptcha'
+ requires :recaptcha_private_key, type: String, desc: 'Generate private key at http://www.google.com/recaptcha'
+ end
+ optional :akismet_enabled, type: Boolean, desc: 'Helps prevent bots from creating issues'
+ given akismet_enabled: ->(val) { val } do
+ requires :akismet_api_key, type: String, desc: 'Generate API key at http://www.akismet.com'
+ end
+ optional :admin_notification_email, type: String, desc: 'Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.'
+ optional :sentry_enabled, type: Boolean, desc: 'Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here: https://getsentry.com'
+ given sentry_enabled: ->(val) { val } do
+ requires :sentry_dsn, type: String, desc: 'Sentry Data Source Name'
+ end
+ optional :repository_storage, type: String, desc: 'Storage paths for new projects'
+ optional :repository_checks_enabled, type: Boolean, desc: "GitLab will periodically run 'git fsck' in all project and wiki repositories to look for silent disk corruption issues."
+ optional :koding_enabled, type: Boolean, desc: 'Enable Koding'
+ given koding_enabled: ->(val) { val } do
+ requires :koding_url, type: String, desc: 'The Koding team URL'
+ end
+ optional :plantuml_enabled, type: Boolean, desc: 'Enable PlantUML'
+ given plantuml_enabled: ->(val) { val } do
+ requires :plantuml_url, type: String, desc: 'The PlantUML server URL'
+ end
+ optional :version_check_enabled, type: Boolean, desc: 'Let GitLab inform you when an update is available.'
+ optional :email_author_in_body, type: Boolean, desc: 'Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead.'
+ optional :html_emails_enabled, type: Boolean, desc: 'By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format.'
+ optional :housekeeping_enabled, type: Boolean, desc: 'Enable automatic repository housekeeping (git repack, git gc)'
+ given housekeeping_enabled: ->(val) { val } do
+ requires :housekeeping_bitmaps_enabled, type: Boolean, desc: "Creating pack file bitmaps makes housekeeping take a little longer but bitmaps should accelerate 'git clone' performance."
+ requires :housekeeping_incremental_repack_period, type: Integer, desc: "Number of Git pushes after which an incremental 'git repack' is run."
+ requires :housekeeping_full_repack_period, type: Integer, desc: "Number of Git pushes after which a full 'git repack' is run."
+ requires :housekeeping_gc_period, type: Integer, desc: "Number of Git pushes after which 'git gc' is run."
+ end
+ optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.'
+ at_least_one_of :default_branch_protection, :default_project_visibility, :default_snippet_visibility,
+ :default_group_visibility, :restricted_visibility_levels, :import_sources,
+ :enabled_git_access_protocol, :gravatar_enabled, :default_projects_limit,
+ :max_attachment_size, :session_expire_delay, :disabled_oauth_sign_in_sources,
+ :user_oauth_applications, :user_default_external, :signup_enabled,
+ :send_user_confirmation_email, :domain_whitelist, :domain_blacklist_enabled,
+ :after_sign_up_text, :signin_enabled, :require_two_factor_authentication,
+ :home_page_url, :after_sign_out_path, :sign_in_text, :help_page_text,
+ :shared_runners_enabled, :max_artifacts_size, :max_pages_size, :container_registry_token_expire_delay,
+ :metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled,
+ :akismet_enabled, :admin_notification_email, :sentry_enabled,
+ :repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled,
+ :version_check_enabled, :email_author_in_body, :html_emails_enabled,
+ :housekeeping_enabled, :terminal_max_session_time
+ end
+ put "application/settings" do
+ if current_settings.update_attributes(declared_params(include_missing: false))
+ present current_settings, with: Entities::ApplicationSetting
+ else
+ render_validation_error!(current_settings)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/snippets.rb b/lib/api/v3/snippets.rb
new file mode 100644
index 00000000000..07dac7e9904
--- /dev/null
+++ b/lib/api/v3/snippets.rb
@@ -0,0 +1,138 @@
+module API
+ module V3
+ class Snippets < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ resource :snippets do
+ helpers do
+ def snippets_for_current_user
+ SnippetsFinder.new.execute(current_user, filter: :by_user, user: current_user)
+ end
+
+ def public_snippets
+ SnippetsFinder.new.execute(current_user, filter: :public)
+ end
+ end
+
+ desc 'Get a snippets list for authenticated user' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success ::API::Entities::PersonalSnippet
+ end
+ params do
+ use :pagination
+ end
+ get do
+ present paginate(snippets_for_current_user), with: ::API::Entities::PersonalSnippet
+ end
+
+ desc 'List all public snippets current_user has access to' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success ::API::Entities::PersonalSnippet
+ end
+ params do
+ use :pagination
+ end
+ get 'public' do
+ present paginate(public_snippets), with: ::API::Entities::PersonalSnippet
+ end
+
+ desc 'Get a single snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success ::API::Entities::PersonalSnippet
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a snippet'
+ end
+ get ':id' do
+ snippet = snippets_for_current_user.find(params[:id])
+ present snippet, with: ::API::Entities::PersonalSnippet
+ end
+
+ desc 'Create new snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success ::API::Entities::PersonalSnippet
+ end
+ params do
+ requires :title, type: String, desc: 'The title of a snippet'
+ requires :file_name, type: String, desc: 'The name of a snippet file'
+ requires :content, type: String, desc: 'The content of a snippet'
+ optional :visibility_level, type: Integer,
+ values: Gitlab::VisibilityLevel.values,
+ default: Gitlab::VisibilityLevel::INTERNAL,
+ desc: 'The visibility level of the snippet'
+ end
+ post do
+ attrs = declared_params(include_missing: false).merge(request: request, api: true)
+ snippet = CreateSnippetService.new(nil, current_user, attrs).execute
+
+ if snippet.persisted?
+ present snippet, with: ::API::Entities::PersonalSnippet
+ else
+ render_validation_error!(snippet)
+ end
+ end
+
+ desc 'Update an existing snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success ::API::Entities::PersonalSnippet
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a snippet'
+ optional :title, type: String, desc: 'The title of a snippet'
+ optional :file_name, type: String, desc: 'The name of a snippet file'
+ optional :content, type: String, desc: 'The content of a snippet'
+ optional :visibility_level, type: Integer,
+ values: Gitlab::VisibilityLevel.values,
+ desc: 'The visibility level of the snippet'
+ at_least_one_of :title, :file_name, :content, :visibility_level
+ end
+ put ':id' do
+ snippet = snippets_for_current_user.find_by(id: params.delete(:id))
+ return not_found!('Snippet') unless snippet
+ authorize! :update_personal_snippet, snippet
+
+ attrs = declared_params(include_missing: false)
+
+ UpdateSnippetService.new(nil, current_user, snippet, attrs).execute
+ if snippet.persisted?
+ present snippet, with: ::API::Entities::PersonalSnippet
+ else
+ render_validation_error!(snippet)
+ end
+ end
+
+ desc 'Remove snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success ::API::Entities::PersonalSnippet
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a snippet'
+ end
+ delete ':id' do
+ snippet = snippets_for_current_user.find_by(id: params.delete(:id))
+ return not_found!('Snippet') unless snippet
+ authorize! :destroy_personal_snippet, snippet
+ snippet.destroy
+ no_content!
+ end
+
+ desc 'Get a raw snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a snippet'
+ end
+ get ":id/raw" do
+ snippet = snippets_for_current_user.find_by(id: params.delete(:id))
+ return not_found!('Snippet') unless snippet
+
+ env['api.format'] = :txt
+ content_type 'text/plain'
+ present snippet.content
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/subscriptions.rb b/lib/api/v3/subscriptions.rb
new file mode 100644
index 00000000000..068750ec077
--- /dev/null
+++ b/lib/api/v3/subscriptions.rb
@@ -0,0 +1,53 @@
+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
diff --git a/lib/api/v3/system_hooks.rb b/lib/api/v3/system_hooks.rb
new file mode 100644
index 00000000000..5787c06fc12
--- /dev/null
+++ b/lib/api/v3/system_hooks.rb
@@ -0,0 +1,32 @@
+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
diff --git a/lib/api/v3/tags.rb b/lib/api/v3/tags.rb
new file mode 100644
index 00000000000..c2541de2f50
--- /dev/null
+++ b/lib/api/v3/tags.rb
@@ -0,0 +1,40 @@
+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::RepoTag
+ end
+ get ":id/repository/tags" do
+ tags = user_project.repository.tags.sort_by(&:name).reverse
+ present tags, with: ::API::Entities::RepoTag, 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
diff --git a/lib/api/v3/templates.rb b/lib/api/v3/templates.rb
new file mode 100644
index 00000000000..4c577a8d2b7
--- /dev/null
+++ b/lib/api/v3/templates.rb
@@ -0,0 +1,122 @@
+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 =
+ /[\<\{\[]
+ (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 =
+ /[\<\{\[]
+ (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::RepoLicense
+ 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::RepoLicense
+ 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::RepoLicense
+ 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::RepoLicense
+ 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
diff --git a/lib/api/v3/time_tracking_endpoints.rb b/lib/api/v3/time_tracking_endpoints.rb
new file mode 100644
index 00000000000..81ae4e8137d
--- /dev/null
+++ b/lib/api/v3/time_tracking_endpoints.rb
@@ -0,0 +1,116 @@
+module API
+ module V3
+ module TimeTrackingEndpoints
+ extend ActiveSupport::Concern
+
+ included do
+ helpers do
+ def issuable_name
+ declared_params.has_key?(:issue_id) ? 'issue' : 'merge_request'
+ end
+
+ def issuable_key
+ "#{issuable_name}_id".to_sym
+ end
+
+ def update_issuable_key
+ "update_#{issuable_name}".to_sym
+ end
+
+ def read_issuable_key
+ "read_#{issuable_name}".to_sym
+ end
+
+ def load_issuable
+ @issuable ||= begin
+ case issuable_name
+ when 'issue'
+ find_project_issue(params.delete(issuable_key))
+ when 'merge_request'
+ find_project_merge_request(params.delete(issuable_key))
+ end
+ end
+ end
+
+ def update_issuable(attrs)
+ custom_params = declared_params(include_missing: false)
+ custom_params.merge!(attrs)
+
+ issuable = update_service.new(user_project, current_user, custom_params).execute(load_issuable)
+ if issuable.valid?
+ present issuable, with: ::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: current_user
+ })
+ end
+
+ desc "Reset spent time for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ end
+ post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_spent_time" do
+ authorize! update_issuable_key, load_issuable
+
+ status :ok
+ update_issuable(spend_time: { duration: :reset, user: current_user })
+ end
+
+ desc "Show time stats for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ end
+ get ":id/#{issuable_collection_name}/:#{issuable_key}/time_stats" do
+ authorize! read_issuable_key, load_issuable
+
+ present load_issuable, with: ::API::Entities::IssuableTimeStats
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/todos.rb b/lib/api/v3/todos.rb
new file mode 100644
index 00000000000..e3b311d61cd
--- /dev/null
+++ b/lib/api/v3/todos.rb
@@ -0,0 +1,30 @@
+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
+ todo = current_user.todos.find(params[:id])
+ TodoService.new.mark_todos_as_done([todo], current_user)
+
+ present todo.reload, 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
diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb
new file mode 100644
index 00000000000..a23d6b6b48c
--- /dev/null
+++ b/lib/api/v3/triggers.rb
@@ -0,0 +1,103 @@
+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
+ project = find_project(params[:id])
+ trigger = Ci::Trigger.find_by_token(params[:token].to_s)
+ not_found! unless project && trigger
+ unauthorized! unless trigger.project == project
+
+ # validate variables
+ variables = params[:variables].to_h
+ unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
+ render_api_error!('variables needs to be a map of key-valued strings', 400)
+ end
+
+ # create request and trigger builds
+ trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables)
+ if trigger_request
+ present trigger_request, with: ::API::V3::Entities::TriggerRequest
+ else
+ errors = 'No builds created'
+ render_api_error!(errors, 400)
+ end
+ end
+
+ desc 'Get triggers list' do
+ success ::API::V3::Entities::Trigger
+ end
+ params do
+ use :pagination
+ end
+ get ':id/triggers' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ triggers = user_project.triggers.includes(:trigger_requests)
+
+ present paginate(triggers), with: ::API::V3::Entities::Trigger
+ end
+
+ desc 'Get specific trigger of a project' do
+ success ::API::V3::Entities::Trigger
+ end
+ params do
+ requires :token, type: String, desc: 'The unique token of trigger'
+ end
+ get ':id/triggers/:token' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ trigger = user_project.triggers.find_by(token: params[:token].to_s)
+ return not_found!('Trigger') unless trigger
+
+ present trigger, with: ::API::V3::Entities::Trigger
+ end
+
+ desc 'Create a trigger' do
+ success ::API::V3::Entities::Trigger
+ end
+ post ':id/triggers' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ trigger = user_project.triggers.create
+
+ present trigger, with: ::API::V3::Entities::Trigger
+ end
+
+ desc 'Delete a trigger' do
+ success ::API::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)
+ return not_found!('Trigger') unless trigger
+
+ trigger.destroy
+
+ present trigger, with: ::API::V3::Entities::Trigger
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/users.rb b/lib/api/v3/users.rb
new file mode 100644
index 00000000000..5e18cecc431
--- /dev/null
+++ b/lib/api/v3/users.rb
@@ -0,0 +1,202 @@
+module API
+ module V3
+ class Users < Grape::API
+ include PaginationParams
+
+ before do
+ allow_access_with_scope :read_user if request.get?
+ authenticate!
+ end
+
+ resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
+ helpers do
+ 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.execute(current_user)).
+ 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
diff --git a/lib/api/v3/variables.rb b/lib/api/v3/variables.rb
new file mode 100644
index 00000000000..83972b1e7ce
--- /dev/null
+++ b/lib/api/v3/variables.rb
@@ -0,0 +1,29 @@
+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
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
index f623b1dfe9f..5acde41551b 100644
--- a/lib/api/variables.rb
+++ b/lib/api/variables.rb
@@ -1,5 +1,4 @@
module API
- # Projects variables API
class Variables < Grape::API
include PaginationParams
@@ -10,7 +9,7 @@ module API
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get project variables' do
success Entities::Variable
end
@@ -81,10 +80,9 @@ module API
end
delete ':id/variables/:key' do
variable = user_project.variables.find_by(key: params[:key])
+ not_found!('Variable') unless variable
- return not_found!('Variable') unless variable
-
- present variable.destroy, with: Entities::Variable
+ variable.destroy
end
end
end
diff --git a/lib/backup/database.rb b/lib/backup/database.rb
index 22319ec6623..4016ac76348 100644
--- a/lib/backup/database.rb
+++ b/lib/backup/database.rb
@@ -5,7 +5,7 @@ module Backup
attr_reader :config, :db_file_name
def initialize
- @config = YAML.load_file(File.join(Rails.root,'config','database.yml'))[Rails.env]
+ @config = YAML.load_file(File.join(Rails.root, 'config', 'database.yml'))[Rails.env]
@db_file_name = File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz')
end
@@ -13,28 +13,32 @@ module Backup
FileUtils.mkdir_p(File.dirname(db_file_name))
FileUtils.rm_f(db_file_name)
compress_rd, compress_wr = IO.pipe
- compress_pid = spawn(*%W(gzip -1 -c), in: compress_rd, out: [db_file_name, 'w', 0600])
+ compress_pid = spawn(*%w(gzip -1 -c), in: compress_rd, out: [db_file_name, 'w', 0600])
compress_rd.close
- dump_pid = case config["adapter"]
- when /^mysql/ then
- $progress.print "Dumping MySQL database #{config['database']} ... "
- # Workaround warnings from MySQL 5.6 about passwords on cmd line
- ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
- spawn('mysqldump', *mysql_args, config['database'], out: compress_wr)
- when "postgresql" then
- $progress.print "Dumping PostgreSQL database #{config['database']} ... "
- pg_env
- pgsql_args = ["--clean"] # Pass '--clean' to include 'DROP TABLE' statements in the DB dump.
- if Gitlab.config.backup.pg_schema
- pgsql_args << "-n"
- pgsql_args << Gitlab.config.backup.pg_schema
+ dump_pid =
+ case config["adapter"]
+ when /^mysql/ then
+ $progress.print "Dumping MySQL database #{config['database']} ... "
+ # Workaround warnings from MySQL 5.6 about passwords on cmd line
+ ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
+ spawn('mysqldump', *mysql_args, config['database'], out: compress_wr)
+ when "postgresql" then
+ $progress.print "Dumping PostgreSQL database #{config['database']} ... "
+ pg_env
+ pgsql_args = ["--clean"] # Pass '--clean' to include 'DROP TABLE' statements in the DB dump.
+ if Gitlab.config.backup.pg_schema
+ pgsql_args << "-n"
+ pgsql_args << Gitlab.config.backup.pg_schema
+ end
+ spawn('pg_dump', *pgsql_args, config['database'], out: compress_wr)
end
- spawn('pg_dump', *pgsql_args, config['database'], out: compress_wr)
- end
compress_wr.close
- success = [compress_pid, dump_pid].all? { |pid| Process.waitpid(pid); $?.success? }
+ success = [compress_pid, dump_pid].all? do |pid|
+ Process.waitpid(pid)
+ $?.success?
+ end
report_success(success)
abort 'Backup failed' unless success
@@ -42,23 +46,27 @@ module Backup
def restore
decompress_rd, decompress_wr = IO.pipe
- decompress_pid = spawn(*%W(gzip -cd), out: decompress_wr, in: db_file_name)
+ decompress_pid = spawn(*%w(gzip -cd), out: decompress_wr, in: db_file_name)
decompress_wr.close
- restore_pid = case config["adapter"]
- when /^mysql/ then
- $progress.print "Restoring MySQL database #{config['database']} ... "
- # Workaround warnings from MySQL 5.6 about passwords on cmd line
- ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
- spawn('mysql', *mysql_args, config['database'], in: decompress_rd)
- when "postgresql" then
- $progress.print "Restoring PostgreSQL database #{config['database']} ... "
- pg_env
- spawn('psql', config['database'], in: decompress_rd)
- end
+ restore_pid =
+ case config["adapter"]
+ when /^mysql/ then
+ $progress.print "Restoring MySQL database #{config['database']} ... "
+ # Workaround warnings from MySQL 5.6 about passwords on cmd line
+ ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
+ spawn('mysql', *mysql_args, config['database'], in: decompress_rd)
+ when "postgresql" then
+ $progress.print "Restoring PostgreSQL database #{config['database']} ... "
+ pg_env
+ spawn('psql', config['database'], in: decompress_rd)
+ end
decompress_rd.close
- success = [decompress_pid, restore_pid].all? { |pid| Process.waitpid(pid); $?.success? }
+ success = [decompress_pid, restore_pid].all? do |pid|
+ Process.waitpid(pid)
+ $?.success?
+ end
report_success(success)
abort 'Restore failed' unless success
diff --git a/lib/backup/files.rb b/lib/backup/files.rb
index cedbb289f6a..30a91647b77 100644
--- a/lib/backup/files.rb
+++ b/lib/backup/files.rb
@@ -8,6 +8,7 @@ module Backup
@name = name
@app_files_dir = File.realpath(app_files_dir)
@files_parent_dir = File.realpath(File.join(@app_files_dir, '..'))
+ @backup_files_dir = File.join(Gitlab.config.backup.path, File.basename(@app_files_dir) )
@backup_tarball = File.join(Gitlab.config.backup.path, name + '.tar.gz')
end
@@ -15,14 +16,28 @@ module Backup
def dump
FileUtils.mkdir_p(Gitlab.config.backup.path)
FileUtils.rm_f(backup_tarball)
- run_pipeline!([%W(tar -C #{app_files_dir} -cf - .), %W(gzip -c -1)], out: [backup_tarball, 'w', 0600])
+
+ if ENV['STRATEGY'] == 'copy'
+ cmd = %W(cp -a #{app_files_dir} #{Gitlab.config.backup.path})
+ output, status = Gitlab::Popen.popen(cmd)
+
+ unless status.zero?
+ puts output
+ abort 'Backup failed'
+ end
+
+ run_pipeline!([%W(tar -C #{@backup_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600])
+ FileUtils.rm_rf(@backup_files_dir)
+ else
+ run_pipeline!([%W(tar -C #{app_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600])
+ end
end
def restore
backup_existing_files_dir
create_files_dir
- run_pipeline!([%W(gzip -cd), %W(tar -C #{app_files_dir} -xf -)], in: backup_tarball)
+ run_pipeline!([%w(gzip -cd), %W(tar -C #{app_files_dir} -xf -)], in: backup_tarball)
end
def backup_existing_files_dir
@@ -32,7 +47,7 @@ module Backup
end
end
- def run_pipeline!(cmd_list, options={})
+ def run_pipeline!(cmd_list, options = {})
status_list = Open3.pipeline(*cmd_list, options)
abort 'Backup failed' unless status_list.compact.all?(&:success?)
end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index cefbfdce3bb..7b4476fa4db 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -1,8 +1,8 @@
module Backup
class Manager
- ARCHIVES_TO_BACKUP = %w[uploads builds artifacts lfs registry]
- FOLDERS_TO_BACKUP = %w[repositories db]
- FILE_NAME_SUFFIX = '_gitlab_backup.tar'
+ ARCHIVES_TO_BACKUP = %w[uploads builds artifacts pages lfs registry].freeze
+ FOLDERS_TO_BACKUP = %w[repositories db].freeze
+ FILE_NAME_SUFFIX = '_gitlab_backup.tar'.freeze
def pack
# Make sure there is a connection
@@ -20,13 +20,13 @@ module Backup
Dir.chdir(Gitlab.config.backup.path) do
File.open("#{Gitlab.config.backup.path}/backup_information.yml",
"w+") do |file|
- file << s.to_yaml.gsub(/^---\n/,'')
+ file << s.to_yaml.gsub(/^---\n/, '')
end
# create archive
$progress.print "Creating backup archive: #{tar_file} ... "
# Set file permissions on open to prevent chmod races.
- tar_system_options = {out: [tar_file, 'w', Gitlab.config.backup.archive_permissions]}
+ tar_system_options = { out: [tar_file, 'w', Gitlab.config.backup.archive_permissions] }
if Kernel.system('tar', '-cf', '-', *backup_contents, tar_system_options)
$progress.puts "done".color(:green)
else
@@ -50,8 +50,9 @@ module Backup
directory = connect_to_remote_directory(connection_settings)
if directory.files.create(key: tar_file, body: File.open(tar_file), public: false,
- multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size,
- encryption: Gitlab.config.backup.upload.encryption)
+ multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size,
+ encryption: Gitlab.config.backup.upload.encryption,
+ storage_class: Gitlab.config.backup.upload.storage_class)
$progress.puts "done".color(:green)
else
puts "uploading backup to #{remote_directory} failed".color(:red)
@@ -123,11 +124,11 @@ module Backup
exit 1
end
- if ENV['BACKUP'].present?
- tar_file = "#{ENV['BACKUP']}#{FILE_NAME_SUFFIX}"
- else
- tar_file = file_list.first
- end
+ tar_file = if ENV['BACKUP'].present?
+ "#{ENV['BACKUP']}#{FILE_NAME_SUFFIX}"
+ else
+ file_list.first
+ end
unless File.exist?(tar_file)
$progress.puts "The backup file #{tar_file} does not exist!"
@@ -158,7 +159,7 @@ module Backup
end
def tar_version
- tar_version, _ = Gitlab::Popen.popen(%W(tar --version))
+ tar_version, _ = Gitlab::Popen.popen(%w(tar --version))
tar_version.force_encoding('locale').split("\n").first
end
diff --git a/lib/backup/pages.rb b/lib/backup/pages.rb
new file mode 100644
index 00000000000..215ded93bfe
--- /dev/null
+++ b/lib/backup/pages.rb
@@ -0,0 +1,13 @@
+require 'backup/files'
+
+module Backup
+ class Pages < Files
+ def initialize
+ super('pages', Gitlab.config.pages.path)
+ end
+
+ def create_files_dir
+ Dir.mkdir(app_files_dir, 0700)
+ end
+ end
+end
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index d746070913d..6b29600a751 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -2,7 +2,7 @@ require 'yaml'
module Backup
class Repository
-
+ # rubocop:disable Metrics/AbcSize
def dump
prepare
@@ -12,7 +12,7 @@ module Backup
path_to_project_bundle = path_to_bundle(project)
# Create namespace dir if missing
- FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.path)) if project.namespace
+ FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.full_path)) if project.namespace
if project.empty_repo?
$progress.puts "[SKIPPED]".color(:cyan)
@@ -68,7 +68,8 @@ module Backup
end
def restore
- Gitlab.config.repositories.storages.each do |name, path|
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ path = repository_storage['path']
next unless File.exist?(path)
# Move repos dir to 'repositories.old' dir
@@ -85,11 +86,11 @@ module Backup
project.ensure_dir_exist
- if File.exists?(path_to_project_bundle)
- cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_project_bundle} #{path_to_project_repo})
- else
- cmd = %W(#{Gitlab.config.git.bin_path} init --bare #{path_to_project_repo})
- end
+ cmd = if File.exist?(path_to_project_bundle)
+ %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_project_bundle} #{path_to_project_repo})
+ else
+ %W(#{Gitlab.config.git.bin_path} init --bare #{path_to_project_repo})
+ end
output, status = Gitlab::Popen.popen(cmd)
if status.zero?
@@ -150,6 +151,7 @@ module Backup
puts output
end
end
+ # rubocop:enable Metrics/AbcSize
protected
@@ -179,8 +181,9 @@ module Backup
return unless Dir.exist?(path)
dir_entries = Dir.entries(path)
- %w[annex custom_hooks].each do |entry|
- yield(entry) if dir_entries.include?(entry)
+
+ if dir_entries.include?('custom_hooks') || dir_entries.include?('custom_hooks.tar')
+ yield('custom_hooks')
end
end
@@ -193,13 +196,13 @@ module Backup
end
def silent
- {err: '/dev/null', out: '/dev/null'}
+ { err: '/dev/null', out: '/dev/null' }
end
private
def repository_storage_paths_args
- Gitlab.config.repositories.storages.values
+ Gitlab.config.repositories.storages.values.map { |rs| rs['path'] }
end
end
end
diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb
index 9261f77f3c9..35118375499 100644
--- a/lib/backup/uploads.rb
+++ b/lib/backup/uploads.rb
@@ -2,7 +2,6 @@ require 'backup/files'
module Backup
class Uploads < Files
-
def initialize
super('uploads', Rails.root.join('public/uploads'))
end
diff --git a/lib/banzai/cross_project_reference.rb b/lib/banzai/cross_project_reference.rb
index 0257848b6bc..e2b57adf611 100644
--- a/lib/banzai/cross_project_reference.rb
+++ b/lib/banzai/cross_project_reference.rb
@@ -14,7 +14,7 @@ module Banzai
def project_from_ref(ref)
return context[:project] unless ref
- Project.find_with_namespace(ref)
+ Project.find_by_full_path(ref)
end
end
end
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index 6d04f68c8f9..8bc2dd18bda 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -33,7 +33,12 @@ module Banzai
# Returns a String replaced with the return of the block.
def self.references_in(text, pattern = object_class.reference_pattern)
text.gsub(pattern) do |match|
- yield match, $~[object_sym].to_i, $~[:project], $~[:namespace], $~
+ symbol = $~[object_sym]
+ if object_class.reference_valid?(symbol)
+ yield match, symbol.to_i, $~[:project], $~[:namespace], $~
+ else
+ match
+ end
end
end
@@ -153,13 +158,14 @@ module Banzai
title = object_link_title(object)
klass = reference_class(object_sym)
- data = data_attributes_for(link_content || match, project, object)
+ data = data_attributes_for(link_content || match, project, object, link: !!link_content)
- if matches.names.include?("url") && matches[:url]
- url = matches[:url]
- else
- url = url_for_object_cached(object, project)
- end
+ url =
+ if matches.names.include?("url") && matches[:url]
+ matches[:url]
+ else
+ url_for_object_cached(object, project)
+ end
content = link_content || object_link_text(object, matches)
@@ -172,9 +178,10 @@ module Banzai
end
end
- def data_attributes_for(text, project, object)
+ def data_attributes_for(text, project, object, link: false)
data_attribute(
original: text,
+ link: link,
project: project.id,
object_sym => object.id
)
@@ -232,18 +239,13 @@ module Banzai
# path.
def projects_per_reference
@projects_per_reference ||= begin
- hash = {}
refs = Set.new
references_per_project.each do |project_ref, _|
refs << project_ref
end
- find_projects_for_paths(refs.to_a).each do |project|
- hash[project.path_with_namespace] = project
- end
-
- hash
+ find_projects_for_paths(refs.to_a).index_by(&:full_path)
end
end
@@ -284,7 +286,7 @@ module Banzai
end
def current_project_namespace_path
- @current_project_namespace_path ||= project.namespace.path
+ @current_project_namespace_path ||= project.namespace.full_path
end
private
diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb
index 80c844baecd..b8d2673c1a6 100644
--- a/lib/banzai/filter/autolink_filter.rb
+++ b/lib/banzai/filter/autolink_filter.rb
@@ -37,7 +37,7 @@ module Banzai
and contains(., '://')
and not(starts-with(., 'http'))
and not(starts-with(., 'ftp'))
- ])
+ ]).freeze
def call
return doc if context[:autolink] == false
diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb
index a8c1ca0c60a..d6138816e70 100644
--- a/lib/banzai/filter/emoji_filter.rb
+++ b/lib/banzai/filter/emoji_filter.rb
@@ -17,8 +17,8 @@ module Banzai
next unless content.include?(':') || node.text.match(emoji_unicode_pattern)
- html = emoji_name_image_filter(content)
- html = emoji_unicode_image_filter(html)
+ html = emoji_unicode_element_unicode_filter(content)
+ html = emoji_name_element_unicode_filter(html)
next if html == content
@@ -27,33 +27,30 @@ module Banzai
doc
end
- # Replace :emoji: with corresponding images.
+ # Replace :emoji: with corresponding gl-emoji unicode.
#
# text - String text to replace :emoji: in.
#
- # Returns a String with :emoji: replaced with images.
- def emoji_name_image_filter(text)
+ # Returns a String with :emoji: replaced with gl-emoji unicode.
+ def emoji_name_element_unicode_filter(text)
text.gsub(emoji_pattern) do |match|
name = $1
- emoji_image_tag(name, emoji_url(name))
+ Gitlab::Emoji.gl_emoji_tag(name)
end
end
- # Replace unicode emoji with corresponding images if they exist.
+ # Replace unicode emoji with corresponding gl-emoji unicode.
#
# text - String text to replace unicode emoji in.
#
- # Returns a String with unicode emoji replaced with images.
- def emoji_unicode_image_filter(text)
+ # Returns a String with unicode emoji replaced with gl-emoji unicode.
+ def emoji_unicode_element_unicode_filter(text)
text.gsub(emoji_unicode_pattern) do |moji|
- emoji_image_tag(Gitlab::Emoji.emojis_by_moji[moji]['name'], emoji_unicode_url(moji))
+ emoji_info = Gitlab::Emoji.emojis_by_moji[moji]
+ Gitlab::Emoji.gl_emoji_tag(emoji_info['name'])
end
end
- def emoji_image_tag(emoji_name, emoji_url)
- "<img class='emoji' title=':#{emoji_name}:' alt=':#{emoji_name}:' src='#{emoji_url}' height='20' width='20' align='absmiddle' />"
- end
-
# Build a regexp that matches all valid :emoji: names.
def self.emoji_pattern
@emoji_pattern ||= /:(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/
@@ -66,52 +63,13 @@ module Banzai
private
- def emoji_url(name)
- emoji_path = emoji_filename(name)
-
- if context[:asset_host]
- # Asset host is specified.
- url_to_image(emoji_path)
- elsif context[:asset_root]
- # Gitlab url is specified
- File.join(context[:asset_root], url_to_image(emoji_path))
- else
- # All other cases
- url_to_image(emoji_path)
- end
- end
-
- def emoji_unicode_url(moji)
- emoji_unicode_path = emoji_unicode_filename(moji)
-
- if context[:asset_host]
- url_to_image(emoji_unicode_path)
- elsif context[:asset_root]
- File.join(context[:asset_root], url_to_image(emoji_unicode_path))
- else
- url_to_image(emoji_unicode_path)
- end
- end
-
- def url_to_image(image)
- ActionController::Base.helpers.url_to_image(image)
- end
-
def emoji_pattern
self.class.emoji_pattern
end
- def emoji_filename(name)
- "#{Gitlab::Emoji.emoji_filename(name)}.png"
- end
-
def emoji_unicode_pattern
self.class.emoji_unicode_pattern
end
-
- def emoji_unicode_filename(name)
- "#{Gitlab::Emoji.emoji_unicode_filename(name)}.png"
- end
end
end
end
diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb
index d08267a9d6c..0ea4eeaed5b 100644
--- a/lib/banzai/filter/gollum_tags_filter.rb
+++ b/lib/banzai/filter/gollum_tags_filter.rb
@@ -149,11 +149,12 @@ module Banzai
name, reference = *parts.compact.map(&:strip)
end
- if url?(reference)
- href = reference
- else
- href = ::File.join(project_wiki_base_path, reference)
- end
+ href =
+ if url?(reference)
+ reference
+ else
+ ::File.join(project_wiki_base_path, reference)
+ end
content_tag(:a, name || reference, href: href, class: 'gfm')
end
diff --git a/lib/banzai/filter/image_link_filter.rb b/lib/banzai/filter/image_link_filter.rb
index f0fb6084a35..123c92fd250 100644
--- a/lib/banzai/filter/image_link_filter.rb
+++ b/lib/banzai/filter/image_link_filter.rb
@@ -2,29 +2,22 @@ module Banzai
module Filter
# HTML filter that wraps links around inline images.
class ImageLinkFilter < HTML::Pipeline::Filter
-
# Find every image that isn't already wrapped in an `a` tag, create
# a new node (a link to the image source), copy the image as a child
# of the anchor, and then replace the img with the link-wrapped version.
def call
doc.xpath('descendant-or-self::img[not(ancestor::a)]').each do |img|
- div = doc.document.create_element(
- 'div',
- class: 'image-container'
- )
-
link = doc.document.create_element(
'a',
class: 'no-attachment-icon',
href: img['src'],
- target: '_blank'
+ target: '_blank',
+ rel: 'noopener noreferrer'
)
link.children = img.clone
- div.children = link
-
- img.replace(div)
+ img.replace(link)
end
doc
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
index 4d1bc687696..044d18ff824 100644
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ b/lib/banzai/filter/issue_reference_filter.rb
@@ -39,11 +39,12 @@ module Banzai
projects_per_reference.each do |path, project|
issue_ids = references_per_project[path]
- if project.default_issues_tracker?
- issues = project.issues.where(iid: issue_ids.to_a)
- else
- issues = issue_ids.map { |id| ExternalIssue.new(id, project) }
- end
+ issues =
+ if project.default_issues_tracker?
+ project.issues.where(iid: issue_ids.to_a)
+ else
+ issue_ids.map { |id| ExternalIssue.new(id, project) }
+ end
issues.each do |issue|
hash[project][issue.iid.to_i] = issue
@@ -62,7 +63,7 @@ module Banzai
end
end
- def data_attributes_for(text, project, object)
+ def data_attributes_for(text, project, object, link: false)
if object.is_a?(ExternalIssue)
data_attribute(
project: project.id,
diff --git a/lib/banzai/filter/merge_request_reference_filter.rb b/lib/banzai/filter/merge_request_reference_filter.rb
index ac5216d9cfb..3888acf935e 100644
--- a/lib/banzai/filter/merge_request_reference_filter.rb
+++ b/lib/banzai/filter/merge_request_reference_filter.rb
@@ -11,8 +11,8 @@ module Banzai
MergeRequest
end
- def find_object(project, id)
- project.merge_requests.find_by(iid: id)
+ def find_object(project, iid)
+ merge_requests_per_project[project][iid]
end
def url_for_object(mr, project)
@@ -21,6 +21,31 @@ module Banzai
only_path: context[:only_path])
end
+ def project_from_ref(ref)
+ projects_per_reference[ref || current_project_path]
+ end
+
+ # Returns a Hash containing the merge_requests per Project instance.
+ def merge_requests_per_project
+ @merge_requests_per_project ||= begin
+ hash = Hash.new { |h, k| h[k] = {} }
+
+ projects_per_reference.each do |path, project|
+ merge_request_ids = references_per_project[path]
+
+ merge_requests = project.merge_requests
+ .where(iid: merge_request_ids.to_a)
+ .includes(target_project: :namespace)
+
+ merge_requests.each do |merge_request|
+ hash[project][merge_request.iid.to_i] = merge_request
+ end
+ end
+
+ hash
+ end
+ end
+
def object_link_text_extras(object, matches)
extras = super
diff --git a/lib/banzai/filter/plantuml_filter.rb b/lib/banzai/filter/plantuml_filter.rb
new file mode 100644
index 00000000000..b2537117558
--- /dev/null
+++ b/lib/banzai/filter/plantuml_filter.rb
@@ -0,0 +1,39 @@
+require "nokogiri"
+require "asciidoctor-plantuml/plantuml"
+
+module Banzai
+ module Filter
+ # HTML that replaces all `code plantuml` tags with PlantUML img tags.
+ #
+ class PlantumlFilter < HTML::Pipeline::Filter
+ def call
+ return doc unless doc.at('pre.plantuml') && settings.plantuml_enabled
+
+ plantuml_setup
+
+ doc.css('pre.plantuml').each do |el|
+ img_tag = Nokogiri::HTML::DocumentFragment.parse(
+ Asciidoctor::PlantUml::Processor.plantuml_content(el.content, {}))
+ el.replace img_tag
+ end
+
+ doc
+ end
+
+ private
+
+ def settings
+ ApplicationSetting.current || ApplicationSetting.create_from_defaults
+ end
+
+ def plantuml_setup
+ Asciidoctor::PlantUml.configure do |conf|
+ conf.url = settings.plantuml_url
+ conf.png_enable = settings.plantuml_enabled
+ conf.svg_enable = false
+ conf.txt_enable = false
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index ab7af1cad21..6640168bfa2 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -53,6 +53,10 @@ module Banzai
context[:project]
end
+ def skip_project_check?
+ context[:skip_project_check]
+ end
+
def reference_class(type)
"gfm gfm-#{type} has-tooltip"
end
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index af1e575fc89..d5f9e252f62 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -35,6 +35,10 @@ module Banzai
# Allow span elements
whitelist[:elements].push('span')
+ # Allow html5 details/summary elements
+ whitelist[:elements].push('details')
+ whitelist[:elements].push('summary')
+
# Allow abbr elements with title attribute
whitelist[:elements].push('abbr')
whitelist[:attributes]['abbr'] = %w(title)
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index 026b81ac175..9f09ca90697 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -5,8 +5,6 @@ module Banzai
# HTML Filter to highlight fenced code blocks
#
class SyntaxHighlightFilter < HTML::Pipeline::Filter
- include Rouge::Plugins::Redcarpet
-
def call
doc.search('pre > code').each do |node|
highlight_node(node)
@@ -20,17 +18,19 @@ module Banzai
code = node.text
css_classes = "code highlight"
lexer = lexer_for(language)
+ lang = lexer.tag
begin
- code = format(lex(lexer, code))
+ code = Rouge::Formatters::HTMLGitlab.format(lex(lexer, code), tag: lang)
- css_classes << " js-syntax-highlight #{lexer.tag}"
+ css_classes << " js-syntax-highlight #{lang}"
rescue
+ lang = nil
# Gracefully handle syntax highlighter bugs/errors to ensure
# users can still access an issue/comment/etc.
end
- highlighted = %(<pre class="#{css_classes}" v-pre="true"><code>#{code}</code></pre>)
+ highlighted = %(<pre class="#{css_classes}" lang="#{lang}" v-pre="true"><code>#{code}</code></pre>)
# Extracted to a method to measure it
replace_parent_pre_element(node, highlighted)
@@ -43,10 +43,6 @@ module Banzai
lexer.lex(code)
end
- def format(tokens)
- rouge_formatter.format(tokens)
- end
-
def lexer_for(language)
(Rouge::Lexer.find(language) || Rouge::Lexers::PlainText).new
end
@@ -55,11 +51,6 @@ module Banzai
# Replace the parent `pre` element with the entire highlighted block
node.parent.replace(highlighted)
end
-
- # Override Rouge::Plugins::Redcarpet#rouge_formatter
- def rouge_formatter(lexer = nil)
- @rouge_formatter ||= Rouge::Formatters::HTML.new
- end
end
end
end
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index f842b1fb779..a798927823f 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -24,7 +24,7 @@ module Banzai
end
def call
- return doc if project.nil?
+ return doc if project.nil? && !skip_project_check?
ref_pattern = User.reference_pattern
ref_pattern_start = /\A#{ref_pattern}\z/
@@ -58,9 +58,9 @@ module Banzai
# have `gfm` and `gfm-project_member` class names attached for styling.
def user_link_filter(text, link_content: nil)
self.class.references_in(text) do |match, username|
- if username == 'all'
+ if username == 'all' && !skip_project_check?
link_to_all(link_content: link_content)
- elsif namespace = namespaces[username]
+ elsif namespace = namespaces[username.downcase]
link_to_namespace(namespace, link_content: link_content) || match
else
match
@@ -74,10 +74,7 @@ module Banzai
# The keys of this Hash are the namespace paths, the values the
# corresponding Namespace objects.
def namespaces
- @namespaces ||=
- Namespace.where(path: usernames).each_with_object({}) do |row, hash|
- hash[row.path] = row
- end
+ @namespaces ||= Namespace.where_full_path_in(usernames).index_by(&:full_path).transform_keys(&:downcase)
end
# Returns all usernames referenced in the current document.
@@ -122,7 +119,7 @@ module Banzai
def link_to_namespace(namespace, link_content: nil)
if namespace.is_a?(Group)
- link_to_group(namespace.path, namespace, link_content: link_content)
+ link_to_group(namespace.full_path, namespace, link_content: link_content)
else
link_to_user(namespace.path, namespace, link_content: link_content)
end
@@ -133,7 +130,7 @@ module Banzai
data = data_attribute(group: namespace.id)
content = link_content || Group.reference_prefix + group
- link_tag(url, data, content, namespace.name)
+ link_tag(url, data, content, namespace.full_name)
end
def link_to_user(user, namespace, link_content: nil)
diff --git a/lib/banzai/filter/video_link_filter.rb b/lib/banzai/filter/video_link_filter.rb
index ac7bbcb0d10..35cb10eae5d 100644
--- a/lib/banzai/filter/video_link_filter.rb
+++ b/lib/banzai/filter/video_link_filter.rb
@@ -35,13 +35,15 @@ module Banzai
src: element['src'],
width: '400',
controls: true,
- 'data-setup' => '{}')
+ 'data-setup' => '{}',
+ 'data-title' => element['title'] || element['alt'])
link = doc.document.create_element(
'a',
element['title'] || element['alt'],
href: element['src'],
target: '_blank',
+ rel: 'noopener noreferrer',
title: "Download '#{element['title'] || element['alt']}'")
download_paragraph = doc.document.create_element('p')
download_paragraph.children = link
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 5a1f873496c..fd4a6a107c2 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -1,9 +1,16 @@
module Banzai
module Pipeline
class GfmPipeline < BasePipeline
+ # These filters convert GitLab Flavored Markdown (GFM) to HTML.
+ # The handlers defined in app/assets/javascripts/copy_as_gfm.js
+ # consequently convert that same HTML to GFM to be copied to the clipboard.
+ # Every filter that generates HTML from GFM should have a handler in
+ # app/assets/javascripts/copy_as_gfm.js, in reverse order.
+ # The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
def self.filters
@filters ||= FilterArray[
Filter::SyntaxHighlightFilter,
+ Filter::PlantumlFilter,
Filter::SanitizationFilter,
Filter::MathFilter,
diff --git a/lib/banzai/querying.rb b/lib/banzai/querying.rb
index 1e1b51e683e..fb2faae02bc 100644
--- a/lib/banzai/querying.rb
+++ b/lib/banzai/querying.rb
@@ -1,18 +1,64 @@
module Banzai
module Querying
+ module_function
+
# Searches a Nokogiri document using a CSS query, optionally optimizing it
# whenever possible.
#
- # document - A document/element to search.
- # query - The CSS query to use.
+ # document - A document/element to search.
+ # query - The CSS query to use.
+ # reference_options - A hash with nodes filter options
#
- # Returns a Nokogiri::XML::NodeSet.
- def self.css(document, query)
+ # Returns an array of Nokogiri::XML::Element objects if location is specified
+ # in reference_options. Otherwise it would a Nokogiri::XML::NodeSet.
+ def css(document, query, reference_options = {})
# When using "a.foo" Nokogiri compiles this to "//a[...]" but
# "descendant::a[...]" is quite a bit faster and achieves the same result.
xpath = Nokogiri::CSS.xpath_for(query)[0].gsub(%r{^//}, 'descendant::')
+ xpath = restrict_to_p_nodes_at_root(xpath) if filter_nodes_at_beginning?(reference_options)
+ nodes = document.xpath(xpath)
+
+ filter_nodes(nodes, reference_options)
+ end
+
+ def restrict_to_p_nodes_at_root(xpath)
+ xpath.gsub('descendant::', './p/')
+ end
+
+ def filter_nodes(nodes, reference_options)
+ if filter_nodes_at_beginning?(reference_options)
+ filter_nodes_at_beginning(nodes)
+ else
+ nodes
+ end
+ end
+
+ def filter_nodes_at_beginning?(reference_options)
+ reference_options && reference_options[:location] == :beginning
+ end
+
+ # Selects child nodes if they are present in the beginning among other siblings.
+ #
+ # nodes - A Nokogiri::XML::NodeSet.
+ #
+ # Returns an array of Nokogiri::XML::Element objects.
+ def filter_nodes_at_beginning(nodes)
+ parents_and_nodes = nodes.group_by(&:parent)
+ filtered_nodes = []
+
+ parents_and_nodes.each do |parent, nodes|
+ children = parent.children
+ nodes = nodes.to_a
+
+ children.each do |child|
+ next if child.text.blank?
+ node = nodes.shift
+ break unless node == child
+ filtered_nodes << node
+ end
+ end
- document.xpath(xpath)
+ filtered_nodes
end
end
end
diff --git a/lib/banzai/reference_extractor.rb b/lib/banzai/reference_extractor.rb
index b26a41a1f3b..8e3b0c4db79 100644
--- a/lib/banzai/reference_extractor.rb
+++ b/lib/banzai/reference_extractor.rb
@@ -16,6 +16,11 @@ module Banzai
processor.process(html_documents)
end
+ def reset_memoized_values
+ @html_documents = nil
+ @texts_and_contexts = []
+ end
+
private
def html_documents
diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb
index d8a855ec1fe..52fdb9a2140 100644
--- a/lib/banzai/reference_parser/base_parser.rb
+++ b/lib/banzai/reference_parser/base_parser.rb
@@ -33,7 +33,7 @@ module Banzai
# they have access to.
class BaseParser
class << self
- attr_accessor :reference_type
+ attr_accessor :reference_type, :reference_options
end
# Returns the attribute name containing the value for every object to be
@@ -134,9 +134,7 @@ module Banzai
ids = unique_attribute_values(nodes, attribute)
rows = collection_objects_for_ids(collection, ids)
- rows.each_with_object({}) do |row, hash|
- hash[row.id] = row
- end
+ rows.index_by(&:id)
end
# Returns an Array containing all unique values of an attribute of the
@@ -182,9 +180,10 @@ module Banzai
# the references.
def process(documents)
type = self.class.reference_type
+ reference_options = self.class.reference_options
nodes = documents.flat_map do |document|
- Querying.css(document, "a[data-reference-type='#{type}'].gfm").to_a
+ Querying.css(document, "a[data-reference-type='#{type}'].gfm", reference_options).to_a
end
gather_references(nodes)
@@ -209,7 +208,7 @@ module Banzai
grouped_objects_for_nodes(nodes, Project, 'data-project')
end
- def can?(user, permission, subject)
+ def can?(user, permission, subject = :global)
Ability.allowed?(user, permission, subject)
end
diff --git a/lib/banzai/reference_parser/directly_addressed_user_parser.rb b/lib/banzai/reference_parser/directly_addressed_user_parser.rb
new file mode 100644
index 00000000000..77df9bbd024
--- /dev/null
+++ b/lib/banzai/reference_parser/directly_addressed_user_parser.rb
@@ -0,0 +1,8 @@
+module Banzai
+ module ReferenceParser
+ class DirectlyAddressedUserParser < UserParser
+ self.reference_type = :user
+ self.reference_options = { location: :beginning }
+ end
+ end
+end
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
index f31fb6c3f71..74663556cbb 100644
--- a/lib/banzai/renderer.rb
+++ b/lib/banzai/renderer.rb
@@ -52,9 +52,9 @@ module Banzai
end
# Same as +render_field+, but without consulting or updating the cache field
- def cacheless_render_field(object, field)
+ def cacheless_render_field(object, field, options = {})
text = object.__send__(field)
- context = object.banzai_render_context(field)
+ context = object.banzai_render_context(field).merge(options)
cacheless_render(text, context)
end
diff --git a/lib/bitbucket/connection.rb b/lib/bitbucket/connection.rb
index 7e55cf4deab..b9279c33f5b 100644
--- a/lib/bitbucket/connection.rb
+++ b/lib/bitbucket/connection.rb
@@ -1,8 +1,8 @@
module Bitbucket
class Connection
- DEFAULT_API_VERSION = '2.0'
- DEFAULT_BASE_URI = 'https://api.bitbucket.org/'
- DEFAULT_QUERY = {}
+ DEFAULT_API_VERSION = '2.0'.freeze
+ DEFAULT_BASE_URI = 'https://api.bitbucket.org/'.freeze
+ DEFAULT_QUERY = {}.freeze
attr_reader :expires_at, :expires_in, :refresh_token, :token
@@ -24,9 +24,7 @@ module Bitbucket
response.parsed
end
- def expired?
- connection.expired?
- end
+ delegate :expired?, to: :connection
def refresh!
response = connection.refresh!
diff --git a/lib/bitbucket/error/unauthorized.rb b/lib/bitbucket/error/unauthorized.rb
index 5e2eb57bb0e..efe10542f19 100644
--- a/lib/bitbucket/error/unauthorized.rb
+++ b/lib/bitbucket/error/unauthorized.rb
@@ -1,6 +1,5 @@
module Bitbucket
module Error
- class Unauthorized < StandardError
- end
+ Unauthorized = Class.new(StandardError)
end
end
diff --git a/lib/bitbucket/representation/repo.rb b/lib/bitbucket/representation/repo.rb
index 423eff8f2a5..59b0fda8e14 100644
--- a/lib/bitbucket/representation/repo.rb
+++ b/lib/bitbucket/representation/repo.rb
@@ -23,7 +23,7 @@ module Bitbucket
url = raw['links']['clone'].find { |link| link['name'] == 'https' }.fetch('href')
if token.present?
- clone_url = URI::parse(url)
+ clone_url = URI.parse(url)
clone_url.user = "x-token-auth:#{token}"
clone_url.to_s
else
diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb
index c10d3616f31..b3ccad7b28d 100644
--- a/lib/ci/ansi2html.rb
+++ b/lib/ci/ansi2html.rb
@@ -13,7 +13,7 @@ module Ci
5 => 'magenta',
6 => 'cyan',
7 => 'white', # not that this is gray in the dark (aka default) color table
- }
+ }.freeze
STYLE_SWITCHES = {
bold: 0x01,
@@ -21,7 +21,7 @@ module Ci
underline: 0x04,
conceal: 0x08,
cross: 0x10,
- }
+ }.freeze
def self.convert(ansi, state = nil)
Converter.new.convert(ansi, state)
@@ -29,64 +29,108 @@ module Ci
class Converter
def on_0(s) reset() end
+
def on_1(s) enable(STYLE_SWITCHES[:bold]) end
+
def on_3(s) enable(STYLE_SWITCHES[:italic]) end
+
def on_4(s) enable(STYLE_SWITCHES[:underline]) end
+
def on_8(s) enable(STYLE_SWITCHES[:conceal]) end
+
def on_9(s) enable(STYLE_SWITCHES[:cross]) end
def on_21(s) disable(STYLE_SWITCHES[:bold]) end
+
def on_22(s) disable(STYLE_SWITCHES[:bold]) end
+
def on_23(s) disable(STYLE_SWITCHES[:italic]) end
+
def on_24(s) disable(STYLE_SWITCHES[:underline]) end
+
def on_28(s) disable(STYLE_SWITCHES[:conceal]) end
+
def on_29(s) disable(STYLE_SWITCHES[:cross]) end
def on_30(s) set_fg_color(0) end
+
def on_31(s) set_fg_color(1) end
+
def on_32(s) set_fg_color(2) end
+
def on_33(s) set_fg_color(3) end
+
def on_34(s) set_fg_color(4) end
+
def on_35(s) set_fg_color(5) end
+
def on_36(s) set_fg_color(6) end
+
def on_37(s) set_fg_color(7) end
+
def on_38(s) set_fg_color_256(s) end
+
def on_39(s) set_fg_color(9) end
def on_40(s) set_bg_color(0) end
+
def on_41(s) set_bg_color(1) end
+
def on_42(s) set_bg_color(2) end
+
def on_43(s) set_bg_color(3) end
+
def on_44(s) set_bg_color(4) end
+
def on_45(s) set_bg_color(5) end
+
def on_46(s) set_bg_color(6) end
+
def on_47(s) set_bg_color(7) end
+
def on_48(s) set_bg_color_256(s) end
+
def on_49(s) set_bg_color(9) end
def on_90(s) set_fg_color(0, 'l') end
+
def on_91(s) set_fg_color(1, 'l') end
+
def on_92(s) set_fg_color(2, 'l') end
+
def on_93(s) set_fg_color(3, 'l') end
+
def on_94(s) set_fg_color(4, 'l') end
+
def on_95(s) set_fg_color(5, 'l') end
+
def on_96(s) set_fg_color(6, 'l') end
+
def on_97(s) set_fg_color(7, 'l') end
+
def on_99(s) set_fg_color(9, 'l') end
def on_100(s) set_bg_color(0, 'l') end
+
def on_101(s) set_bg_color(1, 'l') end
+
def on_102(s) set_bg_color(2, 'l') end
+
def on_103(s) set_bg_color(3, 'l') end
+
def on_104(s) set_bg_color(4, 'l') end
+
def on_105(s) set_bg_color(5, 'l') end
+
def on_106(s) set_bg_color(6, 'l') end
+
def on_107(s) set_bg_color(7, 'l') end
+
def on_109(s) set_bg_color(9, 'l') end
attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask
- STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask]
+ STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask].freeze
def convert(raw, new_state)
reset_state
@@ -126,7 +170,7 @@ module Ci
# We are only interested in color and text style changes - triggered by
# sequences starting with '\e[' and ending with 'm'. Any other control
# sequence gets stripped (including stuff like "delete last line")
- return unless indicator == '[' and terminator == 'm'
+ return unless indicator == '[' && terminator == 'm'
close_open_tags()
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index c4bdef781f7..746e76a1b1f 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -18,24 +18,31 @@ module Ci
if current_runner.is_runner_queue_value_latest?(params[:last_update])
header 'X-GitLab-Last-Update', params[:last_update]
+ Gitlab::Metrics.add_event(:build_not_found_cached)
return build_not_found!
end
new_update = current_runner.ensure_runner_queue_value
- build = Ci::RegisterBuildService.new.execute(current_runner)
+ result = Ci::RegisterJobService.new(current_runner).execute
- if build
- Gitlab::Metrics.add_event(:build_found,
- project: build.project.path_with_namespace)
+ if result.valid?
+ if result.build
+ Gitlab::Metrics.add_event(:build_found,
+ project: result.build.project.path_with_namespace)
- present build, with: Entities::BuildDetails
- else
- Gitlab::Metrics.add_event(:build_not_found)
+ present result.build, with: Entities::BuildDetails
+ else
+ Gitlab::Metrics.add_event(:build_not_found)
- header 'X-GitLab-Last-Update', new_update
+ header 'X-GitLab-Last-Update', new_update
- build_not_found!
+ build_not_found!
+ end
+ else
+ # We received build that is invalid due to concurrency conflict
+ Gitlab::Metrics.add_event(:build_invalid)
+ conflict!
end
end
@@ -160,7 +167,10 @@ module Ci
build.artifacts_file = artifacts
build.artifacts_metadata = metadata
- build.artifacts_expire_in = params['expire_in']
+ build.artifacts_expire_in =
+ params['expire_in'] ||
+ Gitlab::CurrentSettings.current_application_settings
+ .default_artifacts_expire_in
if build.save
present(build, with: Entities::BuildDetails)
@@ -207,6 +217,7 @@ module Ci
build = Ci::Build.find_by_id(params[:id])
authenticate_build!(build)
+ status(200)
build.erase_artifacts!
end
end
diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb
index 5ff25a3a9b2..996990b464f 100644
--- a/lib/ci/api/helpers.rb
+++ b/lib/ci/api/helpers.rb
@@ -1,7 +1,7 @@
module Ci
module API
module Helpers
- BUILD_TOKEN_HEADER = "HTTP_BUILD_TOKEN"
+ BUILD_TOKEN_HEADER = "HTTP_BUILD_TOKEN".freeze
BUILD_TOKEN_PARAM = :token
UPDATE_RUNNER_EVERY = 10 * 60
@@ -60,7 +60,7 @@ module Ci
end
def build_not_found!
- if headers['User-Agent'].to_s.match(/gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /)
+ if headers['User-Agent'].to_s =~ /gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /
no_content!
else
not_found!
@@ -73,7 +73,7 @@ module Ci
def get_runner_version_from_params
return unless params["info"].present?
- attributes_for_keys(["name", "version", "revision", "platform", "architecture"], params["info"])
+ attributes_for_keys(%w(name version revision platform architecture), params["info"])
end
def max_artifacts_size
diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb
index bcc82969eb3..45aa2adccf5 100644
--- a/lib/ci/api/runners.rb
+++ b/lib/ci/api/runners.rb
@@ -1,44 +1,38 @@
module Ci
module API
- # Runners API
class Runners < Grape::API
resource :runners do
- # Delete runner
- # Parameters:
- # token (required) - The unique token of runner
- #
- # Example Request:
- # GET /runners/delete
+ desc 'Delete a runner'
+ params do
+ requires :token, type: String, desc: 'The unique token of the runner'
+ end
delete "delete" do
- required_attributes! [:token]
authenticate_runner!
+
+ status(200)
Ci::Runner.find_by_token(params[:token]).destroy
end
- # Register a new runner
- #
- # Note: This is an "internal" API called when setting up
- # runners, so it is authenticated differently.
- #
- # Parameters:
- # token (required) - The unique token of runner
- #
- # Example Request:
- # POST /runners/register
+ desc 'Register a new runner' do
+ success Entities::Runner
+ end
+ params do
+ requires :token, type: String, desc: 'The unique token of the runner'
+ optional :description, type: String, desc: 'The description of the runner'
+ optional :tag_list, type: Array[String], desc: 'A list of tags the runner should run for'
+ optional :run_untagged, type: Boolean, desc: 'Flag if the runner should execute untagged jobs'
+ optional :locked, type: Boolean, desc: 'Lock this runner for this specific project'
+ end
post "register" do
- required_attributes! [:token]
-
- attributes = attributes_for_keys(
- [:description, :tag_list, :run_untagged, :locked]
- )
+ runner_params = declared(params, include_missing: false).except(:token)
runner =
if runner_registration_token_valid?
# Create shared runner. Requires admin access
- Ci::Runner.create(attributes.merge(is_shared: true))
+ Ci::Runner.create(runner_params.merge(is_shared: true))
elsif project = Project.find_by(runners_token: params[:token])
# Create a specific runner for project.
- project.runners.create(attributes)
+ project.runners.create(runner_params)
end
return forbidden! unless runner
diff --git a/lib/ci/api/triggers.rb b/lib/ci/api/triggers.rb
index 63b42113513..6e622601680 100644
--- a/lib/ci/api/triggers.rb
+++ b/lib/ci/api/triggers.rb
@@ -1,41 +1,30 @@
module Ci
module API
- # Build Trigger API
class Triggers < Grape::API
resource :projects do
- # Trigger a GitLab CI project build
- #
- # Parameters:
- # id (required) - The ID of a CI project
- # ref (required) - The name of project's branch or tag
- # token (required) - The uniq token of trigger
- # Example Request:
- # POST /projects/:id/ref/:ref/trigger
+ desc 'Trigger a GitLab CI project build' do
+ success Entities::TriggerRequest
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a CI project'
+ requires :ref, type: String, desc: "The name of project's branch or tag"
+ requires :token, type: String, desc: 'The unique token of the trigger'
+ optional :variables, type: Hash, desc: 'Optional build variables'
+ end
post ":id/refs/:ref/trigger" do
- required_attributes! [:token]
-
- project = Project.find_by(ci_id: params[:id].to_i)
- trigger = Ci::Trigger.find_by_token(params[:token].to_s)
+ project = Project.find_by(ci_id: params[:id])
+ trigger = Ci::Trigger.find_by_token(params[:token])
not_found! unless project && trigger
unauthorized! unless trigger.project == project
- # validate variables
- variables = params[:variables]
- if variables
- unless variables.is_a?(Hash)
- render_api_error!('variables needs to be a hash', 400)
- end
-
- unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
- render_api_error!('variables needs to be a map of key-valued strings', 400)
- end
-
- # convert variables from Mash to Hash
- variables = variables.to_h
+ # Validate variables
+ variables = params[:variables].to_h
+ unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
+ render_api_error!('variables needs to be a map of key-valued strings', 400)
end
# create request and trigger builds
- trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables)
+ trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref], variables)
if trigger_request
present trigger_request, with: Entities::TriggerRequest
else
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 7463bd719d5..15a461a16dd 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -1,6 +1,6 @@
module Ci
class GitlabCiYamlProcessor
- class ValidationError < StandardError; end
+ ValidationError = Class.new(StandardError)
include Gitlab::Ci::Config::Entry::LegacyValidationHelpers
@@ -58,9 +58,10 @@ module Ci
commands: job[:commands],
tag_list: job[:tags] || [],
name: job[:name].to_s,
- allow_failure: job[:allow_failure] || false,
+ allow_failure: job[:ignore],
when: job[:when] || 'on_success',
environment: job[:environment_name],
+ coverage_regex: job[:coverage],
yaml_variables: yaml_variables(name),
options: {
image: job[:image],
diff --git a/lib/constraints/project_url_constrainer.rb b/lib/constraints/project_url_constrainer.rb
index 730b05bed97..a10b4657d7d 100644
--- a/lib/constraints/project_url_constrainer.rb
+++ b/lib/constraints/project_url_constrainer.rb
@@ -8,6 +8,6 @@ class ProjectUrlConstrainer
return false
end
- Project.find_with_namespace(full_path).present?
+ Project.find_by_full_path(full_path).present?
end
end
diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb
index 2edddb84fc3..7f5f6d9ddb6 100644
--- a/lib/container_registry/client.rb
+++ b/lib/container_registry/client.rb
@@ -5,7 +5,7 @@ module ContainerRegistry
class Client
attr_accessor :uri
- MANIFEST_VERSION = 'application/vnd.docker.distribution.manifest.v2+json'
+ MANIFEST_VERSION = 'application/vnd.docker.distribution.manifest.v2+json'.freeze
# Taken from: FaradayMiddleware::FollowRedirects
REDIRECT_CODES = Set.new [301, 302, 303, 307]
diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb
index 82551f1f222..dd864eea3fa 100644
--- a/lib/extracts_path.rb
+++ b/lib/extracts_path.rb
@@ -2,7 +2,7 @@
# file path string when combined in a request parameter
module ExtractsPath
# Raised when given an invalid file path
- class InvalidPathError < StandardError; end
+ InvalidPathError = Class.new(StandardError)
# Given a string containing both a Git tree-ish, such as a branch or tag, and
# a filesystem path joined by forward slashes, attempts to separate the two.
@@ -42,7 +42,7 @@ module ExtractsPath
return pair unless @project
- if id.match(/^([[:alnum:]]{40})(.+)/)
+ if id =~ /^(\h{40})(.+)/
# If the ref appears to be a SHA, we're done, just split the string
pair = $~.captures
else
diff --git a/lib/file_size_validator.rb b/lib/file_size_validator.rb
index 440dd44ece7..eb19ab45ac3 100644
--- a/lib/file_size_validator.rb
+++ b/lib/file_size_validator.rb
@@ -32,9 +32,9 @@ class FileSizeValidator < ActiveModel::EachValidator
end
def validate_each(record, attribute, value)
- raise(ArgumentError, "A CarrierWave::Uploader::Base object was expected") unless value.kind_of? CarrierWave::Uploader::Base
+ raise(ArgumentError, "A CarrierWave::Uploader::Base object was expected") unless value.is_a? CarrierWave::Uploader::Base
- value = (options[:tokenizer] || DEFAULT_TOKENIZER).call(value) if value.kind_of?(String)
+ value = (options[:tokenizer] || DEFAULT_TOKENIZER).call(value) if value.is_a?(String)
CHECKS.each do |key, validity_check|
next unless check_value = options[key]
diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb
index 9b484a2ecfd..8c28009b9c6 100644
--- a/lib/gitlab/access.rb
+++ b/lib/gitlab/access.rb
@@ -5,7 +5,7 @@
#
module Gitlab
module Access
- class AccessDeniedError < StandardError; end
+ AccessDeniedError = Class.new(StandardError)
NO_ACCESS = 0
GUEST = 10
@@ -21,9 +21,7 @@ module Gitlab
PROTECTION_DEV_CAN_MERGE = 3
class << self
- def values
- options.values
- end
+ delegate :values, to: :options
def all_values
options_with_owner.values
diff --git a/lib/gitlab/allowable.rb b/lib/gitlab/allowable.rb
index f48abcc86d5..e4f7cad2b79 100644
--- a/lib/gitlab/allowable.rb
+++ b/lib/gitlab/allowable.rb
@@ -1,6 +1,6 @@
module Gitlab
module Allowable
- def can?(user, action, subject)
+ def can?(user, action, subject = :global)
Ability.allowed?(user, action, subject)
end
end
diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb
index 0618107e2c3..d575367d81a 100644
--- a/lib/gitlab/asciidoc.rb
+++ b/lib/gitlab/asciidoc.rb
@@ -36,6 +36,9 @@ module Gitlab
html = Banzai.post_process(html, context)
+ filter = Banzai::Filter::SanitizationFilter.new(html)
+ html = filter.call.to_s
+
html.html_safe
end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 8dda65c71ef..eee5601b0ed 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -1,41 +1,55 @@
module Gitlab
module Auth
- class MissingPersonalTokenError < StandardError; end
+ MissingPersonalTokenError = Class.new(StandardError)
- SCOPES = [:api, :read_user]
- DEFAULT_SCOPES = [:api]
- OPTIONAL_SCOPES = SCOPES - DEFAULT_SCOPES
+ # Scopes used for GitLab API access
+ API_SCOPES = [:api, :read_user].freeze
+
+ # Scopes used for OpenID Connect
+ OPENID_SCOPES = [:openid].freeze
+
+ # Default scopes for OAuth applications that don't define their own
+ DEFAULT_SCOPES = [:api].freeze
+
+ # Other available scopes
+ OPTIONAL_SCOPES = (API_SCOPES + OPENID_SCOPES - DEFAULT_SCOPES).freeze
class << self
def find_for_git_client(login, password, project:, ip:)
raise "Must provide an IP for rate limiting" if ip.nil?
+ # `user_with_password_for_git` should be the last check
+ # because it's the most expensive, especially when LDAP
+ # is enabled.
result =
service_request_check(login, password, project) ||
build_access_token_check(login, password) ||
- user_with_password_for_git(login, password) ||
- oauth_access_token_check(login, password) ||
lfs_token_check(login, password) ||
- personal_access_token_check(login, password) ||
+ oauth_access_token_check(login, password) ||
+ user_with_password_for_git(login, password) ||
+ personal_access_token_check(password) ||
Gitlab::Auth::Result.new
rate_limit!(ip, success: result.success?, login: login)
+ Gitlab::Auth::UniqueIpsLimiter.limit_user!(result.actor)
result
end
def find_with_user_password(login, password)
- user = User.by_login(login)
+ Gitlab::Auth::UniqueIpsLimiter.limit_user! do
+ user = User.by_login(login)
- # If no user is found, or it's an LDAP server, try LDAP.
- # LDAP users are only authenticated via LDAP
- if user.nil? || user.ldap_user?
- # Second chance - try LDAP authentication
- return nil unless Gitlab::LDAP::Config.enabled?
+ # If no user is found, or it's an LDAP server, try LDAP.
+ # LDAP users are only authenticated via LDAP
+ if user.nil? || user.ldap_user?
+ # Second chance - try LDAP authentication
+ return nil unless Gitlab::LDAP::Config.enabled?
- Gitlab::LDAP::Authentication.login(login, password)
- else
- user if user.valid_password?(password)
+ Gitlab::LDAP::Authentication.login(login, password)
+ else
+ user if user.active? && user.valid_password?(password)
+ end
end
end
@@ -99,14 +113,13 @@ module Gitlab
end
end
- def personal_access_token_check(login, password)
- if login && password
- token = PersonalAccessToken.active.find_by_token(password)
- validation = User.by_login(login)
+ def personal_access_token_check(password)
+ return unless password.present?
- if valid_personal_access_token?(token, validation)
- Gitlab::Auth::Result.new(validation, nil, :personal_token, full_authentication_abilities)
- end
+ token = PersonalAccessTokensFinder.new(state: 'active').find_by(token: password)
+
+ if token && valid_api_token?(token)
+ Gitlab::Auth::Result.new(token.user, nil, :personal_token, full_authentication_abilities)
end
end
@@ -114,10 +127,6 @@ module Gitlab
token && token.accessible? && valid_api_token?(token)
end
- def valid_personal_access_token?(token, user)
- token && token.user == user && valid_api_token?(token)
- end
-
def valid_api_token?(token)
AccessTokenValidationService.new(token).include_any_scope?(['api'])
end
@@ -143,7 +152,9 @@ module Gitlab
read_authentication_abilities
end
- Result.new(actor, nil, token_handler.type, authentication_abilities) if Devise.secure_compare(token_handler.token, password)
+ if Devise.secure_compare(token_handler.token, password)
+ Gitlab::Auth::Result.new(actor, nil, token_handler.type, authentication_abilities)
+ end
end
def build_access_token_check(login, password)
diff --git a/lib/gitlab/auth/too_many_ips.rb b/lib/gitlab/auth/too_many_ips.rb
new file mode 100644
index 00000000000..ed862791551
--- /dev/null
+++ b/lib/gitlab/auth/too_many_ips.rb
@@ -0,0 +1,17 @@
+module Gitlab
+ module Auth
+ class TooManyIps < StandardError
+ attr_reader :user_id, :ip, :unique_ips_count
+
+ def initialize(user_id, ip, unique_ips_count)
+ @user_id = user_id
+ @ip = ip
+ @unique_ips_count = unique_ips_count
+ end
+
+ def message
+ "User #{user_id} from IP: #{ip} tried logging from too many ips: #{unique_ips_count}"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/unique_ips_limiter.rb b/lib/gitlab/auth/unique_ips_limiter.rb
new file mode 100644
index 00000000000..bf2239ca150
--- /dev/null
+++ b/lib/gitlab/auth/unique_ips_limiter.rb
@@ -0,0 +1,43 @@
+module Gitlab
+ module Auth
+ class UniqueIpsLimiter
+ USER_UNIQUE_IPS_PREFIX = 'user_unique_ips'.freeze
+
+ class << self
+ def limit_user_id!(user_id)
+ if config.unique_ips_limit_enabled
+ ip = RequestContext.client_ip
+ unique_ips = update_and_return_ips_count(user_id, ip)
+
+ raise TooManyIps.new(user_id, ip, unique_ips) if unique_ips > config.unique_ips_limit_per_user
+ end
+ end
+
+ def limit_user!(user = nil)
+ user ||= yield if block_given?
+ limit_user_id!(user.id) unless user.nil?
+ user
+ end
+
+ def config
+ Gitlab::CurrentSettings.current_application_settings
+ end
+
+ def update_and_return_ips_count(user_id, ip)
+ time = Time.now.utc.to_i
+ key = "#{USER_UNIQUE_IPS_PREFIX}:#{user_id}"
+
+ Gitlab::Redis.with do |redis|
+ unique_ips_count = nil
+ redis.multi do |r|
+ r.zadd(key, time, ip)
+ r.zremrangebyscore(key, 0, time - config.unique_ips_limit_time_window)
+ unique_ips_count = r.zcard(key)
+ end
+ unique_ips_count.value
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/award_emoji.rb b/lib/gitlab/award_emoji.rb
deleted file mode 100644
index 39b43ab5489..00000000000
--- a/lib/gitlab/award_emoji.rb
+++ /dev/null
@@ -1,83 +0,0 @@
-module Gitlab
- class AwardEmoji
- CATEGORIES = {
- objects: "Objects",
- travel: "Travel",
- symbols: "Symbols",
- nature: "Nature",
- people: "People",
- activity: "Activity",
- flags: "Flags",
- food: "Food"
- }.with_indifferent_access
-
- def self.normalize_emoji_name(name)
- aliases[name] || name
- end
-
- def self.emoji_by_category
- unless @emoji_by_category
- @emoji_by_category = Hash.new { |h, key| h[key] = [] }
-
- emojis.each do |emoji_name, data|
- data["name"] = emoji_name
-
- # Skip Fitzpatrick(tone) modifiers
- next if data["category"] == "modifier"
-
- category = data["category"]
-
- @emoji_by_category[category] << data
- end
-
- @emoji_by_category = @emoji_by_category.sort.to_h
- end
-
- @emoji_by_category
- end
-
- def self.emojis
- @emojis ||=
- begin
- json_path = File.join(Rails.root, 'fixtures', 'emojis', 'index.json' )
- JSON.parse(File.read(json_path))
- end
- end
-
- def self.aliases
- @aliases ||=
- begin
- json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json')
- JSON.parse(File.read(json_path))
- end
- end
-
- # Returns an Array of Emoji names and their asset URLs.
- def self.urls
- @urls ||= begin
- path = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
- # Construct the full asset path ourselves because
- # ActionView::Helpers::AssetUrlHelper.asset_url is slow for hundreds
- # of entries since it has to do a lot of extra work (e.g. regexps).
- prefix = Gitlab::Application.config.assets.prefix
- digest = Gitlab::Application.config.assets.digest
- base =
- if defined?(Gitlab::Application.config.relative_url_root) && Gitlab::Application.config.relative_url_root
- Gitlab::Application.config.relative_url_root
- else
- ''
- end
-
- JSON.parse(File.read(path)).map do |hash|
- if digest
- fname = "#{hash['unicode']}-#{hash['digest']}"
- else
- fname = hash['unicode']
- end
-
- { name: hash['name'], path: File.join(base, prefix, "#{fname}.png") }
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/badge/build/template.rb b/lib/gitlab/badge/build/template.rb
index 2b95ddfcb53..bc0e0cd441d 100644
--- a/lib/gitlab/badge/build/template.rb
+++ b/lib/gitlab/badge/build/template.rb
@@ -15,7 +15,7 @@ module Gitlab
canceled: '#9f9f9f',
skipped: '#9f9f9f',
unknown: '#9f9f9f'
- }
+ }.freeze
def initialize(badge)
@entity = badge.entity
diff --git a/lib/gitlab/badge/coverage/template.rb b/lib/gitlab/badge/coverage/template.rb
index 06e0d084e9f..fcecb1d9665 100644
--- a/lib/gitlab/badge/coverage/template.rb
+++ b/lib/gitlab/badge/coverage/template.rb
@@ -13,7 +13,7 @@ module Gitlab
medium: '#dfb317',
low: '#e05d44',
unknown: '#9f9f9f'
- }
+ }.freeze
def initialize(badge)
@entity = badge.entity
diff --git a/lib/gitlab/badge/metadata.rb b/lib/gitlab/badge/metadata.rb
index 548f85b78bb..4a049ef758d 100644
--- a/lib/gitlab/badge/metadata.rb
+++ b/lib/gitlab/badge/metadata.rb
@@ -20,6 +20,10 @@ module Gitlab
"[![#{title}](#{image_url})](#{link_url})"
end
+ def to_asciidoc
+ "image:#{image_url}[link=\"#{link_url}\",title=\"#{title}\"]"
+ end
+
def title
raise NotImplementedError
end
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
index 44323b47dca..f4efa20374a 100644
--- a/lib/gitlab/bitbucket_import/importer.rb
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -130,8 +130,13 @@ module Gitlab
end
def create_labels
- LABELS.each do |label|
- @labels[label[:title]] = project.labels.create!(label)
+ LABELS.each do |label_params|
+ label = ::Labels::CreateService.new(label_params).execute(project: project)
+ if label.valid?
+ @labels[label_params[:title]] = label
+ else
+ raise "Failed to create label \"#{label_params[:title]}\" for project \"#{project.name_with_namespace}\""
+ end
end
end
diff --git a/lib/gitlab/changes_list.rb b/lib/gitlab/changes_list.rb
index 95308aca95f..5b32fca00a4 100644
--- a/lib/gitlab/changes_list.rb
+++ b/lib/gitlab/changes_list.rb
@@ -5,7 +5,7 @@ module Gitlab
attr_reader :raw_changes
def initialize(changes)
- @raw_changes = changes.kind_of?(String) ? changes.lines : changes
+ @raw_changes = changes.is_a?(String) ? changes.lines : changes
end
def each(&block)
diff --git a/lib/gitlab/chat_commands/base_command.rb b/lib/gitlab/chat_commands/base_command.rb
index 4fe53ce93a9..25da8474e95 100644
--- a/lib/gitlab/chat_commands/base_command.rb
+++ b/lib/gitlab/chat_commands/base_command.rb
@@ -42,10 +42,6 @@ module Gitlab
def find_by_iid(iid)
collection.find_by(iid: iid)
end
-
- def presenter
- Gitlab::ChatCommands::Presenter.new
- end
end
end
end
diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb
index 145086755e4..f34ed0f4cf2 100644
--- a/lib/gitlab/chat_commands/command.rb
+++ b/lib/gitlab/chat_commands/command.rb
@@ -3,7 +3,7 @@ module Gitlab
class Command < BaseCommand
COMMANDS = [
Gitlab::ChatCommands::IssueShow,
- Gitlab::ChatCommands::IssueCreate,
+ Gitlab::ChatCommands::IssueNew,
Gitlab::ChatCommands::IssueSearch,
Gitlab::ChatCommands::Deploy,
].freeze
@@ -13,51 +13,32 @@ module Gitlab
if command
if command.allowed?(project, current_user)
- present command.new(project, current_user, params).execute(match)
+ command.new(project, current_user, params).execute(match)
else
- access_denied
+ Gitlab::ChatCommands::Presenters::Access.new.access_denied
end
else
- help(help_messages)
+ Gitlab::ChatCommands::Help.new(project, current_user, params).execute(available_commands, params[:text])
end
end
def match_command
match = nil
- service = available_commands.find do |klass|
- match = klass.match(command)
- end
+ service =
+ available_commands.find do |klass|
+ match = klass.match(params[:text])
+ end
[service, match]
end
private
- def help_messages
- available_commands.map(&:help_message)
- end
-
def available_commands
COMMANDS.select do |klass|
klass.available?(project)
end
end
-
- def command
- params[:text]
- end
-
- def help(messages)
- presenter.help(messages, params[:command])
- end
-
- def access_denied
- presenter.access_denied
- end
-
- def present(resource)
- presenter.present(resource)
- end
end
end
end
diff --git a/lib/gitlab/chat_commands/deploy.rb b/lib/gitlab/chat_commands/deploy.rb
index 7127d2f6d04..458d90f84e8 100644
--- a/lib/gitlab/chat_commands/deploy.rb
+++ b/lib/gitlab/chat_commands/deploy.rb
@@ -1,8 +1,6 @@
module Gitlab
module ChatCommands
class Deploy < BaseCommand
- include Gitlab::Routing.url_helpers
-
def self.match(text)
/\Adeploy\s+(?<from>\S+.*)\s+to+\s+(?<to>\S+.*)\z/.match(text)
end
@@ -24,35 +22,29 @@ module Gitlab
to = match[:to]
actions = find_actions(from, to)
- return unless actions.present?
- if actions.one?
- play!(from, to, actions.first)
+ if actions.none?
+ Gitlab::ChatCommands::Presenters::Deploy.new(nil).no_actions
+ elsif actions.one?
+ action = play!(from, to, actions.first)
+ Gitlab::ChatCommands::Presenters::Deploy.new(action).present(from, to)
else
- Result.new(:error, 'Too many actions defined')
+ Gitlab::ChatCommands::Presenters::Deploy.new(actions).too_many_actions
end
end
private
def play!(from, to, action)
- new_action = action.play(current_user)
-
- Result.new(:success, "Deployment from #{from} to #{to} started. Follow the progress: #{url(new_action)}.")
+ action.play(current_user)
end
def find_actions(from, to)
environment = project.environments.find_by(name: from)
- return unless environment
+ return [] unless environment
environment.actions_for(to).select(&:starts_environment?)
end
-
- def url(subject)
- polymorphic_url(
- [subject.project.namespace.becomes(Namespace), subject.project, subject]
- )
- end
end
end
end
diff --git a/lib/gitlab/chat_commands/help.rb b/lib/gitlab/chat_commands/help.rb
new file mode 100644
index 00000000000..6c0e4d304a4
--- /dev/null
+++ b/lib/gitlab/chat_commands/help.rb
@@ -0,0 +1,28 @@
+module Gitlab
+ module ChatCommands
+ class Help < BaseCommand
+ # This class has to be used last, as it always matches. It has to match
+ # because other commands were not triggered and we want to show the help
+ # command
+ def self.match(_text)
+ true
+ end
+
+ def self.help_message
+ 'help'
+ end
+
+ def self.allowed?(_project, _user)
+ true
+ end
+
+ def execute(commands, text)
+ Gitlab::ChatCommands::Presenters::Help.new(commands).present(trigger, text)
+ end
+
+ def trigger
+ params[:command]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_new.rb
index cefb6775db8..016054ecd46 100644
--- a/lib/gitlab/chat_commands/issue_create.rb
+++ b/lib/gitlab/chat_commands/issue_new.rb
@@ -1,8 +1,8 @@
module Gitlab
module ChatCommands
- class IssueCreate < IssueCommand
+ class IssueNew < IssueCommand
def self.match(text)
- # we can not match \n with the dot by passing the m modifier as than
+ # we can not match \n with the dot by passing the m modifier as than
# the title and description are not seperated
/\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text)
end
@@ -19,8 +19,24 @@ module Gitlab
title = match[:title]
description = match[:description].to_s.rstrip
+ issue = create_issue(title: title, description: description)
+
+ if issue.persisted?
+ presenter(issue).present
+ else
+ presenter(issue).display_errors
+ end
+ end
+
+ private
+
+ def create_issue(title:, description:)
Issues::CreateService.new(project, current_user, title: title, description: description).execute
end
+
+ def presenter(issue)
+ Gitlab::ChatCommands::Presenters::IssueNew.new(issue)
+ end
end
end
end
diff --git a/lib/gitlab/chat_commands/issue_search.rb b/lib/gitlab/chat_commands/issue_search.rb
index 51bf80c800b..3491b53093e 100644
--- a/lib/gitlab/chat_commands/issue_search.rb
+++ b/lib/gitlab/chat_commands/issue_search.rb
@@ -10,7 +10,13 @@ module Gitlab
end
def execute(match)
- collection.search(match[:query]).limit(QUERY_LIMIT)
+ issues = collection.search(match[:query]).limit(QUERY_LIMIT)
+
+ if issues.present?
+ Presenters::IssueSearch.new(issues).present
+ else
+ Presenters::Access.new(issues).not_found
+ end
end
end
end
diff --git a/lib/gitlab/chat_commands/issue_show.rb b/lib/gitlab/chat_commands/issue_show.rb
index 2a45d49cf6b..d6013f4d10c 100644
--- a/lib/gitlab/chat_commands/issue_show.rb
+++ b/lib/gitlab/chat_commands/issue_show.rb
@@ -10,7 +10,13 @@ module Gitlab
end
def execute(match)
- find_by_iid(match[:iid])
+ issue = find_by_iid(match[:iid])
+
+ if issue
+ Gitlab::ChatCommands::Presenters::IssueShow.new(issue).present
+ else
+ Gitlab::ChatCommands::Presenters::Access.new.not_found
+ end
end
end
end
diff --git a/lib/gitlab/chat_commands/presenter.rb b/lib/gitlab/chat_commands/presenter.rb
deleted file mode 100644
index 8930a21f406..00000000000
--- a/lib/gitlab/chat_commands/presenter.rb
+++ /dev/null
@@ -1,131 +0,0 @@
-module Gitlab
- module ChatCommands
- class Presenter
- include Gitlab::Routing
-
- def authorize_chat_name(url)
- message = if url
- ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{url})."
- else
- ":sweat_smile: Couldn't identify you, nor can I autorize you!"
- end
-
- ephemeral_response(message)
- end
-
- def help(commands, trigger)
- if commands.none?
- ephemeral_response("No commands configured")
- else
- commands.map! { |command| "#{trigger} #{command}" }
- message = header_with_list("Available commands", commands)
-
- ephemeral_response(message)
- end
- end
-
- def present(subject)
- return not_found unless subject
-
- if subject.is_a?(Gitlab::ChatCommands::Result)
- show_result(subject)
- elsif subject.respond_to?(:count)
- if subject.none?
- not_found
- elsif subject.one?
- single_resource(subject.first)
- else
- multiple_resources(subject)
- end
- else
- single_resource(subject)
- end
- end
-
- def access_denied
- ephemeral_response("Whoops! That action is not allowed. This incident will be [reported](https://xkcd.com/838/).")
- end
-
- private
-
- def show_result(result)
- case result.type
- when :success
- in_channel_response(result.message)
- else
- ephemeral_response(result.message)
- end
- end
-
- def not_found
- ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:")
- end
-
- def single_resource(resource)
- return error(resource) if resource.errors.any? || !resource.persisted?
-
- message = "#{title(resource)}:"
- message << "\n\n#{resource.description}" if resource.try(:description)
-
- in_channel_response(message)
- end
-
- def multiple_resources(resources)
- titles = resources.map { |resource| title(resource) }
-
- message = header_with_list("Multiple results were found:", titles)
-
- ephemeral_response(message)
- end
-
- def error(resource)
- message = header_with_list("The action was not successful, because:", resource.errors.messages)
-
- ephemeral_response(message)
- end
-
- def title(resource)
- reference = resource.try(:to_reference) || resource.try(:id)
- title = resource.try(:title) || resource.try(:name)
-
- "[#{reference} #{title}](#{url(resource)})"
- end
-
- def header_with_list(header, items)
- message = [header]
-
- items.each do |item|
- message << "- #{item}"
- end
-
- message.join("\n")
- end
-
- def url(resource)
- url_for(
- [
- resource.project.namespace.becomes(Namespace),
- resource.project,
- resource
- ]
- )
- end
-
- def ephemeral_response(message)
- {
- response_type: :ephemeral,
- text: message,
- status: 200
- }
- end
-
- def in_channel_response(message)
- {
- response_type: :in_channel,
- text: message,
- status: 200
- }
- end
- end
- end
-end
diff --git a/lib/gitlab/chat_commands/presenters/access.rb b/lib/gitlab/chat_commands/presenters/access.rb
new file mode 100644
index 00000000000..92f4fa17f78
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/access.rb
@@ -0,0 +1,40 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class Access < Presenters::Base
+ def access_denied
+ ephemeral_response(text: "Whoops! This action is not allowed. This incident will be [reported](https://xkcd.com/838/).")
+ end
+
+ def not_found
+ ephemeral_response(text: "404 not found! GitLab couldn't find what you were looking for! :boom:")
+ end
+
+ def authorize
+ message =
+ if @resource
+ ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{@resource})."
+ else
+ ":sweat_smile: Couldn't identify you, nor can I autorize you!"
+ end
+
+ ephemeral_response(text: message)
+ end
+
+ def unknown_command(commands)
+ ephemeral_response(text: help_message(trigger))
+ end
+
+ private
+
+ def help_message(trigger)
+ header_with_list("Command not found, these are the commands you can use", full_commands(trigger))
+ end
+
+ def full_commands(trigger)
+ @resource.map { |command| "#{trigger} #{command.help_message}" }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/base.rb b/lib/gitlab/chat_commands/presenters/base.rb
new file mode 100644
index 00000000000..2700a5a2ad5
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/base.rb
@@ -0,0 +1,77 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class Base
+ include Gitlab::Routing.url_helpers
+
+ def initialize(resource = nil)
+ @resource = resource
+ end
+
+ def display_errors
+ message = header_with_list("The action was not successful, because:", @resource.errors.full_messages)
+
+ ephemeral_response(text: message)
+ end
+
+ private
+
+ def header_with_list(header, items)
+ message = [header]
+
+ items.each do |item|
+ message << "- #{item}"
+ end
+
+ message.join("\n")
+ end
+
+ def ephemeral_response(message)
+ response = {
+ response_type: :ephemeral,
+ status: 200
+ }.merge(message)
+
+ format_response(response)
+ end
+
+ def in_channel_response(message)
+ response = {
+ response_type: :in_channel,
+ status: 200
+ }.merge(message)
+
+ format_response(response)
+ end
+
+ def format_response(response)
+ response[:text] = format(response[:text]) if response.has_key?(:text)
+
+ if response.has_key?(:attachments)
+ response[:attachments].each do |attachment|
+ attachment[:pretext] = format(attachment[:pretext]) if attachment[:pretext]
+ attachment[:text] = format(attachment[:text]) if attachment[:text]
+ end
+ end
+
+ response
+ end
+
+ # Convert Markdown to slacks format
+ def format(string)
+ Slack::Notifier::LinkFormatter.format(string)
+ end
+
+ def resource_url
+ url_for(
+ [
+ @resource.project.namespace.becomes(Namespace),
+ @resource.project,
+ @resource
+ ]
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/deploy.rb b/lib/gitlab/chat_commands/presenters/deploy.rb
new file mode 100644
index 00000000000..863d0bf99ca
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/deploy.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class Deploy < Presenters::Base
+ def present(from, to)
+ message = "Deployment started from #{from} to #{to}. [Follow its progress](#{resource_url})."
+
+ in_channel_response(text: message)
+ end
+
+ def no_actions
+ ephemeral_response(text: "No action found to be executed")
+ end
+
+ def too_many_actions
+ ephemeral_response(text: "Too many actions defined")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/help.rb b/lib/gitlab/chat_commands/presenters/help.rb
new file mode 100644
index 00000000000..cd47b7f4c6a
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/help.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class Help < Presenters::Base
+ def present(trigger, text)
+ ephemeral_response(text: help_message(trigger, text))
+ end
+
+ private
+
+ def help_message(trigger, text)
+ return "No commands available :thinking_face:" unless @resource.present?
+
+ if text.start_with?('help')
+ header_with_list("Available commands", full_commands(trigger))
+ else
+ header_with_list("Unknown command, these commands are available", full_commands(trigger))
+ end
+ end
+
+ def full_commands(trigger)
+ @resource.map { |command| "#{trigger} #{command.help_message}" }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/issue_base.rb b/lib/gitlab/chat_commands/presenters/issue_base.rb
new file mode 100644
index 00000000000..054f7f4be0c
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/issue_base.rb
@@ -0,0 +1,43 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ module IssueBase
+ def color(issuable)
+ issuable.open? ? '#38ae67' : '#d22852'
+ end
+
+ def status_text(issuable)
+ issuable.open? ? 'Open' : 'Closed'
+ end
+
+ def project
+ @resource.project
+ end
+
+ def author
+ @resource.author
+ end
+
+ def fields
+ [
+ {
+ title: "Assignee",
+ value: @resource.assignee ? @resource.assignee.name : "_None_",
+ short: true
+ },
+ {
+ title: "Milestone",
+ value: @resource.milestone ? @resource.milestone.title : "_None_",
+ short: true
+ },
+ {
+ title: "Labels",
+ value: @resource.labels.any? ? @resource.label_names.join(', ') : "_None_",
+ short: true
+ }
+ ]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/issue_new.rb b/lib/gitlab/chat_commands/presenters/issue_new.rb
new file mode 100644
index 00000000000..3674ba25641
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/issue_new.rb
@@ -0,0 +1,50 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class IssueNew < Presenters::Base
+ include Presenters::IssueBase
+
+ def present
+ in_channel_response(new_issue)
+ end
+
+ private
+
+ def new_issue
+ {
+ attachments: [
+ {
+ title: "#{@resource.title} · #{@resource.to_reference}",
+ title_link: resource_url,
+ author_name: author.name,
+ author_icon: author.avatar_url,
+ fallback: "New issue #{@resource.to_reference}: #{@resource.title}",
+ pretext: pretext,
+ color: color(@resource),
+ fields: fields,
+ mrkdwn_in: [
+ :title,
+ :pretext,
+ :text,
+ :fields
+ ]
+ }
+ ]
+ }
+ end
+
+ def pretext
+ "I created an issue on #{author_profile_link}'s behalf: **#{@resource.to_reference}** in #{project_link}"
+ end
+
+ def project_link
+ "[#{project.name_with_namespace}](#{project.web_url})"
+ end
+
+ def author_profile_link
+ "[#{author.to_reference}](#{url_for(author)})"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/issue_search.rb b/lib/gitlab/chat_commands/presenters/issue_search.rb
new file mode 100644
index 00000000000..73788cf9662
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/issue_search.rb
@@ -0,0 +1,47 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class IssueSearch < Presenters::Base
+ include Presenters::IssueBase
+
+ def present
+ text = if @resource.count >= 5
+ "Here are the first 5 issues I found:"
+ elsif @resource.one?
+ "Here is the only issue I found:"
+ else
+ "Here are the #{@resource.count} issues I found:"
+ end
+
+ ephemeral_response(text: text, attachments: attachments)
+ end
+
+ private
+
+ def attachments
+ @resource.map do |issue|
+ url = "[#{issue.to_reference}](#{url_for([namespace, project, issue])})"
+
+ {
+ color: color(issue),
+ fallback: "#{issue.to_reference} #{issue.title}",
+ text: "#{url} · #{issue.title} (#{status_text(issue)})",
+
+ mrkdwn_in: [
+ :text
+ ]
+ }
+ end
+ end
+
+ def project
+ @project ||= @resource.first.project
+ end
+
+ def namespace
+ @namespace ||= project.namespace.becomes(Namespace)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/issue_show.rb b/lib/gitlab/chat_commands/presenters/issue_show.rb
new file mode 100644
index 00000000000..bd784ad241e
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/issue_show.rb
@@ -0,0 +1,61 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class IssueShow < Presenters::Base
+ include Presenters::IssueBase
+
+ def present
+ if @resource.confidential?
+ ephemeral_response(show_issue)
+ else
+ in_channel_response(show_issue)
+ end
+ end
+
+ private
+
+ def show_issue
+ {
+ attachments: [
+ {
+ title: "#{@resource.title} · #{@resource.to_reference}",
+ title_link: resource_url,
+ author_name: author.name,
+ author_icon: author.avatar_url,
+ fallback: "Issue #{@resource.to_reference}: #{@resource.title}",
+ pretext: pretext,
+ text: text,
+ color: color(@resource),
+ fields: fields,
+ mrkdwn_in: [
+ :pretext,
+ :text,
+ :fields
+ ]
+ }
+ ]
+ }
+ end
+
+ def text
+ message = "**#{status_text(@resource)}**"
+
+ if @resource.upvotes.zero? && @resource.downvotes.zero? && @resource.user_notes_count.zero?
+ return message
+ end
+
+ message << " · "
+ message << ":+1: #{@resource.upvotes} " unless @resource.upvotes.zero?
+ message << ":-1: #{@resource.downvotes} " unless @resource.downvotes.zero?
+ message << ":speech_balloon: #{@resource.user_notes_count}" unless @resource.user_notes_count.zero?
+
+ message
+ end
+
+ def pretext
+ "Issue *#{@resource.to_reference}* from #{project.name_with_namespace}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index 273118135a9..c85f79127bc 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -1,16 +1,20 @@
module Gitlab
module Checks
class ChangeAccess
- attr_reader :user_access, :project, :skip_authorization
+ # protocol is currently used only in EE
+ attr_reader :user_access, :project, :skip_authorization, :protocol
def initialize(
- change, user_access:, project:, env: {}, skip_authorization: false)
+ change, user_access:, project:, env: {}, skip_authorization: false,
+ protocol:
+ )
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
@branch_name = Gitlab::Git.branch_name(@ref)
@user_access = user_access
@project = project
@env = env
@skip_authorization = skip_authorization
+ @protocol = protocol
end
def exec
diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb
index cd2e83b4c27..a375ccbece0 100644
--- a/lib/gitlab/ci/build/artifacts/metadata.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata.rb
@@ -6,7 +6,7 @@ module Gitlab
module Build
module Artifacts
class Metadata
- class ParserError < StandardError; end
+ ParserError = Class.new(StandardError)
VERSION_PATTERN = /^[\w\s]+(\d+\.\d+\.\d+)/
INVALID_PATH_PATTERN = %r{(^\.?\.?/)|(/\.?\.?/)}
diff --git a/lib/gitlab/ci/build/artifacts/metadata/entry.rb b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
index 7f4c750b6fd..6f799c2f031 100644
--- a/lib/gitlab/ci/build/artifacts/metadata/entry.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
@@ -27,6 +27,8 @@ module Gitlab
end
end
+ delegate :empty?, to: :children
+
def directory?
blank_node? || @path.end_with?('/')
end
@@ -91,10 +93,6 @@ module Gitlab
blank_node? || @entries.include?(@path)
end
- def empty?
- children.empty?
- end
-
def total_size
descendant_pattern = %r{^#{Regexp.escape(@path)}}
entries.sum do |path, entry|
diff --git a/lib/gitlab/ci/build/image.rb b/lib/gitlab/ci/build/image.rb
new file mode 100644
index 00000000000..c62aeb60fa9
--- /dev/null
+++ b/lib/gitlab/ci/build/image.rb
@@ -0,0 +1,33 @@
+module Gitlab
+ module Ci
+ module Build
+ class Image
+ attr_reader :name
+
+ class << self
+ def from_image(job)
+ image = Gitlab::Ci::Build::Image.new(job.options[:image])
+ return unless image.valid?
+ image
+ end
+
+ def from_services(job)
+ services = job.options[:services].to_a.map do |service|
+ Gitlab::Ci::Build::Image.new(service)
+ end
+
+ services.select(&:valid?).compact
+ end
+ end
+
+ def initialize(image)
+ @name = image
+ end
+
+ def valid?
+ @name.present?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/step.rb b/lib/gitlab/ci/build/step.rb
new file mode 100644
index 00000000000..ee034d9cc56
--- /dev/null
+++ b/lib/gitlab/ci/build/step.rb
@@ -0,0 +1,41 @@
+module Gitlab
+ module Ci
+ module Build
+ class Step
+ WHEN_ON_FAILURE = 'on_failure'.freeze
+ WHEN_ON_SUCCESS = 'on_success'.freeze
+ WHEN_ALWAYS = 'always'.freeze
+
+ attr_reader :name
+ attr_accessor :script, :timeout, :when, :allow_failure
+
+ class << self
+ def from_commands(job)
+ self.new(:script).tap do |step|
+ step.script = job.commands.split("\n")
+ step.timeout = job.timeout
+ step.when = WHEN_ON_SUCCESS
+ end
+ end
+
+ def from_after_script(job)
+ after_script = job.options[:after_script]
+ return unless after_script
+
+ self.new(:after_script).tap do |step|
+ step.script = after_script
+ step.timeout = job.timeout
+ step.when = WHEN_ALWAYS
+ step.allow_failure = true
+ end
+ end
+ end
+
+ def initialize(name)
+ @name = name
+ @allow_failure = false
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/artifacts.rb b/lib/gitlab/ci/config/entry/artifacts.rb
index b756b0d4555..8275aacee9b 100644
--- a/lib/gitlab/ci/config/entry/artifacts.rb
+++ b/lib/gitlab/ci/config/entry/artifacts.rb
@@ -9,7 +9,7 @@ module Gitlab
include Validatable
include Attributable
- ALLOWED_KEYS = %i[name untracked paths when expire_in]
+ ALLOWED_KEYS = %i[name untracked paths when expire_in].freeze
attributes ALLOWED_KEYS
diff --git a/lib/gitlab/ci/config/entry/cache.rb b/lib/gitlab/ci/config/entry/cache.rb
index 7653cab668b..f074df9c7a1 100644
--- a/lib/gitlab/ci/config/entry/cache.rb
+++ b/lib/gitlab/ci/config/entry/cache.rb
@@ -8,7 +8,7 @@ module Gitlab
class Cache < Node
include Configurable
- ALLOWED_KEYS = %i[key untracked paths]
+ ALLOWED_KEYS = %i[key untracked paths].freeze
validations do
validates :config, allowed_keys: ALLOWED_KEYS
@@ -22,6 +22,12 @@ module Gitlab
entry :paths, Entry::Paths,
description: 'Specify which paths should be cached across builds.'
+
+ helpers :key
+
+ def value
+ super.merge(key: key_value)
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/entry/configurable.rb b/lib/gitlab/ci/config/entry/configurable.rb
index 833ae4a0ff3..e05aca9881b 100644
--- a/lib/gitlab/ci/config/entry/configurable.rb
+++ b/lib/gitlab/ci/config/entry/configurable.rb
@@ -58,7 +58,7 @@ module Gitlab
def helpers(*nodes)
nodes.each do |symbol|
define_method("#{symbol}_defined?") do
- @entries[symbol].specified? if @entries[symbol]
+ @entries[symbol]&.specified?
end
define_method("#{symbol}_value") do
diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb
new file mode 100644
index 00000000000..12a063059cb
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/coverage.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents Coverage settings.
+ #
+ class Coverage < Node
+ include Validatable
+
+ validations do
+ validates :config, regexp: true
+ end
+
+ def value
+ @config[1...-1]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/environment.rb b/lib/gitlab/ci/config/entry/environment.rb
index f7c530c7d9f..0c1f9eb7cbf 100644
--- a/lib/gitlab/ci/config/entry/environment.rb
+++ b/lib/gitlab/ci/config/entry/environment.rb
@@ -8,7 +8,7 @@ module Gitlab
class Environment < Node
include Validatable
- ALLOWED_KEYS = %i[name url action on_stop]
+ ALLOWED_KEYS = %i[name url action on_stop].freeze
validations do
validate do
@@ -21,12 +21,14 @@ module Gitlab
validates :name,
type: {
with: String,
- message: Gitlab::Regex.environment_name_regex_message }
+ message: Gitlab::Regex.environment_name_regex_message
+ }
validates :name,
format: {
with: Gitlab::Regex.environment_name_regex,
- message: Gitlab::Regex.environment_name_regex_message }
+ message: Gitlab::Regex.environment_name_regex_message
+ }
with_options if: :hash? do
validates :config, allowed_keys: ALLOWED_KEYS
diff --git a/lib/gitlab/ci/config/entry/factory.rb b/lib/gitlab/ci/config/entry/factory.rb
index 9f5e393d191..6be8288748f 100644
--- a/lib/gitlab/ci/config/entry/factory.rb
+++ b/lib/gitlab/ci/config/entry/factory.rb
@@ -6,7 +6,7 @@ module Gitlab
# Factory class responsible for fabricating entry objects.
#
class Factory
- class InvalidFactory < StandardError; end
+ InvalidFactory = Class.new(StandardError)
def initialize(entry)
@entry = entry
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index a55362f0b6b..176301bcca1 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -11,7 +11,7 @@ module Gitlab
ALLOWED_KEYS = %i[tags script only except type image services allow_failure
type stage when artifacts cache dependencies before_script
- after_script variables environment]
+ after_script variables environment coverage].freeze
validations do
validates :config, allowed_keys: ALLOWED_KEYS
@@ -71,9 +71,12 @@ module Gitlab
entry :environment, Entry::Environment,
description: 'Environment configuration for this job.'
+ entry :coverage, Entry::Coverage,
+ description: 'Coverage configuration for this job.'
+
helpers :before_script, :script, :stage, :type, :after_script,
:cache, :image, :services, :only, :except, :variables,
- :artifacts, :commands, :environment
+ :artifacts, :commands, :environment, :coverage
attributes :script, :tags, :allow_failure, :when, :dependencies
@@ -101,6 +104,14 @@ module Gitlab
(before_script_value.to_a + script_value.to_a).join("\n")
end
+ def manual_action?
+ self.when == 'manual'
+ end
+
+ def ignored?
+ allow_failure.nil? ? manual_action? : allow_failure
+ end
+
private
def inherit!(deps)
@@ -130,8 +141,10 @@ module Gitlab
variables: variables_defined? ? variables_value : nil,
environment: environment_defined? ? environment_value : nil,
environment_name: environment_defined? ? environment_value[:name] : nil,
+ coverage: coverage_defined? ? coverage_value : nil,
artifacts: artifacts_value,
- after_script: after_script_value }
+ after_script: after_script_value,
+ ignore: ignored? }
end
end
end
diff --git a/lib/gitlab/ci/config/entry/key.rb b/lib/gitlab/ci/config/entry/key.rb
index 0e4c9fe6edc..f27ad0a7759 100644
--- a/lib/gitlab/ci/config/entry/key.rb
+++ b/lib/gitlab/ci/config/entry/key.rb
@@ -11,6 +11,10 @@ module Gitlab
validations do
validates :config, key: true
end
+
+ def self.default
+ 'default'
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
index f01975aab5c..9b9a0a8125a 100644
--- a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
+++ b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
@@ -28,17 +28,21 @@ module Gitlab
value.is_a?(String) || value.is_a?(Symbol)
end
+ def validate_regexp(value)
+ !value.nil? && Regexp.new(value.to_s) && true
+ rescue RegexpError, TypeError
+ false
+ end
+
def validate_string_or_regexp(value)
return true if value.is_a?(Symbol)
return false unless value.is_a?(String)
if value.first == '/' && value.last == '/'
- Regexp.new(value[1...-1])
+ validate_regexp(value[1...-1])
else
true
end
- rescue RegexpError
- false
end
def validate_boolean(value)
diff --git a/lib/gitlab/ci/config/entry/node.rb b/lib/gitlab/ci/config/entry/node.rb
index 5eef2868cd6..a6a914d79c1 100644
--- a/lib/gitlab/ci/config/entry/node.rb
+++ b/lib/gitlab/ci/config/entry/node.rb
@@ -6,7 +6,7 @@ module Gitlab
# Base abstract class for each configuration entry node.
#
class Node
- class InvalidError < StandardError; end
+ InvalidError = Class.new(StandardError)
attr_reader :config, :metadata
attr_accessor :key, :parent, :description
@@ -70,6 +70,12 @@ module Gitlab
true
end
+ def inspect
+ val = leaf? ? config : descendants
+ unspecified = specified? ? '' : '(unspecified) '
+ "#<#{self.class.name} #{unspecified}{#{key}: #{val.inspect}}>"
+ end
+
def self.default
end
diff --git a/lib/gitlab/ci/config/entry/trigger.rb b/lib/gitlab/ci/config/entry/trigger.rb
index 28b0a9ffe01..16b234e6c59 100644
--- a/lib/gitlab/ci/config/entry/trigger.rb
+++ b/lib/gitlab/ci/config/entry/trigger.rb
@@ -9,15 +9,7 @@ module Gitlab
include Validatable
validations do
- include LegacyValidationHelpers
-
- validate :array_of_strings_or_regexps
-
- def array_of_strings_or_regexps
- unless validate_array_of_strings_or_regexps(config)
- errors.add(:config, 'should be an array of strings or regexps')
- end
- end
+ validates :config, array_of_strings_or_regexps: true
end
end
end
diff --git a/lib/gitlab/ci/config/entry/undefined.rb b/lib/gitlab/ci/config/entry/undefined.rb
index b33b8238230..1171ac10f22 100644
--- a/lib/gitlab/ci/config/entry/undefined.rb
+++ b/lib/gitlab/ci/config/entry/undefined.rb
@@ -29,6 +29,10 @@ module Gitlab
def relevant?
false
end
+
+ def inspect
+ "#<#{self.class.name}>"
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb
index 8632dd0e233..bd7428b1272 100644
--- a/lib/gitlab/ci/config/entry/validators.rb
+++ b/lib/gitlab/ci/config/entry/validators.rb
@@ -54,6 +54,51 @@ module Gitlab
end
end
+ class RegexpValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless validate_regexp(value)
+ record.errors.add(attribute, 'must be a regular expression')
+ end
+ end
+
+ private
+
+ def look_like_regexp?(value)
+ value.is_a?(String) && value.start_with?('/') &&
+ value.end_with?('/')
+ end
+
+ def validate_regexp(value)
+ look_like_regexp?(value) &&
+ Regexp.new(value.to_s[1...-1]) &&
+ true
+ rescue RegexpError
+ false
+ end
+ end
+
+ class ArrayOfStringsOrRegexpsValidator < RegexpValidator
+ def validate_each(record, attribute, value)
+ unless validate_array_of_strings_or_regexps(value)
+ record.errors.add(attribute, 'should be an array of strings or regexps')
+ end
+ end
+
+ private
+
+ def validate_array_of_strings_or_regexps(values)
+ values.is_a?(Array) && values.all?(&method(:validate_string_or_regexp))
+ end
+
+ def validate_string_or_regexp(value)
+ return false unless value.is_a?(String)
+ return validate_regexp(value) if look_like_regexp?(value)
+ true
+ end
+ end
+
class TypeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
type = options[:with]
diff --git a/lib/gitlab/ci/config/loader.rb b/lib/gitlab/ci/config/loader.rb
index dbf6eb0edbe..e7d9f6a7761 100644
--- a/lib/gitlab/ci/config/loader.rb
+++ b/lib/gitlab/ci/config/loader.rb
@@ -2,7 +2,7 @@ module Gitlab
module Ci
class Config
class Loader
- class FormatError < StandardError; end
+ FormatError = Class.new(StandardError)
def initialize(config)
@config = YAML.safe_load(config, [Symbol], [], true)
diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb
index a979fe7d573..67bbc3c4849 100644
--- a/lib/gitlab/ci/status/build/cancelable.rb
+++ b/lib/gitlab/ci/status/build/cancelable.rb
@@ -10,7 +10,7 @@ module Gitlab
end
def action_icon
- 'ban'
+ 'icon_action_cancel'
end
def action_path
diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb
index eee9a64120b..38ac6edc9f1 100644
--- a/lib/gitlab/ci/status/build/factory.rb
+++ b/lib/gitlab/ci/status/build/factory.rb
@@ -4,8 +4,11 @@ module Gitlab
module Build
class Factory < Status::Factory
def self.extended_statuses
- [Status::Build::Stop, Status::Build::Play,
- Status::Build::Cancelable, Status::Build::Retryable]
+ [[Status::Build::Cancelable,
+ Status::Build::Retryable],
+ [Status::Build::FailedAllowed,
+ Status::Build::Play,
+ Status::Build::Stop]]
end
def self.common_helpers
diff --git a/lib/gitlab/ci/status/build/failed_allowed.rb b/lib/gitlab/ci/status/build/failed_allowed.rb
new file mode 100644
index 00000000000..807afe24bd5
--- /dev/null
+++ b/lib/gitlab/ci/status/build/failed_allowed.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class FailedAllowed < SimpleDelegator
+ include Status::Extended
+
+ def label
+ 'failed (allowed to fail)'
+ end
+
+ def icon
+ 'icon_status_warning'
+ end
+
+ def group
+ 'failed_with_warnings'
+ end
+
+ def self.matches?(build, user)
+ build.failed? && build.allow_failure?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb
index 1bf949c96dd..3495b8d0448 100644
--- a/lib/gitlab/ci/status/build/play.rb
+++ b/lib/gitlab/ci/status/build/play.rb
@@ -5,38 +5,22 @@ module Gitlab
class Play < SimpleDelegator
include Status::Extended
- def text
- 'manual'
- end
-
def label
'manual play action'
end
- def icon
- 'icon_status_manual'
- end
-
- def group
- 'manual'
- end
-
def has_action?
can?(user, :update_build, subject)
end
def action_icon
- 'play'
+ 'icon_action_play'
end
def action_title
'Play'
end
- def action_class
- 'ci-play-icon'
- end
-
def action_path
play_namespace_project_build_path(subject.project.namespace,
subject.project,
diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb
index 8e38d6a8523..6b362af7634 100644
--- a/lib/gitlab/ci/status/build/retryable.rb
+++ b/lib/gitlab/ci/status/build/retryable.rb
@@ -10,7 +10,7 @@ module Gitlab
end
def action_icon
- 'refresh'
+ 'icon_action_retry'
end
def action_title
diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb
index e1dfdb76d41..e8530f2aaae 100644
--- a/lib/gitlab/ci/status/build/stop.rb
+++ b/lib/gitlab/ci/status/build/stop.rb
@@ -5,28 +5,16 @@ module Gitlab
class Stop < SimpleDelegator
include Status::Extended
- def text
- 'manual'
- end
-
def label
'manual stop action'
end
- def icon
- 'icon_status_manual'
- end
-
- def group
- 'manual'
- end
-
def has_action?
can?(user, :update_build, subject)
end
def action_icon
- 'stop'
+ 'icon_action_stop'
end
def action_title
diff --git a/lib/gitlab/ci/status/canceled.rb b/lib/gitlab/ci/status/canceled.rb
index dd6d99e9075..97c121ce7b9 100644
--- a/lib/gitlab/ci/status/canceled.rb
+++ b/lib/gitlab/ci/status/canceled.rb
@@ -13,6 +13,10 @@ module Gitlab
def icon
'icon_status_canceled'
end
+
+ def favicon
+ 'favicon_status_canceled'
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb
index 73b6ab5a635..d4fd83b93f8 100644
--- a/lib/gitlab/ci/status/core.rb
+++ b/lib/gitlab/ci/status/core.rb
@@ -18,6 +18,10 @@ module Gitlab
raise NotImplementedError
end
+ def favicon
+ raise NotImplementedError
+ end
+
def label
raise NotImplementedError
end
@@ -42,9 +46,6 @@ module Gitlab
raise NotImplementedError
end
- def action_class
- end
-
def action_path
raise NotImplementedError
end
diff --git a/lib/gitlab/ci/status/created.rb b/lib/gitlab/ci/status/created.rb
index 6596d7e01ca..0721bf6ec7c 100644
--- a/lib/gitlab/ci/status/created.rb
+++ b/lib/gitlab/ci/status/created.rb
@@ -13,6 +13,10 @@ module Gitlab
def icon
'icon_status_created'
end
+
+ def favicon
+ 'favicon_status_created'
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb
index ae9ef895df4..15836c699c7 100644
--- a/lib/gitlab/ci/status/factory.rb
+++ b/lib/gitlab/ci/status/factory.rb
@@ -5,41 +5,46 @@ module Gitlab
def initialize(subject, user)
@subject = subject
@user = user
+ @status = subject.status || HasStatus::DEFAULT_STATUS
end
def fabricate!
- if extended_status
- extended_status.new(core_status)
- else
+ if extended_statuses.none?
core_status
+ else
+ compound_extended_status
end
end
- def self.extended_statuses
- []
+ def core_status
+ Gitlab::Ci::Status
+ .const_get(@status.capitalize)
+ .new(@subject, @user)
+ .extend(self.class.common_helpers)
end
- def self.common_helpers
- Module.new
+ def compound_extended_status
+ extended_statuses.inject(core_status) do |status, extended|
+ extended.new(status)
+ end
end
- private
+ def extended_statuses
+ return @extended_statuses if defined?(@extended_statuses)
- def simple_status
- @simple_status ||= @subject.status || :created
+ groups = self.class.extended_statuses.map do |group|
+ Array(group).find { |status| status.matches?(@subject, @user) }
+ end
+
+ @extended_statuses = groups.flatten.compact
end
- def core_status
- Gitlab::Ci::Status
- .const_get(simple_status.capitalize)
- .new(@subject, @user)
- .extend(self.class.common_helpers)
+ def self.extended_statuses
+ []
end
- def extended_status
- @extended ||= self.class.extended_statuses.find do |status|
- status.matches?(@subject, @user)
- end
+ def self.common_helpers
+ Module.new
end
end
end
diff --git a/lib/gitlab/ci/status/failed.rb b/lib/gitlab/ci/status/failed.rb
index c5b5e3203ad..cb75e9383a8 100644
--- a/lib/gitlab/ci/status/failed.rb
+++ b/lib/gitlab/ci/status/failed.rb
@@ -13,6 +13,10 @@ module Gitlab
def icon
'icon_status_failed'
end
+
+ def favicon
+ 'favicon_status_failed'
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/manual.rb b/lib/gitlab/ci/status/manual.rb
new file mode 100644
index 00000000000..f8f6c2903ba
--- /dev/null
+++ b/lib/gitlab/ci/status/manual.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module Ci
+ module Status
+ class Manual < Status::Core
+ def text
+ 'manual'
+ end
+
+ def label
+ 'manual action'
+ end
+
+ def icon
+ 'icon_status_manual'
+ end
+
+ def favicon
+ 'favicon_status_manual'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/pending.rb b/lib/gitlab/ci/status/pending.rb
index d30f35a59a2..f40cc1314dc 100644
--- a/lib/gitlab/ci/status/pending.rb
+++ b/lib/gitlab/ci/status/pending.rb
@@ -13,6 +13,10 @@ module Gitlab
def icon
'icon_status_pending'
end
+
+ def favicon
+ 'favicon_status_pending'
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/pipeline/blocked.rb b/lib/gitlab/ci/status/pipeline/blocked.rb
new file mode 100644
index 00000000000..a250c3fcb41
--- /dev/null
+++ b/lib/gitlab/ci/status/pipeline/blocked.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module Ci
+ module Status
+ module Pipeline
+ class Blocked < SimpleDelegator
+ include Status::Extended
+
+ def text
+ 'blocked'
+ end
+
+ def label
+ 'waiting for manual action'
+ end
+
+ def self.matches?(pipeline, user)
+ pipeline.blocked?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb
index 16dcb326be9..17f9a75f436 100644
--- a/lib/gitlab/ci/status/pipeline/factory.rb
+++ b/lib/gitlab/ci/status/pipeline/factory.rb
@@ -4,7 +4,8 @@ module Gitlab
module Pipeline
class Factory < Status::Factory
def self.extended_statuses
- [Pipeline::SuccessWithWarnings]
+ [[Status::SuccessWarning,
+ Status::Pipeline::Blocked]]
end
def self.common_helpers
diff --git a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb
deleted file mode 100644
index 24bf8b869e0..00000000000
--- a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-module Gitlab
- module Ci
- module Status
- module Pipeline
- class SuccessWithWarnings < SimpleDelegator
- include Status::Extended
-
- def text
- 'passed'
- end
-
- def label
- 'passed with warnings'
- end
-
- def icon
- 'icon_status_warning'
- end
-
- def group
- 'success_with_warnings'
- end
-
- def self.matches?(pipeline, user)
- pipeline.success? && pipeline.has_warnings?
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/status/running.rb b/lib/gitlab/ci/status/running.rb
index 2aba3c373c7..1237cd47dc8 100644
--- a/lib/gitlab/ci/status/running.rb
+++ b/lib/gitlab/ci/status/running.rb
@@ -13,6 +13,10 @@ module Gitlab
def icon
'icon_status_running'
end
+
+ def favicon
+ 'favicon_status_running'
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/skipped.rb b/lib/gitlab/ci/status/skipped.rb
index 16282aefd03..28005d91503 100644
--- a/lib/gitlab/ci/status/skipped.rb
+++ b/lib/gitlab/ci/status/skipped.rb
@@ -13,6 +13,10 @@ module Gitlab
def icon
'icon_status_skipped'
end
+
+ def favicon
+ 'favicon_status_skipped'
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/stage/factory.rb b/lib/gitlab/ci/status/stage/factory.rb
index 689a5dd45bc..4c37f084d07 100644
--- a/lib/gitlab/ci/status/stage/factory.rb
+++ b/lib/gitlab/ci/status/stage/factory.rb
@@ -3,6 +3,10 @@ module Gitlab
module Status
module Stage
class Factory < Status::Factory
+ def self.extended_statuses
+ [Status::SuccessWarning]
+ end
+
def self.common_helpers
Status::Stage::Common
end
diff --git a/lib/gitlab/ci/status/success.rb b/lib/gitlab/ci/status/success.rb
index c09c5f006e3..88f7758a270 100644
--- a/lib/gitlab/ci/status/success.rb
+++ b/lib/gitlab/ci/status/success.rb
@@ -13,6 +13,10 @@ module Gitlab
def icon
'icon_status_success'
end
+
+ def favicon
+ 'favicon_status_success'
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/success_warning.rb b/lib/gitlab/ci/status/success_warning.rb
new file mode 100644
index 00000000000..d4cdab6957a
--- /dev/null
+++ b/lib/gitlab/ci/status/success_warning.rb
@@ -0,0 +1,33 @@
+module Gitlab
+ module Ci
+ module Status
+ ##
+ # Extended status used when pipeline or stage passed conditionally.
+ # This means that failed jobs that are allowed to fail were present.
+ #
+ class SuccessWarning < SimpleDelegator
+ include Status::Extended
+
+ def text
+ 'passed'
+ end
+
+ def label
+ 'passed with warnings'
+ end
+
+ def icon
+ 'icon_status_warning'
+ end
+
+ def group
+ 'success_with_warnings'
+ end
+
+ def self.matches?(subject, user)
+ subject.success? && subject.has_warnings?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/trace_reader.rb b/lib/gitlab/ci/trace_reader.rb
index 37e51536e8f..1d7ddeb3e0f 100644
--- a/lib/gitlab/ci/trace_reader.rb
+++ b/lib/gitlab/ci/trace_reader.rb
@@ -42,6 +42,7 @@ module Gitlab
end
chunks.join.lines.last(max_lines).join
+ .force_encoding(Encoding.default_external)
end
end
end
diff --git a/lib/gitlab/conflict/file.rb b/lib/gitlab/conflict/file.rb
index c843315782d..75a213ef752 100644
--- a/lib/gitlab/conflict/file.rb
+++ b/lib/gitlab/conflict/file.rb
@@ -4,8 +4,7 @@ module Gitlab
include Gitlab::Routing.url_helpers
include IconsHelper
- class MissingResolution < ResolutionError
- end
+ MissingResolution = Class.new(ResolutionError)
CONTEXT_LINES = 3
@@ -91,11 +90,12 @@ module Gitlab
our_highlight = Gitlab::Highlight.highlight(our_path, our_file, repository: repository).lines
lines.each do |line|
- if line.type == 'old'
- line.rich_text = their_highlight[line.old_line - 1].try(:html_safe)
- else
- line.rich_text = our_highlight[line.new_line - 1].try(:html_safe)
- end
+ line.rich_text =
+ if line.type == 'old'
+ their_highlight[line.old_line - 1].try(:html_safe)
+ else
+ our_highlight[line.new_line - 1].try(:html_safe)
+ end
end
end
diff --git a/lib/gitlab/conflict/file_collection.rb b/lib/gitlab/conflict/file_collection.rb
index fa5bd4649d4..990b719ecfd 100644
--- a/lib/gitlab/conflict/file_collection.rb
+++ b/lib/gitlab/conflict/file_collection.rb
@@ -1,8 +1,7 @@
module Gitlab
module Conflict
class FileCollection
- class ConflictSideMissing < StandardError
- end
+ ConflictSideMissing = Class.new(StandardError)
attr_reader :merge_request, :our_commit, :their_commit
diff --git a/lib/gitlab/conflict/parser.rb b/lib/gitlab/conflict/parser.rb
index ddd657903fb..84f9ecd3d23 100644
--- a/lib/gitlab/conflict/parser.rb
+++ b/lib/gitlab/conflict/parser.rb
@@ -1,35 +1,23 @@
module Gitlab
module Conflict
class Parser
- class UnresolvableError < StandardError
- end
-
- class UnmergeableFile < UnresolvableError
- end
-
- class UnsupportedEncoding < UnresolvableError
- end
+ UnresolvableError = Class.new(StandardError)
+ UnmergeableFile = Class.new(UnresolvableError)
+ UnsupportedEncoding = Class.new(UnresolvableError)
# Recoverable errors - the conflict can be resolved in an editor, but not with
# sections.
- class ParserError < StandardError
- end
-
- class UnexpectedDelimiter < ParserError
- end
-
- class MissingEndDelimiter < ParserError
- end
+ ParserError = Class.new(StandardError)
+ UnexpectedDelimiter = Class.new(ParserError)
+ MissingEndDelimiter = Class.new(ParserError)
def parse(text, our_path:, their_path:, parent_file: nil)
raise UnmergeableFile if text.blank? # Typically a binary file
raise UnmergeableFile if text.length > 200.kilobytes
- begin
- text.to_json
- rescue Encoding::UndefinedConversionError
- raise UnsupportedEncoding
- end
+ text.force_encoding('UTF-8')
+
+ raise UnsupportedEncoding unless text.valid_encoding?
line_obj_index = 0
line_old = 1
diff --git a/lib/gitlab/conflict/resolution_error.rb b/lib/gitlab/conflict/resolution_error.rb
index a0f2006bc24..0b61256b35a 100644
--- a/lib/gitlab/conflict/resolution_error.rb
+++ b/lib/gitlab/conflict/resolution_error.rb
@@ -1,6 +1,5 @@
module Gitlab
module Conflict
- class ResolutionError < StandardError
- end
+ ResolutionError = Class.new(StandardError)
end
end
diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb
index 7e3d5647b39..15992b77680 100644
--- a/lib/gitlab/contributions_calendar.rb
+++ b/lib/gitlab/contributions_calendar.rb
@@ -22,8 +22,10 @@ module Gitlab
having(action: [Event::CREATED, Event::CLOSED], target_type: "Issue")
mr_events = event_counts(date_from, :merge_requests).
having(action: [Event::MERGED, Event::CREATED, Event::CLOSED], target_type: "MergeRequest")
+ note_events = event_counts(date_from, :merge_requests).
+ having(action: [Event::COMMENTED], target_type: "Note")
- union = Gitlab::SQL::Union.new([repo_events, issue_events, mr_events])
+ union = Gitlab::SQL::Union.new([repo_events, issue_events, mr_events, note_events])
events = Event.find_by_sql(union.to_sql).map(&:attributes)
@activity_events = events.each_with_object(Hash.new {|h, k| h[k] = 0 }) do |event, activities|
@@ -38,7 +40,7 @@ module Gitlab
# Use visible_to_user? instead of the complicated logic in activity_dates
# because we're only viewing the events for a single day.
- events.select {|event| event.visible_to_user?(current_user) }
+ events.select { |event| event.visible_to_user?(current_user) }
end
def starting_year
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 2ff27e46d64..82576d197fe 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -9,7 +9,9 @@ module Gitlab
end
def ensure_application_settings!
- if connect_to_db?
+ return fake_application_settings unless connect_to_db?
+
+ unless ENV['IN_MEMORY_APPLICATION_SETTINGS'] == 'true'
begin
settings = ::ApplicationSetting.current
# In case Redis isn't running or the Redis UNIX socket file is not available
@@ -20,43 +22,21 @@ module Gitlab
settings ||= ::ApplicationSetting.create_from_defaults unless ActiveRecord::Migrator.needs_migration?
end
- settings || fake_application_settings
+ settings || in_memory_application_settings
end
- def sidekiq_throttling_enabled?
- current_application_settings.sidekiq_throttling_enabled?
+ delegate :sidekiq_throttling_enabled?, to: :current_application_settings
+
+ def in_memory_application_settings
+ @in_memory_application_settings ||= ::ApplicationSetting.new(::ApplicationSetting.defaults)
+ # In case migrations the application_settings table is not created yet,
+ # we fallback to a simple OpenStruct
+ rescue ActiveRecord::StatementInvalid, ActiveRecord::UnknownAttributeError
+ fake_application_settings
end
def fake_application_settings
- OpenStruct.new(
- default_projects_limit: Settings.gitlab['default_projects_limit'],
- default_branch_protection: Settings.gitlab['default_branch_protection'],
- signup_enabled: Settings.gitlab['signup_enabled'],
- signin_enabled: Settings.gitlab['signin_enabled'],
- gravatar_enabled: Settings.gravatar['enabled'],
- koding_enabled: false,
- plantuml_enabled: false,
- sign_in_text: nil,
- after_sign_up_text: nil,
- help_page_text: nil,
- shared_runners_text: nil,
- restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
- max_attachment_size: Settings.gitlab['max_attachment_size'],
- session_expire_delay: Settings.gitlab['session_expire_delay'],
- default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
- default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
- domain_whitelist: Settings.gitlab['domain_whitelist'],
- import_sources: %w[gitea github bitbucket gitlab google_code fogbugz git gitlab_project],
- shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
- max_artifacts_size: Settings.artifacts['max_size'],
- require_two_factor_authentication: false,
- two_factor_grace_period: 48,
- akismet_enabled: false,
- repository_checks_enabled: true,
- container_registry_token_expire_delay: 5,
- user_default_external: false,
- sidekiq_throttling_enabled: false,
- )
+ OpenStruct.new(::ApplicationSetting.defaults)
end
private
diff --git a/lib/gitlab/cycle_analytics/base_event_fetcher.rb b/lib/gitlab/cycle_analytics/base_event_fetcher.rb
index 0d8791d396b..ab115afcaa5 100644
--- a/lib/gitlab/cycle_analytics/base_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/base_event_fetcher.rb
@@ -5,6 +5,8 @@ module Gitlab
attr_reader :projections, :query, :stage, :order
+ MAX_EVENTS = 50
+
def initialize(project:, stage:, options:)
@project = project
@stage = stage
@@ -38,7 +40,7 @@ module Gitlab
def events_query
diff_fn = subtract_datetimes_diff(base_query, @options[:start_time_attrs], @options[:end_time_attrs])
- base_query.project(extract_diff_epoch(diff_fn).as('total_time'), *projections).order(order.desc)
+ base_query.project(extract_diff_epoch(diff_fn).as('total_time'), *projections).order(order.desc).take(MAX_EVENTS)
end
def default_order
diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb
index 74bbcdcb3dd..559e3939da6 100644
--- a/lib/gitlab/cycle_analytics/base_stage.rb
+++ b/lib/gitlab/cycle_analytics/base_stage.rb
@@ -13,7 +13,7 @@ module Gitlab
end
def as_json
- AnalyticsStageSerializer.new.represent(self).as_json
+ AnalyticsStageSerializer.new.represent(self)
end
def title
diff --git a/lib/gitlab/cycle_analytics/code_event_fetcher.rb b/lib/gitlab/cycle_analytics/code_event_fetcher.rb
index 5245b9ca8fc..d5bf6149749 100644
--- a/lib/gitlab/cycle_analytics/code_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/code_event_fetcher.rb
@@ -18,7 +18,7 @@ module Gitlab
private
def serialize(event)
- AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json
+ AnalyticsMergeRequestSerializer.new(project: @project).represent(event)
end
end
end
diff --git a/lib/gitlab/cycle_analytics/code_stage.rb b/lib/gitlab/cycle_analytics/code_stage.rb
index d1bc2055ba8..1e52b6614a1 100644
--- a/lib/gitlab/cycle_analytics/code_stage.rb
+++ b/lib/gitlab/cycle_analytics/code_stage.rb
@@ -13,6 +13,10 @@ module Gitlab
:code
end
+ def legend
+ "Related Merge Requests"
+ end
+
def description
"Time until first merge request"
end
diff --git a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
index 0d8da99455e..3df9cbdcfce 100644
--- a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
@@ -16,7 +16,7 @@ module Gitlab
private
def serialize(event)
- AnalyticsIssueSerializer.new(project: @project).represent(event).as_json
+ AnalyticsIssueSerializer.new(project: @project).represent(event)
end
end
end
diff --git a/lib/gitlab/cycle_analytics/issue_stage.rb b/lib/gitlab/cycle_analytics/issue_stage.rb
index d2068fbc38f..213994988a5 100644
--- a/lib/gitlab/cycle_analytics/issue_stage.rb
+++ b/lib/gitlab/cycle_analytics/issue_stage.rb
@@ -14,6 +14,10 @@ module Gitlab
:issue
end
+ def legend
+ "Related Issues"
+ end
+
def description
"Time before an issue gets scheduled"
end
diff --git a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
index 88a8710dbe6..7d342a2d2cb 100644
--- a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
@@ -37,7 +37,7 @@ module Gitlab
def serialize_commit(event, st_commit, query)
commit = Commit.new(Gitlab::Git::Commit.new(st_commit), @project)
- AnalyticsCommitSerializer.new(project: @project, total_time: event['total_time']).represent(commit).as_json
+ AnalyticsCommitSerializer.new(project: @project, total_time: event['total_time']).represent(commit)
end
end
end
diff --git a/lib/gitlab/cycle_analytics/plan_stage.rb b/lib/gitlab/cycle_analytics/plan_stage.rb
index 3b4dfc6a30e..45d51d30ccc 100644
--- a/lib/gitlab/cycle_analytics/plan_stage.rb
+++ b/lib/gitlab/cycle_analytics/plan_stage.rb
@@ -14,6 +14,10 @@ module Gitlab
:plan
end
+ def legend
+ "Related Commits"
+ end
+
def description
"Time before an issue starts implementation"
end
diff --git a/lib/gitlab/cycle_analytics/production_stage.rb b/lib/gitlab/cycle_analytics/production_stage.rb
index 2a6bcc80116..9f387a02945 100644
--- a/lib/gitlab/cycle_analytics/production_stage.rb
+++ b/lib/gitlab/cycle_analytics/production_stage.rb
@@ -15,6 +15,10 @@ module Gitlab
:production
end
+ def legend
+ "Related Issues"
+ end
+
def description
"From issue creation until deploy to production"
end
diff --git a/lib/gitlab/cycle_analytics/review_event_fetcher.rb b/lib/gitlab/cycle_analytics/review_event_fetcher.rb
index 4df0bd06393..4c7b3f4467f 100644
--- a/lib/gitlab/cycle_analytics/review_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/review_event_fetcher.rb
@@ -15,7 +15,7 @@ module Gitlab
end
def serialize(event)
- AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json
+ AnalyticsMergeRequestSerializer.new(project: @project).represent(event)
end
end
end
diff --git a/lib/gitlab/cycle_analytics/review_stage.rb b/lib/gitlab/cycle_analytics/review_stage.rb
index fbaa3010d81..4744be834de 100644
--- a/lib/gitlab/cycle_analytics/review_stage.rb
+++ b/lib/gitlab/cycle_analytics/review_stage.rb
@@ -13,6 +13,10 @@ module Gitlab
:review
end
+ def legend
+ "Relative Merged Requests"
+ end
+
def description
"Time between merge request creation and merge/close"
end
diff --git a/lib/gitlab/cycle_analytics/stage_summary.rb b/lib/gitlab/cycle_analytics/stage_summary.rb
index b34baf5b081..fc77bd86097 100644
--- a/lib/gitlab/cycle_analytics/stage_summary.rb
+++ b/lib/gitlab/cycle_analytics/stage_summary.rb
@@ -16,7 +16,7 @@ module Gitlab
private
def serialize(summary_object)
- AnalyticsSummarySerializer.new.represent(summary_object).as_json
+ AnalyticsSummarySerializer.new.represent(summary_object)
end
end
end
diff --git a/lib/gitlab/cycle_analytics/staging_event_fetcher.rb b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb
index a34731a5fcd..36c0260dbfe 100644
--- a/lib/gitlab/cycle_analytics/staging_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb
@@ -23,7 +23,7 @@ module Gitlab
private
def serialize(event)
- AnalyticsBuildSerializer.new.represent(event['build']).as_json
+ AnalyticsBuildSerializer.new.represent(event['build'])
end
end
end
diff --git a/lib/gitlab/cycle_analytics/staging_stage.rb b/lib/gitlab/cycle_analytics/staging_stage.rb
index 945909a4d62..3cdbe04fbaf 100644
--- a/lib/gitlab/cycle_analytics/staging_stage.rb
+++ b/lib/gitlab/cycle_analytics/staging_stage.rb
@@ -14,6 +14,10 @@ module Gitlab
:staging
end
+ def legend
+ "Relative Deployed Builds"
+ end
+
def description
"From merge request merge until deploy to production"
end
diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb
index 0079d56e0e4..e96943833bc 100644
--- a/lib/gitlab/cycle_analytics/test_stage.rb
+++ b/lib/gitlab/cycle_analytics/test_stage.rb
@@ -13,6 +13,10 @@ module Gitlab
:test
end
+ def legend
+ "Relative Builds Trigger by Commits"
+ end
+
def description
"Total test time for all commits/merges"
end
diff --git a/lib/gitlab/data_builder/build.rb b/lib/gitlab/data_builder/build.rb
index 6548e6475c6..f78106f5b10 100644
--- a/lib/gitlab/data_builder/build.rb
+++ b/lib/gitlab/data_builder/build.rb
@@ -8,6 +8,8 @@ module Gitlab
commit = build.pipeline
user = build.user
+ author_url = build_author_url(build.commit, commit)
+
data = {
object_kind: 'build',
@@ -43,6 +45,7 @@ module Gitlab
message: commit.git_commit_message,
author_name: commit.git_author_name,
author_email: commit.git_author_email,
+ author_url: author_url,
status: commit.status,
duration: commit.duration,
started_at: commit.started_at,
@@ -62,6 +65,13 @@ module Gitlab
data
end
+
+ private
+
+ def build_author_url(commit, pipeline)
+ author = commit.try(:author)
+ author ? Gitlab::Routing.url_helpers.user_url(author) : "mailto:#{pipeline.git_author_email}"
+ end
end
end
end
diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb
index e50e54b6e99..182a30fd74d 100644
--- a/lib/gitlab/data_builder/pipeline.rb
+++ b/lib/gitlab/data_builder/pipeline.rb
@@ -39,7 +39,7 @@ module Gitlab
started_at: build.started_at,
finished_at: build.finished_at,
when: build.when,
- manual: build.manual?,
+ manual: build.action?,
user: build.user.try(:hook_attrs),
runner: build.runner && runner_hook_attrs(build.runner),
artifacts_file: {
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 55b8f888d53..63b8d0d3b9d 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -5,8 +5,12 @@ module Gitlab
# http://dev.mysql.com/doc/refman/5.7/en/integer-types.html
MAX_INT_VALUE = 2147483647
+ def self.config
+ ActiveRecord::Base.configurations[Rails.env]
+ end
+
def self.adapter_name
- connection.adapter_name
+ config['adapter']
end
def self.mysql?
@@ -24,7 +28,7 @@ module Gitlab
def self.nulls_last_order(field, direction = 'ASC')
order = "#{field} #{direction}"
- if Gitlab::Database.postgresql?
+ if postgresql?
order << ' NULLS LAST'
else
# `field IS NULL` will be `0` for non-NULL columns and `1` for NULL
@@ -35,8 +39,22 @@ module Gitlab
order
end
+ def self.nulls_first_order(field, direction = 'ASC')
+ order = "#{field} #{direction}"
+
+ if postgresql?
+ order << ' NULLS FIRST'
+ else
+ # `field IS NULL` will be `0` for non-NULL columns and `1` for NULL
+ # columns. In the (default) ascending order, `0` comes first.
+ order.prepend("#{field} IS NULL, ") if direction == 'DESC'
+ end
+
+ order
+ end
+
def self.random
- Gitlab::Database.postgresql? ? "RANDOM()" : "RAND()"
+ postgresql? ? "RANDOM()" : "RAND()"
end
def true_value
@@ -55,6 +73,36 @@ module Gitlab
end
end
+ def self.with_connection_pool(pool_size)
+ pool = create_connection_pool(pool_size)
+
+ begin
+ yield(pool)
+ ensure
+ pool.disconnect!
+ end
+ end
+
+ # pool_size - The size of the DB pool.
+ # host - An optional host name to use instead of the default one.
+ def self.create_connection_pool(pool_size, host = nil)
+ # See activerecord-4.2.7.1/lib/active_record/connection_adapters/connection_specification.rb
+ env = Rails.env
+ original_config = ActiveRecord::Base.configurations
+
+ env_config = original_config[env].merge('pool' => pool_size)
+ env_config['host'] = host if host
+
+ config = original_config.merge(env => env_config)
+
+ spec =
+ ActiveRecord::
+ ConnectionAdapters::
+ ConnectionSpecification::Resolver.new(config).spec(env.to_sym)
+
+ ActiveRecord::ConnectionAdapters::ConnectionPool.new(spec)
+ end
+
def self.connection
ActiveRecord::Base.connection
end
diff --git a/lib/gitlab/database/median.rb b/lib/gitlab/database/median.rb
index 08607c27c09..23890e5f493 100644
--- a/lib/gitlab/database/median.rb
+++ b/lib/gitlab/database/median.rb
@@ -108,6 +108,7 @@ module Gitlab
Arel.sql(%Q{EXTRACT(EPOCH FROM (#{diff.to_sql}))})
end
+
# Need to cast '0' to an INTERVAL before we can check if the interval is positive
def zero_interval
Arel::Nodes::NamedFunction.new("CAST", [Arel.sql("'0' AS INTERVAL")])
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 0bd6e148ba8..fc445ab9483 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -26,11 +26,68 @@ module Gitlab
add_index(table_name, column_name, options)
end
+ # Adds a foreign key with only minimal locking on the tables involved.
+ #
+ # This method only requires minimal locking when using PostgreSQL. When
+ # using MySQL this method will use Rails' default `add_foreign_key`.
+ #
+ # source - The source table containing the foreign key.
+ # target - The target table the key points to.
+ # column - The name of the column to create the foreign key on.
+ # on_delete - The action to perform when associated data is removed,
+ # defaults to "CASCADE".
+ def add_concurrent_foreign_key(source, target, column:, on_delete: :cascade)
+ # Transactions would result in ALTER TABLE locks being held for the
+ # duration of the transaction, defeating the purpose of this method.
+ if transaction_open?
+ raise 'add_concurrent_foreign_key can not be run inside a transaction'
+ end
+
+ # While MySQL does allow disabling of foreign keys it has no equivalent
+ # of PostgreSQL's "VALIDATE CONSTRAINT". As a result we'll just fall
+ # back to the normal foreign key procedure.
+ if Database.mysql?
+ return add_foreign_key(source, target,
+ column: column,
+ on_delete: on_delete)
+ end
+
+ disable_statement_timeout
+
+ key_name = concurrent_foreign_key_name(source, column)
+
+ # Using NOT VALID allows us to create a key without immediately
+ # validating it. This means we keep the ALTER TABLE lock only for a
+ # short period of time. The key _is_ enforced for any newly created
+ # data.
+ execute <<-EOF.strip_heredoc
+ ALTER TABLE #{source}
+ ADD CONSTRAINT #{key_name}
+ FOREIGN KEY (#{column})
+ REFERENCES #{target} (id)
+ ON DELETE #{on_delete} NOT VALID;
+ EOF
+
+ # Validate the existing constraint. This can potentially take a very
+ # long time to complete, but fortunately does not lock the source table
+ # while running.
+ execute("ALTER TABLE #{source} VALIDATE CONSTRAINT #{key_name};")
+ end
+
+ # Returns the name for a concurrent foreign key.
+ #
+ # PostgreSQL constraint names have a limit of 63 bytes. The logic used
+ # here is based on Rails' foreign_key_name() method, which unfortunately
+ # is private so we can't rely on it directly.
+ def concurrent_foreign_key_name(table, column)
+ "fk_#{Digest::SHA256.hexdigest("#{table}_#{column}_fk").first(10)}"
+ end
+
# Long-running migrations may take more than the timeout allowed by
# the database. Disable the session's statement timeout to ensure
# migrations don't get killed prematurely. (PostgreSQL only)
def disable_statement_timeout
- ActiveRecord::Base.connection.execute('SET statement_timeout TO 0') if Database.postgresql?
+ execute('SET statement_timeout TO 0') if Database.postgresql?
end
# Updates the value of a column in batches.
diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb
index 9ea976e18fa..7db896522a9 100644
--- a/lib/gitlab/diff/highlight.rb
+++ b/lib/gitlab/diff/highlight.rb
@@ -50,7 +50,7 @@ module Gitlab
# Only update text if line is found. This will prevent
# issues with submodules given the line only exists in diff content.
if rich_line
- line_prefix = diff_line.text.match(/\A(.)/) ? $1 : ' '
+ line_prefix = diff_line.text =~ /\A(.)/ ? $1 : ' '
"#{line_prefix}#{rich_line}".html_safe
end
end
diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb
index 87a9b1e23ac..736933b1c4b 100644
--- a/lib/gitlab/diff/inline_diff_marker.rb
+++ b/lib/gitlab/diff/inline_diff_marker.rb
@@ -4,7 +4,7 @@ module Gitlab
MARKDOWN_SYMBOLS = {
addition: "+",
deletion: "-"
- }
+ }.freeze
attr_accessor :raw_line, :rich_line
diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb
index 80a146b4a5a..114656958e3 100644
--- a/lib/gitlab/diff/line.rb
+++ b/lib/gitlab/diff/line.rb
@@ -38,11 +38,11 @@ module Gitlab
end
def added?
- type == 'new'
+ type == 'new' || type == 'new-nonewline'
end
def removed?
- type == 'old'
+ type == 'old' || type == 'old-nonewline'
end
def rich_text
@@ -52,7 +52,7 @@ module Gitlab
end
def meta?
- type == 'match' || type == 'nonewline'
+ type == 'match'
end
def as_json(opts = nil)
diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb
index 59a2367b65d..742f989c50b 100644
--- a/lib/gitlab/diff/parser.rb
+++ b/lib/gitlab/diff/parser.rb
@@ -11,6 +11,7 @@ module Gitlab
line_old = 1
line_new = 1
type = nil
+ context = nil
# By returning an Enumerator we make it possible to search for a single line (with #find)
# without having to instantiate all the others that come after it.
@@ -20,7 +21,7 @@ module Gitlab
full_line = line.delete("\n")
- if line.match(/^@@ -/)
+ if line =~ /^@@ -/
type = "match"
line_old = line.match(/\-[0-9]*/)[0].to_i.abs rescue 0
@@ -31,7 +32,8 @@ module Gitlab
line_obj_index += 1
next
elsif line[0] == '\\'
- type = 'nonewline'
+ type = "#{context}-nonewline"
+
yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new)
line_obj_index += 1
else
@@ -43,9 +45,11 @@ module Gitlab
case line[0]
when "+"
line_new += 1
+ context = :new
when "-"
line_old += 1
- when "\\"
+ context = :old
+ when "\\" # rubocop:disable Lint/EmptyWhen
# No increment
else
line_new += 1
diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb
index ecf62dead35..fc728123c97 100644
--- a/lib/gitlab/diff/position.rb
+++ b/lib/gitlab/diff/position.rb
@@ -140,15 +140,16 @@ module Gitlab
def find_diff_file(repository)
# We're at the initial commit, so just get that as we can't compare to anything.
- if Gitlab::Git.blank_ref?(start_sha)
- compare = Gitlab::Git::Commit.find(repository.raw_repository, head_sha)
- else
- compare = Gitlab::Git::Compare.new(
- repository.raw_repository,
- start_sha,
- head_sha
- )
- end
+ compare =
+ if Gitlab::Git.blank_ref?(start_sha)
+ Gitlab::Git::Commit.find(repository.raw_repository, head_sha)
+ else
+ Gitlab::Git::Compare.new(
+ repository.raw_repository,
+ start_sha,
+ head_sha
+ )
+ end
diff = compare.diffs(paths: paths).first
diff --git a/lib/gitlab/downtime_check/message.rb b/lib/gitlab/downtime_check/message.rb
index 40a4815a9a0..543e62794c5 100644
--- a/lib/gitlab/downtime_check/message.rb
+++ b/lib/gitlab/downtime_check/message.rb
@@ -3,8 +3,8 @@ module Gitlab
class Message
attr_reader :path, :offline
- OFFLINE = "\e[31moffline\e[0m"
- ONLINE = "\e[32monline\e[0m"
+ OFFLINE = "\e[31moffline\e[0m".freeze
+ ONLINE = "\e[32monline\e[0m".freeze
# path - The file path of the migration.
# offline - When set to `true` the migration will require downtime.
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index c8e36d8ff4a..496ee0bdcb0 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -5,35 +5,44 @@ module Gitlab
CE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ce.git'.freeze
EE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze
CHECK_DIR = Rails.root.join('ee_compat_check')
- MAX_FETCH_DEPTH = 500
IGNORED_FILES_REGEX = /(VERSION|CHANGELOG\.md:\d+)/.freeze
-
- attr_reader :repo_dir, :patches_dir, :ce_repo, :ce_branch
+ PLEASE_READ_THIS_BANNER = %Q{
+ ============================================================
+ ===================== PLEASE READ THIS =====================
+ ============================================================
+ }.freeze
+ THANKS_FOR_READING_BANNER = %Q{
+ ============================================================
+ ==================== THANKS FOR READING ====================
+ ============================================================\n
+ }.freeze
+
+ attr_reader :ee_repo_dir, :patches_dir, :ce_repo, :ce_branch, :ee_branch_found
+ attr_reader :failed_files
def initialize(branch:, ce_repo: CE_REPO)
- @repo_dir = CHECK_DIR.join('repo')
+ @ee_repo_dir = CHECK_DIR.join('ee-repo')
@patches_dir = CHECK_DIR.join('patches')
@ce_branch = branch
@ce_repo = ce_repo
end
def check
- ensure_ee_repo
ensure_patches_dir
-
generate_patch(ce_branch, ce_patch_full_path)
- Dir.chdir(repo_dir) do
- step("In the #{repo_dir} directory")
+ ensure_ee_repo
+ Dir.chdir(ee_repo_dir) do
+ step("In the #{ee_repo_dir} directory")
status = catch(:halt_check) do
ce_branch_compat_check!
- delete_ee_branch_locally!
+ delete_ee_branches_locally!
ee_branch_presence_check!
ee_branch_compat_check!
end
- delete_ee_branch_locally!
+ delete_ee_branches_locally!
if status.nil?
true
@@ -46,11 +55,13 @@ module Gitlab
private
def ensure_ee_repo
- if Dir.exist?(repo_dir)
- step("#{repo_dir} already exists")
+ if Dir.exist?(ee_repo_dir)
+ step("#{ee_repo_dir} already exists")
else
- cmd = %W[git clone --branch master --single-branch --depth 200 #{EE_REPO} #{repo_dir}]
- step("Cloning #{EE_REPO} into #{repo_dir}", cmd)
+ step(
+ "Cloning #{EE_REPO} into #{ee_repo_dir}",
+ %W[git clone --branch master --single-branch --depth=200 #{EE_REPO} #{ee_repo_dir}]
+ )
end
end
@@ -61,23 +72,18 @@ module Gitlab
def generate_patch(branch, patch_path)
FileUtils.rm(patch_path, force: true)
- depth = 0
- loop do
- depth += 50
- cmd = %W[git fetch --depth #{depth} origin --prune +refs/heads/master:refs/remotes/origin/master]
- Gitlab::Popen.popen(cmd)
- _, status = Gitlab::Popen.popen(%w[git merge-base FETCH_HEAD HEAD])
+ find_merge_base_with_master(branch: branch)
- raise "#{branch} is too far behind master, please rebase it!" if depth >= MAX_FETCH_DEPTH
- break if status.zero?
- end
+ step(
+ "Generating the patch against origin/master in #{patch_path}",
+ %w[git format-patch origin/master --stdout]
+ ) do |output, status|
+ throw(:halt_check, :ko) unless status.zero?
- step("Generating the patch against master in #{patch_path}")
- output, status = Gitlab::Popen.popen(%w[git format-patch FETCH_HEAD --stdout])
- throw(:halt_check, :ko) unless status.zero?
+ File.write(patch_path, output)
- File.write(patch_path, output)
- throw(:halt_check, :ko) unless File.exist?(patch_path)
+ throw(:halt_check, :ko) unless File.exist?(patch_path)
+ end
end
def ce_branch_compat_check!
@@ -88,9 +94,17 @@ module Gitlab
end
def ee_branch_presence_check!
- status = step("Fetching origin/#{ee_branch}", %W[git fetch origin #{ee_branch}])
+ _, status = step("Fetching origin/#{ee_branch_prefix}", %W[git fetch origin #{ee_branch_prefix}])
- unless status.zero?
+ if status.zero?
+ @ee_branch_found = ee_branch_prefix
+ else
+ _, status = step("Fetching origin/#{ee_branch_suffix}", %W[git fetch origin #{ee_branch_suffix}])
+ end
+
+ if status.zero?
+ @ee_branch_found = ee_branch_suffix
+ else
puts
puts ce_branch_doesnt_apply_cleanly_and_no_ee_branch_msg
@@ -99,9 +113,9 @@ module Gitlab
end
def ee_branch_compat_check!
- step("Checking out origin/#{ee_branch}", %W[git checkout -b #{ee_branch} FETCH_HEAD])
+ step("Checking out origin/#{ee_branch_found}", %W[git checkout -b #{ee_branch_found} FETCH_HEAD])
- generate_patch(ee_branch, ee_patch_full_path)
+ generate_patch(ee_branch_found, ee_patch_full_path)
unless check_patch(ee_patch_full_path).zero?
puts
@@ -111,41 +125,77 @@ module Gitlab
end
puts
- puts applies_cleanly_msg(ee_branch)
+ puts applies_cleanly_msg(ee_branch_found)
end
def check_patch(patch_path)
step("Checking out master", %w[git checkout master])
- step("Reseting to latest master", %w[git reset --hard origin/master])
-
- step("Checking if #{patch_path} applies cleanly to EE/master")
- output, status = Gitlab::Popen.popen(%W[git apply --check #{patch_path}])
-
- unless status.zero?
- failed_files = output.lines.reduce([]) do |memo, line|
- if line.start_with?('error: patch failed:')
- file = line.sub(/\Aerror: patch failed: /, '')
- memo << file unless file =~ IGNORED_FILES_REGEX
+ step("Resetting to latest master", %w[git reset --hard origin/master])
+ step(
+ "Checking if #{patch_path} applies cleanly to EE/master",
+ %W[git apply --check --3way #{patch_path}]
+ ) do |output, status|
+ unless status.zero?
+ @failed_files = output.lines.reduce([]) do |memo, line|
+ if line.start_with?('error: patch failed:')
+ file = line.sub(/\Aerror: patch failed: /, '')
+ memo << file unless file =~ IGNORED_FILES_REGEX
+ end
+ memo
end
- memo
+
+ status = 0 if failed_files.empty?
end
- if failed_files.empty?
- status = 0
- else
- puts "\nConflicting files:"
- failed_files.each do |file|
- puts " - #{file}"
- end
+ status
+ end
+ end
+
+ def delete_ee_branches_locally!
+ command(%w[git checkout master])
+ command(%W[git branch --delete --force #{ee_branch_prefix}])
+ command(%W[git branch --delete --force #{ee_branch_suffix}])
+ end
+
+ def merge_base_found?
+ step(
+ "Finding merge base with master",
+ %w[git merge-base origin/master HEAD]
+ ) do |output, status|
+ if status.zero?
+ puts "Merge base was found: #{output}"
+ true
end
end
+ end
+
+ def find_merge_base_with_master(branch:)
+ return if merge_base_found?
+
+ # Start with (Math.exp(3).to_i = 20) until (Math.exp(6).to_i = 403)
+ # In total we go (20 + 54 + 148 + 403 = 625) commits deeper
+ depth = 20
+ success =
+ (3..6).any? do |factor|
+ depth += Math.exp(factor).to_i
+ # Repository is initially cloned with a depth of 20 so we need to fetch
+ # deeper in the case the branch has more than 20 commits on top of master
+ fetch(branch: branch, depth: depth)
+ fetch(branch: 'master', depth: depth)
+
+ merge_base_found?
+ end
- status
+ raise "\n#{branch} is too far behind master, please rebase it!\n" unless success
end
- def delete_ee_branch_locally!
- command(%w[git checkout master])
- step("Deleting the local #{ee_branch} branch", %W[git branch -D #{ee_branch}])
+ def fetch(branch:, depth:)
+ step(
+ "Fetching deeper...",
+ %W[git fetch --depth=#{depth} --prune origin +refs/heads/#{branch}:refs/remotes/origin/#{branch}]
+ ) do |output, status|
+ raise "Fetch failed: #{output}" unless status.zero?
+ end
end
def ce_patch_name
@@ -156,12 +206,16 @@ module Gitlab
@ce_patch_full_path ||= patches_dir.join(ce_patch_name)
end
- def ee_branch
- @ee_branch ||= "#{ce_branch}-ee"
+ def ee_branch_suffix
+ @ee_branch_suffix ||= "#{ce_branch}-ee"
+ end
+
+ def ee_branch_prefix
+ @ee_branch_prefix ||= "ee-#{ce_branch}"
end
def ee_patch_name
- @ee_patch_name ||= patch_name_from_branch(ee_branch)
+ @ee_patch_name ||= patch_name_from_branch(ee_branch_found)
end
def ee_patch_full_path
@@ -178,98 +232,125 @@ module Gitlab
if cmd
start = Time.now
puts "\n$ #{cmd.join(' ')}"
- status = command(cmd)
- puts "\nFinished in #{Time.now - start} seconds"
- status
+
+ output, status = command(cmd)
+ puts "\n==> Finished in #{Time.now - start} seconds"
+
+ if block_given?
+ yield(output, status)
+ else
+ [output, status]
+ end
end
end
def command(cmd)
- output, status = Gitlab::Popen.popen(cmd)
- puts output
-
- status
+ Gitlab::Popen.popen(cmd)
end
def applies_cleanly_msg(branch)
- <<-MSG.strip_heredoc
- =================================================================
+ %Q{
+ #{PLEASE_READ_THIS_BANNER}
🎉 Congratulations!! 🎉
- The #{branch} branch applies cleanly to EE/master!
+ The `#{branch}` branch applies cleanly to EE/master!
- Much ❤️!!
- =================================================================\n
- MSG
+ Much ❤️! For more information, see
+ https://docs.gitlab.com/ce/development/limit_ee_conflicts.html#check-the-rake-ee_compat_check-in-your-merge-requests
+ #{THANKS_FOR_READING_BANNER}
+ }
end
def ce_branch_doesnt_apply_cleanly_and_no_ee_branch_msg
- <<-MSG.strip_heredoc
- =================================================================
+ %Q{
+ #{PLEASE_READ_THIS_BANNER}
💥 Oh no! 💥
- The #{ce_branch} branch does not apply cleanly to the current
- EE/master, and no #{ee_branch} branch was found in the EE repository.
+ The `#{ce_branch}` branch does not apply cleanly to the current
+ EE/master, and no `#{ee_branch_prefix}` or `#{ee_branch_suffix}` branch
+ was found in the EE repository.
- Please create a #{ee_branch} branch that includes changes from
- #{ce_branch} but also specific changes than can be applied cleanly
- to EE/master.
+ #{conflicting_files_msg}
+
+ We advise you to create a `#{ee_branch_prefix}` or `#{ee_branch_suffix}`
+ branch that includes changes from `#{ce_branch}` but also specific changes
+ than can be applied cleanly to EE/master. In some cases, the conflicts
+ are trivial and you can ignore the warning from this job. As always,
+ use your best judgment!
There are different ways to create such branch:
- 1. Create a new branch based on the CE branch and rebase it on top of EE/master
+ 1. Create a new branch from master and cherry-pick your CE commits
# In the EE repo
- $ git fetch #{ce_repo} #{ce_branch}
- $ git checkout -b #{ee_branch} FETCH_HEAD
-
- # You can squash the #{ce_branch} commits into a single "Port of #{ce_branch} to EE" commit
- # before rebasing to limit the conflicts-resolving steps during the rebase
$ git fetch origin
- $ git rebase origin/master
+ $ git checkout -b #{ee_branch_prefix} origin/master
+ $ git fetch #{ce_repo} #{ce_branch}
+ $ git cherry-pick SHA # Repeat for all the commits you want to pick
- At this point you will likely have conflicts.
- Solve them, and continue/finish the rebase.
+ You can squash the `#{ce_branch}` commits into a single "Port of #{ce_branch} to EE" commit.
- You can squash the #{ce_branch} commits into a single "Port of #{ce_branch} to EE".
+ 2. Apply your branch's patch to EE
- 2. Create a new branch from master and cherry-pick your CE commits
+ # In the CE repo
+ $ git fetch origin master
+ $ git format-patch origin/master --stdout > #{ce_branch}.patch
# In the EE repo
- $ git fetch origin
- $ git checkout -b #{ee_branch} origin/master
- $ git fetch #{ce_repo} #{ce_branch}
- $ git cherry-pick SHA # Repeat for all the commits you want to pick
+ $ git fetch origin master
+ $ git checkout -b #{ee_branch_prefix} origin/master
+ $ git apply --3way path/to/#{ce_branch}.patch
+
+ At this point you might have conflicts such as:
- You can squash the #{ce_branch} commits into a single "Port of #{ce_branch} to EE" commit.
+ error: patch failed: lib/gitlab/ee_compat_check.rb:5
+ Falling back to three-way merge...
+ Applied patch to 'lib/gitlab/ee_compat_check.rb' with conflicts.
+ U lib/gitlab/ee_compat_check.rb
- Don't forget to push your branch to #{EE_REPO}:
+ Resolve them, stage the changes and commit them.
+
+ ⚠️ Don't forget to push your branch to gitlab-ee:
# In the EE repo
- $ git push origin #{ee_branch}
+ $ git push origin #{ee_branch_prefix}
+
+ ⚠️ Also, don't forget to create a new merge request on gitlab-ce and
+ cross-link it with the CE merge request.
- You can then retry this failed build, and hopefully it should pass.
+ Once this is done, you can retry this failed build, and it should pass.
- Stay 💪 !
- =================================================================\n
- MSG
+ Stay 💪 ! For more information, see
+ https://docs.gitlab.com/ce/development/limit_ee_conflicts.html#check-the-rake-ee_compat_check-in-your-merge-requests
+ #{THANKS_FOR_READING_BANNER}
+ }
end
def ee_branch_doesnt_apply_cleanly_msg
- <<-MSG.strip_heredoc
- =================================================================
+ %Q{
+ #{PLEASE_READ_THIS_BANNER}
💥 Oh no! 💥
- The #{ce_branch} does not apply cleanly to the current
- EE/master, and even though a #{ee_branch} branch exists in the EE
- repository, it does not apply cleanly either to EE/master!
+ The `#{ce_branch}` does not apply cleanly to the current EE/master, and
+ even though a `#{ee_branch_found}` branch
+ exists in the EE repository, it does not apply cleanly either to
+ EE/master!
+
+ #{conflicting_files_msg}
- Please update the #{ee_branch}, push it again to #{EE_REPO}, and
+ Please update the `#{ee_branch_found}`, push it again to gitlab-ee, and
retry this build.
- Stay 💪 !
- =================================================================\n
- MSG
+ Stay 💪 ! For more information, see
+ https://docs.gitlab.com/ce/development/limit_ee_conflicts.html#check-the-rake-ee_compat_check-in-your-merge-requests
+ #{THANKS_FOR_READING_BANNER}
+ }
+ end
+
+ def conflicting_files_msg
+ failed_files.reduce("The conflicts detected were as follows:\n") do |memo, file|
+ memo << "\n - #{file}"
+ end
end
end
end
diff --git a/lib/gitlab/email/handler.rb b/lib/gitlab/email/handler.rb
index bd3267e2a80..35ea2e0ef59 100644
--- a/lib/gitlab/email/handler.rb
+++ b/lib/gitlab/email/handler.rb
@@ -1,10 +1,11 @@
require 'gitlab/email/handler/create_note_handler'
require 'gitlab/email/handler/create_issue_handler'
+require 'gitlab/email/handler/unsubscribe_handler'
module Gitlab
module Email
module Handler
- HANDLERS = [CreateNoteHandler, CreateIssueHandler]
+ HANDLERS = [UnsubscribeHandler, CreateNoteHandler, CreateIssueHandler].freeze
def self.for(mail, mail_key)
HANDLERS.find do |klass|
diff --git a/lib/gitlab/email/handler/base_handler.rb b/lib/gitlab/email/handler/base_handler.rb
index 7cccf465334..3f6ace0311a 100644
--- a/lib/gitlab/email/handler/base_handler.rb
+++ b/lib/gitlab/email/handler/base_handler.rb
@@ -9,52 +9,13 @@ module Gitlab
@mail_key = mail_key
end
- def message
- @message ||= process_message
- end
-
- def author
+ def can_execute?
raise NotImplementedError
end
- def project
+ def execute
raise NotImplementedError
end
-
- private
-
- def validate_permission!(permission)
- raise UserNotFoundError unless author
- raise UserBlockedError if author.blocked?
- raise ProjectNotFound unless author.can?(:read_project, project)
- raise UserNotAuthorizedError unless author.can?(permission, project)
- end
-
- def process_message
- message = ReplyParser.new(mail).execute.strip
- add_attachments(message)
- end
-
- def add_attachments(reply)
- attachments = Email::AttachmentUploader.new(mail).execute(project)
-
- reply + attachments.map do |link|
- "\n\n#{link[:markdown]}"
- end.join
- end
-
- def verify_record!(record:, invalid_exception:, record_name:)
- return if record.persisted?
- return if record.errors.key?(:commands_only)
-
- error_title = "The #{record_name} could not be created for the following reasons:"
-
- msg = error_title + record.errors.full_messages.map do |error|
- "\n\n- #{error}"
- end.join
-
- raise invalid_exception, msg
- end
end
end
end
diff --git a/lib/gitlab/email/handler/create_issue_handler.rb b/lib/gitlab/email/handler/create_issue_handler.rb
index 9f90a3ec2b2..b8ec9138c10 100644
--- a/lib/gitlab/email/handler/create_issue_handler.rb
+++ b/lib/gitlab/email/handler/create_issue_handler.rb
@@ -5,6 +5,7 @@ module Gitlab
module Email
module Handler
class CreateIssueHandler < BaseHandler
+ include ReplyProcessing
attr_reader :project_path, :incoming_email_token
def initialize(mail, mail_key)
@@ -33,7 +34,7 @@ module Gitlab
end
def project
- @project ||= Project.find_with_namespace(project_path)
+ @project ||= Project.find_by_full_path(project_path)
end
private
diff --git a/lib/gitlab/email/handler/create_note_handler.rb b/lib/gitlab/email/handler/create_note_handler.rb
index 447c7a6a6b9..d87ba427f4b 100644
--- a/lib/gitlab/email/handler/create_note_handler.rb
+++ b/lib/gitlab/email/handler/create_note_handler.rb
@@ -1,10 +1,13 @@
require 'gitlab/email/handler/base_handler'
+require 'gitlab/email/handler/reply_processing'
module Gitlab
module Email
module Handler
class CreateNoteHandler < BaseHandler
+ include ReplyProcessing
+
def can_handle?
mail_key =~ /\A\w+\z/
end
@@ -24,6 +27,8 @@ module Gitlab
record_name: 'comment')
end
+ private
+
def author
sent_notification.recipient
end
@@ -36,8 +41,6 @@ module Gitlab
@sent_notification ||= SentNotification.for(mail_key)
end
- private
-
def create_note
Notes::CreateService.new(
project,
diff --git a/lib/gitlab/email/handler/reply_processing.rb b/lib/gitlab/email/handler/reply_processing.rb
new file mode 100644
index 00000000000..32c5caf93e8
--- /dev/null
+++ b/lib/gitlab/email/handler/reply_processing.rb
@@ -0,0 +1,54 @@
+module Gitlab
+ module Email
+ module Handler
+ module ReplyProcessing
+ private
+
+ def author
+ raise NotImplementedError
+ end
+
+ def project
+ raise NotImplementedError
+ end
+
+ def message
+ @message ||= process_message
+ end
+
+ def process_message
+ message = ReplyParser.new(mail).execute.strip
+ add_attachments(message)
+ end
+
+ def add_attachments(reply)
+ attachments = Email::AttachmentUploader.new(mail).execute(project)
+
+ reply + attachments.map do |link|
+ "\n\n#{link[:markdown]}"
+ end.join
+ end
+
+ def validate_permission!(permission)
+ raise UserNotFoundError unless author
+ raise UserBlockedError if author.blocked?
+ raise ProjectNotFound unless author.can?(:read_project, project)
+ raise UserNotAuthorizedError unless author.can?(permission, project)
+ end
+
+ def verify_record!(record:, invalid_exception:, record_name:)
+ return if record.persisted?
+ return if record.errors.key?(:commands_only)
+
+ error_title = "The #{record_name} could not be created for the following reasons:"
+
+ msg = error_title + record.errors.full_messages.map do |error|
+ "\n\n- #{error}"
+ end.join
+
+ raise invalid_exception, msg
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/handler/unsubscribe_handler.rb b/lib/gitlab/email/handler/unsubscribe_handler.rb
new file mode 100644
index 00000000000..97d7a8d65ff
--- /dev/null
+++ b/lib/gitlab/email/handler/unsubscribe_handler.rb
@@ -0,0 +1,32 @@
+require 'gitlab/email/handler/base_handler'
+
+module Gitlab
+ module Email
+ module Handler
+ class UnsubscribeHandler < BaseHandler
+ def can_handle?
+ mail_key =~ /\A\w+#{Regexp.escape(Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX)}\z/
+ end
+
+ def execute
+ raise SentNotificationNotFoundError unless sent_notification
+ return unless sent_notification.unsubscribable?
+
+ noteable = sent_notification.noteable
+ raise NoteableNotFoundError unless noteable
+ noteable.unsubscribe(sent_notification.recipient)
+ end
+
+ private
+
+ def sent_notification
+ @sent_notification ||= SentNotification.for(reply_key)
+ end
+
+ def reply_key
+ mail_key.sub(Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX, '')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb
index 0e3b65fceb4..6c69cd9e6a9 100644
--- a/lib/gitlab/email/message/repository_push.rb
+++ b/lib/gitlab/email/message/repository_push.rb
@@ -46,7 +46,7 @@ module Gitlab
end
def diffs_count
- diffs.size if diffs
+ diffs&.size
end
def compare
@@ -58,7 +58,7 @@ module Gitlab
end
def compare_timeout
- diffs.overflow? if diffs
+ diffs&.overflow?
end
def reverse_compare?
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index a40c44eb1bc..ec0529b5a4b 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -4,19 +4,19 @@ require_dependency 'gitlab/email/handler'
# Inspired in great part by Discourse's Email::Receiver
module Gitlab
module Email
- class ProcessingError < StandardError; end
- class EmailUnparsableError < ProcessingError; end
- class SentNotificationNotFoundError < ProcessingError; end
- class ProjectNotFound < ProcessingError; end
- class EmptyEmailError < ProcessingError; end
- class AutoGeneratedEmailError < ProcessingError; end
- class UserNotFoundError < ProcessingError; end
- class UserBlockedError < ProcessingError; end
- class UserNotAuthorizedError < ProcessingError; end
- class NoteableNotFoundError < ProcessingError; end
- class InvalidNoteError < ProcessingError; end
- class InvalidIssueError < ProcessingError; end
- class UnknownIncomingEmail < ProcessingError; end
+ ProcessingError = Class.new(StandardError)
+ EmailUnparsableError = Class.new(ProcessingError)
+ SentNotificationNotFoundError = Class.new(ProcessingError)
+ ProjectNotFound = Class.new(ProcessingError)
+ EmptyEmailError = Class.new(ProcessingError)
+ AutoGeneratedEmailError = Class.new(ProcessingError)
+ UserNotFoundError = Class.new(ProcessingError)
+ UserBlockedError = Class.new(ProcessingError)
+ UserNotAuthorizedError = Class.new(ProcessingError)
+ NoteableNotFoundError = Class.new(ProcessingError)
+ InvalidNoteError = Class.new(ProcessingError)
+ InvalidIssueError = Class.new(ProcessingError)
+ UnknownIncomingEmail = Class.new(ProcessingError)
class Receiver
def initialize(raw)
@@ -35,6 +35,8 @@ module Gitlab
handler.execute
end
+ private
+
def build_mail
Mail::Message.new(@raw)
rescue Encoding::UndefinedConversionError,
@@ -54,7 +56,24 @@ module Gitlab
end
def key_from_additional_headers(mail)
- Array(mail.references).find do |mail_id|
+ references = ensure_references_array(mail.references)
+
+ find_key_from_references(references)
+ end
+
+ def ensure_references_array(references)
+ case references
+ when Array
+ references
+ when String
+ # Handle emails from clients which append with commas,
+ # example clients are Microsoft exchange and iOS app
+ Gitlab::IncomingEmail.scan_fallback_references(references)
+ end
+ end
+
+ def find_key_from_references(references)
+ references.find do |mail_id|
key = Gitlab::IncomingEmail.key_from_fallback_message_id(mail_id)
break key if key
end
diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb
index 8c8dd1b9cef..558df87f36d 100644
--- a/lib/gitlab/email/reply_parser.rb
+++ b/lib/gitlab/email/reply_parser.rb
@@ -31,11 +31,12 @@ module Gitlab
private
def select_body(message)
- if message.multipart?
- part = message.text_part || message.html_part || message
- else
- part = message
- end
+ part =
+ if message.multipart?
+ message.text_part || message.html_part || message
+ else
+ message
+ end
decoded = fix_charset(part)
diff --git a/lib/gitlab/emoji.rb b/lib/gitlab/emoji.rb
index bbbca8acc40..a16d9fc2265 100644
--- a/lib/gitlab/emoji.rb
+++ b/lib/gitlab/emoji.rb
@@ -1,7 +1,7 @@
module Gitlab
module Emoji
extend self
-
+
def emojis
Gemojione.index.instance_variable_get(:@emoji_by_name)
end
@@ -18,6 +18,10 @@ module Gitlab
emojis.keys
end
+ def emojis_aliases
+ @emoji_aliases ||= JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'aliases.json')))
+ end
+
def emoji_filename(name)
emojis[name]["unicode"]
end
@@ -25,5 +29,32 @@ module Gitlab
def emoji_unicode_filename(moji)
emojis_by_moji[moji]["unicode"]
end
+
+ def emoji_unicode_version(name)
+ @emoji_unicode_versions_by_name ||= JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'emoji-unicode-version-map.json')))
+ @emoji_unicode_versions_by_name[name]
+ end
+
+ def normalize_emoji_name(name)
+ emojis_aliases[name] || name
+ end
+
+ def emoji_image_tag(name, src)
+ "<img class='emoji' title=':#{name}:' alt=':#{name}:' src='#{src}' height='20' width='20' align='absmiddle' />"
+ end
+
+ # CSS sprite fallback takes precedence over image fallback
+ def gl_emoji_tag(name)
+ emoji_name = emojis_aliases[name] || name
+ emoji_info = emojis[emoji_name]
+ return unless emoji_info
+
+ data = {
+ name: emoji_name,
+ unicode_version: emoji_unicode_version(emoji_name)
+ }
+
+ ActionController::Base.helpers.content_tag('gl-emoji', emoji_info['moji'], data: data)
+ end
end
end
diff --git a/lib/gitlab/etag_caching/middleware.rb b/lib/gitlab/etag_caching/middleware.rb
new file mode 100644
index 00000000000..9c98f0d1a30
--- /dev/null
+++ b/lib/gitlab/etag_caching/middleware.rb
@@ -0,0 +1,73 @@
+module Gitlab
+ module EtagCaching
+ class Middleware
+ RESERVED_WORDS = ProjectPathValidator::RESERVED.map { |word| "/#{word}/" }.join('|')
+ ROUTE_REGEXP = Regexp.union(
+ %r(^(?!.*(#{RESERVED_WORDS})).*/noteable/issue/\d+/notes\z)
+ )
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ return @app.call(env) unless enabled_for_current_route?(env)
+ Gitlab::Metrics.add_event(:etag_caching_middleware_used)
+
+ etag, cached_value_present = get_etag(env)
+ if_none_match = env['HTTP_IF_NONE_MATCH']
+
+ if if_none_match == etag
+ handle_cache_hit(etag)
+ else
+ track_cache_miss(if_none_match, cached_value_present)
+
+ status, headers, body = @app.call(env)
+ headers['ETag'] = etag
+ [status, headers, body]
+ end
+ end
+
+ private
+
+ def enabled_for_current_route?(env)
+ ROUTE_REGEXP.match(env['PATH_INFO'])
+ end
+
+ def get_etag(env)
+ cache_key = env['PATH_INFO']
+ store = Gitlab::EtagCaching::Store.new
+ current_value = store.get(cache_key)
+ cached_value_present = current_value.present?
+
+ unless cached_value_present
+ current_value = store.touch(cache_key, only_if_missing: true)
+ end
+
+ [weak_etag_format(current_value), cached_value_present]
+ end
+
+ def weak_etag_format(value)
+ %Q{W/"#{value}"}
+ end
+
+ def handle_cache_hit(etag)
+ Gitlab::Metrics.add_event(:etag_caching_cache_hit)
+
+ status_code = Gitlab::PollingInterval.polling_enabled? ? 304 : 429
+
+ [status_code, { 'ETag' => etag }, ['']]
+ end
+
+ def track_cache_miss(if_none_match, cached_value_present)
+ if if_none_match.blank?
+ Gitlab::Metrics.add_event(:etag_caching_header_missing)
+ elsif !cached_value_present
+ Gitlab::Metrics.add_event(:etag_caching_key_not_found)
+ else
+ Gitlab::Metrics.add_event(:etag_caching_resource_changed)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/etag_caching/store.rb b/lib/gitlab/etag_caching/store.rb
new file mode 100644
index 00000000000..0039fc01c8f
--- /dev/null
+++ b/lib/gitlab/etag_caching/store.rb
@@ -0,0 +1,32 @@
+module Gitlab
+ module EtagCaching
+ class Store
+ EXPIRY_TIME = 20.minutes
+ REDIS_NAMESPACE = 'etag:'.freeze
+
+ def get(key)
+ Gitlab::Redis.with { |redis| redis.get(redis_key(key)) }
+ end
+
+ def touch(key, only_if_missing: false)
+ etag = generate_etag
+
+ Gitlab::Redis.with do |redis|
+ redis.set(redis_key(key), etag, ex: EXPIRY_TIME, nx: only_if_missing)
+ end
+
+ etag
+ end
+
+ private
+
+ def generate_etag
+ SecureRandom.hex
+ end
+
+ def redis_key(key)
+ "#{REDIS_NAMESPACE}#{key}"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb
index 2dd42704396..62ddd45785d 100644
--- a/lib/gitlab/exclusive_lease.rb
+++ b/lib/gitlab/exclusive_lease.rb
@@ -10,7 +10,7 @@ module Gitlab
# ExclusiveLease.
#
class ExclusiveLease
- LUA_CANCEL_SCRIPT = <<-EOS
+ LUA_CANCEL_SCRIPT = <<-EOS.freeze
local key, uuid = KEYS[1], ARGV[1]
if redis.call("get", key) == uuid then
redis.call("del", key)
diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb
index 1d93a67dc56..c9ca4cadd1c 100644
--- a/lib/gitlab/file_detector.rb
+++ b/lib/gitlab/file_detector.rb
@@ -14,7 +14,7 @@ module Gitlab
koding: '.koding.yml',
gitlab_ci: '.gitlab-ci.yml',
avatar: /\Alogo\.(png|jpg|gif)\z/
- }
+ }.freeze
# Returns an Array of file types based on the given paths.
#
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index 3cd515e4a3a..d3df3f1bca1 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -6,7 +6,7 @@ module Gitlab
class << self
def ref_name(ref)
- ref.gsub(/\Arefs\/(tags|heads)\//, '')
+ ref.sub(/\Arefs\/(tags|heads)\//, '')
end
def branch_name(ref)
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index b742d9e1e4b..e56eb0d3beb 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -93,163 +93,6 @@ module Gitlab
commit_id: sha,
)
end
-
- # Commit file in repository and return commit sha
- #
- # options should contain next structure:
- # file: {
- # content: 'Lorem ipsum...',
- # path: 'documents/story.txt',
- # update: true
- # },
- # author: {
- # email: 'user@example.com',
- # name: 'Test User',
- # time: Time.now
- # },
- # committer: {
- # email: 'user@example.com',
- # name: 'Test User',
- # time: Time.now
- # },
- # commit: {
- # message: 'Wow such commit',
- # branch: 'master',
- # update_ref: false
- # }
- #
- # rubocop:disable Metrics/AbcSize
- # rubocop:disable Metrics/CyclomaticComplexity
- # rubocop:disable Metrics/PerceivedComplexity
- def commit(repository, options, action = :add)
- file = options[:file]
- update = file[:update].nil? ? true : file[:update]
- author = options[:author]
- committer = options[:committer]
- commit = options[:commit]
- repo = repository.rugged
- ref = commit[:branch]
- update_ref = commit[:update_ref].nil? ? true : commit[:update_ref]
- parents = []
- mode = 0o100644
-
- unless ref.start_with?('refs/')
- ref = 'refs/heads/' + ref
- end
-
- path_name = Gitlab::Git::PathHelper.normalize_path(file[:path])
- # Abort if any invalid characters remain (e.g. ../foo)
- raise Gitlab::Git::Repository::InvalidBlobName.new("Invalid path") if path_name.each_filename.to_a.include?('..')
-
- filename = path_name.to_s
- index = repo.index
-
- unless repo.empty?
- rugged_ref = repo.references[ref]
- raise Gitlab::Git::Repository::InvalidRef.new("Invalid branch name") unless rugged_ref
- last_commit = rugged_ref.target
- index.read_tree(last_commit.tree)
- parents = [last_commit]
- end
-
- if action == :remove
- index.remove(filename)
- else
- file_entry = index.get(filename)
-
- if action == :rename
- old_path_name = Gitlab::Git::PathHelper.normalize_path(file[:previous_path])
- old_filename = old_path_name.to_s
- file_entry = index.get(old_filename)
- index.remove(old_filename) unless file_entry.blank?
- end
-
- if file_entry
- raise Gitlab::Git::Repository::InvalidBlobName.new("Filename already exists; update not allowed") unless update
-
- # Preserve the current file mode if one is available
- mode = file_entry[:mode] if file_entry[:mode]
- end
-
- content = file[:content]
- detect = CharlockHolmes::EncodingDetector.new.detect(content) if content
-
- unless detect && detect[:type] == :binary
- # When writing to the repo directly as we are doing here,
- # the `core.autocrlf` config isn't taken into account.
- content.gsub!("\r\n", "\n") if repository.autocrlf
- end
-
- oid = repo.write(content, :blob)
- index.add(path: filename, oid: oid, mode: mode)
- end
-
- opts = {}
- opts[:tree] = index.write_tree(repo)
- opts[:author] = author
- opts[:committer] = committer
- opts[:message] = commit[:message]
- opts[:parents] = parents
- opts[:update_ref] = ref if update_ref
-
- Rugged::Commit.create(repo, opts)
- end
- # rubocop:enable Metrics/AbcSize
- # rubocop:enable Metrics/CyclomaticComplexity
- # rubocop:enable Metrics/PerceivedComplexity
-
- # Remove file from repository and return commit sha
- #
- # options should contain next structure:
- # file: {
- # path: 'documents/story.txt'
- # },
- # author: {
- # email: 'user@example.com',
- # name: 'Test User',
- # time: Time.now
- # },
- # committer: {
- # email: 'user@example.com',
- # name: 'Test User',
- # time: Time.now
- # },
- # commit: {
- # message: 'Remove FILENAME',
- # branch: 'master'
- # }
- #
- def remove(repository, options)
- commit(repository, options, :remove)
- end
-
- # Rename file from repository and return commit sha
- #
- # options should contain next structure:
- # file: {
- # previous_path: 'documents/old_story.txt'
- # path: 'documents/story.txt'
- # content: 'Lorem ipsum...',
- # update: true
- # },
- # author: {
- # email: 'user@example.com',
- # name: 'Test User',
- # time: Time.now
- # },
- # committer: {
- # email: 'user@example.com',
- # name: 'Test User',
- # time: Time.now
- # },
- # commit: {
- # message: 'Rename FILENAME',
- # branch: 'master'
- # }
- #
- def rename(repository, options)
- commit(repository, options, :rename)
- end
end
def initialize(options)
diff --git a/lib/gitlab/git/blob_snippet.rb b/lib/gitlab/git/blob_snippet.rb
index e98de57fc22..d7975f88aaa 100644
--- a/lib/gitlab/git/blob_snippet.rb
+++ b/lib/gitlab/git/blob_snippet.rb
@@ -13,7 +13,7 @@ module Gitlab
end
def data
- lines.join("\n") if lines
+ lines&.join("\n")
end
def name
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index d785516ebdd..3a73697dc5d 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -14,6 +14,8 @@ module Gitlab
attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator
+ delegate :tree, to: :raw_commit
+
def ==(other)
return false unless other.is_a?(Gitlab::Git::Commit)
@@ -218,10 +220,6 @@ module Gitlab
raw_commit.parents.map { |c| Gitlab::Git::Commit.new(c) }
end
- def tree
- raw_commit.tree
- end
-
def stats
Gitlab::Git::CommitStats.new(self)
end
diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb
index d6b3b5705a9..019be151353 100644
--- a/lib/gitlab/git/diff.rb
+++ b/lib/gitlab/git/diff.rb
@@ -2,7 +2,7 @@
module Gitlab
module Git
class Diff
- class TimeoutError < StandardError; end
+ TimeoutError = Class.new(StandardError)
include Gitlab::Git::EncodingHelper
# Diff properties
@@ -176,9 +176,13 @@ module Gitlab
def initialize(raw_diff, collapse: false)
case raw_diff
when Hash
- init_from_hash(raw_diff, collapse: collapse)
+ init_from_hash(raw_diff)
+ prune_diff_if_eligible(collapse)
when Rugged::Patch, Rugged::Diff::Delta
init_from_rugged(raw_diff, collapse: collapse)
+ when Gitaly::CommitDiffResponse
+ init_from_gitaly(raw_diff)
+ prune_diff_if_eligible(collapse)
when nil
raise "Nil as raw diff passed"
else
@@ -266,13 +270,26 @@ module Gitlab
@diff = encode!(strip_diff_headers(patch.to_s))
end
- def init_from_hash(hash, collapse: false)
+ def init_from_hash(hash)
raw_diff = hash.symbolize_keys
serialize_keys.each do |key|
send(:"#{key}=", raw_diff[key.to_sym])
end
+ end
+
+ def init_from_gitaly(diff_msg)
+ @diff = diff_msg.raw_chunks.join
+ @new_path = encode!(diff_msg.to_path.dup)
+ @old_path = encode!(diff_msg.from_path.dup)
+ @a_mode = diff_msg.old_mode.to_s(8)
+ @b_mode = diff_msg.new_mode.to_s(8)
+ @new_file = diff_msg.from_id == BLANK_SHA
+ @renamed_file = diff_msg.from_path != diff_msg.to_path
+ @deleted_file = diff_msg.to_id == BLANK_SHA
+ end
+ def prune_diff_if_eligible(collapse = false)
prune_large_diff! if too_large?
prune_collapsed_diff! if collapse && collapsible?
end
diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb
index 65e06f5065d..4e45ec7c174 100644
--- a/lib/gitlab/git/diff_collection.rb
+++ b/lib/gitlab/git/diff_collection.rb
@@ -30,7 +30,9 @@ module Gitlab
elsif @deltas_only
each_delta(&block)
else
- each_patch(&block)
+ Gitlab::GitalyClient.migrate(:commit_raw_diffs) do
+ each_patch(&block)
+ end
end
end
diff --git a/lib/gitlab/git/index.rb b/lib/gitlab/git/index.rb
new file mode 100644
index 00000000000..af1744c9c46
--- /dev/null
+++ b/lib/gitlab/git/index.rb
@@ -0,0 +1,126 @@
+module Gitlab
+ module Git
+ class Index
+ DEFAULT_MODE = 0o100644
+
+ attr_reader :repository, :raw_index
+
+ def initialize(repository)
+ @repository = repository
+ @raw_index = repository.rugged.index
+ end
+
+ delegate :read_tree, :get, to: :raw_index
+
+ def write_tree
+ raw_index.write_tree(repository.rugged)
+ end
+
+ def dir_exists?(path)
+ raw_index.find { |entry| entry[:path].start_with?("#{path}/") }
+ end
+
+ def create(options)
+ options = normalize_options(options)
+
+ file_entry = get(options[:file_path])
+ if file_entry
+ raise Gitlab::Git::Repository::InvalidBlobName.new("Filename already exists")
+ end
+
+ add_blob(options)
+ end
+
+ def create_dir(options)
+ options = normalize_options(options)
+
+ file_entry = get(options[:file_path])
+ if file_entry
+ raise Gitlab::Git::Repository::InvalidBlobName.new("Directory already exists as a file")
+ end
+
+ if dir_exists?(options[:file_path])
+ raise Gitlab::Git::Repository::InvalidBlobName.new("Directory already exists")
+ end
+
+ options = options.dup
+ options[:file_path] += '/.gitkeep'
+ options[:content] = ''
+
+ add_blob(options)
+ end
+
+ def update(options)
+ options = normalize_options(options)
+
+ file_entry = get(options[:file_path])
+ unless file_entry
+ raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist")
+ end
+
+ add_blob(options, mode: file_entry[:mode])
+ end
+
+ def move(options)
+ options = normalize_options(options)
+
+ file_entry = get(options[:previous_path])
+ unless file_entry
+ raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist")
+ end
+
+ raw_index.remove(options[:previous_path])
+
+ add_blob(options, mode: file_entry[:mode])
+ end
+
+ def delete(options)
+ options = normalize_options(options)
+
+ file_entry = get(options[:file_path])
+ unless file_entry
+ raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist")
+ end
+
+ raw_index.remove(options[:file_path])
+ end
+
+ private
+
+ def normalize_options(options)
+ options = options.dup
+ options[:file_path] = normalize_path(options[:file_path]) if options[:file_path]
+ options[:previous_path] = normalize_path(options[:previous_path]) if options[:previous_path]
+ options
+ end
+
+ def normalize_path(path)
+ pathname = Gitlab::Git::PathHelper.normalize_path(path.dup)
+
+ if pathname.each_filename.include?('..')
+ raise Gitlab::Git::Repository::InvalidBlobName.new('Invalid path')
+ end
+
+ pathname.to_s
+ end
+
+ def add_blob(options, mode: nil)
+ content = options[:content]
+ content = Base64.decode64(content) if options[:encoding] == 'base64'
+
+ detect = CharlockHolmes::EncodingDetector.new.detect(content)
+ unless detect && detect[:type] == :binary
+ # When writing to the repo directly as we are doing here,
+ # the `core.autocrlf` config isn't taken into account.
+ content.gsub!("\r\n", "\n") if repository.autocrlf
+ end
+
+ oid = repository.rugged.write(content, :blob)
+
+ raw_index.add(path: options[:file_path], oid: oid, mode: mode || DEFAULT_MODE)
+ rescue Rugged::IndexError => e
+ raise Gitlab::Git::Repository::InvalidBlobName.new(e.message)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 7068e68a855..32aebb6f6f0 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -1,5 +1,4 @@
# Gitlab::Git::Repository is a wrapper around native Rugged::Repository object
-require 'forwardable'
require 'tempfile'
require 'forwardable'
require "rubygems/package"
@@ -7,14 +6,13 @@ require "rubygems/package"
module Gitlab
module Git
class Repository
- extend Forwardable
include Gitlab::Git::Popen
SEARCH_CONTEXT_LINES = 3
- class NoRepository < StandardError; end
- class InvalidBlobName < StandardError; end
- class InvalidRef < StandardError; end
+ NoRepository = Class.new(StandardError)
+ InvalidBlobName = Class.new(StandardError)
+ InvalidRef = Class.new(StandardError)
# Full path to repo
attr_reader :path
@@ -33,6 +31,10 @@ module Gitlab
@attributes = Gitlab::Git::Attributes.new(path)
end
+ delegate :empty?,
+ :bare?,
+ to: :rugged
+
# Default branch in the repository
def root_ref
@root_ref ||= discover_default_branch
@@ -162,14 +164,6 @@ module Gitlab
!empty?
end
- def empty?
- rugged.empty?
- end
-
- def bare?
- rugged.bare?
- end
-
def repo_exists?
!!rugged
end
@@ -205,13 +199,17 @@ module Gitlab
nil
end
+ def archive_prefix(ref, sha)
+ project_name = self.name.chomp('.git')
+ "#{project_name}-#{ref.tr('/', '-')}-#{sha}"
+ end
+
def archive_metadata(ref, storage_path, format = "tar.gz")
ref ||= root_ref
commit = Gitlab::Git::Commit.find(self, ref)
return {} if commit.nil?
- project_name = self.name.chomp('.git')
- prefix = "#{project_name}-#{ref}-#{commit.id}"
+ prefix = archive_prefix(ref, commit.id)
{
'RepoPath' => path,
@@ -322,7 +320,7 @@ module Gitlab
def log_by_walk(sha, options)
walk_options = {
show: sha,
- sort: Rugged::SORT_DATE,
+ sort: Rugged::SORT_NONE,
limit: options[:limit],
offset: options[:offset]
}
@@ -330,24 +328,47 @@ module Gitlab
end
def log_by_shell(sha, options)
- cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} log)
- cmd += %W(-n #{options[:limit].to_i})
- cmd += %w(--format=%H)
- cmd += %W(--skip=#{options[:offset].to_i})
- cmd += %w(--follow) if options[:follow]
- cmd += %w(--no-merges) if options[:skip_merges]
- cmd += %W(--after=#{options[:after].iso8601}) if options[:after]
- cmd += %W(--before=#{options[:before].iso8601}) if options[:before]
- cmd += [sha]
- cmd += %W(-- #{options[:path]}) if options[:path].present?
-
- raw_output = IO.popen(cmd) {|io| io.read }
-
- log = raw_output.lines.map do |c|
- Rugged::Commit.new(rugged, c.strip)
+ limit = options[:limit].to_i
+ offset = options[:offset].to_i
+ use_follow_flag = options[:follow] && options[:path].present?
+
+ # We will perform the offset in Ruby because --follow doesn't play well with --skip.
+ # See: https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
+ offset_in_ruby = use_follow_flag && options[:offset].present?
+ limit += offset if offset_in_ruby
+
+ cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} log]
+ cmd << "--max-count=#{limit}"
+ cmd << '--format=%H'
+ cmd << "--skip=#{offset}" unless offset_in_ruby
+ cmd << '--follow' if use_follow_flag
+ cmd << '--no-merges' if options[:skip_merges]
+ cmd << "--after=#{options[:after].iso8601}" if options[:after]
+ cmd << "--before=#{options[:before].iso8601}" if options[:before]
+ cmd << sha
+
+ # :path can be a string or an array of strings
+ if options[:path].present?
+ cmd << '--'
+ cmd += Array(options[:path])
end
- log.is_a?(Array) ? log : []
+ raw_output = IO.popen(cmd) { |io| io.read }
+ lines = offset_in_ruby ? raw_output.lines.drop(offset) : raw_output.lines
+
+ lines.map! { |c| Rugged::Commit.new(rugged, c.strip) }
+ end
+
+ def count_commits(options)
+ cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} rev-list]
+ cmd << "--after=#{options[:after].iso8601}" if options[:after]
+ cmd << "--before=#{options[:before].iso8601}" if options[:before]
+ cmd += %W[--count #{options[:ref]}]
+ cmd += %W[-- #{options[:path]}] if options[:path].present?
+
+ raw_output = IO.popen(cmd) { |io| io.read }
+
+ raw_output.to_i
end
def sha_from_ref(ref)
@@ -366,7 +387,7 @@ module Gitlab
# a detailed list of valid arguments.
def commits_between(from, to)
walker = Rugged::Walker.new(rugged)
- walker.sorting(Rugged::SORT_DATE | Rugged::SORT_REVERSE)
+ walker.sorting(Rugged::SORT_NONE | Rugged::SORT_REVERSE)
sha_from = sha_from_ref(from)
sha_to = sha_from_ref(to)
@@ -390,6 +411,11 @@ module Gitlab
rugged.merge_base(from, to)
end
+ # Returns true is +from+ is direct ancestor to +to+, otherwise false
+ def is_ancestor?(from, to)
+ Gitlab::GitalyClient::Commit.is_ancestor(self, from, to)
+ end
+
# Return an array of Diff objects that represent the diff
# between +from+ and +to+. See Diff::filter_diff_options for the allowed
# diff options. The +options+ hash can also include :break_rewrites to
@@ -444,7 +470,7 @@ module Gitlab
if actual_options[:order] == :topo
walker.sorting(Rugged::SORT_TOPO)
else
- walker.sorting(Rugged::SORT_DATE)
+ walker.sorting(Rugged::SORT_NONE)
end
commits = []
@@ -565,9 +591,7 @@ module Gitlab
# will trigger a +:mixed+ reset and the working directory will be
# replaced with the content of the index. (Untracked and ignored files
# will be left alone)
- def reset(ref, reset_type)
- rugged.reset(ref, reset_type)
- end
+ delegate :reset, to: :rugged
# Mimic the `git clean` command and recursively delete untracked files.
# Valid keys that can be passed in the +options+ hash are:
@@ -814,23 +838,6 @@ module Gitlab
Rugged::Commit.create(rugged, actual_options)
end
- def commits_since(from_date)
- walker = Rugged::Walker.new(rugged)
- walker.sorting(Rugged::SORT_DATE | Rugged::SORT_REVERSE)
-
- rugged.references.each("refs/heads/*") do |ref|
- walker.push(ref.target_id)
- end
-
- commits = []
- walker.each do |commit|
- break if commit.author[:time].to_date < from_date
- commits.push(commit)
- end
-
- commits
- end
-
AUTOCRLF_VALUES = {
"true" => true,
"false" => false,
@@ -845,57 +852,6 @@ module Gitlab
rugged.config['core.autocrlf'] = AUTOCRLF_VALUES.invert[value]
end
- # Create a new directory with a .gitkeep file. Creates
- # all required nested directories (i.e. mkdir -p behavior)
- #
- # options should contain next structure:
- # author: {
- # email: 'user@example.com',
- # name: 'Test User',
- # time: Time.now
- # },
- # committer: {
- # email: 'user@example.com',
- # name: 'Test User',
- # time: Time.now
- # },
- # commit: {
- # message: 'Wow such commit',
- # branch: 'master',
- # update_ref: false
- # }
- def mkdir(path, options = {})
- # Check if this directory exists; if it does, then don't bother
- # adding .gitkeep file.
- ref = options[:commit][:branch]
- path = Gitlab::Git::PathHelper.normalize_path(path).to_s
- rugged_ref = rugged.ref(ref)
-
- raise InvalidRef.new("Invalid ref") if rugged_ref.nil?
-
- target_commit = rugged_ref.target
-
- raise InvalidRef.new("Invalid target commit") if target_commit.nil?
-
- entry = tree_entry(target_commit, path)
-
- if entry
- if entry[:type] == :blob
- raise InvalidBlobName.new("Directory already exists as a file")
- else
- raise InvalidBlobName.new("Directory already exists")
- end
- end
-
- options[:file] = {
- content: '',
- path: "#{path}/.gitkeep",
- update: true
- }
-
- Gitlab::Git::Blob.commit(self, options)
- end
-
# Returns result like "git ls-files" , recursive and full file path
#
# Ex.
diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb
index f7450e8b58f..b722d8a9f56 100644
--- a/lib/gitlab/git/tree.rb
+++ b/lib/gitlab/git/tree.rb
@@ -33,7 +33,7 @@ module Gitlab
root_id: root_tree.oid,
name: entry[:name],
type: entry[:type],
- mode: entry[:filemode],
+ mode: entry[:filemode].to_s(8),
path: path ? File.join(path, entry[:name]) : entry[:name],
commit_id: sha,
)
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 7e1484613f2..eea2f206902 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -10,10 +10,10 @@ module Gitlab
deploy_key_upload:
'This deploy key does not have write access to this project.',
no_repo: 'A repository for this project does not exist yet.'
- }
+ }.freeze
- DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }
- PUSH_COMMANDS = %w{ git-receive-pack }
+ DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }.freeze
+ PUSH_COMMANDS = %w{ git-receive-pack }.freeze
ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS
attr_reader :actor, :project, :protocol, :user_access, :authentication_abilities
@@ -153,7 +153,9 @@ module Gitlab
user_access: user_access,
project: project,
env: @env,
- skip_authorization: deploy_key?).exec
+ skip_authorization: deploy_key?,
+ protocol: protocol
+ ).exec
end
def matching_merge_request?(newrev, branch_name)
diff --git a/lib/gitlab/git_post_receive.rb b/lib/gitlab/git_post_receive.rb
index d32bdd86427..6babea144c7 100644
--- a/lib/gitlab/git_post_receive.rb
+++ b/lib/gitlab/git_post_receive.rb
@@ -30,11 +30,11 @@ module Gitlab
def retrieve_project_and_type
@type = :project
- @project = Project.find_with_namespace(@repo_path)
+ @project = Project.find_by_full_path(@repo_path)
if @repo_path.end_with?('.wiki') && !@project
@type = :wiki
- @project = Project.find_with_namespace(@repo_path.gsub(/\.wiki\z/, ''))
+ @project = Project.find_by_full_path(@repo_path.gsub(/\.wiki\z/, ''))
end
end
diff --git a/lib/gitlab/git_ref_validator.rb b/lib/gitlab/git_ref_validator.rb
index 4d83d8e72a8..0e87ee30c98 100644
--- a/lib/gitlab/git_ref_validator.rb
+++ b/lib/gitlab/git_ref_validator.rb
@@ -5,6 +5,9 @@ module Gitlab
#
# Returns true for a valid reference name, false otherwise
def validate(ref_name)
+ return false if ref_name.start_with?('refs/heads/')
+ return false if ref_name.start_with?('refs/remotes/')
+
Gitlab::Utils.system_silent(
%W(#{Gitlab.config.git.bin_path} check-ref-format refs/#{ref_name}))
end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
new file mode 100644
index 00000000000..fe15fb12adb
--- /dev/null
+++ b/lib/gitlab/gitaly_client.rb
@@ -0,0 +1,54 @@
+require 'gitaly'
+
+module Gitlab
+ module GitalyClient
+ SERVER_VERSION_FILE = 'GITALY_SERVER_VERSION'.freeze
+
+ def self.configure_channel(storage, address)
+ @addresses ||= {}
+ @addresses[storage] = address
+ @channels ||= {}
+ @channels[storage] = new_channel(address)
+ end
+
+ def self.new_channel(address)
+ address = address.sub(%r{^tcp://}, '') if URI(address).scheme == 'tcp'
+ # NOTE: When Gitaly runs on a Unix socket, permissions are
+ # handled using the file system and no additional authentication is
+ # required (therefore the :this_channel_is_insecure flag)
+ # TODO: Add authentication support when Gitaly is running on a TCP socket.
+ GRPC::Core::Channel.new(address, {}, :this_channel_is_insecure)
+ end
+
+ def self.get_channel(storage)
+ @channels[storage]
+ end
+
+ def self.get_address(storage)
+ @addresses[storage]
+ end
+
+ def self.enabled?
+ Gitlab.config.gitaly.enabled
+ end
+
+ def self.feature_enabled?(feature)
+ enabled? && ENV["GITALY_#{feature.upcase}"] == '1'
+ end
+
+ def self.migrate(feature)
+ is_enabled = feature_enabled?(feature)
+ metric_name = feature.to_s
+ metric_name += "_gitaly" if is_enabled
+
+ Gitlab::Metrics.measure(metric_name) do
+ yield is_enabled
+ end
+ end
+
+ def self.expected_server_version
+ path = Rails.root.join(SERVER_VERSION_FILE)
+ path.read.chomp
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client/commit.rb b/lib/gitlab/gitaly_client/commit.rb
new file mode 100644
index 00000000000..f15faebe27e
--- /dev/null
+++ b/lib/gitlab/gitaly_client/commit.rb
@@ -0,0 +1,41 @@
+module Gitlab
+ module GitalyClient
+ class Commit
+ # The ID of empty tree.
+ # See http://stackoverflow.com/a/40884093/1856239 and https://github.com/git/git/blob/3ad8b5bf26362ac67c9020bf8c30eee54a84f56d/cache.h#L1011-L1012
+ EMPTY_TREE_ID = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'.freeze
+
+ class << self
+ def diff_from_parent(commit, options = {})
+ project = commit.project
+ channel = GitalyClient.get_channel(project.repository_storage)
+ stub = Gitaly::Diff::Stub.new(nil, nil, channel_override: channel)
+ repo = Gitaly::Repository.new(path: project.repository.path_to_repo)
+ parent = commit.parents[0]
+ parent_id = parent ? parent.id : EMPTY_TREE_ID
+ request = Gitaly::CommitDiffRequest.new(
+ repository: repo,
+ left_commit_id: parent_id,
+ right_commit_id: commit.id
+ )
+
+ Gitlab::Git::DiffCollection.new(stub.commit_diff(request), options)
+ end
+
+ def is_ancestor(repository, ancestor_id, child_id)
+ project = Project.find_by_path(repository.path)
+ channel = GitalyClient.get_channel(project.repository_storage)
+ stub = Gitaly::Commit::Stub.new(nil, nil, channel_override: channel)
+ repo = Gitaly::Repository.new(path: repository.path_to_repo)
+ request = Gitaly::CommitIsAncestorRequest.new(
+ repository: repo,
+ ancestor_id: ancestor_id,
+ child_id: child_id
+ )
+
+ stub.commit_is_ancestor(request).value
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client/notifications.rb b/lib/gitlab/gitaly_client/notifications.rb
new file mode 100644
index 00000000000..cbfb129c002
--- /dev/null
+++ b/lib/gitlab/gitaly_client/notifications.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module GitalyClient
+ class Notifications
+ attr_accessor :stub
+
+ def initialize(repo_path)
+ full_path = Gitlab::RepoPath.strip_storage_path(repo_path).
+ sub(/\.git\z/, '').sub(/\.wiki\z/, '')
+ @project = Project.find_by_full_path(full_path)
+
+ channel = GitalyClient.get_channel(@project.repository_storage)
+ @stub = Gitaly::Notifications::Stub.new(nil, nil, channel_override: channel)
+ end
+
+ def post_receive
+ repository = Gitaly::Repository.new(path: @project.repository.path_to_repo)
+ request = Gitaly::PostReceiveRequest.new(repository: repository)
+ @stub.post_receive(request)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb
index 95dba9a327b..8c80791e7c9 100644
--- a/lib/gitlab/github_import/base_formatter.rb
+++ b/lib/gitlab/github_import/base_formatter.rb
@@ -1,11 +1,12 @@
module Gitlab
module GithubImport
class BaseFormatter
- attr_reader :formatter, :project, :raw_data
+ attr_reader :client, :formatter, :project, :raw_data
- def initialize(project, raw_data)
+ def initialize(project, raw_data, client = nil)
@project = project
@raw_data = raw_data
+ @client = client
@formatter = Gitlab::ImportFormatter.new
end
@@ -18,19 +19,6 @@ module Gitlab
def url
raw_data.url || ''
end
-
- private
-
- def gitlab_user_id(github_id)
- User.joins(:identities).
- find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s).
- try(:id)
- end
-
- def gitlab_author_id
- return @gitlab_author_id if defined?(@gitlab_author_id)
- @gitlab_author_id = gitlab_user_id(raw_data.user.id)
- end
end
end
end
diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb
index 0a8d05b5fe1..8aa885fb811 100644
--- a/lib/gitlab/github_import/branch_formatter.rb
+++ b/lib/gitlab/github_import/branch_formatter.rb
@@ -11,6 +11,14 @@ module Gitlab
sha.present? && ref.present?
end
+ def user
+ raw_data.user&.login || 'unknown'
+ end
+
+ def short_sha
+ Commit.truncate_sha(sha)
+ end
+
private
def branch_exists?
@@ -18,7 +26,7 @@ module Gitlab
end
def commit_exists?
- project.repository.commit(sha).present?
+ project.repository.branch_names_contains(sha).include?(ref)
end
def short_id
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index ba869faa92e..7dbeec5b010 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -10,6 +10,7 @@ module Gitlab
@access_token = access_token
@host = host.to_s.sub(%r{/+\z}, '')
@api_version = api_version
+ @users = {}
if access_token
::Octokit.auto_paginate = false
@@ -64,6 +65,13 @@ module Gitlab
api.respond_to?(method) || super
end
+ def user(login)
+ return nil unless login.present?
+ return @users[login] if @users.key?(login)
+
+ @users[login] = api.user(login)
+ end
+
private
def api_endpoint
diff --git a/lib/gitlab/github_import/comment_formatter.rb b/lib/gitlab/github_import/comment_formatter.rb
index 2bddcde2b7c..e21922070c1 100644
--- a/lib/gitlab/github_import/comment_formatter.rb
+++ b/lib/gitlab/github_import/comment_formatter.rb
@@ -1,6 +1,8 @@
module Gitlab
module GithubImport
class CommentFormatter < BaseFormatter
+ attr_writer :author_id
+
def attributes
{
project: project,
@@ -17,11 +19,11 @@ module Gitlab
private
def author
- raw_data.user.login
+ @author ||= UserFormatter.new(client, raw_data.user)
end
def author_id
- gitlab_author_id || project.creator_id
+ author.gitlab_id || project.creator_id
end
def body
@@ -52,10 +54,10 @@ module Gitlab
end
def note
- if gitlab_author_id
+ if author.gitlab_id
body
else
- formatter.author_line(author) + body
+ formatter.author_line(author.login) + body
end
end
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index ec1318ab33c..a8c0b47e786 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -110,12 +110,12 @@ module Gitlab
def import_issues
fetch_resources(:issues, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |issues|
issues.each do |raw|
- gh_issue = IssueFormatter.new(project, raw)
+ gh_issue = IssueFormatter.new(project, raw, client)
begin
issuable =
if gh_issue.pull_request?
- MergeRequest.find_by_iid(gh_issue.number)
+ MergeRequest.find_by(target_project_id: project.id, iid: gh_issue.number)
else
gh_issue.create!
end
@@ -131,7 +131,8 @@ module Gitlab
def import_pull_requests
fetch_resources(:pull_requests, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests|
pull_requests.each do |raw|
- gh_pull_request = PullRequestFormatter.new(project, raw)
+ gh_pull_request = PullRequestFormatter.new(project, raw, client)
+
next unless gh_pull_request.valid?
begin
@@ -156,7 +157,7 @@ module Gitlab
end
def restore_source_branch(pull_request)
- project.repository.fetch_ref(repo_url, "pull/#{pull_request.number}/head", pull_request.source_branch_name)
+ project.repository.create_branch(pull_request.source_branch_name, pull_request.source_branch_sha)
end
def restore_target_branch(pull_request)
@@ -170,6 +171,8 @@ module Gitlab
end
def clean_up_restored_branches(pull_request)
+ return if pull_request.opened?
+
remove_branch(pull_request.source_branch_name) unless pull_request.source_branch_exists?
remove_branch(pull_request.target_branch_name) unless pull_request.target_branch_exists?
end
@@ -209,11 +212,17 @@ module Gitlab
ActiveRecord::Base.no_touching do
comments.each do |raw|
begin
- comment = CommentFormatter.new(project, raw)
+ comment = CommentFormatter.new(project, raw, client)
+
# GH does not return info about comment's parent, so we guess it by checking its URL!
*_, parent, iid = URI(raw.html_url).path.split('/')
- issuable_class = parent == 'issues' ? Issue : MergeRequest
- issuable = issuable_class.find_by_iid(iid)
+
+ issuable = if parent == 'issues'
+ Issue.find_by(project_id: project.id, iid: iid)
+ else
+ MergeRequest.find_by(target_project_id: project.id, iid: iid)
+ end
+
next unless issuable
issuable.notes.create!(comment.attributes)
@@ -278,7 +287,7 @@ module Gitlab
def fetch_resources(resource_type, *opts)
return if imported?(resource_type)
- opts.last.merge!(page: current_page(resource_type))
+ opts.last[:page] = current_page(resource_type)
client.public_send(resource_type, *opts) do |resources|
yield resources
diff --git a/lib/gitlab/github_import/issuable_formatter.rb b/lib/gitlab/github_import/issuable_formatter.rb
index 256f360efc7..27b171d6ddb 100644
--- a/lib/gitlab/github_import/issuable_formatter.rb
+++ b/lib/gitlab/github_import/issuable_formatter.rb
@@ -1,13 +1,13 @@
module Gitlab
module GithubImport
class IssuableFormatter < BaseFormatter
+ attr_writer :assignee_id, :author_id
+
def project_association
raise NotImplementedError
end
- def number
- raw_data.number
- end
+ delegate :number, to: :raw_data
def find_condition
{ iid: number }
@@ -23,18 +23,24 @@ module Gitlab
raw_data.assignee.present?
end
- def assignee_id
+ def author
+ @author ||= UserFormatter.new(client, raw_data.user)
+ end
+
+ def author_id
+ @author_id ||= author.gitlab_id || project.creator_id
+ end
+
+ def assignee
if assigned?
- gitlab_user_id(raw_data.assignee.id)
+ @assignee ||= UserFormatter.new(client, raw_data.assignee)
end
end
- def author
- raw_data.user.login
- end
+ def assignee_id
+ return @assignee_id if defined?(@assignee_id)
- def author_id
- gitlab_author_id || project.creator_id
+ @assignee_id = assignee.try(:gitlab_id)
end
def body
@@ -42,10 +48,10 @@ module Gitlab
end
def description
- if gitlab_author_id
+ if author.gitlab_id
body
else
- formatter.author_line(author) + body
+ formatter.author_line(author.login) + body
end
end
diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb
index 3f635be22ba..a55adc9b1c8 100644
--- a/lib/gitlab/github_import/project_creator.rb
+++ b/lib/gitlab/github_import/project_creator.rb
@@ -1,6 +1,8 @@
module Gitlab
module GithubImport
class ProjectCreator
+ include Gitlab::CurrentSettings
+
attr_reader :repo, :name, :namespace, :current_user, :session_data, :type
def initialize(repo, name, namespace, current_user, session_data, type: 'github')
@@ -34,7 +36,7 @@ module Gitlab
end
def visibility_level
- repo.private ? Gitlab::VisibilityLevel::PRIVATE : ApplicationSetting.current.default_project_visibility
+ repo.private ? Gitlab::VisibilityLevel::PRIVATE : current_application_settings.default_project_visibility
end
#
diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb
index 4ea0200e89b..150afa31432 100644
--- a/lib/gitlab/github_import/pull_request_formatter.rb
+++ b/lib/gitlab/github_import/pull_request_formatter.rb
@@ -1,8 +1,8 @@
module Gitlab
module GithubImport
class PullRequestFormatter < IssuableFormatter
- delegate :exists?, :project, :ref, :repo, :sha, to: :source_branch, prefix: true
- delegate :exists?, :project, :ref, :repo, :sha, to: :target_branch, prefix: true
+ delegate :user, :project, :ref, :repo, :sha, to: :source_branch, prefix: true
+ delegate :user, :exists?, :project, :ref, :repo, :sha, :short_sha, to: :target_branch, prefix: true
def attributes
{
@@ -20,7 +20,8 @@ module Gitlab
author_id: author_id,
assignee_id: assignee_id,
created_at: raw_data.created_at,
- updated_at: raw_data.updated_at
+ updated_at: raw_data.updated_at,
+ imported: true
}
end
@@ -37,9 +38,20 @@ module Gitlab
end
def source_branch_name
- @source_branch_name ||= begin
- source_branch_exists? ? source_branch_ref : "pull/#{number}/#{source_branch_ref}"
- end
+ @source_branch_name ||=
+ if cross_project? || !source_branch_exists?
+ source_branch_name_prefixed
+ else
+ source_branch_ref
+ end
+ end
+
+ def source_branch_name_prefixed
+ "gh-#{target_branch_short_sha}/#{number}/#{source_branch_user}/#{source_branch_ref}"
+ end
+
+ def source_branch_exists?
+ !cross_project? && source_branch.exists?
end
def target_branch
@@ -47,9 +59,21 @@ module Gitlab
end
def target_branch_name
- @target_branch_name ||= begin
- target_branch_exists? ? target_branch_ref : "pull/#{number}/#{target_branch_ref}"
- end
+ @target_branch_name ||= target_branch_exists? ? target_branch_ref : target_branch_name_prefixed
+ end
+
+ def target_branch_name_prefixed
+ "gl-#{target_branch_short_sha}/#{number}/#{target_branch_user}/#{target_branch_ref}"
+ end
+
+ def cross_project?
+ return true if source_branch_repo.nil?
+
+ source_branch_repo.id != target_branch_repo.id
+ end
+
+ def opened?
+ state == 'opened'
end
private
diff --git a/lib/gitlab/github_import/user_formatter.rb b/lib/gitlab/github_import/user_formatter.rb
new file mode 100644
index 00000000000..04c2964da20
--- /dev/null
+++ b/lib/gitlab/github_import/user_formatter.rb
@@ -0,0 +1,45 @@
+module Gitlab
+ module GithubImport
+ class UserFormatter
+ attr_reader :client, :raw
+
+ delegate :id, :login, to: :raw, allow_nil: true
+
+ def initialize(client, raw)
+ @client = client
+ @raw = raw
+ end
+
+ def gitlab_id
+ return @gitlab_id if defined?(@gitlab_id)
+
+ @gitlab_id = find_by_external_uid || find_by_email
+ end
+
+ private
+
+ def email
+ @email ||= client.user(raw.login).try(:email)
+ end
+
+ def find_by_email
+ return nil unless email
+
+ User.find_by_any_email(email)
+ .try(:id)
+ end
+
+ def find_by_external_uid
+ return nil unless id
+
+ identities = ::Identity.arel_table
+
+ User.select(:id)
+ .joins(:identities).where(identities[:provider].eq(:github)
+ .and(identities[:extern_uid].eq(id)))
+ .first
+ .try(:id)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 70ffb68c9ab..956e80b4a09 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -3,13 +3,13 @@ include SentryHelper
module Gitlab
module GonHelper
def add_gon_variables
- gon.api_version = API::API.version
- gon.default_avatar_url = URI::join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s
+ gon.api_version = 'v4'
+ gon.default_avatar_url = URI.join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s
gon.max_file_size = current_application_settings.max_attachment_size
+ gon.asset_host = ActionController::Base.asset_host
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
gon.shortcuts_path = help_page_path('shortcuts')
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
- gon.award_menu_url = emojis_path
gon.katex_css_url = ActionController::Base.helpers.asset_path('katex.css')
gon.katex_js_url = ActionController::Base.helpers.asset_path('katex.js')
gon.sentry_dsn = sentry_dsn_public if sentry_enabled?
@@ -20,6 +20,7 @@ module Gitlab
if current_user
gon.current_user_id = current_user.id
gon.current_username = current_user.username
+ gon.current_user_fullname = current_user.name
end
end
end
diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb
index 1f4edc36928..b02b9737493 100644
--- a/lib/gitlab/google_code_import/importer.rb
+++ b/lib/gitlab/google_code_import/importer.rb
@@ -310,7 +310,7 @@ module Gitlab
if name == project.import_source
"##{id}"
else
- "#{project.namespace.path}/#{name}##{id}"
+ "#{project.namespace.full_path}/#{name}##{id}"
end
text = "~~#{text}~~" if deleted
text
diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb
index 9360afedfcb..d787d5db4a0 100644
--- a/lib/gitlab/highlight.rb
+++ b/lib/gitlab/highlight.rb
@@ -14,7 +14,7 @@ module Gitlab
end
def initialize(blob_name, blob_content, repository: nil)
- @formatter = Rouge::Formatters::HTMLGitlab.new
+ @formatter = Rouge::Formatters::HTMLGitlab
@repository = repository
@blob_name = blob_name
@blob_content = blob_content
@@ -28,7 +28,7 @@ module Gitlab
hl_lexer = self.lexer
end
- @formatter.format(hl_lexer.lex(text, continue: continue)).html_safe
+ @formatter.format(hl_lexer.lex(text, continue: continue), tag: hl_lexer.tag).html_safe
rescue
@formatter.format(Rouge::Lexers::PlainText.lex(text)).html_safe
end
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index eb667a85b78..8b327cfc226 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -3,7 +3,7 @@ module Gitlab
extend self
# For every version update, the version history in import_export.md has to be kept up to date.
- VERSION = '0.1.5'
+ VERSION = '0.1.6'.freeze
FILENAME_LIMIT = 50
def export_path(relative_path:)
@@ -35,7 +35,7 @@ module Gitlab
end
def export_filename(project:)
- basename = "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_#{project.namespace.path}_#{project.path}"
+ basename = "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_#{project.full_path.tr('/', '_')}"
"#{basename[0..FILENAME_LIMIT]}_export.tar.gz"
end
diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb
index f00c7460e82..90942774a2e 100644
--- a/lib/gitlab/import_export/command_line_util.rb
+++ b/lib/gitlab/import_export/command_line_util.rb
@@ -15,14 +15,6 @@ module Gitlab
execute(%W(#{git_bin_path} --git-dir=#{repo_path} bundle create #{bundle_path} --all))
end
- def git_unbundle(repo_path:, bundle_path:)
- execute(%W(#{git_bin_path} clone --bare #{bundle_path} #{repo_path}))
- end
-
- def git_restore_hooks
- execute(%W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args)
- end
-
def mkdir_p(path)
FileUtils.mkdir_p(path, mode: DEFAULT_MODE)
FileUtils.chmod(DEFAULT_MODE, path)
@@ -56,10 +48,6 @@ module Gitlab
FileUtils.copy_entry(source, destination)
true
end
-
- def repository_storage_paths_args
- Gitlab.config.repositories.storages.values
- end
end
end
end
diff --git a/lib/gitlab/import_export/error.rb b/lib/gitlab/import_export/error.rb
index e341c4d9cf8..788eedf2686 100644
--- a/lib/gitlab/import_export/error.rb
+++ b/lib/gitlab/import_export/error.rb
@@ -1,5 +1,5 @@
module Gitlab
module ImportExport
- class Error < StandardError; end
+ Error = Class.new(StandardError)
end
end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 08ad3274b38..ab74c8782f6 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -39,7 +39,6 @@ project_tree:
- :author
- :events
- :statuses
- - :variables
- :triggers
- :deploy_keys
- :services
@@ -74,6 +73,9 @@ excluded_attributes:
- :milestone_id
award_emoji:
- :awardable_id
+ statuses:
+ - :trace
+ - :token
methods:
labels:
@@ -82,6 +84,7 @@ methods:
- :type
statuses:
- :type
+ - :gl_project_id
services:
- :type
merge_request_diff:
diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb
index e9ee47fc090..063ce74ecad 100644
--- a/lib/gitlab/import_export/importer.rb
+++ b/lib/gitlab/import_export/importer.rb
@@ -56,7 +56,7 @@ module Gitlab
end
def path_with_namespace
- File.join(@project.namespace.path, @project.path)
+ File.join(@project.namespace.full_path, @project.path)
end
def repo_path
diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb
index 2405b94db50..8b8e48aac76 100644
--- a/lib/gitlab/import_export/members_mapper.rb
+++ b/lib/gitlab/import_export/members_mapper.rb
@@ -32,6 +32,10 @@ module Gitlab
@user.id
end
+ def include?(old_author_id)
+ map.keys.include?(old_author_id) && map[old_author_id] != default_user_id
+ end
+
private
def missing_keys_tracking_hash
@@ -41,6 +45,8 @@ module Gitlab
end
def ensure_default_member!
+ @project.project_members.destroy_all
+
ProjectMember.create!(user: @user, access_level: ProjectMember::MASTER, source_id: @project.id, importing: true)
end
diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb
index 2fbf437ec26..3473b466936 100644
--- a/lib/gitlab/import_export/project_tree_saver.rb
+++ b/lib/gitlab/import_export/project_tree_saver.rb
@@ -5,8 +5,9 @@ module Gitlab
attr_reader :full_path
- def initialize(project:, shared:)
+ def initialize(project:, current_user:, shared:)
@project = project
+ @current_user = current_user
@shared = shared
@full_path = File.join(@shared.export_path, ImportExport.project_filename)
end
@@ -24,7 +25,35 @@ module Gitlab
private
def project_json_tree
- @project.to_json(Gitlab::ImportExport::Reader.new(shared: @shared).project_tree)
+ project_json['project_members'] += group_members_json
+
+ project_json.to_json
+ end
+
+ def project_json
+ @project_json ||= @project.as_json(reader.project_tree)
+ end
+
+ def reader
+ @reader ||= Gitlab::ImportExport::Reader.new(shared: @shared)
+ end
+
+ def group_members_json
+ group_members.as_json(reader.group_members_tree).each do |group_member|
+ group_member['source_type'] = 'Project' # Make group members project members of the future import
+ end
+ end
+
+ def group_members
+ return [] unless @current_user.can?(:admin_group, @project.group)
+
+ # We need `.where.not(user_id: nil)` here otherwise when a group has an
+ # invitee, it would make the following query return 0 rows since a NULL
+ # user_id would be present in the subquery
+ # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
+ non_null_user_ids = @project.project_members.where.not(user_id: nil).select(:user_id)
+
+ GroupMembersFinder.new(@project.group).execute.where.not(user_id: non_null_user_ids)
end
end
end
diff --git a/lib/gitlab/import_export/reader.rb b/lib/gitlab/import_export/reader.rb
index 5021a1a14ce..a1e7159fe42 100644
--- a/lib/gitlab/import_export/reader.rb
+++ b/lib/gitlab/import_export/reader.rb
@@ -21,6 +21,10 @@ module Gitlab
false
end
+ def group_members_tree
+ @attributes_finder.find_included(:project_members).merge(include: @attributes_finder.find(:user))
+ end
+
private
# Builds a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 19e43cce768..d44563333a5 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -4,7 +4,6 @@ module Gitlab
OVERRIDES = { snippets: :project_snippets,
pipelines: 'Ci::Pipeline',
statuses: 'commit_status',
- variables: 'Ci::Variable',
triggers: 'Ci::Trigger',
builds: 'Ci::Build',
hooks: 'ProjectHook',
@@ -16,7 +15,7 @@ module Gitlab
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id merge_user_id resolved_by_id].freeze
- PROJECT_REFERENCES = %w[project_id source_project_id gl_project_id target_project_id].freeze
+ PROJECT_REFERENCES = %w[project_id source_project_id target_project_id].freeze
BUILD_MODELS = %w[Ci::Build commit_status].freeze
@@ -24,6 +23,8 @@ module Gitlab
EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels].freeze
+ TOKEN_RESET_MODELS = %w[Ci::Trigger Ci::Build ProjectHook].freeze
+
def self.create(*args)
new(*args).create
end
@@ -61,7 +62,9 @@ module Gitlab
update_project_references
handle_group_label if group_label?
- reset_ci_tokens if @relation_name == 'Ci::Trigger'
+ reset_tokens!
+ remove_encrypted_attributes!
+
@relation_hash['data'].deep_symbolize_keys! if @relation_name == :events && @relation_hash['data']
set_st_diffs if @relation_name == :merge_request_diff
end
@@ -86,7 +89,7 @@ module Gitlab
end
def has_author?(old_author_id)
- admin_user? && @members_mapper.map.keys.include?(old_author_id)
+ admin_user? && @members_mapper.include?(old_author_id)
end
def missing_author_note(updated_at, author_name)
@@ -95,12 +98,11 @@ module Gitlab
end
def generate_imported_object
- if BUILD_MODELS.include?(@relation_name) # call #trace= method after assigning the other attributes
- trace = @relation_hash.delete('trace')
+ if BUILD_MODELS.include?(@relation_name)
+ @relation_hash.delete('trace') # old export files have trace
@relation_hash.delete('token')
imported_object do |object|
- object.trace = trace
object.commit_id = nil
end
else
@@ -118,7 +120,6 @@ module Gitlab
# project_id may not be part of the export, but we always need to populate it if required.
@relation_hash['project_id'] = project_id
- @relation_hash['gl_project_id'] = project_id if @relation_hash['gl_project_id']
@relation_hash['target_project_id'] = project_id if @relation_hash['target_project_id']
end
@@ -140,11 +141,22 @@ module Gitlab
end
end
- def reset_ci_tokens
- return unless Gitlab::ImportExport.reset_tokens?
+ def reset_tokens!
+ return unless Gitlab::ImportExport.reset_tokens? && TOKEN_RESET_MODELS.include?(@relation_name.to_s)
# If we import/export a project to the same instance, tokens will have to be reset.
- @relation_hash['token'] = nil
+ # We also have to reset them to avoid issues when the gitlab secrets file cannot be copied across.
+ relation_class.attribute_names.select { |name| name.include?('token') }.each do |token|
+ @relation_hash[token] = nil
+ end
+ end
+
+ def remove_encrypted_attributes!
+ return unless relation_class.respond_to?(:encrypted_attributes) && relation_class.encrypted_attributes.any?
+
+ relation_class.encrypted_attributes.each_key do |key|
+ @relation_hash[key.to_s] = nil
+ end
end
def relation_class
diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb
index 48a9a6fa5e2..c824d3ea9fc 100644
--- a/lib/gitlab/import_export/repo_restorer.rb
+++ b/lib/gitlab/import_export/repo_restorer.rb
@@ -2,6 +2,7 @@ module Gitlab
module ImportExport
class RepoRestorer
include Gitlab::ImportExport::CommandLineUtil
+ include Gitlab::ShellAdapter
def initialize(project:, shared:, path_to_bundle:)
@project = project
@@ -12,29 +13,11 @@ module Gitlab
def restore
return true unless File.exist?(@path_to_bundle)
- mkdir_p(path_to_repo)
-
- git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle) && repo_restore_hooks
+ gitlab_shell.import_repository(@project.repository_storage_path, @project.path_with_namespace, @path_to_bundle)
rescue => e
@shared.error(e)
false
end
-
- private
-
- def path_to_repo
- @project.repository.path_to_repo
- end
-
- def repo_restore_hooks
- return true if wiki?
-
- git_restore_hooks
- end
-
- def wiki?
- @project.class.name == 'ProjectWiki'
- end
end
end
end
diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb
index 45958710c13..52276cbcd9a 100644
--- a/lib/gitlab/import_sources.rb
+++ b/lib/gitlab/import_sources.rb
@@ -5,8 +5,6 @@
#
module Gitlab
module ImportSources
- extend CurrentSettings
-
ImportSource = Struct.new(:name, :title, :importer)
ImportTable = [
diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb
index 801dfde9a36..c9122a23568 100644
--- a/lib/gitlab/incoming_email.rb
+++ b/lib/gitlab/incoming_email.rb
@@ -1,10 +1,9 @@
module Gitlab
module IncomingEmail
+ UNSUBSCRIBE_SUFFIX = '+unsubscribe'.freeze
WILDCARD_PLACEHOLDER = '%{key}'.freeze
class << self
- FALLBACK_MESSAGE_ID_REGEX = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\Z/.freeze
-
def enabled?
config.enabled && config.address
end
@@ -18,7 +17,11 @@ module Gitlab
end
def reply_address(key)
- config.address.gsub(WILDCARD_PLACEHOLDER, key)
+ config.address.sub(WILDCARD_PLACEHOLDER, key)
+ end
+
+ def unsubscribe_address(key)
+ config.address.sub(WILDCARD_PLACEHOLDER, "#{key}#{UNSUBSCRIBE_SUFFIX}")
end
def key_from_address(address)
@@ -32,10 +35,14 @@ module Gitlab
end
def key_from_fallback_message_id(mail_id)
- match = mail_id.match(FALLBACK_MESSAGE_ID_REGEX)
- return unless match
+ message_id_regexp = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\z/
- match[1]
+ mail_id[message_id_regexp, 1]
+ end
+
+ def scan_fallback_references(references)
+ # It's looking for each <...>
+ references.scan(/(?!<)[^<>]+(?=>)/)
end
def config
@@ -49,7 +56,7 @@ module Gitlab
return nil unless wildcard_address
regex = Regexp.escape(wildcard_address)
- regex = regex.gsub(Regexp.escape('%{key}'), "(.+)")
+ regex = regex.sub(Regexp.escape(WILDCARD_PLACEHOLDER), '(.+)')
Regexp.new(regex).freeze
end
end
diff --git a/lib/gitlab/job_waiter.rb b/lib/gitlab/job_waiter.rb
new file mode 100644
index 00000000000..8db91d25a4b
--- /dev/null
+++ b/lib/gitlab/job_waiter.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ # JobWaiter can be used to wait for a number of Sidekiq jobs to complete.
+ class JobWaiter
+ # The sleep interval between checking keys, in seconds.
+ INTERVAL = 0.1
+
+ # jobs - The job IDs to wait for.
+ def initialize(jobs)
+ @jobs = jobs
+ end
+
+ # Waits for all the jobs to be completed.
+ #
+ # timeout - The maximum amount of seconds to block the caller for. This
+ # ensures we don't indefinitely block a caller in case a job takes
+ # long to process, or is never processed.
+ def wait(timeout = 60)
+ start = Time.current
+
+ while (Time.current - start) <= timeout
+ break if SidekiqStatus.all_completed?(@jobs)
+
+ sleep(INTERVAL) # to not overload Redis too much.
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes.rb b/lib/gitlab/kubernetes.rb
index 288771c1c12..3a7af363548 100644
--- a/lib/gitlab/kubernetes.rb
+++ b/lib/gitlab/kubernetes.rb
@@ -43,10 +43,10 @@ module Gitlab
end
end
- def add_terminal_auth(terminal, token, ca_pem = nil)
+ def add_terminal_auth(terminal, token:, max_session_time:, ca_pem: nil)
terminal[:headers]['Authorization'] << "Bearer #{token}"
+ terminal[:max_session_time] = max_session_time
terminal[:ca_pem] = ca_pem if ca_pem.present?
- terminal
end
def container_exec_url(api_url, namespace, pod_name, container_name)
diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb
index 28129198438..46deea3cc9f 100644
--- a/lib/gitlab/ldap/config.rb
+++ b/lib/gitlab/ldap/config.rb
@@ -124,9 +124,9 @@ module Gitlab
def name_proc
if allow_username_or_email_login
- Proc.new { |name| name.gsub(/@.*\z/, '') }
+ proc { |name| name.gsub(/@.*\z/, '') }
else
- Proc.new { |name| name }
+ proc { |name| name }
end
end
diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb
index 7084fd1767d..43eb73250b7 100644
--- a/lib/gitlab/ldap/person.rb
+++ b/lib/gitlab/ldap/person.rb
@@ -43,9 +43,7 @@ module Gitlab
attribute_value(:email)
end
- def dn
- entry.dn
- end
+ delegate :dn, to: :entry
private
diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb
index b84c81f1a6c..2d5e47a6f3b 100644
--- a/lib/gitlab/ldap/user.rb
+++ b/lib/gitlab/ldap/user.rb
@@ -1,5 +1,3 @@
-require 'gitlab/o_auth/user'
-
# LDAP extension for User model
#
# * Find or create user from omniauth.auth data
diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb
index 3d1ba33ec68..857e0abf710 100644
--- a/lib/gitlab/metrics.rb
+++ b/lib/gitlab/metrics.rb
@@ -112,7 +112,7 @@ module Gitlab
def self.tag_transaction(name, value)
trans = current_transaction
- trans.add_tag(name, value) if trans
+ trans&.add_tag(name, value)
end
# Sets the action of the current transaction (if any)
@@ -121,7 +121,7 @@ module Gitlab
def self.action=(action)
trans = current_transaction
- trans.action = action if trans
+ trans&.action = action
end
# Tracks an event.
@@ -130,7 +130,7 @@ module Gitlab
def self.add_event(*args)
trans = current_transaction
- trans.add_event(*args) if trans
+ trans&.add_event(*args)
end
# Returns the prefix to use for the name of a series.
diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb
index 4b7a791e497..6aa38542cb4 100644
--- a/lib/gitlab/metrics/instrumentation.rb
+++ b/lib/gitlab/metrics/instrumentation.rb
@@ -143,11 +143,12 @@ module Gitlab
# signature this would break things. As a result we'll make sure the
# generated method _only_ accepts regular arguments if the underlying
# method also accepts them.
- if method.arity == 0
- args_signature = ''
- else
- args_signature = '*args'
- end
+ args_signature =
+ if method.arity == 0
+ ''
+ else
+ '*args'
+ end
proxy_module.class_eval <<-EOF, __FILE__, __LINE__ + 1
def #{name}(#{args_signature})
diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
index 47f88727fc8..adc0db1a874 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -2,8 +2,8 @@ module Gitlab
module Metrics
# Rack middleware for tracking Rails and Grape requests.
class RackMiddleware
- CONTROLLER_KEY = 'action_controller.instance'
- ENDPOINT_KEY = 'api.endpoint'
+ CONTROLLER_KEY = 'action_controller.instance'.freeze
+ ENDPOINT_KEY = 'api.endpoint'.freeze
CONTENT_TYPES = {
'text/html' => :html,
'text/plain' => :txt,
@@ -14,7 +14,7 @@ module Gitlab
'image/jpeg' => :jpeg,
'image/gif' => :gif,
'image/svg+xml' => :svg
- }
+ }.freeze
def initialize(app)
@app = app
diff --git a/lib/gitlab/metrics/subscribers/action_view.rb b/lib/gitlab/metrics/subscribers/action_view.rb
index 2e9dd4645e3..d435a33e9c7 100644
--- a/lib/gitlab/metrics/subscribers/action_view.rb
+++ b/lib/gitlab/metrics/subscribers/action_view.rb
@@ -5,7 +5,7 @@ module Gitlab
class ActionView < ActiveSupport::Subscriber
attach_to :action_view
- SERIES = 'views'
+ SERIES = 'views'.freeze
def render_template(event)
track(event) if current_transaction
diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb
index 287b7a83547..3aaebb3e9c3 100644
--- a/lib/gitlab/metrics/system.rb
+++ b/lib/gitlab/metrics/system.rb
@@ -11,7 +11,7 @@ module Gitlab
mem = 0
match = File.read('/proc/self/status').match(/VmRSS:\s+(\d+)/)
- if match and match[1]
+ if match && match[1]
mem = match[1].to_f * 1024
end
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
index 7bc16181be6..4f9fb1c7853 100644
--- a/lib/gitlab/metrics/transaction.rb
+++ b/lib/gitlab/metrics/transaction.rb
@@ -5,7 +5,7 @@ module Gitlab
THREAD_KEY = :_gitlab_metrics_transaction
# The series to store events (e.g. Git pushes) in.
- EVENT_SERIES = 'events'
+ EVENT_SERIES = 'events'.freeze
attr_reader :tags, :values, :method, :metrics
diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb
index 5764ab15652..6023fa1820f 100644
--- a/lib/gitlab/middleware/go.rb
+++ b/lib/gitlab/middleware/go.rb
@@ -30,21 +30,69 @@ module Gitlab
end
def go_body(request)
- base_url = Gitlab.config.gitlab.url
- # Go subpackages may be in the form of namespace/project/path1/path2/../pathN
- # We can just ignore the paths and leave the namespace/project
- path_info = request.env["PATH_INFO"]
- path_info.sub!(/^\//, '')
- project_path = path_info.split('/').first(2).join('/')
- request_url = URI.join(base_url, project_path)
- domain_path = strip_url(request_url.to_s)
+ project_url = URI.join(Gitlab.config.gitlab.url, project_path(request))
+ import_prefix = strip_url(project_url.to_s)
- "<!DOCTYPE html><html><head><meta content='#{domain_path} git #{request_url}.git' name='go-import'></head></html>\n"
+ "<!DOCTYPE html><html><head><meta content='#{import_prefix} git #{project_url}.git' name='go-import'></head></html>\n"
end
def strip_url(url)
url.gsub(/\Ahttps?:\/\//, '')
end
+
+ def project_path(request)
+ path_info = request.env["PATH_INFO"]
+ path_info.sub!(/^\//, '')
+
+ # Go subpackages may be in the form of `namespace/project/path1/path2/../pathN`.
+ # In a traditional project with a single namespace, this would denote repo
+ # `namespace/project` with subpath `path1/path2/../pathN`, but with nested
+ # groups, this could also be `namespace/project/path1` with subpath
+ # `path2/../pathN`, for example.
+
+ # We find all potential project paths out of the path segments
+ path_segments = path_info.split('/')
+ simple_project_path = path_segments.first(2).join('/')
+
+ # If the path is at most 2 segments long, it is a simple `namespace/project` path and we're done
+ return simple_project_path if path_segments.length <= 2
+
+ project_paths = []
+ begin
+ project_paths << path_segments.join('/')
+ path_segments.pop
+ end while path_segments.length >= 2
+
+ # We see if a project exists with any of these potential paths
+ project = project_for_paths(project_paths, request)
+
+ if project
+ # If a project is found and the user has access, we return the full project path
+ project.full_path
+ else
+ # If not, we return the first two components as if it were a simple `namespace/project` path,
+ # so that we don't reveal the existence of a nested project the user doesn't have access to.
+ # This means that for an unauthenticated request to `group/subgroup/project/subpackage`
+ # for a private `group/subgroup/project` with subpackage path `subpackage`, GitLab will respond
+ # as if the user is looking for project `group/subgroup`, with subpackage path `project/subpackage`.
+ # Since `go get` doesn't authenticate by default, this means that
+ # `go get gitlab.com/group/subgroup/project/subpackage` will not work for private projects.
+ # `go get gitlab.com/group/subgroup/project.git/subpackage` will work, since Go is smart enough
+ # to figure that out. `import 'gitlab.com/...'` behaves the same as `go get`.
+ simple_project_path
+ end
+ end
+
+ def project_for_paths(paths, request)
+ project = Project.where_full_path_in(paths).first
+ return unless Ability.allowed?(current_user(request), :read_project, project)
+
+ project
+ end
+
+ def current_user(request)
+ request.env['warden']&.authenticate
+ end
end
end
end
diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb
index dd99f9bb7d7..fee741b47be 100644
--- a/lib/gitlab/middleware/multipart.rb
+++ b/lib/gitlab/middleware/multipart.rb
@@ -26,7 +26,7 @@
module Gitlab
module Middleware
class Multipart
- RACK_ENV_KEY = 'HTTP_GITLAB_WORKHORSE_MULTIPART_FIELDS'
+ RACK_ENV_KEY = 'HTTP_GITLAB_WORKHORSE_MULTIPART_FIELDS'.freeze
class Handler
def initialize(env, message)
diff --git a/lib/gitlab/middleware/webpack_proxy.rb b/lib/gitlab/middleware/webpack_proxy.rb
new file mode 100644
index 00000000000..6105d165810
--- /dev/null
+++ b/lib/gitlab/middleware/webpack_proxy.rb
@@ -0,0 +1,24 @@
+# This Rack middleware is intended to proxy the webpack assets directory to the
+# webpack-dev-server. It is only intended for use in development.
+
+module Gitlab
+ module Middleware
+ class WebpackProxy < Rack::Proxy
+ def initialize(app = nil, opts = {})
+ @proxy_host = opts.fetch(:proxy_host, 'localhost')
+ @proxy_port = opts.fetch(:proxy_port, 3808)
+ @proxy_path = opts[:proxy_path] if opts[:proxy_path]
+
+ super(app, backend: "http://#{@proxy_host}:#{@proxy_port}", **opts)
+ end
+
+ def perform_request(env)
+ if @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}")
+ super(env)
+ else
+ @app.call(env)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb
index 96ed20af918..f98481c6d3a 100644
--- a/lib/gitlab/o_auth/user.rb
+++ b/lib/gitlab/o_auth/user.rb
@@ -5,7 +5,7 @@
#
module Gitlab
module OAuth
- class SignupDisabledError < StandardError; end
+ SignupDisabledError = Class.new(StandardError)
class User
attr_accessor :auth_hash, :gl_user
@@ -29,12 +29,11 @@ module Gitlab
def save(provider = 'OAuth')
unauthorized_to_create unless gl_user
- if needs_blocking?
- gl_user.save!
- gl_user.block
- else
- gl_user.save!
- end
+ block_after_save = needs_blocking?
+
+ gl_user.save!
+
+ gl_user.block if block_after_save
log.info "(#{provider}) saving user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}"
gl_user
@@ -148,10 +147,8 @@ module Gitlab
end
def build_new_user
- user = ::User.new(user_attributes)
- user.skip_confirmation!
- user.identities.new(extern_uid: auth_hash.uid, provider: auth_hash.provider)
- user
+ user_params = user_attributes.merge(extern_uid: auth_hash.uid, provider: auth_hash.provider, skip_confirmation: true)
+ Users::CreateService.new(nil, user_params).build
end
def user_attributes
diff --git a/lib/gitlab/optimistic_locking.rb b/lib/gitlab/optimistic_locking.rb
index 879d46446b3..962ff4d3985 100644
--- a/lib/gitlab/optimistic_locking.rb
+++ b/lib/gitlab/optimistic_locking.rb
@@ -1,12 +1,12 @@
module Gitlab
module OptimisticLocking
- extend self
+ module_function
def retry_lock(subject, retries = 100, &block)
loop do
begin
ActiveRecord::Base.transaction do
- return block.call(subject)
+ return yield(subject)
end
rescue ActiveRecord::StaleObjectError
retries -= 1
@@ -15,5 +15,7 @@ module Gitlab
end
end
end
+
+ alias_method :retry_optimistic_lock, :retry_lock
end
end
diff --git a/lib/gitlab/other_markup.rb b/lib/gitlab/other_markup.rb
index 4e2f8ed5587..e67acf28c94 100644
--- a/lib/gitlab/other_markup.rb
+++ b/lib/gitlab/other_markup.rb
@@ -17,6 +17,9 @@ module Gitlab
html = Banzai.post_process(html, context)
+ filter = Banzai::Filter::SanitizationFilter.new(html)
+ html = filter.call.to_s
+
html.html_safe
end
end
diff --git a/lib/gitlab/pages_transfer.rb b/lib/gitlab/pages_transfer.rb
new file mode 100644
index 00000000000..fb215f27cbd
--- /dev/null
+++ b/lib/gitlab/pages_transfer.rb
@@ -0,0 +1,7 @@
+module Gitlab
+ class PagesTransfer < ProjectTransfer
+ def root_dir
+ Gitlab.config.pages.path
+ end
+ end
+end
diff --git a/lib/gitlab/polling_interval.rb b/lib/gitlab/polling_interval.rb
new file mode 100644
index 00000000000..c44bb1cd14d
--- /dev/null
+++ b/lib/gitlab/polling_interval.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ class PollingInterval
+ include Gitlab::CurrentSettings
+
+ HEADER_NAME = 'Poll-Interval'.freeze
+
+ def self.set_header(response, interval:)
+ if polling_enabled?
+ multiplier = current_application_settings.polling_interval_multiplier
+ value = (interval * multiplier).to_i
+ else
+ value = -1
+ end
+
+ response.headers[HEADER_NAME] = value
+ end
+
+ def self.polling_enabled?
+ !current_application_settings.polling_interval_multiplier.zero?
+ end
+ end
+end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 6bdf3db9cb8..db325c00705 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -71,6 +71,14 @@ module Gitlab
)
end
+ def single_commit_result?
+ commits_count == 1 && total_result_count == 1
+ end
+
+ def total_result_count
+ issues_count + merge_requests_count + milestones_count + notes_count + blobs_count + wiki_blobs_count + commits_count
+ end
+
private
def blobs
@@ -114,7 +122,25 @@ module Gitlab
end
def commits
- @commits ||= project.repository.find_commits_by_message(query)
+ @commits ||= find_commits(query)
+ end
+
+ def find_commits(query)
+ return [] unless Ability.allowed?(@current_user, :download_code, @project)
+
+ commits = find_commits_by_message(query)
+ commit_by_sha = find_commit_by_sha(query)
+ commits |= [commit_by_sha] if commit_by_sha
+ commits
+ end
+
+ def find_commits_by_message(query)
+ project.repository.find_commits_by_message(query)
+ end
+
+ def find_commit_by_sha(query)
+ key = query.strip
+ project.repository.commit(key) if Commit.valid_hash?(key)
end
def project_ids_relation
diff --git a/lib/gitlab/project_transfer.rb b/lib/gitlab/project_transfer.rb
new file mode 100644
index 00000000000..1bba0b78e2f
--- /dev/null
+++ b/lib/gitlab/project_transfer.rb
@@ -0,0 +1,35 @@
+module Gitlab
+ class ProjectTransfer
+ def move_project(project_path, namespace_path_was, namespace_path)
+ new_namespace_folder = File.join(root_dir, namespace_path)
+ FileUtils.mkdir_p(new_namespace_folder) unless Dir.exist?(new_namespace_folder)
+ from = File.join(root_dir, namespace_path_was, project_path)
+ to = File.join(root_dir, namespace_path, project_path)
+ move(from, to, "")
+ end
+
+ def rename_project(path_was, path, namespace_path)
+ base_dir = File.join(root_dir, namespace_path)
+ move(path_was, path, base_dir)
+ end
+
+ def rename_namespace(path_was, path)
+ move(path_was, path)
+ end
+
+ def root_dir
+ raise NotImplementedError
+ end
+
+ private
+
+ def move(path_was, path, base_dir = nil)
+ base_dir = root_dir unless base_dir
+ from = File.join(base_dir, path_was)
+ to = File.join(base_dir, path)
+ FileUtils.mv(from, to)
+ rescue Errno::ENOENT
+ false
+ end
+ end
+end
diff --git a/lib/gitlab/prometheus.rb b/lib/gitlab/prometheus.rb
new file mode 100644
index 00000000000..62239779454
--- /dev/null
+++ b/lib/gitlab/prometheus.rb
@@ -0,0 +1,70 @@
+module Gitlab
+ PrometheusError = Class.new(StandardError)
+
+ # Helper methods to interact with Prometheus network services & resources
+ class Prometheus
+ attr_reader :api_url
+
+ def initialize(api_url:)
+ @api_url = api_url
+ end
+
+ def ping
+ json_api_get('query', query: '1')
+ end
+
+ def query(query)
+ get_result('vector') do
+ json_api_get('query', query: query)
+ end
+ end
+
+ def query_range(query, start: 8.hours.ago)
+ get_result('matrix') do
+ json_api_get('query_range',
+ query: query,
+ start: start.to_f,
+ end: Time.now.utc.to_f,
+ step: 1.minute.to_i)
+ end
+ end
+
+ private
+
+ def json_api_get(type, args = {})
+ get(join_api_url(type, args))
+ rescue Errno::ECONNREFUSED
+ raise PrometheusError, 'Connection refused'
+ end
+
+ def join_api_url(type, args = {})
+ url = URI.parse(api_url)
+ rescue URI::Error
+ raise PrometheusError, "Invalid API URL: #{api_url}"
+ else
+ url.path = [url.path.sub(%r{/+\z}, ''), 'api', 'v1', type].join('/')
+ url.query = args.to_query
+
+ url.to_s
+ end
+
+ def get(url)
+ handle_response(HTTParty.get(url))
+ end
+
+ def handle_response(response)
+ if response.code == 200 && response['status'] == 'success'
+ response['data'] || {}
+ elsif response.code == 400
+ raise PrometheusError, response['error'] || 'Bad data received'
+ else
+ raise PrometheusError, "#{response.code} - #{response.body}"
+ end
+ end
+
+ def get_result(expected_type)
+ data = yield
+ data['result'] if data['resultType'] == expected_type
+ end
+ end
+end
diff --git a/lib/gitlab/recaptcha.rb b/lib/gitlab/recaptcha.rb
index 70e7f25d518..4bc76ea033f 100644
--- a/lib/gitlab/recaptcha.rb
+++ b/lib/gitlab/recaptcha.rb
@@ -10,5 +10,9 @@ module Gitlab
true
end
end
+
+ def self.enabled?
+ current_application_settings.recaptcha_enabled
+ end
end
end
diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb
index 9384102acec..bc5370de32a 100644
--- a/lib/gitlab/redis.rb
+++ b/lib/gitlab/redis.rb
@@ -1,27 +1,18 @@
# This file should not have any direct dependency on Rails environment
# please require all dependencies below:
require 'active_support/core_ext/hash/keys'
+require 'active_support/core_ext/module/delegation'
module Gitlab
class Redis
- CACHE_NAMESPACE = 'cache:gitlab'
- SESSION_NAMESPACE = 'session:gitlab'
- SIDEKIQ_NAMESPACE = 'resque:gitlab'
- MAILROOM_NAMESPACE = 'mail_room:gitlab'
- DEFAULT_REDIS_URL = 'redis://localhost:6379'
- CONFIG_FILE = File.expand_path('../../config/resque.yml', __dir__)
+ CACHE_NAMESPACE = 'cache:gitlab'.freeze
+ SESSION_NAMESPACE = 'session:gitlab'.freeze
+ SIDEKIQ_NAMESPACE = 'resque:gitlab'.freeze
+ MAILROOM_NAMESPACE = 'mail_room:gitlab'.freeze
+ DEFAULT_REDIS_URL = 'redis://localhost:6379'.freeze
class << self
- # Do NOT cache in an instance variable. Result may be mutated by caller.
- def params
- new.params
- end
-
- # Do NOT cache in an instance variable. Result may be mutated by caller.
- # @deprecated Use .params instead to get sentinel support
- def url
- new.url
- end
+ delegate :params, :url, to: :new
def with
@pool ||= ConnectionPool.new(size: pool_size) { ::Redis.new(params) }
@@ -42,13 +33,17 @@ module Gitlab
return @_raw_config if defined?(@_raw_config)
begin
- @_raw_config = ERB.new(File.read(CONFIG_FILE)).result.freeze
+ @_raw_config = ERB.new(File.read(config_file)).result.freeze
rescue Errno::ENOENT
@_raw_config = false
end
@_raw_config
end
+
+ def config_file
+ ENV['GITLAB_REDIS_CONFIG_FILE'] || File.expand_path('../../config/resque.yml', __dir__)
+ end
end
def initialize(rails_env = nil)
diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb
index 11c0b01f0dc..7668ecacc4b 100644
--- a/lib/gitlab/reference_extractor.rb
+++ b/lib/gitlab/reference_extractor.rb
@@ -1,13 +1,12 @@
module Gitlab
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor < Banzai::ReferenceExtractor
- REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range)
+ REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range directly_addressed_user).freeze
attr_accessor :project, :current_user, :author
def initialize(project, current_user = nil)
@project = project
@current_user = current_user
-
@references = {}
super()
@@ -21,6 +20,11 @@ module Gitlab
super(type, project, current_user)
end
+ def reset_memoized_values
+ @references = {}
+ super()
+ end
+
REFERABLES.each do |type|
define_method("#{type}s") do
@references[type] ||= references(type)
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index a3fa7c1331a..5e5f5ff1589 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -5,13 +5,18 @@ module Gitlab
# The namespace regex is used in Javascript to validate usernames in the "Register" form. However, Javascript
# does not support the negative lookbehind assertion (?<!) that disallows usernames ending in `.git` and `.atom`.
# Since this is a non-trivial problem to solve in Javascript (heavily complicate the regex, modify view code to
- # allow non-regex validatiions, etc), `NAMESPACE_REGEX_STR_SIMPLE` serves as a Javascript-compatible version of
+ # allow non-regex validatiions, etc), `NAMESPACE_REGEX_STR_JS` serves as a Javascript-compatible version of
# `NAMESPACE_REGEX_STR`, with the negative lookbehind assertion removed. This means that the client-side validation
# will pass for usernames ending in `.atom` and `.git`, but will be caught by the server-side validation.
PATH_REGEX_STR = '[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*'.freeze
- NAMESPACE_REGEX_STR_SIMPLE = PATH_REGEX_STR + '[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze
- NAMESPACE_REGEX_STR = '(?:' + NAMESPACE_REGEX_STR_SIMPLE + ')(?<!\.git|\.atom)'.freeze
- PROJECT_REGEX_STR = PATH_REGEX_STR + '(?<!\.git|\.atom)'.freeze
+ NAMESPACE_REGEX_STR_JS = PATH_REGEX_STR + '[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze
+ NO_SUFFIX_REGEX_STR = '(?<!\.git|\.atom)'.freeze
+ NAMESPACE_REGEX_STR = "(?:#{NAMESPACE_REGEX_STR_JS})#{NO_SUFFIX_REGEX_STR}".freeze
+ PROJECT_REGEX_STR = "(?:#{PATH_REGEX_STR})#{NO_SUFFIX_REGEX_STR}".freeze
+
+ # Same as NAMESPACE_REGEX_STR but allows `/` in the path.
+ # So `group/subgroup` will match this regex but not NAMESPACE_REGEX_STR
+ FULL_NAMESPACE_REGEX_STR = "(?:#{NAMESPACE_REGEX_STR}/)*#{NAMESPACE_REGEX_STR}".freeze
def namespace_regex
@namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze
diff --git a/lib/gitlab/repo_path.rb b/lib/gitlab/repo_path.rb
new file mode 100644
index 00000000000..4b1d828c45c
--- /dev/null
+++ b/lib/gitlab/repo_path.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module RepoPath
+ NotFoundError = Class.new(StandardError)
+
+ def self.strip_storage_path(repo_path)
+ result = nil
+
+ Gitlab.config.repositories.storages.values.each do |params|
+ storage_path = params['path']
+ if repo_path.start_with?(storage_path)
+ result = repo_path.sub(storage_path, '')
+ break
+ end
+ end
+
+ if result.nil?
+ raise NotFoundError.new("No known storage path matches #{repo_path.inspect}")
+ end
+
+ result.sub(/\A\/*/, '')
+ end
+ end
+end
diff --git a/lib/gitlab/request_context.rb b/lib/gitlab/request_context.rb
new file mode 100644
index 00000000000..fef536ecb0b
--- /dev/null
+++ b/lib/gitlab/request_context.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ class RequestContext
+ class << self
+ def client_ip
+ RequestStore[:client_ip]
+ end
+ end
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ req = Rack::Request.new(env)
+
+ RequestStore[:client_ip] = req.ip
+
+ @app.call(env)
+ end
+ end
+end
diff --git a/lib/gitlab/request_profiler.rb b/lib/gitlab/request_profiler.rb
index 8130e55351e..0c9ab759e81 100644
--- a/lib/gitlab/request_profiler.rb
+++ b/lib/gitlab/request_profiler.rb
@@ -2,7 +2,7 @@ require 'fileutils'
module Gitlab
module RequestProfiler
- PROFILES_DIR = "#{Gitlab.config.shared.path}/tmp/requests_profiles"
+ PROFILES_DIR = "#{Gitlab.config.shared.path}/tmp/requests_profiles".freeze
def profile_token
Rails.cache.fetch('profile-token') do
diff --git a/lib/gitlab/request_profiler/middleware.rb b/lib/gitlab/request_profiler/middleware.rb
index 786e1d49f5e..ef42b0557e0 100644
--- a/lib/gitlab/request_profiler/middleware.rb
+++ b/lib/gitlab/request_profiler/middleware.rb
@@ -1,5 +1,4 @@
require 'ruby-prof'
-require_dependency 'gitlab/request_profiler'
module Gitlab
module RequestProfiler
@@ -20,7 +19,7 @@ module Gitlab
header_token = env['HTTP_X_PROFILE_TOKEN']
return unless header_token.present?
- profile_token = RequestProfiler.profile_token
+ profile_token = Gitlab::RequestProfiler.profile_token
return unless profile_token.present?
header_token == profile_token
diff --git a/lib/gitlab/route_map.rb b/lib/gitlab/route_map.rb
new file mode 100644
index 00000000000..36791fae60f
--- /dev/null
+++ b/lib/gitlab/route_map.rb
@@ -0,0 +1,50 @@
+module Gitlab
+ class RouteMap
+ FormatError = Class.new(StandardError)
+
+ def initialize(data)
+ begin
+ entries = YAML.safe_load(data)
+ rescue
+ raise FormatError, 'Route map is not valid YAML'
+ end
+
+ raise FormatError, 'Route map is not an array' unless entries.is_a?(Array)
+
+ @map = entries.map { |entry| parse_entry(entry) }
+ end
+
+ def public_path_for_source_path(path)
+ mapping = @map.find { |mapping| mapping[:source] === path }
+ return unless mapping
+
+ path.sub(mapping[:source], mapping[:public])
+ end
+
+ private
+
+ def parse_entry(entry)
+ raise FormatError, 'Route map entry is not a hash' unless entry.is_a?(Hash)
+ raise FormatError, 'Route map entry does not have a source key' unless entry.has_key?('source')
+ raise FormatError, 'Route map entry does not have a public key' unless entry.has_key?('public')
+
+ source_pattern = entry['source']
+ public_path = entry['public']
+
+ if source_pattern.start_with?('/') && source_pattern.end_with?('/')
+ source_pattern = source_pattern[1...-1].gsub('\/', '/')
+
+ begin
+ source_pattern = /\A#{source_pattern}\z/
+ rescue RegexpError => e
+ raise FormatError, "Route map entry source is not a valid regular expression: #{e}"
+ end
+ end
+
+ {
+ source: source_pattern,
+ public: public_path
+ }
+ end
+ end
+end
diff --git a/lib/gitlab/saml/user.rb b/lib/gitlab/saml/user.rb
index f253dc7477e..8a7cc690046 100644
--- a/lib/gitlab/saml/user.rb
+++ b/lib/gitlab/saml/user.rb
@@ -28,11 +28,12 @@ module Gitlab
if external_users_enabled? && @user
# Check if there is overlap between the user's groups and the external groups
# setting then set user as external or internal.
- if (auth_hash.groups & Gitlab::Saml::Config.external_groups).empty?
- @user.external = false
- else
- @user.external = true
- end
+ @user.external =
+ if (auth_hash.groups & Gitlab::Saml::Config.external_groups).empty?
+ false
+ else
+ true
+ end
end
@user
diff --git a/lib/gitlab/sanitizers/svg/whitelist.rb b/lib/gitlab/sanitizers/svg/whitelist.rb
index 7b6b70d8dbc..d50f826f924 100644
--- a/lib/gitlab/sanitizers/svg/whitelist.rb
+++ b/lib/gitlab/sanitizers/svg/whitelist.rb
@@ -6,18 +6,19 @@ module Gitlab
module SVG
class Whitelist
ALLOWED_ELEMENTS = %w[
- a altGlyph altGlyphDef altGlyphItem animate
- animateColor animateMotion animateTransform circle clipPath color-profile
- cursor defs desc ellipse feBlend feColorMatrix feComponentTransfer
- feComposite feConvolveMatrix feDiffuseLighting feDisplacementMap
- feDistantLight feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur
- feImage feMerge feMergeNode feMorphology feOffset fePointLight
- feSpecularLighting feSpotLight feTile feTurbulence filter font font-face
- font-face-format font-face-name font-face-src font-face-uri foreignObject
- g glyph glyphRef hkern image line linearGradient marker mask metadata
- missing-glyph mpath path pattern polygon polyline radialGradient rect
- script set stop style svg switch symbol text textPath title tref tspan use
- view vkern].freeze
+ a altGlyph altGlyphDef altGlyphItem animate
+ animateColor animateMotion animateTransform circle clipPath color-profile
+ cursor defs desc ellipse feBlend feColorMatrix feComponentTransfer
+ feComposite feConvolveMatrix feDiffuseLighting feDisplacementMap
+ feDistantLight feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur
+ feImage feMerge feMergeNode feMorphology feOffset fePointLight
+ feSpecularLighting feSpotLight feTile feTurbulence filter font font-face
+ font-face-format font-face-name font-face-src font-face-uri foreignObject
+ g glyph glyphRef hkern image line linearGradient marker mask metadata
+ missing-glyph mpath path pattern polygon polyline radialGradient rect
+ script set stop style svg switch symbol text textPath title tref tspan use
+ view vkern
+ ].freeze
ALLOWED_DATA_ATTRIBUTES_IN_ELEMENTS = %w[svg].freeze
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 35212992698..ccfa517e04b 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -43,6 +43,10 @@ module Gitlab
@milestones_count ||= milestones.count
end
+ def single_commit_result?
+ false
+ end
+
private
def projects
@@ -52,11 +56,12 @@ module Gitlab
def issues
issues = IssuesFinder.new(current_user).execute.where(project_id: project_ids_relation)
- if query =~ /#(\d+)\z/
- issues = issues.where(iid: $1)
- else
- issues = issues.full_search(query)
- end
+ issues =
+ if query =~ /#(\d+)\z/
+ issues.where(iid: $1)
+ else
+ issues.full_search(query)
+ end
issues.order('updated_at DESC')
end
@@ -69,11 +74,12 @@ module Gitlab
def merge_requests
merge_requests = MergeRequestsFinder.new(current_user).execute.in_projects(project_ids_relation)
- if query =~ /[#!](\d+)\z/
- merge_requests = merge_requests.where(iid: $1)
- else
- merge_requests = merge_requests.full_search(query)
- end
+ merge_requests =
+ if query =~ /[#!](\d+)\z/
+ merge_requests.where(iid: $1)
+ else
+ merge_requests.full_search(query)
+ end
merge_requests.order('updated_at DESC')
end
diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb
index 7cf506ebe64..823f697f51c 100644
--- a/lib/gitlab/seeder.rb
+++ b/lib/gitlab/seeder.rb
@@ -1,24 +1,23 @@
+module DeliverNever
+ def deliver_later
+ self
+ end
+end
+
module Gitlab
class Seeder
def self.quiet
mute_mailer
SeedFu.quiet = true
+
yield
+
SeedFu.quiet = false
puts "\nOK".color(:green)
end
- def self.by_user(user)
- yield
- end
-
def self.mute_mailer
- code = <<-eos
-def Notify.deliver_later
- self
-end
- eos
- eval(code)
+ ActionMailer::MessageDelivery.prepend(DeliverNever)
end
end
end
diff --git a/lib/gitlab/serialize/ci/variables.rb b/lib/gitlab/serializer/ci/variables.rb
index 3a9443bfcd9..c059c454eac 100644
--- a/lib/gitlab/serialize/ci/variables.rb
+++ b/lib/gitlab/serializer/ci/variables.rb
@@ -1,5 +1,5 @@
module Gitlab
- module Serialize
+ module Serializer
module Ci
# This serializer could make sure our YAML variables' keys and values
# are always strings. This is more for legacy build data because
diff --git a/lib/gitlab/serializer/pagination.rb b/lib/gitlab/serializer/pagination.rb
new file mode 100644
index 00000000000..9c92b83dddc
--- /dev/null
+++ b/lib/gitlab/serializer/pagination.rb
@@ -0,0 +1,36 @@
+module Gitlab
+ module Serializer
+ class Pagination
+ InvalidResourceError = Class.new(StandardError)
+ include ::API::Helpers::Pagination
+
+ def initialize(request, response)
+ @request = request
+ @response = response
+ end
+
+ def paginate(resource)
+ if resource.respond_to?(:page)
+ super(resource)
+ else
+ raise InvalidResourceError
+ end
+ end
+
+ private
+
+ # Methods needed by `API::Helpers::Pagination`
+ #
+
+ attr_reader :request
+
+ def params
+ @request.query_parameters
+ end
+
+ def header(header, value)
+ @response.headers[header] = value
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/shell.rb
index 82e194c1af1..b631ef11ce7 100644
--- a/lib/gitlab/backend/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -2,7 +2,7 @@ require 'securerandom'
module Gitlab
class Shell
- class Error < StandardError; end
+ Error = Class.new(StandardError)
KeyAdder = Struct.new(:io) do
def add_key(id, key)
@@ -80,8 +80,30 @@ module Gitlab
# import_repository("/path/to/storage", "gitlab/gitlab-ci", "https://github.com/randx/six.git")
#
def import_repository(storage, name, url)
- output, status = Popen::popen([gitlab_shell_projects_path, 'import-project',
- storage, "#{name}.git", url, '900'])
+ # Timeout should be less than 900 ideally, to prevent the memory killer
+ # to silently kill the process without knowing we are timing out here.
+ output, status = Popen.popen([gitlab_shell_projects_path, 'import-project',
+ storage, "#{name}.git", url, '800'])
+ raise Error, output unless status.zero?
+ true
+ end
+
+ # Fetch remote for repository
+ #
+ # name - project path with namespace
+ # remote - remote name
+ # forced - should we use --force flag?
+ # no_tags - should we use --no-tags flag?
+ #
+ # Ex.
+ # fetch_remote("gitlab/gitlab-ci", "upstream")
+ #
+ def fetch_remote(storage, name, remote, forced: false, no_tags: false)
+ args = [gitlab_shell_projects_path, 'fetch-remote', storage, "#{name}.git", remote, '800']
+ args << '--force' if forced
+ args << '--no-tags' if no_tags
+
+ output, status = Popen.popen(args)
raise Error, output unless status.zero?
true
end
@@ -143,7 +165,7 @@ module Gitlab
# batch_add_keys { |adder| adder.add_key("key-42", "sha-rsa ...") }
def batch_add_keys(&block)
IO.popen(%W(#{gitlab_shell_path}/bin/gitlab-keys batch-add-keys), 'w') do |io|
- block.call(KeyAdder.new(io))
+ yield(KeyAdder.new(io))
end
end
@@ -172,7 +194,10 @@ module Gitlab
# add_namespace("/path/to/storage", "gitlab")
#
def add_namespace(storage, name)
- FileUtils.mkdir(full_path(storage, name), mode: 0770) unless exists?(storage, name)
+ path = full_path(storage, name)
+ FileUtils.mkdir_p(path, mode: 0770) unless exists?(storage, name)
+ rescue Errno::EEXIST => e
+ Rails.logger.warn("Directory exists as a file: #{e} at: #{path}")
end
# Remove directory from repositories storage
diff --git a/lib/gitlab/backend/shell_adapter.rb b/lib/gitlab/shell_adapter.rb
index fbe2a7a0d72..fbe2a7a0d72 100644
--- a/lib/gitlab/backend/shell_adapter.rb
+++ b/lib/gitlab/shell_adapter.rb
diff --git a/lib/gitlab/sherlock/query.rb b/lib/gitlab/sherlock/query.rb
index 4917c4ae2ac..99e56e923eb 100644
--- a/lib/gitlab/sherlock/query.rb
+++ b/lib/gitlab/sherlock/query.rb
@@ -94,11 +94,12 @@ module Gitlab
private
def raw_explain(query)
- if Gitlab::Database.postgresql?
- explain = "EXPLAIN ANALYZE #{query};"
- else
- explain = "EXPLAIN #{query};"
- end
+ explain =
+ if Gitlab::Database.postgresql?
+ "EXPLAIN ANALYZE #{query};"
+ else
+ "EXPLAIN #{query};"
+ end
ActiveRecord::Base.connection.execute(explain)
end
diff --git a/lib/gitlab/sidekiq_status.rb b/lib/gitlab/sidekiq_status.rb
new file mode 100644
index 00000000000..11e5f1b645c
--- /dev/null
+++ b/lib/gitlab/sidekiq_status.rb
@@ -0,0 +1,89 @@
+module Gitlab
+ # The SidekiqStatus module and its child classes can be used for checking if a
+ # Sidekiq job has been processed or not.
+ #
+ # To check if a job has been completed, simply pass the job ID to the
+ # `completed?` method:
+ #
+ # job_id = SomeWorker.perform_async(...)
+ #
+ # if Gitlab::SidekiqStatus.completed?(job_id)
+ # ...
+ # end
+ #
+ # For each job ID registered a separate key is stored in Redis, making lookups
+ # much faster than using Sidekiq's built-in job finding/status API. These keys
+ # expire after a certain period of time to prevent storing too many keys in
+ # Redis.
+ module SidekiqStatus
+ STATUS_KEY = 'gitlab-sidekiq-status:%s'.freeze
+
+ # The default time (in seconds) after which a status key is expired
+ # automatically. The default of 30 minutes should be more than sufficient
+ # for most jobs.
+ DEFAULT_EXPIRATION = 30.minutes.to_i
+
+ # Starts tracking of the given job.
+ #
+ # jid - The Sidekiq job ID
+ # expire - The expiration time of the Redis key.
+ def self.set(jid, expire = DEFAULT_EXPIRATION)
+ Sidekiq.redis do |redis|
+ redis.set(key_for(jid), 1, ex: expire)
+ end
+ end
+
+ # Stops the tracking of the given job.
+ #
+ # jid - The Sidekiq job ID to remove.
+ def self.unset(jid)
+ Sidekiq.redis do |redis|
+ redis.del(key_for(jid))
+ end
+ end
+
+ # Returns true if all the given job have been completed.
+ #
+ # job_ids - The Sidekiq job IDs to check.
+ #
+ # Returns true or false.
+ def self.all_completed?(job_ids)
+ self.num_running(job_ids).zero?
+ end
+
+ # Returns the number of jobs that are running.
+ #
+ # job_ids - The Sidekiq job IDs to check.
+ def self.num_running(job_ids)
+ responses = self.job_status(job_ids)
+
+ responses.select(&:present?).count
+ end
+
+ # Returns the number of jobs that have completed.
+ #
+ # job_ids - The Sidekiq job IDs to check.
+ def self.num_completed(job_ids)
+ job_ids.size - self.num_running(job_ids)
+ end
+
+ # Returns the job status for each of the given job IDs.
+ #
+ # job_ids - The Sidekiq job IDs to check.
+ #
+ # Returns an array of true or false indicating job completion.
+ def self.job_status(job_ids)
+ keys = job_ids.map { |jid| key_for(jid) }
+
+ Sidekiq.redis do |redis|
+ redis.pipelined do
+ keys.each { |key| redis.exists(key) }
+ end
+ end
+ end
+
+ def self.key_for(jid)
+ STATUS_KEY % jid
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_status/client_middleware.rb b/lib/gitlab/sidekiq_status/client_middleware.rb
new file mode 100644
index 00000000000..d47609f490d
--- /dev/null
+++ b/lib/gitlab/sidekiq_status/client_middleware.rb
@@ -0,0 +1,10 @@
+module Gitlab
+ module SidekiqStatus
+ class ClientMiddleware
+ def call(_, job, _, _)
+ Gitlab::SidekiqStatus.set(job['jid'])
+ yield
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_status/server_middleware.rb b/lib/gitlab/sidekiq_status/server_middleware.rb
new file mode 100644
index 00000000000..ceab10b8301
--- /dev/null
+++ b/lib/gitlab/sidekiq_status/server_middleware.rb
@@ -0,0 +1,13 @@
+module Gitlab
+ module SidekiqStatus
+ class ServerMiddleware
+ def call(worker, job, queue)
+ ret = yield
+
+ Gitlab::SidekiqStatus.unset(job['jid'])
+
+ ret
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/extractor.rb b/lib/gitlab/slash_commands/extractor.rb
index a672e5e4855..6dbb467d70d 100644
--- a/lib/gitlab/slash_commands/extractor.rb
+++ b/lib/gitlab/slash_commands/extractor.rb
@@ -103,7 +103,7 @@ module Gitlab
(?<cmd>#{Regexp.union(names)})
(?:
[ ]
- (?<arg>[^\/\n]*)
+ (?<arg>[^\n]*)
)?
(?:\n|$)
)
diff --git a/lib/gitlab/snippet_search_results.rb b/lib/gitlab/snippet_search_results.rb
index 9e01f02029c..b85f70e450e 100644
--- a/lib/gitlab/snippet_search_results.rb
+++ b/lib/gitlab/snippet_search_results.rb
@@ -31,11 +31,11 @@ module Gitlab
private
def snippet_titles
- limit_snippets.search(query).order('updated_at DESC')
+ limit_snippets.search(query).order('updated_at DESC').includes(:author)
end
def snippet_blobs
- limit_snippets.search_code(query).order('updated_at DESC')
+ limit_snippets.search_code(query).order('updated_at DESC').includes(:author)
end
def default_scope
diff --git a/lib/gitlab/template/finders/repo_template_finder.rb b/lib/gitlab/template/finders/repo_template_finder.rb
index 22c39436cb2..cb7957e2af9 100644
--- a/lib/gitlab/template/finders/repo_template_finder.rb
+++ b/lib/gitlab/template/finders/repo_template_finder.rb
@@ -4,7 +4,7 @@ module Gitlab
module Finders
class RepoTemplateFinder < BaseTemplateFinder
# Raised when file is not found
- class FileNotFoundError < StandardError; end
+ FileNotFoundError = Class.new(StandardError)
def initialize(project, base_dir, extension, categories = {})
@categories = categories
diff --git a/lib/gitlab/template/gitlab_ci_yml_template.rb b/lib/gitlab/template/gitlab_ci_yml_template.rb
index 9d2ecee9756..fd040148a1e 100644
--- a/lib/gitlab/template/gitlab_ci_yml_template.rb
+++ b/lib/gitlab/template/gitlab_ci_yml_template.rb
@@ -28,7 +28,7 @@ module Gitlab
end
def dropdown_names(context)
- categories = context == 'autodeploy' ? ['Auto deploy'] : ['General', 'Pages']
+ categories = context == 'autodeploy' ? ['Auto deploy'] : %w(General Pages)
super().slice(*categories)
end
end
diff --git a/lib/gitlab/testing/request_blocker_middleware.rb b/lib/gitlab/testing/request_blocker_middleware.rb
new file mode 100644
index 00000000000..aa67fa08577
--- /dev/null
+++ b/lib/gitlab/testing/request_blocker_middleware.rb
@@ -0,0 +1,61 @@
+# rubocop:disable Style/ClassVars
+
+# This is inspired by http://www.salsify.com/blog/engineering/tearing-capybara-ajax-tests
+# Rack middleware that keeps track of the number of active requests and can block new requests.
+module Gitlab
+ module Testing
+ class RequestBlockerMiddleware
+ @@num_active_requests = Concurrent::AtomicFixnum.new(0)
+ @@block_requests = Concurrent::AtomicBoolean.new(false)
+
+ # Returns the number of requests the server is currently processing.
+ def self.num_active_requests
+ @@num_active_requests.value
+ end
+
+ # Prevents the server from accepting new requests. Any new requests will return an HTTP
+ # 503 status.
+ def self.block_requests!
+ @@block_requests.value = true
+ end
+
+ # Allows the server to accept requests again.
+ def self.allow_requests!
+ @@block_requests.value = false
+ end
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ increment_active_requests
+ if block_requests?
+ block_request(env)
+ else
+ @app.call(env)
+ end
+ ensure
+ decrement_active_requests
+ end
+
+ private
+
+ def block_requests?
+ @@block_requests.true?
+ end
+
+ def block_request(env)
+ [503, {}, []]
+ end
+
+ def increment_active_requests
+ @@num_active_requests.increment
+ end
+
+ def decrement_active_requests
+ @@num_active_requests.decrement
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb
deleted file mode 100644
index 19ab76ae80f..00000000000
--- a/lib/gitlab/themes.rb
+++ /dev/null
@@ -1,87 +0,0 @@
-module Gitlab
- # Module containing GitLab's application theme definitions and helper methods
- # for accessing them.
- module Themes
- extend self
-
- # Theme ID used when no `default_theme` configuration setting is provided.
- APPLICATION_DEFAULT = 2
-
- # Struct class representing a single Theme
- Theme = Struct.new(:id, :name, :css_class)
-
- # All available Themes
- THEMES = [
- Theme.new(1, 'Graphite', 'ui_graphite'),
- Theme.new(2, 'Charcoal', 'ui_charcoal'),
- Theme.new(3, 'Green', 'ui_green'),
- Theme.new(4, 'Black', 'ui_black'),
- Theme.new(5, 'Violet', 'ui_violet'),
- Theme.new(6, 'Blue', 'ui_blue')
- ].freeze
-
- # Convenience method to get a space-separated String of all the theme
- # classes that might be applied to the `body` element
- #
- # Returns a String
- def body_classes
- THEMES.collect(&:css_class).uniq.join(' ')
- end
-
- # Get a Theme by its ID
- #
- # If the ID is invalid, returns the default Theme.
- #
- # id - Integer ID
- #
- # Returns a Theme
- def by_id(id)
- THEMES.detect { |t| t.id == id } || default
- end
-
- # Returns the number of defined Themes
- def count
- THEMES.size
- end
-
- # Get the default Theme
- #
- # Returns a Theme
- def default
- by_id(default_id)
- end
-
- # Iterate through each Theme
- #
- # Yields the Theme object
- def each(&block)
- THEMES.each(&block)
- end
-
- # Get the Theme for the specified user, or the default
- #
- # user - User record
- #
- # Returns a Theme
- def for_user(user)
- if user
- by_id(user.theme_id)
- else
- default
- end
- end
-
- private
-
- def default_id
- id = Gitlab.config.gitlab.default_theme.to_i
-
- # Prevent an invalid configuration setting from causing an infinite loop
- if id < THEMES.first.id || id > THEMES.last.id
- APPLICATION_DEFAULT
- else
- id
- end
- end
- end
-end
diff --git a/lib/gitlab/update_path_error.rb b/lib/gitlab/update_path_error.rb
index ce14cc887d0..8947ecfb92e 100644
--- a/lib/gitlab/update_path_error.rb
+++ b/lib/gitlab/update_path_error.rb
@@ -1,3 +1,3 @@
module Gitlab
- class UpdatePathError < StandardError; end
+ UpdatePathError = Class.new(StandardError)
end
diff --git a/lib/gitlab/upgrader.rb b/lib/gitlab/upgrader.rb
index f3567f3ef85..961df0468a4 100644
--- a/lib/gitlab/upgrader.rb
+++ b/lib/gitlab/upgrader.rb
@@ -46,7 +46,7 @@ module Gitlab
git_tags = fetch_git_tags
git_tags = git_tags.select { |version| version =~ /v\d+\.\d+\.\d+\Z/ }
git_versions = git_tags.map { |tag| Gitlab::VersionInfo.parse(tag.match(/v\d+\.\d+\.\d+/).to_s) }
- "v#{git_versions.sort.last.to_s}"
+ "v#{git_versions.sort.last}"
end
def fetch_git_tags
@@ -59,15 +59,18 @@ module Gitlab
"Stash changed files" => %W(#{Gitlab.config.git.bin_path} stash),
"Get latest code" => %W(#{Gitlab.config.git.bin_path} fetch),
"Switch to new version" => %W(#{Gitlab.config.git.bin_path} checkout v#{latest_version}),
- "Install gems" => %W(bundle),
- "Migrate DB" => %W(bundle exec rake db:migrate),
- "Recompile assets" => %W(bundle exec rake assets:clean assets:precompile),
- "Clear cache" => %W(bundle exec rake cache:clear)
+ "Install gems" => %w(bundle),
+ "Migrate DB" => %w(bundle exec rake db:migrate),
+ "Recompile assets" => %w(bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile),
+ "Clear cache" => %w(bundle exec rake cache:clear)
}
end
def env
- { 'RAILS_ENV' => 'production' }
+ {
+ 'RAILS_ENV' => 'production',
+ 'NODE_ENV' => 'production'
+ }
end
def upgrade
diff --git a/lib/gitlab/uploads_transfer.rb b/lib/gitlab/uploads_transfer.rb
index be8fcc7b2d2..7d0c47c5361 100644
--- a/lib/gitlab/uploads_transfer.rb
+++ b/lib/gitlab/uploads_transfer.rb
@@ -1,35 +1,7 @@
module Gitlab
- class UploadsTransfer
- def move_project(project_path, namespace_path_was, namespace_path)
- new_namespace_folder = File.join(root_dir, namespace_path)
- FileUtils.mkdir_p(new_namespace_folder) unless Dir.exist?(new_namespace_folder)
- from = File.join(root_dir, namespace_path_was, project_path)
- to = File.join(root_dir, namespace_path, project_path)
- move(from, to, "")
- end
-
- def rename_project(path_was, path, namespace_path)
- base_dir = File.join(root_dir, namespace_path)
- move(path_was, path, base_dir)
- end
-
- def rename_namespace(path_was, path)
- move(path_was, path)
- end
-
- private
-
- def move(path_was, path, base_dir = nil)
- base_dir = root_dir unless base_dir
- from = File.join(base_dir, path_was)
- to = File.join(base_dir, path)
- FileUtils.mv(from, to)
- rescue Errno::ENOENT
- false
- end
-
+ class UploadsTransfer < ProjectTransfer
def root_dir
- File.join(Rails.root, "public", "uploads")
+ File.join(CarrierWave.root, GitlabUploader.base_dir)
end
end
end
diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb
new file mode 100644
index 00000000000..7e14a566696
--- /dev/null
+++ b/lib/gitlab/url_blocker.rb
@@ -0,0 +1,59 @@
+require 'resolv'
+
+module Gitlab
+ class UrlBlocker
+ class << self
+ # Used to specify what hosts and port numbers should be prohibited for project
+ # imports.
+ VALID_PORTS = [22, 80, 443].freeze
+
+ def blocked_url?(url)
+ return false if url.nil?
+
+ blocked_ips = ["127.0.0.1", "::1", "0.0.0.0"]
+ blocked_ips.concat(Socket.ip_address_list.map(&:ip_address))
+
+ begin
+ uri = Addressable::URI.parse(url)
+ # Allow imports from the GitLab instance itself but only from the configured ports
+ return false if internal?(uri)
+
+ return true if blocked_port?(uri.port)
+
+ server_ips = Resolv.getaddresses(uri.hostname)
+ return true if (blocked_ips & server_ips).any?
+ rescue Addressable::URI::InvalidURIError
+ return true
+ end
+
+ false
+ end
+
+ private
+
+ def blocked_port?(port)
+ return false if port.blank?
+
+ port < 1024 && !VALID_PORTS.include?(port)
+ end
+
+ def internal?(uri)
+ internal_web?(uri) || internal_shell?(uri)
+ end
+
+ def internal_web?(uri)
+ uri.hostname == config.gitlab.host &&
+ (uri.port.blank? || uri.port == config.gitlab.port)
+ end
+
+ def internal_shell?(uri)
+ uri.hostname == config.gitlab_shell.ssh_host &&
+ (uri.port.blank? || uri.port == config.gitlab_shell.ssh_port)
+ end
+
+ def config
+ Gitlab.config
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb
index 19dad699edf..9ce13feb79a 100644
--- a/lib/gitlab/url_sanitizer.rb
+++ b/lib/gitlab/url_sanitizer.rb
@@ -1,7 +1,7 @@
module Gitlab
class UrlSanitizer
def self.sanitize(content)
- regexp = URI::Parser.new.make_regexp(['http', 'https', 'ssh', 'git'])
+ regexp = URI::Parser.new.make_regexp(%w(http https ssh git))
content.gsub(regexp) { |url| new(url).masked_url }
rescue Addressable::URI::InvalidURIError
@@ -9,6 +9,8 @@ module Gitlab
end
def self.valid?(url)
+ return false unless url
+
Addressable::URI.parse(url.strip)
true
@@ -16,6 +18,12 @@ module Gitlab
false
end
+ def self.http_credentials_for_user(user)
+ return {} unless user.respond_to?(:username)
+
+ { user: user.username }
+ end
+
def initialize(url, credentials: nil)
@url = Addressable::URI.parse(url.strip)
@credentials = credentials
diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb
index 6c7e673fb9f..f260c0c535f 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -8,7 +8,7 @@ module Gitlab
end
def can_do_action?(action)
- return false if no_user_or_blocked?
+ return false unless can_access_git?
@permission_cache ||= {}
@permission_cache[action] ||= user.can?(action, project)
@@ -19,7 +19,7 @@ module Gitlab
end
def allowed?
- return false if no_user_or_blocked?
+ return false unless can_access_git?
if user.requires_ldap_check? && user.try_obtain_ldap_lease
return false unless Gitlab::LDAP::Access.allowed?(user)
@@ -29,20 +29,22 @@ module Gitlab
end
def can_push_to_branch?(ref)
- return false if no_user_or_blocked?
+ return false unless can_access_git?
if project.protected_branch?(ref)
return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user)
access_levels = project.protected_branches.matching(ref).map(&:push_access_levels).flatten
- access_levels.any? { |access_level| access_level.check_access(user) }
+ has_access = access_levels.any? { |access_level| access_level.check_access(user) }
+
+ has_access || !project.repository.branch_exists?(ref) && can_merge_to_branch?(ref)
else
user.can?(:push_code, project)
end
end
def can_merge_to_branch?(ref)
- return false if no_user_or_blocked?
+ return false unless can_access_git?
if project.protected_branch?(ref)
access_levels = project.protected_branches.matching(ref).map(&:merge_access_levels).flatten
@@ -53,15 +55,15 @@ module Gitlab
end
def can_read_project?
- return false if no_user_or_blocked?
+ return false unless can_access_git?
user.can?(:read_project, project)
end
private
- def no_user_or_blocked?
- user.nil? || user.blocked?
+ def can_access_git?
+ user && user.can?(:access_git)
end
end
end
diff --git a/lib/gitlab/view/presenter/base.rb b/lib/gitlab/view/presenter/base.rb
index 83c8ba5c1cf..dbfe0941e4d 100644
--- a/lib/gitlab/view/presenter/base.rb
+++ b/lib/gitlab/view/presenter/base.rb
@@ -1,6 +1,8 @@
module Gitlab
module View
module Presenter
+ CannotOverrideMethodError = Class.new(StandardError)
+
module Base
extend ActiveSupport::Concern
diff --git a/lib/gitlab/view/presenter/delegated.rb b/lib/gitlab/view/presenter/delegated.rb
index f4d330c590e..387ff0f5d43 100644
--- a/lib/gitlab/view/presenter/delegated.rb
+++ b/lib/gitlab/view/presenter/delegated.rb
@@ -8,6 +8,10 @@ module Gitlab
@subject = subject
attributes.each do |key, value|
+ if subject.respond_to?(key)
+ raise CannotOverrideMethodError.new("#{subject} already respond to #{key}!")
+ end
+
define_singleton_method(key) { value }
end
diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb
index 9462f3368e6..8f1d1fdc02e 100644
--- a/lib/gitlab/visibility_level.rb
+++ b/lib/gitlab/visibility_level.rb
@@ -11,8 +11,21 @@ module Gitlab
included do
scope :public_only, -> { where(visibility_level: PUBLIC) }
scope :public_and_internal_only, -> { where(visibility_level: [PUBLIC, INTERNAL] ) }
-
- scope :public_to_user, -> (user) { user && !user.external ? public_and_internal_only : public_only }
+ scope :non_public_only, -> { where.not(visibility_level: PUBLIC) }
+
+ scope :public_to_user, -> (user) do
+ if user
+ if user.admin?
+ all
+ elsif !user.external?
+ public_and_internal_only
+ else
+ public_only
+ end
+ else
+ public_only
+ end
+ end
end
PRIVATE = 0 unless const_defined?(:PRIVATE)
@@ -20,8 +33,10 @@ module Gitlab
PUBLIC = 20 unless const_defined?(:PUBLIC)
class << self
- def values
- options.values
+ delegate :values, to: :options
+
+ def string_values
+ string_options.keys
end
def options
@@ -32,6 +47,14 @@ module Gitlab
}
end
+ def string_options
+ {
+ 'private' => PRIVATE,
+ 'internal' => INTERNAL,
+ 'public' => PUBLIC
+ }
+ end
+
def highest_allowed_level
restricted_levels = current_application_settings.restricted_visibility_levels
@@ -71,18 +94,39 @@ module Gitlab
level_name
end
+
+ def level_value(level)
+ return level.to_i if level.to_i.to_s == level.to_s && string_options.key(level.to_i)
+ string_options[level] || PRIVATE
+ end
+
+ def string_level(level)
+ string_options.key(level)
+ end
end
def private?
- visibility_level_field == PRIVATE
+ visibility_level_value == PRIVATE
end
def internal?
- visibility_level_field == INTERNAL
+ visibility_level_value == INTERNAL
end
def public?
- visibility_level_field == PUBLIC
+ visibility_level_value == PUBLIC
+ end
+
+ def visibility_level_value
+ self[visibility_level_field]
+ end
+
+ def visibility
+ Gitlab::VisibilityLevel.string_level(visibility_level_value)
+ end
+
+ def visibility=(level)
+ self[visibility_level_field] = Gitlab::VisibilityLevel.level_value(level)
end
end
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index a3b502ffd6a..d0637f8b394 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -1,29 +1,51 @@
require 'base64'
require 'json'
require 'securerandom'
+require 'uri'
module Gitlab
class Workhorse
- SEND_DATA_HEADER = 'Gitlab-Workhorse-Send-Data'
- VERSION_FILE = 'GITLAB_WORKHORSE_VERSION'
- INTERNAL_API_CONTENT_TYPE = 'application/vnd.gitlab-workhorse+json'
- INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request'
+ SEND_DATA_HEADER = 'Gitlab-Workhorse-Send-Data'.freeze
+ VERSION_FILE = 'GITLAB_WORKHORSE_VERSION'.freeze
+ INTERNAL_API_CONTENT_TYPE = 'application/vnd.gitlab-workhorse+json'.freeze
+ INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request'.freeze
+ NOTIFICATION_CHANNEL = 'workhorse:notifications'.freeze
# Supposedly the effective key size for HMAC-SHA256 is 256 bits, i.e. 32
# bytes https://tools.ietf.org/html/rfc4868#section-2.6
SECRET_LENGTH = 32
class << self
- def git_http_ok(repository, user)
+ def git_http_ok(repository, user, action)
+ repo_path = repository.path_to_repo
params = {
GL_ID: Gitlab::GlId.gl_id(user),
- RepoPath: repository.path_to_repo,
+ RepoPath: repo_path,
}
- params.merge!(
- GitalySocketPath: Gitlab.config.gitaly.socket_path,
- GitalyResourcePath: "/projects/#{repository.project.id}/git-http/info-refs",
- ) if Gitlab.config.gitaly.socket_path.present?
+ if Gitlab.config.gitaly.enabled
+ storage = repository.project.repository_storage
+ address = Gitlab::GitalyClient.get_address(storage)
+ # TODO: use GitalyClient code to assemble the Repository message
+ params[:Repository] = Gitaly::Repository.new(
+ path: repo_path,
+ storage_name: storage,
+ relative_path: Gitlab::RepoPath.strip_storage_path(repo_path),
+ ).to_h
+
+ feature_enabled = case action.to_s
+ when 'git_receive_pack'
+ Gitlab::GitalyClient.feature_enabled?(:post_receive_pack)
+ when 'git_upload_pack'
+ Gitlab::GitalyClient.feature_enabled?(:post_upload_pack)
+ when 'info_refs'
+ true
+ else
+ raise "Unsupported action: #{action}"
+ end
+
+ params[:GitalySocketPath] = URI(address).path if feature_enabled
+ end
params
end
@@ -107,7 +129,8 @@ module Gitlab
'Terminal' => {
'Subprotocols' => terminal[:subprotocols],
'Url' => terminal[:url],
- 'Header' => terminal[:headers]
+ 'Header' => terminal[:headers],
+ 'MaxSessionTime' => terminal[:max_session_time],
}
}
details['Terminal']['CAPem'] = terminal[:ca_pem] if terminal.has_key?(:ca_pem)
@@ -153,6 +176,18 @@ module Gitlab
Rails.root.join('.gitlab_workhorse_secret')
end
+ def set_key_and_notify(key, value, expire: nil, overwrite: true)
+ Gitlab::Redis.with do |redis|
+ result = redis.set(key, value, ex: expire, nx: !overwrite)
+ if result
+ redis.publish(NOTIFICATION_CHANNEL, "#{key}=#{value}")
+ value
+ else
+ redis.get(key)
+ end
+ end
+ end
+
protected
def encode(hash)
diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb
index ec2903b7ec6..3d60618006c 100644
--- a/lib/mattermost/client.rb
+++ b/lib/mattermost/client.rb
@@ -1,5 +1,5 @@
module Mattermost
- class ClientError < Mattermost::Error; end
+ ClientError = Class.new(Mattermost::Error)
class Client
attr_reader :user
@@ -8,21 +8,31 @@ module Mattermost
@user = user
end
- private
-
def with_session(&blk)
Mattermost::Session.new(user).with_session(&blk)
end
- def json_get(path, options = {})
+ private
+
+ # Should be used in a session manually
+ def get(session, path, options = {})
+ json_response session.get(path, options)
+ end
+
+ # Should be used in a session manually
+ def post(session, path, options = {})
+ json_response session.post(path, options)
+ end
+
+ def session_get(path, options = {})
with_session do |session|
- json_response session.get(path, options)
+ get(session, path, options)
end
end
- def json_post(path, options = {})
+ def session_post(path, options = {})
with_session do |session|
- json_response session.post(path, options)
+ post(session, path, options)
end
end
diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb
index d1e4bb0eccf..33e450d7f0a 100644
--- a/lib/mattermost/command.rb
+++ b/lib/mattermost/command.rb
@@ -1,7 +1,7 @@
module Mattermost
class Command < Client
def create(params)
- response = json_post("/api/v3/teams/#{params[:team_id]}/commands/create",
+ response = session_post("/api/v3/teams/#{params[:team_id]}/commands/create",
body: params.to_json)
response['token']
diff --git a/lib/mattermost/error.rb b/lib/mattermost/error.rb
index 014df175be0..dee6deb7974 100644
--- a/lib/mattermost/error.rb
+++ b/lib/mattermost/error.rb
@@ -1,3 +1,3 @@
module Mattermost
- class Error < StandardError; end
+ Error = Class.new(StandardError)
end
diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb
index 377cb7b1021..688a79c0441 100644
--- a/lib/mattermost/session.rb
+++ b/lib/mattermost/session.rb
@@ -5,7 +5,7 @@ module Mattermost
end
end
- class ConnectionError < Mattermost::Error; end
+ ConnectionError = Class.new(Mattermost::Error)
# This class' prime objective is to obtain a session token on a Mattermost
# instance with SSO configured where this GitLab instance is the provider.
@@ -153,7 +153,7 @@ module Mattermost
yield
rescue HTTParty::Error => e
raise Mattermost::ConnectionError.new(e.message)
- rescue Errno::ECONNREFUSED
+ rescue Errno::ECONNREFUSED => e
raise Mattermost::ConnectionError.new(e.message)
end
end
diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb
index 784eca6ab5a..2cdbbdece16 100644
--- a/lib/mattermost/team.rb
+++ b/lib/mattermost/team.rb
@@ -1,7 +1,18 @@
module Mattermost
class Team < Client
+ # Returns **all** teams for an admin
def all
- json_get('/api/v3/teams/all')
+ session_get('/api/v3/teams/all').values
+ end
+
+ # Creates a team on the linked Mattermost instance, the team admin will be the
+ # `current_user` passed to the Mattermost::Client instance
+ def create(name:, display_name:, type:)
+ session_post('/api/v3/teams/create', body: {
+ name: name,
+ display_name: display_name,
+ type: type
+ }.to_json)
end
end
end
diff --git a/lib/omniauth/strategies/bitbucket.rb b/lib/omni_auth/strategies/bitbucket.rb
index 5a7d67c2390..5a7d67c2390 100644
--- a/lib/omniauth/strategies/bitbucket.rb
+++ b/lib/omni_auth/strategies/bitbucket.rb
diff --git a/lib/rouge/formatters/html_gitlab.rb b/lib/rouge/formatters/html_gitlab.rb
index 4edfd015074..be0d97370d0 100644
--- a/lib/rouge/formatters/html_gitlab.rb
+++ b/lib/rouge/formatters/html_gitlab.rb
@@ -5,10 +5,10 @@ module Rouge
# Creates a new <tt>Rouge::Formatter::HTMLGitlab</tt> instance.
#
- # [+linenostart+] The line number for the first line (default: 1).
- def initialize(linenostart: 1)
- @linenostart = linenostart
- @line_number = linenostart
+ # [+tag+] The tag (language) of the lexer used to generate the formatted tokens
+ def initialize(tag: nil)
+ @line_number = 1
+ @tag = tag
end
def stream(tokens, &b)
@@ -17,7 +17,7 @@ module Rouge
yield "\n" unless is_first
is_first = false
- yield %(<span id="LC#{@line_number}" class="line">)
+ yield %(<span id="LC#{@line_number}" class="line" lang="#{@tag}">)
line.each { |token, value| yield span(token, value.chomp) }
yield %(</span>)
diff --git a/lib/rouge/lexers/plantuml.rb b/lib/rouge/lexers/plantuml.rb
new file mode 100644
index 00000000000..7d5700b7f6d
--- /dev/null
+++ b/lib/rouge/lexers/plantuml.rb
@@ -0,0 +1,21 @@
+module Rouge
+ module Lexers
+ class Plantuml < Lexer
+ title "A passthrough lexer used for PlantUML input"
+ desc "A boring lexer that doesn't highlight anything"
+
+ tag 'plantuml'
+ mimetypes 'text/plain'
+
+ default_options token: 'Text'
+
+ def token
+ @token ||= Token[option :token]
+ end
+
+ def stream_tokens(string, &b)
+ yield self.token, string
+ end
+ end
+ end
+end
diff --git a/lib/support/deploy/deploy.sh b/lib/support/deploy/deploy.sh
index adea4c7a747..ab46c47d8f5 100755
--- a/lib/support/deploy/deploy.sh
+++ b/lib/support/deploy/deploy.sh
@@ -31,8 +31,8 @@ echo 'Deploy: Bundle and migrate'
sudo -u git -H bundle --without aws development test mysql --deployment
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
-sudo -u git -H bundle exec rake assets:clean RAILS_ENV=production
-sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production
+sudo -u git -H bundle exec rake gitlab:assets:clean RAILS_ENV=production
+sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production
sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
# return stashed changes (if necessary)
diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab
index 31b00ff128a..09e121e5120 100755
--- a/lib/support/init.d/gitlab
+++ b/lib/support/init.d/gitlab
@@ -42,7 +42,16 @@ gitlab_workhorse_dir=$(cd $app_root/../gitlab-workhorse 2> /dev/null && pwd)
gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid"
gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080 -authSocket $rails_socket -documentRoot $app_root/public"
gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log"
+gitlab_pages_enabled=false
+gitlab_pages_dir=$(cd $app_root/../gitlab-pages 2> /dev/null && pwd)
+gitlab_pages_pid_path="$pid_path/gitlab-pages.pid"
+gitlab_pages_options="-pages-domain example.com -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090"
+gitlab_pages_log="$app_root/log/gitlab-pages.log"
shell_path="/bin/bash"
+gitaly_enabled=false
+gitaly_dir=$(cd $app_root/../gitaly 2> /dev/null && pwd)
+gitaly_pid_path="$pid_path/gitaly.pid"
+gitaly_log="$app_root/log/gitaly.log"
# Read configuration variable file if it is present
test -f /etc/default/gitlab && . /etc/default/gitlab
@@ -89,13 +98,27 @@ check_pids(){
mpid=0
fi
fi
+ if [ "$gitlab_pages_enabled" = true ]; then
+ if [ -f "$gitlab_pages_pid_path" ]; then
+ gppid=$(cat "$gitlab_pages_pid_path")
+ else
+ gppid=0
+ fi
+ fi
+ if [ "$gitaly_enabled" = true ]; then
+ if [ -f "$gitaly_pid_path" ]; then
+ gapid=$(cat "$gitaly_pid_path")
+ else
+ gapid=0
+ fi
+ fi
}
## Called when we have started the two processes and are waiting for their pid files.
wait_for_pids(){
# We are sleeping a bit here mostly because sidekiq is slow at writing its pid
i=0;
- while [ ! -f $web_server_pid_path ] || [ ! -f $sidekiq_pid_path ] || [ ! -f $gitlab_workhorse_pid_path ] || { [ "$mail_room_enabled" = true ] && [ ! -f $mail_room_pid_path ]; }; do
+ while [ ! -f $web_server_pid_path ] || [ ! -f $sidekiq_pid_path ] || [ ! -f $gitlab_workhorse_pid_path ] || { [ "$mail_room_enabled" = true ] && [ ! -f $mail_room_pid_path ]; } || { [ "$gitlab_pages_enabled" = true ] && [ ! -f $gitlab_pages_pid_path ]; } || { [ "$gitaly_enabled" = true ] && [ ! -f $gitaly_pid_path ]; }; do
sleep 0.1;
i=$((i+1))
if [ $((i%10)) = 0 ]; then
@@ -144,7 +167,23 @@ check_status(){
mail_room_status="-1"
fi
fi
- if [ $web_status = 0 ] && [ $sidekiq_status = 0 ] && [ $gitlab_workhorse_status = 0 ] && { [ "$mail_room_enabled" != true ] || [ $mail_room_status = 0 ]; }; then
+ if [ "$gitlab_pages_enabled" = true ]; then
+ if [ $gppid -ne 0 ]; then
+ kill -0 "$gppid" 2>/dev/null
+ gitlab_pages_status="$?"
+ else
+ gitlab_pages_status="-1"
+ fi
+ fi
+ if [ "$gitaly_enabled" = true ]; then
+ if [ $gapid -ne 0 ]; then
+ kill -0 "$gapid" 2>/dev/null
+ gitaly_status="$?"
+ else
+ gitaly_status="-1"
+ fi
+ fi
+ if [ $web_status = 0 ] && [ $sidekiq_status = 0 ] && [ $gitlab_workhorse_status = 0 ] && { [ "$mail_room_enabled" != true ] || [ $mail_room_status = 0 ]; } && { [ "$gitlab_pages_enabled" != true ] || [ $gitlab_pages_status = 0 ]; } && { [ "$gitaly_enabled" != true ] || [ $gitaly_status = 0 ]; }; then
gitlab_status=0
else
# http://refspecs.linuxbase.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html
@@ -186,12 +225,26 @@ check_stale_pids(){
exit 1
fi
fi
+ if [ "$gitlab_pages_enabled" = true ] && [ "$gppid" != "0" ] && [ "$gitlab_pages_status" != "0" ]; then
+ echo "Removing stale GitLab Pages job dispatcher pid. This is most likely caused by GitLab Pages crashing the last time it ran."
+ if ! rm "$gitlab_pages_pid_path"; then
+ echo "Unable to remove stale pid, exiting"
+ exit 1
+ fi
+ fi
+ if [ "$gitaly_enabled" = true ] && [ "$gapid" != "0" ] && [ "$gitaly_status" != "0" ]; then
+ echo "Removing stale Gitaly pid. This is most likely caused by Gitaly crashing the last time it ran."
+ if ! rm "$gitaly_pid_path"; then
+ echo "Unable to remove stale pid, exiting"
+ exit 1
+ fi
+ fi
}
## If no parts of the service is running, bail out.
exit_if_not_running(){
check_stale_pids
- if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
+ if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" != "0" ]; } && { [ "$gitaly_enabled" != true ] || [ "$gitaly_status" != "0" ]; }; then
echo "GitLab is not running."
exit
fi
@@ -213,6 +266,12 @@ start_gitlab() {
if [ "$mail_room_enabled" = true ] && [ "$mail_room_status" != "0" ]; then
echo "Starting GitLab MailRoom"
fi
+ if [ "$gitlab_pages_enabled" = true ] && [ "$gitlab_pages_status" != "0" ]; then
+ echo "Starting GitLab Pages"
+ fi
+ if [ "$gitaly_enabled" = true ] && [ "$gitaly_status" != "0" ]; then
+ echo "Starting Gitaly"
+ fi
# Then check if the service is running. If it is: don't start again.
if [ "$web_status" = "0" ]; then
@@ -252,6 +311,26 @@ start_gitlab() {
fi
fi
+ if [ "$gitlab_pages_enabled" = true ]; then
+ if [ "$gitlab_pages_status" = "0" ]; then
+ echo "The GitLab Pages is already running with pid $spid, not restarting"
+ else
+ $app_root/bin/daemon_with_pidfile $gitlab_pages_pid_path \
+ $gitlab_pages_dir/gitlab-pages $gitlab_pages_options \
+ >> $gitlab_pages_log 2>&1 &
+ fi
+ fi
+
+ if [ "$gitaly_enabled" = true ]; then
+ if [ "$gitaly_status" = "0" ]; then
+ echo "Gitaly is already running with pid $gapid, not restarting"
+ else
+ $app_root/bin/daemon_with_pidfile $gitaly_pid_path \
+ $app_root/bin/with_env $gitaly_dir/env \
+ $gitaly_dir/gitaly >> $gitaly_log 2>&1 &
+ fi
+ fi
+
# Wait for the pids to be planted
wait_for_pids
# Finally check the status to tell wether or not GitLab is running
@@ -278,13 +357,21 @@ stop_gitlab() {
echo "Shutting down GitLab MailRoom"
RAILS_ENV=$RAILS_ENV bin/mail_room stop
fi
+ if [ "$gitlab_pages_status" = "0" ]; then
+ echo "Shutting down gitlab-pages"
+ kill -- $(cat $gitlab_pages_pid_path)
+ fi
+ if [ "$gitaly_status" = "0" ]; then
+ echo "Shutting down Gitaly"
+ kill -- $(cat $gitaly_pid_path)
+ fi
# If something needs to be stopped, lets wait for it to stop. Never use SIGKILL in a script.
- while [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse_status" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; do
+ while [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse_status" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; } || { [ "$gitlab_pages_enabled" = true ] && [ "$gitlab_pages_status" = "0" ]; } || { [ "$gitaly_enabled" = true ] && [ "$gitaly_status" = "0" ]; }; do
sleep 1
check_status
printf "."
- if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
+ if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" != "0" ]; } && { [ "$gitaly_enabled" != true ] || [ "$gitaly_status" != "0" ]; }; then
printf "\n"
break
fi
@@ -298,6 +385,8 @@ stop_gitlab() {
if [ "$mail_room_enabled" = true ]; then
rm "$mail_room_pid_path" 2>/dev/null
fi
+ rm -f "$gitlab_pages_pid_path"
+ rm -f "$gitaly_pid_path"
print_status
}
@@ -305,7 +394,7 @@ stop_gitlab() {
## Prints the status of GitLab and its components.
print_status() {
check_status
- if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
+ if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" != "0" ]; } && { [ "$gitaly_enabled" != true ] || [ "$gitaly_status" != "0" ]; }; then
echo "GitLab is not running."
return
fi
@@ -331,7 +420,21 @@ print_status() {
printf "The GitLab MailRoom email processor is \033[31mnot running\033[0m.\n"
fi
fi
- if [ "$web_status" = "0" ] && [ "$sidekiq_status" = "0" ] && [ "$gitlab_workhorse_status" = "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" = "0" ]; }; then
+ if [ "$gitlab_pages_enabled" = true ]; then
+ if [ "$gitlab_pages_status" = "0" ]; then
+ echo "The GitLab Pages with pid $mpid is running."
+ else
+ printf "The GitLab Pages is \033[31mnot running\033[0m.\n"
+ fi
+ fi
+ if [ "$gitaly_enabled" = true ]; then
+ if [ "$gitaly_status" = "0" ]; then
+ echo "Gitaly with pid $gapid is running."
+ else
+ printf "Gitaly is \033[31mnot running\033[0m.\n"
+ fi
+ fi
+ if [ "$web_status" = "0" ] && [ "$sidekiq_status" = "0" ] && [ "$gitlab_workhorse_status" = "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" = "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" = "0" ]; } && { [ "$gitaly_enabled" != true ] || [ "$gitaly_status" = "0" ]; }; then
printf "GitLab and all its components are \033[32mup and running\033[0m.\n"
fi
}
@@ -362,7 +465,7 @@ reload_gitlab(){
## Restarts Sidekiq and Unicorn.
restart_gitlab(){
check_status
- if [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; then
+ if [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; } || { [ "$gitlab_pages_enabled" = true ] && [ "$gitlab_pages_status" = "0" ]; } || { [ "$gitaly_enabled" = true ] && [ "$gitaly_status" = "0" ]; }; then
stop_gitlab
fi
start_gitlab
diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example
index cc8617b72ca..9472c3c992f 100755..100644
--- a/lib/support/init.d/gitlab.default.example
+++ b/lib/support/init.d/gitlab.default.example
@@ -47,6 +47,30 @@ gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid"
gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080 -authSocket $socket_path/gitlab.socket -documentRoot $app_root/public"
gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log"
+# The GitLab Pages Daemon needs either a separate IP address on which it will
+# listen or use different ports than 80 or 443 that will be forwarded to GitLab
+# Pages Daemon.
+#
+# To enable HTTP support for custom domains add the `-listen-http` directive
+# in `gitlab_pages_options` below.
+# The value of -listen-http must be set to `gitlab.yml > pages > external_http`
+# as well. For example:
+#
+# -listen-http 1.1.1.1:80 -listen-http [2001::1]:80
+#
+# To enable HTTPS support for custom domains add the `-listen-https`,
+# `-root-cert` and `-root-key` directives in `gitlab_pages_options` below.
+# The value of -listen-https must be set to `gitlab.yml > pages > external_https`
+# as well. For example:
+#
+# -listen-https 1.1.1.1:443 -listen-http [2001::1]:443 -root-cert /path/to/example.com.crt -root-key /path/to/example.com.key
+#
+# The -pages-domain must be specified the same as in `gitlab.yml > pages > host`.
+# Set `gitlab_pages_enabled=true` if you want to enable the Pages feature.
+gitlab_pages_enabled=false
+gitlab_pages_options="-pages-domain example.com -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090"
+gitlab_pages_log="$app_root/log/gitlab-pages.log"
+
# mail_room_enabled specifies whether mail_room, which is used to process incoming email, is enabled.
# This is required for the Reply by email feature.
# The default is "false"
@@ -60,3 +84,7 @@ mail_room_pid_path="$pid_path/mail_room.pid"
# shell other than "bash"
# The default is "/bin/bash"
shell_path="/bin/bash"
+
+# This variable controls whether the init script starts/stops Gitaly
+gitaly_enabled=false
+gitaly_log="$app_root/log/gitaly.log"
diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab
index 2f7c34a3f31..f25e66d54c8 100644
--- a/lib/support/nginx/gitlab
+++ b/lib/support/nginx/gitlab
@@ -38,6 +38,13 @@ server {
## See app/controllers/application_controller.rb for headers set
+ ## Real IP Module Config
+ ## http://nginx.org/en/docs/http/ngx_http_realip_module.html
+ real_ip_header X-Real-IP; ## X-Real-IP or X-Forwarded-For or proxy_protocol
+ real_ip_recursive off; ## If you enable 'on'
+ ## If you have a trusted IP address, uncomment it and set it
+ # set_real_ip_from YOUR_TRUSTED_ADDRESS; ## Replace this with something like 192.168.1.0/24
+
## Individual nginx logs for this GitLab vhost
access_log /var/log/nginx/gitlab_access.log;
error_log /var/log/nginx/gitlab_error.log;
diff --git a/lib/support/nginx/gitlab-pages b/lib/support/nginx/gitlab-pages
new file mode 100644
index 00000000000..d9746c5c1aa
--- /dev/null
+++ b/lib/support/nginx/gitlab-pages
@@ -0,0 +1,28 @@
+## GitLab
+##
+
+## Pages serving host
+server {
+ listen 0.0.0.0:80;
+ listen [::]:80 ipv6only=on;
+
+ ## Replace this with something like pages.gitlab.com
+ server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$;
+
+ ## Individual nginx logs for GitLab pages
+ access_log /var/log/nginx/gitlab_pages_access.log;
+ error_log /var/log/nginx/gitlab_pages_error.log;
+
+ location / {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ # The same address as passed to GitLab Pages: `-listen-proxy`
+ proxy_pass http://localhost:8090/;
+ }
+
+ # Define custom error pages
+ error_page 403 /403.html;
+ error_page 404 /404.html;
+}
diff --git a/lib/support/nginx/gitlab-pages-ssl b/lib/support/nginx/gitlab-pages-ssl
new file mode 100644
index 00000000000..a1ccf266835
--- /dev/null
+++ b/lib/support/nginx/gitlab-pages-ssl
@@ -0,0 +1,77 @@
+## GitLab
+##
+
+## Redirects all HTTP traffic to the HTTPS host
+server {
+ ## Either remove "default_server" from the listen line below,
+ ## or delete the /etc/nginx/sites-enabled/default file. This will cause gitlab
+ ## to be served if you visit any address that your server responds to, eg.
+ ## the ip address of the server (http://x.x.x.x/)
+ listen 0.0.0.0:80;
+ listen [::]:80 ipv6only=on;
+
+ ## Replace this with something like pages.gitlab.com
+ server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$;
+ server_tokens off; ## Don't show the nginx version number, a security best practice
+
+ return 301 https://$http_host$request_uri;
+
+ access_log /var/log/nginx/gitlab_pages_access.log;
+ error_log /var/log/nginx/gitlab_pages_access.log;
+}
+
+## Pages serving host
+server {
+ listen 0.0.0.0:443 ssl;
+ listen [::]:443 ipv6only=on ssl http2;
+
+ ## Replace this with something like pages.gitlab.com
+ server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$;
+ server_tokens off; ## Don't show the nginx version number, a security best practice
+
+ ## Strong SSL Security
+ ## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html & https://cipherli.st/
+ ssl on;
+ ssl_certificate /etc/nginx/ssl/gitlab-pages.crt;
+ ssl_certificate_key /etc/nginx/ssl/gitlab-pages.key;
+
+ # GitLab needs backwards compatible ciphers to retain compatibility with Java IDEs
+ ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
+ ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
+ ssl_prefer_server_ciphers on;
+ ssl_session_cache shared:SSL:10m;
+ ssl_session_timeout 5m;
+
+ ## See app/controllers/application_controller.rb for headers set
+
+ ## [Optional] If your certficate has OCSP, enable OCSP stapling to reduce the overhead and latency of running SSL.
+ ## Replace with your ssl_trusted_certificate. For more info see:
+ ## - https://medium.com/devops-programming/4445f4862461
+ ## - https://www.ruby-forum.com/topic/4419319
+ ## - https://www.digitalocean.com/community/tutorials/how-to-configure-ocsp-stapling-on-apache-and-nginx
+ # ssl_stapling on;
+ # ssl_stapling_verify on;
+ # ssl_trusted_certificate /etc/nginx/ssl/stapling.trusted.crt;
+
+ ## [Optional] Generate a stronger DHE parameter:
+ ## sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096
+ ##
+ # ssl_dhparam /etc/ssl/certs/dhparam.pem;
+
+ ## Individual nginx logs for GitLab pages
+ access_log /var/log/nginx/gitlab_pages_access.log;
+ error_log /var/log/nginx/gitlab_pages_error.log;
+
+ location / {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ # The same address as passed to GitLab Pages: `-listen-proxy`
+ proxy_pass http://localhost:8090/;
+ }
+
+ # Define custom error pages
+ error_page 403 /403.html;
+ error_page 404 /404.html;
+}
diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl
index 5661394058d..2b40da18bab 100644
--- a/lib/support/nginx/gitlab-ssl
+++ b/lib/support/nginx/gitlab-ssl
@@ -82,6 +82,16 @@ server {
##
# ssl_dhparam /etc/ssl/certs/dhparam.pem;
+ ## [Optional] Enable HTTP Strict Transport Security
+ # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
+
+ ## Real IP Module Config
+ ## http://nginx.org/en/docs/http/ngx_http_realip_module.html
+ real_ip_header X-Real-IP; ## X-Real-IP or X-Forwarded-For or proxy_protocol
+ real_ip_recursive off; ## If you enable 'on'
+ ## If you have a trusted IP address, uncomment it and set it
+ # set_real_ip_from YOUR_TRUSTED_ADDRESS; ## Replace this with something like 192.168.1.0/24
+
## Individual nginx logs for this GitLab vhost
access_log /var/log/nginx/gitlab_access.log;
error_log /var/log/nginx/gitlab_error.log;
diff --git a/lib/tasks/brakeman.rake b/lib/tasks/brakeman.rake
index d5a402907d8..2301ec9b228 100644
--- a/lib/tasks/brakeman.rake
+++ b/lib/tasks/brakeman.rake
@@ -2,7 +2,7 @@ desc 'Security check via brakeman'
task :brakeman do
# We get 0 warnings at level 'w3' but we would like to reach 'w2'. Merge
# requests are welcome!
- if system(*%W(brakeman --no-progress --skip-files lib/backup/repository.rb -w3 -z))
+ if system(*%w(brakeman --no-progress --skip-files lib/backup/repository.rb -w3 -z))
puts 'Security check succeed'
else
puts 'Security check failed'
diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake
index 78ae187817a..d55923673b1 100644
--- a/lib/tasks/cache.rake
+++ b/lib/tasks/cache.rake
@@ -1,7 +1,7 @@
namespace :cache do
namespace :clear do
REDIS_CLEAR_BATCH_SIZE = 1000 # There seems to be no speedup when pushing beyond 1,000
- REDIS_SCAN_START_STOP = '0' # Magic value, see http://redis.io/commands/scan
+ REDIS_SCAN_START_STOP = '0'.freeze # Magic value, see http://redis.io/commands/scan
desc "GitLab | Clear redis cache"
task redis: :environment do
diff --git a/lib/tasks/config_lint.rake b/lib/tasks/config_lint.rake
new file mode 100644
index 00000000000..ddbcf1e1eb8
--- /dev/null
+++ b/lib/tasks/config_lint.rake
@@ -0,0 +1,25 @@
+module ConfigLint
+ def self.run(files)
+ failures = files.reject do |file|
+ yield(file)
+ end
+
+ if failures.present?
+ puts failures
+ exit failures.count
+ end
+ end
+end
+
+desc "Checks syntax for shell scripts and nginx config files in 'lib/support/'"
+task :config_lint do
+ shell_scripts = [
+ 'lib/support/init.d/gitlab',
+ 'lib/support/init.d/gitlab.default.example',
+ 'lib/support/deploy/deploy.sh'
+ ]
+
+ ConfigLint.run(shell_scripts) do |file|
+ Kernel.system('bash', '-n', file)
+ end
+end
diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake
index 5e94fba97bf..e65609d7001 100644
--- a/lib/tasks/dev.rake
+++ b/lib/tasks/dev.rake
@@ -2,7 +2,7 @@ task dev: ["dev:setup"]
namespace :dev do
desc "GitLab | Setup developer environment (db, fixtures)"
- task :setup => :environment do
+ task setup: :environment do
ENV['force'] = 'yes'
Rake::Task["gitlab:setup"].invoke
Rake::Task["gitlab:shell:setup"].invoke
diff --git a/lib/tasks/downtime_check.rake b/lib/tasks/downtime_check.rake
index afe5d42910c..557f4fef10b 100644
--- a/lib/tasks/downtime_check.rake
+++ b/lib/tasks/downtime_check.rake
@@ -1,10 +1,10 @@
desc 'Checks if migrations in a branch require downtime'
task downtime_check: :environment do
- if defined?(Gitlab::License)
- repo = 'gitlab-ee'
- else
- repo = 'gitlab-ce'
- end
+ repo = if defined?(Gitlab::License)
+ 'gitlab-ee'
+ else
+ 'gitlab-ce'
+ end
`git fetch https://gitlab.com/gitlab-org/#{repo}.git --depth 1`
diff --git a/lib/tasks/eslint.rake b/lib/tasks/eslint.rake
index d43cbad1909..51f5d768102 100644
--- a/lib/tasks/eslint.rake
+++ b/lib/tasks/eslint.rake
@@ -1,7 +1,8 @@
unless Rails.env.production?
desc "GitLab | Run ESLint"
- task :eslint do
- system("npm", "run", "eslint")
+ task eslint: ['yarn:check'] do
+ unless system('yarn run eslint')
+ abort('rake eslint failed')
+ end
end
end
-
diff --git a/lib/tasks/flay.rake b/lib/tasks/flay.rake
index e9587595fef..7ad2b2e4d39 100644
--- a/lib/tasks/flay.rake
+++ b/lib/tasks/flay.rake
@@ -1,6 +1,6 @@
desc 'Code duplication analyze via flay'
task :flay do
- output = %x(bundle exec flay --mass 35 app/ lib/gitlab/)
+ output = `bundle exec flay --mass 35 app/ lib/gitlab/`
if output.include? "Similar code found"
puts output
diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake
index 993112aee3b..5293f5af12d 100644
--- a/lib/tasks/gemojione.rake
+++ b/lib/tasks/gemojione.rake
@@ -1,33 +1,36 @@
namespace :gemojione do
desc 'Generates Emoji SHA256 digests'
- task digests: :environment do
+ task digests: ['yarn:check', 'environment'] do
require 'digest/sha2'
require 'json'
- dir = Gemojione.images_path
- digests = []
- aliases = Hash.new { |hash, key| hash[key] = [] }
- aliases_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json')
-
- JSON.parse(File.read(aliases_path)).each do |alias_name, real_name|
- aliases[real_name] << alias_name
- end
-
- Gitlab::AwardEmoji.emojis.map do |name, emoji_hash|
- fpath = File.join(dir, "#{emoji_hash['unicode']}.png")
- digest = Digest::SHA256.file(fpath).hexdigest
+ # We don't have `node_modules` available in built versions of GitLab
+ FileUtils.cp_r(Rails.root.join('node_modules', 'emoji-unicode-version', 'emoji-unicode-version-map.json'), File.join(Rails.root, 'fixtures', 'emojis'))
- digests << { name: name, unicode: emoji_hash['unicode'], digest: digest }
+ dir = Gemojione.images_path
+ resultant_emoji_map = {}
+
+ Gitlab::Emoji.emojis.each do |name, emoji_hash|
+ # Ignore aliases
+ unless Gitlab::Emoji.emojis_aliases.key?(name)
+ fpath = File.join(dir, "#{emoji_hash['unicode']}.png")
+ hash_digest = Digest::SHA256.file(fpath).hexdigest
+
+ entry = {
+ category: emoji_hash['category'],
+ moji: emoji_hash['moji'],
+ unicodeVersion: Gitlab::Emoji.emoji_unicode_version(name),
+ digest: hash_digest,
+ }
- aliases[name].each do |alias_name|
- digests << { name: alias_name, unicode: emoji_hash['unicode'], digest: digest }
+ resultant_emoji_map[name] = entry
end
end
out = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
File.open(out, 'w') do |handle|
- handle.write(JSON.pretty_generate(digests))
+ handle.write(JSON.pretty_generate(resultant_emoji_map))
end
end
@@ -55,21 +58,40 @@ namespace :gemojione do
SPRITESHEET_WIDTH = 860
SPRITESHEET_HEIGHT = 840
+ # Setup a map to rename image files
+ emoji_unicode_string_to_name_map = {}
+ Gitlab::Emoji.emojis.each do |name, emoji_hash|
+ # Ignore aliases
+ unless Gitlab::Emoji.emojis_aliases.key?(name)
+ emoji_unicode_string_to_name_map[emoji_hash['unicode']] = name
+ end
+ end
+
+ # Copy the Gemojione assets to the temporary folder for renaming
+ emoji_dir = "app/assets/images/emoji"
+ FileUtils.rm_rf(emoji_dir)
+ FileUtils.mkdir_p(emoji_dir, mode: 0700)
+ FileUtils.cp_r(File.join(Gemojione.images_path, '.'), emoji_dir)
+ Dir[File.join(emoji_dir, "**/*.png")].each do |png|
+ image_path = png
+ rename_to_named_emoji_image!(emoji_unicode_string_to_name_map, image_path)
+ end
+
Dir.mktmpdir do |tmpdir|
- # Copy the Gemojione assets to the temporary folder for resizing
- FileUtils.cp_r(Gemojione.images_path, tmpdir)
+ FileUtils.cp_r(File.join(emoji_dir, '.'), tmpdir)
Dir.chdir(tmpdir) do
Dir["**/*.png"].each do |png|
- resize!(File.join(tmpdir, png), SIZE)
+ tmp_image_path = File.join(tmpdir, png)
+ resize!(tmp_image_path, SIZE)
end
end
- style_path = Rails.root.join(*%w(app assets stylesheets pages emojis.scss))
+ style_path = Rails.root.join(*%w(app assets stylesheets framework emoji-sprites.scss))
# Combine the resized assets into a packed sprite and re-generate the SCSS
SpriteFactory.cssurl = "image-url('$IMAGE')"
- SpriteFactory.run!(File.join(tmpdir, 'png'), {
+ SpriteFactory.run!(tmpdir, {
output_style: style_path,
output_image: "app/assets/images/emoji.png",
selector: '.emoji-',
@@ -83,7 +105,7 @@ namespace :gemojione do
# let's simplify it
system(%Q(sed -i '' "s/width: #{SIZE}px; height: #{SIZE}px; background: image-url('emoji.png')/background-position:/" #{style_path}))
system(%Q(sed -i '' "s/ no-repeat//" #{style_path}))
- system(%Q(sed -i '' "s/ 0px/ 0/" #{style_path}))
+ system(%Q(sed -i '' "s/ 0px/ 0/g" #{style_path}))
# Append a generic rule that applies to all Emojis
File.open(style_path, 'a') do |f|
@@ -92,6 +114,8 @@ namespace :gemojione do
.emoji-icon {
background-image: image-url('emoji.png');
background-repeat: no-repeat;
+ color: transparent;
+ text-indent: -99em;
height: #{SIZE}px;
width: #{SIZE}px;
@@ -112,16 +136,17 @@ namespace :gemojione do
# Now do it again but for Retina
Dir.mktmpdir do |tmpdir|
# Copy the Gemojione assets to the temporary folder for resizing
- FileUtils.cp_r(Gemojione.images_path, tmpdir)
+ FileUtils.cp_r(File.join(emoji_dir, '.'), tmpdir)
Dir.chdir(tmpdir) do
Dir["**/*.png"].each do |png|
- resize!(File.join(tmpdir, png), RETINA)
+ tmp_image_path = File.join(tmpdir, png)
+ resize!(tmp_image_path, RETINA)
end
end
# Combine the resized assets into a packed sprite and re-generate the SCSS
- SpriteFactory.run!(File.join(tmpdir), {
+ SpriteFactory.run!(tmpdir, {
output_image: "app/assets/images/emoji@2x.png",
style: false,
nocomments: true,
@@ -155,4 +180,20 @@ namespace :gemojione do
image.write(image_path) { self.quality = 100 }
image.destroy!
end
+
+ EMOJI_IMAGE_PATH_RE = /(.*?)(([0-9a-f]-?)+)\.png$/i
+ def rename_to_named_emoji_image!(emoji_unicode_string_to_name_map, image_path)
+ # Rename file from unicode to emoji name
+ matches = EMOJI_IMAGE_PATH_RE.match(image_path)
+ preceding_path = matches[1]
+ unicode_string = matches[2]
+ name = emoji_unicode_string_to_name_map[unicode_string]
+ if name
+ new_png_path = File.join(preceding_path, "#{name}.png")
+ FileUtils.mv(image_path, new_png_path)
+ new_png_path
+ else
+ puts "Warning: emoji_unicode_string_to_name_map missing entry for #{unicode_string}. Full path: #{image_path}"
+ end
+ end
end
diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake
new file mode 100644
index 00000000000..003d57adbbd
--- /dev/null
+++ b/lib/tasks/gitlab/assets.rake
@@ -0,0 +1,48 @@
+namespace :gitlab do
+ namespace :assets do
+ desc 'GitLab | Assets | Compile all frontend assets'
+ task compile: [
+ 'yarn:check',
+ 'rake:assets:precompile',
+ 'webpack:compile',
+ 'fix_urls'
+ ]
+
+ desc 'GitLab | Assets | Clean up old compiled frontend assets'
+ task clean: ['rake:assets:clean']
+
+ desc 'GitLab | Assets | Remove all compiled frontend assets'
+ task purge: ['rake:assets:clobber']
+
+ desc 'GitLab | Assets | Uninstall frontend dependencies'
+ task purge_modules: ['yarn:clobber']
+
+ desc 'GitLab | Assets | Fix all absolute url references in CSS'
+ task :fix_urls do
+ css_files = Dir['public/assets/*.css']
+ css_files.each do |file|
+ # replace url(/assets/*) with url(./*)
+ puts "Fixing #{file}"
+ system "sed", "-i", "-e", 's/url(\([\"\']\?\)\/assets\//url(\1.\//g', file
+
+ # rewrite the corresponding gzip file (if it exists)
+ gzip = "#{file}.gz"
+ if File.exist?(gzip)
+ puts "Fixing #{gzip}"
+
+ FileUtils.rm(gzip)
+ mtime = File.stat(file).mtime
+
+ File.open(gzip, 'wb+') do |f|
+ gz = Zlib::GzipWriter.new(f, Zlib::BEST_COMPRESSION)
+ gz.mtime = mtime
+ gz.write IO.binread(file)
+ gz.close
+
+ File.utime(mtime, mtime, f.path)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index a9f1255e8cf..1650263b98d 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -13,6 +13,7 @@ namespace :gitlab do
Rake::Task["gitlab:backup:uploads:create"].invoke
Rake::Task["gitlab:backup:builds:create"].invoke
Rake::Task["gitlab:backup:artifacts:create"].invoke
+ Rake::Task["gitlab:backup:pages:create"].invoke
Rake::Task["gitlab:backup:lfs:create"].invoke
Rake::Task["gitlab:backup:registry:create"].invoke
@@ -56,6 +57,7 @@ namespace :gitlab do
Rake::Task['gitlab:backup:uploads:restore'].invoke unless backup.skipped?('uploads')
Rake::Task['gitlab:backup:builds:restore'].invoke unless backup.skipped?('builds')
Rake::Task['gitlab:backup:artifacts:restore'].invoke unless backup.skipped?('artifacts')
+ Rake::Task["gitlab:backup:pages:restore"].invoke unless backup.skipped?('pages')
Rake::Task['gitlab:backup:lfs:restore'].invoke unless backup.skipped?('lfs')
Rake::Task['gitlab:backup:registry:restore'].invoke unless backup.skipped?('registry')
Rake::Task['gitlab:shell:setup'].invoke
@@ -159,6 +161,25 @@ namespace :gitlab do
end
end
+ namespace :pages do
+ task create: :environment do
+ $progress.puts "Dumping pages ... ".color(:blue)
+
+ if ENV["SKIP"] && ENV["SKIP"].include?("pages")
+ $progress.puts "[SKIPPED]".color(:cyan)
+ else
+ Backup::Pages.new.dump
+ $progress.puts "done".color(:green)
+ end
+ end
+
+ task restore: :environment do
+ $progress.puts "Restoring pages ... ".color(:blue)
+ Backup::Pages.new.restore
+ $progress.puts "done".color(:green)
+ end
+ end
+
namespace :lfs do
task create: :environment do
$progress.puts "Dumping lfs objects ... ".color(:blue)
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 35c4194e87c..a9a48f7188f 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -6,8 +6,6 @@ namespace :gitlab do
gitlab:ldap:check
gitlab:app:check}
-
-
namespace :app do
desc "GitLab | Check the configuration of the GitLab Rails app"
task check: :environment do
@@ -34,7 +32,6 @@ namespace :gitlab do
finished_checking "GitLab"
end
-
# Checks
########################
@@ -194,7 +191,7 @@ namespace :gitlab do
def check_migrations_are_up
print "All migrations up? ... "
- migration_status, _ = Gitlab::Popen.popen(%W(bundle exec rake db:migrate:status))
+ migration_status, _ = Gitlab::Popen.popen(%w(bundle exec rake db:migrate:status))
unless migration_status =~ /down\s+\d{14}/
puts "yes".color(:green)
@@ -279,7 +276,7 @@ namespace :gitlab do
upload_path_tmp = File.join(upload_path, 'tmp')
if File.stat(upload_path).mode == 040700
- unless Dir.exists?(upload_path_tmp)
+ unless Dir.exist?(upload_path_tmp)
puts 'skipped (no tmp uploads folder yet)'.color(:magenta)
return
end
@@ -316,7 +313,7 @@ namespace :gitlab do
min_redis_version = "2.8.0"
print "Redis version >= #{min_redis_version}? ... "
- redis_version = run_command(%W(redis-cli --version))
+ redis_version = run_command(%w(redis-cli --version))
redis_version = redis_version.try(:match, /redis-cli (\d+\.\d+\.\d+)/)
if redis_version &&
(Gem::Version.new(redis_version[1]) > Gem::Version.new(min_redis_version))
@@ -351,14 +348,14 @@ namespace :gitlab do
finished_checking "GitLab Shell"
end
-
# Checks
########################
def check_repo_base_exists
puts "Repo base directory exists?"
- Gitlab.config.repositories.storages.each do |name, repo_base_path|
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ repo_base_path = repository_storage['path']
print "#{name}... "
if File.exist?(repo_base_path)
@@ -382,12 +379,13 @@ namespace :gitlab do
def check_repo_base_is_not_symlink
puts "Repo storage directories are symlinks?"
- Gitlab.config.repositories.storages.each do |name, repo_base_path|
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ repo_base_path = repository_storage['path']
print "#{name}... "
unless File.exist?(repo_base_path)
puts "can't check because of previous errors".color(:magenta)
- return
+ break
end
unless File.symlink?(repo_base_path)
@@ -405,12 +403,13 @@ namespace :gitlab do
def check_repo_base_permissions
puts "Repo paths access is drwxrws---?"
- Gitlab.config.repositories.storages.each do |name, repo_base_path|
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ repo_base_path = repository_storage['path']
print "#{name}... "
unless File.exist?(repo_base_path)
puts "can't check because of previous errors".color(:magenta)
- return
+ break
end
if File.stat(repo_base_path).mode.to_s(8).ends_with?("2770")
@@ -435,12 +434,13 @@ namespace :gitlab do
gitlab_shell_owner_group = Gitlab.config.gitlab_shell.owner_group
puts "Repo paths owned by #{gitlab_shell_ssh_user}:#{gitlab_shell_owner_group}?"
- Gitlab.config.repositories.storages.each do |name, repo_base_path|
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ repo_base_path = repository_storage['path']
print "#{name}... "
unless File.exist?(repo_base_path)
puts "can't check because of previous errors".color(:magenta)
- return
+ break
end
uid = uid_for(gitlab_shell_ssh_user)
@@ -493,7 +493,6 @@ namespace :gitlab do
)
fix_and_rerun
end
-
end
end
@@ -565,8 +564,6 @@ namespace :gitlab do
end
end
-
-
namespace :sidekiq do
desc "GitLab | Check the configuration of Sidekiq"
task check: :environment do
@@ -579,7 +576,6 @@ namespace :gitlab do
finished_checking "Sidekiq"
end
-
# Checks
########################
@@ -621,12 +617,11 @@ namespace :gitlab do
end
def sidekiq_process_count
- ps_ux, _ = Gitlab::Popen.popen(%W(ps ux))
+ ps_ux, _ = Gitlab::Popen.popen(%w(ps uxww))
ps_ux.scan(/sidekiq \d+\.\d+\.\d+/).count
end
end
-
namespace :incoming_email do
desc "GitLab | Check the configuration of Reply by email"
task check: :environment do
@@ -649,7 +644,6 @@ namespace :gitlab do
finished_checking "Reply by email"
end
-
# Checks
########################
@@ -724,8 +718,11 @@ namespace :gitlab do
def check_imap_authentication
print "IMAP server credentials are correct? ... "
- config_path = Rails.root.join('config', 'mail_room.yml')
- config_file = YAML.load(ERB.new(File.read(config_path)).result)
+ config_path = Rails.root.join('config', 'mail_room.yml').to_s
+ erb = ERB.new(File.read(config_path))
+ erb.filename = config_path
+ config_file = YAML.load(erb.result)
+
config = config_file[:mailboxes].first
if config
@@ -754,7 +751,7 @@ namespace :gitlab do
end
def mail_room_running?
- ps_ux, _ = Gitlab::Popen.popen(%W(ps ux))
+ ps_ux, _ = Gitlab::Popen.popen(%w(ps uxww))
ps_ux.include?("mail_room")
end
end
@@ -802,13 +799,13 @@ namespace :gitlab do
def check_ldap_auth(adapter)
auth = adapter.config.has_auth?
- if auth && adapter.ldap.bind
- message = 'Success'.color(:green)
- elsif auth
- message = 'Failed. Check `bind_dn` and `password` configuration values'.color(:red)
- else
- message = 'Anonymous. No `bind_dn` or `password` configured'.color(:yellow)
- end
+ message = if auth && adapter.ldap.bind
+ 'Success'.color(:green)
+ elsif auth
+ 'Failed. Check `bind_dn` and `password` configuration values'.color(:red)
+ else
+ 'Anonymous. No `bind_dn` or `password` configured'.color(:yellow)
+ end
puts "LDAP authentication... #{message}"
end
@@ -817,8 +814,8 @@ namespace :gitlab do
namespace :repo do
desc "GitLab | Check the integrity of the repositories managed by GitLab"
task check: :environment do
- Gitlab.config.repositories.storages.each do |name, path|
- namespace_dirs = Dir.glob(File.join(path, '*'))
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ namespace_dirs = Dir.glob(File.join(repository_storage['path'], '*'))
namespace_dirs.each do |namespace_dir|
repo_dirs = Dir.glob(File.join(namespace_dir, '*'))
@@ -835,11 +832,11 @@ namespace :gitlab do
user = User.find_by(username: username)
if user
repo_dirs = user.authorized_projects.map do |p|
- File.join(
- p.repository_storage_path,
- "#{p.path_with_namespace}.git"
- )
- end
+ File.join(
+ p.repository_storage_path,
+ "#{p.path_with_namespace}.git"
+ )
+ end
repo_dirs.each { |repo_dir| check_repo_integrity(repo_dir) }
else
@@ -852,7 +849,7 @@ namespace :gitlab do
##########################
def fix_and_rerun
- puts " Please #{"fix the error above"} and rerun the checks.".color(:red)
+ puts " Please fix the error above and rerun the checks.".color(:red)
end
def for_more_information(*sources)
@@ -914,7 +911,7 @@ namespace :gitlab do
def check_ruby_version
required_version = Gitlab::VersionInfo.new(2, 1, 0)
- current_version = Gitlab::VersionInfo.parse(run_command(%W(ruby --version)))
+ current_version = Gitlab::VersionInfo.parse(run_command(%w(ruby --version)))
print "Ruby version >= #{required_version} ? ... "
@@ -985,13 +982,13 @@ namespace :gitlab do
end
def check_config_lock(repo_dir)
- config_exists = File.exist?(File.join(repo_dir,'config.lock'))
+ config_exists = File.exist?(File.join(repo_dir, 'config.lock'))
config_output = config_exists ? 'yes'.color(:red) : 'no'.color(:green)
puts "'config.lock' file exists?".color(:yellow) + " ... #{config_output}"
end
def check_ref_locks(repo_dir)
- lock_files = Dir.glob(File.join(repo_dir,'refs/heads/*.lock'))
+ lock_files = Dir.glob(File.join(repo_dir, 'refs/heads/*.lock'))
if lock_files.present?
puts "Ref lock files exist:".color(:red)
lock_files.each do |lock_file|
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index 4a696a52b4d..f76bef5f4bf 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -6,7 +6,8 @@ namespace :gitlab do
remove_flag = ENV['REMOVE']
namespaces = Namespace.pluck(:path)
- Gitlab.config.repositories.storages.each do |name, git_base_path|
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ git_base_path = repository_storage['path']
all_dirs = Dir.glob(git_base_path + '/*')
puts git_base_path.color(:yellow)
@@ -25,7 +26,6 @@ namespace :gitlab do
end
all_dirs.each do |dir_path|
-
if remove_flag
if FileUtils.rm_rf dir_path
puts "Removed...#{dir_path}".color(:red)
@@ -48,17 +48,18 @@ namespace :gitlab do
warn_user_is_not_gitlab
move_suffix = "+orphaned+#{Time.now.to_i}"
- Gitlab.config.repositories.storages.each do |name, repo_root|
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ repo_root = repository_storage['path']
# Look for global repos (legacy, depth 1) and normal repos (depth 2)
IO.popen(%W(find #{repo_root} -mindepth 1 -maxdepth 2 -name *.git)) do |find|
find.each_line do |path|
path.chomp!
- repo_with_namespace = path.
- sub(repo_root, '').
- sub(%r{^/*}, '').
- chomp('.git').
- chomp('.wiki')
- next if Project.find_with_namespace(repo_with_namespace)
+ repo_with_namespace = path
+ .sub(repo_root, '')
+ .sub(%r{^/*}, '')
+ .chomp('.git')
+ .chomp('.wiki')
+ next if Project.find_by_full_path(repo_with_namespace)
new_path = path + move_suffix
puts path.inspect + ' -> ' + new_path.inspect
File.rename(path, new_path)
diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake
index 7c96bc864ce..5476438b8fa 100644
--- a/lib/tasks/gitlab/db.rake
+++ b/lib/tasks/gitlab/db.rake
@@ -23,7 +23,7 @@ namespace :gitlab do
end
desc 'Drop all tables'
- task :drop_tables => :environment do
+ task drop_tables: :environment do
connection = ActiveRecord::Base.connection
# If MySQL, turn off foreign key checks
@@ -62,9 +62,9 @@ namespace :gitlab do
ref = Shellwords.escape(args[:ref])
- migrations = `git diff #{ref}.. --name-only -- db/migrate`.lines.
- map { |file| Rails.root.join(file.strip).to_s }.
- select { |file| File.file?(file) }
+ migrations = `git diff #{ref}.. --diff-filter=A --name-only -- db/migrate`.lines
+ .map { |file| Rails.root.join(file.strip).to_s }
+ .select { |file| File.file?(file) }
Gitlab::DowntimeCheck.new.check_and_print(migrations)
end
diff --git a/lib/tasks/gitlab/dev.rake b/lib/tasks/gitlab/dev.rake
index 7db0779def8..7ccda04a35f 100644
--- a/lib/tasks/gitlab/dev.rake
+++ b/lib/tasks/gitlab/dev.rake
@@ -4,7 +4,7 @@ namespace :gitlab do
task :ee_compat_check, [:branch] => :environment do |_, args|
opts =
if ENV['CI']
- { branch: ENV['CI_BUILD_REF_NAME'] }
+ { branch: ENV['CI_COMMIT_REF_NAME'] }
else
unless args[:branch]
puts "Must specify a branch as an argument".color(:red)
diff --git a/lib/tasks/gitlab/git.rake b/lib/tasks/gitlab/git.rake
index a67c1fe1f27..cf82134d97e 100644
--- a/lib/tasks/gitlab/git.rake
+++ b/lib/tasks/gitlab/git.rake
@@ -1,6 +1,5 @@
namespace :gitlab do
namespace :git do
-
desc "GitLab | Git | Repack"
task repack: :environment do
failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} repack -a --quiet), "Repacking repo")
@@ -50,6 +49,5 @@ namespace :gitlab do
puts "The following repositories reported errors:".color(:red)
failures.each { |f| puts "- #{f}" }
end
-
end
end
diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake
new file mode 100644
index 00000000000..c288e17ac8d
--- /dev/null
+++ b/lib/tasks/gitlab/gitaly.rake
@@ -0,0 +1,23 @@
+namespace :gitlab do
+ namespace :gitaly do
+ desc "GitLab | Install or upgrade gitaly"
+ task :install, [:dir] => :environment do |t, args|
+ warn_user_is_not_gitlab
+ unless args.dir.present?
+ abort %(Please specify the directory where you want to install gitaly:\n rake "gitlab:gitaly:install[/home/git/gitaly]")
+ end
+
+ tag = "v#{Gitlab::GitalyClient.expected_server_version}"
+ repo = 'https://gitlab.com/gitlab-org/gitaly.git'
+
+ checkout_or_clone_tag(tag: tag, repo: repo, target_dir: args.dir)
+
+ _, status = Gitlab::Popen.popen(%w[which gmake])
+ command = status.zero? ? 'gmake' : 'make'
+
+ Dir.chdir(args.dir) do
+ run_command!([command])
+ end
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake
index a2eca74a3c8..48bd9139ce8 100644
--- a/lib/tasks/gitlab/import.rake
+++ b/lib/tasks/gitlab/import.rake
@@ -11,7 +11,8 @@ namespace :gitlab do
#
desc "GitLab | Import bare repositories from repositories -> storages into GitLab project instance"
task repos: :environment do
- Gitlab.config.repositories.storages.each do |name, git_base_path|
+ Gitlab.config.repositories.storages.each_value do |repository_storage|
+ git_base_path = repository_storage['path']
repos_to_import = Dir.glob(git_base_path + '/**/*.git')
repos_to_import.each do |repo_path|
@@ -29,7 +30,7 @@ namespace :gitlab do
next
end
- project = Project.find_with_namespace(path)
+ project = Project.find_by_full_path(path)
if project
puts " * #{project.name} (#{repo_path}) exists"
@@ -46,7 +47,7 @@ namespace :gitlab do
group = Namespace.find_by(path: group_name)
# create group namespace
unless group
- group = Group.new(:name => group_name)
+ group = Group.new(name: group_name)
group.path = group_name
group.owner = user
if group.save
@@ -63,7 +64,7 @@ namespace :gitlab do
if project.persisted?
puts " * Created #{project.name} (#{repo_path})".color(:green)
- ProjectCacheWorker.perform(project.id)
+ ProjectCacheWorker.perform_async(project.id)
else
puts " * Failed trying to create #{project.name} (#{repo_path})".color(:red)
puts " Errors: #{project.errors.messages}".color(:red)
diff --git a/lib/tasks/gitlab/import_export.rake b/lib/tasks/gitlab/import_export.rake
index c2c6031db67..dd1825c8a9e 100644
--- a/lib/tasks/gitlab/import_export.rake
+++ b/lib/tasks/gitlab/import_export.rake
@@ -7,7 +7,7 @@ namespace :gitlab do
desc "GitLab | Display exported DB structure"
task data: :environment do
- puts YAML.load_file(Gitlab::ImportExport.config_file)['project_tree'].to_yaml(:SortKeys => true)
+ puts YAML.load_file(Gitlab::ImportExport.config_file)['project_tree'].to_yaml(SortKeys: true)
end
end
end
diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake
index dffea8ed155..a2a2db487b7 100644
--- a/lib/tasks/gitlab/info.rake
+++ b/lib/tasks/gitlab/info.rake
@@ -2,31 +2,35 @@ namespace :gitlab do
namespace :env do
desc "GitLab | Show information about GitLab and its environment"
task info: :environment do
-
# check if there is an RVM environment
- rvm_version = run_and_match(%W(rvm --version), /[\d\.]+/).try(:to_s)
+ rvm_version = run_and_match(%w(rvm --version), /[\d\.]+/).try(:to_s)
# check Ruby version
- ruby_version = run_and_match(%W(ruby --version), /[\d\.p]+/).try(:to_s)
+ ruby_version = run_and_match(%w(ruby --version), /[\d\.p]+/).try(:to_s)
# check Gem version
- gem_version = run_command(%W(gem --version))
- # check Bundler version
- bunder_version = run_and_match(%W(bundle --version), /[\d\.]+/).try(:to_s)
+ gem_version = run_command(%w(gem --version))
# check Bundler version
- rake_version = run_and_match(%W(rake --version), /[\d\.]+/).try(:to_s)
+ bunder_version = run_and_match(%w(bundle --version), /[\d\.]+/).try(:to_s)
+ # check Rake version
+ rake_version = run_and_match(%w(rake --version), /[\d\.]+/).try(:to_s)
+ # check redis version
+ redis_version = run_and_match(%w(redis-cli --version), /redis-cli (\d+\.\d+\.\d+)/).to_a
+ # check Git version
+ git_version = run_and_match([Gitlab.config.git.bin_path, '--version'], /git version ([\d\.]+)/).to_a
puts ""
puts "System information".color(:yellow)
puts "System:\t\t#{os_name || "unknown".color(:red)}"
- puts "Current User:\t#{run_command(%W(whoami))}"
+ puts "Current User:\t#{run_command(%w(whoami))}"
puts "Using RVM:\t#{rvm_version.present? ? "yes".color(:green) : "no"}"
puts "RVM Version:\t#{rvm_version}" if rvm_version.present?
puts "Ruby Version:\t#{ruby_version || "unknown".color(:red)}"
puts "Gem Version:\t#{gem_version || "unknown".color(:red)}"
puts "Bundler Version:#{bunder_version || "unknown".color(:red)}"
puts "Rake Version:\t#{rake_version || "unknown".color(:red)}"
+ puts "Redis Version:\t#{redis_version[1] || "unknown".color(:red)}"
+ puts "Git Version:\t#{git_version[1] || "unknown".color(:red)}"
puts "Sidekiq Version:#{Sidekiq::VERSION}"
-
# check database adapter
database_adapter = ActiveRecord::Base.connection.adapter_name.downcase
@@ -51,8 +55,6 @@ namespace :gitlab do
puts "Using Omniauth:\t#{Gitlab.config.omniauth.enabled ? "yes".color(:green) : "no"}"
puts "Omniauth Providers: #{omniauth_providers.join(', ')}" if Gitlab.config.omniauth.enabled
-
-
# check Gitolite version
gitlab_shell_version_file = "#{Gitlab.config.gitlab_shell.hooks_path}/../VERSION"
if File.readable?(gitlab_shell_version_file)
@@ -63,12 +65,11 @@ namespace :gitlab do
puts "GitLab Shell".color(:yellow)
puts "Version:\t#{gitlab_shell_version || "unknown".color(:red)}"
puts "Repository storage paths:"
- Gitlab.config.repositories.storages.each do |name, path|
- puts "- #{name}: \t#{path}"
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ puts "- #{name}: \t#{repository_storage['path']}"
end
puts "Hooks:\t\t#{Gitlab.config.gitlab_shell.hooks_path}"
puts "Git:\t\t#{Gitlab.config.git.bin_path}"
-
end
end
end
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index 5a09cd7ce41..dd2fda54e62 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -20,10 +20,10 @@ namespace :gitlab do
config = {
user: Gitlab.config.gitlab.user,
gitlab_url: gitlab_url,
- http_settings: {self_signed_cert: false}.stringify_keys,
+ http_settings: { self_signed_cert: false }.stringify_keys,
auth_file: File.join(user_home, ".ssh", "authorized_keys"),
redis: {
- bin: %x{which redis-cli}.chomp,
+ bin: `which redis-cli`.chomp,
namespace: "resque:gitlab"
}.stringify_keys,
log_level: "INFO",
@@ -43,7 +43,7 @@ namespace :gitlab do
File.open("config.yml", "w+") {|f| f.puts config.to_yaml}
# Launch installation process
- system(*%W(bin/install) + repository_storage_paths_args)
+ system(*%w(bin/install) + repository_storage_paths_args)
end
# (Re)create hooks
diff --git a/lib/tasks/gitlab/sidekiq.rake b/lib/tasks/gitlab/sidekiq.rake
index 7e2a6668e59..6cbc83b8973 100644
--- a/lib/tasks/gitlab/sidekiq.rake
+++ b/lib/tasks/gitlab/sidekiq.rake
@@ -1,13 +1,13 @@
namespace :gitlab do
namespace :sidekiq do
- QUEUE = 'queue:post_receive'
+ QUEUE = 'queue:post_receive'.freeze
desc 'Drop all Sidekiq PostReceive jobs for a given project'
- task :drop_post_receive , [:project] => :environment do |t, args|
+ task :drop_post_receive, [:project] => :environment do |t, args|
unless args.project.present?
abort "Please specify the project you want to drop PostReceive jobs for:\n rake gitlab:sidekiq:drop_post_receive[group/project]"
end
- project_path = Project.find_with_namespace(args.project).repository.path_to_repo
+ project_path = Project.find_by_full_path(args.project).repository.path_to_repo
Sidekiq.redis do |redis|
unless redis.exists(QUEUE)
@@ -21,7 +21,7 @@ namespace :gitlab do
# new jobs already. We will repopulate it with the old jobs, skipping the
# ones we want to drop.
dropped = 0
- while (job = redis.lpop(temp_queue)) do
+ while (job = redis.lpop(temp_queue))
if repo_path(job) == project_path
dropped += 1
else
diff --git a/lib/tasks/gitlab/task_helpers.rb b/lib/tasks/gitlab/task_helpers.rb
index e128738b5f8..cdba2262bc2 100644
--- a/lib/tasks/gitlab/task_helpers.rb
+++ b/lib/tasks/gitlab/task_helpers.rb
@@ -19,23 +19,20 @@ module Gitlab
# It will primarily use lsb_relase to determine the OS.
# It has fallbacks to Debian, SuSE, OS X and systems running systemd.
def os_name
- os_name = run_command(%W(lsb_release -irs))
- os_name ||= if File.readable?('/etc/system-release')
- File.read('/etc/system-release')
- end
- os_name ||= if File.readable?('/etc/debian_version')
- debian_version = File.read('/etc/debian_version')
- "Debian #{debian_version}"
- end
- os_name ||= if File.readable?('/etc/SuSE-release')
- File.read('/etc/SuSE-release')
- end
- os_name ||= if os_x_version = run_command(%W(sw_vers -productVersion))
- "Mac OS X #{os_x_version}"
- end
- os_name ||= if File.readable?('/etc/os-release')
- File.read('/etc/os-release').match(/PRETTY_NAME=\"(.+)\"/)[1]
- end
+ os_name = run_command(%w(lsb_release -irs))
+ os_name ||=
+ if File.readable?('/etc/system-release')
+ File.read('/etc/system-release')
+ elsif File.readable?('/etc/debian_version')
+ "Debian #{File.read('/etc/debian_version')}"
+ elsif File.readable?('/etc/SuSE-release')
+ File.read('/etc/SuSE-release')
+ elsif os_x_version = run_command(%w(sw_vers -productVersion))
+ "Mac OS X #{os_x_version}"
+ elsif File.readable?('/etc/os-release')
+ File.read('/etc/os-release').match(/PRETTY_NAME=\"(.+)\"/)[1]
+ end
+
os_name.try(:squish!)
end
@@ -84,7 +81,7 @@ module Gitlab
def run_command!(command)
output, status = Gitlab::Popen.popen(command)
- raise Gitlab::TaskFailedError unless status.zero?
+ raise Gitlab::TaskFailedError.new(output) unless status.zero?
output
end
@@ -104,7 +101,7 @@ module Gitlab
def warn_user_is_not_gitlab
unless @warned_user_not_gitlab
gitlab_user = Gitlab.config.gitlab.user
- current_user = run_command(%W(whoami)).chomp
+ current_user = run_command(%w(whoami)).chomp
unless current_user == gitlab_user
puts " Warning ".color(:black).background(:yellow)
puts " You are running as user #{current_user.color(:magenta)}, we hope you know what you are doing."
@@ -133,8 +130,8 @@ module Gitlab
end
def all_repos
- Gitlab.config.repositories.storages.each do |name, path|
- IO.popen(%W(find #{path} -mindepth 2 -maxdepth 2 -type d -name *.git)) do |find|
+ Gitlab.config.repositories.storages.each_value do |repository_storage|
+ IO.popen(%W(find #{repository_storage['path']} -mindepth 2 -maxdepth 2 -type d -name *.git)) do |find|
find.each_line do |path|
yield path.chomp
end
@@ -143,7 +140,7 @@ module Gitlab
end
def repository_storage_paths_args
- Gitlab.config.repositories.storages.values
+ Gitlab.config.repositories.storages.values.map { |rs| rs['path'] }
end
def user_home
@@ -171,14 +168,14 @@ module Gitlab
def reset_to_tag(tag_wanted, target_dir)
tag =
- begin
- # First try to checkout without fetching
- # to avoid stalling tests if the Internet is down.
- run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- #{tag_wanted}])
- rescue Gitlab::TaskFailedError
- run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} fetch origin])
- run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- origin/#{tag_wanted}])
- end
+ begin
+ # First try to checkout without fetching
+ # to avoid stalling tests if the Internet is down.
+ run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- #{tag_wanted}])
+ rescue Gitlab::TaskFailedError
+ run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} fetch origin])
+ run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- origin/#{tag_wanted}])
+ end
if tag
run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} reset --hard #{tag.strip}])
diff --git a/lib/tasks/gitlab/test.rake b/lib/tasks/gitlab/test.rake
index 4d4e746503a..523b0fa055b 100644
--- a/lib/tasks/gitlab/test.rake
+++ b/lib/tasks/gitlab/test.rake
@@ -2,15 +2,15 @@ namespace :gitlab do
desc "GitLab | Run all tests"
task :test do
cmds = [
- %W(rake brakeman),
- %W(rake rubocop),
- %W(rake spinach),
- %W(rake spec),
- %W(rake teaspoon)
+ %w(rake brakeman),
+ %w(rake rubocop),
+ %w(rake spinach),
+ %w(rake spec),
+ %w(rake karma)
]
cmds.each do |cmd|
- system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) or raise("#{cmd} failed!")
+ system({ 'RAILS_ENV' => 'test', 'force' => 'yes' }, *cmd) || raise("#{cmd} failed!")
end
end
end
diff --git a/lib/tasks/gitlab/track_deployment.rake b/lib/tasks/gitlab/track_deployment.rake
index 84aa2e8507a..6f101aea303 100644
--- a/lib/tasks/gitlab/track_deployment.rake
+++ b/lib/tasks/gitlab/track_deployment.rake
@@ -1,8 +1,8 @@
namespace :gitlab do
desc 'GitLab | Tracks a deployment in GitLab Performance Monitoring'
task track_deployment: :environment do
- metric = Gitlab::Metrics::Metric.
- new('deployments', version: Gitlab::VERSION)
+ metric = Gitlab::Metrics::Metric
+ .new('deployments', version: Gitlab::VERSION)
Gitlab::Metrics.submit_metrics([metric.to_hash])
end
diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake
index b77a5bb62d1..dbdfb335a5c 100644
--- a/lib/tasks/gitlab/update_templates.rake
+++ b/lib/tasks/gitlab/update_templates.rake
@@ -46,7 +46,7 @@ namespace :gitlab do
"https://gitlab.com/gitlab-org/gitlab-ci-yml.git",
/(\.{1,2}|LICENSE|Pages|autodeploy|\.gitlab-ci.yml)\z/
)
- ]
+ ].freeze
def vendor_directory
Rails.root.join('vendor')
diff --git a/lib/tasks/gitlab/web_hook.rake b/lib/tasks/gitlab/web_hook.rake
index 49530e7a372..5a1c8006052 100644
--- a/lib/tasks/gitlab/web_hook.rake
+++ b/lib/tasks/gitlab/web_hook.rake
@@ -1,7 +1,7 @@
namespace :gitlab do
namespace :web_hook do
desc "GitLab | Adds a webhook to the projects"
- task :add => :environment do
+ task add: :environment do
web_hook_url = ENV['URL']
namespace_path = ENV['NAMESPACE']
@@ -21,7 +21,7 @@ namespace :gitlab do
end
desc "GitLab | Remove a webhook from the projects"
- task :rm => :environment do
+ task rm: :environment do
web_hook_url = ENV['URL']
namespace_path = ENV['NAMESPACE']
@@ -34,7 +34,7 @@ namespace :gitlab do
end
desc "GitLab | List webhooks"
- task :list => :environment do
+ task list: :environment do
namespace_path = ENV['NAMESPACE']
projects = find_projects(namespace_path)
diff --git a/lib/tasks/grape.rake b/lib/tasks/grape.rake
index 9980e0b7984..ea2698da606 100644
--- a/lib/tasks/grape.rake
+++ b/lib/tasks/grape.rake
@@ -2,7 +2,11 @@ namespace :grape do
desc 'Print compiled grape routes'
task routes: :environment do
API::API.routes.each do |route|
- puts route
+ puts "#{route.options[:method]} #{route.path} - #{route_description(route.options)}"
end
end
+
+ def route_description(options)
+ options[:settings][:description][:description] if options[:settings][:description]
+ end
end
diff --git a/lib/tasks/karma.rake b/lib/tasks/karma.rake
new file mode 100644
index 00000000000..62a12174efa
--- /dev/null
+++ b/lib/tasks/karma.rake
@@ -0,0 +1,21 @@
+unless Rails.env.production?
+ namespace :karma do
+ desc 'GitLab | Karma | Generate fixtures for JavaScript tests'
+ RSpec::Core::RakeTask.new(:fixtures, [:pattern]) do |t, args|
+ args.with_defaults(pattern: 'spec/javascripts/fixtures/*.rb')
+ ENV['NO_KNAPSACK'] = 'true'
+ t.pattern = args[:pattern]
+ t.rspec_opts = '--format documentation'
+ end
+
+ desc 'GitLab | Karma | Run JavaScript tests'
+ task tests: ['yarn:check'] do
+ sh "yarn run karma" do |ok, res|
+ abort('rake karma:tests failed') unless ok
+ end
+ end
+ end
+
+ desc 'GitLab | Karma | Shortcut for karma:fixtures and karma:tests'
+ task karma: ['karma:fixtures', 'karma:tests']
+end
diff --git a/lib/tasks/lint.rake b/lib/tasks/lint.rake
index 32b668df3bf..7b63e93db0e 100644
--- a/lib/tasks/lint.rake
+++ b/lib/tasks/lint.rake
@@ -6,4 +6,3 @@ unless Rails.env.production?
end
end
end
-
diff --git a/lib/tasks/migrate/migrate_iids.rake b/lib/tasks/migrate/migrate_iids.rake
index 4f2486157b7..fc2cea8c016 100644
--- a/lib/tasks/migrate/migrate_iids.rake
+++ b/lib/tasks/migrate/migrate_iids.rake
@@ -24,7 +24,7 @@ task migrate_iids: :environment do
else
print 'F'
end
- rescue => ex
+ rescue
print 'F'
end
end
diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake
index f5caca3ddbf..8938bc515f5 100644
--- a/lib/tasks/migrate/setup_postgresql.rake
+++ b/lib/tasks/migrate/setup_postgresql.rake
@@ -3,10 +3,12 @@ require Rails.root.join('lib/gitlab/database/migration_helpers')
require Rails.root.join('db/migrate/20151007120511_namespaces_projects_path_lower_indexes')
require Rails.root.join('db/migrate/20151008110232_add_users_lower_username_email_indexes')
require Rails.root.join('db/migrate/20161212142807_add_lower_path_index_to_routes')
+require Rails.root.join('db/migrate/20170317203554_index_routes_path_for_like')
desc 'GitLab | Sets up PostgreSQL'
task setup_postgresql: :environment do
NamespacesProjectsPathLowerIndexes.new.up
AddUsersLowerUsernameEmailIndexes.new.up
AddLowerPathIndexToRoutes.new.up
+ IndexRoutesPathForLike.new.up
end
diff --git a/lib/tasks/services.rake b/lib/tasks/services.rake
index 39541c0b9c6..56b81106c5f 100644
--- a/lib/tasks/services.rake
+++ b/lib/tasks/services.rake
@@ -76,23 +76,23 @@ namespace :services do
end
param_hash
- end.sort_by { |p| p[:required] ? 0 : 1 }
+ end
+ service_hash[:params].sort_by! { |p| p[:required] ? 0 : 1 }
- puts "Collected data for: #{service.title}, #{Time.now-service_start}"
+ puts "Collected data for: #{service.title}, #{Time.now - service_start}"
service_hash
end
doc_start = Time.now
doc_path = File.join(Rails.root, 'doc', 'api', 'services.md')
- result = ERB.new(services_template, 0 , '>')
+ result = ERB.new(services_template, 0, '>')
.result(OpenStruct.new(services: services).instance_eval { binding })
File.open(doc_path, 'w') do |f|
f.write result
end
- puts "write a new service.md to: #{doc_path.to_s}, #{Time.now-doc_start}"
-
+ puts "write a new service.md to: #{doc_path}, #{Time.now - doc_start}"
end
end
diff --git a/lib/tasks/sidekiq.rake b/lib/tasks/sidekiq.rake
index d1f6ed87704..dd9ce86f7ca 100644
--- a/lib/tasks/sidekiq.rake
+++ b/lib/tasks/sidekiq.rake
@@ -1,21 +1,21 @@
namespace :sidekiq do
desc "GitLab | Stop sidekiq"
task :stop do
- system *%W(bin/background_jobs stop)
+ system(*%w(bin/background_jobs stop))
end
desc "GitLab | Start sidekiq"
task :start do
- system *%W(bin/background_jobs start)
+ system(*%w(bin/background_jobs start))
end
desc 'GitLab | Restart sidekiq'
task :restart do
- system *%W(bin/background_jobs restart)
+ system(*%w(bin/background_jobs restart))
end
desc "GitLab | Start sidekiq with launchd on Mac OS X"
task :launchd do
- system *%W(bin/background_jobs start_no_deamonize)
+ system(*%w(bin/background_jobs start_no_deamonize))
end
end
diff --git a/lib/tasks/spec.rake b/lib/tasks/spec.rake
index 2cf7a25a0fd..602c60be828 100644
--- a/lib/tasks/spec.rake
+++ b/lib/tasks/spec.rake
@@ -4,8 +4,8 @@ namespace :spec do
desc 'GitLab | Rspec | Run request specs'
task :api do
cmds = [
- %W(rake gitlab:setup),
- %W(rspec spec --tag @api)
+ %w(rake gitlab:setup),
+ %w(rspec spec --tag @api)
]
run_commands(cmds)
end
@@ -13,8 +13,8 @@ namespace :spec do
desc 'GitLab | Rspec | Run feature specs'
task :feature do
cmds = [
- %W(rake gitlab:setup),
- %W(rspec spec --tag @feature)
+ %w(rake gitlab:setup),
+ %w(rspec spec --tag @feature)
]
run_commands(cmds)
end
@@ -22,8 +22,8 @@ namespace :spec do
desc 'GitLab | Rspec | Run model specs'
task :models do
cmds = [
- %W(rake gitlab:setup),
- %W(rspec spec --tag @models)
+ %w(rake gitlab:setup),
+ %w(rspec spec --tag @models)
]
run_commands(cmds)
end
@@ -31,8 +31,8 @@ namespace :spec do
desc 'GitLab | Rspec | Run service specs'
task :services do
cmds = [
- %W(rake gitlab:setup),
- %W(rspec spec --tag @services)
+ %w(rake gitlab:setup),
+ %w(rspec spec --tag @services)
]
run_commands(cmds)
end
@@ -40,8 +40,8 @@ namespace :spec do
desc 'GitLab | Rspec | Run lib specs'
task :lib do
cmds = [
- %W(rake gitlab:setup),
- %W(rspec spec --tag @lib)
+ %w(rake gitlab:setup),
+ %w(rspec spec --tag @lib)
]
run_commands(cmds)
end
@@ -49,8 +49,8 @@ namespace :spec do
desc 'GitLab | Rspec | Run other specs'
task :other do
cmds = [
- %W(rake gitlab:setup),
- %W(rspec spec --tag ~@api --tag ~@feature --tag ~@models --tag ~@lib --tag ~@services)
+ %w(rake gitlab:setup),
+ %w(rspec spec --tag ~@api --tag ~@feature --tag ~@models --tag ~@lib --tag ~@services)
]
run_commands(cmds)
end
@@ -59,14 +59,14 @@ end
desc "GitLab | Run specs"
task :spec do
cmds = [
- %W(rake gitlab:setup),
- %W(rspec spec),
+ %w(rake gitlab:setup),
+ %w(rspec spec),
]
run_commands(cmds)
end
def run_commands(cmds)
cmds.each do |cmd|
- system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) or raise("#{cmd} failed!")
+ system({ 'RAILS_ENV' => 'test', 'force' => 'yes' }, *cmd) || raise("#{cmd} failed!")
end
end
diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake
index 8dbfa7751dc..19ff13f06c0 100644
--- a/lib/tasks/spinach.rake
+++ b/lib/tasks/spinach.rake
@@ -35,7 +35,7 @@ task :spinach do
end
def run_system_command(cmd)
- system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd)
+ system({ 'RAILS_ENV' => 'test', 'force' => 'yes' }, *cmd)
end
def run_spinach_command(args)
diff --git a/lib/tasks/teaspoon.rake b/lib/tasks/teaspoon.rake
deleted file mode 100644
index 08caedd7ff3..00000000000
--- a/lib/tasks/teaspoon.rake
+++ /dev/null
@@ -1,25 +0,0 @@
-unless Rails.env.production?
- Rake::Task['teaspoon'].clear if Rake::Task.task_defined?('teaspoon')
-
- namespace :teaspoon do
- desc 'GitLab | Teaspoon | Generate fixtures for JavaScript tests'
- RSpec::Core::RakeTask.new(:fixtures) do |t|
- ENV['NO_KNAPSACK'] = 'true'
- t.pattern = 'spec/javascripts/fixtures/*.rb'
- t.rspec_opts = '--format documentation'
- end
-
- desc 'GitLab | Teaspoon | Run JavaScript tests'
- task :tests do
- require "teaspoon/console"
- options = {}
- abort('rake teaspoon:tests failed') if Teaspoon::Console.new(options).failures?
- end
- end
-
- desc 'GitLab | Teaspoon | Shortcut for teaspoon:fixtures and teaspoon:tests'
- task :teaspoon do
- Rake::Task['teaspoon:fixtures'].invoke
- Rake::Task['teaspoon:tests'].invoke
- end
-end
diff --git a/lib/tasks/test.rake b/lib/tasks/test.rake
index d3dcbd2c29b..3e01f91d32c 100644
--- a/lib/tasks/test.rake
+++ b/lib/tasks/test.rake
@@ -7,5 +7,5 @@ end
unless Rails.env.production?
desc "GitLab | Run all tests on CI with simplecov"
- task test_ci: [:rubocop, :brakeman, :teaspoon, :spinach, :spec]
+ task test_ci: [:rubocop, :brakeman, :karma, :spinach, :spec]
end
diff --git a/lib/tasks/tokens.rake b/lib/tasks/tokens.rake
new file mode 100644
index 00000000000..95735f43802
--- /dev/null
+++ b/lib/tasks/tokens.rake
@@ -0,0 +1,38 @@
+require_relative '../../app/models/concerns/token_authenticatable.rb'
+
+namespace :tokens do
+ desc "Reset all GitLab user auth tokens"
+ task reset_all_auth: :environment do
+ reset_all_users_token(:reset_authentication_token!)
+ end
+
+ desc "Reset all GitLab email tokens"
+ task reset_all_email: :environment do
+ reset_all_users_token(:reset_incoming_email_token!)
+ end
+
+ def reset_all_users_token(reset_token_method)
+ TmpUser.find_in_batches do |batch|
+ puts "Processing batch starting with user ID: #{batch.first.id}"
+ STDOUT.flush
+
+ batch.each(&reset_token_method)
+ end
+ end
+end
+
+class TmpUser < ActiveRecord::Base
+ include TokenAuthenticatable
+
+ self.table_name = 'users'
+
+ def reset_authentication_token!
+ write_new_token(:authentication_token)
+ save!(validate: false)
+ end
+
+ def reset_incoming_email_token!
+ write_new_token(:incoming_email_token)
+ save!(validate: false)
+ end
+end
diff --git a/lib/tasks/yarn.rake b/lib/tasks/yarn.rake
new file mode 100644
index 00000000000..2ac88a039e7
--- /dev/null
+++ b/lib/tasks/yarn.rake
@@ -0,0 +1,40 @@
+
+namespace :yarn do
+ desc 'Ensure Yarn is installed'
+ task :available do
+ unless system('yarn --version', out: File::NULL)
+ warn(
+ 'Error: Yarn executable was not detected in the system.'.color(:red),
+ 'Download Yarn at https://yarnpkg.com/en/docs/install'.color(:green)
+ )
+ abort
+ end
+ end
+
+ desc 'Ensure Node dependencies are installed'
+ task check: ['yarn:available'] do
+ unless system('yarn check --ignore-engines', out: File::NULL)
+ warn(
+ 'Error: You have unmet dependencies. (`yarn check` command failed)'.color(:red),
+ 'Run `yarn install` to install missing modules.'.color(:green)
+ )
+ abort
+ end
+ end
+
+ desc 'Install Node dependencies with Yarn'
+ task install: ['yarn:available'] do
+ unless system('yarn install --pure-lockfile --ignore-engines')
+ abort 'Error: Unable to install node modules.'.color(:red)
+ end
+ end
+
+ desc 'Remove Node dependencies'
+ task :clobber do
+ warn 'Purging ./node_modules directory'.color(:red)
+ FileUtils.rm_rf 'node_modules'
+ end
+end
+
+desc 'Install Node dependencies with Yarn'
+task yarn: ['yarn:install']