summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.flayignore1
-rw-r--r--.gitlab-ci.yml37
-rw-r--r--.pkgr.yml12
-rw-r--r--.rubocop_todo.yml125
-rw-r--r--CHANGELOG105
-rw-r--r--CONTRIBUTING.md20
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile13
-rw-r--r--Gemfile.lock42
-rw-r--r--README.md3
-rw-r--r--VERSION2
-rw-r--r--app/assets/javascripts/api.js8
-rw-r--r--app/assets/javascripts/application.js1
-rw-r--r--app/assets/javascripts/blob/template_selector.js7
-rw-r--r--app/assets/javascripts/boards/components/board_list.js.es65
-rw-r--r--app/assets/javascripts/build.js13
-rw-r--r--app/assets/javascripts/cycle-analytics.js.es693
-rw-r--r--app/assets/javascripts/dispatcher.js8
-rw-r--r--app/assets/javascripts/gl_dropdown.js45
-rw-r--r--app/assets/javascripts/importer_status.js16
-rw-r--r--app/assets/javascripts/issuable.js.es641
-rw-r--r--app/assets/javascripts/labels_select.js2
-rw-r--r--app/assets/javascripts/layout_nav.js25
-rw-r--r--app/assets/javascripts/merge_request_tabs.js4
-rw-r--r--app/assets/javascripts/project_find_file.js24
-rw-r--r--app/assets/javascripts/protected_branch_edit.js.es61
-rw-r--r--app/assets/javascripts/shortcuts_navigation.js3
-rw-r--r--app/assets/javascripts/users/calendar.js2
-rw-r--r--app/assets/stylesheets/framework/blocks.scss11
-rw-r--r--app/assets/stylesheets/framework/flash.scss2
-rw-r--r--app/assets/stylesheets/framework/lists.scss4
-rw-r--r--app/assets/stylesheets/framework/nav.scss3
-rw-r--r--app/assets/stylesheets/framework/typography.scss4
-rw-r--r--app/assets/stylesheets/framework/variables.scss8
-rw-r--r--app/assets/stylesheets/pages/boards.scss98
-rw-r--r--app/assets/stylesheets/pages/builds.scss4
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss144
-rw-r--r--app/assets/stylesheets/pages/diff.scss9
-rw-r--r--app/assets/stylesheets/pages/groups.scss13
-rw-r--r--app/assets/stylesheets/pages/issues.scss4
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss41
-rw-r--r--app/assets/stylesheets/pages/milestone.scss27
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss110
-rw-r--r--app/assets/stylesheets/pages/profile.scss41
-rw-r--r--app/assets/stylesheets/pages/projects.scss14
-rw-r--r--app/assets/stylesheets/pages/snippets.scss42
-rw-r--r--app/assets/stylesheets/pages/tree.scss9
-rw-r--r--app/controllers/admin/groups_controller.rb10
-rw-r--r--app/controllers/concerns/toggle_award_emoji.rb8
-rw-r--r--app/controllers/groups_controller.rb12
-rw-r--r--app/controllers/import/github_controller.rb7
-rw-r--r--app/controllers/jwt_controller.rb36
-rw-r--r--app/controllers/projects/builds_controller.rb8
-rw-r--r--app/controllers/projects/cycle_analytics_controller.rb67
-rw-r--r--app/controllers/projects/git_http_client_controller.rb66
-rw-r--r--app/controllers/projects/git_http_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb11
-rw-r--r--app/controllers/projects/merge_requests_controller.rb14
-rw-r--r--app/controllers/projects/snippets_controller.rb5
-rw-r--r--app/controllers/search_controller.rb4
-rw-r--r--app/controllers/sent_notifications_controller.rb7
-rw-r--r--app/controllers/snippets_controller.rb3
-rw-r--r--app/controllers/users_controller.rb2
-rw-r--r--app/finders/issuable_finder.rb9
-rw-r--r--app/finders/issues_finder.rb4
-rw-r--r--app/finders/merge_requests_finder.rb10
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/helpers/award_emoji_helper.rb9
-rw-r--r--app/helpers/gitlab_routing_helper.rb16
-rw-r--r--app/helpers/groups_helper.rb25
-rw-r--r--app/helpers/lfs_helper.rb12
-rw-r--r--app/helpers/milestones_helper.rb24
-rw-r--r--app/helpers/namespaces_helper.rb5
-rw-r--r--app/helpers/notes_helper.rb4
-rw-r--r--app/helpers/projects_helper.rb2
-rw-r--r--app/helpers/search_helper.rb31
-rw-r--r--app/helpers/snippets_helper.rb6
-rw-r--r--app/mailers/notify.rb6
-rw-r--r--app/models/blob.rb12
-rw-r--r--app/models/ci/build.rb46
-rw-r--r--app/models/ci/pipeline.rb48
-rw-r--r--app/models/ci/variable.rb6
-rw-r--r--app/models/commit_status.rb14
-rw-r--r--app/models/concerns/awardable.rb6
-rw-r--r--app/models/concerns/has_status.rb9
-rw-r--r--app/models/concerns/issuable.rb13
-rw-r--r--app/models/cycle_analytics.rb97
-rw-r--r--app/models/cycle_analytics/summary.rb24
-rw-r--r--app/models/deployment.rb34
-rw-r--r--app/models/environment.rb16
-rw-r--r--app/models/event.rb27
-rw-r--r--app/models/global_milestone.rb13
-rw-r--r--app/models/group.rb7
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/issue/metrics.rb21
-rw-r--r--app/models/member.rb33
-rw-r--r--app/models/merge_request.rb50
-rw-r--r--app/models/merge_request/metrics.rb11
-rw-r--r--app/models/merge_request_diff.rb14
-rw-r--r--app/models/merge_requests_closing_issues.rb7
-rw-r--r--app/models/namespace.rb5
-rw-r--r--app/models/note.rb4
-rw-r--r--app/models/project.rb27
-rw-r--r--app/models/project_feature.rb6
-rw-r--r--app/models/project_team.rb72
-rw-r--r--app/models/repository.rb143
-rw-r--r--app/models/snippet.rb1
-rw-r--r--app/policies/project_policy.rb19
-rw-r--r--app/services/akismet_service.rb43
-rw-r--r--app/services/auth/container_registry_authentication_service.rb32
-rw-r--r--app/services/commits/change_service.rb20
-rw-r--r--app/services/commits/cherry_pick_service.rb14
-rw-r--r--app/services/commits/revert_service.rb14
-rw-r--r--app/services/create_deployment_service.rb44
-rw-r--r--app/services/files/base_service.rb2
-rw-r--r--app/services/files/create_dir_service.rb2
-rw-r--r--app/services/files/create_service.rb2
-rw-r--r--app/services/files/delete_service.rb2
-rw-r--r--app/services/files/update_service.rb4
-rw-r--r--app/services/git_push_service.rb10
-rw-r--r--app/services/issuable_base_service.rb5
-rw-r--r--app/services/merge_requests/create_service.rb1
-rw-r--r--app/services/merge_requests/refresh_service.rb9
-rw-r--r--app/services/merge_requests/update_service.rb4
-rw-r--r--app/services/milestones/create_service.rb2
-rw-r--r--app/services/notes/slash_commands_service.rb21
-rw-r--r--app/views/admin/application_settings/_form.html.haml54
-rw-r--r--app/views/admin/groups/_form.html.haml2
-rw-r--r--app/views/admin/groups/show.html.haml6
-rw-r--r--app/views/award_emoji/_awards_block.html.haml2
-rw-r--r--app/views/discussions/_jump_to_next.html.haml2
-rw-r--r--app/views/discussions/_notes.html.haml3
-rw-r--r--app/views/groups/_group_lfs_settings.html.haml11
-rw-r--r--app/views/groups/edit.html.haml2
-rw-r--r--app/views/groups/show.html.haml2
-rw-r--r--app/views/help/_shortcuts.html.haml6
-rw-r--r--app/views/import/github/status.html.haml12
-rw-r--r--app/views/layouts/nav/_project.html.haml6
-rw-r--r--app/views/layouts/notify.html.haml4
-rw-r--r--app/views/layouts/project.html.haml3
-rw-r--r--app/views/profiles/update_username.js.haml2
-rw-r--r--app/views/projects/_zen.html.haml3
-rw-r--r--app/views/projects/builds/_sidebar.html.haml4
-rw-r--r--app/views/projects/builds/_table.html.haml2
-rw-r--r--app/views/projects/buttons/_download.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build_pipeline.html.haml25
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml12
-rw-r--r--app/views/projects/commit/_pipeline.html.haml3
-rw-r--r--app/views/projects/commit/_pipeline_stage.html.haml14
-rw-r--r--app/views/projects/commit/_pipeline_status_group.html.haml11
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml59
-rw-r--r--app/views/projects/diffs/_file.html.haml2
-rw-r--r--app/views/projects/edit.html.haml17
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml17
-rw-r--r--app/views/projects/issues/_issue.html.haml2
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_versions.html.haml112
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml25
-rw-r--r--app/views/projects/notes/_form.html.haml6
-rw-r--r--app/views/projects/notes/_notes_with_form.html.haml4
-rw-r--r--app/views/projects/pipelines/_head.html.haml6
-rw-r--r--app/views/projects/pipelines/index.html.haml8
-rw-r--r--app/views/projects/protected_branches/_protected_branch.html.haml2
-rw-r--r--app/views/projects/snippets/_actions.html.haml14
-rw-r--r--app/views/projects/snippets/show.html.haml12
-rw-r--r--app/views/projects/tree/_readme.html.haml2
-rw-r--r--app/views/projects/variables/_table.html.haml2
-rw-r--r--app/views/search/_results.html.haml16
-rw-r--r--app/views/search/results/_blob.html.haml2
-rw-r--r--app/views/search/results/_commit.html.haml3
-rw-r--r--app/views/search/results/_wiki_blob.html.haml2
-rw-r--r--app/views/sent_notifications/unsubscribe.html.haml19
-rw-r--r--app/views/shared/_milestones_filter.html.haml15
-rw-r--r--app/views/shared/_visibility_level.html.haml2
-rw-r--r--app/views/shared/icons/_icon_cycle_analytics_splash.svg1
-rw-r--r--app/views/shared/icons/_icon_fork.svg4
-rw-r--r--app/views/shared/issuable/_filter.html.haml8
-rw-r--r--app/views/shared/issuable/_form.html.haml2
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_search_form.html.haml4
-rw-r--r--app/views/shared/milestones/_milestone.html.haml2
-rw-r--r--app/views/shared/snippets/_header.html.haml8
-rw-r--r--app/views/shared/snippets/_snippet.html.haml21
-rw-r--r--app/views/snippets/_actions.html.haml14
-rw-r--r--app/views/snippets/show.html.haml21
-rw-r--r--app/views/users/calendar.html.haml4
-rw-r--r--app/views/users/show.html.haml94
-rw-r--r--config/routes.rb20
-rw-r--r--db/fixtures/development/04_project.rb11
-rw-r--r--db/fixtures/development/14_pipelines.rb10
-rw-r--r--db/fixtures/development/17_cycle_analytics.rb246
-rw-r--r--db/migrate/20140502125220_migrate_repo_size.rb5
-rw-r--r--db/migrate/20160725104020_merge_request_diff_remove_uniq.rb20
-rw-r--r--db/migrate/20160808085531_add_token_to_build.rb10
-rw-r--r--db/migrate/20160808085602_add_index_for_build_token.rb12
-rw-r--r--db/migrate/20160824124900_add_table_issue_metrics.rb37
-rw-r--r--db/migrate/20160825052008_add_table_merge_request_metrics.rb38
-rw-r--r--db/migrate/20160901213340_add_lfs_enabled_to_namespaces.rb12
-rw-r--r--db/migrate/20160907131111_add_environment_type_to_environments.rb9
-rw-r--r--db/migrate/20160913162434_remove_projects_pushes_since_gc.rb2
-rw-r--r--db/migrate/20160913212128_change_artifacts_size_column.rb15
-rw-r--r--db/migrate/20160915042921_create_merge_requests_closing_issues.rb34
-rw-r--r--db/migrate/20160920160832_add_index_to_labels_title.rb11
-rw-r--r--db/schema.rb53
-rw-r--r--doc/README.md4
-rw-r--r--doc/administration/issue_closing_pattern.md49
-rw-r--r--doc/api/README.md17
-rw-r--r--doc/api/award_emoji.md15
-rw-r--r--doc/api/groups.md18
-rw-r--r--doc/api/notes.md6
-rw-r--r--doc/api/projects.md24
-rw-r--r--doc/api/repository_files.md12
-rw-r--r--doc/api/settings.md2
-rw-r--r--doc/ci/README.md2
-rw-r--r--doc/ci/examples/README.md2
-rw-r--r--doc/ci/quick_start/README.md3
-rw-r--r--doc/ci/triggers/README.md4
-rw-r--r--doc/ci/yaml/README.md79
-rw-r--r--doc/container_registry/README.md6
-rw-r--r--doc/customization/issue_closing.md41
-rw-r--r--doc/development/migration_style_guide.md22
-rw-r--r--doc/gitlab-basics/create-issue.md4
-rw-r--r--doc/install/installation.md2
-rw-r--r--doc/intro/README.md2
-rw-r--r--doc/raketasks/backup_restore.md6
-rw-r--r--doc/update/8.11-to-8.12.md4
-rw-r--r--doc/user/permissions.md32
-rw-r--r--doc/user/project/builds/artifacts.md32
-rw-r--r--doc/user/project/builds/img/build_latest_artifacts_browser.pngbin0 -> 26617 bytes
-rw-r--r--doc/user/project/cycle_analytics.md114
-rw-r--r--doc/user/project/img/cycle_analytics_landing_page.pngbin0 -> 58203 bytes
-rw-r--r--doc/user/project/issues/automatic_issue_closing.md55
-rw-r--r--doc/user/project/merge_requests.md3
-rw-r--r--doc/user/project/merge_requests/img/versions-compare.pngbin0 -> 68722 bytes
-rw-r--r--doc/user/project/merge_requests/img/versions-dropdown.pngbin0 -> 60587 bytes
-rw-r--r--doc/user/project/merge_requests/img/versions.pngbin35001 -> 171413 bytes
-rw-r--r--doc/user/project/merge_requests/versions.md6
-rw-r--r--doc/user/project/new_ci_build_permissions_model.md289
-rw-r--r--doc/user/project/repository/web_editor.md2
-rw-r--r--doc/user/project/settings/import_export.md18
-rw-r--r--doc/workflow/README.md2
-rw-r--r--doc/workflow/importing/img/import_projects_from_github_importer.pngbin22711 -> 65288 bytes
-rw-r--r--doc/workflow/importing/img/import_projects_from_github_new_project_page.pngbin13668 -> 24911 bytes
-rw-r--r--doc/workflow/importing/img/import_projects_from_github_select_auth_method.pngbin0 -> 42043 bytes
-rw-r--r--doc/workflow/importing/import_projects_from_github.md130
-rw-r--r--doc/workflow/lfs/lfs_administration.md4
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md8
-rw-r--r--features/steps/project/issues/issues.rb2
-rw-r--r--features/steps/project/merge_requests.rb2
-rw-r--r--lib/api/access_requests.rb2
-rw-r--r--lib/api/api_guard.rb56
-rw-r--r--lib/api/award_emoji.rb29
-rw-r--r--lib/api/entities.rb12
-rw-r--r--lib/api/files.rb12
-rw-r--r--lib/api/groups.rb24
-rw-r--r--lib/api/helpers.rb23
-rw-r--r--lib/api/internal.rb25
-rw-r--r--lib/api/members.rb8
-rw-r--r--lib/api/notes.rb8
-rw-r--r--lib/api/projects.rb93
-rw-r--r--lib/banzai/filter/sanitization_filter.rb64
-rw-r--r--lib/banzai/filter/wiki_link_filter/rewriter.rb1
-rw-r--r--lib/ci/api/entities.rb9
-rw-r--r--lib/ci/api/helpers.rb14
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb3
-rw-r--r--lib/ci/mask_secret.rb10
-rw-r--r--lib/expand_variables.rb17
-rw-r--r--lib/gitlab/auth.rb123
-rw-r--r--lib/gitlab/auth/result.rb21
-rw-r--r--lib/gitlab/backend/shell.rb13
-rw-r--r--lib/gitlab/ci/config/node/environment.rb68
-rw-r--r--lib/gitlab/ci/config/node/job.rb41
-rw-r--r--lib/gitlab/contributions_calendar.rb17
-rw-r--r--lib/gitlab/current_settings.rb4
-rw-r--r--lib/gitlab/database/date_time.rb27
-rw-r--r--lib/gitlab/database/median.rb112
-rw-r--r--lib/gitlab/database/migration_helpers.rb10
-rw-r--r--lib/gitlab/git.rb10
-rw-r--r--lib/gitlab/git/hook.rb12
-rw-r--r--lib/gitlab/git_access.rb19
-rw-r--r--lib/gitlab/github_import/project_creator.rb7
-rw-r--r--lib/gitlab/gon_helper.rb1
-rw-r--r--lib/gitlab/import_export.rb3
-rw-r--r--lib/gitlab/import_export/import_export.yml7
-rw-r--r--lib/gitlab/import_export/relation_factory.rb28
-rw-r--r--lib/gitlab/import_export/repo_restorer.rb4
-rw-r--r--lib/gitlab/import_export/version_checker.rb4
-rw-r--r--lib/gitlab/ldap/adapter.rb6
-rw-r--r--lib/gitlab/lfs_token.rb54
-rw-r--r--lib/gitlab/redis.rb31
-rw-r--r--lib/gitlab/regex.rb10
-rwxr-xr-xscripts/lint-doc.sh9
-rw-r--r--spec/controllers/import/github_controller_spec.rb46
-rw-r--r--spec/controllers/sent_notifications_controller_spec.rb109
-rw-r--r--spec/controllers/snippets_controller_spec.rb33
-rw-r--r--spec/factories/ci/runner_projects.rb11
-rw-r--r--spec/factories/ci/runners.rb19
-rw-r--r--spec/factories/ci/variables.rb14
-rw-r--r--spec/factories/deployments.rb3
-rw-r--r--spec/factories/events.rb5
-rw-r--r--spec/factories/group_members.rb19
-rw-r--r--spec/factories/milestones.rb7
-rw-r--r--spec/features/boards/boards_spec.rb202
-rw-r--r--spec/features/boards/keyboard_shortcut_spec.rb24
-rw-r--r--spec/features/calendar_spec.rb39
-rw-r--r--spec/features/environments_spec.rb2
-rw-r--r--spec/features/issues/filter_issues_spec.rb43
-rw-r--r--spec/features/issues/reset_filters_spec.rb4
-rw-r--r--spec/features/issues_spec.rb2
-rw-r--r--spec/features/merge_requests/merge_request_versions_spec.rb14
-rw-r--r--spec/features/milestone_spec.rb21
-rw-r--r--spec/features/projects/branches/download_buttons_spec.rb6
-rw-r--r--spec/features/projects/files/download_buttons_spec.rb6
-rw-r--r--spec/features/projects/gfm_autocomplete_load_spec.rb21
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb80
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb2
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin676870 -> 1363770 bytes
-rw-r--r--spec/features/projects/issuable_templates_spec.rb33
-rw-r--r--spec/features/projects/main/download_buttons_spec.rb6
-rw-r--r--spec/features/projects/tags/download_buttons_spec.rb6
-rw-r--r--spec/features/projects_spec.rb8
-rw-r--r--spec/features/search_spec.rb32
-rw-r--r--spec/features/todos/todos_filtering_spec.rb20
-rw-r--r--spec/features/unsubscribe_links_spec.rb75
-rw-r--r--spec/features/users/snippets_spec.rb5
-rw-r--r--spec/finders/issues_finder_spec.rb20
-rw-r--r--spec/helpers/groups_helper_spec.rb63
-rw-r--r--spec/helpers/milestones_helper_spec.rb33
-rw-r--r--spec/helpers/search_helper_spec.rb32
-rw-r--r--spec/lib/banzai/pipeline/wiki_pipeline_spec.rb7
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb21
-rw-r--r--spec/lib/ci/mask_secret_spec.rb27
-rw-r--r--spec/lib/expand_variables_spec.rb73
-rw-r--r--spec/lib/gitlab/auth_spec.rb96
-rw-r--r--spec/lib/gitlab/backend/shell_spec.rb32
-rw-r--r--spec/lib/gitlab/ci/config/node/environment_spec.rb155
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb99
-rw-r--r--spec/lib/gitlab/git_access_spec.rb116
-rw-r--r--spec/lib/gitlab/git_access_wiki_spec.rb9
-rw-r--r--spec/lib/gitlab/git_spec.rb45
-rw-r--r--spec/lib/gitlab/github_import/project_creator_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml187
-rw-r--r--spec/lib/gitlab/import_export/attribute_configuration_spec.rb55
-rw-r--r--spec/lib/gitlab/import_export/model_configuration_spec.rb57
-rw-r--r--spec/lib/gitlab/import_export/project.json80
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb36
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml330
-rw-r--r--spec/lib/gitlab/import_export/version_checker_spec.rb2
-rw-r--r--spec/lib/gitlab/lfs_token_spec.rb51
-rw-r--r--spec/lib/gitlab/redis_spec.rb52
-rw-r--r--spec/mailers/notify_spec.rb10
-rw-r--r--spec/mailers/shared/notify.rb8
-rw-r--r--spec/models/blob_spec.rb20
-rw-r--r--spec/models/build_spec.rb78
-rw-r--r--spec/models/ci/build_spec.rb2
-rw-r--r--spec/models/ci/pipeline_spec.rb83
-rw-r--r--spec/models/commit_status_spec.rb43
-rw-r--r--spec/models/cycle_analytics/code_spec.rb42
-rw-r--r--spec/models/cycle_analytics/issue_spec.rb50
-rw-r--r--spec/models/cycle_analytics/plan_spec.rb52
-rw-r--r--spec/models/cycle_analytics/production_spec.rb54
-rw-r--r--spec/models/cycle_analytics/review_spec.rb35
-rw-r--r--spec/models/cycle_analytics/staging_spec.rb64
-rw-r--r--spec/models/cycle_analytics/summary_spec.rb53
-rw-r--r--spec/models/cycle_analytics/test_spec.rb94
-rw-r--r--spec/models/environment_spec.rb16
-rw-r--r--spec/models/event_spec.rb43
-rw-r--r--spec/models/global_milestone_spec.rb5
-rw-r--r--spec/models/group_spec.rb46
-rw-r--r--spec/models/hooks/project_hook_spec.rb18
-rw-r--r--spec/models/hooks/service_hook_spec.rb18
-rw-r--r--spec/models/hooks/system_hook_spec.rb20
-rw-r--r--spec/models/hooks/web_hook_spec.rb18
-rw-r--r--spec/models/issue/metrics_spec.rb55
-rw-r--r--spec/models/member_spec.rb42
-rw-r--r--spec/models/members/group_member_spec.rb19
-rw-r--r--spec/models/members/project_member_spec.rb19
-rw-r--r--spec/models/merge_request/metrics_spec.rb18
-rw-r--r--spec/models/merge_request_diff_spec.rb22
-rw-r--r--spec/models/merge_request_spec.rb139
-rw-r--r--spec/models/project_services/asana_service_spec.rb20
-rw-r--r--spec/models/project_services/assembla_service_spec.rb20
-rw-r--r--spec/models/project_services/bamboo_service_spec.rb20
-rw-r--r--spec/models/project_services/bugzilla_service_spec.rb20
-rw-r--r--spec/models/project_services/buildkite_service_spec.rb20
-rw-r--r--spec/models/project_services/campfire_service_spec.rb20
-rw-r--r--spec/models/project_services/custom_issue_tracker_service_spec.rb20
-rw-r--r--spec/models/project_services/drone_ci_service_spec.rb20
-rw-r--r--spec/models/project_services/external_wiki_service_spec.rb21
-rw-r--r--spec/models/project_services/flowdock_service_spec.rb20
-rw-r--r--spec/models/project_services/gemnasium_service_spec.rb20
-rw-r--r--spec/models/project_services/gitlab_issue_tracker_service_spec.rb20
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb20
-rw-r--r--spec/models/project_services/irker_service_spec.rb20
-rw-r--r--spec/models/project_services/jira_service_spec.rb20
-rw-r--r--spec/models/project_services/pivotaltracker_service_spec.rb20
-rw-r--r--spec/models/project_services/pushover_service_spec.rb20
-rw-r--r--spec/models/project_services/redmine_service_spec.rb20
-rw-r--r--spec/models/project_services/slack_service_spec.rb20
-rw-r--r--spec/models/project_services/teamcity_service_spec.rb20
-rw-r--r--spec/models/project_spec.rb154
-rw-r--r--spec/models/project_team_spec.rb62
-rw-r--r--spec/models/repository_spec.rb162
-rw-r--r--spec/models/snippet_spec.rb2
-rw-r--r--spec/policies/project_policy_spec.rb13
-rw-r--r--spec/requests/api/api_helpers_spec.rb25
-rw-r--r--spec/requests/api/award_emoji_spec.rb54
-rw-r--r--spec/requests/api/commits_spec.rb2
-rw-r--r--spec/requests/api/files_spec.rb75
-rw-r--r--spec/requests/api/fork_spec.rb4
-rw-r--r--spec/requests/api/groups_spec.rb11
-rw-r--r--spec/requests/api/internal_spec.rb46
-rw-r--r--spec/requests/api/members_spec.rb27
-rw-r--r--spec/requests/api/milestones_spec.rb23
-rw-r--r--spec/requests/api/notes_spec.rb9
-rw-r--r--spec/requests/api/projects_spec.rb18
-rw-r--r--spec/requests/ci/api/builds_spec.rb86
-rw-r--r--spec/requests/git_http_spec.rb74
-rw-r--r--spec/requests/jwt_controller_spec.rb30
-rw-r--r--spec/requests/lfs_http_spec.rb249
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb64
-rw-r--r--spec/services/create_deployment_service_spec.rb122
-rw-r--r--spec/services/git_push_service_spec.rb52
-rw-r--r--spec/services/merge_requests/create_service_spec.rb29
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb52
-rw-r--r--spec/services/merge_requests/update_service_spec.rb37
-rw-r--r--spec/services/notes/slash_commands_service_spec.rb69
-rw-r--r--spec/services/notification_service_spec.rb2
-rw-r--r--spec/services/projects/housekeeping_service_spec.rb4
-rw-r--r--spec/services/protected_branches/create_service_spec.rb23
-rw-r--r--spec/support/cycle_analytics_helpers.rb64
-rw-r--r--spec/support/cycle_analytics_helpers/test_generation.rb161
-rw-r--r--spec/support/git_helpers.rb9
-rw-r--r--spec/support/import_export/configuration_helper.rb29
-rw-r--r--spec/support/import_export/export_file_helper.rb133
-rw-r--r--spec/support/wait_for_vue_resource.rb7
-rw-r--r--spec/views/projects/merge_requests/_heading.html.haml_spec.rb2
-rw-r--r--spec/views/projects/notes/_form.html.haml_spec.rb36
-rw-r--r--spec/views/projects/pipelines/show.html.haml_spec.rb53
440 files changed, 9825 insertions, 2478 deletions
diff --git a/.flayignore b/.flayignore
index 9c9875d4f9e..f120de527bd 100644
--- a/.flayignore
+++ b/.flayignore
@@ -1 +1,2 @@
*.erb
+lib/gitlab/sanitizers/svg/whitelist.rb
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b5eef681d0e..3f3873e57c1 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -12,7 +12,7 @@ variables:
RSPEC_RETRY_RETRY_COUNT: "3"
RAILS_ENV: "test"
SIMPLECOV: "true"
- USE_DB: "true"
+ SETUP_DB: "true"
USE_BUNDLE_INSTALL: "true"
GIT_DEPTH: "20"
PHANTOMJS_VERSION: "2.1.1"
@@ -23,7 +23,7 @@ before_script:
- bundle --version
- '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"'
- retry gem install knapsack
- - '[ "$USE_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate'
+ - '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate'
stages:
- prepare
@@ -35,7 +35,7 @@ stages:
.knapsack-state: &knapsack-state
services: []
variables:
- USE_DB: "false"
+ SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
cache:
key: "knapsack"
@@ -196,7 +196,7 @@ spinach 9 10 ruby21: *spinach-knapsack-ruby21
.ruby-static-analysis: &ruby-static-analysis
variables:
SIMPLECOV: "false"
- USE_DB: "false"
+ SETUP_DB: "false"
USE_BUNDLE_INSTALL: "true"
.exec: &exec
@@ -209,8 +209,12 @@ rubocop: *exec
rake haml_lint: *exec
rake scss_lint: *exec
rake brakeman: *exec
-rake flog: *exec
-rake flay: *exec
+rake flog:
+ <<: *exec
+ allow_failure: yes
+rake flay:
+ <<: *exec
+ allow_failure: yes
license_finder: *exec
rake downtime_check: *exec
@@ -220,6 +224,23 @@ rake db:migrate:reset:
script:
- rake db:migrate:reset
+rake db:seed_fu:
+ stage: test
+ <<: *use-db
+ variables:
+ SIZE: "1"
+ SETUP_DB: "false"
+ RAILS_ENV: "development"
+ script:
+ - git clone https://gitlab.com/gitlab-org/gitlab-test.git
+ /home/git/repositories/gitlab-org/gitlab-test.git
+ - bundle exec rake db:setup db:seed_fu
+ artifacts:
+ when: on_failure
+ expire_in: 1d
+ paths:
+ - log/development.log
+
teaspoon:
stage: test
<<: *use-db
@@ -268,7 +289,7 @@ coverage:
stage: post-test
services: []
variables:
- USE_DB: "false"
+ SETUP_DB: "false"
USE_BUNDLE_INSTALL: "true"
script:
- bundle exec scripts/merge-simplecov
@@ -284,7 +305,7 @@ coverage:
notify:slack:
stage: post-test
variables:
- USE_DB: "false"
+ SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
script:
- ./scripts/notify_slack.sh "#builds" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/builds>"
diff --git a/.pkgr.yml b/.pkgr.yml
index 8fc9fddf8f7..10bcd7bd4bd 100644
--- a/.pkgr.yml
+++ b/.pkgr.yml
@@ -3,6 +3,8 @@ group: git
services:
- postgres
before_precompile: ./bin/pkgr_before_precompile.sh
+env:
+ - SKIP_STORAGE_VALIDATION=true
targets:
debian-7: &wheezy
build_dependencies:
@@ -25,6 +27,16 @@ targets:
- libicu52
- libpcre3
- git
+ ubuntu-16.04:
+ build_dependencies:
+ - libkrb5-dev
+ - libicu-dev
+ - cmake
+ - pkg-config
+ dependencies:
+ - libicu55
+ - libpcre3
+ - git
centos-6:
build_dependencies:
- krb5-devel
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 20daf1619a7..87520c67dd5 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -1,21 +1,21 @@
# This configuration was generated by
# `rubocop --auto-gen-config --exclude-limit 0`
-# on 2016-07-13 12:36:08 -0600 using RuboCop version 0.41.2.
+# on 2016-09-14 15:44:53 -0400 using RuboCop version 0.42.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
-# Offense count: 154
+# Offense count: 158
Lint/AmbiguousRegexpLiteral:
Enabled: false
-# Offense count: 43
+# Offense count: 41
# Configuration parameters: AllowSafeAssignment.
Lint/AssignmentInCondition:
Enabled: false
-# Offense count: 14
+# Offense count: 16
Lint/HandleExceptions:
Enabled: false
@@ -23,28 +23,28 @@ Lint/HandleExceptions:
Lint/Loop:
Enabled: false
-# Offense count: 15
+# Offense count: 16
Lint/ShadowingOuterLocalVariable:
Enabled: false
-# Offense count: 3
+# Offense count: 6
# Cop supports --auto-correct.
Lint/StringConversionInInterpolation:
Enabled: false
-# Offense count: 44
+# Offense count: 49
# Cop supports --auto-correct.
# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
Lint/UnusedBlockArgument:
Enabled: false
-# Offense count: 129
+# Offense count: 144
# Cop supports --auto-correct.
# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods.
Lint/UnusedMethodArgument:
Enabled: false
-# Offense count: 12
+# Offense count: 9
# Cop supports --auto-correct.
Performance/PushSplat:
Enabled: false
@@ -59,51 +59,51 @@ Performance/RedundantBlockCall:
Performance/RedundantMatch:
Enabled: false
-# Offense count: 24
+# Offense count: 27
# Cop supports --auto-correct.
# Configuration parameters: MaxKeyValuePairs.
Performance/RedundantMerge:
Enabled: false
-# Offense count: 60
+# Offense count: 61
Rails/OutputSafety:
Enabled: false
-# Offense count: 128
+# Offense count: 129
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: strict, flexible
Rails/TimeZone:
Enabled: false
-# Offense count: 12
+# Offense count: 15
# Cop supports --auto-correct.
# Configuration parameters: Include.
# Include: app/models/**/*.rb
Rails/Validation:
Enabled: false
-# Offense count: 217
+# Offense count: 273
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
# SupportedStyles: with_first_parameter, with_fixed_indentation
Style/AlignParameters:
Enabled: false
-# Offense count: 32
+# Offense count: 30
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: always, conditionals
Style/AndOr:
Enabled: false
-# Offense count: 47
+# Offense count: 50
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: percent_q, bare_percent
Style/BarePercentLiterals:
Enabled: false
-# Offense count: 258
+# Offense count: 289
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: braces, no_braces, context_dependent
@@ -126,14 +126,14 @@ Style/ColonMethodCall:
Style/CommentAnnotation:
Enabled: false
-# Offense count: 34
+# Offense count: 33
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly.
# SupportedStyles: assign_to_condition, assign_inside_condition
Style/ConditionalAssignment:
Enabled: false
-# Offense count: 789
+# Offense count: 881
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: leading, trailing
@@ -144,11 +144,12 @@ Style/DotPosition:
Style/DoubleNegation:
Enabled: false
-# Offense count: 3
+# Offense count: 4
+# Cop supports --auto-correct.
Style/EachWithObject:
Enabled: false
-# Offense count: 30
+# Offense count: 25
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: empty, nil, both
@@ -160,7 +161,7 @@ Style/EmptyElse:
Style/EmptyLiteral:
Enabled: false
-# Offense count: 123
+# Offense count: 135
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment, ForceEqualSignAlignment.
Style/ExtraSpacing:
@@ -172,16 +173,16 @@ Style/ExtraSpacing:
Style/FormatString:
Enabled: false
-# Offense count: 48
+# Offense count: 51
# Configuration parameters: MinBodyLength.
Style/GuardClause:
Enabled: false
-# Offense count: 11
+# Offense count: 9
Style/IfInsideElse:
Enabled: false
-# Offense count: 177
+# Offense count: 174
# Cop supports --auto-correct.
# Configuration parameters: MaxLineLength.
Style/IfUnlessModifier:
@@ -194,7 +195,7 @@ Style/IfUnlessModifier:
Style/IndentArray:
Enabled: false
-# Offense count: 89
+# Offense count: 97
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
# SupportedStyles: special_inside_parentheses, consistent, align_braces
@@ -208,7 +209,7 @@ Style/IndentHash:
Style/Lambda:
Enabled: false
-# Offense count: 6
+# Offense count: 5
# Cop supports --auto-correct.
Style/LineEndConcatenation:
Enabled: false
@@ -218,17 +219,21 @@ Style/LineEndConcatenation:
Style/MethodCallParentheses:
Enabled: false
-# Offense count: 62
+# Offense count: 8
+Style/MethodMissing:
+ Enabled: false
+
+# Offense count: 85
# Cop supports --auto-correct.
Style/MutableConstant:
Enabled: false
-# Offense count: 10
+# Offense count: 8
# Cop supports --auto-correct.
Style/NestedParenthesizedCalls:
Enabled: false
-# Offense count: 12
+# Offense count: 13
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles.
# SupportedStyles: skip_modifier_ifs, always
@@ -242,12 +247,19 @@ Style/Next:
Style/NumericLiteralPrefix:
Enabled: false
+# Offense count: 64
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: predicate, comparison
+Style/NumericPredicate:
+ Enabled: false
+
# Offense count: 29
# Cop supports --auto-correct.
Style/ParallelAssignment:
Enabled: false
-# Offense count: 208
+# Offense count: 264
# Cop supports --auto-correct.
# Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters:
@@ -265,7 +277,7 @@ Style/PercentQLiterals:
Style/PerlBackrefs:
Enabled: false
-# Offense count: 32
+# Offense count: 35
# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist.
# NamePrefix: is_, has_, have_
# NamePrefixBlacklist: is_, has_, have_
@@ -273,7 +285,7 @@ Style/PerlBackrefs:
Style/PredicateName:
Enabled: false
-# Offense count: 28
+# Offense count: 27
# Cop supports --auto-correct.
Style/PreferredHashMethods:
Enabled: false
@@ -283,14 +295,14 @@ Style/PreferredHashMethods:
Style/Proc:
Enabled: false
-# Offense count: 20
+# Offense count: 22
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: compact, exploded
Style/RaiseArgs:
Enabled: false
-# Offense count: 3
+# Offense count: 4
# Cop supports --auto-correct.
Style/RedundantBegin:
Enabled: false
@@ -300,29 +312,29 @@ Style/RedundantBegin:
Style/RedundantException:
Enabled: false
-# Offense count: 23
+# Offense count: 24
# Cop supports --auto-correct.
Style/RedundantFreeze:
Enabled: false
-# Offense count: 377
+# Offense count: 408
# Cop supports --auto-correct.
Style/RedundantSelf:
Enabled: false
-# Offense count: 94
+# Offense count: 93
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes.
# SupportedStyles: slashes, percent_r, mixed
Style/RegexpLiteral:
Enabled: false
-# Offense count: 17
+# Offense count: 18
# Cop supports --auto-correct.
Style/RescueModifier:
Enabled: false
-# Offense count: 2
+# Offense count: 5
# Cop supports --auto-correct.
Style/SelfAssignment:
Enabled: false
@@ -339,42 +351,42 @@ Style/SingleLineBlockParams:
Style/SingleLineMethods:
Enabled: false
-# Offense count: 119
+# Offense count: 124
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: space, no_space
Style/SpaceBeforeBlockBraces:
Enabled: false
-# Offense count: 11
+# Offense count: 10
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment.
Style/SpaceBeforeFirstArg:
Enabled: false
-# Offense count: 130
+# Offense count: 141
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
# SupportedStyles: space, no_space
Style/SpaceInsideBlockBraces:
Enabled: false
-# Offense count: 98
+# Offense count: 96
# Cop supports --auto-correct.
Style/SpaceInsideBrackets:
Enabled: false
-# Offense count: 60
+# Offense count: 62
# Cop supports --auto-correct.
Style/SpaceInsideParens:
Enabled: false
-# Offense count: 5
+# Offense count: 7
# Cop supports --auto-correct.
Style/SpaceInsidePercentLiteralDelimiters:
Enabled: false
-# Offense count: 36
+# Offense count: 40
# Cop supports --auto-correct.
# Configuration parameters: SupportedStyles.
# SupportedStyles: use_perl_names, use_english_names
@@ -388,21 +400,28 @@ Style/SpecialGlobalVars:
Style/StringLiteralsInInterpolation:
Enabled: false
-# Offense count: 24
+# Offense count: 32
# Cop supports --auto-correct.
# Configuration parameters: IgnoredMethods.
# IgnoredMethods: respond_to, define_method
Style/SymbolProc:
Enabled: false
-# Offense count: 23
+# Offense count: 5
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles, AllowSafeAssignment.
+# SupportedStyles: require_parentheses, require_no_parentheses
+Style/TernaryParentheses:
+ Enabled: false
+
+# Offense count: 24
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
# SupportedStyles: comma, consistent_comma, no_comma
Style/TrailingCommaInArguments:
Enabled: false
-# Offense count: 113
+# Offense count: 102
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
# SupportedStyles: comma, consistent_comma, no_comma
@@ -415,7 +434,7 @@ Style/TrailingCommaInLiteral:
Style/TrailingUnderscoreVariable:
Enabled: false
-# Offense count: 90
+# Offense count: 76
# Cop supports --auto-correct.
Style/TrailingWhitespace:
Enabled: false
@@ -427,12 +446,12 @@ Style/TrailingWhitespace:
Style/TrivialAccessors:
Enabled: false
-# Offense count: 3
+# Offense count: 2
# Cop supports --auto-correct.
Style/UnlessElse:
Enabled: false
-# Offense count: 13
+# Offense count: 14
# Cop supports --auto-correct.
Style/UnneededInterpolation:
Enabled: false
diff --git a/CHANGELOG b/CHANGELOG
index ae5470964ff..592a3fec134 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,47 +1,87 @@
Please view this file on the master branch, on stable branches it's out of date.
-v 8.12.0 (unreleased)
+v 8.13.0 (unreleased)
+ - Speed-up group milestones show page
+
+v 8.12.2 (unreleased)
+
+v 8.12.1
+ - Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST
+ - Fix issue with search filter labels not displaying
+
+v 8.12.0
- Update the rouge gem to 2.0.6, which adds highlighting support for JSX, Prometheus, and others. !6251
- Only check :can_resolve permission if the note is resolvable
+ - Bump fog-aws to v0.11.0 to support ap-south-1 region
- Add ability to fork to a specific namespace using API. (ritave)
+ - Allow to set request_access_enabled for groups and projects
- Cleanup misalignments in Issue list view !6206
+ - Only create a protected branch upon a push to a new branch if a rule for that branch doesn't exist
- Prune events older than 12 months. (ritave)
- Prepend blank line to `Closes` message on merge request linked to issue (lukehowell)
+ - Fix issues/merge-request templates dropdown for forked projects
- Filter tags by name !6121
- Update gitlab shell secret file also when it is empty. !3774 (glensc)
- Give project selection dropdowns responsive width, make non-wrapping.
+ - Fix note form hint showing slash commands supported for commits.
- Make push events have equal vertical spacing.
+ - API: Ensure invitees are not returned in Members API.
+ - Preserve applied filters on issues search.
- Add two-factor recovery endpoint to internal API !5510
- Pass the "Remember me" value to the U2F authentication form
+ - Display stages in valid order in stages dropdown on build page
+ - Only update projects.last_activity_at once per hour when creating a new event
+ - Cycle analytics (first iteration) !5986
- Remove vendor prefixes for linear-gradient CSS (ClemMakesApps)
- Move pushes_since_gc from the database to Redis
+ - Limit number of shown environments on Merge Request: show only environments for target_branch, source_branch and tags
- Add font color contrast to external label in admin area (ClemMakesApps)
+ - Fix find file navigation links (ClemMakesApps)
- Change logo animation to CSS (ClemMakesApps)
- Instructions for enabling Git packfile bitmaps !6104
- Use Search::GlobalService.new in the `GET /projects/search/:query` endpoint
+ - Fix long comments in diffs messing with table width
+ - Add spec covering 'Gitlab::Git::committer_hash' !6433 (dandunckelman)
- Fix pagination on user snippets page
+ - Run CI builds with the permissions of users !5735
- Fix sorting of issues in API
+ - Fix download artifacts button links !6407
+ - Sort project variables by key. !6275 (Diego Souza)
- Ensure specs on sorting of issues in API are deterministic on MySQL
+ - Added ability to use predefined CI variables for environment name
+ - Added ability to specify URL in environment configuration in gitlab-ci.yml
- Escape search term before passing it to Regexp.new !6241 (winniehell)
- Fix pinned sidebar behavior in smaller viewports !6169
- Fix file permissions change when updating a file on the Gitlab UI !5979
+ - Added horizontal padding on build page sidebar on code coverage block. !6196 (Vitaly Baev)
- Change merge_error column from string to text type
+ - Fix issue with search filter labels not displaying
- Reduce contributions calendar data payload (ClemMakesApps)
+ - Show all pipelines for merge requests even from discarded commits !6414
+ - Replace contributions calendar timezone payload with dates (ClemMakesApps)
- Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel)
- Enable pipeline events by default !6278
- Move parsing of sidekiq ps into helper !6245 (pascalbetz)
+ - Added go to issue boards keyboard shortcut
- Expose `sha` and `merge_commit_sha` in merge request API (Ben Boeckel)
+ - Emoji can be awarded on Snippets !4456
- Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling)
- Fix blame table layout width
+ - Spec testing if issue authors can read issues on private projects
- Fix bug where pagination is still displayed despite all todos marked as done (ClemMakesApps)
- Request only the LDAP attributes we need !6187
- Center build stage columns in pipeline overview (ClemMakesApps)
+ - Fix bug with tooltip not hiding on discussion toggle button
- Rename behaviour to behavior in bug issue template for consistency (ClemMakesApps)
+ - Fix bug stopping issue description being scrollable after selecting issue template
- Remove suggested colors hover underline (ClemMakesApps)
+ - Fix jump to discussion button being displayed on commit notes
- Shorten task status phrase (ClemMakesApps)
- Fix project visibility level fields on settings
- Add hover color to emoji icon (ClemMakesApps)
+ - Increase ci_builds artifacts_size column to 8-byte integer to allow larger files
- Add textarea autoresize after comment (ClemMakesApps)
+ - Do not write SSH public key 'comments' to authorized_keys !6381
- Refresh todos count cache when an Issue/MR is deleted
- Fix branches page dropdown sort alignment (ClemMakesApps)
- Hides merge request button on branches page is user doesn't have permissions
@@ -50,9 +90,13 @@ v 8.12.0 (unreleased)
- Fix markdown anchor icon interaction (ClemMakesApps)
- Test migration paths from 8.5 until current release !4874
- Replace animateEmoji timeout with eventListener (ClemMakesApps)
+ - Show badges in Milestone tabs. !5946 (Dan Rowden)
- Optimistic locking for Issues and Merge Requests (title and description overriding prevention)
+ - Require confirmation when not logged in for unsubscribe links !6223 (Maximiliano Perez Coto)
- Add `wiki_page_events` to project hook APIs (Ben Boeckel)
- Remove Gitorious import
+ - Loads GFM autocomplete source only when required
+ - Fix issue with slash commands not loading on new issue page
- Fix inconsistent background color for filter input field (ClemMakesApps)
- Remove prefixes from transition CSS property (ClemMakesApps)
- Add Sentry logging to API calls
@@ -60,21 +104,26 @@ v 8.12.0 (unreleased)
- Use 'git update-ref' for safer web commits !6130
- Sort pipelines requested through the API
- Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling)
+ - Fix issue boards loading on large screens
- Change pipeline duration to be jobs running time instead of simple wall time from start to end !6084
- Show queued time when showing a pipeline !6084
- Remove unused mixins (ClemMakesApps)
- Add search to all issue board lists
+ - Scroll active tab into view on mobile
- Fix groups sort dropdown alignment (ClemMakesApps)
- Add horizontal scrolling to all sub-navs on mobile viewports (ClemMakesApps)
- Use JavaScript tooltips for mentions !5301 (winniehell)
- Add hover state to todos !5361 (winniehell)
- Fix icon alignment of star and fork buttons !5451 (winniehell)
- Fix alignment of icon buttons !5887 (winniehell)
+ - Added Ubuntu 16.04 support for packager.io (JonTheNiceGuy)
- Fix markdown help references (ClemMakesApps)
- Add last commit time to repo view (ClemMakesApps)
- Fix accessibility and visibility of project list dropdown button !6140
- Fix missing flash messages on service edit page (airatshigapov)
- - Added project specific enable/disable setting for LFS !5997
+ - Added project-specific enable/disable setting for LFS !5997
+ - Added group-specific enable/disable setting for LFS !6164
+ - Add optional 'author' param when making commits. !5822 (dandunckelman)
- Don't expose a user's token in the `/api/v3/user` API (!6047)
- Remove redundant js-timeago-pending from user activity log (ClemMakesApps)
- Ability to manage project issues, snippets, wiki, merge requests and builds access level
@@ -92,6 +141,7 @@ v 8.12.0 (unreleased)
- Remove green outline from `New branch unavailable` button on issue page !5858 (winniehell)
- Fix repo title alignment (ClemMakesApps)
- Change update interval of contacted_at
+ - Add LFS support to SSH !6043
- Fix branch title trailing space on hover (ClemMakesApps)
- Don't include 'Created By' tag line when importing from GitHub if there is a linked GitLab account (EspadaV8)
- Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison)
@@ -126,17 +176,36 @@ v 8.12.0 (unreleased)
- Refactor the triggers page and documentation !6217
- Show values of CI trigger variables only when clicked (Katarzyna Kobierska Ula Budziszewska)
- Use default clone protocol on "check out, review, and merge locally" help page URL
+ - Let the user choose a namespace and name on GitHub imports
- API for Ci Lint !5953 (Katarzyna Kobierska Urszula Budziszewska)
- Allow bulk update merge requests from merge requests index page
+ - Ensure validation messages are shown within the milestone form
- Add notification_settings API calls !5632 (mahcsig)
- Remove duplication between project builds and admin builds view !5680 (Katarzyna Kobierska Ula Budziszewska)
+ - Fix URLs with anchors in wiki !6300 (houqp)
- Deleting source project with existing fork link will close all related merge requests !6177 (Katarzyna Kobierska Ula Budziszeska)
- Return 204 instead of 404 for /ci/api/v1/builds/register.json if no builds are scheduled for a runner !6225
- Fix Gitlab::Popen.popen thread-safety issue
-
-v 8.11.6 (unreleased)
- - Fix an error where we were unable to create a CommitStatus for running state
+ - Add specs to removing project (Katarzyna Kobierska Ula Budziszewska)
+ - Clean environment variables when running git hooks
+ - Fix Import/Export issues importing protected branches and some specific models
+ - Fix non-master branch readme display in tree view
+ - Add UX improvements for merge request version diffs
+
+v 8.11.7
+ - Avoid conflict with admin labels when importing GitHub labels. !6158
+ - Restores `fieldName` to allow only string values in `gl_dropdown.js`. !6234
+ - Allow the Rails cookie to be used for API authentication.
+
+v 8.11.6
+ - Fix unnecessary horizontal scroll area in pipeline visualizations. !6005
+ - Make merge conflict file size limit 200 KB, to match the docs. !6052
+ - Fix an error where we were unable to create a CommitStatus for running state. !6107
+ - Optimize discussion notes resolving and unresolving. !6141
+ - Fix GitLab import button. !6167
- Restore SSH Key title auto-population behavior. !6186
+ - Fix DB schema to match latest migration. !6256
+ - Exclude some pending or inactivated rows in Member scopes.
v 8.11.5
- Optimize branch lookups and force a repository reload for Repository#find_branch. !6087
@@ -149,6 +218,7 @@ v 8.11.5
- Scope webhooks/services that will run for confidential issues
- Remove gitorious from import_sources
- Fix confidential issues being exposed as public using gitlab.com export
+ - Use oj gem for faster JSON processing
v 8.11.4
- Fix resolving conflicts on forks. !6082
@@ -341,6 +411,16 @@ v 8.11.0
- Update gitlab_git gem to 10.4.7
- Simplify SQL queries of marking a todo as done
+v 8.10.10
+ - Allow the Rails cookie to be used for API authentication.
+
+v 8.10.9
+ - Exclude some pending or inactivated rows in Member scopes
+
+v 8.10.8
+ - Fix information disclosure in issue boards.
+ - Fix privilege escalation in project import.
+
v 8.10.7
- Upgrade Hamlit to 2.6.1. !5873
- Upgrade Doorkeeper to 4.2.0. !5881
@@ -565,6 +645,13 @@ v 8.10.0
- Export and import avatar as part of project import/export
- Fix migration corrupting import data for old version upgrades
- Show tooltip on GitLab export link in new project page
+ - Fix import_data wrongly saved as a result of an invalid import_url !5206
+
+v 8.9.10
+ - Allow the Rails cookie to be used for API authentication.
+
+v 8.9.9
+ - Exclude some pending or inactivated rows in Member scopes
v 8.9.8
- Upgrade Doorkeeper to 4.2.0. !5881
@@ -582,12 +669,6 @@ v 8.9.6
- Keeps issue number when importing from Gitlab.com
- Add Pending tab for Builds (Katarzyna Kobierska, Urszula Budziszewska)
-v 8.9.7 (unreleased)
- - Fix import_data wrongly saved as a result of an invalid import_url
-
-v 8.9.6
- - Fix importing of events under notes for GitLab projects
-
v 8.9.5
- Add more debug info to import/export and memory killer. !5108
- Fixed avatar alignment in new MR view. !5095
@@ -1853,7 +1934,7 @@ v 8.1.3
- Use issue editor as cross reference comment author when issue is edited with a new mention
- Add Facebook authentication
-v 8.1.1
+v 8.1.2
- Fix cloning Wiki repositories via HTTP (Stan Hu)
- Add migration to remove satellites directory
- Fix specific runners visibility
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c77dcd96a7d..d5e15bfce14 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -91,19 +91,7 @@ This was inspired by [an article by Kent C. Dodds][medium-up-for-grabs].
## Implement design & UI elements
-### Design reference
-
-The GitLab design reference can be found in the [gitlab-design] project.
-The designs are made using Antetype (`.atype` files). You can use the
-[free Antetype viewer (Mac OSX only)] or grab an exported PNG from the design
-(the PNG is 1:1).
-
-The current designs can be found in the [`gitlab8.atype` file].
-
-### UI development kit
-
-Implemented UI elements can also be found at https://gitlab.com/help/ui. Please
-note that this page isn't comprehensive at this time.
+Please see the [UI Guide for building GitLab].
## Issue tracker
@@ -289,6 +277,8 @@ request is as follows:
1. For more complex migrations, write tests.
1. Merge requests **must** adhere to the [merge request performance
guidelines](doc/development/merge_request_performance_guidelines.md).
+1. For tests that use Capybara or PhantomJS, see this [article on how
+ to write reliable asynchronous tests](https://robots.thoughtbot.com/write-reliable-asynchronous-integration-tests-with-capybara).
The **official merge window** is in the beginning of the month from the 1st to
the 7th day of the month. This is the best time to submit an MR and get
@@ -489,7 +479,5 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide"
[scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide"
[newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide"
-[gitlab-design]: https://gitlab.com/gitlab-org/gitlab-design
-[free Antetype viewer (Mac OSX only)]: https://itunes.apple.com/us/app/antetype-viewer/id824152298?mt=12
-[`gitlab8.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/current/
+[UI Guide for building GitLab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/ui_guide.md
[license-finder-doc]: doc/development/licensing.md
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 1545d966571..40c341bdcdb 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-3.5.0
+3.6.0
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 6f4eebdf6f6..100435be135 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-0.8.1
+0.8.2
diff --git a/Gemfile b/Gemfile
index df546849fad..2ac0f8af755 100644
--- a/Gemfile
+++ b/Gemfile
@@ -26,7 +26,7 @@ gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6'
gem 'omniauth-bitbucket', '~> 0.0.2'
gem 'omniauth-cas3', '~> 1.1.2'
-gem 'omniauth-facebook', '~> 3.0.0'
+gem 'omniauth-facebook', '~> 4.0.0'
gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.0'
gem 'omniauth-google-oauth2', '~> 0.4.1'
@@ -53,7 +53,7 @@ gem 'browser', '~> 2.2'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
-gem 'gitlab_git', '~> 10.6.3'
+gem 'gitlab_git', '~> 10.6.6'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
@@ -135,8 +135,7 @@ gem 'after_commit_queue', '~> 1.3.0'
gem 'acts-as-taggable-on', '~> 3.4'
# Background jobs
-gem 'sinatra', '~> 1.4.4', require: false
-gem 'sidekiq', '~> 4.0'
+gem 'sidekiq', '~> 4.2'
gem 'sidekiq-cron', '~> 0.4.0'
gem 'redis-namespace', '~> 1.5.2'
@@ -206,6 +205,9 @@ gem 'mousetrap-rails', '~> 1.4.6'
# Detect and convert string character encoding
gem 'charlock_holmes', '~> 0.7.3'
+# Faster JSON
+gem 'oj', '~> 2.17.4'
+
# Parse time & duration
gem 'chronic', '~> 0.10.2'
gem 'chronic_duration', '~> 0.10.6'
@@ -295,7 +297,7 @@ group :development, :test do
gem 'spring-commands-spinach', '~> 1.1.0'
gem 'spring-commands-teaspoon', '~> 0.0.2'
- gem 'rubocop', '~> 0.41.2', require: false
+ gem 'rubocop', '~> 0.42.0', require: false
gem 'rubocop-rspec', '~> 1.5.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false
gem 'haml_lint', '~> 0.18.2', require: false
@@ -317,6 +319,7 @@ group :test do
gem 'webmock', '~> 1.21.0'
gem 'test_after_commit', '~> 0.4.2'
gem 'sham_rack', '~> 1.3.6'
+ gem 'timecop', '~> 0.8.0'
end
group :production do
diff --git a/Gemfile.lock b/Gemfile.lock
index b6307f72fa8..a5a2e5785ac 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -189,7 +189,7 @@ GEM
erubis (2.7.0)
escape_utils (1.1.1)
eventmachine (1.0.8)
- excon (0.49.0)
+ excon (0.52.0)
execjs (2.6.0)
expression_parser (0.9.0)
factory_girl (4.5.0)
@@ -215,8 +215,8 @@ GEM
flowdock (0.7.1)
httparty (~> 0.7)
multi_json
- fog-aws (0.9.2)
- fog-core (~> 1.27)
+ fog-aws (0.11.0)
+ fog-core (~> 1.38)
fog-json (~> 1.0)
fog-xml (~> 0.1)
ipaddress (~> 0.8)
@@ -225,7 +225,7 @@ GEM
fog-core (~> 1.27)
fog-json (~> 1.0)
fog-xml (~> 0.1)
- fog-core (1.40.0)
+ fog-core (1.42.0)
builder
excon (~> 0.49)
formatador (~> 0.2)
@@ -279,7 +279,7 @@ GEM
diff-lcs (~> 1.1)
mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3)
- gitlab_git (10.6.3)
+ gitlab_git (10.6.6)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
@@ -333,7 +333,7 @@ GEM
temple (~> 0.7.6)
thor
tilt
- hashie (3.4.3)
+ hashie (3.4.4)
health_check (2.1.0)
rails (>= 4.0)
hipchat (1.5.2)
@@ -401,7 +401,7 @@ GEM
mime-types (>= 1.16, < 4)
mail_room (0.8.0)
method_source (0.8.2)
- mime-types (2.99.2)
+ mime-types (2.99.3)
mimemagic (0.3.0)
mini_portile2 (2.1.0)
minitest (5.7.0)
@@ -427,6 +427,7 @@ GEM
rack (>= 1.2, < 3)
octokit (4.3.0)
sawyer (~> 0.7.0, >= 0.5.3)
+ oj (2.17.4)
omniauth (1.3.1)
hashie (>= 1.2, < 4)
rack (>= 1.0, < 3)
@@ -444,7 +445,7 @@ GEM
addressable (~> 2.3)
nokogiri (~> 1.6.6)
omniauth (~> 1.2)
- omniauth-facebook (3.0.0)
+ omniauth-facebook (4.0.0)
omniauth-oauth2 (~> 1.2)
omniauth-github (1.1.2)
omniauth (~> 1.0)
@@ -619,7 +620,7 @@ GEM
rspec-retry (0.4.5)
rspec-core
rspec-support (3.5.0)
- rubocop (0.41.2)
+ rubocop (0.42.0)
parser (>= 2.3.1.1, < 3.0)
powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0)
@@ -672,11 +673,11 @@ GEM
rack
shoulda-matchers (2.8.0)
activesupport (>= 3.0.0)
- sidekiq (4.1.4)
+ sidekiq (4.2.1)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
+ rack-protection (~> 1.5)
redis (~> 3.2, >= 3.2.1)
- sinatra (>= 1.4.7)
sidekiq-cron (0.4.0)
redis-namespace (>= 1.5.2)
rufus-scheduler (>= 2.0.24)
@@ -686,10 +687,6 @@ GEM
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.0)
- sinatra (1.4.7)
- rack (~> 1.5)
- rack-protection (~> 1.4)
- tilt (>= 1.3, < 3)
slack-notifier (1.2.1)
slop (3.6.0)
spinach (0.8.10)
@@ -762,7 +759,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.2)
- unicode-display_width (1.1.0)
+ unicode-display_width (1.1.1)
unicorn (4.9.0)
kgio (~> 2.6)
rack
@@ -866,7 +863,7 @@ DEPENDENCIES
github-linguist (~> 4.7.0)
github-markup (~> 1.4)
gitlab-flowdock-git-hook (~> 1.0.1)
- gitlab_git (~> 10.6.3)
+ gitlab_git (~> 10.6.6)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2)
@@ -904,12 +901,13 @@ DEPENDENCIES
nokogiri (~> 1.6.7, >= 1.6.7.2)
oauth2 (~> 1.2.0)
octokit (~> 4.3.0)
+ oj (~> 2.17.4)
omniauth (~> 1.3.1)
omniauth-auth0 (~> 1.4.1)
omniauth-azure-oauth2 (~> 0.0.6)
omniauth-bitbucket (~> 0.0.2)
omniauth-cas3 (~> 1.1.2)
- omniauth-facebook (~> 3.0.0)
+ omniauth-facebook (~> 4.0.0)
omniauth-github (~> 1.1.1)
omniauth-gitlab (~> 1.0.0)
omniauth-google-oauth2 (~> 0.4.1)
@@ -944,7 +942,7 @@ DEPENDENCIES
rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.5.0)
rspec-retry (~> 0.4.5)
- rubocop (~> 0.41.2)
+ rubocop (~> 0.42.0)
rubocop-rspec (~> 1.5.0)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.15.9)
@@ -958,10 +956,9 @@ DEPENDENCIES
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0)
- sidekiq (~> 4.0)
+ sidekiq (~> 4.2)
sidekiq-cron (~> 0.4.0)
simplecov (= 0.12.0)
- sinatra (~> 1.4.4)
slack-notifier (~> 1.2.0)
spinach-rails (~> 0.2.1)
spinach-rerun-reporter (~> 0.0.2)
@@ -978,6 +975,7 @@ DEPENDENCIES
teaspoon-jasmine (~> 2.2.0)
test_after_commit (~> 0.4.2)
thin (~> 1.7.0)
+ timecop (~> 0.8.0)
turbolinks (~> 2.5.0)
u2f (~> 0.2.1)
uglifier (~> 2.7.2)
@@ -993,4 +991,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.12.5
+ 1.13.0
diff --git a/README.md b/README.md
index 3df8bfa04c7..9661a554b9f 100644
--- a/README.md
+++ b/README.md
@@ -3,10 +3,11 @@
[![build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
[![coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
+[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
## Canonical source
-The source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/) and there are mirrors to make [contributing](CONTRIBUTING.md) as easy as possible.
+The cannonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/).
## Open source software to collaborate on code
diff --git a/VERSION b/VERSION
index 8a5bbc87ef9..dff4cd02d5f 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.12.0-pre
+8.13.0-pre
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 6df2ecf57a2..1cd2302111e 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -16,9 +16,6 @@
.replace(':id', group_id);
return $.ajax({
url: url,
- data: {
- private_token: gon.api_token
- },
dataType: "json"
}).done(function(group) {
return callback(group);
@@ -31,7 +28,6 @@
return $.ajax({
url: url,
data: {
- private_token: gon.api_token,
search: query,
per_page: 20
},
@@ -46,7 +42,6 @@
return $.ajax({
url: url,
data: {
- private_token: gon.api_token,
search: query,
per_page: 20
},
@@ -61,7 +56,6 @@
return $.ajax({
url: url,
data: {
- private_token: gon.api_token,
search: query,
order_by: order,
per_page: 20
@@ -74,7 +68,6 @@
newLabel: function(project_id, data, callback) {
var url = Api.buildUrl(Api.labelsPath)
.replace(':id', project_id);
- data.private_token = gon.api_token;
return $.ajax({
url: url,
type: "POST",
@@ -93,7 +86,6 @@
return $.ajax({
url: url,
data: {
- private_token: gon.api_token,
search: query,
per_page: 20
},
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 31fa508d6c1..c029bf3b5ca 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -251,6 +251,7 @@
} else {
notesHolders.hide();
}
+ $this.trigger('blur');
return e.preventDefault();
});
$document.off("click", '.js-confirm-danger');
diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js
index b18b6962382..95352164d76 100644
--- a/app/assets/javascripts/blob/template_selector.js
+++ b/app/assets/javascripts/blob/template_selector.js
@@ -13,6 +13,9 @@
this.buildDropdown();
this.bindEvents();
this.onFilenameUpdate();
+
+ this.autosizeUpdateEvent = document.createEvent('Event');
+ this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
}
TemplateSelector.prototype.buildDropdown = function() {
@@ -72,6 +75,10 @@
TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) {
this.editor.setValue(file.content, 1);
if (!skipFocus) this.editor.focus();
+
+ if (this.editor instanceof jQuery) {
+ this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
+ }
};
TemplateSelector.prototype.startLoadingSpinner = function() {
diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6
index 50fc11d7737..474805c1437 100644
--- a/app/assets/javascripts/boards/components/board_list.js.es6
+++ b/app/assets/javascripts/boards/components/board_list.js.es6
@@ -34,6 +34,11 @@
},
issues () {
this.$nextTick(() => {
+ if (this.scrollHeight() <= this.listHeight() && this.list.issuesSize > this.list.issues.length) {
+ this.list.page++;
+ this.list.getIssues(false);
+ }
+
if (this.scrollHeight() > this.listHeight()) {
this.showCount = true;
} else {
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 10abeb50f4b..78d21c0552a 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -27,10 +27,11 @@
$(document).off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
$(window).off('resize.build').on('resize.build', this.hideSidebar);
$(document).off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
+ $('#js-build-scroll > a').off('click').on('click', this.stepTrace);
this.updateArtifactRemoveDate();
if ($('#build-trace').length) {
this.getInitialBuildTrace();
- this.initScrollButtonAffix();
+ this.initScrollButtons();
}
if (this.build_status === "running" || this.build_status === "pending") {
$('#autoscroll-button').on('click', function() {
@@ -106,7 +107,7 @@
}
};
- Build.prototype.initScrollButtonAffix = function() {
+ Build.prototype.initScrollButtons = function() {
var $body, $buildScroll, $buildTrace;
$buildScroll = $('#js-build-scroll');
$body = $('body');
@@ -165,6 +166,14 @@
this.populateJobs(stage);
};
+ Build.prototype.stepTrace = function(e) {
+ e.preventDefault();
+ $currentTarget = $(e.currentTarget);
+ $.scrollTo($currentTarget.attr('href'), {
+ offset: -($('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight())
+ });
+ };
+
return Build;
})();
diff --git a/app/assets/javascripts/cycle-analytics.js.es6 b/app/assets/javascripts/cycle-analytics.js.es6
new file mode 100644
index 00000000000..cd9886ba58d
--- /dev/null
+++ b/app/assets/javascripts/cycle-analytics.js.es6
@@ -0,0 +1,93 @@
+((global) => {
+
+ const COOKIE_NAME = 'cycle_analytics_help_dismissed';
+ const store = gl.cycleAnalyticsStore = {
+ isLoading: true,
+ hasError: false,
+ isHelpDismissed: $.cookie(COOKIE_NAME),
+ analytics: {}
+ };
+
+ gl.CycleAnalytics = class CycleAnalytics {
+ constructor() {
+ const that = this;
+
+ this.vue = new Vue({
+ el: '#cycle-analytics',
+ name: 'CycleAnalytics',
+ created: this.fetchData(),
+ data: store,
+ methods: {
+ dismissLanding() {
+ that.dismissLanding();
+ }
+ }
+ });
+ }
+
+ fetchData(options) {
+ store.isLoading = true;
+ options = options || { startDate: 30 };
+
+ $.ajax({
+ url: $('#cycle-analytics').data('request-path'),
+ method: 'GET',
+ dataType: 'json',
+ contentType: 'application/json',
+ data: { start_date: options.startDate }
+ }).done((data) => {
+ this.decorateData(data);
+ this.initDropdown();
+ })
+ .error((data) => {
+ this.handleError(data);
+ })
+ .always(() => {
+ store.isLoading = false;
+ })
+ }
+
+ decorateData(data) {
+ data.summary = data.summary || [];
+ data.stats = data.stats || [];
+
+ data.summary.forEach((item) => {
+ item.value = item.value || '-';
+ });
+
+ data.stats.forEach((item) => {
+ item.value = item.value || '- - -';
+ });
+
+ store.analytics = data;
+ }
+
+ handleError(data) {
+ store.hasError = true;
+ new Flash('There was an error while fetching cycle analytics data.', 'alert');
+ }
+
+ dismissLanding() {
+ store.isHelpDismissed = true;
+ $.cookie(COOKIE_NAME, true, {
+ path: gon.relative_url_root || '/'
+ });
+ }
+
+ initDropdown() {
+ const $dropdown = $('.js-ca-dropdown');
+ const $label = $dropdown.find('.dropdown-label');
+
+ $dropdown.find('li a').off('click').on('click', (e) => {
+ e.preventDefault();
+ const $target = $(e.currentTarget);
+ const value = $target.data('value');
+
+ $label.text($target.text().trim());
+ this.fetchData({ startDate: value });
+ })
+ }
+
+ }
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 99b16f7d59b..ddf11ecf34c 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -94,6 +94,11 @@
break;
case "projects:merge_requests:conflicts":
window.mcui = new MergeConflictResolver()
+ break;
+ case 'projects:merge_requests:index':
+ shortcut_handler = new ShortcutsNavigation();
+ Issuable.init();
+ break;
case 'dashboard:activity':
new Activities();
break;
@@ -185,6 +190,9 @@
new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList();
break;
+ case 'projects:cycle_analytics:show':
+ new gl.CycleAnalytics();
+ break;
}
switch (path.first()) {
case 'admin':
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index bea141bae51..1b6db641200 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -352,7 +352,13 @@
if (self.options.clicked) {
self.options.clicked(selected, $el, e);
}
- return $el.trigger('blur');
+
+ // Update label right after all modifications in dropdown has been done
+ if (self.options.toggleLabel) {
+ self.updateLabel(selected, $el, self);
+ }
+
+ $el.trigger('blur');
});
}
}
@@ -529,7 +535,7 @@
} else {
if (!selected) {
value = this.options.id ? this.options.id(data) : data.id;
- fieldName = typeof this.options.fieldName === 'function' ? this.options.fieldName() : this.options.fieldName;
+ fieldName = this.options.fieldName;
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
if (field.length) {
@@ -589,6 +595,7 @@
GitLabDropdown.prototype.rowClicked = function(el) {
var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value;
+ fieldName = this.options.fieldName;
isInput = $(this.el).is('input');
if (this.renderedData) {
groupName = el.data('group');
@@ -600,27 +607,29 @@
selectedObject = this.renderedData[selectedIndex];
}
}
- fieldName = typeof this.options.fieldName === 'function' ? this.options.fieldName(selectedObject) : this.options.fieldName;
+ field = [];
value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id;
if (isInput) {
field = $(this.el);
- } else {
- field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + escape(value) + "']");
+ } else if(value) {
+ field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
}
if (el.hasClass(ACTIVE_CLASS)) {
el.removeClass(ACTIVE_CLASS);
- if (isInput) {
- field.val('');
- } else {
- field.remove();
+ if (field && field.length) {
+ if (isInput) {
+ field.val('');
+ } else {
+ field.remove();
+ }
}
} else if (el.hasClass(INDETERMINATE_CLASS)) {
el.addClass(ACTIVE_CLASS);
el.removeClass(INDETERMINATE_CLASS);
- if (value == null) {
+ if (field && field.length && value == null) {
field.remove();
}
- if (!field.length && fieldName) {
+ if ((!field || !field.length) && fieldName) {
this.addInput(fieldName, value, selectedObject);
}
} else {
@@ -630,25 +639,20 @@
this.dropdown.parent().find("input[name='" + fieldName + "']").remove();
}
}
- if (value == null) {
+ if (field && field.length && value == null) {
field.remove();
}
// Toggle active class for the tick mark
el.addClass(ACTIVE_CLASS);
if (value != null) {
- if (!field.length && fieldName) {
+ if ((!field || !field.length) && fieldName) {
this.addInput(fieldName, value, selectedObject);
- } else {
+ } else if (field && field.length) {
field.val(value).trigger('change');
}
}
}
- // Update label right after input has been added
- if (this.options.toggleLabel) {
- this.updateLabel(selectedObject, el, this);
- }
-
return selectedObject;
};
@@ -659,9 +663,6 @@
if (this.options.inputId != null) {
$input.attr('id', this.options.inputId);
}
- if (selectedObject && selectedObject.type) {
- $input.attr('data-type', selectedObject.type);
- }
return this.dropdown.before($input);
};
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js
index 9efad1ce943..4aced1e618f 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -10,24 +10,24 @@
ImporterStatus.prototype.initStatusPage = function() {
$('.js-add-to-import').off('click').on('click', (function(_this) {
return function(e) {
- var $btn, $namespace_input, $target_field, $tr, id, target_namespace;
+ var $btn, $namespace_input, $target_field, $tr, id, target_namespace, newName;
$btn = $(e.currentTarget);
$tr = $btn.closest('tr');
$target_field = $tr.find('.import-target');
- $namespace_input = $target_field.find('input');
+ $namespace_input = $target_field.find('.js-select-namespace option:selected');
id = $tr.attr('id').replace('repo_', '');
target_namespace = null;
-
+ newName = null;
if ($namespace_input.length > 0) {
- target_namespace = $namespace_input.prop('value');
- $target_field.empty().append(target_namespace + "/" + ($target_field.data('project_name')));
+ target_namespace = $namespace_input[0].innerHTML;
+ newName = $target_field.find('#path').prop('value');
+ $target_field.empty().append(target_namespace + "/" + newName);
}
-
$btn.disable().addClass('is-loading');
-
return $.post(_this.import_url, {
repo_id: id,
- target_namespace: target_namespace
+ target_namespace: target_namespace,
+ new_name: newName
}, {
dataType: 'script'
});
diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6
index 53faaa38a0c..73e2664e9c0 100644
--- a/app/assets/javascripts/issuable.js.es6
+++ b/app/assets/javascripts/issuable.js.es6
@@ -15,25 +15,32 @@
return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>');
},
initSearch: function() {
- this.timer = null;
- return $('#issue_search').off('keyup').on('keyup', function() {
- clearTimeout(this.timer);
- return this.timer = setTimeout(function() {
- var $form, $input, $search;
- $search = $('#issue_search');
- $form = $('.js-filter-form');
- $input = $("input[name='" + ($search.attr('name')) + "']", $form);
- if ($input.length === 0) {
- $form.append("<input type='hidden' name='" + ($search.attr('name')) + "' value='" + (_.escape($search.val())) + "'/>");
- } else {
- $input.val($search.val());
- }
- if ($search.val() !== '') {
- return Issuable.filterResults($form);
- }
- }, 500);
+ // `immediate` param set to false debounces on the `trailing` edge, lets user finish typing
+ const debouncedExecSearch = _.debounce(Issuable.executeSearch, 500, false);
+
+ $('#issuable_search').off('keyup').on('keyup', debouncedExecSearch);
+
+ // ensures existing filters are preserved when manually submitted
+ $('#issue_search_form').on('submit', (e) => {
+ e.preventDefault();
+ debouncedExecSearch(e);
});
},
+ executeSearch: function(e) {
+ const $search = $('#issuable_search');
+ const $searchName = $search.attr('name');
+ const $searchValue = $search.val();
+ const $filtersForm = $('.js-filter-form');
+ const $input = $(`input[name='${$searchName}']`, $filtersForm);
+
+ if (!$input.length) {
+ $filtersForm.append(`<input type='hidden' name='${$searchName}' value='${_.escape($searchValue)}'/>`);
+ } else {
+ $input.val($searchValue);
+ }
+
+ Issuable.filterResults($filtersForm);
+ },
initLabelFilterRemove: function() {
return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) {
var $button;
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 29a967a35a0..3f15a117ca8 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -166,7 +166,7 @@
instance.addInput(this.fieldName, label.id);
}
}
- if ($form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + escape(this.id(label)) + "']").length) {
+ if (this.id(label) && $form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + this.id(label).toString().replace(/'/g, '\\\'') + "']").length) {
selectedClass.push('is-active');
}
if ($dropdown.hasClass('js-multiselect') && removesAll) {
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index ce472f3bcd0..8e2fc0d1479 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -10,11 +10,13 @@
};
$(function() {
- hideEndFade($('.scrolling-tabs'));
+ var $scrollingTabs = $('.scrolling-tabs');
+
+ hideEndFade($scrollingTabs);
$(window).off('resize.nav').on('resize.nav', function() {
- return hideEndFade($('.scrolling-tabs'));
+ return hideEndFade($scrollingTabs);
});
- return $('.scrolling-tabs').on('scroll', function(event) {
+ $scrollingTabs.off('scroll').on('scroll', function(event) {
var $this, currentPosition, maxPosition;
$this = $(this);
currentPosition = $this.scrollLeft();
@@ -22,6 +24,23 @@
$this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0);
return $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1);
});
+
+ $scrollingTabs.each(function () {
+ var $this = $(this),
+ scrollingTabWidth = $this.width(),
+ $active = $this.find('.active'),
+ activeWidth = $active.width();
+
+ if ($active.length) {
+ var offset = $active.offset().left + activeWidth;
+
+ if (offset > scrollingTabWidth - 30) {
+ var scrollLeft = scrollingTabWidth / 2;
+ scrollLeft = (offset - scrollLeft) - (activeWidth / 2);
+ $this.scrollLeft(scrollLeft);
+ }
+ }
+ });
});
}).call(this);
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index dcba4a8d275..18bbfa7a459 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -232,10 +232,10 @@
$('.hll').removeClass('hll');
locationHash = window.location.hash;
if (locationHash !== '') {
- hashClassString = "." + (locationHash.replace('#', ''));
+ dataLineString = '[data-line-code="' + locationHash.replace('#', '') + '"]';
$diffLine = $(locationHash + ":not(.match)", $('#diffs'));
if (!$diffLine.is('tr')) {
- $diffLine = $('#diffs').find("td" + locationHash + ", td" + hashClassString);
+ $diffLine = $('#diffs').find("td" + locationHash + ", td" + dataLineString);
} else {
$diffLine = $diffLine.find('td');
}
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index 5bf900f3e1d..8e38ccf7e44 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -7,7 +7,6 @@
function ProjectFindFile(element1, options) {
this.element = element1;
this.options = options;
- this.goToBlob = bind(this.goToBlob, this);
this.goToTree = bind(this.goToTree, this);
this.selectRowDown = bind(this.selectRowDown, this);
this.selectRowUp = bind(this.selectRowUp, this);
@@ -36,16 +35,6 @@
}
};
})(this));
- return this.element.find(".tree-content-holder .tree-table").on("click", function(event) {
- var path;
- if (event.target.nodeName !== "A") {
- path = this.element.find(".tree-item-file-name a", this).attr("href");
- if (path) {
- return location.href = path;
- }
- }
- });
- // init event
};
ProjectFindFile.prototype.findFile = function() {
@@ -121,11 +110,12 @@
// make tbody row html
ProjectFindFile.prototype.makeHtml = function(filePath, matches, blobItemUrl) {
var $tr;
- $tr = $("<tr class='tree-item'><td class='tree-item-file-name'><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'><a></a></span></td></tr>");
+ $tr = $("<tr class='tree-item'><td class='tree-item-file-name link-container'><a><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'></span></a></td></tr>");
if (matches) {
$tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl));
} else {
- $tr.find("a").attr("href", blobItemUrl).text(filePath);
+ $tr.find("a").attr("href", blobItemUrl);
+ $tr.find(".str-truncated").text(filePath);
}
return $tr;
};
@@ -164,14 +154,6 @@
return location.href = this.options.treeUrl;
};
- ProjectFindFile.prototype.goToBlob = function() {
- var path;
- path = this.element.find(".tree-item.selected .tree-item-file-name a").attr("href");
- if (path) {
- return location.href = path;
- }
- };
-
return ProjectFindFile;
})();
diff --git a/app/assets/javascripts/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branch_edit.js.es6
index 40bc4adb71b..15a6dca2875 100644
--- a/app/assets/javascripts/protected_branch_edit.js.es6
+++ b/app/assets/javascripts/protected_branch_edit.js.es6
@@ -40,7 +40,6 @@
dataType: 'json',
data: {
_method: 'PATCH',
- id: this.$wrap.data('banchId'),
protected_branch: {
merge_access_levels_attributes: [{
id: this.$allowedToMergeDropdown.data('access-level-id'),
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
index 469e25482bb..b04159420d1 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -34,6 +34,9 @@
Mousetrap.bind('g i', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-issues');
});
+ Mousetrap.bind('g l', function() {
+ ShortcutsNavigation.findAndFollowLink('.shortcuts-issue-boards');
+ });
Mousetrap.bind('g m', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-merge_requests');
});
diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js
index b8da7c4f297..3bd4c3c066f 100644
--- a/app/assets/javascripts/users/calendar.js
+++ b/app/assets/javascripts/users/calendar.js
@@ -29,7 +29,7 @@
date.setDate(date.getDate() + i);
var day = date.getDay();
- var count = timestamps[date.getTime() * 0.001];
+ var count = timestamps[dateFormat(date, 'yyyy-mm-dd')];
// Create a new group array if this is the first day of the week
// or if is first object
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index f5223207f3a..2432ddb72f4 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -129,8 +129,6 @@
position: relative;
.avatar-holder {
- margin-bottom: 16px;
-
.avatar, .identicon {
margin: 0 auto;
float: none;
@@ -143,13 +141,7 @@
.cover-title {
color: $gl-header-color;
- margin: 0;
- font-size: 24px;
- font-weight: normal;
- margin-bottom: 10px;
- color: #4c4e54;
font-size: 23px;
- line-height: 1.1;
h1 {
color: $gl-gray-dark;
@@ -213,6 +205,9 @@
}
}
}
+ &.user-cover-block {
+ padding: 24px 0 0;
+ }
.group-info {
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index 0c21d0240b3..7ae309ba103 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -3,7 +3,6 @@
margin: 0;
margin-bottom: $gl-padding;
font-size: 14px;
- z-index: 100;
.flash-notice {
@extend .alert;
@@ -41,4 +40,3 @@
}
}
}
-
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 965fcc06518..46af18580d5 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -162,6 +162,10 @@ ul.content-list {
margin-right: 0;
}
}
+
+ .no-comments {
+ opacity: 0.5;
+ }
}
// When dragging a list item
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 553768b2e68..ea43f4afc37 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -99,8 +99,7 @@
.top-area {
@include clearfix;
-
- border-bottom: 1px solid #eee;
+ border-bottom: 1px solid $btn-gray-hover;
.nav-text {
padding-top: 16px;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 3f8433a0e7f..9f2d53d5206 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -164,7 +164,7 @@
text-decoration: none;
&:after {
- content: url('icon_anchor.svg');
+ content: image-url('icon_anchor.svg');
visibility: hidden;
}
}
@@ -204,7 +204,7 @@ body {
}
h1, h2, h3, h4, h5, h6 {
- color: $gl-header-color;
+ color: $gl-title-color;
font-weight: 600;
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 9f563a4de35..14ec310de2d 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -102,7 +102,7 @@ $gl-grayish-blue: #7f8fa4;
$gl-gray: $gl-text-color;
$gl-gray-dark: #313236;
$gl-gray-light: $gl-placeholder-color;
-$gl-header-color: $gl-title-color;
+$gl-header-color: #4c4e54;
/*
* Lists
@@ -270,6 +270,12 @@ $calendar-border-color: rgba(#000, .1);
$calendar-unselectable-bg: $gray-light;
/*
+ * Cycle Analytics
+ */
+$cycle-analytics-box-padding: 30px;
+$cycle-analytics-box-text-color: #8c8c8c;
+
+/*
* Personal Access Tokens
*/
$personal-access-tokens-disabled-label-color: #bbb;
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 037278bb083..9c84dceed05 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -1,3 +1,4 @@
+lex
[v-cloak] {
display: none;
}
@@ -18,6 +19,10 @@
}
}
+.is-ghost {
+ opacity: 0.3;
+}
+
.dropdown-menu-issues-board-new {
width: 320px;
@@ -34,47 +39,13 @@
> p {
margin: 0;
font-size: 14px;
- color: #9c9c9c;
}
}
.issue-boards-page {
- .content-wrapper {
- display: -webkit-flex;
- display: flex;
- -webkit-flex-direction: column;
- flex-direction: column;
- }
-
- .sub-nav,
- .issues-filters {
- -webkit-flex: none;
- flex: none;
- }
-
.page-with-sidebar {
- display: -webkit-flex;
- display: flex;
- min-height: 100vh;
- max-height: 100vh;
padding-bottom: 0;
}
-
- .issue-boards-content {
- display: -webkit-flex;
- display: flex;
- -webkit-flex: 1;
- flex: 1;
- width: 100%;
-
- .content {
- display: -webkit-flex;
- display: flex;
- -webkit-flex-direction: column;
- flex-direction: column;
- width: 100%;
- }
- }
}
.boards-app-loading {
@@ -83,46 +54,38 @@
}
.boards-list {
- display: -webkit-flex;
- display: flex;
- -webkit-flex: 1;
- flex: 1;
- -webkit-flex-basis: 0;
- flex-basis: 0;
- min-height: calc(100vh - 152px);
- max-height: calc(100vh - 152px);
+ height: calc(100vh - 152px);
+ width: 100%;
padding-top: 25px;
+ padding-bottom: 25px;
padding-right: ($gl-padding / 2);
padding-left: ($gl-padding / 2);
overflow-x: scroll;
+ white-space: nowrap;
@media (min-width: $screen-sm-min) {
+ height: 475px; // Needed for PhantomJS
+ height: calc(100vh - 220px);
min-height: 475px;
- max-height: none;
}
}
.board {
- display: -webkit-flex;
- display: flex;
- min-width: calc(85vw - 15px);
- max-width: calc(85vw - 15px);
- margin-bottom: 25px;
+ display: inline-block;
+ width: calc(85vw - 15px);
+ height: 100%;
padding-right: ($gl-padding / 2);
padding-left: ($gl-padding / 2);
+ white-space: normal;
+ vertical-align: top;
@media (min-width: $screen-sm-min) {
- min-width: 400px;
- max-width: 400px;
+ width: 400px;
}
}
.board-inner {
- display: -webkit-flex;
- display: flex;
- -webkit-flex-direction: column;
- flex-direction: column;
- width: 100%;
+ height: 100%;
font-size: $issue-boards-font-size;
background: $background-color;
border: 1px solid $border-color;
@@ -193,45 +156,31 @@
}
.board-list {
- -webkit-flex: 1;
- flex: 1;
- height: 400px;
+ height: calc(100% - 49px);
margin-bottom: 0;
padding: 5px;
+ list-style: none;
overflow-y: scroll;
overflow-x: hidden;
}
.board-list-loading {
margin-top: 10px;
- font-size: 26px;
-}
-
-.is-ghost {
- opacity: 0.3;
+ font-size: (26px / $issue-boards-font-size) * 1em;
}
.card {
position: relative;
- width: 100%;
padding: 10px $gl-padding;
background: #fff;
border-radius: $border-radius-default;
box-shadow: 0 1px 2px rgba(186, 186, 186, 0.5);
list-style: none;
- &.user-can-drag {
- padding-left: $gl-padding;
- }
-
&:not(:last-child) {
margin-bottom: 5px;
}
- a {
- cursor: pointer;
- }
-
.label {
border: 0;
outline: 0;
@@ -256,14 +205,13 @@
line-height: 25px;
.label {
- margin-right: 4px;
+ margin-right: 5px;
font-size: (14px / $issue-boards-font-size) * 1em;
}
}
.card-number {
- margin-right: 8px;
- font-weight: 500;
+ margin-right: 5px;
}
.issue-boards-search {
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index c879074c7fe..a5a260d4c8f 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -109,6 +109,10 @@
width: 100%;
}
+ .block-first {
+ padding: 5px 16px 11px;
+ }
+
.js-build-variable {
color: $code-color;
}
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
new file mode 100644
index 00000000000..778471a34d7
--- /dev/null
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -0,0 +1,144 @@
+#cycle-analytics {
+ margin: 24px auto 0;
+ max-width: 800px;
+ position: relative;
+
+ .panel {
+
+ .content-block {
+ padding: 24px 0;
+ border-bottom: none;
+ position: relative;
+
+ @media (max-width: $screen-sm-min) {
+ padding: 6px 0 24px;
+ }
+ }
+
+ .column {
+ text-align: center;
+
+ @media (max-width: $screen-sm-min) {
+ padding: 15px 0;
+ }
+
+ .header {
+ font-size: 30px;
+ line-height: 38px;
+ font-weight: normal;
+ margin: 0;
+ }
+
+ .text {
+ color: $layout-link-gray;
+ margin: 0;
+ }
+
+ &:last-child {
+ text-align: right;
+
+ @media (max-width: $screen-sm-min) {
+ text-align: center;
+ }
+ }
+ }
+
+ .dropdown {
+ top: 13px;
+ }
+ }
+
+ .bordered-box {
+ border: 1px solid $border-color;
+ @include border-radius($border-radius-default);
+
+ }
+
+ .content-list {
+ li {
+ padding: 18px $gl-padding $gl-padding;
+
+ .container-fluid {
+ padding: 0;
+ }
+ }
+
+ .title-col {
+ p {
+ margin: 0;
+
+ &.title {
+ line-height: 19px;
+ font-size: 15px;
+ font-weight: 600;
+ color: $gl-title-color;
+ }
+
+ &.text {
+ color: $layout-link-gray;
+
+ &.value-col {
+ color: $gl-title-color;
+ }
+ }
+ }
+ }
+
+ .value-col {
+ text-align: right;
+
+ span {
+ position: relative;
+ vertical-align: middle;
+ top: 3px;
+ }
+ }
+ }
+
+ .landing {
+ margin-bottom: $gl-padding;
+ overflow: hidden;
+
+ .dismiss-icon {
+ position: absolute;
+ right: $cycle-analytics-box-padding;
+ cursor: pointer;
+ color: #b2b2b2;
+ }
+
+ .svg-container {
+ text-align: center;
+
+ svg {
+ width: 136px;
+ height: 136px;
+ }
+ }
+
+ .inner-content {
+ @media (max-width: $screen-sm-min) {
+ padding: 0 28px;
+ text-align: center;
+ }
+
+ h4 {
+ color: $gl-text-color;
+ font-size: 17px;
+ }
+
+ p {
+ color: $cycle-analytics-box-text-color;
+ margin-bottom: $gl-padding;
+ }
+ }
+ }
+
+ .fa-spinner {
+ font-size: 28px;
+ position: relative;
+ margin-left: -20px;
+ left: 50%;
+ margin-top: 36px;
+ }
+
+}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 21cee2e3a70..b8ef76cc74e 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -68,6 +68,11 @@
border-collapse: separate;
margin: 0;
padding: 0;
+ table-layout: fixed;
+
+ .diff-line-num {
+ width: 50px;
+ }
.line_holder td {
line-height: $code_line_height;
@@ -98,10 +103,6 @@
}
tr.line_holder.parallel {
- .old_line, .new_line {
- min-width: 50px;
- }
-
td.line_content.parallel {
width: 46%;
}
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index b657ca47d38..732dc645c66 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -55,3 +55,16 @@
}
}
}
+
+.groups-header {
+
+ @media (min-width: $screen-sm-min) {
+ .nav-links {
+ width: 35%;
+ }
+
+ .nav-controls {
+ width: 65%;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 60a0d50ba73..3ac34cbc829 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -10,10 +10,6 @@
.issue-labels {
display: inline-block;
}
-
- .issue-no-comments {
- opacity: 0.5;
- }
}
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 2a44b95de64..926247e5e87 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -231,10 +231,6 @@
.merge-request-labels {
display: inline-block;
}
-
- .merge-request-no-comments {
- opacity: 0.5;
- }
}
.merge-request-angle {
@@ -377,11 +373,40 @@
.mr-version-controls {
background: $background-color;
- padding: $gl-btn-padding;
- color: $gl-placeholder-color;
+ border-bottom: 1px solid $border-color;
+ color: $gl-text-color;
+
+ .mr-version-menus-container {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-align-items: center;
+ align-items: center;
+ padding: 16px;
+ }
+
+ .comments-disabled-notif {
+ padding: 10px 16px;
+ .btn {
+ margin-left: 5px;
+ }
+ }
+
+ .mr-version-dropdown,
+ .mr-version-compare-dropdown {
+ margin: 0 7px;
+ }
+
+ .comments-disabled-notif {
+ border-top: 1px solid $border-color;
+ }
+
+ .dropdown-title {
+ color: $gl-text-color;
+ }
- a.btn-link {
- color: $gl-dark-link-color;
+ .fa-info-circle {
+ color: $orange-normal;
+ padding-right: 5px;
}
}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index b94f524b513..6b865730487 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -2,13 +2,17 @@
max-width: 90%;
}
-li.milestone {
- h4 {
- font-weight: bold;
- }
+.milestones {
+ .milestone {
+ padding: 10px 16px;
+
+ h4 {
+ font-weight: bold;
+ }
- .progress {
- height: 6px;
+ .progress {
+ height: 6px;
+ }
}
}
@@ -64,3 +68,14 @@ li.milestone {
border-bottom: 1px solid $border-color;
padding: 20px 0;
}
+
+@media (max-width: $screen-sm-min) {
+ .milestone-actions {
+ @include clearfix();
+ padding-top: $gl-vert-padding;
+
+ .btn:first-child {
+ margin-left: 0;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 2d66ab25da6..b035bfc9f3c 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -147,14 +147,41 @@
}
.stage-cell {
- text-align: center;
+ font-size: 0;
svg {
height: 18px;
width: 18px;
+ position: relative;
+ z-index: 2;
vertical-align: middle;
overflow: visible;
}
+
+ .stage-container {
+ display: inline-block;
+ position: relative;
+ margin-right: 6px;
+
+ .tooltip {
+ white-space: nowrap;
+ }
+
+ &:not(:last-child) {
+ &::after {
+ content: '';
+ width: 8px;
+ position: absolute;;
+ right: -7px;
+ bottom: 8px;
+ border-bottom: 2px solid $border-color;
+ }
+ }
+
+ a {
+ display: block;
+ }
+ }
}
.duration,
@@ -318,9 +345,17 @@
.build-content {
width: 130px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
+
+ .ci-status-text {
+ width: 110px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: middle;
+ display: inline-block;
+ position: relative;
+ top: -1px;
+ }
a {
color: $layout-link-gray;
@@ -331,13 +366,74 @@
text-decoration: underline;
}
}
+ }
+
+ .dropdown-menu-toggle {
+ border: none;
+ width: auto;
+ padding: 0;
+ color: $layout-link-gray;
+
+ .ci-status-text {
+ width: 80px;
+ }
+ }
+
+ .grouped-pipeline-dropdown {
+ padding: 8px 0;
+ width: 200px;
+ left: auto;
+ right: -214px;
+ top: -9px;
+
+ a:hover {
+ .ci-status-text {
+ text-decoration: none;
+ }
+ }
+
+ .ci-status-text {
+ width: 145px;
+ }
+ .arrow {
+ &:before,
+ &:after {
+ content: '';
+ display: inline-block;
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid;
+ top: 18px;
+ }
+
+ &:before {
+ left: -5px;
+ margin-top: -6px;
+ border-width: 7px 5px 7px 0;
+ border-right-color: $border-color;
+ }
+
+ &:after {
+ left: -4px;
+ margin-top: -9px;
+ border-width: 10px 7px 10px 0;
+ border-right-color: $white-light;
+ }
+ }
+ }
+
+ .badge {
+ background-color: $gray-dark;
+ color: $layout-link-gray;
+ font-weight: normal;
}
}
svg {
- position: relative;
- top: 2px;
+ vertical-align: middle;
margin-right: 5px;
}
@@ -442,7 +538,7 @@
width: 21px;
height: 25px;
position: absolute;
- top: -28.5px;
+ top: -29px;
border-top: 2px solid $border-color;
}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 6f58203f49c..0fcdaf94a21 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -93,8 +93,9 @@
.profile-user-bio {
// Limits the width of the user bio for readability.
- max-width: 750px;
- margin: auto;
+ max-width: 600px;
+ margin: 15px auto 0;
+ padding: 0 16px;
}
.user-avatar-button {
@@ -212,6 +213,28 @@
}
.user-profile {
+ .cover-controls a {
+ margin-left: 5px;
+ }
+ .profile-header {
+ margin: 0 auto;
+ .avatar-holder {
+ width: 90px;
+ display: inline-block;
+ }
+ .user-info {
+ display: inline-block;
+ text-align: left;
+ vertical-align: middle;
+ margin-left: 15px;
+ .handle {
+ color: $gl-gray-light;
+ }
+ .member-date {
+ margin-bottom: 4px;
+ }
+ }
+ }
@media (max-width: $screen-xs-max) {
.cover-block {
padding-top: 20px;
@@ -219,16 +242,26 @@
.cover-controls {
position: static;
+ padding: 0 16px;
margin-bottom: 20px;
+ display: -webkit-flex;
+ display: flex;
.btn {
- display: inline-block;
- width: 46%;
+ -webkit-flex-grow: 1;
+ flex-grow: 1;
+ &:first-child {
+ margin-left: 0;
+ }
}
}
}
}
+.user-profile-nav {
+ margin-top: 15px;
+}
+
table.u2f-registrations {
th:not(:last-child), td:not(:last-child) {
border-right: solid 1px transparent;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 3e6e50375f6..8c8c403244e 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -334,6 +334,10 @@ a.deploy-project-label {
a {
color: $gl-dark-link-color;
}
+
+ .dropdown-menu {
+ width: 240px;
+ }
}
.last-push-widget {
@@ -766,3 +770,13 @@ pre.light-well {
}
}
}
+
+.project-path {
+ .form-control {
+ min-width: 100px;
+ }
+ .select2-choice {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss
index 2aa939b7dc3..4d5df566d9b 100644
--- a/app/assets/stylesheets/pages/snippets.scss
+++ b/app/assets/stylesheets/pages/snippets.scss
@@ -2,20 +2,6 @@
padding: 2px;
}
-.snippet-holder {
- margin-bottom: -$gl-padding;
-
- .file-holder {
- border-top: 0;
- }
-
- .file-actions {
- .btn-clipboard {
- @extend .btn;
- }
- }
-}
-
.markdown-snippet-copy {
position: fixed;
top: -10px;
@@ -24,29 +10,25 @@
max-width: 0;
}
-.file-holder.snippet-file-content {
- padding-bottom: $gl-padding;
- border-bottom: 1px solid $border-color;
-
- .file-title {
- padding-top: $gl-padding;
- padding-bottom: $gl-padding;
- }
+.snippet-file-content {
+ border-radius: 3px;
+ margin-bottom: $gl-padding;
- .file-actions {
- top: 12px;
+ .btn-clipboard {
+ @extend .btn;
}
+}
- .file-content {
- border-left: 1px solid $border-color;
- border-right: 1px solid $border-color;
- border-bottom: 1px solid $border-color;
- }
+.project-snippets .awards {
+ border-bottom: 1px solid $table-border-color;
+ padding-bottom: $gl-padding;
}
.snippet-title {
font-size: 24px;
- font-weight: normal;
+ font-weight: 600;
+ padding: $gl-padding;
+ padding-left: 0;
}
.snippet-actions {
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 1778c069706..7b6577c513e 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -55,6 +55,15 @@
}
.tree-item {
+ .link-container {
+ padding: 0;
+
+ a {
+ padding: 10px $gl-padding;
+ display: block;
+ }
+ }
+
.tree-item-file-name {
max-width: 320px;
vertical-align: middle;
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index cdfa8d91a28..aed77d0358a 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -60,6 +60,14 @@ class Admin::GroupsController < Admin::ApplicationController
end
def group_params
- params.require(:group).permit(:name, :description, :path, :avatar, :visibility_level, :request_access_enabled)
+ params.require(:group).permit(
+ :avatar,
+ :description,
+ :lfs_enabled,
+ :name,
+ :path,
+ :request_access_enabled,
+ :visibility_level
+ )
end
end
diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb
index 172d5344b7a..3717c49f272 100644
--- a/app/controllers/concerns/toggle_award_emoji.rb
+++ b/app/controllers/concerns/toggle_award_emoji.rb
@@ -10,7 +10,9 @@ module ToggleAwardEmoji
if awardable.user_can_award?(current_user, name)
awardable.toggle_award_emoji(name, current_user)
- TodoService.new.new_award_emoji(to_todoable(awardable), current_user)
+
+ todoable = to_todoable(awardable)
+ TodoService.new.new_award_emoji(todoable, current_user) if todoable
render json: { ok: true }
else
@@ -24,8 +26,10 @@ module ToggleAwardEmoji
case awardable
when Note
awardable.noteable
- else
+ when MergeRequest, Issue
awardable
+ when Snippet
+ nil
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index cb82d62616c..b83c3a872cf 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -121,7 +121,17 @@ class GroupsController < Groups::ApplicationController
end
def group_params
- params.require(:group).permit(:name, :description, :path, :avatar, :public, :visibility_level, :share_with_group_lock, :request_access_enabled)
+ params.require(:group).permit(
+ :avatar,
+ :description,
+ :lfs_enabled,
+ :name,
+ :path,
+ :public,
+ :request_access_enabled,
+ :share_with_group_lock,
+ :visibility_level
+ )
end
def load_events
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index 8c6bdd16383..ee7d498c59c 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -40,11 +40,12 @@ class Import::GithubController < Import::BaseController
def create
@repo_id = params[:repo_id].to_i
repo = client.repo(@repo_id)
- @project_name = repo.name
- @target_namespace = find_or_create_namespace(repo.owner.login, client.user.login)
+ @project_name = params[:new_name].presence || repo.name
+ namespace_path = params[:target_namespace].presence || current_user.namespace_path
+ @target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path)
if current_user.can?(:create_projects, @target_namespace)
- @project = Gitlab::GithubImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute
+ @project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params).execute
else
render 'unauthorized'
end
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 66ebdcc37a7..34d5d99558e 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -11,7 +11,8 @@ class JwtController < ApplicationController
service = SERVICES[params[:service]]
return head :not_found unless service
- result = service.new(@project, @user, auth_params).execute
+ result = service.new(@authentication_result.project, @authentication_result.actor, auth_params).
+ execute(authentication_abilities: @authentication_result.authentication_abilities || [])
render json: result, status: result[:http_status]
end
@@ -19,31 +20,26 @@ class JwtController < ApplicationController
private
def authenticate_project_or_user
- authenticate_with_http_basic do |login, password|
- # if it's possible we first try to authenticate project with login and password
- @project = authenticate_project(login, password)
- return if @project
+ @authentication_result = Gitlab::Auth::Result.new
- @user = authenticate_user(login, password)
- return if @user
+ authenticate_with_http_basic do |login, password|
+ @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
- render_403
+ render_403 unless @authentication_result.success? &&
+ (@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User))
end
+ rescue Gitlab::Auth::MissingPersonalTokenError
+ render_missing_personal_token
end
- def auth_params
- params.permit(:service, :scope, :account, :client_id)
+ def render_missing_personal_token
+ render plain: "HTTP Basic: Access denied\n" \
+ "You have 2FA enabled, please use a personal access token for Git over HTTP.\n" \
+ "You can generate one at #{profile_personal_access_tokens_url}",
+ status: 401
end
- def authenticate_project(login, password)
- if login == 'gitlab-ci-token'
- Project.with_builds_enabled.find_by(runners_token: password)
- end
- end
-
- def authenticate_user(login, password)
- user = Gitlab::Auth.find_with_user_password(login, password)
- Gitlab::Auth.rate_limit!(request.ip, success: user.present?, login: login)
- user
+ def auth_params
+ params.permit(:service, :scope, :account, :client_id)
end
end
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index 77934ff9962..3b2e35a7a05 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -35,7 +35,11 @@ class Projects::BuildsController < Projects::ApplicationController
respond_to do |format|
format.html
format.json do
- render json: @build.to_json(methods: :trace_html)
+ render json: {
+ id: @build.id,
+ status: @build.status,
+ trace_html: @build.trace_html
+ }
end
end
end
@@ -74,7 +78,7 @@ class Projects::BuildsController < Projects::ApplicationController
def erase
@build.erase(erased_by: current_user)
redirect_to namespace_project_build_path(project.namespace, project, @build),
- notice: "Build has been sucessfully erased!"
+ notice: "Build has been successfully erased!"
end
def raw
diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb
new file mode 100644
index 00000000000..16a7b1fc6e2
--- /dev/null
+++ b/app/controllers/projects/cycle_analytics_controller.rb
@@ -0,0 +1,67 @@
+class Projects::CycleAnalyticsController < Projects::ApplicationController
+ include ActionView::Helpers::DateHelper
+ include ActionView::Helpers::TextHelper
+
+ before_action :authorize_read_cycle_analytics!
+
+ def show
+ @cycle_analytics = CycleAnalytics.new(@project, from: parse_start_date)
+
+ respond_to do |format|
+ format.html
+ format.json { render json: cycle_analytics_json }
+ end
+ end
+
+ private
+
+ def parse_start_date
+ case cycle_analytics_params[:start_date]
+ when '30' then 30.days.ago
+ when '90' then 90.days.ago
+ else 90.days.ago
+ end
+ end
+
+ def cycle_analytics_params
+ return {} unless params[:cycle_analytics].present?
+
+ { start_date: params[:cycle_analytics][:start_date] }
+ end
+
+ def cycle_analytics_json
+ cycle_analytics_view_data = [[:issue, "Issue", "Time before an issue gets scheduled"],
+ [:plan, "Plan", "Time before an issue starts implementation"],
+ [:code, "Code", "Time until first merge request"],
+ [:test, "Test", "Total test time for all commits/merges"],
+ [:review, "Review", "Time between merge request creation and merge/close"],
+ [:staging, "Staging", "From merge request merge until deploy to production"],
+ [:production, "Production", "From issue creation until deploy to production"]]
+
+ stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_description)|
+ value = @cycle_analytics.send(stage_method).presence
+
+ stats << {
+ title: stage_text,
+ description: stage_description,
+ value: value && !value.zero? ? distance_of_time_in_words(value) : nil
+ }
+ stats
+ end
+
+ issues = @cycle_analytics.summary.new_issues
+ commits = @cycle_analytics.summary.commits
+ deploys = @cycle_analytics.summary.deploys
+
+ summary = [
+ { title: "New Issue".pluralize(issues), value: issues },
+ { title: "Commit".pluralize(commits), value: commits },
+ { title: "Deploy".pluralize(deploys), value: deploys }
+ ]
+
+ {
+ summary: summary,
+ stats: stats
+ }
+ end
+end
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index f5ce63fdfed..383e184d796 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -4,7 +4,11 @@ class Projects::GitHttpClientController < Projects::ApplicationController
include ActionController::HttpAuthentication::Basic
include KerberosSpnegoHelper
- attr_reader :user
+ attr_reader :authentication_result
+
+ delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true
+
+ alias_method :user, :actor
# Git clients will not know what authenticity token to send along
skip_before_action :verify_authenticity_token
@@ -15,32 +19,25 @@ class Projects::GitHttpClientController < Projects::ApplicationController
private
def authenticate_user
+ @authentication_result = Gitlab::Auth::Result.new
+
if project && project.public? && download_request?
return # Allow access
end
if allow_basic_auth? && basic_auth_provided?
login, password = user_name_and_password(request)
- auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip)
-
- if auth_result.type == :ci && download_request?
- @ci = true
- elsif auth_result.type == :oauth && !download_request?
- # Not allowed
- elsif auth_result.type == :missing_personal_token
- render_missing_personal_token
- return # Render above denied access, nothing left to do
- else
- @user = auth_result.user
- end
- if ci? || user
+ if handle_basic_authentication(login, password)
return # Allow access
end
elsif allow_kerberos_spnego_auth? && spnego_provided?
- @user = find_kerberos_user
+ kerberos_user = find_kerberos_user
+
+ if kerberos_user
+ @authentication_result = Gitlab::Auth::Result.new(
+ kerberos_user, nil, :kerberos, Gitlab::Auth.full_authentication_abilities)
- if user
send_final_spnego_response
return # Allow access
end
@@ -48,6 +45,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController
send_challenges
render plain: "HTTP Basic: Access denied\n", status: 401
+ rescue Gitlab::Auth::MissingPersonalTokenError
+ render_missing_personal_token
end
def basic_auth_provided?
@@ -114,8 +113,41 @@ class Projects::GitHttpClientController < Projects::ApplicationController
render plain: 'Not Found', status: :not_found
end
+ def handle_basic_authentication(login, password)
+ @authentication_result = Gitlab::Auth.find_for_git_client(
+ login, password, project: project, ip: request.ip)
+
+ return false unless @authentication_result.success?
+
+ if download_request?
+ authentication_has_download_access?
+ else
+ authentication_has_upload_access?
+ end
+ end
+
def ci?
- @ci.present?
+ authentication_result.ci?(project)
+ end
+
+ def lfs_deploy_token?
+ authentication_result.lfs_deploy_token?(project)
+ end
+
+ def authentication_has_download_access?
+ has_authentication_ability?(:download_code) || has_authentication_ability?(:build_download_code)
+ end
+
+ def authentication_has_upload_access?
+ has_authentication_ability?(:push_code)
+ end
+
+ def has_authentication_ability?(capability)
+ (authentication_abilities || []).include?(capability)
+ end
+
+ def authentication_project
+ authentication_result.project
end
def verify_workhorse_api!
diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb
index 9805705c4e3..662d38b10a5 100644
--- a/app/controllers/projects/git_http_controller.rb
+++ b/app/controllers/projects/git_http_controller.rb
@@ -86,7 +86,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
end
def access
- @access ||= Gitlab::GitAccess.new(user, project, 'http')
+ @access ||= Gitlab::GitAccess.new(user, project, 'http', authentication_abilities: authentication_abilities)
end
def access_check
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index de02e28e384..3eb13a121bf 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -23,18 +23,9 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to :html
def index
- terms = params['issue_search']
@issues = issues_collection
-
- if terms.present?
- if terms =~ /\A#(\d+)\z/
- @issues = @issues.where(iid: $1)
- else
- @issues = @issues.full_search(terms)
- end
- end
-
@issues = @issues.page(params[:page])
+
@labels = @project.labels.where(title: params[:label_name])
respond_to do |format|
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index aa8645ba8cc..935417d4ae8 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -31,17 +31,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :resolve_conflicts]
def index
- terms = params['issue_search']
@merge_requests = merge_requests_collection
-
- if terms.present?
- if terms =~ /\A[#!](\d+)\z/
- @merge_requests = @merge_requests.where(iid: $1)
- else
- @merge_requests = @merge_requests.full_search(terms)
- end
- end
-
@merge_requests = @merge_requests.page(params[:page])
@merge_requests = @merge_requests.preload(:target_project)
@@ -428,6 +418,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def validates_merge_request
+ # If source project was removed and merge request for some reason
+ # wasn't close (Ex. mr from fork to origin)
+ return invalid_mr if !@merge_request.source_project && @merge_request.open?
+
# Show git not found page
# if there is no saved commits between source & target branch
if @merge_request.commits.blank?
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 17ceefec3b8..e290a0eadda 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -1,6 +1,8 @@
class Projects::SnippetsController < Projects::ApplicationController
+ include ToggleAwardEmoji
+
before_action :module_enabled
- before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
+ before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji]
# Allow read any snippet
before_action :authorize_read_project_snippet!, except: [:new, :create, :index]
@@ -80,6 +82,7 @@ class Projects::SnippetsController < Projects::ApplicationController
def snippet
@snippet ||= @project.snippets.find(params[:id])
end
+ alias_method :awardable, :snippet
def authorize_read_project_snippet!
return render_404 unless can?(current_user, :read_project_snippet, @snippet)
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 61517d21f9f..d01e0dedf52 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -6,8 +6,6 @@ class SearchController < ApplicationController
layout 'search'
def show
- return if params[:search].nil? || params[:search].blank?
-
if params[:project_id].present?
@project = Project.find_by(id: params[:project_id])
@project = nil unless can?(current_user, :download_code, @project)
@@ -18,6 +16,8 @@ class SearchController < ApplicationController
@group = nil unless can?(current_user, :read_group, @group)
end
+ return if params[:search].nil? || params[:search].blank?
+
@search_term = params[:search]
@scope = params[:scope]
diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb
index 7271c933b9b..3085ff33aba 100644
--- a/app/controllers/sent_notifications_controller.rb
+++ b/app/controllers/sent_notifications_controller.rb
@@ -3,12 +3,19 @@ class SentNotificationsController < ApplicationController
def unsubscribe
@sent_notification = SentNotification.for(params[:id])
+
return render_404 unless @sent_notification && @sent_notification.unsubscribable?
+ return unsubscribe_and_redirect if current_user || params[:force]
+ end
+ private
+
+ def unsubscribe_and_redirect
noteable = @sent_notification.noteable
noteable.unsubscribe(@sent_notification.recipient)
flash[:notice] = "You have been unsubscribed from this thread."
+
if current_user
case noteable
when Issue
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 2a17c1f34db..d198782138a 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -1,4 +1,6 @@
class SnippetsController < ApplicationController
+ include ToggleAwardEmoji
+
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow read snippet
@@ -85,6 +87,7 @@ class SnippetsController < ApplicationController
PersonalSnippet.find(params[:id])
end
end
+ alias_method :awardable, :snippet
def authorize_read_snippet!
authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet)
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index a99632454d9..a4bedb3bfe6 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -73,7 +73,7 @@ class UsersController < ApplicationController
def calendar
calendar = contributions_calendar
- @timestamps = calendar.timestamps
+ @activity_dates = calendar.activity_dates
render 'calendar', layout: false
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 60996b181f2..8f9ef8f725c 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -216,7 +216,14 @@ class IssuableFinder
end
def by_search(items)
- items = items.search(search) if search
+ if search
+ items =
+ if search =~ iid_pattern
+ items.where(iid: $~[:iid])
+ else
+ items.full_search(search)
+ end
+ end
items
end
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index c2befa5a5b3..be00a219205 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -25,4 +25,8 @@ class IssuesFinder < IssuableFinder
def init_collection
Issue.visible_to_user(current_user)
end
+
+ def iid_pattern
+ @iid_pattern ||= %r{\A#{Regexp.escape(Issue.reference_prefix)}(?<iid>\d+)\z}
+ end
end
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index b258216d0d4..3b254e7d9d5 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -19,4 +19,14 @@ class MergeRequestsFinder < IssuableFinder
def klass
MergeRequest
end
+
+ private
+
+ def iid_pattern
+ @iid_pattern ||= %r{\A[
+ #{Regexp.escape(MergeRequest.reference_prefix)}
+ #{Regexp.escape(Issue.reference_prefix)}
+ ](?<iid>\d+)\z
+ }x
+ end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 5f3765cad0d..1df430e6279 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -249,7 +249,7 @@ module ApplicationHelper
milestone_title: params[:milestone_title],
assignee_id: params[:assignee_id],
author_id: params[:author_id],
- issue_search: params[:issue_search],
+ search: params[:search],
label_name: params[:label_name]
}
diff --git a/app/helpers/award_emoji_helper.rb b/app/helpers/award_emoji_helper.rb
new file mode 100644
index 00000000000..aa134cea31c
--- /dev/null
+++ b/app/helpers/award_emoji_helper.rb
@@ -0,0 +1,9 @@
+module AwardEmojiHelper
+ def toggle_award_url(awardable)
+ if @project
+ url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
+ else
+ url_for([:toggle_award_emoji, awardable])
+ end
+ end
+end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index a322a90cc4e..670a7ca36f4 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -46,6 +46,10 @@ module GitlabRoutingHelper
namespace_project_environments_path(project.namespace, project, *args)
end
+ def project_cycle_analytics_path(project, *args)
+ namespace_project_cycle_analytics_path(project.namespace, project, *args)
+ end
+
def project_builds_path(project, *args)
namespace_project_builds_path(project.namespace, project, *args)
end
@@ -66,6 +70,10 @@ module GitlabRoutingHelper
namespace_project_runner_path(@project.namespace, @project, runner, *args)
end
+ def environment_path(environment, *args)
+ namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args)
+ end
+
def issue_path(entity, *args)
namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args)
end
@@ -98,6 +106,14 @@ module GitlabRoutingHelper
end
end
+ def toggle_award_emoji_personal_snippet_path(*args)
+ toggle_award_emoji_snippet_path(*args)
+ end
+
+ def toggle_award_emoji_namespace_project_project_snippet_path(*args)
+ toggle_award_emoji_namespace_project_snippet_path(*args)
+ end
+
## Members
def project_members_url(project, *args)
namespace_project_project_members_url(project.namespace, project)
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index b9211e88473..ab880ed6de0 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -23,4 +23,29 @@ module GroupsHelper
full_title
end
end
+
+ def projects_lfs_status(group)
+ lfs_status =
+ if group.lfs_enabled?
+ group.projects.select(&:lfs_enabled?).size
+ else
+ group.projects.reject(&:lfs_enabled?).size
+ end
+
+ size = group.projects.size
+
+ if lfs_status == size
+ 'for all projects'
+ else
+ "for #{lfs_status} out of #{pluralize(size, 'project')}"
+ end
+ end
+
+ def group_lfs_status(group)
+ status = group.lfs_enabled? ? 'enabled' : 'disabled'
+
+ content_tag(:span, class: "lfs-#{status}") do
+ "#{status.humanize} #{projects_lfs_status(group)}"
+ end
+ end
end
diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb
index 5d82abfca79..c15ecc8f86e 100644
--- a/app/helpers/lfs_helper.rb
+++ b/app/helpers/lfs_helper.rb
@@ -25,13 +25,21 @@ module LfsHelper
def lfs_download_access?
return false unless project.lfs_enabled?
- project.public? || ci? || (user && user.can?(:download_code, project))
+ project.public? || ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code?
+ end
+
+ def user_can_download_code?
+ has_authentication_ability?(:download_code) && can?(user, :download_code, project)
+ end
+
+ def build_can_download_code?
+ has_authentication_ability?(:build_download_code) && can?(user, :build_download_code, project)
end
def lfs_upload_access?
return false unless project.lfs_enabled?
- user && user.can?(:push_code, project)
+ has_authentication_ability?(:push_code) && can?(user, :push_code, project)
end
def render_lfs_forbidden
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index b3e6e468ecd..a11c313a6b8 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -35,6 +35,30 @@ module MilestonesHelper
milestone.issues.with_label(label.title).send(state).size
end
+ # Returns count of milestones for different states
+ # Uses explicit hash keys as the 'opened' state URL params differs from the db value
+ # and we need to add the total
+ def milestone_counts(milestones)
+ counts = milestones.reorder(nil).group(:state).count
+
+ {
+ opened: counts['active'] || 0,
+ closed: counts['closed'] || 0,
+ all: counts.values.sum || 0
+ }
+ end
+
+ # Show 'active' class if provided GET param matches check
+ # `or_blank` allows the function to return 'active' when given an empty param
+ # Could be refactored to be simpler but that may make it harder to read
+ def milestone_class_for_state(param, check, match_blank_param = false)
+ if match_blank_param
+ 'active' if param.blank? || param == check
+ else
+ 'active' if param == check
+ end
+ end
+
def milestone_progress_bar(milestone)
options = {
class: 'progress-bar progress-bar-success',
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index 94c6b548ecd..e0b8dc1393b 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -1,6 +1,9 @@
module NamespacesHelper
- def namespaces_options(selected = :current_user, display_path: false)
+ def namespaces_options(selected = :current_user, display_path: false, extra_group: nil)
groups = current_user.owned_groups + current_user.masters_groups
+
+ groups << extra_group if extra_group && !Group.exists?(name: extra_group.name)
+
users = [current_user.namespace]
data_attr_group = { 'data-options-parent' => 'groups' }
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index da230f76bae..b0331f36a2f 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -10,6 +10,10 @@ module NotesHelper
Ability.can_edit_note?(current_user, note)
end
+ def note_supports_slash_commands?(note)
+ Notes::SlashCommandsService.supported?(note, current_user)
+ end
+
def noteable_json(noteable)
{
id: noteable.id,
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 16a8e52a4ca..56477733ea2 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -27,7 +27,7 @@ module ProjectsHelper
author_html = ""
# Build avatar image tag
- author_html << image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt: '') if opts[:avatar]
+ author_html << image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]} #{opts[:avatar_class] if opts[:avatar_class]}", alt: '') if opts[:avatar]
# Build name span tag
if opts[:by_username]
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index e523c46e879..8a7446b7cc7 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -30,6 +30,37 @@ module SearchHelper
"Showing #{from} - #{to} of #{count} #{scope.humanize(capitalize: false)} for \"#{term}\""
end
+ def parse_search_result(result)
+ ref = nil
+ filename = nil
+ basename = nil
+ startline = 0
+
+ result.each_line.each_with_index do |line, index|
+ if line =~ /^.*:.*:\d+:/
+ ref, filename, startline = line.split(':')
+ startline = startline.to_i - index
+ extname = Regexp.escape(File.extname(filename))
+ basename = filename.sub(/#{extname}$/, '')
+ break
+ end
+ end
+
+ data = ""
+
+ result.each_line do |line|
+ data << line.sub(ref, '').sub(filename, '').sub(/^:-\d+-/, '').sub(/^::\d+:/, '')
+ end
+
+ OpenStruct.new(
+ filename: filename,
+ basename: basename,
+ ref: ref,
+ startline: startline,
+ data: data
+ )
+ end
+
private
# Autocomplete results for various settings pages
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index 0a5a8eb5aee..7e33a562077 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -1,10 +1,10 @@
module SnippetsHelper
- def reliable_snippet_path(snippet)
+ def reliable_snippet_path(snippet, opts = nil)
if snippet.project_id?
namespace_project_snippet_path(snippet.project.namespace,
- snippet.project, snippet)
+ snippet.project, snippet, opts)
else
- snippet_path(snippet)
+ snippet_path(snippet, opts)
end
end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 0cc709f68e4..9799f1dc886 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -108,6 +108,12 @@ class Notify < BaseMailer
headers["X-GitLab-#{model.class.name}-ID"] = model.id
headers['X-GitLab-Reply-Key'] = reply_key
+ if !@labels_url && @sent_notification && @sent_notification.unsubscribable?
+ headers['List-Unsubscribe'] = unsubscribe_sent_notification_url(@sent_notification, force: true)
+
+ @sent_notification_url = unsubscribe_sent_notification_url(@sent_notification)
+ end
+
if Gitlab::IncomingEmail.enabled?
address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
address.display_name = @project.name_with_namespace
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 12cc5aaafba..ab92e820335 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -22,6 +22,18 @@ class Blob < SimpleDelegator
new(blob)
end
+ # Returns the data of the blob.
+ #
+ # If the blob is a text based blob the content is converted to UTF-8 and any
+ # invalid byte sequences are replaced.
+ def data
+ if binary?
+ super
+ else
+ @data ||= super.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
+ end
+ end
+
def no_highlighting?
size && size > 1.megabyte
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index fb16bc06d71..522e2264bb8 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -1,5 +1,7 @@
module Ci
class Build < CommitStatus
+ include TokenAuthenticatable
+
belongs_to :runner, class_name: 'Ci::Runner'
belongs_to :trigger_request, class_name: 'Ci::TriggerRequest'
belongs_to :erased_by, class_name: 'User'
@@ -23,7 +25,10 @@ module Ci
acts_as_taggable
+ add_authentication_token_field :token
+
before_save :update_artifacts_size, if: :artifacts_file_changed?
+ before_save :ensure_token
before_destroy { project }
after_create :execute_hooks
@@ -38,6 +43,7 @@ module Ci
new_build.status = 'pending'
new_build.runner_id = nil
new_build.trigger_request_id = nil
+ new_build.token = nil
new_build.save
end
@@ -79,11 +85,14 @@ module Ci
after_transition any => [:success] do |build|
if build.environment.present?
- service = CreateDeploymentService.new(build.project, build.user,
- environment: build.environment,
- sha: build.sha,
- ref: build.ref,
- tag: build.tag)
+ service = CreateDeploymentService.new(
+ build.project, build.user,
+ environment: build.environment,
+ sha: build.sha,
+ ref: build.ref,
+ tag: build.tag,
+ options: build.options.to_h[:environment],
+ variables: build.variables)
service.execute(build)
end
end
@@ -173,7 +182,7 @@ module Ci
end
def repo_url
- auth = "gitlab-ci-token:#{token}@"
+ auth = "gitlab-ci-token:#{ensure_token!}@"
project.http_url_to_repo.sub(/^https?:\/\//) do |prefix|
prefix + auth
end
@@ -235,12 +244,7 @@ module Ci
end
def trace
- trace = raw_trace
- if project && trace.present? && project.runners_token.present?
- trace.gsub(project.runners_token, 'xxxxxx')
- else
- trace
- end
+ hide_secrets(raw_trace)
end
def trace_length
@@ -253,6 +257,7 @@ module Ci
def trace=(trace)
recreate_trace_dir
+ trace = hide_secrets(trace)
File.write(path_to_trace, trace)
end
@@ -266,6 +271,8 @@ module Ci
def append_trace(trace_part, offset)
recreate_trace_dir
+ trace_part = hide_secrets(trace_part)
+
File.truncate(path_to_trace, offset) if File.exist?(path_to_trace)
File.open(path_to_trace, 'ab') do |f|
f.write(trace_part)
@@ -341,12 +348,8 @@ module Ci
)
end
- def token
- project.runners_token
- end
-
def valid_token?(token)
- project.valid_runners_token?(token)
+ self.token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end
def has_tags?
@@ -488,5 +491,14 @@ module Ci
pipeline.config_processor.build_attributes(name)
end
+
+ def hide_secrets(trace)
+ return unless trace
+
+ trace = trace.dup
+ Ci::MaskSecret.mask!(trace, project.runners_token) if project
+ Ci::MaskSecret.mask!(trace, token)
+ trace
+ end
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 0b1df9f4294..663c5b1e231 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -2,6 +2,7 @@ module Ci
class Pipeline < ActiveRecord::Base
extend Ci::Model
include HasStatus
+ include Importable
self.table_name = 'ci_commits'
@@ -12,12 +13,12 @@ module Ci
has_many :builds, class_name: 'Ci::Build', foreign_key: :commit_id
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id
- validates_presence_of :sha
- validates_presence_of :ref
- validates_presence_of :status
- validate :valid_commit_sha
+ validates_presence_of :sha, unless: :importing?
+ validates_presence_of :ref, unless: :importing?
+ validates_presence_of :status, unless: :importing?
+ validate :valid_commit_sha, unless: :importing?
- after_save :keep_around_commits
+ after_save :keep_around_commits, unless: :importing?
delegate :stages, to: :statuses
@@ -55,6 +56,16 @@ module Ci
pipeline.finished_at = Time.now
end
+ after_transition [:created, :pending] => :running do |pipeline|
+ MergeRequest::Metrics.where(merge_request_id: pipeline.merge_requests.map(&:id)).
+ update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil)
+ end
+
+ after_transition any => [:success] do |pipeline|
+ MergeRequest::Metrics.where(merge_request_id: pipeline.merge_requests.map(&:id)).
+ update_all(latest_build_finished_at: pipeline.finished_at)
+ end
+
before_transition do |pipeline|
pipeline.update_duration
end
@@ -241,13 +252,16 @@ module Ci
end
def build_updated
- case latest_builds_status
- when 'pending' then enqueue
- when 'running' then run
- when 'success' then succeed
- when 'failed' then drop
- when 'canceled' then cancel
- when 'skipped' then skip
+ with_lock do
+ reload
+ case latest_builds_status
+ when 'pending' then enqueue
+ when 'running' then run
+ when 'success' then succeed
+ when 'failed' then drop
+ when 'canceled' then cancel
+ when 'skipped' then skip
+ end
end
end
@@ -276,6 +290,16 @@ module Ci
project.execute_services(data, :pipeline_hooks)
end
+ # Merge requests for which the current pipeline is running against
+ # the merge request's latest commit.
+ def merge_requests
+ @merge_requests ||=
+ begin
+ project.merge_requests.where(source_branch: self.ref).
+ select { |merge_request| merge_request.pipeline.try(:id) == self.id }
+ end
+ end
+
private
def pipeline_data
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index c9c47ec7419..6959223aed9 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -1,7 +1,7 @@
module Ci
class Variable < ActiveRecord::Base
extend Ci::Model
-
+
belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
validates_uniqueness_of :key, scope: :gl_project_id
@@ -11,7 +11,9 @@ module Ci
format: { with: /\A[a-zA-Z0-9_]+\z/,
message: "can contain only letters, digits and '_'." }
- attr_encrypted :value,
+ scope :order_key_asc, -> { reorder(key: :asc) }
+
+ attr_encrypted :value,
mode: :per_attribute_iv_and_salt,
insecure_mode: true,
key: Gitlab::Application.secrets.db_key_base,
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 4a628924499..736db1ab0f6 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -69,11 +69,9 @@ class CommitStatus < ActiveRecord::Base
commit_status.update_attributes finished_at: Time.now
end
- # We use around_transition to process pipeline on next stages as soon as possible, before the `after_*` is executed
- around_transition any => [:success, :failed, :canceled] do |commit_status, block|
- block.call
-
+ after_transition any => [:success, :failed, :canceled] do |commit_status|
commit_status.pipeline.try(:process!)
+ true
end
after_transition do |commit_status, transition|
@@ -95,6 +93,10 @@ class CommitStatus < ActiveRecord::Base
pipeline.before_sha || Gitlab::Git::BLANK_SHA
end
+ def group_name
+ name.gsub(/\d+[\s:\/\\]+\d+\s*/, '').strip
+ end
+
def self.stages
# We group by stage name, but order stages by theirs' index
unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').pluck('sg.stage')
@@ -113,6 +115,10 @@ class CommitStatus < ActiveRecord::Base
allow_failure? && (failed? || canceled?)
end
+ def playable?
+ false
+ end
+
def duration
calculate_duration
end
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index d8d4575bb4d..073ac4c1b65 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -71,6 +71,12 @@ module Awardable
end
end
+ def user_authored?(current_user)
+ author = self.respond_to?(:author) ? self.author : self.user
+
+ author == current_user
+ end
+
def awarded_emoji?(emoji_name, current_user)
award_emoji.where(name: emoji_name, user: current_user).exists?
end
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index f7b8352405c..0fa4df0fb56 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -8,8 +8,9 @@ module HasStatus
class_methods do
def status_sql
- scope = all.relevant
+ scope = all
builds = scope.select('count(*)').to_sql
+ created = scope.created.select('count(*)').to_sql
success = scope.success.select('count(*)').to_sql
ignored = scope.ignored.select('count(*)').to_sql if scope.respond_to?(:ignored)
ignored ||= '0'
@@ -19,12 +20,12 @@ module HasStatus
skipped = scope.skipped.select('count(*)').to_sql
deduce_status = "(CASE
- WHEN (#{builds})=0 THEN NULL
+ WHEN (#{builds})=(#{created}) THEN 'created'
WHEN (#{builds})=(#{skipped}) THEN 'skipped'
WHEN (#{builds})=(#{success})+(#{ignored})+(#{skipped}) THEN 'success'
- WHEN (#{builds})=(#{pending})+(#{skipped}) THEN 'pending'
+ WHEN (#{builds})=(#{created})+(#{pending})+(#{skipped}) THEN 'pending'
WHEN (#{builds})=(#{canceled})+(#{success})+(#{ignored})+(#{skipped}) THEN 'canceled'
- WHEN (#{running})+(#{pending})>0 THEN 'running'
+ WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running'
ELSE 'failed'
END)"
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 22231b2e0f0..ff465d2c745 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -28,10 +28,13 @@ module Issuable
loaded? && to_a.all? { |note| note.association(:award_emoji).loaded? }
end
end
+
has_many :label_links, as: :target, dependent: :destroy
has_many :labels, through: :label_links
has_many :todos, as: :target, dependent: :destroy
+ has_one :metrics
+
validates :author, presence: true
validates :title, presence: true, length: { within: 0..255 }
@@ -81,6 +84,7 @@ module Issuable
acts_as_paranoid
after_save :update_assignee_cache_counts, if: :assignee_id_changed?
+ after_save :record_metrics
def update_assignee_cache_counts
# make sure we flush the cache for both the old *and* new assignee
@@ -196,10 +200,6 @@ module Issuable
end
end
- def user_authored?(user)
- user == author
- end
-
def subscribed_without_subscriptions?(user)
participants(user).include?(user)
end
@@ -286,4 +286,9 @@ module Issuable
def can_move?(*)
false
end
+
+ def record_metrics
+ metrics = self.metrics || create_metrics
+ metrics.record!
+ end
end
diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb
new file mode 100644
index 00000000000..be295487fd2
--- /dev/null
+++ b/app/models/cycle_analytics.rb
@@ -0,0 +1,97 @@
+class CycleAnalytics
+ include Gitlab::Database::Median
+ include Gitlab::Database::DateTime
+
+ def initialize(project, from:)
+ @project = project
+ @from = from
+ end
+
+ def summary
+ @summary ||= Summary.new(@project, from: @from)
+ end
+
+ def issue
+ calculate_metric(:issue,
+ Issue.arel_table[:created_at],
+ [Issue::Metrics.arel_table[:first_associated_with_milestone_at],
+ Issue::Metrics.arel_table[:first_added_to_board_at]])
+ end
+
+ def plan
+ calculate_metric(:plan,
+ [Issue::Metrics.arel_table[:first_associated_with_milestone_at],
+ Issue::Metrics.arel_table[:first_added_to_board_at]],
+ Issue::Metrics.arel_table[:first_mentioned_in_commit_at])
+ end
+
+ def code
+ calculate_metric(:code,
+ Issue::Metrics.arel_table[:first_mentioned_in_commit_at],
+ MergeRequest.arel_table[:created_at])
+ end
+
+ def test
+ calculate_metric(:test,
+ MergeRequest::Metrics.arel_table[:latest_build_started_at],
+ MergeRequest::Metrics.arel_table[:latest_build_finished_at])
+ end
+
+ def review
+ calculate_metric(:review,
+ MergeRequest.arel_table[:created_at],
+ MergeRequest::Metrics.arel_table[:merged_at])
+ end
+
+ def staging
+ calculate_metric(:staging,
+ MergeRequest::Metrics.arel_table[:merged_at],
+ MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
+ end
+
+ def production
+ calculate_metric(:production,
+ Issue.arel_table[:created_at],
+ MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
+ end
+
+ private
+
+ def calculate_metric(name, start_time_attrs, end_time_attrs)
+ cte_table = Arel::Table.new("cte_table_for_#{name}")
+
+ # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
+ # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
+ # We compute the (end_time - start_time) interval, and give it an alias based on the current
+ # cycle analytics stage.
+ interval_query = Arel::Nodes::As.new(
+ cte_table,
+ subtract_datetimes(base_query, end_time_attrs, start_time_attrs, name.to_s))
+
+ median_datetime(cte_table, interval_query, name)
+ end
+
+ # Join table with a row for every <issue,merge_request> pair (where the merge request
+ # closes the given issue) with issue and merge request metrics included. The metrics
+ # are loaded with an inner join, so issues / merge requests without metrics are
+ # automatically excluded.
+ def base_query
+ arel_table = MergeRequestsClosingIssues.arel_table
+
+ # Load issues
+ query = arel_table.join(Issue.arel_table).on(Issue.arel_table[:id].eq(arel_table[:issue_id])).
+ join(Issue::Metrics.arel_table).on(Issue.arel_table[:id].eq(Issue::Metrics.arel_table[:issue_id])).
+ where(Issue.arel_table[:project_id].eq(@project.id)).
+ where(Issue.arel_table[:deleted_at].eq(nil)).
+ where(Issue.arel_table[:created_at].gteq(@from))
+
+ # Load merge_requests
+ query = query.join(MergeRequest.arel_table, Arel::Nodes::OuterJoin).
+ on(MergeRequest.arel_table[:id].eq(arel_table[:merge_request_id])).
+ join(MergeRequest::Metrics.arel_table).
+ on(MergeRequest.arel_table[:id].eq(MergeRequest::Metrics.arel_table[:merge_request_id]))
+
+ # Limit to merge requests that have been deployed to production after `@from`
+ query.where(MergeRequest::Metrics.arel_table[:first_deployed_to_production_at].gteq(@from))
+ end
+end
diff --git a/app/models/cycle_analytics/summary.rb b/app/models/cycle_analytics/summary.rb
new file mode 100644
index 00000000000..53b2cacb131
--- /dev/null
+++ b/app/models/cycle_analytics/summary.rb
@@ -0,0 +1,24 @@
+class CycleAnalytics
+ class Summary
+ def initialize(project, from:)
+ @project = project
+ @from = from
+ end
+
+ def new_issues
+ @project.issues.created_after(@from).count
+ end
+
+ def commits
+ repository = @project.repository.raw_repository
+
+ if @project.default_branch
+ repository.log(ref: @project.default_branch, after: @from).count
+ end
+ end
+
+ def deploys
+ @project.deployments.where("created_at > ?", @from).count
+ end
+ end
+end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 1e338889714..07d7e19e70d 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -42,4 +42,38 @@ class Deployment < ActiveRecord::Base
project.repository.is_ancestor?(commit.id, sha)
end
+
+ def update_merge_request_metrics!
+ return unless environment.update_merge_request_metrics?
+
+ merge_requests = project.merge_requests.
+ joins(:metrics).
+ where(target_branch: self.ref, merge_request_metrics: { first_deployed_to_production_at: nil }).
+ where("merge_request_metrics.merged_at <= ?", self.created_at)
+
+ if previous_deployment
+ merge_requests = merge_requests.where("merge_request_metrics.merged_at >= ?", previous_deployment.created_at)
+ end
+
+ # Need to use `map` instead of `select` because MySQL doesn't allow `SELECT`ing from the same table
+ # that we're updating.
+ merge_request_ids =
+ if Gitlab::Database.postgresql?
+ merge_requests.select(:id)
+ elsif Gitlab::Database.mysql?
+ merge_requests.map(&:id)
+ end
+
+ MergeRequest::Metrics.
+ where(merge_request_id: merge_request_ids, first_deployed_to_production_at: nil).
+ update_all(first_deployed_to_production_at: self.created_at)
+ end
+
+ def previous_deployment
+ @previous_deployment ||=
+ project.deployments.joins(:environment).
+ where(environments: { name: self.environment.name }, ref: self.ref).
+ where.not(id: self.id).
+ take
+ end
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 75e6f869786..49e0a20640c 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -4,6 +4,7 @@ class Environment < ActiveRecord::Base
has_many :deployments
before_validation :nullify_external_url
+ before_save :set_environment_type
validates :name,
presence: true,
@@ -26,9 +27,24 @@ class Environment < ActiveRecord::Base
self.external_url = nil if self.external_url.blank?
end
+ def set_environment_type
+ names = name.split('/')
+
+ self.environment_type =
+ if names.many?
+ names.first
+ else
+ nil
+ end
+ end
+
def includes_commit?(commit)
return false unless last_deployment
last_deployment.includes_commit?(commit)
end
+
+ def update_merge_request_metrics?
+ self.name == "production"
+ end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index a0b7b0dc2b5..55a76e26f3c 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -13,6 +13,8 @@ class Event < ActiveRecord::Base
LEFT = 9 # User left project
DESTROYED = 10
+ RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
+
delegate :name, :email, to: :author, prefix: true, allow_nil: true
delegate :title, to: :issue, prefix: true, allow_nil: true
delegate :title, to: :merge_request, prefix: true, allow_nil: true
@@ -324,8 +326,27 @@ class Event < ActiveRecord::Base
end
def reset_project_activity
- if project && Gitlab::ExclusiveLease.new("project:update_last_activity_at:#{project.id}", timeout: 60).try_obtain
- project.update_column(:last_activity_at, self.created_at)
- end
+ return unless project
+
+ # Don't even bother obtaining a lock if the last update happened less than
+ # 60 minutes ago.
+ return if recent_update?
+
+ return unless try_obtain_lease
+
+ project.update_column(:last_activity_at, created_at)
+ end
+
+ private
+
+ def recent_update?
+ project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago
+ end
+
+ def try_obtain_lease
+ Gitlab::ExclusiveLease.
+ new("project:update_last_activity_at:#{project.id}",
+ timeout: RESET_PROJECT_ACTIVITY_INTERVAL.to_i).
+ try_obtain
end
end
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index da7c265a371..bda2b5c5d5d 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -8,7 +8,8 @@ class GlobalMilestone
milestones = milestones.group_by(&:title)
milestones.map do |title, milestones|
- new(title, milestones)
+ milestones_relation = Milestone.where(id: milestones.map(&:id))
+ new(title, milestones_relation)
end
end
@@ -31,7 +32,7 @@ class GlobalMilestone
end
def projects
- @projects ||= Project.for_milestones(milestones.map(&:id))
+ @projects ||= Project.for_milestones(milestones.select(:id))
end
def state
@@ -53,19 +54,19 @@ class GlobalMilestone
end
def issues
- @issues ||= Issue.of_milestones(milestones.map(&:id)).includes(:project)
+ @issues ||= Issue.of_milestones(milestones.select(:id)).includes(:project, :assignee, :labels)
end
def merge_requests
- @merge_requests ||= MergeRequest.of_milestones(milestones.map(&:id)).includes(:target_project)
+ @merge_requests ||= MergeRequest.of_milestones(milestones.select(:id)).includes(:target_project, :assignee, :labels)
end
def participants
- @participants ||= milestones.map(&:participants).flatten.compact.uniq
+ @participants ||= milestones.includes(:participants).map(&:participants).flatten.compact.uniq
end
def labels
- @labels ||= GlobalLabel.build_collection(milestones.map(&:labels).flatten)
+ @labels ||= GlobalLabel.build_collection(milestones.includes(:labels).map(&:labels).flatten)
.sort_by!(&:title)
end
diff --git a/app/models/group.rb b/app/models/group.rb
index c48869ae465..aefb94b2ada 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -95,6 +95,13 @@ class Group < Namespace
end
end
+ def lfs_enabled?
+ return false unless Gitlab.config.lfs.enabled
+ return Gitlab.config.lfs.enabled if self[:lfs_enabled].nil?
+
+ self[:lfs_enabled]
+ end
+
def add_users(user_ids, access_level, current_user: nil, expires_at: nil)
user_ids.each do |user_id|
Member.add_user(
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 788611305fe..abd58e0454a 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -23,6 +23,8 @@ class Issue < ActiveRecord::Base
has_many :events, as: :target, dependent: :destroy
+ has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all
+
validates :project, presence: true
scope :cared, ->(user) { where(assignee_id: user) }
@@ -36,6 +38,8 @@ class Issue < ActiveRecord::Base
scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
+ scope :created_after, -> (datetime) { where("created_at >= ?", datetime) }
+
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
diff --git a/app/models/issue/metrics.rb b/app/models/issue/metrics.rb
new file mode 100644
index 00000000000..012d545c440
--- /dev/null
+++ b/app/models/issue/metrics.rb
@@ -0,0 +1,21 @@
+class Issue::Metrics < ActiveRecord::Base
+ belongs_to :issue
+
+ def record!
+ if issue.milestone_id.present? && self.first_associated_with_milestone_at.blank?
+ self.first_associated_with_milestone_at = Time.now
+ end
+
+ if issue_assigned_to_list_label? && self.first_added_to_board_at.blank?
+ self.first_added_to_board_at = Time.now
+ end
+
+ self.save
+ end
+
+ private
+
+ def issue_assigned_to_list_label?
+ issue.labels.any? { |label| label.lists.present? }
+ end
+end
diff --git a/app/models/member.rb b/app/models/member.rb
index 64e0d33fb20..69406379948 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -28,17 +28,34 @@ class Member < ActiveRecord::Base
allow_nil: true
}
+ # This scope encapsulates (most of) the conditions a row in the member table
+ # must satisfy if it is a valid permission. Of particular note:
+ #
+ # * Access requests must be excluded
+ # * Blocked users must be excluded
+ # * Invitations take effect immediately
+ # * expires_at is not implemented. A background worker purges expired rows
+ scope :active, -> do
+ is_external_invite = arel_table[:user_id].eq(nil).and(arel_table[:invite_token].not_eq(nil))
+ user_is_active = User.arel_table[:state].eq(:active)
+
+ includes(:user).references(:users)
+ .where(is_external_invite.or(user_is_active))
+ .where(requested_at: nil)
+ end
+
scope :invite, -> { where.not(invite_token: nil) }
scope :non_invite, -> { where(invite_token: nil) }
scope :request, -> { where.not(requested_at: nil) }
- scope :has_access, -> { where('access_level > 0') }
-
- scope :guests, -> { where(access_level: GUEST) }
- scope :reporters, -> { where(access_level: REPORTER) }
- scope :developers, -> { where(access_level: DEVELOPER) }
- scope :masters, -> { where(access_level: MASTER) }
- scope :owners, -> { where(access_level: OWNER) }
- scope :owners_and_masters, -> { where(access_level: [OWNER, MASTER]) }
+
+ scope :has_access, -> { active.where('access_level > 0') }
+
+ scope :guests, -> { active.where(access_level: GUEST) }
+ scope :reporters, -> { active.where(access_level: REPORTER) }
+ scope :developers, -> { active.where(access_level: DEVELOPER) }
+ scope :masters, -> { active.where(access_level: MASTER) }
+ scope :owners, -> { active.where(access_level: OWNER) }
+ scope :owners_and_masters, -> { active.where(access_level: [OWNER, MASTER]) }
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index f7d1253d957..2dcf7f89bfc 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -16,6 +16,8 @@ class MergeRequest < ActiveRecord::Base
has_many :events, as: :target, dependent: :destroy
+ has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all
+
serialize :merge_params, Hash
after_create :ensure_merge_request_diff, unless: :importing?
@@ -501,6 +503,19 @@ class MergeRequest < ActiveRecord::Base
target_project
end
+ # If the merge request closes any issues, save this information in the
+ # `MergeRequestsClosingIssues` model. This is a performance optimization.
+ # Calculating this information for a number of merge requests requires
+ # running `ReferenceExtractor` on each of them separately.
+ def cache_merge_request_closes_issues!(current_user = self.author)
+ transaction do
+ self.merge_requests_closing_issues.delete_all
+ closes_issues(current_user).each do |issue|
+ self.merge_requests_closing_issues.create!(issue: issue)
+ end
+ end
+ end
+
def closes_issue?(issue)
closes_issues.include?(issue)
end
@@ -508,7 +523,8 @@ class MergeRequest < ActiveRecord::Base
# Return the set of issues that will be closed if this merge request is accepted.
def closes_issues(current_user = self.author)
if target_branch == project.default_branch
- messages = commits.map(&:safe_message) << description
+ messages = [description]
+ messages.concat(commits.map(&:safe_message)) if merge_request_diff
Gitlab::ClosingIssueExtractor.new(project, current_user).
closed_by_message(messages.join("\n"))
@@ -652,11 +668,14 @@ class MergeRequest < ActiveRecord::Base
end
def environments
- return unless diff_head_commit
+ return [] unless diff_head_commit
- target_project.environments.select do |environment|
- environment.includes_commit?(diff_head_commit)
- end
+ environments = source_project.environments_for(
+ source_branch, diff_head_commit)
+ environments += target_project.environments_for(
+ target_branch, diff_head_commit, with_tags: true)
+
+ environments.uniq
end
def state_human_name
@@ -745,10 +764,23 @@ class MergeRequest < ActiveRecord::Base
end
def all_pipelines
- @all_pipelines ||=
- if diff_head_sha && source_project
- source_project.pipelines.order(id: :desc).where(sha: commits_sha, ref: source_branch)
- end
+ return unless source_project
+
+ @all_pipelines ||= begin
+ sha = if persisted?
+ all_commits_sha
+ else
+ diff_head_sha
+ end
+
+ source_project.pipelines.order(id: :desc).
+ where(sha: sha, ref: source_branch)
+ end
+ end
+
+ # Note that this could also return SHA from now dangling commits
+ def all_commits_sha
+ merge_request_diffs.flat_map(&:commits_sha).uniq
end
def merge_commit
diff --git a/app/models/merge_request/metrics.rb b/app/models/merge_request/metrics.rb
new file mode 100644
index 00000000000..99c49a020c9
--- /dev/null
+++ b/app/models/merge_request/metrics.rb
@@ -0,0 +1,11 @@
+class MergeRequest::Metrics < ActiveRecord::Base
+ belongs_to :merge_request
+
+ def record!
+ if merge_request.merged? && self.merged_at.blank?
+ self.merged_at = Time.now
+ end
+
+ self.save
+ end
+end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 18c583add88..36b8b70870b 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -30,6 +30,10 @@ class MergeRequestDiff < ActiveRecord::Base
select(column_names - ['st_diffs'])
end
+ def st_commits
+ super || []
+ end
+
# Collect information about commits and diff from repository
# and save it to the database as serialized data
def save_git_content
@@ -83,7 +87,7 @@ class MergeRequestDiff < ActiveRecord::Base
end
def commits
- @commits ||= load_commits(st_commits || [])
+ @commits ||= load_commits(st_commits)
end
def reload_commits
@@ -117,6 +121,14 @@ class MergeRequestDiff < ActiveRecord::Base
project.commit(head_commit_sha)
end
+ def commits_sha
+ if @commits
+ commits.map(&:sha)
+ else
+ st_commits.map { |commit| commit[:id] }
+ end
+ end
+
def diff_refs
return unless start_commit_sha || base_commit_sha
diff --git a/app/models/merge_requests_closing_issues.rb b/app/models/merge_requests_closing_issues.rb
new file mode 100644
index 00000000000..ab597c37947
--- /dev/null
+++ b/app/models/merge_requests_closing_issues.rb
@@ -0,0 +1,7 @@
+class MergeRequestsClosingIssues < ActiveRecord::Base
+ belongs_to :merge_request
+ belongs_to :issue
+
+ validates :merge_request_id, uniqueness: { scope: :issue_id }, presence: true
+ validates :issue_id, presence: true
+end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 7c29d27ce97..919b3b1f095 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -141,6 +141,11 @@ class Namespace < ActiveRecord::Base
projects.joins(:forked_project_link).find_by('forked_project_links.forked_from_project_id = ?', project.id)
end
+ def lfs_enabled?
+ # User namespace will always default to the global setting
+ Gitlab.config.lfs.enabled
+ end
+
private
def repository_storage_paths
diff --git a/app/models/note.rb b/app/models/note.rb
index b94e3cff2ce..f2656df028b 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -223,10 +223,6 @@ class Note < ActiveRecord::Base
end
end
- def user_authored?(user)
- user == author
- end
-
def award_emoji?
can_be_award_emoji? && contains_emoji_only?
end
diff --git a/app/models/project.rb b/app/models/project.rb
index f3f3ffbbd28..7265cb55594 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -393,10 +393,9 @@ class Project < ActiveRecord::Base
end
def lfs_enabled?
- return false unless Gitlab.config.lfs.enabled
- return Gitlab.config.lfs.enabled if self[:lfs_enabled].nil?
+ return namespace.lfs_enabled? if self[:lfs_enabled].nil?
- self[:lfs_enabled]
+ self[:lfs_enabled] && Gitlab.config.lfs.enabled
end
def repository_storage_path
@@ -1138,12 +1137,6 @@ class Project < ActiveRecord::Base
self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end
- # TODO (ayufan): For now we use runners_token (backward compatibility)
- # In 8.4 every build will have its own individual token valid for time of build
- def valid_build_token?(token)
- self.builds_enabled? && self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
- end
-
def build_coverage_enabled?
build_coverage_regex.present?
end
@@ -1300,6 +1293,22 @@ class Project < ActiveRecord::Base
Gitlab::Redis.with { |redis| redis.del(pushes_since_gc_redis_key) }
end
+ def environments_for(ref, commit, with_tags: false)
+ environment_ids = deployments.group(:environment_id).
+ select(:environment_id)
+
+ environment_ids =
+ if with_tags
+ environment_ids.where('ref=? OR tag IS TRUE', ref)
+ else
+ environment_ids.where(ref: ref)
+ end
+
+ environments.where(id: environment_ids).select do |environment|
+ environment.includes_commit?(commit)
+ end
+ end
+
private
def pushes_since_gc_redis_key
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 9c602c582bd..8c9534c3565 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -22,6 +22,12 @@ class ProjectFeature < ActiveRecord::Base
belongs_to :project
+ default_value_for :builds_access_level, value: ENABLED, allows_nil: false
+ default_value_for :issues_access_level, value: ENABLED, allows_nil: false
+ default_value_for :merge_requests_access_level, value: ENABLED, allows_nil: false
+ default_value_for :snippets_access_level, value: ENABLED, allows_nil: false
+ default_value_for :wiki_access_level, value: ENABLED, allows_nil: false
+
def feature_available?(feature, user)
raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature)
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index ab6ea2aae36..d9ce5088903 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -163,7 +163,7 @@ class ProjectTeam
# Each group produces a list of maximum access level per user. We take the
# max of the values produced by each group.
- if project.invited_groups.any? && project.allowed_to_share_with_group?
+ if project_shared_with_group?
project.project_group_links.each do |group_link|
invited_access = max_invited_level_for_users(group_link, user_ids)
merge_max!(access, invited_access)
@@ -200,43 +200,17 @@ class ProjectTeam
def fetch_members(level = nil)
project_members = project.members
group_members = group ? group.members : []
- invited_members = []
-
- if project.invited_groups.any? && project.allowed_to_share_with_group?
- project.project_group_links.includes(group: [:group_members]).each do |group_link|
- invited_group = group_link.group
- im = invited_group.members
-
- if level
- int_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
-
- # Skip group members if we ask for masters
- # but max group access is developers
- next if int_level > group_link.group_access
-
- # If we ask for developers and max
- # group access is developers we need to provide
- # both group master, developers as devs
- if int_level == group_link.group_access
- im.where("access_level >= ?)", group_link.group_access)
- else
- im.send(level)
- end
- end
-
- invited_members << im
- end
-
- invited_members = invited_members.flatten.compact
- end
if level
- project_members = project_members.send(level)
- group_members = group_members.send(level) if group
+ project_members = project_members.public_send(level)
+ group_members = group_members.public_send(level) if group
end
user_ids = project_members.pluck(:user_id)
+
+ invited_members = fetch_invited_members(level)
user_ids.push(*invited_members.map(&:user_id)) if invited_members.any?
+
user_ids.push(*group_members.pluck(:user_id)) if group
User.where(id: user_ids)
@@ -249,4 +223,38 @@ class ProjectTeam
def merge_max!(first_hash, second_hash)
first_hash.merge!(second_hash) { |_key, old, new| old > new ? old : new }
end
+
+ def project_shared_with_group?
+ project.invited_groups.any? && project.allowed_to_share_with_group?
+ end
+
+ def fetch_invited_members(level = nil)
+ invited_members = []
+
+ return invited_members unless project_shared_with_group?
+
+ project.project_group_links.includes(group: [:group_members]).each do |link|
+ invited_group_members = link.group.members
+
+ if level
+ numeric_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
+
+ # If we're asked for a level that's higher than the group's access,
+ # there's nothing left to do
+ next if numeric_level > link.group_access
+
+ # Make sure we include everyone _above_ the requested level as well
+ invited_group_members =
+ if numeric_level == link.group_access
+ invited_group_members.where("access_level >= ?", link.group_access)
+ else
+ invited_group_members.public_send(level)
+ end
+ end
+
+ invited_members << invited_group_members
+ end
+
+ invited_members.flatten.compact
+ end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 3c354c25c6f..51557228ab9 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -756,62 +756,59 @@ class Repository
@root_ref ||= cache.fetch(:root_ref) { raw_repository.root_ref }
end
- def commit_dir(user, path, message, branch)
+ def commit_dir(user, path, message, branch, author_email: nil, author_name: nil)
update_branch_with_hooks(user, branch) do |ref|
- committer = user_to_committer(user)
- options = {}
- options[:committer] = committer
- options[:author] = committer
-
- options[:commit] = {
- message: message,
- branch: ref,
- update_ref: false,
+ options = {
+ commit: {
+ branch: ref,
+ message: message,
+ update_ref: false
+ }
}
+ options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
+
raw_repository.mkdir(path, options)
end
end
- def commit_file(user, path, content, message, branch, update)
+ def commit_file(user, path, content, message, branch, update, author_email: nil, author_name: nil)
update_branch_with_hooks(user, branch) do |ref|
- committer = user_to_committer(user)
- options = {}
- options[:committer] = committer
- options[:author] = committer
- options[:commit] = {
- message: message,
- branch: ref,
- update_ref: false,
+ options = {
+ commit: {
+ branch: ref,
+ message: message,
+ update_ref: false
+ },
+ file: {
+ content: content,
+ path: path,
+ update: update
+ }
}
- options[:file] = {
- content: content,
- path: path,
- update: update
- }
+ options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
Gitlab::Git::Blob.commit(raw_repository, options)
end
end
- def update_file(user, path, content, branch:, previous_path:, message:)
+ def update_file(user, path, content, branch:, previous_path:, message:, author_email: nil, author_name: nil)
update_branch_with_hooks(user, branch) do |ref|
- committer = user_to_committer(user)
- options = {}
- options[:committer] = committer
- options[:author] = committer
- options[:commit] = {
- message: message,
- branch: ref,
- update_ref: false
+ options = {
+ commit: {
+ branch: ref,
+ message: message,
+ update_ref: false
+ },
+ file: {
+ content: content,
+ path: path,
+ update: true
+ }
}
- options[:file] = {
- content: content,
- path: path,
- update: true
- }
+ options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
if previous_path && previous_path != path
options[:file][:previous_path] = previous_path
@@ -822,34 +819,39 @@ class Repository
end
end
- def remove_file(user, path, message, branch)
+ def remove_file(user, path, message, branch, author_email: nil, author_name: nil)
update_branch_with_hooks(user, branch) do |ref|
- committer = user_to_committer(user)
- options = {}
- options[:committer] = committer
- options[:author] = committer
- options[:commit] = {
- message: message,
- branch: ref,
- update_ref: false,
+ options = {
+ commit: {
+ branch: ref,
+ message: message,
+ update_ref: false
+ },
+ file: {
+ path: path
+ }
}
- options[:file] = {
- path: path
- }
+ options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
Gitlab::Git::Blob.remove(raw_repository, options)
end
end
- def user_to_committer(user)
+ def get_committer_and_author(user, email: nil, name: nil)
+ committer = user_to_committer(user)
+ author = Gitlab::Git::committer_hash(email: email, name: name) || committer
+
{
- email: user.email,
- name: user.name,
- time: Time.now
+ author: author,
+ committer: committer
}
end
+ def user_to_committer(user)
+ Gitlab::Git::committer_hash(email: user.email, name: user.name)
+ end
+
def can_be_merged?(source_sha, target_branch)
our_commit = rugged.branches[target_branch].target
their_commit = rugged.lookup(source_sha)
@@ -990,37 +992,6 @@ class Repository
Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
end
- def parse_search_result(result)
- ref = nil
- filename = nil
- basename = nil
- startline = 0
-
- result.each_line.each_with_index do |line, index|
- if line =~ /^.*:.*:\d+:/
- ref, filename, startline = line.split(':')
- startline = startline.to_i - index
- extname = Regexp.escape(File.extname(filename))
- basename = filename.sub(/#{extname}$/, '')
- break
- end
- end
-
- data = ""
-
- result.each_line do |line|
- data << line.sub(ref, '').sub(filename, '').sub(/^:-\d+-/, '').sub(/^::\d+:/, '')
- end
-
- OpenStruct.new(
- filename: filename,
- basename: basename,
- ref: ref,
- startline: startline,
- data: data
- )
- end
-
def fetch_ref(source_path, source_ref, target_ref)
args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
Gitlab::Popen.popen(args, path_to_repo)
@@ -1048,7 +1019,7 @@ class Repository
GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do
update_ref!(ref, newrev, oldrev)
-
+
if was_empty || !target_branch
# If repo was empty expire cache
after_create if was_empty
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 5ec933601ac..8a1730f3f36 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -4,6 +4,7 @@ class Snippet < ActiveRecord::Base
include Participable
include Referable
include Sortable
+ include Awardable
default_value_for :visibility_level, Snippet::PRIVATE
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index acf36d422d1..be25c750d67 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -46,6 +46,7 @@ class ProjectPolicy < BasePolicy
can! :create_issue
can! :create_note
can! :upload_file
+ can! :read_cycle_analytics
end
def reporter_access!
@@ -64,6 +65,12 @@ class ProjectPolicy < BasePolicy
can! :read_deployment
end
+ # Permissions given when an user is team member of a project
+ def team_member_reporter_access!
+ can! :build_download_code
+ can! :build_read_container_image
+ end
+
def developer_access!
can! :admin_merge_request
can! :update_merge_request
@@ -109,6 +116,8 @@ class ProjectPolicy < BasePolicy
can! :read_commit_status
can! :read_pipeline
can! :read_container_image
+ can! :build_download_code
+ can! :build_read_container_image
end
def owner_access!
@@ -130,10 +139,11 @@ class ProjectPolicy < BasePolicy
def team_access!(user)
access = project.team.max_member_access(user.id)
- guest_access! if access >= Gitlab::Access::GUEST
- reporter_access! if access >= Gitlab::Access::REPORTER
- developer_access! if access >= Gitlab::Access::DEVELOPER
- master_access! if access >= Gitlab::Access::MASTER
+ guest_access! if access >= Gitlab::Access::GUEST
+ reporter_access! if access >= Gitlab::Access::REPORTER
+ team_member_reporter_access! if access >= Gitlab::Access::REPORTER
+ developer_access! if access >= Gitlab::Access::DEVELOPER
+ master_access! if access >= Gitlab::Access::MASTER
end
def archived_access!
@@ -195,6 +205,7 @@ class ProjectPolicy < BasePolicy
can! :read_commit_status
can! :read_container_image
can! :download_code
+ can! :read_cycle_analytics
# NOTE: may be overridden by IssuePolicy
can! :read_issue
diff --git a/app/services/akismet_service.rb b/app/services/akismet_service.rb
index 5c60addbe7c..76b9f1feda7 100644
--- a/app/services/akismet_service.rb
+++ b/app/services/akismet_service.rb
@@ -29,25 +29,25 @@ class AkismetService
end
def submit_ham
- return false unless akismet_enabled?
+ submit(:ham)
+ end
- params = {
- type: 'comment',
- text: text,
- author: owner.name,
- author_email: owner.email
- }
+ def submit_spam
+ submit(:spam)
+ end
- begin
- akismet_client.submit_ham(options[:ip_address], options[:user_agent], params)
- true
- rescue => e
- Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!")
- false
- end
+ private
+
+ def akismet_client
+ @akismet_client ||= ::Akismet::Client.new(current_application_settings.akismet_api_key,
+ Gitlab.config.gitlab.url)
end
- def submit_spam
+ def akismet_enabled?
+ current_application_settings.akismet_enabled
+ end
+
+ def submit(type)
return false unless akismet_enabled?
params = {
@@ -58,22 +58,11 @@ class AkismetService
}
begin
- akismet_client.submit_spam(options[:ip_address], options[:user_agent], params)
+ akismet_client.public_send(type, options[:ip_address], options[:user_agent], params)
true
rescue => e
Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!")
false
end
end
-
- private
-
- def akismet_client
- @akismet_client ||= ::Akismet::Client.new(current_application_settings.akismet_api_key,
- Gitlab.config.gitlab.url)
- end
-
- def akismet_enabled?
- current_application_settings.akismet_enabled
- end
end
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 6072123b851..38ac6631228 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -4,7 +4,9 @@ module Auth
AUDIENCE = 'container_registry'
- def execute
+ def execute(authentication_abilities:)
+ @authentication_abilities = authentication_abilities
+
return error('not found', 404) unless registry.enabled
unless current_user || project
@@ -74,9 +76,9 @@ module Auth
case requested_action
when 'pull'
- requested_project == project || can?(current_user, :read_container_image, requested_project)
+ requested_project.public? || build_can_pull?(requested_project) || user_can_pull?(requested_project)
when 'push'
- requested_project == project || can?(current_user, :create_container_image, requested_project)
+ build_can_push?(requested_project) || user_can_push?(requested_project)
else
false
end
@@ -85,5 +87,29 @@ module Auth
def registry
Gitlab.config.registry
end
+
+ def build_can_pull?(requested_project)
+ # Build can:
+ # 1. pull from its own project (for ex. a build)
+ # 2. read images from dependent projects if creator of build is a team member
+ @authentication_abilities.include?(:build_read_container_image) &&
+ (requested_project == project || can?(current_user, :build_read_container_image, requested_project))
+ end
+
+ def user_can_pull?(requested_project)
+ @authentication_abilities.include?(:read_container_image) &&
+ can?(current_user, :read_container_image, requested_project)
+ end
+
+ def build_can_push?(requested_project)
+ # Build can push only to the project from which it originates
+ @authentication_abilities.include?(:build_create_container_image) &&
+ requested_project == project
+ end
+
+ def user_can_push?(requested_project)
+ @authentication_abilities.include?(:create_container_image) &&
+ can?(current_user, :create_container_image, requested_project)
+ end
end
end
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index ed73d8cb8c2..1c82599c579 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -16,11 +16,29 @@ module Commits
error(ex.message)
end
+ private
+
def commit
raise NotImplementedError
end
- private
+ def commit_change(action)
+ raise NotImplementedError unless repository.respond_to?(action)
+
+ into = @create_merge_request ? @commit.public_send("#{action}_branch_name") : @target_branch
+ tree_id = repository.public_send("check_#{action}_content", @commit, @target_branch)
+
+ if tree_id
+ create_target_branch(into) if @create_merge_request
+
+ repository.public_send(action, current_user, @commit, into, tree_id)
+ success
+ else
+ error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title} automatically.
+ It may have already been #{action.to_s.dasherize}, or a more recent commit may have updated some of its content."
+ raise ChangeError, error_msg
+ end
+ end
def check_push_permissions
allowed = ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(@target_branch)
diff --git a/app/services/commits/cherry_pick_service.rb b/app/services/commits/cherry_pick_service.rb
index f9a4efa7182..605cca36f9c 100644
--- a/app/services/commits/cherry_pick_service.rb
+++ b/app/services/commits/cherry_pick_service.rb
@@ -1,19 +1,7 @@
module Commits
class CherryPickService < ChangeService
def commit
- cherry_pick_into = @create_merge_request ? @commit.cherry_pick_branch_name : @target_branch
- cherry_pick_tree_id = repository.check_cherry_pick_content(@commit, @target_branch)
-
- if cherry_pick_tree_id
- create_target_branch(cherry_pick_into) if @create_merge_request
-
- repository.cherry_pick(current_user, @commit, cherry_pick_into, cherry_pick_tree_id)
- success
- else
- error_msg = "Sorry, we cannot cherry-pick this #{@commit.change_type_title} automatically.
- It may have already been cherry-picked, or a more recent commit may have updated some of its content."
- raise ChangeError, error_msg
- end
+ commit_change(:cherry_pick)
end
end
end
diff --git a/app/services/commits/revert_service.rb b/app/services/commits/revert_service.rb
index c7de9f6f35e..addd55cb32f 100644
--- a/app/services/commits/revert_service.rb
+++ b/app/services/commits/revert_service.rb
@@ -1,19 +1,7 @@
module Commits
class RevertService < ChangeService
def commit
- revert_into = @create_merge_request ? @commit.revert_branch_name : @target_branch
- revert_tree_id = repository.check_revert_content(@commit, @target_branch)
-
- if revert_tree_id
- create_target_branch(revert_into) if @create_merge_request
-
- repository.revert(current_user, @commit, revert_into, revert_tree_id)
- success
- else
- error_msg = "Sorry, we cannot revert this #{@commit.change_type_title} automatically.
- It may have already been reverted, or a more recent commit may have updated some of its content."
- raise ChangeError, error_msg
- end
+ commit_change(:revert)
end
end
end
diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb
index efeb9df9527..799ad3e1bd0 100644
--- a/app/services/create_deployment_service.rb
+++ b/app/services/create_deployment_service.rb
@@ -2,11 +2,9 @@ require_relative 'base_service'
class CreateDeploymentService < BaseService
def execute(deployable = nil)
- environment = project.environments.find_or_create_by(
- name: params[:environment]
- )
+ environment = find_or_create_environment
- project.deployments.create(
+ deployment = project.deployments.create(
environment: environment,
ref: params[:ref],
tag: params[:tag],
@@ -14,5 +12,43 @@ class CreateDeploymentService < BaseService
user: current_user,
deployable: deployable
)
+
+ deployment.update_merge_request_metrics!
+
+ deployment
+ end
+
+ private
+
+ def find_or_create_environment
+ project.environments.find_or_create_by(name: expanded_name) do |environment|
+ environment.external_url = expanded_url
+ end
+ end
+
+ def expanded_name
+ ExpandVariables.expand(name, variables)
+ end
+
+ def expanded_url
+ return unless url
+
+ @expanded_url ||= ExpandVariables.expand(url, variables)
+ end
+
+ def name
+ params[:environment]
+ end
+
+ def url
+ options[:url]
+ end
+
+ def options
+ params[:options] || {}
+ end
+
+ def variables
+ params[:variables] || []
end
end
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index ea94818713b..e8465729d06 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -16,6 +16,8 @@ module Files
params[:file_content]
end
@last_commit_sha = params[:last_commit_sha]
+ @author_email = params[:author_email]
+ @author_name = params[:author_name]
# Validate parameters
validate
diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb
index 6107254a34e..d00d78cee7e 100644
--- a/app/services/files/create_dir_service.rb
+++ b/app/services/files/create_dir_service.rb
@@ -3,7 +3,7 @@ require_relative "base_service"
module Files
class CreateDirService < Files::BaseService
def commit
- repository.commit_dir(current_user, @file_path, @commit_message, @target_branch)
+ repository.commit_dir(current_user, @file_path, @commit_message, @target_branch, author_email: @author_email, author_name: @author_name)
end
def validate
diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb
index 8eaf6db8012..bf127843d55 100644
--- a/app/services/files/create_service.rb
+++ b/app/services/files/create_service.rb
@@ -3,7 +3,7 @@ require_relative "base_service"
module Files
class CreateService < Files::BaseService
def commit
- repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch, false)
+ repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch, false, author_email: @author_email, author_name: @author_name)
end
def validate
diff --git a/app/services/files/delete_service.rb b/app/services/files/delete_service.rb
index 27c881c3430..8b27ad51789 100644
--- a/app/services/files/delete_service.rb
+++ b/app/services/files/delete_service.rb
@@ -3,7 +3,7 @@ require_relative "base_service"
module Files
class DeleteService < Files::BaseService
def commit
- repository.remove_file(current_user, @file_path, @commit_message, @target_branch)
+ repository.remove_file(current_user, @file_path, @commit_message, @target_branch, author_email: @author_email, author_name: @author_name)
end
end
end
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index 4fc3b640799..9e9b5b63f26 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -8,7 +8,9 @@ module Files
repository.update_file(current_user, @file_path, @file_content,
branch: @target_branch,
previous_path: @previous_path,
- message: @commit_message)
+ message: @commit_message,
+ author_email: @author_email,
+ author_name: @author_name)
end
private
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index 78feb37aa2a..c499427605a 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -87,7 +87,7 @@ class GitPushService < BaseService
project.change_head(branch_name)
# Set protection on the default branch if configured
- if current_application_settings.default_branch_protection != PROTECTION_NONE
+ if current_application_settings.default_branch_protection != PROTECTION_NONE && !@project.protected_branch?(@project.default_branch)
params = {
name: @project.default_branch,
@@ -134,6 +134,7 @@ class GitPushService < BaseService
end
commit.create_cross_references!(authors[commit], closed_issues)
+ update_issue_metrics(commit, authors)
end
end
@@ -186,4 +187,11 @@ class GitPushService < BaseService
def branch_name
@branch_name ||= Gitlab::Git.ref_name(params[:ref])
end
+
+ def update_issue_metrics(commit, authors)
+ mentioned_issues = commit.all_references(authors[commit]).issues
+
+ Issue::Metrics.where(issue_id: mentioned_issues.map(&:id), first_mentioned_in_commit_at: nil).
+ update_all(first_mentioned_in_commit_at: commit.committed_date)
+ end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 4c8d93999a7..fbce46769f7 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -157,6 +157,10 @@ class IssuableBaseService < BaseService
# To be overridden by subclasses
end
+ def after_update(issuable)
+ # To be overridden by subclasses
+ end
+
def update_issuable(issuable, attributes)
issuable.with_transaction_returning_status do
issuable.update(attributes.merge(updated_by: current_user))
@@ -182,6 +186,7 @@ class IssuableBaseService < BaseService
end
handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users)
+ after_update(issuable)
issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update')
end
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 73247e62421..b0ae2dfe4ce 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -20,6 +20,7 @@ module MergeRequests
event_service.open_mr(issuable, current_user)
notification_service.new_merge_request(issuable, current_user)
todo_service.new_merge_request(issuable, current_user)
+ issuable.cache_merge_request_closes_issues!(current_user)
end
end
end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 5cedd6f11d9..22596b4014a 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -13,6 +13,7 @@ module MergeRequests
reload_merge_requests
reset_merge_when_build_succeeds
mark_pending_todos_done
+ cache_merge_requests_closing_issues
# Leave a system note if a branch was deleted/added
if branch_added? || branch_removed?
@@ -141,6 +142,14 @@ module MergeRequests
end
end
+ # If the merge requests closes any issues, save this information in the
+ # `MergeRequestsClosingIssues` model (as a performance optimization).
+ def cache_merge_requests_closing_issues
+ @project.merge_requests.where(source_branch: @branch_name).each do |merge_request|
+ merge_request.cache_merge_request_closes_issues!(@current_user)
+ end
+ end
+
def filter_merge_requests(merge_requests)
merge_requests.uniq.select(&:source_project)
end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 398ec47f0ea..f14f9e4b327 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -77,5 +77,9 @@ module MergeRequests
def close_service
MergeRequests::CloseService
end
+
+ def after_update(issuable)
+ issuable.cache_merge_request_closes_issues!(current_user)
+ end
end
end
diff --git a/app/services/milestones/create_service.rb b/app/services/milestones/create_service.rb
index 3b90399af64..b8e08c9f1eb 100644
--- a/app/services/milestones/create_service.rb
+++ b/app/services/milestones/create_service.rb
@@ -3,7 +3,7 @@ module Milestones
def execute
milestone = project.milestones.new(params)
- if milestone.save!
+ if milestone.save
event_service.open_milestone(milestone, current_user)
end
diff --git a/app/services/notes/slash_commands_service.rb b/app/services/notes/slash_commands_service.rb
index 4a9a8a64653..2edbd39a9e7 100644
--- a/app/services/notes/slash_commands_service.rb
+++ b/app/services/notes/slash_commands_service.rb
@@ -5,9 +5,18 @@ module Notes
'MergeRequest' => MergeRequests::UpdateService
}
- def supported?(note)
+ def self.noteable_update_service(note)
+ UPDATE_SERVICES[note.noteable_type]
+ end
+
+ def self.supported?(note, current_user)
noteable_update_service(note) &&
- can?(current_user, :"update_#{note.noteable_type.underscore}", note.noteable)
+ current_user &&
+ current_user.can?(:"update_#{note.noteable_type.underscore}", note.noteable)
+ end
+
+ def supported?(note)
+ self.class.supported?(note, current_user)
end
def extract_commands(note)
@@ -21,13 +30,7 @@ module Notes
return if command_params.empty?
return unless supported?(note)
- noteable_update_service(note).new(project, current_user, command_params).execute(note.noteable)
- end
-
- private
-
- def noteable_update_service(note)
- UPDATE_SERVICES[note.noteable_type]
+ self.class.noteable_update_service(note).new(project, current_user, command_params).execute(note.noteable)
end
end
end
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index d929364fc96..0d79ca7dc52 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -49,28 +49,6 @@
= select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
%span.help-block#clone-protocol-help
Allow only the selected protocols to be used for Git access.
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :version_check_enabled do
- = f.check_box :version_check_enabled
- Version check enabled
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :email_author_in_body do
- = f.check_box :email_author_in_body
- Include author name in notification email body
- .help-block
- 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.
- .form-group
- = f.label :admin_notification_email, class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :admin_notification_email, class: 'form-control'
- .help-block
- Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.
%fieldset
%legend Account and Limit Settings
@@ -341,6 +319,15 @@
%a{ href: 'http://www.akismet.com', target: 'blank'} http://www.akismet.com
%fieldset
+ %legend Abuse reports
+ .form-group
+ = f.label :admin_notification_email, 'Abuse reports notification email', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :admin_notification_email, class: 'form-control'
+ .help-block
+ Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.
+
+ %fieldset
%legend Error Reporting and Logging
%p
These settings require a restart to take effect.
@@ -407,6 +394,29 @@
= succeed "." do
= link_to "Koding administration documentation", help_page_path("administration/integration/koding")
+ %fieldset
+ %legend Usage statistics
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :version_check_enabled do
+ = f.check_box :version_check_enabled
+ Version check enabled
+ .help-block
+ Let GitLab inform you when an update is available.
+
+ %fieldset
+ %legend Email
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :email_author_in_body do
+ = f.check_box :email_author_in_body
+ Include author name in notification email body
+ .help-block
+ 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.
.form-actions
= f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index 5f7fdfdb011..817910f7ddf 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -13,6 +13,8 @@
.col-sm-offset-2.col-sm-10
= render 'shared/allow_request_access', form: f
+ = render 'groups/group_lfs_settings', f: f
+
- if @group.new_record?
.form-group
.col-sm-offset-2.col-sm-10
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index bb374694400..0188ed448ce 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -37,6 +37,12 @@
%strong
= @group.created_at.to_s(:medium)
+ %li
+ %span.light Group Git LFS status:
+ %strong
+ = group_lfs_status(@group)
+ = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
+
.panel.panel-default
.panel-heading
%h3.panel-title
diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml
index 02efcecc889..fbe3ab912b6 100644
--- a/app/views/award_emoji/_awards_block.html.haml
+++ b/app/views/award_emoji/_awards_block.html.haml
@@ -1,5 +1,5 @@
- grouped_emojis = awardable.grouped_awards(with_thumbs: inline)
-.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) } }
+.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } }
- awards_sort(grouped_emojis).each do |emoji, awards|
%button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), data: { placement: "bottom", title: award_user_list(awards, current_user) } }
= emoji_icon(emoji, sprite: false)
diff --git a/app/views/discussions/_jump_to_next.html.haml b/app/views/discussions/_jump_to_next.html.haml
index 69bd416c4de..7ed09dd1a98 100644
--- a/app/views/discussions/_jump_to_next.html.haml
+++ b/app/views/discussions/_jump_to_next.html.haml
@@ -5,5 +5,5 @@
%button.btn.btn-default.discussion-next-btn.has-tooltip{ "@click" => "jumpToNextUnresolvedDiscussion",
title: "Jump to next unresolved discussion",
"aria-label" => "Jump to next unresolved discussion",
- data: { container: "body" } }
+ data: { container: "body" }}
= custom_icon("next_discussion")
diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml
index fbe470bed2c..dfdbdf1f969 100644
--- a/app/views/discussions/_notes.html.haml
+++ b/app/views/discussions/_notes.html.haml
@@ -10,6 +10,7 @@
.btn-group{ role: "group" }
= link_to_reply_discussion(discussion, line_type)
= render "discussions/resolve_all", discussion: discussion
- = render "discussions/jump_to_next", discussion: discussion
+ - if discussion.for_merge_request?
+ = render "discussions/jump_to_next", discussion: discussion
- else
= link_to_reply_discussion(discussion)
diff --git a/app/views/groups/_group_lfs_settings.html.haml b/app/views/groups/_group_lfs_settings.html.haml
new file mode 100644
index 00000000000..af57065f0fc
--- /dev/null
+++ b/app/views/groups/_group_lfs_settings.html.haml
@@ -0,0 +1,11 @@
+- if current_user.admin?
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :lfs_enabled do
+ = f.check_box :lfs_enabled, checked: @group.lfs_enabled?
+ %strong
+ Allow projects within this group to use Git LFS
+ = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
+ %br/
+ %span.descr This setting can be overridden in each project. \ No newline at end of file
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index decb89b2fd6..c766370d5a0 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -25,6 +25,8 @@
.col-sm-offset-2.col-sm-10
= render 'shared/allow_request_access', form: f
+ = render 'group_lfs_settings', f: f
+
.form-group
%hr
= f.label :share_with_group_lock, class: 'control-label' do
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 53ed4fa991d..31db6ee0cad 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -23,7 +23,7 @@
.cover-desc.description
= markdown(@group.description, pipeline: :description)
-%div{ class: container_class }
+%div.groups-header{ class: container_class }
.top-area
%ul.nav-links
%li.active
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 16c16cec137..65842a0479b 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -165,6 +165,12 @@
%tr
%td.shortcut
.key g
+ .key l
+ %td
+ Go to issue boards
+ %tr
+ %td.shortcut
+ .key g
.key m
%td
Go to merge requests
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index bd3be20c4f8..4c721d40b55 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -45,7 +45,17 @@
%td
= github_project_link(repo.full_name)
%td.import-target
- = import_project_target(repo.owner.login, repo.name)
+ %fieldset.row
+ .input-group
+ .project-path.input-group-btn
+ - if current_user.can_select_namespace?
+ - selected = params[:namespace_id] || :current_user
+ - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {}
+ = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 }
+ - else
+ = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true
+ %span.input-group-addon /
+ = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true
%td.import-actions.job-status
= button_tag class: "btn btn-import js-add-to-import" do
Import
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index f7012595a5a..e44a2bfed9d 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -47,7 +47,7 @@
Repository
- if project_nav_tab? :pipelines
- = nav_link(controller: [:pipelines, :builds, :environments]) do
+ = nav_link(controller: [:pipelines, :builds, :environments, :cycle_analytics]) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
%span
Pipelines
@@ -113,3 +113,7 @@
%li.hidden
= link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do
Commits
+
+ -# Shortcut to issue boards
+ %li.hidden
+ = link_to 'Issue Boards', namespace_project_board_path(@project.namespace, @project), title: 'Issue Boards', class: 'shortcuts-issue-boards'
diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml
index dde2e2889dc..1ec4c3f0c67 100644
--- a/app/views/layouts/notify.html.haml
+++ b/app/views/layouts/notify.html.haml
@@ -25,8 +25,8 @@
- if @labels_url
adjust your #{link_to 'label subscriptions', @labels_url}.
- else
- - if @sent_notification && @sent_notification.unsubscribable?
- = link_to "unsubscribe", unsubscribe_sent_notification_url(@sent_notification)
+ - if @sent_notification_url
+ = link_to "unsubscribe", @sent_notification_url
from this thread or
adjust your notification settings.
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index 9fe94291db7..277eb71ea73 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -14,9 +14,6 @@
window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}";
window.preview_markdown_path = "#{preview_markdown_path}";
-- content_for :scripts_body do
- = render "layouts/init_auto_complete" if current_user
-
- content_for :header_content do
.js-dropdown-menu-projects
.dropdown-menu.dropdown-select.dropdown-menu-projects
diff --git a/app/views/profiles/update_username.js.haml b/app/views/profiles/update_username.js.haml
index 249680bcab6..de1337a2a24 100644
--- a/app/views/profiles/update_username.js.haml
+++ b/app/views/profiles/update_username.js.haml
@@ -1,6 +1,6 @@
- if @user.valid?
:plain
- new Flash("Username sucessfully changed", "notice")
+ new Flash("Username successfully changed", "notice")
- else
:plain
new Flash("Username change failed - #{@user.errors.full_messages.first}", "alert")
diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml
index 3978fa60d66..cb97181b9e1 100644
--- a/app/views/projects/_zen.html.haml
+++ b/app/views/projects/_zen.html.haml
@@ -7,3 +7,6 @@
= text_area_tag attr, nil, class: classes, placeholder: placeholder
%a.zen-control.zen-control-leave.js-zen-leave{ href: "#" }
= icon('compress')
+
+- content_for :scripts_body do
+ = render "layouts/init_auto_complete" if current_user && (@target_project || @project)
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index 56306b05934..0aa3092baa2 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -112,14 +112,14 @@
%span.label.label-primary
= tag
- - if builds.size > 1
+ - if @build.pipeline.stages.many?
.dropdown.build-dropdown
.title Stage
%button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
%span.stage-selection More
= icon('caret-down')
%ul.dropdown-menu
- - builds.map(&:stage).uniq.each do |stage|
+ - @build.pipeline.stages.each do |stage|
%li
%a.stage-item= stage
diff --git a/app/views/projects/builds/_table.html.haml b/app/views/projects/builds/_table.html.haml
index 61eff73da26..c2bcfb773a6 100644
--- a/app/views/projects/builds/_table.html.haml
+++ b/app/views/projects/builds/_table.html.haml
@@ -9,7 +9,7 @@
%thead
%tr
%th Status
- %th Commit
+ %th Build
- if admin
%th Project
%th Runner
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 5f5e071eb40..24de020917a 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -37,6 +37,6 @@
%li.dropdown-header Previous Artifacts
- artifacts.each do |job|
%li
- = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, ref, 'download', job: job.name), rel: 'nofollow' do
+ = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{ref}/download", job: job.name), rel: 'nofollow' do
%i.fa.fa-download
%span Download '#{job.name}'
diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml
index 36fb0300aeb..547bc0c9c19 100644
--- a/app/views/projects/ci/builds/_build_pipeline.html.haml
+++ b/app/views/projects/ci/builds/_build_pipeline.html.haml
@@ -1,15 +1,12 @@
- is_playable = subject.playable? && can?(current_user, :update_build, @project)
-%li.build{class: ("playable" if is_playable)}
- .curve
- .build-content
- - if is_playable
- = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: 'Play' do
- = render_status_with_link('build', 'play')
- %span.ci-status-text= subject.name
- - elsif can?(current_user, :read_build, @project)
- = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do
- = render_status_with_link('build', subject.status)
- %span.ci-status-text= subject.name
- - else
- = render_status_with_link('build', subject.status)
- = ci_icon_for_status(subject.status)
+- if is_playable
+ = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: 'Play' do
+ = render_status_with_link('build', 'play')
+ .ci-status-text= subject.name
+- elsif can?(current_user, :read_build, @project)
+ = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do
+ = render_status_with_link('build', subject.status)
+ .ci-status-text= subject.name
+- else
+ = render_status_with_link('build', subject.status)
+ = ci_icon_for_status(subject.status)
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index bb9493f5158..6391c67021b 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -36,16 +36,14 @@
- stages_status = pipeline.statuses.relevant.latest.stages_status
- - stages.each do |stage|
- %td.stage-cell
+ %td.stage-cell
+ - stages.each do |stage|
- status = stages_status[stage]
- tooltip = "#{stage.titleize}: #{status || 'not found'}"
- if status
- = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do
- = ci_icon_for_status(status)
- - else
- .light.has-tooltip{ title: tooltip }
- \-
+ .stage-container
+ = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do
+ = ci_icon_for_status(status)
%td
- if pipeline.duration
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index 20a85148ab5..9258f4b3c25 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -39,8 +39,7 @@
= stage.titleize
.builds-container
%ul
- - statuses.each do |status|
- = render "projects/#{status.to_partial_path}_pipeline", subject: status
+ = render "projects/commit/pipeline_stage", statuses: statuses
- if pipeline.yaml_errors.present?
diff --git a/app/views/projects/commit/_pipeline_stage.html.haml b/app/views/projects/commit/_pipeline_stage.html.haml
new file mode 100644
index 00000000000..23c5c51fbc2
--- /dev/null
+++ b/app/views/projects/commit/_pipeline_stage.html.haml
@@ -0,0 +1,14 @@
+- status_groups = statuses.group_by(&:group_name)
+- status_groups.each do |group_name, grouped_statuses|
+ - if grouped_statuses.one?
+ - status = grouped_statuses.first
+ - is_playable = status.playable? && can?(current_user, :update_build, @project)
+ %li.build{ class: ("playable" if is_playable) }
+ .curve
+ .build-content
+ = render "projects/#{status.to_partial_path}_pipeline", subject: status
+ - else
+ %li.build
+ .curve
+ .build-content
+ = render "projects/commit/pipeline_status_group", name: group_name, subject: grouped_statuses
diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml
new file mode 100644
index 00000000000..4e7a6f1af08
--- /dev/null
+++ b/app/views/projects/commit/_pipeline_status_group.html.haml
@@ -0,0 +1,11 @@
+- group_status = CommitStatus.where(id: subject).status
+= render_status_with_link('build', group_status)
+.dropdown.inline
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
+ %span.ci-status-text
+ = name
+ %span.badge= subject.size
+ %ul.dropdown-menu.grouped-pipeline-dropdown
+ .arrow
+ - subject.each do |status|
+ = render "projects/#{status.to_partial_path}_pipeline", subject: status
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
new file mode 100644
index 00000000000..7f346df8797
--- /dev/null
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -0,0 +1,59 @@
+- @no_container = true
+- page_title "Cycle Analytics"
+= render "projects/pipelines/head"
+
+#cycle-analytics{class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project)}}
+
+ .bordered-box.landing.content-block{"v-if" => "!isHelpDismissed"}
+ = icon('times', class: 'dismiss-icon', "@click": "dismissLanding()")
+ .row
+ .col-sm-3.col-xs-12.svg-container
+ = custom_icon('icon_cycle_analytics_splash')
+ .col-sm-8.col-xs-12.inner-content
+ %h4
+ Introducing Cycle Analytics
+ %p
+ Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.
+
+ = link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn'
+
+ = icon("spinner spin", "v-show" => "isLoading")
+
+ .wrapper{"v-show" => "!isLoading && !hasError"}
+ .panel.panel-default
+ .panel-heading
+ Pipeline Health
+
+ .content-block
+ .container-fluid
+ .row
+ .col-sm-3.col-xs-12.column{"v-for" => "item in analytics.summary"}
+ %h3.header {{item.value}}
+ %p.text {{item.title}}
+
+ .col-sm-3.col-xs-12.column
+ .dropdown.inline.js-ca-dropdown
+ %button.dropdown-menu-toggle{"data-toggle" => "dropdown", :type => "button"}
+ %span.dropdown-label Last 30 days
+ %i.fa.fa-chevron-down
+ %ul.dropdown-menu.dropdown-menu-align-right
+ %li
+ %a{'href' => "#", 'data-value' => '30'}
+ Last 30 days
+ %li
+ %a{'href' => "#", 'data-value' => '90'}
+ Last 90 days
+
+ .bordered-box
+ %ul.content-list
+ %li{"v-for" => "item in analytics.stats"}
+ .container-fluid
+ .row
+ .col-xs-8.title-col
+ %p.title
+ {{item.title}}
+ %p.text
+ {{item.description}}
+ .col-xs-4.value-col
+ %span
+ {{item.value}}
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index ad2eb3e504f..1a51ccd4c7d 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -5,7 +5,7 @@
- unless diff_file.submodule?
.file-actions.hidden-xs
- if blob_text_viewable?(blob)
- = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file" do
+ = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this files", disabled: @diff_notes_disabled do
= icon('comment')
\
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index f6d751a343e..a04d53e02bf 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -84,15 +84,14 @@
= project_feature_access_select(:snippets_access_level)
- if Gitlab.config.lfs.enabled && current_user.admin?
- .form-group
- .checkbox
- = f.label :lfs_enabled do
- = f.check_box :lfs_enabled, checked: @project.lfs_enabled?
- %strong LFS
- %br
- %span.descr
- Git Large File Storage
- = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
+ .row
+ .col-md-9
+ = f.label :lfs_enabled, 'LFS', class: 'label-light'
+ %span.help-block
+ Git Large File Storage
+ = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
+ .col-md-3
+ = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control'
- if Gitlab.config.registry.enabled
.form-group
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
index 576d0bec51b..409f4701e4b 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
@@ -1,10 +1,7 @@
-%li.build
- .curve
- .build-content
- - if subject.target_url
- - link_to subject.target_url do
- = render_status_with_link('commit status', subject.status)
- %span.ci-status-text= subject.name
- - else
- = render_status_with_link('commit status', subject.status)
- %span.ci-status-text= subject.name
+- if subject.target_url
+ = link_to subject.target_url do
+ = render_status_with_link('commit status', subject.status)
+ %span.ci-status-text= subject.name
+- else
+ = render_status_with_link('commit status', subject.status)
+ %span.ci-status-text= subject.name
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 851d4c06990..8b1a8a8a2d9 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -29,7 +29,7 @@
- note_count = issue.notes.user.count
%li
- = link_to issue_path(issue, anchor: 'notes'), class: ('issue-no-comments' if note_count.zero?) do
+ = link_to issue_path(issue, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
= icon('comments')
= note_count
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 31f8d0aeb5b..68fb7d5a414 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -41,7 +41,7 @@
- note_count = merge_request.mr_and_commit_notes.user.count
%li
- = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('merge-request-no-comments' if note_count.zero?) do
+ = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
= icon('comments')
= note_count
diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml
index 00287f2d245..49819519759 100644
--- a/app/views/projects/merge_requests/show/_versions.html.haml
+++ b/app/views/projects/merge_requests/show/_versions.html.haml
@@ -1,60 +1,74 @@
- if @merge_request_diffs.size > 1
.mr-version-controls
- Changes between
- %span.dropdown.inline.mr-version-dropdown
- %a.btn-link.dropdown-toggle{ data: {toggle: :dropdown} }
- %strong
- - if @merge_request_diff.latest?
- latest version
- - else
- version #{version_index(@merge_request_diff)}
- %span.caret
- %ul.dropdown-menu.dropdown-menu-selectable
- - @merge_request_diffs.each do |merge_request_diff|
- %li
- = link_to merge_request_version_path(@project, @merge_request, merge_request_diff), class: ('is-active' if merge_request_diff == @merge_request_diff) do
- %strong
- - if merge_request_diff.latest?
- latest version
- - else
- version #{version_index(merge_request_diff)}
- .monospace #{short_sha(merge_request_diff.head_commit_sha)}
- %small
- #{number_with_delimiter(merge_request_diff.commits.count)} #{'commit'.pluralize(merge_request_diff.commits.count)},
- = time_ago_with_tooltip(merge_request_diff.created_at)
-
- - if @merge_request_diff.base_commit_sha
- and
- %span.dropdown.inline.mr-version-compare-dropdown
- %a.btn-link.dropdown-toggle{ data: {toggle: :dropdown} }
- %strong
- - if @start_sha
- version #{version_index(@start_version)}
+ %div.mr-version-menus-container.content-block
+ Changes between
+ %span.dropdown.inline.mr-version-dropdown
+ %a.dropdown-toggle.btn.btn-default{ data: {toggle: :dropdown} }
+ %span
+ - if @merge_request_diff.latest?
+ latest version
- else
- #{@merge_request.target_branch}
+ version #{version_index(@merge_request_diff)}
%span.caret
- %ul.dropdown-menu.dropdown-menu-selectable
- - @comparable_diffs.each do |merge_request_diff|
- %li
- = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff, merge_request_diff.head_commit_sha), class: ('is-active' if merge_request_diff == @start_version) do
- %strong
- - if merge_request_diff.latest?
- latest version
- - else
- version #{version_index(merge_request_diff)}
- .monospace #{short_sha(merge_request_diff.head_commit_sha)}
- %small
- = time_ago_with_tooltip(merge_request_diff.created_at)
- %li
- = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do
- %strong
- #{@merge_request.target_branch} (base)
- .monospace #{short_sha(@merge_request_diff.base_commit_sha)}
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ .dropdown-title
+ %span Version:
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times', class: 'dropdown-menu-close-icon')
+ .dropdown-content
+ %ul
+ - @merge_request_diffs.each do |merge_request_diff|
+ %li
+ = link_to merge_request_version_path(@project, @merge_request, merge_request_diff), class: ('is-active' if merge_request_diff == @merge_request_diff) do
+ %strong
+ - if merge_request_diff.latest?
+ latest version
+ - else
+ version #{version_index(merge_request_diff)}
+ .monospace #{short_sha(merge_request_diff.head_commit_sha)}
+ %small
+ #{number_with_delimiter(merge_request_diff.commits.count)} #{'commit'.pluralize(merge_request_diff.commits.count)},
+ = time_ago_with_tooltip(merge_request_diff.created_at)
+
+ - if @merge_request_diff.base_commit_sha
+ and
+ %span.dropdown.inline.mr-version-compare-dropdown
+ %a.btn.btn-default.dropdown-toggle{ data: {toggle: :dropdown} }
+ %span
+ - if @start_sha
+ version #{version_index(@start_version)}
+ - else
+ #{@merge_request.target_branch}
+ %span.caret
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ .dropdown-title
+ %span Compared with:
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times', class: 'dropdown-menu-close-icon')
+ .dropdown-content
+ %ul
+ - @comparable_diffs.each do |merge_request_diff|
+ %li
+ = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff, merge_request_diff.head_commit_sha), class: ('is-active' if merge_request_diff == @start_version) do
+ %strong
+ - if merge_request_diff.latest?
+ latest version
+ - else
+ version #{version_index(merge_request_diff)}
+ .monospace #{short_sha(merge_request_diff.head_commit_sha)}
+ %small
+ = time_ago_with_tooltip(merge_request_diff.created_at)
+ %li
+ = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do
+ %strong
+ #{@merge_request.target_branch} (base)
+ .monospace #{short_sha(@merge_request_diff.base_commit_sha)}
- unless @merge_request_diff.latest? && !@start_sha
- .prepend-top-10
+ .comments-disabled-notif.content-block
= icon('info-circle')
- if @start_sha
Comments are disabled because you're comparing two versions of this merge request.
- else
Comments are disabled because you're viewing an old version of this merge request.
+ = link_to 'Show latest version', diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-sm'
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index 494695a03a5..44e645a7e81 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -43,15 +43,16 @@
= icon("times-circle")
Could not connect to the CI server. Please check your settings and try again.
-- @merge_request.environments.each do |environment|
- .mr-widget-heading
- .ci_widget.ci-success
- = ci_icon_for_status("success")
- %span.hidden-sm
- Deployed to
- = succeed '.' do
- = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment), class: 'environment'
- - external_url = environment.external_url
- - if external_url
- = link_to external_url, target: '_blank' do
- = icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}", right: true)
+- @merge_request.environments.sort_by(&:name).each do |environment|
+ - if can?(current_user, :read_environment, environment)
+ .mr-widget-heading
+ .ci_widget.ci-success
+ = ci_icon_for_status("success")
+ %span.hidden-sm
+ Deployed to
+ = succeed '.' do
+ = link_to environment.name, environment_path(environment), class: 'environment'
+ - external_url = environment.external_url
+ - if external_url
+ = link_to external_url, target: '_blank' do
+ = icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}", right: true)
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index 402f5b52f5e..46b402545cd 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -1,3 +1,5 @@
+- supports_slash_commands = note_supports_slash_commands?(@note)
+
= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f|
= hidden_field_tag :view, diff_view
= hidden_field_tag :line_type
@@ -14,8 +16,8 @@
attr: :note,
classes: 'note-textarea js-note-text',
placeholder: "Write a comment or drag your files here...",
- supports_slash_commands: true
- = render 'projects/notes/hints', supports_slash_commands: true
+ supports_slash_commands: supports_slash_commands
+ = render 'projects/notes/hints', supports_slash_commands: supports_slash_commands
.error-alert
.note-form-actions.clearfix
diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml
index 74538a9723e..8352eba7446 100644
--- a/app/views/projects/notes/_notes_with_form.html.haml
+++ b/app/views/projects/notes/_notes_with_form.html.haml
@@ -14,9 +14,9 @@
.disabled-comment.text-center
.disabled-comment-text.inline
Please
- = link_to "register", new_session_path(:user, redirect_to_referer: 'yes')
+ = link_to "sign up", new_session_path(:user, redirect_to_referer: 'yes')
or
- = link_to "login", new_session_path(:user, redirect_to_referer: 'yes')
+ = link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes')
to post a comment
:javascript
diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml
index f611ddc8f5f..5f571499e80 100644
--- a/app/views/projects/pipelines/_head.html.haml
+++ b/app/views/projects/pipelines/_head.html.haml
@@ -19,3 +19,9 @@
= link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
%span
Environments
+
+ - if can?(current_user, :read_cycle_analytics, @project)
+ = nav_link(controller: %w(cycle_analytics)) do
+ = link_to project_cycle_analytics_path(@project), title: 'Cycle Analytics' do
+ %span
+ Cycle Analytics
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 4d957e0d890..faf28db68d1 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -47,13 +47,7 @@
%tbody
%th Status
%th Commit
- - stages.each do |stage|
- %th.stage
- - if stage.titleize.length > 12
- %span.has-tooltip{ title: "#{stage.titleize}" }
- = stage.titleize
- - else
- = stage.titleize
+ %th Stages
%th
%th
= render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages
diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml
index 0628134b1bb..0193800dedf 100644
--- a/app/views/projects/protected_branches/_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_protected_branch.html.haml
@@ -1,4 +1,4 @@
-%tr.js-protected-branch-edit-form{ data: { url: namespace_project_protected_branch_path(@project.namespace, @project, protected_branch), branch_id: protected_branch.id } }
+%tr.js-protected-branch-edit-form{ data: { url: namespace_project_protected_branch_path(@project.namespace, @project, protected_branch) } }
%td
= protected_branch.name
- if @project.root_ref?(protected_branch.name)
diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml
index bdbf3e5f4d6..4aa4ab46a2f 100644
--- a/app/views/projects/snippets/_actions.html.haml
+++ b/app/views/projects/snippets/_actions.html.haml
@@ -3,11 +3,11 @@
= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New Snippet" do
New Snippet
- if can?(current_user, :update_project_snippet, @snippet)
+ = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do
+ Delete
+ - if can?(current_user, :update_project_snippet, @snippet)
= link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do
Edit
- - if can?(current_user, :update_project_snippet, @snippet)
- = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-warning", title: 'Delete Snippet' do
- Delete
- if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet)
.visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
@@ -21,9 +21,9 @@
New Snippet
- if can?(current_user, :update_project_snippet, @snippet)
%li
- = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do
- Edit
- - if can?(current_user, :update_project_snippet, @snippet)
- %li
= link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do
Delete
+ - if can?(current_user, :update_project_snippet, @snippet)
+ %li
+ = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do
+ Edit
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index bae4d8f349f..9503dbded13 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -1,15 +1,17 @@
- page_title @snippet.title, "Snippets"
-.snippet-holder
- = render 'shared/snippets/header'
+= render 'shared/snippets/header'
- %article.file-holder.file-holder-no-border.snippet-file-content
- .file-title.file-title-clear
+.project-snippets
+ %article.file-holder.snippet-file-content
+ .file-title
= blob_icon 0, @snippet.file_name
= @snippet.file_name
- .file-actions.hidden-xs
+ .file-actions
= clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
= link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank"
= render 'shared/snippets/blob'
+ = render 'award_emoji/awards_block', awardable: @snippet, inline: true
+
%div#notes= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml
index baaa2caa6de..a1f4e3e8ed6 100644
--- a/app/views/projects/tree/_readme.html.haml
+++ b/app/views/projects/tree/_readme.html.haml
@@ -1,7 +1,7 @@
%article.file-holder.readme-holder
.file-title
= blob_icon readme.mode, readme.name
- = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, @path, readme.name)) do
+ = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, @path, readme.name)) do
%strong
= readme.name
.file-content.wiki
diff --git a/app/views/projects/variables/_table.html.haml b/app/views/projects/variables/_table.html.haml
index 6c43f822db4..07cee86ba4c 100644
--- a/app/views/projects/variables/_table.html.haml
+++ b/app/views/projects/variables/_table.html.haml
@@ -9,7 +9,7 @@
%th Value
%th
%tbody
- - @project.variables.each do |variable|
+ - @project.variables.order_key_asc.each do |variable|
- if variable.id?
%tr
%td= variable.key
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 252c37532e1..7fe2bce3e7c 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -10,12 +10,16 @@
in group #{link_to @group.name, @group}
.results.prepend-top-10
- .search-results
- - if @scope == 'projects'
- .term
- = render 'shared/projects/list', projects: @search_objects
- - else
- = render partial: "search/results/#{@scope.singularize}", collection: @search_objects
+ - if @scope == 'commits'
+ %ul.list-unstyled
+ = render partial: "search/results/commit", collection: @search_objects
+ - else
+ .search-results
+ - if @scope == 'projects'
+ .term
+ = render 'shared/projects/list', projects: @search_objects
+ - else
+ = render partial: "search/results/#{@scope.singularize}", collection: @search_objects
- if @scope != 'projects'
= paginate(@search_objects, theme: 'gitlab')
diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml
index 290743feb4a..6f0a0ea36ec 100644
--- a/app/views/search/results/_blob.html.haml
+++ b/app/views/search/results/_blob.html.haml
@@ -1,4 +1,4 @@
-- blob = @project.repository.parse_search_result(blob)
+- blob = parse_search_result(blob)
.blob-result
.file-holder
.file-title
diff --git a/app/views/search/results/_commit.html.haml b/app/views/search/results/_commit.html.haml
index 4e6c3965dc6..5b2d83d6b92 100644
--- a/app/views/search/results/_commit.html.haml
+++ b/app/views/search/results/_commit.html.haml
@@ -1,2 +1 @@
-.search-result-row
- = render 'projects/commits/commit', project: @project, commit: commit
+= render 'projects/commits/commit', project: @project, commit: commit
diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml
index 235106c4f74..648d0bd76cb 100644
--- a/app/views/search/results/_wiki_blob.html.haml
+++ b/app/views/search/results/_wiki_blob.html.haml
@@ -1,4 +1,4 @@
-- wiki_blob = @project.repository.parse_search_result(wiki_blob)
+- wiki_blob = parse_search_result(wiki_blob)
.blob-result
.file-holder
.file-title
diff --git a/app/views/sent_notifications/unsubscribe.html.haml b/app/views/sent_notifications/unsubscribe.html.haml
new file mode 100644
index 00000000000..9ce6a1aeef5
--- /dev/null
+++ b/app/views/sent_notifications/unsubscribe.html.haml
@@ -0,0 +1,19 @@
+- noteable = @sent_notification.noteable
+- noteable_type = @sent_notification.noteable_type.humanize(capitalize: false)
+- noteable_text = %(#{noteable.title} (#{noteable.to_reference}))
+
+- page_title "Unsubscribe", noteable_text, @sent_notification.noteable_type.humanize.pluralize, @sent_notification.project.name_with_namespace
+
+
+%h3.page-title
+ Unsubscribe from #{noteable_type} #{noteable_text}
+
+%p
+ = succeed '?' do
+ Are you sure you want to unsubscribe from #{noteable_type}
+ = link_to noteable_text, url_for([@sent_notification.project.namespace.becomes(Namespace), @sent_notification.project, noteable])
+
+%p
+ = link_to 'Unsubscribe', unsubscribe_sent_notification_path(@sent_notification, force: true),
+ class: 'btn btn-primary append-right-10'
+ = link_to 'Cancel', new_user_session_path, class: 'btn append-right-10'
diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index cf16c203f9c..73d288e2236 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -1,10 +1,19 @@
+- if @project
+ - counts = milestone_counts(@project.milestones)
+
%ul.nav-links
- %li{class: ("active" if params[:state].blank? || params[:state] == 'opened')}
+ %li{class: milestone_class_for_state(params[:state], 'opened', true)}
= link_to milestones_filter_path(state: 'opened') do
Open
- %li{class: ("active" if params[:state] == 'closed')}
+ - if @project
+ %span.badge #{counts[:opened]}
+ %li{class: milestone_class_for_state(params[:state], 'closed')}
= link_to milestones_filter_path(state: 'closed') do
Closed
- %li{class: ("active" if params[:state] == 'all')}
+ - if @project
+ %span.badge #{counts[:closed]}
+ %li{class: milestone_class_for_state(params[:state], 'all')}
= link_to milestones_filter_path(state: 'all') do
All
+ - if @project
+ %span.badge #{counts[:all]}
diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml
index 107ad19177c..add4536a0a2 100644
--- a/app/views/shared/_visibility_level.html.haml
+++ b/app/views/shared/_visibility_level.html.haml
@@ -1,7 +1,7 @@
.form-group.project-visibility-level-holder
= f.label :visibility_level, class: 'control-label' do
Visibility Level
- = link_to "(?)", help_page_path("public_access/public_access")
+ = link_to icon('question-circle'), help_page_path("public_access/public_access")
.col-sm-10
- if can_change_visibility_level
= render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model)
diff --git a/app/views/shared/icons/_icon_cycle_analytics_splash.svg b/app/views/shared/icons/_icon_cycle_analytics_splash.svg
new file mode 100644
index 00000000000..eb5a962d651
--- /dev/null
+++ b/app/views/shared/icons/_icon_cycle_analytics_splash.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 99 102" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="0" d="m35.12 56.988c4.083-4.385 5.968-12.155 5.968-24.04 0-20.2-15.874-32.16-15.874-32.16-1.114-.954-2.929-.979-4.04 0 0 0-15.874 11.957-15.874 32.16 0 11.882 1.884 19.652 5.968 24.04h23.848"/><mask id="1" width="35.783" height="56.924" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(0-4)"><g transform="translate(32.15 3.976)"><g fill="#6b4fbb"><path d="m11.928 56.988l1.325-1.325v3.313c0 .737.59 1.325 1.325 1.325h17.229c.736 0 1.325-.59 1.325-1.325v-3.313l1.325 1.325h-22.53m22.53-1.325v3.313c0 1.464-1.18 2.651-2.651 2.651h-17.229c-1.464 0-2.651-1.178-2.651-2.651v-3.313h22.53m-5.964 7.361h.663c0 3.294-2.67 5.964-5.964 5.964-3.294 0-5.964-2.67-5.964-5.964h.663.663c0 2.562 2.077 4.639 4.639 4.639 2.562 0 4.639-2.077 4.639-4.639h.663"/><path d="m5.816 42.535c-.346-2.839-.515-6.03-.515-9.584 0-20.2 15.874-32.16 15.874-32.16 1.106-.979 2.921-.954 4.04 0 0 0 15.874 11.957 15.874 32.16 0 11.882-1.884 19.652-5.968 24.04h-23.848c-2.861-3.073-4.643-7.807-5.453-14.453-.06-.493-.115-.997-.164-1.511l-4.04 2.884c-.891.637-1.614 2.041-1.614 3.137v14.581c0 1.465.971 1.958 2.165 1.106l8.691-6.208c-.282-.332-.553-.681-.813-1.048l-8.648 6.177c-.147.105-.069.152-.069-.027v-14.581c0-.668.516-1.671 1.059-2.059l3.432-2.451m38.4 20.2c1.193.852 2.165.359 2.165-1.106v-14.581c0-1.096-.723-2.5-1.614-3.137l-4.04-2.884c-.049.514-.104 1.018-.164 1.511l3.432 2.451c.543.388 1.059 1.391 1.059 2.059v14.581c0 .179.078.132-.069.027l-8.648-6.177c-.26.367-.531.716-.813 1.048l8.691 6.208"/></g><use fill="#fff" stroke="#6b4fbb" stroke-width="2.651" mask="url(#1)" xlink:href="#0"/><g fill="#b5a7dd"><path d="m30.482 28.494c0-4.03-3.263-7.289-7.289-7.289-4.03 0-7.289 3.263-7.289 7.289 0 4.03 3.263 7.289 7.289 7.289 4.03 0 7.289-3.263 7.289-7.289m-15.904 0c0-4.758 3.857-8.614 8.614-8.614 4.758 0 8.614 3.857 8.614 8.614 0 4.758-3.857 8.614-8.614 8.614-4.758 0-8.614-3.857-8.614-8.614"/><path d="m27.17 28.494c0-2.196-1.78-3.976-3.976-3.976-2.196 0-3.976 1.78-3.976 3.976 0 2.196 1.78 3.976 3.976 3.976 2.196 0 3.976-1.78 3.976-3.976m-9.277 0c0-2.928 2.373-5.301 5.301-5.301 2.928 0 5.301 2.373 5.301 5.301 0 2.928-2.373 5.301-5.301 5.301-2.928 0-5.301-2.373-5.301-5.301"/></g><path fill="#6b4fbb" d="m34.458 87.47c0 1.098.89 1.988 1.988 1.988 1.098 0 1.988-.89 1.988-1.988 0-.366.297-.663.663-.663.366 0 .663.297.663.663 0 1.83-1.483 3.313-3.313 3.313-1.826 0-3.307-1.478-3.313-3.302 0-.002 0-.003 0-.005v-2.663c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.657m-21.2-6.615c0-.002 0-.003 0-.005v-2.663c0-.358-.297-.657-.663-.657-.369 0-.663.294-.663.657v2.657c0 1.098-.89 1.988-1.988 1.988-1.098 0-1.988-.89-1.988-1.988 0-.366-.297-.663-.663-.663-.366 0-.663.297-.663.663 0 1.83 1.483 3.313 3.313 3.313 1.826 0 3.307-1.477 3.313-3.302m5.301 7.285c0-.001 0-.002 0-.003v-16.576c0-.362-.297-.658-.663-.658-.369 0-.663.295-.663.658v16.571c0 2.01-1.632 3.645-3.645 3.645-2.01 0-3.645-1.632-3.645-3.645 0-.366-.297-.663-.663-.663-.366 0-.663.297-.663.663 0 2.745 2.225 4.97 4.97 4.97 2.742 0 4.966-2.221 4.97-4.963m10.602 8.607v-18.555c0-.365-.297-.661-.663-.661-.369 0-.663.296-.663.661v18.557c0 0 0 0 0 .001.001 2.744 2.226 4.968 4.97 4.968 2.745 0 4.97-2.225 4.97-4.97 0-.366-.297-.663-.663-.663-.366 0-.663.297-.663.663 0 2.01-1.632 3.645-3.645 3.645-2.01 0-3.645-1.632-3.645-3.645m3.976-25.19c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m0 6.627c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m-10.602-6.627c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m5.301 0c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m-5.301 6.627c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m0 6.627c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m-10.602-13.253c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663"/></g><path fill="#e2ddf2" d="m97.75 76.54c0-2.745-2.225-4.97-4.97-4.97-2.745 0-4.97 2.225-4.97 4.97 0 2.745 2.225 4.97 4.97 4.97 2.745 0 4.97-2.225 4.97-4.97m-8.614 0c0-2.01 1.632-3.645 3.645-3.645 2.01 0 3.645 1.632 3.645 3.645 0 2.01-1.632 3.645-3.645 3.645-2.01 0-3.645-1.632-3.645-3.645m-60.964-57.651c0-2.745-2.225-4.97-4.97-4.97-2.745 0-4.97 2.225-4.97 4.97 0 2.745 2.225 4.97 4.97 4.97 2.745 0 4.97-2.225 4.97-4.97m-8.614 0c0-2.01 1.632-3.645 3.645-3.645 2.01 0 3.645 1.632 3.645 3.645 0 2.01-1.632 3.645-3.645 3.645-2.01 0-3.645-1.632-3.645-3.645"/><path fill="#b5a7dd" d="m98.41 34.458c0-1.83-1.483-3.313-3.313-3.313-1.83 0-3.313 1.483-3.313 3.313 0 1.83 1.483 3.313 3.313 3.313 1.83 0 3.313-1.483 3.313-3.313m-5.301 0c0-1.098.89-1.988 1.988-1.988 1.098 0 1.988.89 1.988 1.988 0 1.098-.89 1.988-1.988 1.988-1.098 0-1.988-.89-1.988-1.988m-86.14 20.542c0-1.83-1.483-3.313-3.313-3.313-1.83 0-3.313 1.483-3.313 3.313 0 1.83 1.483 3.313 3.313 3.313 1.83 0 3.313-1.483 3.313-3.313m-5.301 0c0-1.098.89-1.988 1.988-1.988 1.098 0 1.988.89 1.988 1.988 0 1.098-.89 1.988-1.988 1.988-1.098 0-1.988-.89-1.988-1.988"/></g></svg>
diff --git a/app/views/shared/icons/_icon_fork.svg b/app/views/shared/icons/_icon_fork.svg
index fc970e4ce50..ce22b6cdaea 100644
--- a/app/views/shared/icons/_icon_fork.svg
+++ b/app/views/shared/icons/_icon_fork.svg
@@ -1,3 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="30" height="40" viewBox="5 0 30 40">
- <path fill="#7E7E7E" fill-rule="evenodd" d="M22,29.5351288 L22,22.7193602 C26.1888699,21.5098039 29.3985457,16.802989 29.3985457,16.802989 C29.740988,16.3567547 30,15.5559546 30,15.0081969 L30,10.4648712 C31.1956027,9.77325238 32,8.48056471 32,7 C32,4.790861 30.209139,3 28,3 C25.790861,3 24,4.790861 24,7 C24,8.48056471 24.8043973,9.77325238 26,10.4648712 L26,14.7083871 C26,14.8784435 25.9055559,15.0987329 25.7890533,15.2104147 C25.7890533,15.2104147 24.5373893,16.4126202 23.9488702,16.9515733 C22.5015398,18.2770075 21.1191354,19 20.090554,19 C19.0477772,19 17.6172728,18.2608988 16.1128852,16.9142923 C15.5030182,16.3683886 14.3672121,15.3403307 14.3672121,15.3403307 C14.1659605,15.1583364 14.0000086,14.7846305 14.0000192,14.5088473 C14.0000192,14.5088473 14.0000932,12.7539451 14.0001308,10.4647956 C15.1956614,9.77315812 16,8.48051074 16,7 C16,4.790861 14.209139,3 12,3 C9.790861,3 8,4.790861 8,7 C8,8.48056471 8.80439726,9.77325238 10,10.4648712 L10,15.0081969 C10,15.5446944 10.2736352,16.3534183 10.6111812,16.7893819 C10.6111812,16.7893819 13.8599776,21.3779363 18,22.6668724 L18,29.5351288 C16.8043973,30.2267476 16,31.5194353 16,33 C16,35.209139 17.790861,37 20,37 C22.209139,37 24,35.209139 24,33 C24,31.5194353 23.1956027,30.2267476 22,29.5351288 Z M14,7 C14,5.8954305 13.1045695,5 12,5 C10.8954305,5 10,5.8954305 10,7 C10,8.1045695 10.8954305,9 12,9 C13.1045695,9 14,8.1045695 14,7 Z M30,7 C30,5.8954305 29.1045695,5 28,5 C26.8954305,5 26,5.8954305 26,7 C26,8.1045695 26.8954305,9 28,9 C29.1045695,9 30,8.1045695 30,7 Z M22,33 C22,31.8954305 21.1045695,31 20,31 C18.8954305,31 18,31.8954305 18,33 C18,34.1045695 18.8954305,35 20,35 C21.1045695,35 22,34.1045695 22,33 Z"/>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="30" height="40" viewBox="5 0 30 40"><path fill="#7E7E7E" fill-rule="evenodd" d="M22 29.535V22.72c4.19-1.21 7.4-5.917 7.4-5.917.34-.446.6-1.247.6-1.795v-4.543C31.196 9.773 32 8.48 32 7c0-2.21-1.79-4-4-4s-4 1.79-4 4c0 1.48.804 2.773 2 3.465v4.243c0 .17-.094.39-.21.502 0 0-1.253 1.203-1.84 1.742C22.5 18.277 21.12 19 20.09 19c-1.042 0-2.473-.74-3.977-2.086-.61-.546-1.746-1.574-1.746-1.574-.2-.182-.367-.555-.367-.83v-4.045C15.196 9.773 16 8.48 16 7c0-2.21-1.79-4-4-4S8 4.79 8 7c0 1.48.804 2.773 2 3.465v4.543c0 .537.274 1.345.61 1.78 0 0 3.25 4.59 7.39 5.88v6.867c-1.196.692-2 1.984-2 3.465 0 2.21 1.79 4 4 4s4-1.79 4-4c0-1.48-.804-2.773-2-3.465zM14 7c0-1.105-.895-2-2-2s-2 .895-2 2 .895 2 2 2 2-.895 2-2zm16 0c0-1.105-.895-2-2-2s-2 .895-2 2 .895 2 2 2 2-.895 2-2zm-8 26c0-1.105-.895-2-2-2s-2 .895-2 2 .895 2 2 2 2-.895 2-2z"/></svg>
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 93c4d5c3d30..cf26197f7d7 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -2,9 +2,9 @@
.issues-filters
.issues-details-filters.row-content-block.second-block
- = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :issue_search]), method: :get, class: 'filter-form js-filter-form' do
- - if params[:issue_search].present?
- = hidden_field_tag :issue_search, params[:issue_search]
+ = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
+ - if params[:search].present?
+ = hidden_field_tag :search, params[:search]
- if @bulk_edit
.check-all-holder
= check_box_tag "check_all_issues", nil, false,
@@ -29,7 +29,7 @@
= render "shared/issuable/label_dropdown"
.filter-item.inline.reset-filters
- %a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :issue_search])} Reset filters
+ %a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search])} Reset filters
.pull-right
- if boards_page
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 3856a4917b4..04373684ee9 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -19,7 +19,7 @@
= dropdown_tag(title, options: { toggle_class: 'js-issuable-selector',
title: title, filter: true, placeholder: 'Filter', footer_content: true,
- data: { data: issuable_template_names, field_name: 'issuable_template', selected: selected_template(issuable), project_path: @project.path, namespace_path: @project.namespace.path } } ) do
+ data: { data: issuable_template_names, field_name: 'issuable_template', selected: selected_template(issuable), project_path: ref_project.path, namespace_path: ref_project.namespace.path } } ) do
%ul.dropdown-footer-list
%li
%a.reset-template
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index 24a1a616919..d34d28f6736 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -12,7 +12,7 @@
- if params[:label_name].present?
- if params[:label_name].respond_to?('any?')
- params[:label_name].each do |label|
- = hidden_field_tag "label_name[]", u(label), id: nil
+ = hidden_field_tag "label_name[]", label, id: nil
.dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data}
%span.dropdown-toggle-text
diff --git a/app/views/shared/issuable/_search_form.html.haml b/app/views/shared/issuable/_search_form.html.haml
index 186963b32b8..2c89217cadd 100644
--- a/app/views/shared/issuable/_search_form.html.haml
+++ b/app/views/shared/issuable/_search_form.html.haml
@@ -1,2 +1,2 @@
-= form_tag(path, method: :get, id: "issue_search_form", class: 'issue-search-form') do
- = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by name ...', class: 'form-control issue_search search-text-input input-short', spellcheck: false }
+= form_tag(path, method: :get, id: "issuable_search_form", class: 'issuable-search-form') do
+ = search_field_tag :search, params[:search], { id: 'issuable_search', placeholder: 'Filter by name ...', class: 'form-control issuable_search search-text-input input-short', spellcheck: false }
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index acc3ccf4dcf..3dccfb147bf 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -33,7 +33,7 @@
- if @project
.row
.col-sm-6= render('shared/milestone_expired', milestone: milestone)
- .col-sm-6
+ .col-sm-6.milestone-actions
- if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
= link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs btn-grouped" do
Edit
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index af753496260..7ae4211ddfd 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -6,12 +6,13 @@
%strong.item-title
Snippet #{@snippet.to_reference}
%span.creator
- created by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title")}
+ authored
= time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago')
- if @snippet.updated_at != @snippet.created_at
%span
= icon('edit', title: 'edited')
= time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago')
+ by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "hidden-xs")}
.snippet-actions
- if @snippet.project_id?
@@ -19,6 +20,5 @@
- else
= render "snippets/actions"
-.content-block.second-block
- %h2.snippet-title.prepend-top-0.append-bottom-0
- = markdown escape_once(@snippet.title), pipeline: :single_line, author: @snippet.author
+%h2.snippet-title.prepend-top-0.append-bottom-0
+ = markdown escape_once(@snippet.title), pipeline: :single_line, author: @snippet.author
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index c96dfefe17f..ea17bec8677 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -3,19 +3,30 @@
.title
= link_to reliable_snippet_path(snippet) do
- = truncate(snippet.title, length: 60)
+ = snippet.title
- if snippet.private?
- %span.label.label-gray
+ %span.label.label-gray.hidden-xs
= icon('lock')
private
- %span.monospace.pull-right
+ %span.monospace.pull-right.hidden-xs
= snippet.file_name
- %small.pull-right.cgray
+ %ul.controls.visible-xs
+ %li
+ - note_count = snippet.notes.user.count
+ = link_to reliable_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
+ = icon('comments')
+ = note_count
+ %li
+ %span.sr-only
+ = visibility_level_label(snippet.visibility_level)
+ = visibility_level_icon(snippet.visibility_level, fw: false)
+
+ %small.pull-right.cgray.hidden-xs
- if snippet.project_id?
= link_to snippet.project.name_with_namespace, namespace_project_path(snippet.project.namespace, snippet.project)
- .snippet-info
+ .snippet-info.hidden-xs
= link_to user_snippets_path(snippet.author) do
= snippet.author_name
authored #{time_ago_with_tooltip(snippet.created_at)}
diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml
index 160c6cd84da..fdaca199218 100644
--- a/app/views/snippets/_actions.html.haml
+++ b/app/views/snippets/_actions.html.haml
@@ -2,12 +2,12 @@
- if current_user
= link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New Snippet" do
New Snippet
- - if can?(current_user, :update_personal_snippet, @snippet)
- = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do
- Edit
- if can?(current_user, :admin_personal_snippet, @snippet)
= link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do
Delete
+ - if can?(current_user, :update_personal_snippet, @snippet)
+ = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do
+ Edit
- if current_user
.visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
@@ -18,11 +18,11 @@
%li
= link_to new_snippet_path, title: "New Snippet" do
New Snippet
- - if can?(current_user, :update_personal_snippet, @snippet)
- %li
- = link_to edit_snippet_path(@snippet) do
- Edit
- if can?(current_user, :admin_personal_snippet, @snippet)
%li
= link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do
Delete
+ - if can?(current_user, :update_personal_snippet, @snippet)
+ %li
+ = link_to edit_snippet_path(@snippet) do
+ Edit
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index ed3992650d4..cd89155c616 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -1,13 +1,14 @@
- page_title @snippet.title, "Snippets"
-.snippet-holder
- = render 'shared/snippets/header'
+= render 'shared/snippets/header'
- %article.file-holder.file-holder-no-border.snippet-file-content
- .file-title.file-title-clear
- = blob_icon 0, @snippet.file_name
- = @snippet.file_name
- .file-actions.hidden-xs
- = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
- = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank"
- = render 'shared/snippets/blob'
+%article.file-holder.snippet-file-content
+ .file-title
+ = blob_icon 0, @snippet.file_name
+ = @snippet.file_name
+ .file-actions
+ = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
+ = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank"
+ = render 'shared/snippets/blob'
+
+= render 'award_emoji/awards_block', awardable: @snippet, inline: true \ No newline at end of file
diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml
index 77f2ddefb1e..09ff8a76d27 100644
--- a/app/views/users/calendar.html.haml
+++ b/app/views/users/calendar.html.haml
@@ -4,6 +4,6 @@
Summary of issues, merge requests, and push events
:javascript
new Calendar(
- #{@timestamps.to_json},
+ #{@activity_dates.to_json},
'#{user_calendar_activities_path}'
- );
+ ); \ No newline at end of file
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 9a052abe40a..2a57ac90bab 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -10,72 +10,72 @@
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
.user-profile
- .cover-block
+ .cover-block.user-cover-block
.cover-controls
- if @user == current_user
= link_to profile_path, class: 'btn btn-gray' do
= icon('pencil')
- elsif current_user
- %span.report-abuse
- - if @user.abuse_report
- %button.btn.btn-danger{ title: 'Already reported for abuse',
- data: { toggle: 'tooltip', placement: 'left', container: 'body' }}
- = icon('exclamation-circle')
- - else
- = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray',
- title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do
- = icon('exclamation-circle')
+ - if @user.abuse_report
+ %button.btn.btn-danger{ title: 'Already reported for abuse',
+ data: { toggle: 'tooltip', placement: 'bottom', container: 'body' }}
+ = icon('exclamation-circle')
+ - else
+ = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray',
+ title: 'Report abuse', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('exclamation-circle')
- if current_user
- &nbsp;
= link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do
= icon('rss')
- if current_user.admin?
- &nbsp;
= link_to [:admin, @user], class: 'btn btn-gray', title: 'View user in admin area',
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('users')
- .avatar-holder
- = link_to avatar_icon(@user, 400), target: '_blank' do
- = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
- .cover-title
- = @user.name
-
- .cover-desc
- %span.middle-dot-divider
- @#{@user.username}
- %span.middle-dot-divider
- Member since #{@user.created_at.to_s(:medium)}
+ .profile-header
+ .avatar-holder
+ = link_to avatar_icon(@user, 400), target: '_blank' do
+ = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
+
+ .user-info
+ .cover-title
+ = @user.name
+ %span.handle
+ @#{@user.username}
+
+ .cover-desc.member-date
+ %span.middle-dot-divider
+ Member since #{@user.created_at.to_s(:medium)}
+
+ .cover-desc
+ - unless @user.public_email.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to @user.public_email, "mailto:#{@user.public_email}"
+ - unless @user.skype.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to "skype:#{@user.skype}", title: "Skype" do
+ = icon('skype')
+ - unless @user.linkedin.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do
+ = icon('linkedin-square')
+ - unless @user.twitter.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do
+ = icon('twitter-square')
+ - unless @user.website_url.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to @user.short_website_url, @user.full_website_url
+ - unless @user.location.blank?
+ .profile-link-holder.middle-dot-divider
+ = icon('map-marker')
+ = @user.location
- if @user.bio.present?
.cover-desc
%p.profile-user-bio
= @user.bio
- .cover-desc
- - unless @user.public_email.blank?
- .profile-link-holder.middle-dot-divider
- = link_to @user.public_email, "mailto:#{@user.public_email}"
- - unless @user.skype.blank?
- .profile-link-holder.middle-dot-divider
- = link_to "skype:#{@user.skype}", title: "Skype" do
- = icon('skype')
- - unless @user.linkedin.blank?
- .profile-link-holder.middle-dot-divider
- = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do
- = icon('linkedin-square')
- - unless @user.twitter.blank?
- .profile-link-holder.middle-dot-divider
- = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do
- = icon('twitter-square')
- - unless @user.website_url.blank?
- .profile-link-holder.middle-dot-divider
- = link_to @user.short_website_url, @user.full_website_url
- - unless @user.location.blank?
- .profile-link-holder.middle-dot-divider
- = icon('map-marker')
- = @user.location
-
%ul.nav-links.center.user-profile-nav
%li.js-activity-tab
= link_to user_calendar_activities_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do
diff --git a/config/routes.rb b/config/routes.rb
index 068c92d1400..4d6ec699cbd 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -35,6 +35,10 @@ Rails.application.routes.draw do
post :approve_access_request, on: :member
end
+ concern :awardable do
+ post :toggle_award_emoji, on: :member
+ end
+
namespace :ci do
# CI API
Ci::API::API.logger Rails.logger
@@ -98,7 +102,7 @@ Rails.application.routes.draw do
#
# Global snippets
#
- resources :snippets do
+ resources :snippets, concerns: :awardable do
member do
get 'raw'
end
@@ -110,7 +114,6 @@ Rails.application.routes.draw do
#
# Invites
#
-
resources :invites, only: [:show], constraints: { id: /[A-Za-z0-9_-]+/ } do
member do
post :accept
@@ -662,7 +665,7 @@ Rails.application.routes.draw do
end
end
- resources :snippets, constraints: { id: /\d+/ } do
+ resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
member do
get 'raw'
end
@@ -724,7 +727,7 @@ Rails.application.routes.draw do
end
end
- resources :merge_requests, constraints: { id: /\d+/ } do
+ resources :merge_requests, concerns: :awardable, constraints: { id: /\d+/ } do
member do
get :commits
get :diffs
@@ -736,7 +739,6 @@ Rails.application.routes.draw do
post :cancel_merge_when_build_succeeds
get :ci_status
post :toggle_subscription
- post :toggle_award_emoji
post :remove_wip
get :diff_for_path
post :resolve_conflicts
@@ -780,6 +782,8 @@ Rails.application.routes.draw do
resources :environments
+ resource :cycle_analytics, only: [:show]
+
resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
collection do
post :cancel_all
@@ -838,10 +842,9 @@ Rails.application.routes.draw do
end
end
- resources :issues, constraints: { id: /\d+/ } do
+ resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do
member do
post :toggle_subscription
- post :toggle_award_emoji
post :mark_as_spam
get :referenced_merge_requests
get :related_branches
@@ -869,9 +872,8 @@ Rails.application.routes.draw do
resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ }
- resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do
+ resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
member do
- post :toggle_award_emoji
delete :delete_attachment
post :resolve
delete :resolve, action: :unresolve
diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb
index e3316ecdb6c..a984eda5ab5 100644
--- a/db/fixtures/development/04_project.rb
+++ b/db/fixtures/development/04_project.rb
@@ -3,11 +3,11 @@ require 'sidekiq/testing'
Sidekiq::Testing.inline! do
Gitlab::Seeder.quiet do
project_urls = [
- 'https://github.com/documentcloud/underscore.git',
+ 'https://gitlab.com/gitlab-org/gitlab-test.git',
'https://gitlab.com/gitlab-org/gitlab-ce.git',
'https://gitlab.com/gitlab-org/gitlab-ci.git',
'https://gitlab.com/gitlab-org/gitlab-shell.git',
- 'https://gitlab.com/gitlab-org/gitlab-test.git',
+ 'https://github.com/documentcloud/underscore.git',
'https://github.com/twitter/flight.git',
'https://github.com/twitter/typeahead.js.git',
'https://github.com/h5bp/html5-boilerplate.git',
@@ -38,12 +38,7 @@ Sidekiq::Testing.inline! do
]
# You can specify how many projects you need during seed execution
- size = if ENV['SIZE'].present?
- ENV['SIZE'].to_i
- else
- 8
- end
-
+ size = ENV['SIZE'].present? ? ENV['SIZE'].to_i : 8
project_urls.first(size).each_with_index do |url, i|
group_path, project_path = url.split('/')[-2..-1]
diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb
index 49e6e2361b1..650b410595c 100644
--- a/db/fixtures/development/14_pipelines.rb
+++ b/db/fixtures/development/14_pipelines.rb
@@ -3,9 +3,13 @@ class Gitlab::Seeder::Pipelines
BUILDS = [
{ name: 'build:linux', stage: 'build', status: :success },
{ name: 'build:osx', stage: 'build', status: :success },
- { name: 'rspec:linux', stage: 'test', status: :success },
- { name: 'rspec:windows', stage: 'test', status: :success },
- { name: 'rspec:windows', stage: 'test', status: :success },
+ { name: 'rspec:linux 0 3', stage: 'test', status: :success },
+ { name: 'rspec:linux 1 3', stage: 'test', status: :success },
+ { name: 'rspec:linux 2 3', stage: 'test', status: :success },
+ { name: 'rspec:windows 0 3', stage: 'test', status: :success },
+ { name: 'rspec:windows 1 3', stage: 'test', status: :success },
+ { name: 'rspec:windows 2 3', stage: 'test', status: :success },
+ { name: 'rspec:windows 2 3', stage: 'test', status: :success },
{ name: 'rspec:osx', stage: 'test', status_event: :success },
{ name: 'spinach:linux', stage: 'test', status: :success },
{ name: 'spinach:osx', stage: 'test', status: :failed, allow_failure: true},
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
new file mode 100644
index 00000000000..e882a492757
--- /dev/null
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -0,0 +1,246 @@
+require 'sidekiq/testing'
+require './spec/support/test_env'
+
+class Gitlab::Seeder::CycleAnalytics
+ def initialize(project, perf: false)
+ @project = project
+ @user = User.order(:id).last
+ @issue_count = perf ? 1000 : 5
+ stub_git_pre_receive!
+ end
+
+ # The GitLab API needn't be running for the fixtures to be
+ # created. Since we're performing a number of git actions
+ # here (like creating a branch or committing a file), we need
+ # to disable the `pre_receive` hook in order to remove this
+ # dependency on the GitLab API.
+ def stub_git_pre_receive!
+ GitHooksService.class_eval do
+ def run_hook(name)
+ [true, '']
+ end
+ end
+ end
+
+ def seed_metrics!
+ @issue_count.times do |index|
+ # Issue
+ Timecop.travel 5.days.from_now
+ title = "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}"
+ issue = Issue.create(project: @project, title: title, author: @user)
+ issue_metrics = issue.metrics
+
+ # Milestones / Labels
+ Timecop.travel 5.days.from_now
+ if index.even?
+ issue_metrics.first_associated_with_milestone_at = rand(6..12).hours.from_now
+ else
+ issue_metrics.first_added_to_board_at = rand(6..12).hours.from_now
+ end
+
+ # Commit
+ Timecop.travel 5.days.from_now
+ issue_metrics.first_mentioned_in_commit_at = rand(6..12).hours.from_now
+
+ # MR
+ Timecop.travel 5.days.from_now
+ branch_name = "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}"
+ @project.repository.add_branch(@user, branch_name, 'master')
+ merge_request = MergeRequest.create(target_project: @project, source_project: @project, source_branch: branch_name, target_branch: 'master', title: branch_name, author: @user)
+ merge_request_metrics = merge_request.metrics
+
+ # MR closing issues
+ Timecop.travel 5.days.from_now
+ MergeRequestsClosingIssues.create!(issue: issue, merge_request: merge_request)
+
+ # Merge
+ Timecop.travel 5.days.from_now
+ merge_request_metrics.merged_at = rand(6..12).hours.from_now
+
+ # Start build
+ Timecop.travel 5.days.from_now
+ merge_request_metrics.latest_build_started_at = rand(6..12).hours.from_now
+
+ # Finish build
+ Timecop.travel 5.days.from_now
+ merge_request_metrics.latest_build_finished_at = rand(6..12).hours.from_now
+
+ # Deploy to production
+ Timecop.travel 5.days.from_now
+ merge_request_metrics.first_deployed_to_production_at = rand(6..12).hours.from_now
+
+ issue_metrics.save!
+ merge_request_metrics.save!
+
+ print '.'
+ end
+ end
+
+ def seed!
+ Sidekiq::Testing.inline! do
+ issues = create_issues
+ puts '.'
+
+ # Stage 1
+ Timecop.travel 5.days.from_now
+ add_milestones_and_list_labels(issues)
+ print '.'
+
+ # Stage 2
+ Timecop.travel 5.days.from_now
+ branches = mention_in_commits(issues)
+ print '.'
+
+ # Stage 3
+ Timecop.travel 5.days.from_now
+ merge_requests = create_merge_requests_closing_issues(issues, branches)
+ print '.'
+
+ # Stage 4
+ Timecop.travel 5.days.from_now
+ run_builds(merge_requests)
+ print '.'
+
+ # Stage 5
+ Timecop.travel 5.days.from_now
+ merge_merge_requests(merge_requests)
+ print '.'
+
+ # Stage 6 / 7
+ Timecop.travel 5.days.from_now
+ deploy_to_production(merge_requests)
+ print '.'
+ end
+
+ print '.'
+ end
+
+ private
+
+ def create_issues
+ Array.new(@issue_count) do
+ issue_params = {
+ title: "Cycle Analytics: #{FFaker::Lorem.sentence(6)}",
+ description: FFaker::Lorem.sentence,
+ state: 'opened',
+ assignee: @project.team.users.sample
+ }
+
+ Issues::CreateService.new(@project, @project.team.users.sample, issue_params).execute
+ end
+ end
+
+ def add_milestones_and_list_labels(issues)
+ issues.shuffle.map.with_index do |issue, index|
+ Timecop.travel 12.hours.from_now
+
+ if index.even?
+ issue.update(milestone: @project.milestones.sample)
+ else
+ label_name = "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}"
+ list_label = FactoryGirl.create(:label, title: label_name, project: issue.project)
+ FactoryGirl.create(:list, board: FactoryGirl.create(:board, project: issue.project), label: list_label)
+ issue.update(labels: [list_label])
+ end
+
+ issue
+ end
+ end
+
+ def mention_in_commits(issues)
+ issues.map do |issue|
+ Timecop.travel 12.hours.from_now
+
+ branch_name = filename = "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}"
+
+ issue.project.repository.add_branch(@user, branch_name, 'master')
+
+ options = {
+ committer: issue.project.repository.user_to_committer(@user),
+ author: issue.project.repository.user_to_committer(@user),
+ commit: { message: "Commit for ##{issue.iid}", branch: branch_name, update_ref: true },
+ file: { content: "content", path: filename, update: false }
+ }
+
+ commit_sha = Gitlab::Git::Blob.commit(issue.project.repository, options)
+ issue.project.repository.commit(commit_sha)
+
+
+ GitPushService.new(issue.project,
+ @user,
+ oldrev: issue.project.repository.commit("master").sha,
+ newrev: commit_sha,
+ ref: 'refs/heads/master').execute
+
+ branch_name
+ end
+ end
+
+ def create_merge_requests_closing_issues(issues, branches)
+ issues.zip(branches).map do |issue, branch|
+ Timecop.travel 12.hours.from_now
+
+ opts = {
+ title: 'Cycle Analytics merge_request',
+ description: "Fixes #{issue.to_reference}",
+ source_branch: branch,
+ target_branch: 'master'
+ }
+
+ MergeRequests::CreateService.new(issue.project, @user, opts).execute
+ end
+ end
+
+ def run_builds(merge_requests)
+ merge_requests.each do |merge_request|
+ Timecop.travel 12.hours.from_now
+
+ service = Ci::CreatePipelineService.new(merge_request.project,
+ @user,
+ ref: "refs/heads/#{merge_request.source_branch}")
+ pipeline = service.execute(ignore_skip_ci: true, save_on_errors: false)
+
+ pipeline.run!
+ Timecop.travel rand(1..6).hours.from_now
+ pipeline.succeed!
+ end
+ end
+
+ def merge_merge_requests(merge_requests)
+ merge_requests.each do |merge_request|
+ Timecop.travel 12.hours.from_now
+
+ MergeRequests::MergeService.new(merge_request.project, @user).execute(merge_request)
+ end
+ end
+
+ def deploy_to_production(merge_requests)
+ merge_requests.each do |merge_request|
+ Timecop.travel 12.hours.from_now
+
+ CreateDeploymentService.new(merge_request.project, @user, {
+ environment: 'production',
+ ref: 'master',
+ tag: false,
+ sha: @project.repository.commit('master').sha
+ }).execute
+ end
+ end
+end
+
+Gitlab::Seeder.quiet do
+ if ENV['SEED_CYCLE_ANALYTICS']
+ Project.all.each do |project|
+ seeder = Gitlab::Seeder::CycleAnalytics.new(project)
+ seeder.seed!
+ end
+ elsif ENV['CYCLE_ANALYTICS_PERF_TEST']
+ seeder = Gitlab::Seeder::CycleAnalytics.new(Project.order(:id).first, perf: true)
+ seeder.seed!
+ elsif ENV['CYCLE_ANALYTICS_POPULATE_METRICS_DIRECTLY']
+ seeder = Gitlab::Seeder::CycleAnalytics.new(Project.order(:id).first, perf: true)
+ seeder.seed_metrics!
+ else
+ puts "Not running the cycle analytics seed file. Use the `SEED_CYCLE_ANALYTICS` environment variable to enable it."
+ end
+end
diff --git a/db/migrate/20140502125220_migrate_repo_size.rb b/db/migrate/20140502125220_migrate_repo_size.rb
index 84463727b3b..e8de7ccf3db 100644
--- a/db/migrate/20140502125220_migrate_repo_size.rb
+++ b/db/migrate/20140502125220_migrate_repo_size.rb
@@ -1,12 +1,15 @@
# rubocop:disable all
class MigrateRepoSize < ActiveRecord::Migration
+ DOWNTIME = false
+
def up
project_data = execute('SELECT projects.id, namespaces.path AS namespace_path, projects.path AS project_path FROM projects LEFT JOIN namespaces ON projects.namespace_id = namespaces.id')
project_data.each do |project|
id = project['id']
namespace_path = project['namespace_path'] || ''
- path = File.join(Gitlab.config.gitlab_shell.repos_path, namespace_path, project['project_path'] + '.git')
+ repos_path = Gitlab.config.gitlab_shell['repos_path'] || Gitlab.config.repositories.storages.default
+ path = File.join(repos_path, namespace_path, project['project_path'] + '.git')
begin
repo = Gitlab::Git::Repository.new(path)
diff --git a/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb b/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb
index c8cbd2718ff..75a3eb15124 100644
--- a/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb
+++ b/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb
@@ -8,14 +8,28 @@ class MergeRequestDiffRemoveUniq < ActiveRecord::Migration
DOWNTIME = false
def up
- if index_exists?(:merge_request_diffs, :merge_request_id)
- remove_index :merge_request_diffs, :merge_request_id
+ constraint_name = 'merge_request_diffs_merge_request_id_key'
+
+ transaction do
+ if index_exists?(:merge_request_diffs, :merge_request_id)
+ remove_index(:merge_request_diffs, :merge_request_id)
+ end
+
+ # In some bizarre cases PostgreSQL might have a separate unique constraint
+ # that we'll need to drop.
+ if constraint_exists?(constraint_name) && Gitlab::Database.postgresql?
+ execute("ALTER TABLE merge_request_diffs DROP CONSTRAINT IF EXISTS #{constraint_name};")
+ end
end
end
def down
unless index_exists?(:merge_request_diffs, :merge_request_id)
- add_concurrent_index :merge_request_diffs, :merge_request_id, unique: true
+ add_concurrent_index(:merge_request_diffs, :merge_request_id, unique: true)
end
end
+
+ def constraint_exists?(name)
+ indexes(:merge_request_diffs).map(&:name).include?(name)
+ end
end
diff --git a/db/migrate/20160808085531_add_token_to_build.rb b/db/migrate/20160808085531_add_token_to_build.rb
new file mode 100644
index 00000000000..3ed2a103ae3
--- /dev/null
+++ b/db/migrate/20160808085531_add_token_to_build.rb
@@ -0,0 +1,10 @@
+class AddTokenToBuild < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ add_column :ci_builds, :token, :string
+ end
+end
diff --git a/db/migrate/20160808085602_add_index_for_build_token.rb b/db/migrate/20160808085602_add_index_for_build_token.rb
new file mode 100644
index 00000000000..10ef42afce1
--- /dev/null
+++ b/db/migrate/20160808085602_add_index_for_build_token.rb
@@ -0,0 +1,12 @@
+class AddIndexForBuildToken < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index :ci_builds, :token, unique: true
+ end
+end
diff --git a/db/migrate/20160824124900_add_table_issue_metrics.rb b/db/migrate/20160824124900_add_table_issue_metrics.rb
new file mode 100644
index 00000000000..e9bb79b3c62
--- /dev/null
+++ b/db/migrate/20160824124900_add_table_issue_metrics.rb
@@ -0,0 +1,37 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddTableIssueMetrics < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = true
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ DOWNTIME_REASON = 'Adding foreign key'
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ create_table :issue_metrics do |t|
+ t.references :issue, index: { name: "index_issue_metrics" }, foreign_key: { on_delete: :cascade }, null: false
+
+ t.datetime 'first_mentioned_in_commit_at'
+ t.datetime 'first_associated_with_milestone_at'
+ t.datetime 'first_added_to_board_at'
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20160825052008_add_table_merge_request_metrics.rb b/db/migrate/20160825052008_add_table_merge_request_metrics.rb
new file mode 100644
index 00000000000..e01cc5038b9
--- /dev/null
+++ b/db/migrate/20160825052008_add_table_merge_request_metrics.rb
@@ -0,0 +1,38 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddTableMergeRequestMetrics < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = true
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ DOWNTIME_REASON = 'Adding foreign key'
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ create_table :merge_request_metrics do |t|
+ t.references :merge_request, index: { name: "index_merge_request_metrics" }, foreign_key: { on_delete: :cascade }, null: false
+
+ t.datetime 'latest_build_started_at'
+ t.datetime 'latest_build_finished_at'
+ t.datetime 'first_deployed_to_production_at', index: true
+ t.datetime 'merged_at'
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20160901213340_add_lfs_enabled_to_namespaces.rb b/db/migrate/20160901213340_add_lfs_enabled_to_namespaces.rb
new file mode 100644
index 00000000000..fd413d1ca8c
--- /dev/null
+++ b/db/migrate/20160901213340_add_lfs_enabled_to_namespaces.rb
@@ -0,0 +1,12 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddLfsEnabledToNamespaces < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :namespaces, :lfs_enabled, :boolean
+ end
+end
diff --git a/db/migrate/20160907131111_add_environment_type_to_environments.rb b/db/migrate/20160907131111_add_environment_type_to_environments.rb
new file mode 100644
index 00000000000..fac73753d5b
--- /dev/null
+++ b/db/migrate/20160907131111_add_environment_type_to_environments.rb
@@ -0,0 +1,9 @@
+class AddEnvironmentTypeToEnvironments < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :environments, :environment_type, :string
+ end
+end
diff --git a/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb b/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb
index c5b8c35e961..18ea9d43a43 100644
--- a/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb
+++ b/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb
@@ -14,6 +14,6 @@ class RemoveProjectsPushesSinceGc < ActiveRecord::Migration
end
def down
- add_column_with_default! :projects, :pushes_since_gc, :integer, default: 0
+ add_column_with_default :projects, :pushes_since_gc, :integer, default: 0
end
end
diff --git a/db/migrate/20160913212128_change_artifacts_size_column.rb b/db/migrate/20160913212128_change_artifacts_size_column.rb
new file mode 100644
index 00000000000..063bbca537c
--- /dev/null
+++ b/db/migrate/20160913212128_change_artifacts_size_column.rb
@@ -0,0 +1,15 @@
+class ChangeArtifactsSizeColumn < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+
+ DOWNTIME_REASON = 'Changing an integer column size requires a full table rewrite.'
+
+ def up
+ change_column :ci_builds, :artifacts_size, :integer, limit: 8
+ end
+
+ def down
+ # do nothing
+ end
+end
diff --git a/db/migrate/20160915042921_create_merge_requests_closing_issues.rb b/db/migrate/20160915042921_create_merge_requests_closing_issues.rb
new file mode 100644
index 00000000000..94874a853da
--- /dev/null
+++ b/db/migrate/20160915042921_create_merge_requests_closing_issues.rb
@@ -0,0 +1,34 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CreateMergeRequestsClosingIssues < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = true
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ DOWNTIME_REASON = 'Adding foreign keys'
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ create_table :merge_requests_closing_issues do |t|
+ t.references :merge_request, foreign_key: { on_delete: :cascade }, index: true, null: false
+ t.references :issue, foreign_key: { on_delete: :cascade }, index: true, null: false
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20160920160832_add_index_to_labels_title.rb b/db/migrate/20160920160832_add_index_to_labels_title.rb
new file mode 100644
index 00000000000..b5de552b98c
--- /dev/null
+++ b/db/migrate/20160920160832_add_index_to_labels_title.rb
@@ -0,0 +1,11 @@
+class AddIndexToLabelsTitle < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index :labels, :title
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 61873e38113..425fc33b7b3 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20160913162434) do
+ActiveRecord::Schema.define(version: 20160920160832) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -158,9 +158,9 @@ ActiveRecord::Schema.define(version: 20160913162434) do
t.text "commands"
t.integer "job_id"
t.string "name"
- t.boolean "deploy", default: false
+ t.boolean "deploy", default: false
t.text "options"
- t.boolean "allow_failure", default: false, null: false
+ t.boolean "allow_failure", default: false, null: false
t.string "stage"
t.integer "trigger_request_id"
t.integer "stage_idx"
@@ -177,10 +177,11 @@ ActiveRecord::Schema.define(version: 20160913162434) do
t.datetime "erased_at"
t.datetime "artifacts_expire_at"
t.string "environment"
- t.integer "artifacts_size"
+ t.integer "artifacts_size", limit: 8
t.string "when"
t.text "yaml_variables"
t.datetime "queued_at"
+ t.string "token"
end
add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
@@ -192,6 +193,7 @@ ActiveRecord::Schema.define(version: 20160913162434) do
add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree
+ add_index "ci_builds", ["token"], name: "index_ci_builds_on_token", unique: true, using: :btree
create_table "ci_commits", force: :cascade do |t|
t.integer "project_id"
@@ -390,10 +392,11 @@ ActiveRecord::Schema.define(version: 20160913162434) do
create_table "environments", force: :cascade do |t|
t.integer "project_id"
- t.string "name", null: false
+ t.string "name", null: false
t.datetime "created_at"
t.datetime "updated_at"
t.string "external_url"
+ t.string "environment_type"
end
add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree
@@ -436,6 +439,17 @@ ActiveRecord::Schema.define(version: 20160913162434) do
add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree
+ create_table "issue_metrics", force: :cascade do |t|
+ t.integer "issue_id", null: false
+ t.datetime "first_mentioned_in_commit_at"
+ t.datetime "first_associated_with_milestone_at"
+ t.datetime "first_added_to_board_at"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "issue_metrics", ["issue_id"], name: "index_issue_metrics", using: :btree
+
create_table "issues", force: :cascade do |t|
t.string "title"
t.integer "assignee_id"
@@ -507,6 +521,7 @@ ActiveRecord::Schema.define(version: 20160913162434) do
add_index "labels", ["priority"], name: "index_labels_on_priority", using: :btree
add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree
+ add_index "labels", ["title"], name: "index_labels_on_title", using: :btree
create_table "lfs_objects", force: :cascade do |t|
t.string "oid", null: false
@@ -578,6 +593,19 @@ ActiveRecord::Schema.define(version: 20160913162434) do
add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", using: :btree
+ create_table "merge_request_metrics", force: :cascade do |t|
+ t.integer "merge_request_id", null: false
+ t.datetime "latest_build_started_at"
+ t.datetime "latest_build_finished_at"
+ t.datetime "first_deployed_to_production_at"
+ t.datetime "merged_at"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "merge_request_metrics", ["first_deployed_to_production_at"], name: "index_merge_request_metrics_on_first_deployed_to_production_at", using: :btree
+ add_index "merge_request_metrics", ["merge_request_id"], name: "index_merge_request_metrics", using: :btree
+
create_table "merge_requests", force: :cascade do |t|
t.string "target_branch", null: false
t.string "source_branch", null: false
@@ -619,6 +647,16 @@ ActiveRecord::Schema.define(version: 20160913162434) do
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
+ create_table "merge_requests_closing_issues", force: :cascade do |t|
+ t.integer "merge_request_id", null: false
+ t.integer "issue_id", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "merge_requests_closing_issues", ["issue_id"], name: "index_merge_requests_closing_issues_on_issue_id", using: :btree
+ add_index "merge_requests_closing_issues", ["merge_request_id"], name: "index_merge_requests_closing_issues_on_merge_request_id", using: :btree
+
create_table "milestones", force: :cascade do |t|
t.string "title", null: false
t.integer "project_id", null: false
@@ -650,6 +688,7 @@ ActiveRecord::Schema.define(version: 20160913162434) do
t.integer "visibility_level", default: 20, null: false
t.boolean "request_access_enabled", default: true, null: false
t.datetime "deleted_at"
+ t.boolean "lfs_enabled"
end
add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
@@ -1143,8 +1182,12 @@ ActiveRecord::Schema.define(version: 20160913162434) do
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
add_foreign_key "boards", "projects"
+ add_foreign_key "issue_metrics", "issues", on_delete: :cascade
add_foreign_key "lists", "boards"
add_foreign_key "lists", "labels"
+ add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
+ add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade
+ add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
add_foreign_key "personal_access_tokens", "users"
add_foreign_key "protected_branch_merge_access_levels", "protected_branches"
add_foreign_key "protected_branch_push_access_levels", "protected_branches"
diff --git a/doc/README.md b/doc/README.md
index 254394eb63e..dd0eb97489e 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -29,9 +29,9 @@
- [Install](install/README.md) Requirements, directory structures and installation from source.
- [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components.
- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter.
-- [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages.
+- [Issue closing pattern](administration/issue_closing_pattern.md) Customize how to close an issue from commit messages.
- [Koding](administration/integration/koding.md) Set up Koding to use with GitLab.
-- [Libravatar](customization/libravatar.md) Use Libravatar for user avatars.
+- [Libravatar](customization/libravatar.md) Use Libravatar instead of Gravatar for user avatars.
- [Log system](administration/logs.md) Log system.
- [Environment Variables](administration/environment_variables.md) to configure GitLab.
- [Operations](operations/README.md) Keeping GitLab up and running.
diff --git a/doc/administration/issue_closing_pattern.md b/doc/administration/issue_closing_pattern.md
new file mode 100644
index 00000000000..28e1fd4e12e
--- /dev/null
+++ b/doc/administration/issue_closing_pattern.md
@@ -0,0 +1,49 @@
+# Issue closing pattern
+
+>**Note:**
+This is the administration documentation.
+There is a separate [user documentation] on issue closing pattern.
+
+When a commit or merge request resolves one or more issues, it is possible to
+automatically have these issues closed when the commit or merge request lands
+in the project's default branch.
+
+## Change the issue closing pattern
+
+In order to change the pattern you need to have access to the server that GitLab
+is installed on.
+
+The default pattern can be located in [gitlab.yml.example] under the
+"Automatic issue closing" section.
+
+> **Tip:**
+You are advised to use http://rubular.com to test the issue closing pattern.
+Because Rubular doesn't understand `%{issue_ref}`, you can replace this by
+`#\d+` when testing your patterns, which matches only local issue references like `#123`.
+
+**For Omnibus installations**
+
+1. Open `/etc/gitlab/gitlab.rb` with your editor.
+1. Change the value of `gitlab_rails['issue_closing_pattern']` to a regular
+ expression of your liking:
+
+ ```ruby
+ gitlab_rails['issue_closing_pattern'] = "((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)"
+ ```
+1. [Reconfigure] GitLab for the changes to take effect.
+
+**For installations from source**
+
+1. Open `gitlab.yml` with your editor.
+1. Change the value of `issue_closing_pattern`:
+
+ ```yaml
+ issue_closing_pattern: "((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)"
+ ```
+
+1. [Restart] GitLab for the changes to take effect.
+
+[gitlab.yml.example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example
+[reconfigure]: restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart]: restart_gitlab.md#installations-from-source
+[user documentation]: ../user/project/issues/automatic_issue_closing.md
diff --git a/doc/api/README.md b/doc/api/README.md
index 7661e1eea02..8e4f7f12b4b 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -10,6 +10,7 @@ following locations:
- [Award Emoji](award_emoji.md)
- [Branches](branches.md)
+- [Broadcast Messages](broadcast_messages.md)
- [Builds](builds.md)
- [Build Triggers](build_triggers.md)
- [Build Variables](build_variables.md)
@@ -55,11 +56,12 @@ The following documentation is for the [internal CI API](ci/README.md):
## Authentication
-All API requests require authentication via a token. There are three types of tokens
-available: private tokens, OAuth 2 tokens, and personal access tokens.
+All API requests require authentication via a session cookie or token. There are
+three types of tokens available: private tokens, OAuth 2 tokens, and personal
+access tokens.
-If a token is invalid or omitted, an error message will be returned with
-status code `401`:
+If authentication information is invalid or omitted, an error message will be
+returned with status code `401`:
```json
{
@@ -98,6 +100,13 @@ that needs access to the GitLab API.
Once you have your token, pass it to the API using either the `private_token`
parameter or the `PRIVATE-TOKEN` header.
+
+### Session cookie
+
+When signing in to GitLab as an ordinary user, a `_gitlab_session` cookie is
+set. The API will use this cookie for authentication if it is present, but using
+the API to generate a new session cookie is currently not supported.
+
## Basic Usage
API requests should be prefixed with `api` and the API version. The API version
diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md
index 72ec99b7c56..c464e3f3f71 100644
--- a/doc/api/award_emoji.md
+++ b/doc/api/award_emoji.md
@@ -1,12 +1,13 @@
# Award Emoji
-> [Introduced][ce-4575] in GitLab 8.9.
+> [Introduced][ce-4575] in GitLab 8.9, Snippet support in 8.12
+
An awarded emoji tells a thousand words, and can be awarded on issues, merge
-requests and notes/comments. Issues, merge requests and notes are further called
+requests, snippets, and notes/comments. Issues, merge requests, snippets, and notes are further called
`awardables`.
-## Issues and merge requests
+## Issues, merge requests, and snippets
### List an awardable's award emoji
@@ -15,6 +16,7 @@ Gets a list of all award emoji
```
GET /projects/:id/issues/:issue_id/award_emoji
GET /projects/:id/merge_requests/:merge_request_id/award_emoji
+GET /projects/:id/snippets/:snippet_id/award_emoji
```
Parameters:
@@ -69,11 +71,12 @@ Example Response:
### Get single award emoji
-Gets a single award emoji from an issue or merge request.
+Gets a single award emoji from an issue, snippet, or merge request.
```
GET /projects/:id/issues/:issue_id/award_emoji/:award_id
GET /projects/:id/merge_requests/:merge_request_id/award_emoji/:award_id
+GET /projects/:id/snippets/:snippet_id/award_emoji/:award_id
```
Parameters:
@@ -116,6 +119,7 @@ This end point creates an award emoji on the specified resource
```
POST /projects/:id/issues/:issue_id/award_emoji
POST /projects/:id/merge_requests/:merge_request_id/award_emoji
+POST /projects/:id/snippets/:snippet_id/award_emoji
```
Parameters:
@@ -159,6 +163,7 @@ admins or the author of the award. Status code 200 on success, 401 if unauthoriz
```
DELETE /projects/:id/issues/:issue_id/award_emoji/:award_id
DELETE /projects/:id/merge_requests/:merge_request_id/award_emoji/:award_id
+DELETE /projects/:id/snippets/:snippet_id/award_emoji/:award_id
```
Parameters:
@@ -197,7 +202,7 @@ Example Response:
## Award Emoji on Notes
The endpoints documented above are available for Notes as well. Notes
-are a sub-resource of Issues and Merge Requests. The examples below
+are a sub-resource of Issues, Merge Requests, or Snippets. The examples below
describe working with Award Emoji on notes for an Issue, but can be
easily adapted for notes on a Merge Request.
diff --git a/doc/api/groups.md b/doc/api/groups.md
index a898387eaa2..e81d6f9de4b 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -84,7 +84,8 @@ Parameters:
"forks_count": 0,
"open_issues_count": 3,
"public_builds": true,
- "shared_with_groups": []
+ "shared_with_groups": [],
+ "request_access_enabled": false
}
]
```
@@ -118,6 +119,7 @@ Example response:
"visibility_level": 20,
"avatar_url": null,
"web_url": "https://gitlab.example.com/groups/twitter",
+ "request_access_enabled": false,
"projects": [
{
"id": 7,
@@ -163,7 +165,8 @@ Example response:
"forks_count": 0,
"open_issues_count": 3,
"public_builds": true,
- "shared_with_groups": []
+ "shared_with_groups": [],
+ "request_access_enabled": false
},
{
"id": 6,
@@ -209,7 +212,8 @@ Example response:
"forks_count": 0,
"open_issues_count": 8,
"public_builds": true,
- "shared_with_groups": []
+ "shared_with_groups": [],
+ "request_access_enabled": false
}
],
"shared_projects": [
@@ -288,6 +292,8 @@ Parameters:
- `path` (required) - The path of the group
- `description` (optional) - The group's description
- `visibility_level` (optional) - The group's visibility. 0 for private, 10 for internal, 20 for public.
+- `lfs_enabled` (optional) - Enable/disable Large File Storage (LFS) for the projects in this group
+- `request_access_enabled` (optional) - Allow users to request member access.
## Transfer project to group
@@ -317,6 +323,8 @@ PUT /groups/:id
| `path` | string | no | The path of the group |
| `description` | string | no | The description of the group |
| `visibility_level` | integer | no | The visibility level of the group. 0 for private, 10 for internal, 20 for public. |
+| `lfs_enabled` (optional) | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group |
+| `request_access_enabled` | boolean | no | Allow users to request member access. |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/groups/5?name=Experimental"
@@ -334,6 +342,7 @@ Example response:
"visibility_level": 10,
"avatar_url": null,
"web_url": "http://gitlab.example.com/groups/h5bp",
+ "request_access_enabled": false,
"projects": [
{
"id": 9,
@@ -378,7 +387,8 @@ Example response:
"forks_count": 0,
"open_issues_count": 3,
"public_builds": true,
- "shared_with_groups": []
+ "shared_with_groups": [],
+ "request_access_enabled": false
}
]
}
diff --git a/doc/api/notes.md b/doc/api/notes.md
index 85d140d06ac..572844b8b3f 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -78,7 +78,8 @@ Parameters:
### Create new issue note
-Creates a new note to a single project issue.
+Creates a new note to a single project issue. If you create a note where the body
+only contains an Award Emoji, you'll receive this object back.
```
POST /projects/:id/issues/:issue_id/notes
@@ -204,6 +205,7 @@ Parameters:
### Create new snippet note
Creates a new note for a single snippet. Snippet notes are comments users can post to a snippet.
+If you create a note where the body only contains an Award Emoji, you'll receive this object back.
```
POST /projects/:id/snippets/:snippet_id/notes
@@ -332,6 +334,8 @@ Parameters:
### Create new merge request note
Creates a new note for a single merge request.
+If you create a note where the body only contains an Award Emoji, you'll receive
+this object back.
```
POST /projects/:id/merge_requests/:merge_request_id/notes
diff --git a/doc/api/projects.md b/doc/api/projects.md
index fe3c8709d13..750ce1508df 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -85,7 +85,8 @@ Parameters:
"runners_token": "b8547b1dc37721d05889db52fa2f02",
"public_builds": true,
"shared_with_groups": [],
- "only_allow_merge_if_build_succeeds": false
+ "only_allow_merge_if_build_succeeds": false,
+ "request_access_enabled": false
},
{
"id": 6,
@@ -146,7 +147,8 @@ Parameters:
"runners_token": "b8547b1dc37721d05889db52fa2f02",
"public_builds": true,
"shared_with_groups": [],
- "only_allow_merge_if_build_succeeds": false
+ "only_allow_merge_if_build_succeeds": false,
+ "request_access_enabled": false
}
]
```
@@ -283,7 +285,8 @@ Parameters:
"group_access_level": 10
}
],
- "only_allow_merge_if_build_succeeds": false
+ "only_allow_merge_if_build_succeeds": false,
+ "request_access_enabled": false
}
```
@@ -453,6 +456,7 @@ Parameters:
- `public_builds` (optional)
- `only_allow_merge_if_build_succeeds` (optional)
- `lfs_enabled` (optional)
+- `request_access_enabled` (optional) - Allow users to request member access.
### Create project for user
@@ -480,6 +484,7 @@ Parameters:
- `public_builds` (optional)
- `only_allow_merge_if_build_succeeds` (optional)
- `lfs_enabled` (optional)
+- `request_access_enabled` (optional) - Allow users to request member access.
### Edit project
@@ -508,6 +513,7 @@ Parameters:
- `public_builds` (optional)
- `only_allow_merge_if_build_succeeds` (optional)
- `lfs_enabled` (optional)
+- `request_access_enabled` (optional) - Allow users to request member access.
On success, method returns 200 with the updated project. If parameters are
invalid, 400 is returned.
@@ -588,7 +594,8 @@ Example response:
"star_count": 1,
"public_builds": true,
"shared_with_groups": [],
- "only_allow_merge_if_build_succeeds": false
+ "only_allow_merge_if_build_succeeds": false,
+ "request_access_enabled": false
}
```
@@ -655,7 +662,8 @@ Example response:
"star_count": 0,
"public_builds": true,
"shared_with_groups": [],
- "only_allow_merge_if_build_succeeds": false
+ "only_allow_merge_if_build_succeeds": false,
+ "request_access_enabled": false
}
```
@@ -742,7 +750,8 @@ Example response:
"runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b",
"public_builds": true,
"shared_with_groups": [],
- "only_allow_merge_if_build_succeeds": false
+ "only_allow_merge_if_build_succeeds": false,
+ "request_access_enabled": false
}
```
@@ -829,7 +838,8 @@ Example response:
"runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b",
"public_builds": true,
"shared_with_groups": [],
- "only_allow_merge_if_build_succeeds": false
+ "only_allow_merge_if_build_succeeds": false,
+ "request_access_enabled": false
}
```
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index fc3af5544de..1bc6a24e914 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -44,7 +44,7 @@ POST /projects/:id/repository/files
```
```bash
-curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&content=some%20content&commit_message=create%20a%20new%20file'
+curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20content&commit_message=create%20a%20new%20file'
```
Example response:
@@ -61,6 +61,8 @@ Parameters:
- `file_path` (required) - Full path to new file. Ex. lib/class.rb
- `branch_name` (required) - The name of branch
- `encoding` (optional) - 'text' or 'base64'. Text is default.
+- `author_email` (optional) - Specify the commit author's email address
+- `author_name` (optional) - Specify the commit author's name
- `content` (required) - File content
- `commit_message` (required) - Commit message
@@ -71,7 +73,7 @@ PUT /projects/:id/repository/files
```
```bash
-curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&content=some%20other%20content&commit_message=update%20file'
+curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20other%20content&commit_message=update%20file'
```
Example response:
@@ -88,6 +90,8 @@ Parameters:
- `file_path` (required) - Full path to file. Ex. lib/class.rb
- `branch_name` (required) - The name of branch
- `encoding` (optional) - 'text' or 'base64'. Text is default.
+- `author_email` (optional) - Specify the commit author's email address
+- `author_name` (optional) - Specify the commit author's name
- `content` (required) - New file content
- `commit_message` (required) - Commit message
@@ -107,7 +111,7 @@ DELETE /projects/:id/repository/files
```
```bash
-curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&commit_message=delete%20file'
+curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file'
```
Example response:
@@ -123,4 +127,6 @@ Parameters:
- `file_path` (required) - Full path to file. Ex. lib/class.rb
- `branch_name` (required) - The name of branch
+- `author_email` (optional) - Specify the commit author's email address
+- `author_name` (optional) - Specify the commit author's name
- `commit_message` (required) - Commit message
diff --git a/doc/api/settings.md b/doc/api/settings.md
index a76dad0ebd4..aaa2c99642b 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -67,7 +67,7 @@ PUT /application/settings
| `default_snippet_visibility` | integer | no | What visibility level new snippets receive. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is `0`.|
| `domain_whitelist` | array of strings | no | Force people to use only corporate emails for sign-up. Default is null, meaning there is no restriction. |
| `domain_blacklist_enabled` | boolean | no | Enable/disable the `domain_blacklist` |
-| `domain_blacklist` | array of strings | yes (if `domain_whitelist_enabled` is `true` | People trying to sign-up with emails from this domain will not be allowed to do so. |
+| `domain_blacklist` | array of strings | yes (if `domain_blacklist_enabled` is `true`) | People trying to sign-up with emails from this domain will not be allowed to do so. |
| `user_oauth_applications` | boolean | no | Allow users to register any application to use GitLab as an OAuth provider |
| `after_sign_out_path` | string | no | Where to redirect users after logout |
| `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes |
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 10ce4ac8940..341bc85a16a 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -16,5 +16,7 @@
- [Trigger builds through the API](triggers/README.md)
- [Build artifacts](../user/project/builds/artifacts.md)
- [User permissions](../user/permissions.md#gitlab-ci)
+- [Build permissions](../user/permissions.md#build-permissions)
- [API](../api/ci/README.md)
- [CI services (linked docker containers)](services/README.md)
+- [**New CI build permissions model**](../user/project/new_ci_build_permissions_model.md) Read about what changed in GitLab 8.12 and how that affects your builds. There's a new way to access your Git submodules and LFS objects in builds.
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index 406396deaaa..71670e6247c 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -16,4 +16,4 @@ Apart from those, here is an collection of tutorials and guides on setting up yo
- [Repo's with examples for various languages](https://gitlab.com/groups/gitlab-examples)
- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
-[gitlab-ci-templates][https://gitlab.com/gitlab-org/gitlab-ci-yml]
+[gitlab-ci-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index c835ebc2d44..c40cdd55ea5 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -105,7 +105,8 @@ What is important is that each job is run independently from each other.
If you want to check whether your `.gitlab-ci.yml` file is valid, there is a
Lint tool under the page `/ci/lint` of your GitLab instance. You can also find
-the link under **Settings > CI settings** in your project.
+a "CI Lint" button to go to this page under **Pipelines > Pipelines** and
+**Pipelines > Builds** in your project.
For more information and a complete `.gitlab-ci.yml` syntax, please read
[the documentation on .gitlab-ci.yml](../yaml/README.md).
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index 6c6767fea0b..b78422f6d0e 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -2,6 +2,10 @@
> [Introduced][ci-229] in GitLab CE 7.14.
+> **Note**:
+GitLab 8.12 has a completely redesigned build permissions system.
+Read all about the [new model and its implications][../../user/project/new_ci_build_permissions_model.md#build-triggers].
+
Triggers can be used to force a rebuild of a specific branch, tag or commit,
with an API call.
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index ff4c8ddc54b..16868554c1f 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -90,8 +90,7 @@ builds, including deploy builds. This can be an array or a multi-line string.
### after_script
->**Note:**
-Introduced in GitLab 8.7 and requires Gitlab Runner v1.2
+> Introduced in GitLab 8.7 and requires Gitlab Runner v1.2
`after_script` is used to define the command that will be run after for all
builds. This has to be an array or a multi-line string.
@@ -135,8 +134,7 @@ Alias for [stages](#stages).
### variables
->**Note:**
-Introduced in GitLab Runner v0.5.0.
+> Introduced in GitLab Runner v0.5.0.
GitLab CI allows you to add variables to `.gitlab-ci.yml` that are set in the
build environment. The variables are stored in the Git repository and are meant
@@ -158,8 +156,7 @@ Variables can be also defined on [job level](#job-variables).
### cache
->**Note:**
-Introduced in GitLab Runner v0.7.0.
+> Introduced in GitLab Runner v0.7.0.
`cache` is used to specify a list of files and directories which should be
cached between builds.
@@ -220,8 +217,7 @@ will be always present. For implementation details, please check GitLab Runner.
#### cache:key
->**Note:**
-Introduced in GitLab Runner v1.0.0.
+> Introduced in GitLab Runner v1.0.0.
The `key` directive allows you to define the affinity of caching
between jobs, allowing to have a single cache for all jobs,
@@ -531,8 +527,7 @@ The above script will:
#### Manual actions
->**Note:**
-Introduced in GitLab 8.10.
+> Introduced in GitLab 8.10.
Manual actions are a special type of job that are not executed automatically;
they need to be explicitly started by a user. Manual actions can be started
@@ -543,17 +538,16 @@ An example usage of manual actions is deployment to production.
### environment
->**Note:**
-Introduced in GitLab 8.9.
+> Introduced in GitLab 8.9.
-`environment` is used to define that a job deploys to a specific environment.
+`environment` is used to define that a job deploys to a specific [environment].
This allows easy tracking of all deployments to your environments straight from
GitLab.
If `environment` is specified and no environment under that name exists, a new
one will be created automatically.
-The `environment` name must contain only letters, digits, '-' and '_'. Common
+The `environment` name must contain only letters, digits, '-', '_', '/', '$', '{', '}' and spaces. Common
names are `qa`, `staging`, and `production`, but you can use whatever name works
with your workflow.
@@ -571,6 +565,35 @@ deploy to production:
The `deploy to production` job will be marked as doing deployment to
`production` environment.
+#### dynamic environments
+
+> [Introduced][ce-6323] in GitLab 8.12 and GitLab Runner 1.6.
+
+`environment` can also represent a configuration hash with `name` and `url`.
+These parameters can use any of the defined CI [variables](#variables)
+(including predefined, secure variables and `.gitlab-ci.yml` variables).
+
+The common use case is to create dynamic environments for branches and use them
+as review apps.
+
+---
+
+**Example configurations**
+
+```
+deploy as review app:
+ stage: deploy
+ script: ...
+ environment:
+ name: review-apps/$CI_BUILD_REF_NAME
+ url: https://$CI_BUILD_REF_NAME.review.example.com/
+```
+
+The `deploy as review app` job will be marked as deployment to dynamically
+create the `review-apps/branch-name` environment.
+
+This environment should be accessible under `https://branch-name.review.example.com/`.
+
### artifacts
>**Notes:**
@@ -638,8 +661,7 @@ be available for download in the GitLab UI.
#### artifacts:name
->**Note:**
-Introduced in GitLab 8.6 and GitLab Runner v1.1.0.
+> Introduced in GitLab 8.6 and GitLab Runner v1.1.0.
The `name` directive allows you to define the name of the created artifacts
archive. That way, you can have a unique name for every archive which could be
@@ -702,8 +724,7 @@ job:
#### artifacts:when
->**Note:**
-Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
+> Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
`artifacts:when` is used to upload artifacts on build failure or despite the
failure.
@@ -728,8 +749,7 @@ job:
#### artifacts:expire_in
->**Note:**
-Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
+> Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
`artifacts:expire_in` is used to delete uploaded artifacts after the specified
time. By default, artifacts are stored on GitLab forever. `expire_in` allows you
@@ -764,8 +784,7 @@ job:
### dependencies
->**Note:**
-Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
+> Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
This feature should be used in conjunction with [`artifacts`](#artifacts) and
allows you to define the artifacts to pass between different builds.
@@ -839,9 +858,8 @@ job:
## Git Strategy
->**Note:**
-Introduced in GitLab 8.9 as an experimental feature. May change in future
-releases or be removed completely.
+> Introduced in GitLab 8.9 as an experimental feature. May change in future
+ releases or be removed completely.
You can set the `GIT_STRATEGY` used for getting recent application code. `clone`
is slower, but makes sure you have a clean directory before every build. `fetch`
@@ -863,8 +881,7 @@ variables:
## Shallow cloning
->**Note:**
-Introduced in GitLab 8.9 as an experimental feature. May change in future
+> Introduced in GitLab 8.9 as an experimental feature. May change in future
releases or be removed completely.
You can specify the depth of fetching and cloning using `GIT_DEPTH`. This allows
@@ -894,8 +911,7 @@ variables:
## Hidden keys
->**Note:**
-Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
+> Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
Keys that start with a dot (`.`) will be not processed by GitLab CI. You can
use this feature to ignore jobs, or use the
@@ -923,8 +939,7 @@ Read more about the various [YAML features](https://learnxinyminutes.com/docs/ya
### Anchors
->**Note:**
-Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
+> Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
YAML also has a handy feature called 'anchors', which let you easily duplicate
content across your document. Anchors can be used to duplicate/inherit
@@ -1067,3 +1082,5 @@ Visit the [examples README][examples] to see a list of examples using GitLab
CI with various languages.
[examples]: ../examples/README.md
+[ce-6323]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6323
+[environment]: ../environments.md
diff --git a/doc/container_registry/README.md b/doc/container_registry/README.md
index 047a0b08406..d7740647a91 100644
--- a/doc/container_registry/README.md
+++ b/doc/container_registry/README.md
@@ -78,9 +78,9 @@ delete them.
> **Note:**
This feature requires GitLab 8.8 and GitLab Runner 1.2.
-Make sure that your GitLab Runner is configured to allow building docker images.
-You have to check the [Using Docker Build documentation](../ci/docker/using_docker_build.md).
-Then see the CI documentation on [Using the GitLab Container Registry](../ci/docker/using_docker_build.md#using-the-gitlab-container-registry).
+Make sure that your GitLab Runner is configured to allow building Docker images by
+following the [Using Docker Build](../ci/docker/using_docker_build.md)
+and [Using the GitLab Container Registry documentation](../ci/docker/using_docker_build.md#using-the-gitlab-container-registry).
## Limitations
diff --git a/doc/customization/issue_closing.md b/doc/customization/issue_closing.md
index 4620bb2dcde..31164ccd465 100644
--- a/doc/customization/issue_closing.md
+++ b/doc/customization/issue_closing.md
@@ -1,39 +1,4 @@
-# Issue closing pattern
+This document was split into:
-When a commit or merge request resolves one or more issues, it is possible to automatically have these issues closed when the commit or merge request lands in the project's default branch.
-
-If a commit message or merge request description contains a sentence matching the regular expression below, all issues referenced from
-the matched text will be closed. This happens when the commit is pushed to a project's default branch, or when a commit or merge request is merged into there.
-
-When not specified, the default `issue_closing_pattern` as shown below will be used:
-
-```bash
-((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)
-```
-
-Here, `%{issue_ref}` is a complex regular expression defined inside GitLab, that matches a reference to a local issue (`#123`), cross-project issue (`group/project#123`) or a link to an issue (`https://gitlab.example.com/group/project/issues/123`).
-
-For example:
-
-```
-git commit -m "Awesome commit message (Fix #20, Fixes #21 and Closes group/otherproject#22). This commit is also related to #17 and fixes #18, #19 and https://gitlab.example.com/group/otherproject/issues/23."
-```
-
-will close `#18`, `#19`, `#20`, and `#21` in the project this commit is pushed to, as well as `#22` and `#23` in group/otherproject. `#17` won't be closed as it does not match the pattern. It also works with multiline commit messages.
-
-Tip: you can test this closing pattern at [http://rubular.com][1]. Use this site
-to test your own patterns.
-Because Rubular doesn't understand `%{issue_ref}`, you can replace this by `#\d+` in testing, which matches only local issue references like `#123`.
-
-## Change the pattern
-
-For Omnibus installs you can change the default pattern in `/etc/gitlab/gitlab.rb`:
-
-```
-issue_closing_pattern: '((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)'
-```
-
-For manual installs you can customize the pattern in [gitlab.yml][0] using the `issue_closing_pattern` key.
-
-[0]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example
-[1]: http://rubular.com/r/Xmbexed1OJ
+- [administration/issue_closing_pattern.md](../administration/issue_closing_pattern.md).
+- [user/project/issues/automatic_issue_closing](../user/project/issues/automatic_issue_closing.md).
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index b8fab3aaff7..295eae0a88e 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -111,6 +111,28 @@ class MyMigration < ActiveRecord::Migration
end
```
+
+## Integer column type
+
+By default, an integer column can hold up to a 4-byte (32-bit) number. That is
+a max value of 2,147,483,647. Be aware of this when creating a column that will
+hold file sizes in byte units. If you are tracking file size in bytes this
+restricts the maximum file size to just over 2GB.
+
+To allow an integer column to hold up to an 8-byte (64-bit) number, explicitly
+set the limit to 8-bytes. This will allow the column to hold a value up to
+9,223,372,036,854,775,807.
+
+Rails migration example:
+
+```
+add_column_with_default(:projects, :foo, :integer, default: 10, limit: 8)
+
+# or
+
+add_column(:projects, :foo, :integer, default: 10, limit: 8)
+```
+
## Testing
Make sure that your migration works with MySQL and PostgreSQL with data. An empty database does not guarantee that your migration is correct.
diff --git a/doc/gitlab-basics/create-issue.md b/doc/gitlab-basics/create-issue.md
index 5221d85b661..da9a165b8f5 100644
--- a/doc/gitlab-basics/create-issue.md
+++ b/doc/gitlab-basics/create-issue.md
@@ -1,6 +1,6 @@
# How to create an Issue in GitLab
-The Issue Tracker is a good place to add things that need to be improved or solved in a project.
+The Issue Tracker is a good place to add things that need to be improved or solved in a project.
To create an Issue, sign in to GitLab.
@@ -24,4 +24,4 @@ You may assign the Issue to a user, add a milestone and add labels (they are all
![Submit new issue](basicsimages/submit_new_issue.png)
-Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](http://docs.gitlab.com/ce/customization/issue_closing.html).
+Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](../user/project/issues/automatic_issue_closing.md).
diff --git a/doc/install/installation.md b/doc/install/installation.md
index df98655c396..3ac813aa914 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -400,7 +400,7 @@ If you are not using Linux you may have to run `gmake` instead of
cd /home/git
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git
cd gitlab-workhorse
- sudo -u git -H git checkout v0.8.1
+ sudo -u git -H git checkout v0.8.2
sudo -u git -H make
### Initialize Database and Activate Advanced Features
diff --git a/doc/intro/README.md b/doc/intro/README.md
index 71fef50ceb4..1790b2b761f 100644
--- a/doc/intro/README.md
+++ b/doc/intro/README.md
@@ -22,7 +22,7 @@ Create merge requests and review code.
- [Fork a project and contribute to it](../workflow/forking_workflow.md)
- [Create a new merge request](../gitlab-basics/add-merge-request.md)
-- [Automatically close issues from merge requests](../customization/issue_closing.md)
+- [Automatically close issues from merge requests](../user/project/issues/automatic_issue_closing.md)
- [Automatically merge when your builds succeed](../user/project/merge_requests/merge_when_build_succeeds.md)
- [Revert any commit](../user/project/merge_requests/revert_changes.md)
- [Cherry-pick any commit](../user/project/merge_requests/cherry_pick_changes.md)
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 835af5443a3..3f4056dc440 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -79,6 +79,9 @@ gitlab_rails['backup_upload_connection'] = {
'region' => 'eu-west-1',
'aws_access_key_id' => 'AKIAKIAKI',
'aws_secret_access_key' => 'secret123'
+ # If using an IAM Profile, leave aws_access_key_id & aws_secret_access_key empty
+ # ie. 'aws_access_key_id' => '',
+ # 'use_iam_profile' => 'true'
}
gitlab_rails['backup_upload_remote_directory'] = 'my.s3.bucket'
```
@@ -95,6 +98,9 @@ For installations from source:
region: eu-west-1
aws_access_key_id: AKIAKIAKI
aws_secret_access_key: 'secret123'
+ # If using an IAM Profile, leave aws_access_key_id & aws_secret_access_key empty
+ # ie. aws_access_key_id: ''
+ # use_iam_profile: 'true'
# The remote 'directory' to store your backups. For S3, this would be the bucket name.
remote_directory: 'my.s3.bucket'
# Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional
diff --git a/doc/update/8.11-to-8.12.md b/doc/update/8.11-to-8.12.md
index 8017c36587e..076696f565b 100644
--- a/doc/update/8.11-to-8.12.md
+++ b/doc/update/8.11-to-8.12.md
@@ -70,7 +70,7 @@ sudo -u git -H git checkout 8-12-stable-ee
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v3.5.0
+sudo -u git -H git checkout v3.6.0
```
### 6. Update gitlab-workhorse
@@ -82,7 +82,7 @@ GitLab 8.1.
```bash
cd /home/git/gitlab-workhorse
sudo -u git -H git fetch --all
-sudo -u git -H git checkout v0.8.1
+sudo -u git -H git checkout v0.8.2
sudo -u git -H make
```
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 1498cb361c8..12d5b8f8744 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -63,7 +63,7 @@ The following table depicts the various user permission levels in a project.
| Force push to protected branches [^2] | | | | | |
| Remove protected branches [^2] | | | | | |
-[^1]: If **Allow guest to access builds** is enabled in CI settings
+[^1]: If **Public pipelines** is enabled in **Project Settings > CI/CD Pipelines**
[^2]: Not allowed for Guest, Reporter, Developer, Master, or Owner
## Group
@@ -138,3 +138,33 @@ instance and project. In addition, all admins can use the admin interface under
| Add shared runners | | | | ✓ |
| See events in the system | | | | ✓ |
| Admin interface | | | | ✓ |
+
+### Build permissions
+
+> Changed in GitLab 8.12.
+
+GitLab 8.12 has a completely redesigned build permissions system.
+Read all about the [new model and its implications][new-mod].
+
+This table shows granted privileges for builds triggered by specific types of
+users:
+
+| Action | Guest, Reporter | Developer | Master | Admin |
+|---------------------------------------------|-----------------|-------------|----------|--------|
+| Run CI build | | ✓ | ✓ | ✓ |
+| Clone source and LFS from current project | | ✓ | ✓ | ✓ |
+| Clone source and LFS from public projects | | ✓ | ✓ | ✓ |
+| Clone source and LFS from internal projects | | ✓ [^3] | ✓ [^3] | ✓ |
+| Clone source and LFS from private projects | | ✓ [^4] | ✓ [^4] | ✓ [^4] |
+| Push source and LFS | | | | |
+| Pull container images from current project | | ✓ | ✓ | ✓ |
+| Pull container images from public projects | | ✓ | ✓ | ✓ |
+| Pull container images from internal projects| | ✓ [^3] | ✓ [^3] | ✓ |
+| Pull container images from private projects | | ✓ [^4] | ✓ [^4] | ✓ [^4] |
+| Push container images to current project | | ✓ | ✓ | ✓ |
+| Push container images to other projects | | | | |
+
+[^3]: Only if user is not external one.
+[^4]: Only if user is a member of the project.
+[ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994
+[new-mod]: project/new_ci_build_permissions_model.md
diff --git a/doc/user/project/builds/artifacts.md b/doc/user/project/builds/artifacts.md
index c93ae1c369c..88f1863dddb 100644
--- a/doc/user/project/builds/artifacts.md
+++ b/doc/user/project/builds/artifacts.md
@@ -101,4 +101,36 @@ inside GitLab that make that possible.
![Build artifacts browser](img/build_artifacts_browser.png)
+## Downloading the latest build artifacts
+
+It is possible to download the latest artifacts of a build via a well known URL
+so you can use it for scripting purposes.
+
+The structure of the URL is the following:
+
+```
+https://example.com/<namespace>/<project>/builds/artifacts/<ref>/download?job=<job_name>
+```
+
+For example, to download the latest artifacts of the job named `rspec 6 20` of
+the `master` branch of the `gitlab-ce` project that belongs to the `gitlab-org`
+namespace, the URL would be:
+
+```
+https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/download?job=rspec+6+20
+```
+
+The latest builds are also exposed in the UI in various places. Specifically,
+look for the download button in:
+
+- the main project's page
+- the branches page
+- the tags page
+
+If the latest build has failed to upload the artifacts, you can see that
+information in the UI.
+
+![Latest artifacts button](img/build_latest_artifacts_browser.png)
+
+
[gitlab workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse "GitLab Workhorse repository"
diff --git a/doc/user/project/builds/img/build_latest_artifacts_browser.png b/doc/user/project/builds/img/build_latest_artifacts_browser.png
new file mode 100644
index 00000000000..d8e9071958c
--- /dev/null
+++ b/doc/user/project/builds/img/build_latest_artifacts_browser.png
Binary files differ
diff --git a/doc/user/project/cycle_analytics.md b/doc/user/project/cycle_analytics.md
new file mode 100644
index 00000000000..abef80e7914
--- /dev/null
+++ b/doc/user/project/cycle_analytics.md
@@ -0,0 +1,114 @@
+# Cycle Analytics
+
+> [Introduced][ce-5986] in GitLab 8.12.
+>
+> **Note:**
+This the first iteration of Cycle Analytics, you can follow the following issue
+to track the changes that are coming to this feature: [#20975][ce-20975].
+
+Cycle Analytics measures the time it takes to go from [an idea to production] for
+each project you have. This is achieved by not only indicating the total time it
+takes to reach at that point, but the total time is broken down into the
+multiple stages an idea has to pass through to be shipped.
+
+Cycle Analytics is that it is tightly coupled with the [GitLab flow] and
+calculates a separate median for each stage.
+
+## Overview
+
+You can find the Cycle Analytics page under your project's **Pipelines > Cycle
+Analytics** tab.
+
+![Cycle Analytics landing page](img/cycle_analytics_landing_page.png)
+
+You can see that there are seven stages in total:
+
+- **Issue** (Tracker)
+ - Median time from issue creation until given a milestone or list label
+ (first assignment, any milestone, milestone date or assignee is not required)
+- **Plan** (Board)
+ - Median time from giving an issue a milestone or label until pushing the
+ first commit
+- **Code** (IDE)
+ - Median time from the first commit until the merge request is created
+- **Test** (CI)
+ - Median total test time for all commits/merges
+- **Review** (Merge Request/MR)
+ - Median time from merge request creation until the merge request is merged
+ (closed merge requests won't be taken into account)
+- **Staging** (Continuous Deployment)
+ - Median time from when the merge request got merged until the deploy to
+ production (production is last stage/environment)
+- **Production** (Total)
+ - Sum of all the above stages excluding the Test (CI) time
+
+## How the data is measured
+
+Cycle Analytics records cycle time so only data on the issues that have been
+deployed to production are measured. In case you just started a new project and
+you have not pushed anything to production, then you will not be able to
+properly see the Cycle Analytics of your project.
+
+Specifically, if your CI is not set up and you have not defined a `production`
+[environment], then you will not have any data.
+
+Below you can see in more detail what the various stages of Cycle Analytics mean.
+
+| **Stage** | **Description** |
+| --------- | --------------- |
+| Issue | Measures the median time between creating an issue and taking action to solve it, by either labeling it or adding it to a milestone, whatever comes first. The label will be tracked only if it already has an [Issue Board list][board] created for it. |
+| Plan | Measures the median time between the action you took for the previous stage, and pushing the first commit to the repository. To make this change tracked, the pushed commit needs to contain the [issue closing pattern], for example `Closes #xxx`, where `xxx` is the number of the issue related to this commit. If the commit does not contain the issue closing pattern, it is not considered to the measurement time of the stage. |
+| Code | Measures the median time between pushing a first commit (previous stage) and creating a merge request related to that commit. The key to keep the process tracked is include the [issue closing pattern] to the description of the merge request. |
+| Test | Measures the median time to run the entire pipeline for that project. It's related to the time GitLab CI takes to run every job for the commits pushed to that merge request defined in the previous stage. It is basically the start->finish time for all pipelines. `master` is not excluded. It does not attempt to track time for any particular stages. |
+| Review | Measures the median time taken to review the merge request, between its creation and until it's merged. |
+| Staging | Measures the median time between merging the merge request until the very first deployment to production. It's tracked by the [environment] set to `production` in your GitLab CI configuration. If there isn't a `production` environment, this is not tracked. |
+| Production| The sum of all time taken to run the entire process, from issue creation to deploying the code to production. |
+
+---
+
+Here's a little explanation of how this works behind the scenes:
+
+1. Issues and merge requests are grouped together in pairs, such that for each
+ `<issue, merge request>` pair, the merge request has `Fixes #xxx` for the
+ corresponding issue. All other issues and merge requests are **not** considered.
+
+1. Then the <issue, merge request> pairs are filtered out. Any merge request
+ that has **not** been deployed to production in the last XX days (specified
+ by the UI - default is 90 days) prohibits these pairs from being considered.
+
+1. For the remaining `<issue, merge request>` pairs, we check the information that
+ we need for the stages, like issue creation date, merge request merge time,
+ etc.
+
+To sum up, anything that doesn't follow the [GitLab flow] won't be tracked at all.
+So, if a merge request doesn't close an issue or an issue is not labeled with a
+label present in the Issue Board or assigned a milestone or a project has no
+`production` environment, the Cycle Analytics dashboard won't present any data
+at all.
+
+## Permissions
+
+The current permissions on the Cycle Analytics dashboard are:
+
+- Public projects - anyone can access
+- Private/internal projects - any member (guest level and above) can access
+
+You can [read more about permissions][permissions] in general.
+
+## More resources
+
+Learn more about Cycle Analytics in the following resources:
+
+- [Cycle Analytics feature page](https://about.gitlab.com/solutions/cycle-analytics/)
+- [Cycle Analytics feature preview](https://about.gitlab.com/2016/09/16/feature-preview-introducing-cycle-analytics/)
+- [Cycle Analytics feature highlight](https://about.gitlab.com/2016/09/21/cycle-analytics-feature-highlight/)
+
+
+[ce-5986]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5986
+[ce-20975]: https://gitlab.com/gitlab-org/gitlab-ce/issues/20975
+[GitLab flow]: ../../workflow/gitlab_flow.md
+[permissions]: ../permissions.md
+[environment]: ../../ci/yaml/README.md#environment
+[board]: issue_board.md#creating-a-new-list
+[idea to production]: https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab
+[issue closing pattern]: issues/automatic_issue_closing.md
diff --git a/doc/user/project/img/cycle_analytics_landing_page.png b/doc/user/project/img/cycle_analytics_landing_page.png
new file mode 100644
index 00000000000..4fa42c87395
--- /dev/null
+++ b/doc/user/project/img/cycle_analytics_landing_page.png
Binary files differ
diff --git a/doc/user/project/issues/automatic_issue_closing.md b/doc/user/project/issues/automatic_issue_closing.md
new file mode 100644
index 00000000000..d6f3a7d5555
--- /dev/null
+++ b/doc/user/project/issues/automatic_issue_closing.md
@@ -0,0 +1,55 @@
+# Automatic issue closing
+
+>**Note:**
+This is the user docs. In order to change the default issue closing pattern,
+follow the steps in the [administration docs].
+
+When a commit or merge request resolves one or more issues, it is possible to
+automatically have these issues closed when the commit or merge request lands
+in the project's default branch.
+
+If a commit message or merge request description contains a sentence matching
+a certain regular expression, all issues referenced from the matched text will
+be closed. This happens when the commit is pushed to a project's **default**
+branch, or when a commit or merge request is merged into it.
+
+## Default closing pattern value
+
+When not specified, the default issue closing pattern as shown below will be
+used:
+
+```bash
+((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)
+```
+
+Note that `%{issue_ref}` is a complex regular expression defined inside GitLab's
+source code that can match a reference to 1) a local issue (`#123`),
+2) a cross-project issue (`group/project#123`) or 3) a link to an issue
+(`https://gitlab.example.com/group/project/issues/123`).
+
+---
+
+This translates to the following keywords:
+
+- Close, Closes, Closed, Closing, close, closes, closed, closing
+- Fix, Fixes, Fixed, Fixing, fix, fixes, fixed, fixing
+- Resolve, Resolves, Resolved, Resolving, resolve, resolves, resolved, resolving
+
+---
+
+For example the following commit message:
+
+```
+Awesome commit message
+
+Fix #20, Fixes #21 and Closes group/otherproject#22.
+This commit is also related to #17 and fixes #18, #19
+and https://gitlab.example.com/group/otherproject/issues/23.
+```
+
+will close `#18`, `#19`, `#20`, and `#21` in the project this commit is pushed
+to, as well as `#22` and `#23` in group/otherproject. `#17` won't be closed as
+it does not match the pattern. It works with multi-line commit messages as well
+as one-liners when used with `git commit -m`.
+
+[administration docs]: ../../../administration/issue_closing_pattern.md
diff --git a/doc/user/project/merge_requests.md b/doc/user/project/merge_requests.md
index f79535d1542..5af9a5d049c 100644
--- a/doc/user/project/merge_requests.md
+++ b/doc/user/project/merge_requests.md
@@ -93,6 +93,9 @@ A merge request contains all the history from a repository, plus the additional
commits added to the branch associated with the merge request. Here's a few
tricks to checkout a merge request locally.
+Please note that you can checkout a merge request locally even if the source
+project is a fork (even a private fork) of the target project.
+
#### Checkout locally by adding a git alias
Add the following alias to your `~/.gitconfig`:
diff --git a/doc/user/project/merge_requests/img/versions-compare.png b/doc/user/project/merge_requests/img/versions-compare.png
new file mode 100644
index 00000000000..890cae7768c
--- /dev/null
+++ b/doc/user/project/merge_requests/img/versions-compare.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/versions-dropdown.png b/doc/user/project/merge_requests/img/versions-dropdown.png
new file mode 100644
index 00000000000..9bab9304e14
--- /dev/null
+++ b/doc/user/project/merge_requests/img/versions-dropdown.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/versions.png b/doc/user/project/merge_requests/img/versions.png
index f279ccd7ce3..6c86f2c68ac 100644
--- a/doc/user/project/merge_requests/img/versions.png
+++ b/doc/user/project/merge_requests/img/versions.png
Binary files differ
diff --git a/doc/user/project/merge_requests/versions.md b/doc/user/project/merge_requests/versions.md
index a6aa4b47835..2805fdf635c 100644
--- a/doc/user/project/merge_requests/versions.md
+++ b/doc/user/project/merge_requests/versions.md
@@ -7,14 +7,18 @@ of merge request diff is created. When you visit a merge request that contains
more than one pushes, you can select and compare the versions of those merge
request diffs.
+![Merge Request Versions](img/versions.png)
+
By default, the latest version of changes is shown. However, you
can select an older one from version dropdown.
-![Merge Request Versions](img/versions.png)
+![Merge Request Versions](img/versions-dropdown.png)
You can also compare the merge request version with older one to see what is
changed since then.
+![Merge Request Versions](img/versions-compare.png)
+
Please note that comments are disabled while viewing outdated merge versions
or comparing to versions other than base.
diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md
new file mode 100644
index 00000000000..e73f60023b5
--- /dev/null
+++ b/doc/user/project/new_ci_build_permissions_model.md
@@ -0,0 +1,289 @@
+# New CI build permissions model
+
+> Introduced in GitLab 8.12.
+
+GitLab 8.12 has a completely redesigned [build permissions] system. You can find
+all discussion and all our concerns when choosing the current approach in issue
+[#18994](https://gitlab.com/gitlab-org/gitlab-ce/issues/18994).
+
+---
+
+Builds permissions should be tightly integrated with the permissions of a user
+who is triggering a build.
+
+The reasons to do it like that are:
+
+- We already have a permissions system in place: group and project membership
+ of users.
+- We already fully know who is triggering a build (using `git push`, using the
+ web UI, executing triggers).
+- We already know what user is allowed to do.
+- We use the user permissions for builds that are triggered by the user.
+- It opens a lot of possibilities to further enforce user permissions, like
+ allowing only specific users to access runners or use secure variables and
+ environments.
+- It is simple and convenient that your build can access everything that you
+ as a user have access to.
+- Short living unique tokens are now used, granting access for time of the build
+ and maximizing security.
+
+With the new behavior, any build that is triggered by the user, is also marked
+with their permissions. When a user does a `git push` or changes files through
+the web UI, a new pipeline will be usually created. This pipeline will be marked
+as created be the pusher (local push or via the UI) and any build created in this
+pipeline will have the permissions of the pusher.
+
+This allows us to make it really easy to evaluate the access for all projects
+that have Git submodules or are using container images that the pusher would
+have access too. **The permission is granted only for time that build is running.
+The access is revoked after the build is finished.**
+
+## Types of users
+
+It is important to note that we have a few types of users:
+
+- **Administrators**: CI builds created by Administrators will not have access
+ to all GitLab projects, but only to projects and container images of projects
+ that the administrator is a member of.That means that if a project is either
+ public or internal users have access anyway, but if a project is private, the
+ Administrator will have to be a member of it in order to have access to it
+ via another project's build.
+
+- **External users**: CI builds created by [external users][ext] will have
+ access only to projects to which user has at least reporter access. This
+ rules out accessing all internal projects by default,
+
+This allows us to make the CI and permission system more trustworthy.
+Let's consider the following scenario:
+
+1. You are an employee of a company. Your company has a number of internal tools
+ hosted in private repositories and you have multiple CI builds that make use
+ of these repositories.
+
+2. You invite a new [external user][ext]. CI builds created by that user do not
+ have access to internal repositories, because the user also doesn't have the
+ access from within GitLab. You as an employee have to grant explicit access
+ for this user. This allows us to prevent from accidental data leakage.
+
+## Build token
+
+A unique build token is generated for each build and it allows the user to
+access all projects that would be normally accessible to the user creating that
+build.
+
+We try to make sure that this token doesn't leak by:
+
+1. Securing all API endpoints to not expose the build token.
+1. Masking the build token from build logs.
+1. Allowing to use the build token **only** when build is running.
+
+However, this brings a question about the Runners security. To make sure that
+this token doesn't leak, you should also make sure that you configure
+your Runners in the most possible secure way, by avoiding the following:
+
+1. Any usage of Docker's `privileged` mode is risky if the machines are re-used.
+1. Using the `shell` executor since builds run on the same machine.
+
+By using an insecure GitLab Runner configuration, you allow the rogue developers
+to steal the tokens of other builds.
+
+## Debugging problems
+
+With the new permission model in place, there may be times that your build will
+fail. This is most likely because your project tries to access other project's
+sources, and you don't have the appropriate permissions. In the build log look
+for information about 403 or forbidden access messages
+
+As an Administrator, you can verify that the user is a member of the group or
+project they're trying to have access to, and you can impersonate the user to
+retry the failing build in order to verify that everything is correct.
+
+## Build triggers
+
+[Build triggers][triggers] do not support the new permission model.
+They continue to use the old authentication mechanism where the CI build
+can access only its own sources. We plan to remove that limitation in one of
+the upcoming releases.
+
+## Before GitLab 8.12
+
+In versions before GitLab 8.12, all CI builds would use the CI Runner's token
+to checkout project sources.
+
+The project's Runner's token was a token that you could find under the
+project's **Settings > CI/CD Pipelines** and was limited to access only that
+project.
+It could be used for registering new specific Runners assigned to the project
+and to checkout project sources.
+It could also be used with the GitLab Container Registry for that project,
+allowing pulling and pushing Docker images from within the CI build.
+
+---
+
+GitLab would create a special checkout URL like:
+
+```
+https://gitlab-ci-token:<project-runners-token>/gitlab.com/gitlab-org/gitlab-ce.git
+```
+
+And then the users could also use it in their CI builds all Docker related
+commands to interact with GitLab Container Registry. For example:
+
+```
+docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.gitlab.com
+```
+
+Using single token had multiple security implications:
+
+- The token would be readable to anyone who had developer access to a project
+ that could run CI builds, allowing the developer to register any specific
+ Runner for that project.
+- The token would allow to access only the project's sources, forbidding from
+ accessing any other projects.
+- The token was not expiring and was multi-purpose: used for checking out sources,
+ for registering specific runners and for accessing a project's container
+ registry with read-write permissions.
+
+All the above led to a new permission model for builds that was introduced
+with GitLab 8.12.
+
+## Making use of the new CI build permissions model
+
+With the new build permission model, there is now an easy way to access all
+dependent source code in a project. That way, we can:
+
+1. Access a project's Git submodules
+1. Access private container images
+1. Access project's and submodule LFS objects
+
+Let's see how that works with Git submodules and private Docker images hosted on
+the container registry.
+
+## Git submodules
+
+>
+It often happens that while working on one project, you need to use another
+project from within it; perhaps it’s a library that a third party developed or
+you’re developing a project separately and are using it in multiple parent
+projects.
+A common issue arises in these scenarios: you want to be able to treat the two
+projects as separate yet still be able to use one from within the other.
+>
+_Excerpt from the [Git website][git-scm] about submodules._
+
+If dealing with submodules, your project will probably have a file named
+`.gitmodules`. And this is how it usually looks like:
+
+```
+[submodule "tools"]
+ path = tools
+ url = git@gitlab.com/group/tools.git
+```
+
+> **Note:**
+If you are **not** using GitLab 8.12 or higher, you would need to work your way
+around this issue in order to access the sources of `gitlab.com/group/tools`
+(e.g., use [SSH keys](../ssh_keys/README.md)).
+>
+With GitLab 8.12 onward, your permissions are used to evaluate what a CI build
+can access. More information about how this system works can be found in the
+[Build permissions model](../../user/permissions.md#builds-permissions).
+
+To make use of the new changes, you have to update your `.gitmodules` file to
+use a relative URL.
+
+Let's consider the following example:
+
+1. Your project is located at `https://gitlab.com/secret-group/my-project`.
+1. To checkout your sources you usually use an SSH address like
+ `git@gitlab.com:secret-group/my-project.git`.
+1. Your project depends on `https://gitlab.com/group/tools`.
+1. You have the `.gitmodules` file with above content.
+
+Since Git allows the usage of relative URLs for your `.gitmodules` configuration,
+this easily allows you to use HTTP for cloning all your CI builds and SSH
+for all your local checkouts.
+
+For example, if you change the `url` of your `tools` dependency, from
+`git@gitlab.com/group/tools.git` to `../../group/tools.git`, this will instruct
+Git to automatically deduce the URL that should be used when cloning sources.
+Whether you use HTTP or SSH, Git will use that same channel and it will allow
+to make all your CI builds use HTTPS (because GitLab CI uses HTTPS for cloning
+your sources), and all your local clones will continue using SSH.
+
+Given the above explanation, your `.gitmodules` file should eventually look
+like this:
+
+```
+[submodule "tools"]
+ path = tools
+ url = ../../group/tools.git
+```
+
+However, you have to explicitly tell GitLab CI to clone your submodules as this
+is not done automatically. You can achieve that by adding a `before_script`
+section to your `.gitlab-ci.yml`:
+
+```
+before_script:
+ - git submodule update --init --recursive
+
+test:
+ script:
+ - run-my-tests
+```
+
+This will make GitLab CI initialize (fetch) and update (checkout) all your
+submodules recursively.
+
+In case your environment or your Docker image doesn't have Git installed,
+you have to either ask your Administrator or install the missing dependency
+yourself:
+
+```
+# Debian / Ubuntu
+before_script:
+ - apt-get update -y
+ - apt-get install -y git-core
+ - git submodule update --init --recursive
+
+# CentOS / RedHat
+before_script:
+ - yum install git
+ - git submodule update --init --recursive
+
+# Alpine
+before_script:
+ - apk add -U git
+ - git submodule update --init --recursive
+```
+
+### Container Registry
+
+With the update permission model we also extended the support for accessing
+Container Registries for private projects.
+
+> **Note:**
+As GitLab Runner 1.6 doesn't yet incorporate the introduced changes for
+permissions, this makes the `image:` directive to not work with private projects
+automatically. The manual configuration by an Administrator is required to use
+private images. We plan to remove that limitation in one of the upcoming releases.
+
+Your builds can access all container images that you would normally have access
+to. The only implication is that you can push to the Container Registry of the
+project for which the build is triggered.
+
+This is how an example usage can look like:
+
+```
+test:
+ script:
+ - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
+ - docker pull $CI_REGISTRY/group/other-project:latest
+ - docker run $CI_REGISTRY/group/other-project:latest
+```
+
+[git-scm]: https://git-scm.com/book/en/v2/Git-Tools-Submodules
+[build permissions]: ../permissions.md#builds-permissions
+[ext]: ../permissions.md#external-users
+[triggers]: ../../ci/triggers/README.md
diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md
index 7c041d019bb..993c6bfb7e9 100644
--- a/doc/user/project/repository/web_editor.md
+++ b/doc/user/project/repository/web_editor.md
@@ -172,4 +172,4 @@ you commit the changes you will be taken to a new merge request form.
![New file button](basicsimages/file_button.png)
[ce-2808]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2808
-[issue closing pattern]: ../customization/issue_closing.md
+[issue closing pattern]: ../user/project/issues/automatic_issue_closing.md
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index 08ff89ce6ae..445c0ee8333 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -3,8 +3,8 @@
>**Notes:**
>
> - [Introduced][ce-3050] in GitLab 8.9.
-> - Importing will not be possible if the import instance version is lower
-> than that of the exporter.
+> - Importing will not be possible if the import instance version differs from
+> that of the exporter.
> - For existing installations, the project import option has to be enabled in
> application settings (`/admin/application_settings`) under 'Import sources'.
> You will have to be an administrator to enable and use the import functionality.
@@ -17,6 +17,20 @@
Existing projects running on any GitLab instance or GitLab.com can be exported
with all their related data and be moved into a new GitLab instance.
+## Version history
+
+| GitLab version | Import/Export version |
+| -------- | -------- |
+| 8.12.0 to current | 0.1.4 |
+| 8.10.3 | 0.1.3 |
+| 8.10.0 | 0.1.2 |
+| 8.9.5 | 0.1.1 |
+| 8.9.0 | 0.1.0 |
+
+ > The table reflects what GitLab version we updated the Import/Export version at.
+ > For instance, 8.10.3 and 8.11 will have the same Import/Export version (0.1.3)
+ > and the exports between them will be compatible.
+
## Exported contents
The following items will be exported:
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index dcb9c32ad58..2d9bfbc0629 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -1,6 +1,8 @@
# Workflow
+- [Automatic issue closing](../user/project/issues/automatic_issue_closing.md)
- [Change your time zone](timezone.md)
+- [Cycle Analytics](../user/project/cycle_analytics.md)
- [Description templates](../user/project/description_templates.md)
- [Feature branch workflow](workflow.md)
- [GitLab Flow](gitlab_flow.md)
diff --git a/doc/workflow/importing/img/import_projects_from_github_importer.png b/doc/workflow/importing/img/import_projects_from_github_importer.png
index b6ed8dd692a..eadd33c695f 100644
--- a/doc/workflow/importing/img/import_projects_from_github_importer.png
+++ b/doc/workflow/importing/img/import_projects_from_github_importer.png
Binary files differ
diff --git a/doc/workflow/importing/img/import_projects_from_github_new_project_page.png b/doc/workflow/importing/img/import_projects_from_github_new_project_page.png
index c8f35a50f48..6e91c430a33 100644
--- a/doc/workflow/importing/img/import_projects_from_github_new_project_page.png
+++ b/doc/workflow/importing/img/import_projects_from_github_new_project_page.png
Binary files differ
diff --git a/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png b/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png
new file mode 100644
index 00000000000..c11863ab10c
--- /dev/null
+++ b/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png
Binary files differ
diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md
index 370d885d366..c36dfdb78ec 100644
--- a/doc/workflow/importing/import_projects_from_github.md
+++ b/doc/workflow/importing/import_projects_from_github.md
@@ -1,54 +1,118 @@
# Import your project from GitHub to GitLab
+Import your projects from GitHub to GitLab with minimal effort.
+
+## Overview
+
>**Note:**
-In order to enable the GitHub import setting, you may also want to
-enable the [GitHub integration][gh-import] in your GitLab instance. This
-configuration is optional, you will be able import your GitHub repositories
-with a Personal Access Token.
+If you are an administrator you can enable the [GitHub integration][gh-import]
+in your GitLab instance sitewide. This configuration is optional, users will be
+able import their GitHub repositories with a [personal access token][gh-token].
-At its current state, GitHub importer can import:
+- At its current state, GitHub importer can import:
+ - the repository description (GitLab 7.7+)
+ - the Git repository data (GitLab 7.7+)
+ - the issues (GitLab 7.7+)
+ - the pull requests (GitLab 8.4+)
+ - the wiki pages (GitLab 8.4+)
+ - the milestones (GitLab 8.7+)
+ - the labels (GitLab 8.7+)
+ - the release note descriptions (GitLab 8.12+)
+- References to pull requests and issues are preserved (GitLab 8.7+)
+- Repository public access is retained. If a repository is private in GitHub
+ it will be created as private in GitLab as well.
-- the repository description (introduced in GitLab 7.7)
-- the git repository data (introduced in GitLab 7.7)
-- the issues (introduced in GitLab 7.7)
-- the pull requests (introduced in GitLab 8.4)
-- the wiki pages (introduced in GitLab 8.4)
-- the milestones (introduced in GitLab 8.7)
-- the labels (introduced in GitLab 8.7)
-- the release note descriptions (introduced in GitLab 8.12)
+## How it works
-With GitLab 8.7+, references to pull requests and issues are preserved.
+When issues/pull requests are being imported, the GitHub importer tries to find
+the GitHub author/assignee in GitLab's database using the GitHub ID. For this
+to work, the GitHub author/assignee should have signed in beforehand in GitLab
+and [**associated their GitHub account**][social sign-in]. If the user is not
+found in GitLab's database, the project creator (most of the times the current
+user that started the import process) is set as the author, but a reference on
+the issue about the original GitHub author is kept.
-The importer page is visible when you [create a new project][new-project].
-Click on the **GitHub** link and, if you are logged in via the GitHub
-integration, you will be redirected to GitHub for permission to access your
-projects. After accepting, you'll be automatically redirected to the importer.
+The importer will create any new namespaces (groups) if they don't exist or in
+the case the namespace is taken, the repository will be imported under the user's
+namespace that started the import process.
-If you are not using the GitHub integration, you can still perform a one-off
-authorization with GitHub to access your projects.
+## Importing your GitHub repositories
-Alternatively, you can also enter a GitHub Personal Access Token. Once you enter
-your token, you'll be taken to the importer.
+The importer page is visible when you create a new project.
![New project page on GitLab](img/import_projects_from_github_new_project_page.png)
----
+Click on the **GitHub** link and the import authorization process will start.
+There are two ways to authorize access to your GitHub repositories:
+
+1. [Using the GitHub integration][gh-integration] (if it's enabled by your
+ GitLab administrator). This is the preferred way as it's possible to
+ preserve the GitHub authors/assignees. Read more in the [How it works](#how-it-works)
+ section.
+1. [Using a personal access token][gh-token] provided by GitHub.
+
+![Select authentication method](img/import_projects_from_github_select_auth_method.png)
+
+### Authorize access to your repositories using the GitHub integration
+
+If the [GitHub integration][gh-import] is enabled by your GitLab administrator,
+you can use it instead of the personal access token.
+
+1. First you may want to connect your GitHub account to GitLab in order for
+ the username mapping to be correct. Follow the [social sign-in] documentation
+ on how to do so.
+1. Once you connect GitHub, click the **List your GitHub repositories** button
+ and you will be redirected to GitHub for permission to access your projects.
+1. After accepting, you'll be automatically redirected to the importer.
+
+You can now go on and [select which repositories to import](#select-which-repositories-to-import).
+
+### Authorize access to your repositories using a personal access token
+
+>**Note:**
+For a proper author/assignee mapping for issues and pull requests, the
+[GitHub integration][gh-integration] should be used instead of the
+[personal access token][gh-token]. If the GitHub integration is enabled by your
+GitLab administrator, it should be the preferred method to import your repositories.
+Read more in the [How it works](#how-it-works) section.
+
+If you are not using the GitHub integration, you can still perform a one-off
+authorization with GitHub to grant GitLab access your repositories:
+
+1. Go to <https://github.com/settings/tokens/new>.
+1. Enter a token description.
+1. Check the `repo` scope.
+1. Click **Generate token**.
+1. Copy the token hash.
+1. Go back to GitLab and provide the token to the GitHub importer.
+1. Hit the **List your GitHub repositories** button and wait while GitLab reads
+ your repositories' information. Once done, you'll be taken to the importer
+ page to select the repositories to import.
+
+### Select which repositories to import
+
+After you've authorized access to your GitHub repositories, you will be
+redirected to the GitHub importer page.
+
+From there, you can see the import statuses of your GitHub repositories.
+
+- Those that are being imported will show a _started_ status,
+- those already successfully imported will be green with a _done_ status,
+- whereas those that are not yet imported will have an **Import** button on the
+ right side of the table.
-While at the GitHub importer page, you can see the import statuses of your
-GitHub projects. Those that are being imported will show a _started_ status,
-those already imported will be green, whereas those that are not yet imported
-have an **Import** button on the right side of the table. If you want, you can
-import all your GitHub projects in one go by hitting **Import all projects**
-in the upper left corner.
+If you want, you can import all your GitHub projects in one go by hitting
+**Import all projects** in the upper left corner.
![GitHub importer page](img/import_projects_from_github_importer.png)
---
-The importer will create any new namespaces if they don't exist or in the
-case the namespace is taken, the project will be imported on the user's
-namespace.
+You can also choose a different name for the project and a different namespace,
+if you have the privileges to do so.
[gh-import]: ../../integration/github.md "GitHub integration"
-[ee-gh]: http://docs.gitlab.com/ee/integration/github.html "GitHub integration for GitLab EE"
[new-project]: ../../gitlab-basics/create-project.md "How to create a new project in GitLab"
+[gh-integration]: #authorize-access-to-your-repositories-using-the-github-integration
+[gh-token]: #authorize-access-to-your-repositories-using-a-personal-access-token
+[social sign-in]: ../../profile/account/social_sign_in.md
diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md
index 9dc1e9b47e3..b3c73e947f0 100644
--- a/doc/workflow/lfs/lfs_administration.md
+++ b/doc/workflow/lfs/lfs_administration.md
@@ -45,5 +45,5 @@ In `config/gitlab.yml`:
* Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets)
is not supported
* Currently, removing LFS objects from GitLab Git LFS storage is not supported
-* LFS authentications via SSH is not supported for the time being
-* Only compatible with the GitLFS client versions 1.1.0 or 1.0.2.
+* LFS authentications via SSH was added with GitLab 8.12
+* Only compatible with the GitLFS client versions 1.1.0 and up, or 1.0.2.
diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
index 9fe065fa680..1a4f213a792 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -35,6 +35,10 @@ Documentation for GitLab instance administrators is under [LFS administration do
credentials store is recommended
* Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have
to add the URL to Git config manually (see #troubleshooting)
+
+>**Note**: With 8.12 GitLab added LFS support to SSH. The Git LFS communication
+ still goes over HTTP, but now the SSH client passes the correct credentials
+ to the Git LFS client, so no action is required by the user.
## Using Git LFS
@@ -132,6 +136,10 @@ git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs"
### Credentials are always required when pushing an object
+>**Note**: With 8.12 GitLab added LFS support to SSH. The Git LFS communication
+ still goes over HTTP, but now the SSH client passes the correct credentials
+ to the Git LFS client, so no action is required by the user.
+
Given that Git LFS uses HTTP Basic Authentication to authenticate the user pushing
the LFS object on every push for every object, user HTTPS credentials are required.
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index e21f76d00d9..ed7241679ee 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -356,6 +356,6 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
def filter_issue(text)
- fill_in 'issue_search', with: text
+ fill_in 'issuable_search', with: text
end
end
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index df17b5626c6..4a67cf06fba 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -493,7 +493,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I fill in merge request search with "Fe"' do
- fill_in 'issue_search', with: "Fe"
+ fill_in 'issuable_search', with: "Fe"
end
step 'I click the "Target branch" dropdown' do
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb
index d02b469dac8..29a97ccbd75 100644
--- a/lib/api/access_requests.rb
+++ b/lib/api/access_requests.rb
@@ -20,7 +20,7 @@ module API
access_requesters = paginate(source.requesters.includes(:user))
- present access_requesters.map(&:user), with: Entities::AccessRequester, access_requesters: access_requesters
+ present access_requesters.map(&:user), with: Entities::AccessRequester, source: source
end
# Request access to the group/project
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index 7e67edb203a..8cc7a26f1fa 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -33,46 +33,29 @@ module API
#
# If the token is revoked, then it raises RevokedError.
#
- # If the token is not found (nil), then it raises TokenNotFoundError.
+ # If the token is not found (nil), then it returns nil
#
# Arguments:
#
# scopes: (optional) scopes required for this guard.
# Defaults to empty array.
#
- def doorkeeper_guard!(scopes: [])
- if (access_token = find_access_token).nil?
- raise TokenNotFoundError
-
- else
- case validate_access_token(access_token, scopes)
- when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
- raise InsufficientScopeError.new(scopes)
- when Oauth2::AccessTokenValidationService::EXPIRED
- raise ExpiredError
- when Oauth2::AccessTokenValidationService::REVOKED
- raise RevokedError
- when Oauth2::AccessTokenValidationService::VALID
- @current_user = User.find(access_token.resource_owner_id)
- end
- end
- end
-
def doorkeeper_guard(scopes: [])
- if access_token = find_access_token
- case validate_access_token(access_token, scopes)
- when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
- raise InsufficientScopeError.new(scopes)
+ access_token = find_access_token
+ return nil unless access_token
+
+ case validate_access_token(access_token, scopes)
+ when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
+ raise InsufficientScopeError.new(scopes)
- when Oauth2::AccessTokenValidationService::EXPIRED
- raise ExpiredError
+ when Oauth2::AccessTokenValidationService::EXPIRED
+ raise ExpiredError
- when Oauth2::AccessTokenValidationService::REVOKED
- raise RevokedError
+ when Oauth2::AccessTokenValidationService::REVOKED
+ raise RevokedError
- when Oauth2::AccessTokenValidationService::VALID
- @current_user = User.find(access_token.resource_owner_id)
- end
+ when Oauth2::AccessTokenValidationService::VALID
+ @current_user = User.find(access_token.resource_owner_id)
end
end
@@ -96,19 +79,6 @@ module API
end
module ClassMethods
- # Installs the doorkeeper guard on the whole Grape API endpoint.
- #
- # Arguments:
- #
- # scopes: (optional) scopes required for this guard.
- # Defaults to empty array.
- #
- def guard_all!(scopes: [])
- before do
- guard! scopes: scopes
- end
- end
-
private
def install_error_responders(base)
diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb
index 7c22b17e4e5..2461a783ea8 100644
--- a/lib/api/award_emoji.rb
+++ b/lib/api/award_emoji.rb
@@ -1,12 +1,12 @@
module API
class AwardEmoji < Grape::API
before { authenticate! }
- AWARDABLES = [Issue, MergeRequest]
+ AWARDABLES = %w[issue merge_request snippet]
resource :projects do
AWARDABLES.each do |awardable_type|
- awardable_string = awardable_type.to_s.underscore.pluralize
- awardable_id_string = "#{awardable_type.to_s.underscore}_id"
+ awardable_string = awardable_type.pluralize
+ awardable_id_string = "#{awardable_type}_id"
[ ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji",
":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji"
@@ -87,9 +87,7 @@ module API
helpers do
def can_read_awardable?
- ability = "read_#{awardable.class.to_s.underscore}".to_sym
-
- can?(current_user, ability, awardable)
+ can?(current_user, read_ability(awardable), awardable)
end
def can_award_awardable?
@@ -100,18 +98,25 @@ module API
@awardable ||=
begin
if params.include?(:note_id)
- noteable.notes.find(params[: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
- noteable
+ user_project.snippets.find(params[:snippet_id])
end
end
end
- def noteable
- if params.include?(:issue_id)
- user_project.issues.find(params[:issue_id])
+ def read_ability(awardable)
+ case awardable
+ when Note
+ read_ability(awardable.noteable)
else
- user_project.merge_requests.find(params[:merge_request_id])
+ :"read_#{awardable.class.to_s.underscore}"
end
end
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 4f736e4ec2b..92a6f29adb0 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -86,7 +86,8 @@ module API
expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:user]) }
expose :created_at, :last_activity_at
- expose :shared_runners_enabled, :lfs_enabled
+ expose :shared_runners_enabled
+ expose :lfs_enabled?, as: :lfs_enabled
expose :creator_id
expose :namespace
expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? }
@@ -99,30 +100,33 @@ module API
SharedGroup.represent(project.project_group_links.all, options)
end
expose :only_allow_merge_if_build_succeeds
+ expose :request_access_enabled
end
class Member < UserBasic
expose :access_level do |user, options|
- member = options[:member] || options[:members].find { |m| m.user_id == user.id }
+ member = options[:member] || options[:source].members.find_by(user_id: user.id)
member.access_level
end
expose :expires_at do |user, options|
- member = options[:member] || options[:members].find { |m| m.user_id == user.id }
+ member = options[:member] || options[:source].members.find_by(user_id: user.id)
member.expires_at
end
end
class AccessRequester < UserBasic
expose :requested_at do |user, options|
- access_requester = options[:access_requester] || options[:access_requesters].find { |m| m.user_id == user.id }
+ access_requester = options[:access_requester] || options[:source].requesters.find_by(user_id: user.id)
access_requester.requested_at
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
end
class GroupDetail < Group
diff --git a/lib/api/files.rb b/lib/api/files.rb
index c1d86f313b0..96510e651a3 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -11,14 +11,16 @@ module API
target_branch: attrs[:branch_name],
commit_message: attrs[:commit_message],
file_content: attrs[:content],
- file_content_encoding: attrs[:encoding]
+ 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_name: attrs[:branch_name],
+ branch_name: attrs[:branch_name]
}
end
end
@@ -96,7 +98,7 @@ module API
authorize! :push_code, user_project
required_attributes! [:file_path, :branch_name, :content, :commit_message]
- attrs = attributes_for_keys [:file_path, :branch_name, :content, :commit_message, :encoding]
+ attrs = attributes_for_keys [:file_path, :branch_name, :content, :commit_message, :encoding, :author_email, :author_name]
result = ::Files::CreateService.new(user_project, current_user, commit_params(attrs)).execute
if result[:status] == :success
@@ -122,7 +124,7 @@ module API
authorize! :push_code, user_project
required_attributes! [:file_path, :branch_name, :content, :commit_message]
- attrs = attributes_for_keys [:file_path, :branch_name, :content, :commit_message, :encoding]
+ attrs = attributes_for_keys [:file_path, :branch_name, :content, :commit_message, :encoding, :author_email, :author_name]
result = ::Files::UpdateService.new(user_project, current_user, commit_params(attrs)).execute
if result[:status] == :success
@@ -149,7 +151,7 @@ module API
authorize! :push_code, user_project
required_attributes! [:file_path, :branch_name, :commit_message]
- attrs = attributes_for_keys [:file_path, :branch_name, :commit_message]
+ attrs = attributes_for_keys [:file_path, :branch_name, :commit_message, :author_email, :author_name]
result = ::Files::DeleteService.new(user_project, current_user, commit_params(attrs)).execute
if result[:status] == :success
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index d2df77238d5..953fa474e88 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -23,17 +23,19 @@ module API
# Create group. Available only for users who can create groups.
#
# Parameters:
- # name (required) - The name of the group
- # path (required) - The path of the group
- # description (optional) - The description of the group
- # visibility_level (optional) - The visibility level of the group
+ # name (required) - The name of the group
+ # path (required) - The path of the group
+ # description (optional) - The description of the group
+ # visibility_level (optional) - The visibility level of the group
+ # lfs_enabled (optional) - Enable/disable LFS for the projects in this group
+ # request_access_enabled (optional) - Allow users to request member access
# Example Request:
# POST /groups
post do
authorize! :create_group
required_attributes! [:name, :path]
- attrs = attributes_for_keys [:name, :path, :description, :visibility_level]
+ attrs = attributes_for_keys [:name, :path, :description, :visibility_level, :lfs_enabled, :request_access_enabled]
@group = Group.new(attrs)
if @group.save
@@ -47,17 +49,19 @@ module API
# Update group. Available only for users who can administrate groups.
#
# Parameters:
- # id (required) - The ID of a group
- # path (optional) - The path of the group
- # description (optional) - The description of the group
- # visibility_level (optional) - The visibility level of the group
+ # id (required) - The ID of a group
+ # path (optional) - The path of the group
+ # description (optional) - The description of the group
+ # visibility_level (optional) - The visibility level of the group
+ # lfs_enabled (optional) - Enable/disable LFS for the projects in this group
+ # request_access_enabled (optional) - Allow users to request member access
# Example Request:
# PUT /groups/:id
put ':id' do
group = find_group(params[:id])
authorize! :admin_group, group
- attrs = attributes_for_keys [:name, :path, :description, :visibility_level]
+ attrs = attributes_for_keys [:name, :path, :description, :visibility_level, :lfs_enabled, :request_access_enabled]
if ::Groups::UpdateService.new(group, current_user, attrs).execute
present group, with: Entities::GroupDetail
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 150875ed4f0..714d4ea3dc6 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -12,13 +12,30 @@ module API
nil
end
+ def private_token
+ params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]
+ end
+
+ def warden
+ env['warden']
+ end
+
+ # Check the Rails session for valid authentication details
+ def find_user_from_warden
+ warden ? warden.authenticate : nil
+ end
+
def find_user_by_private_token
- token_string = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
- User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string)
+ token = private_token
+ return nil unless token.present?
+
+ User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
end
def current_user
- @current_user ||= (find_user_by_private_token || doorkeeper_guard)
+ @current_user ||= find_user_by_private_token
+ @current_user ||= doorkeeper_guard
+ @current_user ||= find_user_from_warden
unless @current_user && Gitlab::UserAccess.new(@current_user).allowed?
return nil
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 6e6efece7c4..090d04544da 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -35,6 +35,14 @@ module API
Project.find_with_namespace(project_path)
end
end
+
+ def ssh_authentication_abilities
+ [
+ :read_project,
+ :download_code,
+ :push_code
+ ]
+ end
end
post "/allowed" do
@@ -51,9 +59,9 @@ module API
access =
if wiki?
- Gitlab::GitAccessWiki.new(actor, project, protocol)
+ Gitlab::GitAccessWiki.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
else
- Gitlab::GitAccess.new(actor, project, protocol)
+ Gitlab::GitAccess.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
end
access_status = access.check(params[:action], params[:changes])
@@ -74,6 +82,19 @@ module API
response
end
+ post "/lfs_authenticate" do
+ status 200
+
+ key = Key.find(params[:key_id])
+ token_handler = Gitlab::LfsToken.new(key)
+
+ {
+ username: token_handler.actor_name,
+ lfs_token: token_handler.generate,
+ repository_http_path: project.http_url_to_repo
+ }
+ end
+
get "/merge_request_urls" do
::MergeRequests::GetUrlsService.new(project).execute(params[:changes])
end
diff --git a/lib/api/members.rb b/lib/api/members.rb
index 94c16710d9a..37f0a6512f4 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -18,11 +18,11 @@ module API
get ":id/members" do
source = find_source(source_type, params[:id])
- members = source.members.includes(:user)
- members = members.joins(:user).merge(User.search(params[:query])) if params[:query]
- members = paginate(members)
+ users = source.users
+ users = users.merge(User.search(params[:query])) if params[:query]
+ users = paginate(users)
- present members.map(&:user), with: Entities::Member, members: members
+ present users, with: Entities::Member, source: source
end
# Get a group/project member
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 8bfa998dc53..c5c214d4d13 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -83,12 +83,12 @@ module API
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::Note
+ if note.valid?
+ present note, with: Entities::const_get(note.class.name)
else
- not_found!("Note #{@note.errors.messages}")
+ not_found!("Note #{note.errors.messages}")
end
end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 644d836ed0b..6d99617b56f 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -91,8 +91,8 @@ module API
# Create new project
#
# Parameters:
- # name (required) - name for new project
- # description (optional) - short project description
+ # name (required) - name for new project
+ # description (optional) - short project description
# issues_enabled (optional)
# merge_requests_enabled (optional)
# builds_enabled (optional)
@@ -100,33 +100,35 @@ module API
# snippets_enabled (optional)
# container_registry_enabled (optional)
# shared_runners_enabled (optional)
- # namespace_id (optional) - defaults to user namespace
- # public (optional) - if true same as setting visibility_level = 20
- # visibility_level (optional) - 0 by default
+ # namespace_id (optional) - defaults to user namespace
+ # public (optional) - if true same as setting visibility_level = 20
+ # visibility_level (optional) - 0 by default
# import_url (optional)
# public_builds (optional)
# lfs_enabled (optional)
+ # request_access_enabled (optional) - Allow users to request member access
# Example Request
# POST /projects
post do
required_attributes! [:name]
- attrs = attributes_for_keys [:name,
- :path,
+ attrs = attributes_for_keys [:builds_enabled,
+ :container_registry_enabled,
:description,
+ :import_url,
:issues_enabled,
+ :lfs_enabled,
:merge_requests_enabled,
- :builds_enabled,
- :wiki_enabled,
- :snippets_enabled,
- :container_registry_enabled,
- :shared_runners_enabled,
+ :name,
:namespace_id,
+ :only_allow_merge_if_build_succeeds,
+ :path,
:public,
- :visibility_level,
- :import_url,
:public_builds,
- :only_allow_merge_if_build_succeeds,
- :lfs_enabled]
+ :request_access_enabled,
+ :shared_runners_enabled,
+ :snippets_enabled,
+ :visibility_level,
+ :wiki_enabled]
attrs = map_public_to_visibility_level(attrs)
@project = ::Projects::CreateService.new(current_user, attrs).execute
if @project.saved?
@@ -143,10 +145,10 @@ module API
# Create new project for a specified user. Only available to admin users.
#
# Parameters:
- # user_id (required) - The ID of a user
- # name (required) - name for new project
- # description (optional) - short project description
- # default_branch (optional) - 'master' by default
+ # user_id (required) - The ID of a user
+ # name (required) - name for new project
+ # description (optional) - short project description
+ # default_branch (optional) - 'master' by default
# issues_enabled (optional)
# merge_requests_enabled (optional)
# builds_enabled (optional)
@@ -154,31 +156,33 @@ module API
# snippets_enabled (optional)
# container_registry_enabled (optional)
# shared_runners_enabled (optional)
- # public (optional) - if true same as setting visibility_level = 20
+ # public (optional) - if true same as setting visibility_level = 20
# visibility_level (optional)
# import_url (optional)
# public_builds (optional)
# lfs_enabled (optional)
+ # request_access_enabled (optional) - Allow users to request member access
# Example Request
# POST /projects/user/:user_id
post "user/:user_id" do
authenticated_as_admin!
user = User.find(params[:user_id])
- attrs = attributes_for_keys [:name,
- :description,
+ attrs = attributes_for_keys [:builds_enabled,
:default_branch,
+ :description,
+ :import_url,
:issues_enabled,
+ :lfs_enabled,
:merge_requests_enabled,
- :builds_enabled,
- :wiki_enabled,
- :snippets_enabled,
- :shared_runners_enabled,
+ :name,
+ :only_allow_merge_if_build_succeeds,
:public,
- :visibility_level,
- :import_url,
:public_builds,
- :only_allow_merge_if_build_succeeds,
- :lfs_enabled]
+ :request_access_enabled,
+ :shared_runners_enabled,
+ :snippets_enabled,
+ :visibility_level,
+ :wiki_enabled]
attrs = map_public_to_visibility_level(attrs)
@project = ::Projects::CreateService.new(user, attrs).execute
if @project.saved?
@@ -203,7 +207,9 @@ module API
if namespace_id.present?
namespace = Namespace.find_by(id: namespace_id) || Namespace.find_by_path_or_name(namespace_id)
- not_found!('Target Namespace') unless namespace
+ unless namespace && can?(current_user, :create_projects, namespace)
+ not_found!('Target Namespace')
+ end
attrs[:namespace] = namespace
end
@@ -242,22 +248,23 @@ module API
# Example Request
# PUT /projects/:id
put ':id' do
- attrs = attributes_for_keys [:name,
- :path,
- :description,
+ attrs = attributes_for_keys [:builds_enabled,
+ :container_registry_enabled,
:default_branch,
+ :description,
:issues_enabled,
+ :lfs_enabled,
:merge_requests_enabled,
- :builds_enabled,
- :wiki_enabled,
- :snippets_enabled,
- :container_registry_enabled,
- :shared_runners_enabled,
+ :name,
+ :only_allow_merge_if_build_succeeds,
+ :path,
:public,
- :visibility_level,
:public_builds,
- :only_allow_merge_if_build_succeeds,
- :lfs_enabled]
+ :request_access_enabled,
+ :shared_runners_enabled,
+ :snippets_enabled,
+ :visibility_level,
+ :wiki_enabled]
attrs = map_public_to_visibility_level(attrs)
authorize_admin_project
authorize! :rename_project, user_project if attrs[:name].present?
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index 6e13282d5f4..2470362e019 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -7,7 +7,7 @@ module Banzai
UNSAFE_PROTOCOLS = %w(data javascript vbscript).freeze
def whitelist
- whitelist = super.dup
+ whitelist = super
customize_whitelist(whitelist)
@@ -42,58 +42,58 @@ module Banzai
# Allow any protocol in `a` elements...
whitelist[:protocols].delete('a')
- whitelist[:transformers] = whitelist[:transformers].dup
-
# ...but then remove links with unsafe protocols
- whitelist[:transformers].push(remove_unsafe_links)
+ whitelist[:transformers].push(self.class.remove_unsafe_links)
# Remove `rel` attribute from `a` elements
- whitelist[:transformers].push(remove_rel)
+ whitelist[:transformers].push(self.class.remove_rel)
# Remove `class` attribute from non-highlight spans
- whitelist[:transformers].push(clean_spans)
+ whitelist[:transformers].push(self.class.clean_spans)
whitelist
end
- def remove_unsafe_links
- lambda do |env|
- node = env[:node]
+ class << self
+ def remove_unsafe_links
+ lambda do |env|
+ node = env[:node]
- return unless node.name == 'a'
- return unless node.has_attribute?('href')
+ return unless node.name == 'a'
+ return unless node.has_attribute?('href')
- begin
- uri = Addressable::URI.parse(node['href'])
- uri.scheme = uri.scheme.strip.downcase if uri.scheme
+ begin
+ uri = Addressable::URI.parse(node['href'])
+ uri.scheme = uri.scheme.strip.downcase if uri.scheme
- node.remove_attribute('href') if UNSAFE_PROTOCOLS.include?(uri.scheme)
- rescue Addressable::URI::InvalidURIError
- node.remove_attribute('href')
+ node.remove_attribute('href') if UNSAFE_PROTOCOLS.include?(uri.scheme)
+ rescue Addressable::URI::InvalidURIError
+ node.remove_attribute('href')
+ end
end
end
- end
- def remove_rel
- lambda do |env|
- if env[:node_name] == 'a'
- env[:node].remove_attribute('rel')
+ def remove_rel
+ lambda do |env|
+ if env[:node_name] == 'a'
+ env[:node].remove_attribute('rel')
+ end
end
end
- end
- def clean_spans
- lambda do |env|
- node = env[:node]
+ def clean_spans
+ lambda do |env|
+ node = env[:node]
- return unless node.name == 'span'
- return unless node.has_attribute?('class')
+ return unless node.name == 'span'
+ return unless node.has_attribute?('class')
- unless has_ancestor?(node, 'pre')
- node.remove_attribute('class')
- end
+ unless node.ancestors.any? { |n| n.name.casecmp('pre').zero? }
+ node.remove_attribute('class')
+ end
- { node_whitelist: [node] }
+ { node_whitelist: [node] }
+ end
end
end
end
diff --git a/lib/banzai/filter/wiki_link_filter/rewriter.rb b/lib/banzai/filter/wiki_link_filter/rewriter.rb
index 2e2c8da311e..e7a1ec8457d 100644
--- a/lib/banzai/filter/wiki_link_filter/rewriter.rb
+++ b/lib/banzai/filter/wiki_link_filter/rewriter.rb
@@ -31,6 +31,7 @@ module Banzai
def apply_relative_link_rules!
if @uri.relative? && @uri.path.present?
link = ::File.join(@wiki_base_path, @uri.path)
+ link = "#{link}##{@uri.fragment}" if @uri.fragment
@uri = Addressable::URI.parse(link)
end
end
diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb
index 3f5bdaba3f5..66c05773b68 100644
--- a/lib/ci/api/entities.rb
+++ b/lib/ci/api/entities.rb
@@ -15,6 +15,15 @@ module Ci
expose :filename, :size
end
+ class BuildOptions < Grape::Entity
+ expose :image
+ expose :services
+ expose :artifacts
+ expose :cache
+ expose :dependencies
+ expose :after_script
+ end
+
class Build < Grape::Entity
expose :id, :ref, :tag, :sha, :status
expose :name, :token, :stage
diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb
index ba80c89df78..23353c62885 100644
--- a/lib/ci/api/helpers.rb
+++ b/lib/ci/api/helpers.rb
@@ -14,12 +14,20 @@ module Ci
end
def authenticate_build_token!(build)
- token = (params[BUILD_TOKEN_PARAM] || env[BUILD_TOKEN_HEADER]).to_s
- forbidden! unless token && build.valid_token?(token)
+ forbidden! unless build_token_valid?(build)
end
def runner_registration_token_valid?
- params[:token] == current_application_settings.runners_registration_token
+ ActiveSupport::SecurityUtils.variable_size_secure_compare(
+ params[:token],
+ current_application_settings.runners_registration_token)
+ end
+
+ def build_token_valid?(build)
+ token = (params[BUILD_TOKEN_PARAM] || env[BUILD_TOKEN_HEADER]).to_s
+
+ # We require to also check `runners_token` to maintain compatibility with old version of runners
+ token && (build.valid_token?(token) || build.project.valid_runners_token?(token))
end
def update_runner_last_contact(save: true)
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index caa815f720f..0369e80312a 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -60,7 +60,7 @@ module Ci
name: job[:name].to_s,
allow_failure: job[:allow_failure] || false,
when: job[:when] || 'on_success',
- environment: job[:environment],
+ environment: job[:environment_name],
yaml_variables: yaml_variables(name),
options: {
image: job[:image],
@@ -69,6 +69,7 @@ module Ci
cache: job[:cache],
dependencies: job[:dependencies],
after_script: job[:after_script],
+ environment: job[:environment],
}.compact
}
end
diff --git a/lib/ci/mask_secret.rb b/lib/ci/mask_secret.rb
new file mode 100644
index 00000000000..997377abc55
--- /dev/null
+++ b/lib/ci/mask_secret.rb
@@ -0,0 +1,10 @@
+module Ci::MaskSecret
+ class << self
+ def mask!(value, token)
+ return value unless value.present? && token.present?
+
+ value.gsub!(token, 'x' * token.length)
+ value
+ end
+ end
+end
diff --git a/lib/expand_variables.rb b/lib/expand_variables.rb
new file mode 100644
index 00000000000..7b1533d0d32
--- /dev/null
+++ b/lib/expand_variables.rb
@@ -0,0 +1,17 @@
+module ExpandVariables
+ class << self
+ def expand(value, variables)
+ # Convert hash array to variables
+ if variables.is_a?(Array)
+ variables = variables.reduce({}) do |hash, variable|
+ hash[variable[:key]] = variable[:value]
+ hash
+ end
+ end
+
+ value.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)|\${\g<1>}|%\g<1>%/) do
+ variables[$1 || $2]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 91f0270818a..7c0f2115d43 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -1,21 +1,22 @@
module Gitlab
module Auth
- Result = Struct.new(:user, :type)
+ class MissingPersonalTokenError < StandardError; end
class << self
def find_for_git_client(login, password, project:, ip:)
raise "Must provide an IP for rate limiting" if ip.nil?
- result = Result.new
+ 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) ||
+ Gitlab::Auth::Result.new
- if valid_ci_request?(login, password, project)
- result.type = :ci
- else
- result = populate_result(login, password)
- end
+ rate_limit!(ip, success: result.success?, login: login)
- success = result.user.present? || [:ci, :missing_personal_token].include?(result.type)
- rate_limit!(ip, success: success, login: login)
result
end
@@ -57,44 +58,31 @@ module Gitlab
private
- def valid_ci_request?(login, password, project)
+ def service_request_check(login, password, project)
matched_login = /(?<service>^[a-zA-Z]*-ci)-token$/.match(login)
- return false unless project && matched_login.present?
+ return unless project && matched_login.present?
underscored_service = matched_login['service'].underscore
- if underscored_service == 'gitlab_ci'
- project && project.valid_build_token?(password)
- elsif Service.available_services_names.include?(underscored_service)
+ if Service.available_services_names.include?(underscored_service)
# We treat underscored_service as a trusted input because it is included
# in the Service.available_services_names whitelist.
service = project.public_send("#{underscored_service}_service")
- service && service.activated? && service.valid_token?(password)
- end
- end
-
- def populate_result(login, password)
- result =
- user_with_password_for_git(login, password) ||
- oauth_access_token_check(login, password) ||
- personal_access_token_check(login, password)
-
- if result
- result.type = nil unless result.user
-
- if result.user && result.user.two_factor_enabled? && result.type == :gitlab_or_ldap
- result.type = :missing_personal_token
+ if service && service.activated? && service.valid_token?(password)
+ Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities)
end
end
-
- result || Result.new
end
def user_with_password_for_git(login, password)
user = find_with_user_password(login, password)
- Result.new(user, :gitlab_or_ldap) if user
+ return unless user
+
+ raise Gitlab::Auth::MissingPersonalTokenError if user.two_factor_enabled?
+
+ Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)
end
def oauth_access_token_check(login, password)
@@ -102,7 +90,7 @@ module Gitlab
token = Doorkeeper::AccessToken.by_token(password)
if token && token.accessible?
user = User.find_by(id: token.resource_owner_id)
- Result.new(user, :oauth)
+ Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)
end
end
end
@@ -111,9 +99,76 @@ module Gitlab
if login && password
user = User.find_by_personal_access_token(password)
validation = User.by_login(login)
- Result.new(user, :personal_token) if user == validation
+ Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities) if user.present? && user == validation
+ end
+ end
+
+ def lfs_token_check(login, password)
+ deploy_key_matches = login.match(/\Alfs\+deploy-key-(\d+)\z/)
+
+ actor =
+ if deploy_key_matches
+ DeployKey.find(deploy_key_matches[1])
+ else
+ User.by_login(login)
+ end
+
+ return unless actor
+
+ token_handler = Gitlab::LfsToken.new(actor)
+
+ authentication_abilities =
+ if token_handler.user?
+ full_authentication_abilities
+ else
+ read_authentication_abilities
+ end
+
+ Result.new(actor, nil, token_handler.type, authentication_abilities) if Devise.secure_compare(token_handler.value, password)
+ end
+
+ def build_access_token_check(login, password)
+ return unless login == 'gitlab-ci-token'
+ return unless password
+
+ build = ::Ci::Build.running.find_by_token(password)
+ return unless build
+ return unless build.project.builds_enabled?
+
+ if build.user
+ # If user is assigned to build, use restricted credentials of user
+ Gitlab::Auth::Result.new(build.user, build.project, :build, build_authentication_abilities)
+ else
+ # Otherwise use generic CI credentials (backward compatibility)
+ Gitlab::Auth::Result.new(nil, build.project, :ci, build_authentication_abilities)
end
end
+
+ public
+
+ def build_authentication_abilities
+ [
+ :read_project,
+ :build_download_code,
+ :build_read_container_image,
+ :build_create_container_image
+ ]
+ end
+
+ def read_authentication_abilities
+ [
+ :read_project,
+ :download_code,
+ :read_container_image
+ ]
+ end
+
+ def full_authentication_abilities
+ read_authentication_abilities + [
+ :push_code,
+ :create_container_image
+ ]
+ end
end
end
end
diff --git a/lib/gitlab/auth/result.rb b/lib/gitlab/auth/result.rb
new file mode 100644
index 00000000000..6be7f690676
--- /dev/null
+++ b/lib/gitlab/auth/result.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module Auth
+ Result = Struct.new(:actor, :project, :type, :authentication_abilities) do
+ def ci?(for_project)
+ type == :ci &&
+ project &&
+ project == for_project
+ end
+
+ def lfs_deploy_token?(for_project)
+ type == :lfs_deploy_token &&
+ actor &&
+ actor.projects.include?(for_project)
+ end
+
+ def success?
+ actor.present? || type == :ci
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb
index c412249a01e..79eac66b364 100644
--- a/lib/gitlab/backend/shell.rb
+++ b/lib/gitlab/backend/shell.rb
@@ -6,7 +6,12 @@ module Gitlab
KeyAdder = Struct.new(:io) do
def add_key(id, key)
- key.gsub!(/[[:space:]]+/, ' ').strip!
+ key = Gitlab::Shell.strip_key(key)
+ # Newline and tab are part of the 'protocol' used to transmit id+key to the other end
+ if key.include?("\t") || key.include?("\n")
+ raise Error.new("Invalid key: #{key.inspect}")
+ end
+
io.puts("#{id}\t#{key}")
end
end
@@ -16,6 +21,10 @@ module Gitlab
@version_required ||= File.read(Rails.root.
join('GITLAB_SHELL_VERSION')).strip
end
+
+ def strip_key(key)
+ key.split(/ /)[0, 2].join(' ')
+ end
end
# Init new repository
@@ -107,7 +116,7 @@ module Gitlab
#
def add_key(key_id, key_content)
Gitlab::Utils.system_silent([gitlab_shell_keys_path,
- 'add-key', key_id, key_content])
+ 'add-key', key_id, self.class.strip_key(key_content)])
end
# Batch-add keys to authorized_keys
diff --git a/lib/gitlab/ci/config/node/environment.rb b/lib/gitlab/ci/config/node/environment.rb
new file mode 100644
index 00000000000..d388ab6b879
--- /dev/null
+++ b/lib/gitlab/ci/config/node/environment.rb
@@ -0,0 +1,68 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # Entry that represents an environment.
+ #
+ class Environment < Entry
+ include Validatable
+
+ ALLOWED_KEYS = %i[name url]
+
+ validations do
+ validate do
+ unless hash? || string?
+ errors.add(:config, 'should be a hash or a string')
+ end
+ end
+
+ validates :name, presence: true
+ validates :name,
+ type: {
+ with: String,
+ message: Gitlab::Regex.environment_name_regex_message }
+
+ validates :name,
+ format: {
+ with: Gitlab::Regex.environment_name_regex,
+ message: Gitlab::Regex.environment_name_regex_message }
+
+ with_options if: :hash? do
+ validates :config, allowed_keys: ALLOWED_KEYS
+
+ validates :url,
+ length: { maximum: 255 },
+ addressable_url: true,
+ allow_nil: true
+ end
+ end
+
+ def hash?
+ @config.is_a?(Hash)
+ end
+
+ def string?
+ @config.is_a?(String)
+ end
+
+ def name
+ value[:name]
+ end
+
+ def url
+ value[:url]
+ end
+
+ def value
+ case @config
+ when String then { name: @config }
+ when Hash then @config
+ else {}
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb
index 0cbdf7619c0..603334d6793 100644
--- a/lib/gitlab/ci/config/node/job.rb
+++ b/lib/gitlab/ci/config/node/job.rb
@@ -13,7 +13,7 @@ module Gitlab
type stage when artifacts cache dependencies before_script
after_script variables environment]
- attributes :tags, :allow_failure, :when, :environment, :dependencies
+ attributes :tags, :allow_failure, :when, :dependencies
validations do
validates :config, allowed_keys: ALLOWED_KEYS
@@ -29,58 +29,53 @@ module Gitlab
inclusion: { in: %w[on_success on_failure always manual],
message: 'should be on_success, on_failure, ' \
'always or manual' }
- validates :environment,
- type: {
- with: String,
- message: Gitlab::Regex.environment_name_regex_message }
- validates :environment,
- format: {
- with: Gitlab::Regex.environment_name_regex,
- message: Gitlab::Regex.environment_name_regex_message }
validates :dependencies, array_of_strings: true
end
end
- node :before_script, Script,
+ node :before_script, Node::Script,
description: 'Global before script overridden in this job.'
- node :script, Commands,
+ node :script, Node::Commands,
description: 'Commands that will be executed in this job.'
- node :stage, Stage,
+ node :stage, Node::Stage,
description: 'Pipeline stage this job will be executed into.'
- node :type, Stage,
+ node :type, Node::Stage,
description: 'Deprecated: stage this job will be executed into.'
- node :after_script, Script,
+ node :after_script, Node::Script,
description: 'Commands that will be executed when finishing job.'
- node :cache, Cache,
+ node :cache, Node::Cache,
description: 'Cache definition for this job.'
- node :image, Image,
+ node :image, Node::Image,
description: 'Image that will be used to execute this job.'
- node :services, Services,
+ node :services, Node::Services,
description: 'Services that will be used to execute this job.'
- node :only, Trigger,
+ node :only, Node::Trigger,
description: 'Refs policy this job will be executed for.'
- node :except, Trigger,
+ node :except, Node::Trigger,
description: 'Refs policy this job will be executed for.'
- node :variables, Variables,
+ node :variables, Node::Variables,
description: 'Environment variables available for this job.'
- node :artifacts, Artifacts,
+ node :artifacts, Node::Artifacts,
description: 'Artifacts configuration for this job.'
+ node :environment, Node::Environment,
+ description: 'Environment configuration for this job.'
+
helpers :before_script, :script, :stage, :type, :after_script,
:cache, :image, :services, :only, :except, :variables,
- :artifacts, :commands
+ :artifacts, :commands, :environment
def compose!(deps = nil)
super do
@@ -133,6 +128,8 @@ module Gitlab
only: only,
except: except,
variables: variables_defined? ? variables : nil,
+ environment: environment_defined? ? environment : nil,
+ environment_name: environment_defined? ? environment[:name] : nil,
artifacts: artifacts,
after_script: after_script }
end
diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb
index bd681f03173..b164f5a2eea 100644
--- a/lib/gitlab/contributions_calendar.rb
+++ b/lib/gitlab/contributions_calendar.rb
@@ -1,16 +1,16 @@
module Gitlab
class ContributionsCalendar
- attr_reader :timestamps, :projects, :user
+ attr_reader :activity_dates, :projects, :user
def initialize(projects, user)
@projects = projects
@user = user
end
- def timestamps
- return @timestamps if @timestamps.present?
+ def activity_dates
+ return @activity_dates if @activity_dates.present?
- @timestamps = {}
+ @activity_dates = {}
date_from = 1.year.ago
events = Event.reorder(nil).contributions.where(author_id: user.id).
@@ -19,18 +19,17 @@ module Gitlab
select('date(created_at) as date, count(id) as total_amount').
map(&:attributes)
- dates = (1.year.ago.to_date..Date.today).to_a
+ activity_dates = (1.year.ago.to_date..Date.today).to_a
- dates.each do |date|
- date_id = date.to_time.to_i.to_s
+ activity_dates.each do |date|
day_events = events.find { |day_events| day_events["date"] == date }
if day_events
- @timestamps[date_id] = day_events["total_amount"]
+ @activity_dates[date] = day_events["total_amount"]
end
end
- @timestamps
+ @activity_dates
end
def events_by_date(date)
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 12fbb78c53e..ef9160d6437 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -59,10 +59,8 @@ module Gitlab
# When the DBMS is not available, an exception (e.g. PG::ConnectionBad) is raised
active_db_connection = ActiveRecord::Base.connection.active? rescue false
- ENV['USE_DB'] != 'false' &&
active_db_connection &&
- ActiveRecord::Base.connection.table_exists?('application_settings')
-
+ ActiveRecord::Base.connection.table_exists?('application_settings')
rescue ActiveRecord::NoDatabaseError
false
end
diff --git a/lib/gitlab/database/date_time.rb b/lib/gitlab/database/date_time.rb
new file mode 100644
index 00000000000..b6a89f715fd
--- /dev/null
+++ b/lib/gitlab/database/date_time.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module Database
+ module DateTime
+ # Find the first of the `end_time_attrs` that isn't `NULL`. Subtract from it
+ # the first of the `start_time_attrs` that isn't NULL. `SELECT` the resulting interval
+ # along with an alias specified by the `as` parameter.
+ #
+ # Note: For MySQL, the interval is returned in seconds.
+ # For PostgreSQL, the interval is returned as an INTERVAL type.
+ def subtract_datetimes(query_so_far, end_time_attrs, start_time_attrs, as)
+ diff_fn = if Gitlab::Database.postgresql?
+ Arel::Nodes::Subtraction.new(
+ Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(end_time_attrs)),
+ Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(start_time_attrs)))
+ elsif Gitlab::Database.mysql?
+ Arel::Nodes::NamedFunction.new(
+ "TIMESTAMPDIFF",
+ [Arel.sql('second'),
+ Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(start_time_attrs)),
+ Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(end_time_attrs))])
+ end
+
+ query_so_far.project(diff_fn.as(as))
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/median.rb b/lib/gitlab/database/median.rb
new file mode 100644
index 00000000000..1444d25ebc7
--- /dev/null
+++ b/lib/gitlab/database/median.rb
@@ -0,0 +1,112 @@
+# https://www.periscopedata.com/blog/medians-in-sql.html
+module Gitlab
+ module Database
+ module Median
+ def median_datetime(arel_table, query_so_far, column_sym)
+ median_queries =
+ if Gitlab::Database.postgresql?
+ pg_median_datetime_sql(arel_table, query_so_far, column_sym)
+ elsif Gitlab::Database.mysql?
+ mysql_median_datetime_sql(arel_table, query_so_far, column_sym)
+ end
+
+ results = Array.wrap(median_queries).map do |query|
+ ActiveRecord::Base.connection.execute(query)
+ end
+ extract_median(results).presence
+ end
+
+ def extract_median(results)
+ result = results.compact.first
+
+ if Gitlab::Database.postgresql?
+ result = result.first.presence
+ median = result['median'] if result
+ median.to_f if median
+ elsif Gitlab::Database.mysql?
+ result.to_a.flatten.first
+ end
+ end
+
+ def mysql_median_datetime_sql(arel_table, query_so_far, column_sym)
+ query = arel_table.
+ from(arel_table.project(Arel.sql('*')).order(arel_table[column_sym]).as(arel_table.table_name)).
+ project(average([arel_table[column_sym]], 'median')).
+ where(
+ Arel::Nodes::Between.new(
+ Arel.sql("(select @row_id := @row_id + 1)"),
+ Arel::Nodes::And.new(
+ [Arel.sql('@ct/2.0'),
+ Arel.sql('@ct/2.0 + 1')]
+ )
+ )
+ ).
+ # Disallow negative values
+ where(arel_table[column_sym].gteq(0))
+
+ [
+ Arel.sql("CREATE TEMPORARY TABLE IF NOT EXISTS #{query_so_far.to_sql}"),
+ Arel.sql("set @ct := (select count(1) from #{arel_table.table_name});"),
+ Arel.sql("set @row_id := 0;"),
+ query.to_sql,
+ Arel.sql("DROP TEMPORARY TABLE IF EXISTS #{arel_table.table_name};")
+ ]
+ end
+
+ def pg_median_datetime_sql(arel_table, query_so_far, column_sym)
+ # Create a CTE with the column we're operating on, row number (after sorting by the column
+ # we're operating on), and count of the table we're operating on (duplicated across) all rows
+ # of the CTE. For example, if we're looking to find the median of the `projects.star_count`
+ # column, the CTE might look like this:
+ #
+ # star_count | row_id | ct
+ # ------------+--------+----
+ # 5 | 1 | 3
+ # 9 | 2 | 3
+ # 15 | 3 | 3
+ cte_table = Arel::Table.new("ordered_records")
+ cte = Arel::Nodes::As.new(
+ cte_table,
+ arel_table.
+ project(
+ arel_table[column_sym].as(column_sym.to_s),
+ Arel::Nodes::Over.new(Arel::Nodes::NamedFunction.new("row_number", []),
+ Arel::Nodes::Window.new.order(arel_table[column_sym])).as('row_id'),
+ arel_table.project("COUNT(1)").as('ct')).
+ # Disallow negative values
+ where(arel_table[column_sym].gteq(zero_interval)))
+
+ # From the CTE, select either the middle row or the middle two rows (this is accomplished
+ # by 'where cte.row_id between cte.ct / 2.0 AND cte.ct / 2.0 + 1'). Find the average of the
+ # selected rows, and this is the median value.
+ cte_table.project(average([extract_epoch(cte_table[column_sym])], "median")).
+ where(
+ Arel::Nodes::Between.new(
+ cte_table[:row_id],
+ Arel::Nodes::And.new(
+ [(cte_table[:ct] / Arel.sql('2.0')),
+ (cte_table[:ct] / Arel.sql('2.0') + 1)]
+ )
+ )
+ ).
+ with(query_so_far, cte).
+ to_sql
+ end
+
+ private
+
+ def average(args, as)
+ Arel::Nodes::NamedFunction.new("AVG", args, as)
+ end
+
+ def extract_epoch(arel_attribute)
+ Arel.sql(%Q{EXTRACT(EPOCH FROM "#{arel_attribute.relation.name}"."#{arel_attribute.name}")})
+ 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")])
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 927f9dad20b..0bd6e148ba8 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -129,12 +129,14 @@ module Gitlab
# column - The name of the column to add.
# type - The column type (e.g. `:integer`).
# default - The default value for the column.
+ # limit - Sets a column limit. For example, for :integer, the default is
+ # 4-bytes. Set `limit: 8` to allow 8-byte integers.
# allow_null - When set to `true` the column will allow NULL values, the
# default is to not allow NULL values.
#
# This method can also take a block which is passed directly to the
# `update_column_in_batches` method.
- def add_column_with_default(table, column, type, default:, allow_null: false, &block)
+ def add_column_with_default(table, column, type, default:, limit: nil, allow_null: false, &block)
if transaction_open?
raise 'add_column_with_default can not be run inside a transaction, ' \
'you can disable transactions by calling disable_ddl_transaction! ' \
@@ -144,7 +146,11 @@ module Gitlab
disable_statement_timeout
transaction do
- add_column(table, column, type, default: nil)
+ if limit
+ add_column(table, column, type, default: nil, limit: limit)
+ else
+ add_column(table, column, type, default: nil)
+ end
# Changing the default before the update ensures any newly inserted
# rows already use the proper default value.
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index 7584efe4fa8..3cd515e4a3a 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -18,6 +18,16 @@ module Gitlab
end
end
+ def committer_hash(email:, name:)
+ return if email.nil? || name.nil?
+
+ {
+ email: email,
+ name: name,
+ time: Time.now
+ }
+ end
+
def tag_name(ref)
ref = ref.to_s
if self.tag_ref?(ref)
diff --git a/lib/gitlab/git/hook.rb b/lib/gitlab/git/hook.rb
index 9b681e636c7..bd90d24a2ec 100644
--- a/lib/gitlab/git/hook.rb
+++ b/lib/gitlab/git/hook.rb
@@ -17,11 +17,13 @@ module Gitlab
def trigger(gl_id, oldrev, newrev, ref)
return [true, nil] unless exists?
- case name
- when "pre-receive", "post-receive"
- call_receive_hook(gl_id, oldrev, newrev, ref)
- when "update"
- call_update_hook(gl_id, oldrev, newrev, ref)
+ Bundler.with_clean_env do
+ case name
+ when "pre-receive", "post-receive"
+ call_receive_hook(gl_id, oldrev, newrev, ref)
+ when "update"
+ call_update_hook(gl_id, oldrev, newrev, ref)
+ end
end
end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 1882eb8d050..799794c0171 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -5,12 +5,13 @@ module Gitlab
DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }
PUSH_COMMANDS = %w{ git-receive-pack }
- attr_reader :actor, :project, :protocol, :user_access
+ attr_reader :actor, :project, :protocol, :user_access, :authentication_abilities
- def initialize(actor, project, protocol)
+ def initialize(actor, project, protocol, authentication_abilities:)
@actor = actor
@project = project
@protocol = protocol
+ @authentication_abilities = authentication_abilities
@user_access = UserAccess.new(user, project: project)
end
@@ -60,14 +61,26 @@ module Gitlab
end
def user_download_access_check
- unless user_access.can_do_action?(:download_code)
+ unless user_can_download_code? || build_can_download_code?
return build_status_object(false, "You are not allowed to download code from this project.")
end
build_status_object(true)
end
+ def user_can_download_code?
+ authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_code)
+ end
+
+ def build_can_download_code?
+ authentication_abilities.include?(:build_download_code) && user_access.can_do_action?(:build_download_code)
+ end
+
def user_push_access_check(changes)
+ unless authentication_abilities.include?(:push_code)
+ return build_status_object(false, "You are not allowed to upload code for this project.")
+ end
+
if changes.blank?
return build_status_object(true)
end
diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb
index e9725880c5e..605abfabdab 100644
--- a/lib/gitlab/github_import/project_creator.rb
+++ b/lib/gitlab/github_import/project_creator.rb
@@ -3,8 +3,9 @@ module Gitlab
class ProjectCreator
attr_reader :repo, :namespace, :current_user, :session_data
- def initialize(repo, namespace, current_user, session_data)
+ def initialize(repo, name, namespace, current_user, session_data)
@repo = repo
+ @name = name
@namespace = namespace
@current_user = current_user
@session_data = session_data
@@ -13,8 +14,8 @@ module Gitlab
def execute
project = ::Projects::CreateService.new(
current_user,
- name: repo.name,
- path: repo.name,
+ name: @name,
+ path: @name,
description: repo.description,
namespace_id: namespace.id,
visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : ApplicationSetting.current.default_project_visibility,
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index c5a11148d33..2c21804fe7a 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -11,7 +11,6 @@ module Gitlab
if current_user
gon.current_user_id = current_user.id
- gon.api_token = current_user.private_token
end
end
end
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index bb562bdcd2c..181e288a014 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -2,7 +2,8 @@ module Gitlab
module ImportExport
extend self
- VERSION = '0.1.3'
+ # For every version update, the version history in import_export.md has to be kept up to date.
+ VERSION = '0.1.4'
FILENAME_LIMIT = 50
def export_path(relative_path:)
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index c2e8a1ca5dd..88803d76623 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -10,6 +10,7 @@ project_tree:
- milestone:
- :events
- snippets:
+ - :award_emoji
- notes:
:author
- :releases
@@ -35,7 +36,9 @@ project_tree:
- :deploy_keys
- :services
- :hooks
- - :protected_branches
+ - protected_branches:
+ - :merge_access_levels
+ - :push_access_levels
- :labels
- milestones:
- :events
@@ -64,6 +67,8 @@ excluded_attributes:
- :milestone_id
merge_requests:
- :milestone_id
+ award_emoji:
+ - :awardable_id
methods:
statuses:
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index b0726268ca6..354ccd64696 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -7,7 +7,9 @@ module Gitlab
variables: 'Ci::Variable',
triggers: 'Ci::Trigger',
builds: 'Ci::Build',
- hooks: 'ProjectHook' }.freeze
+ hooks: 'ProjectHook',
+ merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
+ push_access_levels: 'ProtectedBranch::PushAccessLevel' }.freeze
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id].freeze
@@ -17,6 +19,8 @@ module Gitlab
EXISTING_OBJECT_CHECK = %i[milestone milestones label labels].freeze
+ FINDER_ATTRIBUTES = %w[title project_id].freeze
+
def self.create(*args)
new(*args).create
end
@@ -149,7 +153,7 @@ module Gitlab
end
def parsed_relation_hash
- @relation_hash.reject { |k, _v| !relation_class.attribute_method?(k) }
+ @parsed_relation_hash ||= @relation_hash.reject { |k, _v| !relation_class.attribute_method?(k) }
end
def set_st_diffs
@@ -161,14 +165,30 @@ module Gitlab
# Otherwise always create the record, skipping the extra SELECT clause.
@existing_or_new_object ||= begin
if EXISTING_OBJECT_CHECK.include?(@relation_name)
- existing_object = relation_class.find_or_initialize_by(parsed_relation_hash.slice('title', 'project_id'))
- existing_object.assign_attributes(parsed_relation_hash)
+ events = parsed_relation_hash.delete('events')
+
+ unless events.blank?
+ existing_object.assign_attributes(events: events)
+ end
+
existing_object
else
relation_class.new(parsed_relation_hash)
end
end
end
+
+ def existing_object
+ @existing_object ||=
+ begin
+ finder_hash = parsed_relation_hash.slice(*FINDER_ATTRIBUTES)
+ existing_object = relation_class.find_or_create_by(finder_hash)
+ # Done in two steps, as MySQL behaves differently than PostgreSQL using
+ # the +find_or_create_by+ method and does not return the ID the second time.
+ existing_object.update(parsed_relation_hash)
+ existing_object
+ end
+ end
end
end
end
diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb
index 6d9379acf25..d1e33ea8678 100644
--- a/lib/gitlab/import_export/repo_restorer.rb
+++ b/lib/gitlab/import_export/repo_restorer.rb
@@ -22,10 +22,6 @@ module Gitlab
private
- def repos_path
- Gitlab.config.gitlab_shell.repos_path
- end
-
def path_to_repo
@project.repository.path_to_repo
end
diff --git a/lib/gitlab/import_export/version_checker.rb b/lib/gitlab/import_export/version_checker.rb
index de3fe6d822e..fc08082fc86 100644
--- a/lib/gitlab/import_export/version_checker.rb
+++ b/lib/gitlab/import_export/version_checker.rb
@@ -24,8 +24,8 @@ module Gitlab
end
def verify_version!(version)
- if Gem::Version.new(version) > Gem::Version.new(Gitlab::ImportExport.version)
- raise Gitlab::ImportExport::Error.new("Import version mismatch: Required <= #{Gitlab::ImportExport.version} but was #{version}")
+ if Gem::Version.new(version) != Gem::Version.new(Gitlab::ImportExport.version)
+ raise Gitlab::ImportExport::Error.new("Import version mismatch: Required #{Gitlab::ImportExport.version} but was #{version}")
else
true
end
diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb
index 9100719da87..82cb8cef754 100644
--- a/lib/gitlab/ldap/adapter.rb
+++ b/lib/gitlab/ldap/adapter.rb
@@ -70,7 +70,7 @@ module Gitlab
private
def user_options(field, value, limit)
- options = { attributes: %W(#{config.uid} cn mail dn) }
+ options = { attributes: user_attributes }
options[:size] = limit if limit
if field.to_sym == :dn
@@ -98,6 +98,10 @@ module Gitlab
filter
end
end
+
+ def user_attributes
+ %W(#{config.uid} cn mail dn)
+ end
end
end
end
diff --git a/lib/gitlab/lfs_token.rb b/lib/gitlab/lfs_token.rb
new file mode 100644
index 00000000000..d089a2f9b0b
--- /dev/null
+++ b/lib/gitlab/lfs_token.rb
@@ -0,0 +1,54 @@
+module Gitlab
+ class LfsToken
+ attr_accessor :actor
+
+ TOKEN_LENGTH = 50
+ EXPIRY_TIME = 1800
+
+ def initialize(actor)
+ @actor =
+ case actor
+ when DeployKey, User
+ actor
+ when Key
+ actor.user
+ else
+ raise 'Bad Actor'
+ end
+ end
+
+ def generate
+ token = Devise.friendly_token(TOKEN_LENGTH)
+
+ Gitlab::Redis.with do |redis|
+ redis.set(redis_key, token, ex: EXPIRY_TIME)
+ end
+
+ token
+ end
+
+ def value
+ Gitlab::Redis.with do |redis|
+ redis.get(redis_key)
+ end
+ end
+
+ def user?
+ actor.is_a?(User)
+ end
+
+ def type
+ actor.is_a?(User) ? :lfs_token : :lfs_deploy_token
+ end
+
+ def actor_name
+ actor.is_a?(User) ? actor.username : "lfs+deploy-key-#{actor.id}"
+ end
+
+ private
+
+ def redis_key
+ "gitlab:lfs_token:#{actor.class.name.underscore}_#{actor.id}" if actor
+ end
+ end
+end
diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb
index 9376b54f43b..69c4ef721d5 100644
--- a/lib/gitlab/redis.rb
+++ b/lib/gitlab/redis.rb
@@ -9,19 +9,22 @@ module 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__)
# To be thread-safe we must be careful when writing the class instance
- # variables @url and @pool. Because @pool depends on @url we need two
+ # variables @_raw_config and @pool. Because @pool depends on @_raw_config we need two
# mutexes to prevent deadlock.
- PARAMS_MUTEX = Mutex.new
+ RAW_CONFIG_MUTEX = Mutex.new
POOL_MUTEX = Mutex.new
- private_constant :PARAMS_MUTEX, :POOL_MUTEX
+ private_constant :RAW_CONFIG_MUTEX, :POOL_MUTEX
class << self
+ # Do NOT cache in an instance variable. Result may be mutated by caller.
def params
- @params || PARAMS_MUTEX.synchronize { @params = new.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
@@ -36,8 +39,17 @@ module Gitlab
@pool.with { |redis| yield redis }
end
- def reset_params!
- @params = nil
+ def _raw_config
+ return @_raw_config if defined?(@_raw_config)
+
+ RAW_CONFIG_MUTEX.synchronize do
+ begin
+ @_raw_config = File.read(CONFIG_FILE).freeze
+ rescue Errno::ENOENT
+ @_raw_config = false
+ end
+ end
+ @_raw_config
end
end
@@ -83,12 +95,7 @@ module Gitlab
end
def fetch_config
- file = config_file
- File.exist?(file) ? YAML.load_file(file)[@rails_env] : false
- end
-
- def config_file
- File.expand_path('../../../config/resque.yml', __FILE__)
+ self.class._raw_config ? YAML.load(self.class._raw_config)[@rails_env] : false
end
end
end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index ffad5e17c78..776bbcbb5d0 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -44,7 +44,7 @@ module Gitlab
end
def file_name_regex_message
- "can contain only letters, digits, '_', '-', '@' and '.'. "
+ "can contain only letters, digits, '_', '-', '@' and '.'."
end
def file_path_regex
@@ -52,7 +52,7 @@ module Gitlab
end
def file_path_regex_message
- "can contain only letters, digits, '_', '-', '@' and '.'. Separate directories with a '/'. "
+ "can contain only letters, digits, '_', '-', '@' and '.'. Separate directories with a '/'."
end
def directory_traversal_regex
@@ -60,7 +60,7 @@ module Gitlab
end
def directory_traversal_regex_message
- "cannot include directory traversal. "
+ "cannot include directory traversal."
end
def archive_formats_regex
@@ -96,11 +96,11 @@ module Gitlab
end
def environment_name_regex
- @environment_name_regex ||= /\A[a-zA-Z0-9_-]+\z/.freeze
+ @environment_name_regex ||= /\A[a-zA-Z0-9_\\\/\${}. -]+\z/.freeze
end
def environment_name_regex_message
- "can contain only letters, digits, '-' and '_'."
+ "can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.' and spaces"
end
end
end
diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh
index bc6e4d94061..fb4d8463981 100755
--- a/scripts/lint-doc.sh
+++ b/scripts/lint-doc.sh
@@ -10,6 +10,15 @@ then
exit 1
fi
+# Ensure that the CHANGELOG does not contain duplicate versions
+DUPLICATE_CHANGELOG_VERSIONS=$(grep --extended-regexp '^v [0-9.]+' CHANGELOG | sed 's| (unreleased)||' | sort | uniq -d)
+if [ "${DUPLICATE_CHANGELOG_VERSIONS}" != "" ]
+then
+ echo '✖ ERROR: Duplicate versions in CHANGELOG:' >&2
+ echo "${DUPLICATE_CHANGELOG_VERSIONS}" >&2
+ exit 1
+fi
+
echo "✔ Linting passed"
exit 0
diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb
index ebfbf54182b..4f96567192d 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -124,8 +124,8 @@ describe Import::GithubController do
context "when the GitHub user and GitLab user's usernames match" do
it "takes the current user's namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
- to receive(:new).with(github_repo, user.namespace, user, access_params).
- and_return(double(execute: true))
+ to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params).
+ and_return(double(execute: true))
post :create, format: :js
end
@@ -136,8 +136,8 @@ describe Import::GithubController do
it "takes the current user's namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
- to receive(:new).with(github_repo, user.namespace, user, access_params).
- and_return(double(execute: true))
+ to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params).
+ and_return(double(execute: true))
post :create, format: :js
end
@@ -158,8 +158,8 @@ describe Import::GithubController do
context "when the namespace is owned by the GitLab user" do
it "takes the existing namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
- to receive(:new).with(github_repo, existing_namespace, user, access_params).
- and_return(double(execute: true))
+ to receive(:new).with(github_repo, github_repo.name, existing_namespace, user, access_params).
+ and_return(double(execute: true))
post :create, format: :js
end
@@ -171,9 +171,10 @@ describe Import::GithubController do
existing_namespace.save
end
- it "doesn't create a project" do
+ it "creates a project using user's namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
- not_to receive(:new)
+ to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params).
+ and_return(double(execute: true))
post :create, format: :js
end
@@ -186,15 +187,15 @@ describe Import::GithubController do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).and_return(double(execute: true))
- expect { post :create, format: :js }.to change(Namespace, :count).by(1)
+ expect { post :create, target_namespace: github_repo.name, format: :js }.to change(Namespace, :count).by(1)
end
it "takes the new namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
- to receive(:new).with(github_repo, an_instance_of(Group), user, access_params).
+ to receive(:new).with(github_repo, github_repo.name, an_instance_of(Group), user, access_params).
and_return(double(execute: true))
- post :create, format: :js
+ post :create, target_namespace: github_repo.name, format: :js
end
end
@@ -212,13 +213,34 @@ describe Import::GithubController do
it "takes the current user's namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
- to receive(:new).with(github_repo, user.namespace, user, access_params).
+ to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params).
and_return(double(execute: true))
post :create, format: :js
end
end
end
+
+ context 'user has chosen a namespace and name for the project' do
+ let(:test_namespace) { create(:namespace, name: 'test_namespace', owner: user) }
+ let(:test_name) { 'test_name' }
+
+ it 'takes the selected namespace and name' do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(github_repo, test_name, test_namespace, user, access_params).
+ and_return(double(execute: true))
+
+ post :create, { target_namespace: test_namespace.name, new_name: test_name, format: :js }
+ end
+
+ it 'takes the selected name and default namespace' do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(github_repo, test_name, user.namespace, user, access_params).
+ and_return(double(execute: true))
+
+ post :create, { new_name: test_name, format: :js }
+ end
+ end
end
end
end
diff --git a/spec/controllers/sent_notifications_controller_spec.rb b/spec/controllers/sent_notifications_controller_spec.rb
index 9ced397bd4a..191e290a118 100644
--- a/spec/controllers/sent_notifications_controller_spec.rb
+++ b/spec/controllers/sent_notifications_controller_spec.rb
@@ -1,25 +1,108 @@
require 'rails_helper'
describe SentNotificationsController, type: :controller do
- let(:user) { create(:user) }
- let(:issue) { create(:issue, author: user) }
- let(:sent_notification) { create(:sent_notification, noteable: issue) }
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:sent_notification) { create(:sent_notification, noteable: issue, recipient: user) }
- describe 'GET #unsubscribe' do
- it 'returns a 404 when calling without existing id' do
- get(:unsubscribe, id: '0' * 32)
+ let(:issue) do
+ create(:issue, project: project, author: user) do |issue|
+ issue.subscriptions.create(user: user, subscribed: true)
+ end
+ end
+
+ describe 'GET unsubscribe' do
+ context 'when the user is not logged in' do
+ context 'when the force param is passed' do
+ before { get(:unsubscribe, id: sent_notification.reply_key, force: true) }
+
+ it 'unsubscribes the user' do
+ expect(issue.subscribed?(user)).to be_falsey
+ end
+
+ it 'sets the flash message' do
+ expect(controller).to set_flash[:notice].to(/unsubscribed/).now
+ end
+
+ it 'redirects to the login page' do
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ context 'when the force param is not passed' do
+ before { get(:unsubscribe, id: sent_notification.reply_key) }
+
+ it 'does not unsubscribe the user' do
+ expect(issue.subscribed?(user)).to be_truthy
+ end
- expect(response.status).to be 404
+ it 'does not set the flash message' do
+ expect(controller).not_to set_flash[:notice]
+ end
+
+ it 'redirects to the login page' do
+ expect(response).to render_template :unsubscribe
+ end
+ end
end
- context 'calling with id' do
- it 'shows a flash message to the user' do
- get(:unsubscribe, id: sent_notification.reply_key)
+ context 'when the user is logged in' do
+ before { sign_in(user) }
+
+ context 'when the ID passed does not exist' do
+ before { get(:unsubscribe, id: sent_notification.reply_key.reverse) }
+
+ it 'does not unsubscribe the user' do
+ expect(issue.subscribed?(user)).to be_truthy
+ end
+
+ it 'does not set the flash message' do
+ expect(controller).not_to set_flash[:notice]
+ end
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'when the force param is passed' do
+ before { get(:unsubscribe, id: sent_notification.reply_key, force: true) }
+
+ it 'unsubscribes the user' do
+ expect(issue.subscribed?(user)).to be_falsey
+ end
+
+ it 'sets the flash message' do
+ expect(controller).to set_flash[:notice].to(/unsubscribed/).now
+ end
+
+ it 'redirects to the issue page' do
+ expect(response).
+ to redirect_to(namespace_project_issue_path(project.namespace, project, issue))
+ end
+ end
+
+ context 'when the force param is not passed' do
+ let(:merge_request) do
+ create(:merge_request, source_project: project, author: user) do |merge_request|
+ merge_request.subscriptions.create(user: user, subscribed: true)
+ end
+ end
+ let(:sent_notification) { create(:sent_notification, noteable: merge_request, recipient: user) }
+ before { get(:unsubscribe, id: sent_notification.reply_key) }
+
+ it 'unsubscribes the user' do
+ expect(merge_request.subscribed?(user)).to be_falsey
+ end
- expect(response.status).to be 302
+ it 'sets the flash message' do
+ expect(controller).to set_flash[:notice].to(/unsubscribed/).now
+ end
- expect(response).to redirect_to new_user_session_path
- expect(controller).to set_flash[:notice].to(/unsubscribed/).now
+ it 'redirects to the merge request page' do
+ expect(response).
+ to redirect_to(namespace_project_merge_request_path(project.namespace, project, merge_request))
+ end
end
end
end
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index 2a89159c070..41d263a46a4 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
describe SnippetsController do
- describe 'GET #show' do
- let(:user) { create(:user) }
+ let(:user) { create(:user) }
+ describe 'GET #show' do
context 'when the personal snippet is private' do
let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
@@ -230,4 +230,33 @@ describe SnippetsController do
end
end
end
+
+ context 'award emoji on snippets' do
+ let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
+ let(:another_user) { create(:user) }
+
+ before do
+ sign_in(another_user)
+ end
+
+ describe 'POST #toggle_award_emoji' do
+ it "toggles the award emoji" do
+ expect do
+ post(:toggle_award_emoji, id: personal_snippet.to_param, name: "thumbsup")
+ end.to change { personal_snippet.award_emoji.count }.from(0).to(1)
+
+ expect(response.status).to eq(200)
+ end
+
+ it "removes the already awarded emoji" do
+ post(:toggle_award_emoji, id: personal_snippet.to_param, name: "thumbsup")
+
+ expect do
+ post(:toggle_award_emoji, id: personal_snippet.to_param, name: "thumbsup")
+ end.to change { personal_snippet.award_emoji.count }.from(1).to(0)
+
+ expect(response.status).to eq(200)
+ end
+ end
+ end
end
diff --git a/spec/factories/ci/runner_projects.rb b/spec/factories/ci/runner_projects.rb
index 83fccad679f..3372e5ab685 100644
--- a/spec/factories/ci/runner_projects.rb
+++ b/spec/factories/ci/runner_projects.rb
@@ -1,14 +1,3 @@
-# == Schema Information
-#
-# Table name: runner_projects
-#
-# id :integer not null, primary key
-# runner_id :integer not null
-# project_id :integer not null
-# created_at :datetime
-# updated_at :datetime
-#
-
FactoryGirl.define do
factory :ci_runner_project, class: Ci::RunnerProject do
runner_id 1
diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb
index 45eaebb2576..e3b73e29987 100644
--- a/spec/factories/ci/runners.rb
+++ b/spec/factories/ci/runners.rb
@@ -1,22 +1,3 @@
-# == Schema Information
-#
-# Table name: runners
-#
-# id :integer not null, primary key
-# token :string(255)
-# created_at :datetime
-# updated_at :datetime
-# description :string(255)
-# contacted_at :datetime
-# active :boolean default(TRUE), not null
-# is_shared :boolean default(FALSE)
-# name :string(255)
-# version :string(255)
-# revision :string(255)
-# platform :string(255)
-# architecture :string(255)
-#
-
FactoryGirl.define do
factory :ci_runner, class: Ci::Runner do
sequence :description do |n|
diff --git a/spec/factories/ci/variables.rb b/spec/factories/ci/variables.rb
index 856a8e725eb..6653f0bb5c3 100644
--- a/spec/factories/ci/variables.rb
+++ b/spec/factories/ci/variables.rb
@@ -1,17 +1,3 @@
-# == Schema Information
-#
-# Table name: ci_variables
-#
-# id :integer not null, primary key
-# project_id :integer not null
-# key :string(255)
-# value :text
-# encrypted_value :text
-# encrypted_value_salt :string(255)
-# encrypted_value_iv :string(255)
-# gl_project_id :integer
-#
-
FactoryGirl.define do
factory :ci_variable, class: Ci::Variable do
sequence(:key) { |n| "VARIABLE_#{n}" }
diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb
index 82591604fcb..6f24bf58d14 100644
--- a/spec/factories/deployments.rb
+++ b/spec/factories/deployments.rb
@@ -3,11 +3,12 @@ FactoryGirl.define do
sha '97de212e80737a608d939f648d959671fb0a0142'
ref 'master'
tag false
+ project nil
environment factory: :environment
after(:build) do |deployment, evaluator|
- deployment.project = deployment.environment.project
+ deployment.project ||= deployment.environment.project
end
end
end
diff --git a/spec/factories/events.rb b/spec/factories/events.rb
index 90788f30ac9..8820d527c61 100644
--- a/spec/factories/events.rb
+++ b/spec/factories/events.rb
@@ -1,10 +1,11 @@
FactoryGirl.define do
factory :event do
+ project
+ author factory: :user
+
factory :closed_issue_event do
- project
action { Event::CLOSED }
target factory: :closed_issue
- author factory: :user
end
end
end
diff --git a/spec/factories/group_members.rb b/spec/factories/group_members.rb
index debb86d997f..795df5dfda9 100644
--- a/spec/factories/group_members.rb
+++ b/spec/factories/group_members.rb
@@ -1,20 +1,13 @@
-# == Schema Information
-#
-# Table name: group_members
-#
-# id :integer not null, primary key
-# group_access :integer not null
-# group_id :integer not null
-# user_id :integer not null
-# created_at :datetime
-# updated_at :datetime
-# notification_level :integer default(3), not null
-#
-
FactoryGirl.define do
factory :group_member do
access_level { GroupMember::OWNER }
group
user
+
+ trait(:guest) { access_level GroupMember::GUEST }
+ trait(:reporter) { access_level GroupMember::REPORTER }
+ trait(:developer) { access_level GroupMember::DEVELOPER }
+ trait(:master) { access_level GroupMember::MASTER }
+ trait(:owner) { access_level GroupMember::OWNER }
end
end
diff --git a/spec/factories/milestones.rb b/spec/factories/milestones.rb
index e9e85962fe4..84da71ed6dc 100644
--- a/spec/factories/milestones.rb
+++ b/spec/factories/milestones.rb
@@ -3,10 +3,15 @@ FactoryGirl.define do
title
project
+ trait :active do
+ state "active"
+ end
+
trait :closed do
- state :closed
+ state "closed"
end
+ factory :active_milestone, traits: [:active]
factory :closed_milestone, traits: [:closed]
end
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index c6c2e2095df..19941978c5f 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -2,6 +2,7 @@ require 'rails_helper'
describe 'Issue Boards', feature: true, js: true do
include WaitForAjax
+ include WaitForVueResource
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
@@ -93,15 +94,8 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'shows issues in lists' do
- page.within(find('.board:nth-child(2)')) do
- expect(page.find('.board-header')).to have_content('2')
- expect(page).to have_selector('.card', count: 2)
- end
-
- page.within(find('.board:nth-child(3)')) do
- expect(page.find('.board-header')).to have_content('2')
- expect(page).to have_selector('.card', count: 2)
- end
+ wait_for_board_cards(2, 2)
+ wait_for_board_cards(3, 2)
end
it 'shows confidential issues with icon' do
@@ -187,13 +181,13 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_content('Showing 20 of 56 issues')
evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
- wait_for_vue_resource(spinner: false)
+ wait_for_vue_resource
expect(page).to have_selector('.card', count: 40)
expect(page).to have_content('Showing 40 of 56 issues')
evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
- wait_for_vue_resource(spinner: false)
+ wait_for_vue_resource
expect(page).to have_selector('.card', count: 56)
expect(page).to have_content('Showing all issues')
@@ -202,37 +196,33 @@ describe 'Issue Boards', feature: true, js: true do
context 'backlog' do
it 'shows issues in backlog with no labels' do
- page.within(find('.board', match: :first)) do
- expect(page.find('.board-header')).to have_content('6')
- expect(page).to have_selector('.card', count: 6)
- end
+ wait_for_board_cards(1, 6)
end
it 'moves issue from backlog into list' do
drag_to(list_to_index: 1)
- page.within(find('.board', match: :first)) do
- expect(page.find('.board-header')).to have_content('5')
- expect(page).to have_selector('.card', count: 5)
- end
-
wait_for_vue_resource
-
- page.within(find('.board:nth-child(2)')) do
- expect(page.find('.board-header')).to have_content('3')
- expect(page).to have_selector('.card', count: 3)
- end
+ wait_for_board_cards(1, 5)
+ wait_for_board_cards(2, 3)
end
end
context 'done' do
it 'shows list of done issues' do
- expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1)
+ wait_for_board_cards(4, 1)
+ wait_for_ajax
end
it 'moves issue to done' do
drag_to(list_from_index: 0, list_to_index: 3)
+ wait_for_board_cards(1, 5)
+ wait_for_board_cards(2, 2)
+ wait_for_board_cards(3, 2)
+ wait_for_board_cards(4, 2)
+
+ expect(find('.board:nth-child(1)')).not_to have_content(issue9.title)
expect(find('.board:nth-child(4)')).to have_selector('.card', count: 2)
expect(find('.board:nth-child(4)')).to have_content(issue9.title)
expect(find('.board:nth-child(4)')).not_to have_content(planning.title)
@@ -241,8 +231,12 @@ describe 'Issue Boards', feature: true, js: true do
it 'removes all of the same issue to done' do
drag_to(list_from_index: 1, list_to_index: 3)
- expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1)
- expect(find('.board:nth-child(3)')).to have_selector('.card', count: 1)
+ wait_for_board_cards(1, 6)
+ wait_for_board_cards(2, 1)
+ wait_for_board_cards(3, 1)
+ wait_for_board_cards(4, 2)
+
+ expect(find('.board:nth-child(2)')).not_to have_content(issue6.title)
expect(find('.board:nth-child(4)')).to have_content(issue6.title)
expect(find('.board:nth-child(4)')).not_to have_content(planning.title)
end
@@ -252,6 +246,11 @@ describe 'Issue Boards', feature: true, js: true do
it 'changes position of list' do
drag_to(list_from_index: 1, list_to_index: 2, selector: '.board-header')
+ wait_for_board_cards(1, 6)
+ wait_for_board_cards(2, 2)
+ wait_for_board_cards(3, 2)
+ wait_for_board_cards(4, 1)
+
expect(find('.board:nth-child(2)')).to have_content(development.title)
expect(find('.board:nth-child(2)')).to have_content(planning.title)
end
@@ -259,8 +258,11 @@ describe 'Issue Boards', feature: true, js: true do
it 'issue moves between lists' do
drag_to(list_from_index: 1, card_index: 1, list_to_index: 2)
- expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1)
- expect(find('.board:nth-child(3)')).to have_selector('.card', count: 3)
+ wait_for_board_cards(1, 6)
+ wait_for_board_cards(2, 1)
+ wait_for_board_cards(3, 3)
+ wait_for_board_cards(4, 1)
+
expect(find('.board:nth-child(3)')).to have_content(issue6.title)
expect(find('.board:nth-child(3)').all('.card').last).not_to have_content(development.title)
end
@@ -268,8 +270,11 @@ describe 'Issue Boards', feature: true, js: true do
it 'issue moves between lists' do
drag_to(list_from_index: 2, list_to_index: 1)
- expect(find('.board:nth-child(2)')).to have_selector('.card', count: 3)
- expect(find('.board:nth-child(3)')).to have_selector('.card', count: 1)
+ wait_for_board_cards(1, 6)
+ wait_for_board_cards(2, 3)
+ wait_for_board_cards(3, 1)
+ wait_for_board_cards(4, 1)
+
expect(find('.board:nth-child(2)')).to have_content(issue7.title)
expect(find('.board:nth-child(2)').all('.card').first).not_to have_content(planning.title)
end
@@ -277,8 +282,12 @@ describe 'Issue Boards', feature: true, js: true do
it 'issue moves from done' do
drag_to(list_from_index: 3, list_to_index: 1)
- expect(find('.board:nth-child(2)')).to have_selector('.card', count: 3)
expect(find('.board:nth-child(2)')).to have_content(issue8.title)
+
+ wait_for_board_cards(1, 6)
+ wait_for_board_cards(2, 3)
+ wait_for_board_cards(3, 2)
+ wait_for_board_cards(4, 0)
end
context 'issue card' do
@@ -341,10 +350,7 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'moves issues from backlog into new list' do
- page.within(find('.board', match: :first)) do
- expect(page.find('.board-header')).to have_content('6')
- expect(page).to have_selector('.card', count: 6)
- end
+ wait_for_board_cards(1, 6)
click_button 'Create new list'
wait_for_ajax
@@ -355,10 +361,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
- page.within(find('.board', match: :first)) do
- expect(page.find('.board-header')).to have_content('5')
- expect(page).to have_selector('.card', count: 5)
- end
+ wait_for_board_cards(1, 5)
end
end
end
@@ -372,22 +375,14 @@ describe 'Issue Boards', feature: true, js: true do
page.within '.dropdown-menu-author' do
click_link(user2.name)
end
- wait_for_vue_resource(spinner: false)
+ wait_for_vue_resource
expect(find('.js-author-search')).to have_content(user2.name)
end
wait_for_vue_resource
-
- page.within(find('.board', match: :first)) do
- expect(page.find('.board-header')).to have_content('1')
- expect(page).to have_selector('.card', count: 1)
- end
-
- page.within(find('.board:nth-child(2)')) do
- expect(page.find('.board-header')).to have_content('0')
- expect(page).to have_selector('.card', count: 0)
- end
+ wait_for_board_cards(1, 1)
+ wait_for_empty_boards((2..4))
end
it 'filters by assignee' do
@@ -398,22 +393,15 @@ describe 'Issue Boards', feature: true, js: true do
page.within '.dropdown-menu-assignee' do
click_link(user.name)
end
- wait_for_vue_resource(spinner: false)
+ wait_for_vue_resource
expect(find('.js-assignee-search')).to have_content(user.name)
end
wait_for_vue_resource
- page.within(find('.board', match: :first)) do
- expect(page.find('.board-header')).to have_content('1')
- expect(page).to have_selector('.card', count: 1)
- end
-
- page.within(find('.board:nth-child(2)')) do
- expect(page.find('.board-header')).to have_content('0')
- expect(page).to have_selector('.card', count: 0)
- end
+ wait_for_board_cards(1, 1)
+ wait_for_empty_boards((2..4))
end
it 'filters by milestone' do
@@ -424,22 +412,16 @@ describe 'Issue Boards', feature: true, js: true do
page.within '.milestone-filter' do
click_link(milestone.title)
end
- wait_for_vue_resource(spinner: false)
+ wait_for_vue_resource
expect(find('.js-milestone-select')).to have_content(milestone.title)
end
wait_for_vue_resource
-
- page.within(find('.board', match: :first)) do
- expect(page.find('.board-header')).to have_content('0')
- expect(page).to have_selector('.card', count: 0)
- end
-
- page.within(find('.board:nth-child(2)')) do
- expect(page.find('.board-header')).to have_content('1')
- expect(page).to have_selector('.card', count: 1)
- end
+ wait_for_board_cards(1, 0)
+ wait_for_board_cards(2, 1)
+ wait_for_board_cards(3, 0)
+ wait_for_board_cards(4, 0)
end
it 'filters by label' do
@@ -449,22 +431,14 @@ describe 'Issue Boards', feature: true, js: true do
page.within '.dropdown-menu-labels' do
click_link(testing.title)
- wait_for_vue_resource(spinner: false)
+ wait_for_vue_resource
find('.dropdown-menu-close').click
end
end
wait_for_vue_resource
-
- page.within(find('.board', match: :first)) do
- expect(page.find('.board-header')).to have_content('1')
- expect(page).to have_selector('.card', count: 1)
- end
-
- page.within(find('.board:nth-child(2)')) do
- expect(page.find('.board-header')).to have_content('0')
- expect(page).to have_selector('.card', count: 0)
- end
+ wait_for_board_cards(1, 1)
+ wait_for_empty_boards((2..4))
end
it 'infinite scrolls list with label filter' do
@@ -478,7 +452,7 @@ describe 'Issue Boards', feature: true, js: true do
page.within '.dropdown-menu-labels' do
click_link(testing.title)
- wait_for_vue_resource(spinner: false)
+ wait_for_vue_resource
find('.dropdown-menu-close').click
end
end
@@ -509,24 +483,17 @@ describe 'Issue Boards', feature: true, js: true do
page.within(find('.dropdown-menu-labels')) do
click_link(testing.title)
- wait_for_vue_resource(spinner: false)
+ wait_for_vue_resource
click_link(bug.title)
- wait_for_vue_resource(spinner: false)
+ wait_for_vue_resource
find('.dropdown-menu-close').click
end
end
wait_for_vue_resource
- page.within(find('.board', match: :first)) do
- expect(page.find('.board-header')).to have_content('1')
- expect(page).to have_selector('.card', count: 1)
- end
-
- page.within(find('.board:nth-child(2)')) do
- expect(page.find('.board-header')).to have_content('0')
- expect(page).to have_selector('.card', count: 0)
- end
+ wait_for_board_cards(1, 1)
+ wait_for_empty_boards((2..4))
end
it 'filters by no label' do
@@ -536,22 +503,17 @@ describe 'Issue Boards', feature: true, js: true do
page.within '.dropdown-menu-labels' do
click_link("No Label")
- wait_for_vue_resource(spinner: false)
+ wait_for_vue_resource
find('.dropdown-menu-close').click
end
end
wait_for_vue_resource
- page.within(find('.board', match: :first)) do
- expect(page.find('.board-header')).to have_content('5')
- expect(page).to have_selector('.card', count: 5)
- end
-
- page.within(find('.board:nth-child(2)')) do
- expect(page.find('.board-header')).to have_content('0')
- expect(page).to have_selector('.card', count: 0)
- end
+ wait_for_board_cards(1, 5)
+ wait_for_board_cards(2, 0)
+ wait_for_board_cards(3, 0)
+ wait_for_board_cards(4, 1)
end
it 'filters by clicking label button on issue' do
@@ -559,20 +521,13 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_selector('.card', count: 6)
expect(find('.card', match: :first)).to have_content(bug.title)
click_button(bug.title)
- wait_for_vue_resource(spinner: false)
+ wait_for_vue_resource
end
wait_for_vue_resource
- page.within(find('.board', match: :first)) do
- expect(page.find('.board-header')).to have_content('1')
- expect(page).to have_selector('.card', count: 1)
- end
-
- page.within(find('.board:nth-child(2)')) do
- expect(page.find('.board-header')).to have_content('0')
- expect(page).to have_selector('.card', count: 0)
- end
+ wait_for_board_cards(1, 1)
+ wait_for_empty_boards((2..4))
page.within('.labels-filter') do
expect(find('.dropdown-toggle-text')).to have_content(bug.title)
@@ -584,7 +539,7 @@ describe 'Issue Boards', feature: true, js: true do
page.within(find('.card', match: :first)) do
click_button(bug.title)
end
- wait_for_vue_resource(spinner: false)
+ wait_for_vue_resource
expect(page).to have_selector('.card', count: 1)
end
@@ -648,13 +603,16 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
end
- def wait_for_vue_resource(spinner: true)
- Timeout.timeout(Capybara.default_max_wait_time) do
- loop until page.evaluate_script('Vue.activeResources').zero?
+ def wait_for_board_cards(board_number, expected_cards)
+ page.within(find(".board:nth-child(#{board_number})")) do
+ expect(page.find('.board-header')).to have_content(expected_cards.to_s)
+ expect(page).to have_selector('.card', count: expected_cards)
end
+ end
- if spinner
- expect(find('.boards-list')).not_to have_selector('.fa-spinner')
+ def wait_for_empty_boards(board_numbers)
+ board_numbers.each do |board|
+ wait_for_board_cards(board, 0)
end
end
end
diff --git a/spec/features/boards/keyboard_shortcut_spec.rb b/spec/features/boards/keyboard_shortcut_spec.rb
new file mode 100644
index 00000000000..7ef68e9eb8d
--- /dev/null
+++ b/spec/features/boards/keyboard_shortcut_spec.rb
@@ -0,0 +1,24 @@
+require 'rails_helper'
+
+describe 'Issue Boards shortcut', feature: true, js: true do
+ include WaitForVueResource
+
+ let(:project) { create(:empty_project) }
+
+ before do
+ project.create_board
+ project.board.lists.create(list_type: :backlog)
+ project.board.lists.create(list_type: :done)
+
+ login_as :admin
+
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ it 'takes user to issue board index' do
+ find('body').native.send_keys('gl')
+ expect(page).to have_selector('.boards-list')
+
+ wait_for_vue_resource
+ end
+end
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
new file mode 100644
index 00000000000..fd5fbaf2af4
--- /dev/null
+++ b/spec/features/calendar_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+feature 'Contributions Calendar', js: true, feature: true do
+ include WaitForAjax
+
+ let(:contributed_project) { create(:project, :public) }
+
+ before do
+ login_as :user
+
+ issue_params = { title: 'Bug in old browser' }
+ Issues::CreateService.new(contributed_project, @user, issue_params).execute
+
+ # Push code contribution
+ push_params = {
+ project: contributed_project,
+ action: Event::PUSHED,
+ author_id: @user.id,
+ data: { commit_count: 3 }
+ }
+
+ Event.create(push_params)
+
+ visit @user.username
+ wait_for_ajax
+ end
+
+ it 'displays calendar', js: true do
+ expect(page).to have_css('.js-contrib-calendar')
+ end
+
+ it 'displays calendar activity log', js: true do
+ expect(find('.content_list .event-note')).to have_content "Bug in old browser"
+ end
+
+ it 'displays calendar activity square color', js: true do
+ expect(page).to have_selector('.user-contrib-cell[fill=\'#acd5f2\']', count: 1)
+ end
+end
diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb
index fcd41b38413..4309a726917 100644
--- a/spec/features/environments_spec.rb
+++ b/spec/features/environments_spec.rb
@@ -150,7 +150,7 @@ feature 'Environments', feature: true do
context 'for invalid name' do
before do
- fill_in('Name', with: 'name with spaces')
+ fill_in('Name', with: 'name,with,commas')
click_on 'Save'
end
diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb
index 0e9f814044e..d1501c9791a 100644
--- a/spec/features/issues/filter_issues_spec.rb
+++ b/spec/features/issues/filter_issues_spec.rb
@@ -101,7 +101,7 @@ describe 'Filter issues', feature: true do
expect(find('.js-label-select .dropdown-toggle-text')).to have_content('No Label')
end
- it 'filters by no label' do
+ it 'filters by a label' do
find('.dropdown-menu-labels a', text: label.title).click
page.within '.labels-filter' do
expect(page).to have_content label.title
@@ -109,7 +109,7 @@ describe 'Filter issues', feature: true do
expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
end
- it 'filters by wont fix labels' do
+ it "filters by `won't fix` and another label" do
find('.dropdown-menu-labels a', text: label.title).click
page.within '.labels-filter' do
expect(page).to have_content wontfix.title
@@ -117,6 +117,33 @@ describe 'Filter issues', feature: true do
end
expect(find('.js-label-select .dropdown-toggle-text')).to have_content(wontfix.title)
end
+
+ it "filters by `won't fix` label followed by another label after page load" do
+ find('.dropdown-menu-labels a', text: wontfix.title).click
+ # Close label dropdown to load
+ find('body').click
+ expect(find('.filtered-labels')).to have_content(wontfix.title)
+
+ find('.js-label-select').click
+ wait_for_ajax
+ find('.dropdown-menu-labels a', text: label.title).click
+ # Close label dropdown to load
+ find('body').click
+ expect(find('.filtered-labels')).to have_content(label.title)
+
+ find('.js-label-select').click
+ wait_for_ajax
+ expect(find('.dropdown-menu-labels li', text: wontfix.title)).to have_css('.is-active')
+ expect(find('.dropdown-menu-labels li', text: label.title)).to have_css('.is-active')
+ end
+
+ it "selects and unselects `won't fix`" do
+ find('.dropdown-menu-labels a', text: wontfix.title).click
+ find('.dropdown-menu-labels a', text: wontfix.title).click
+ # Close label dropdown to load
+ find('body').click
+ expect(page).not_to have_css('.filtered-labels')
+ end
end
describe 'Filter issues for assignee and label from issues#index' do
@@ -179,7 +206,7 @@ describe 'Filter issues', feature: true do
context 'only text', js: true do
it 'filters issues by searched text' do
- fill_in 'issue_search', with: 'Bug'
+ fill_in 'issuable_search', with: 'Bug'
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 2)
@@ -187,7 +214,7 @@ describe 'Filter issues', feature: true do
end
it 'does not show any issues' do
- fill_in 'issue_search', with: 'testing'
+ fill_in 'issuable_search', with: 'testing'
page.within '.issues-list' do
expect(page).not_to have_selector('.issue')
@@ -197,7 +224,7 @@ describe 'Filter issues', feature: true do
context 'text and dropdown options', js: true do
it 'filters by text and label' do
- fill_in 'issue_search', with: 'Bug'
+ fill_in 'issuable_search', with: 'Bug'
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 2)
@@ -215,7 +242,7 @@ describe 'Filter issues', feature: true do
end
it 'filters by text and milestone' do
- fill_in 'issue_search', with: 'Bug'
+ fill_in 'issuable_search', with: 'Bug'
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 2)
@@ -232,7 +259,7 @@ describe 'Filter issues', feature: true do
end
it 'filters by text and assignee' do
- fill_in 'issue_search', with: 'Bug'
+ fill_in 'issuable_search', with: 'Bug'
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 2)
@@ -249,7 +276,7 @@ describe 'Filter issues', feature: true do
end
it 'filters by text and author' do
- fill_in 'issue_search', with: 'Bug'
+ fill_in 'issuable_search', with: 'Bug'
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 2)
diff --git a/spec/features/issues/reset_filters_spec.rb b/spec/features/issues/reset_filters_spec.rb
index 41f218eaa8b..f4d0f13c3d5 100644
--- a/spec/features/issues/reset_filters_spec.rb
+++ b/spec/features/issues/reset_filters_spec.rb
@@ -37,7 +37,7 @@ feature 'Issues filter reset button', feature: true, js: true do
context 'when a text search has been conducted' do
it 'resets the text search filter' do
- visit_issues(project, issue_search: 'Bug')
+ visit_issues(project, search: 'Bug')
expect(page).to have_css('.issue', count: 1)
reset_filters
@@ -67,7 +67,7 @@ feature 'Issues filter reset button', feature: true, js: true do
context 'when all filters have been applied' do
it 'resets all filters' do
- visit_issues(project, assignee_id: user.id, author_id: user.id, milestone_title: milestone.title, label_name: bug.name, issue_search: 'Bug')
+ visit_issues(project, assignee_id: user.id, author_id: user.id, milestone_title: milestone.title, label_name: bug.name, search: 'Bug')
expect(page).to have_css('.issue', count: 0)
reset_filters
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index d744d0eb6af..22359c8f938 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -144,7 +144,7 @@ describe 'Issues', feature: true do
visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id)
expect(page).to have_content 'foobar'
- expect(page.all('.issue-no-comments').first.text).to eq "0"
+ expect(page.all('.no-comments').first.text).to eq "0"
end
end
diff --git a/spec/features/merge_requests/merge_request_versions_spec.rb b/spec/features/merge_requests/merge_request_versions_spec.rb
index 9e759de3752..22d9e42119d 100644
--- a/spec/features/merge_requests/merge_request_versions_spec.rb
+++ b/spec/features/merge_requests/merge_request_versions_spec.rb
@@ -21,7 +21,7 @@ feature 'Merge Request versions', js: true, feature: true do
describe 'switch between versions' do
before do
page.within '.mr-version-dropdown' do
- find('.btn-link').click
+ find('.btn-default').click
click_link 'version 1'
end
end
@@ -42,12 +42,12 @@ feature 'Merge Request versions', js: true, feature: true do
describe 'compare with older version' do
before do
page.within '.mr-version-compare-dropdown' do
- find('.btn-link').click
+ find('.btn-default').click
click_link 'version 1'
end
end
- it 'should has correct value in the compare dropdown' do
+ it 'should have correct value in the compare dropdown' do
page.within '.mr-version-compare-dropdown' do
expect(page).to have_content 'version 1'
end
@@ -64,5 +64,13 @@ feature 'Merge Request versions', js: true, feature: true do
it 'show diff between new and old version' do
expect(page).to have_content '4 changed files with 15 additions and 6 deletions'
end
+
+ it 'should return to latest version when "Show latest version" button is clicked' do
+ click_link 'Show latest version'
+ page.within '.mr-version-dropdown' do
+ expect(page).to have_content 'latest version'
+ end
+ expect(page).to have_content '8 changed files'
+ end
end
end
diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb
index c43661e5681..b8c838bf7ab 100644
--- a/spec/features/milestone_spec.rb
+++ b/spec/features/milestone_spec.rb
@@ -3,9 +3,8 @@ require 'rails_helper'
feature 'Milestone', feature: true do
include WaitForAjax
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
- let(:milestone) { create(:milestone, project: project, title: 8.7) }
before do
project.team << [user, :master]
@@ -13,7 +12,7 @@ feature 'Milestone', feature: true do
end
feature 'Create a milestone' do
- scenario 'shows an informative message for a new issue' do
+ scenario 'shows an informative message for a new milestone' do
visit new_namespace_project_milestone_path(project.namespace, project)
page.within '.milestone-form' do
fill_in "milestone_title", with: '8.7'
@@ -26,10 +25,26 @@ feature 'Milestone', feature: true do
feature 'Open a milestone with closed issues' do
scenario 'shows an informative message' do
+ milestone = create(:milestone, project: project, title: 8.7)
+
create(:issue, title: "Bugfix1", project: project, milestone: milestone, state: "closed")
visit namespace_project_milestone_path(project.namespace, project, milestone)
expect(find('.alert-success')).to have_content('All issues for this milestone are closed. You may close this milestone now.')
end
end
+
+ feature 'Open a milestone with an existing title' do
+ scenario 'displays validation message' do
+ milestone = create(:milestone, project: project, title: 8.7)
+
+ visit new_namespace_project_milestone_path(project.namespace, project)
+ page.within '.milestone-form' do
+ fill_in "milestone_title", with: milestone.title
+ end
+ find('input[name="commit"]').click
+
+ expect(find('.alert-danger')).to have_content('Title has already been taken')
+ end
+ end
end
diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb
index 04058300570..92028c19361 100644
--- a/spec/features/projects/branches/download_buttons_spec.rb
+++ b/spec/features/projects/branches/download_buttons_spec.rb
@@ -33,7 +33,11 @@ feature 'Download buttons in branches page', feature: true do
end
scenario 'shows download artifacts button' do
- expect(page).to have_link "Download '#{build.name}'"
+ href = latest_succeeded_namespace_project_artifacts_path(
+ project.namespace, project, 'binary-encoding/download',
+ job: 'build')
+
+ expect(page).to have_link "Download '#{build.name}'", href: href
end
end
end
diff --git a/spec/features/projects/files/download_buttons_spec.rb b/spec/features/projects/files/download_buttons_spec.rb
index be5cebcd7c9..d7c29a7e074 100644
--- a/spec/features/projects/files/download_buttons_spec.rb
+++ b/spec/features/projects/files/download_buttons_spec.rb
@@ -34,7 +34,11 @@ feature 'Download buttons in files tree', feature: true do
end
scenario 'shows download artifacts button' do
- expect(page).to have_link "Download '#{build.name}'"
+ href = latest_succeeded_namespace_project_artifacts_path(
+ project.namespace, project, "#{project.default_branch}/download",
+ job: 'build')
+
+ expect(page).to have_link "Download '#{build.name}'", href: href
end
end
end
diff --git a/spec/features/projects/gfm_autocomplete_load_spec.rb b/spec/features/projects/gfm_autocomplete_load_spec.rb
new file mode 100644
index 00000000000..1921ea6d8ae
--- /dev/null
+++ b/spec/features/projects/gfm_autocomplete_load_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe 'GFM autocomplete loading', feature: true, js: true do
+ let(:project) { create(:project) }
+
+ before do
+ login_as :admin
+
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ it 'does not load on project#show' do
+ expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).to eq('')
+ end
+
+ it 'loads on new issue page' do
+ visit new_namespace_project_issue_path(project.namespace, project)
+
+ expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).not_to eq('')
+ end
+end
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
new file mode 100644
index 00000000000..27c986c5187
--- /dev/null
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -0,0 +1,80 @@
+require 'spec_helper'
+
+# Integration test that exports a file using the Import/Export feature
+# It looks up for any sensitive word inside the JSON, so if a sensitive word is found
+# we''l have to either include it adding the model that includes it to the +safe_list+
+# or make sure the attribute is blacklisted in the +import_export.yml+ configuration
+feature 'Import/Export - project export integration test', feature: true, js: true do
+ include Select2Helper
+ include ExportFileHelper
+
+ let(:user) { create(:admin) }
+ let(:export_path) { "#{Dir::tmpdir}/import_file_spec" }
+ let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
+
+ let(:sensitive_words) { %w[pass secret token key] }
+ let(:safe_list) do
+ {
+ token: [ProjectHook, Ci::Trigger, CommitStatus],
+ key: [Project, Ci::Variable, :yaml_variables]
+ }
+ end
+ let(:safe_hashes) { { yaml_variables: %w[key value public] } }
+
+ let(:project) { setup_project }
+
+ background do
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ end
+
+ after do
+ FileUtils.rm_rf(export_path, secure: true)
+ end
+
+ context 'admin user' do
+ before do
+ login_as(user)
+ end
+
+ scenario 'exports a project successfully' do
+ visit edit_namespace_project_path(project.namespace, project)
+
+ expect(page).to have_content('Export project')
+
+ click_link 'Export project'
+
+ visit edit_namespace_project_path(project.namespace, project)
+
+ expect(page).to have_content('Download export')
+
+ in_directory_with_expanded_export(project) do |exit_status, tmpdir|
+ expect(exit_status).to eq(0)
+
+ project_json_path = File.join(tmpdir, 'project.json')
+ expect(File).to exist(project_json_path)
+
+ project_hash = JSON.parse(IO.read(project_json_path))
+
+ sensitive_words.each do |sensitive_word|
+ found = find_sensitive_attributes(sensitive_word, project_hash)
+
+ expect(found).to be_nil, failure_message(found.try(:key_found), found.try(:parent), sensitive_word)
+ end
+ end
+ end
+
+ def failure_message(key_found, parent, sensitive_word)
+ <<-MSG
+ Found a new sensitive word <#{key_found}>, which is part of the hash #{parent.inspect}
+
+ If you think this information shouldn't get exported, please exclude the model or attribute in IMPORT_EXPORT_CONFIG.
+
+ Otherwise, please add the exception to +safe_list+ in CURRENT_SPEC using #{sensitive_word} as the key and the
+ correspondent hash or model as the value.
+
+ IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file}
+ CURRENT_SPEC: #{__FILE__}
+ MSG
+ end
+ end
+end
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index f707ccf4e93..09cd6369881 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'project import', feature: true, js: true do
+feature 'Import/Export - project import integration test', feature: true, js: true do
include Select2Helper
let(:admin) { create(:admin) }
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index e14b2705704..d04bdea0fe4 100644
--- a/spec/features/projects/import_export/test_project_export.tar.gz
+++ b/spec/features/projects/import_export/test_project_export.tar.gz
Binary files differ
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index 4a83740621a..f76c4fe8b57 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -13,10 +13,12 @@ feature 'issuable templates', feature: true, js: true do
context 'user creates an issue using templates' do
let(:template_content) { 'this is a test "bug" template' }
+ let(:longtemplate_content) { %Q(this\n\n\n\n\nis\n\n\n\n\na\n\n\n\n\nbug\n\n\n\n\ntemplate) }
let(:issue) { create(:issue, author: user, assignee: user, project: project) }
background do
project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false)
+ project.repository.commit_file(user, '.gitlab/issue_templates/test.md', longtemplate_content, 'added issue template', 'master', false)
visit edit_namespace_project_issue_path project.namespace, project, issue
fill_in :'issue[title]', with: 'test issue title'
end
@@ -27,6 +29,17 @@ feature 'issuable templates', feature: true, js: true do
preview_template
save_changes
end
+
+ it 'updates height of markdown textarea' do
+ start_height = page.evaluate_script('$(".markdown-area").outerHeight()')
+
+ select_template 'test'
+ wait_for_ajax
+
+ end_height = page.evaluate_script('$(".markdown-area").outerHeight()')
+
+ expect(end_height).not_to eq(start_height)
+ end
end
context 'user creates a merge request using templates' do
@@ -51,7 +64,7 @@ feature 'issuable templates', feature: true, js: true do
let(:template_content) { 'this is a test "feature-proposal" template' }
let(:fork_user) { create(:user) }
let(:fork_project) { create(:project, :public) }
- let(:merge_request) { create(:merge_request, :with_diffs, source_project: fork_project) }
+ let(:merge_request) { create(:merge_request, :with_diffs, source_project: fork_project, target_project: project) }
background do
logout
@@ -59,16 +72,20 @@ feature 'issuable templates', feature: true, js: true do
fork_project.team << [fork_user, :master]
create(:forked_project_link, forked_to_project: fork_project, forked_from_project: project)
login_as fork_user
- fork_project.repository.commit_file(fork_user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false)
- visit edit_namespace_project_merge_request_path fork_project.namespace, fork_project, merge_request
+ project.repository.commit_file(fork_user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false)
+ visit edit_namespace_project_merge_request_path project.namespace, project, merge_request
fill_in :'merge_request[title]', with: 'test merge request title'
end
- scenario 'user selects "feature-proposal" template' do
- select_template 'feature-proposal'
- wait_for_ajax
- preview_template
- save_changes
+ context 'feature proposal template' do
+ context 'template exists in target project' do
+ scenario 'user selects template' do
+ select_template 'feature-proposal'
+ wait_for_ajax
+ preview_template
+ save_changes
+ end
+ end
end
end
diff --git a/spec/features/projects/main/download_buttons_spec.rb b/spec/features/projects/main/download_buttons_spec.rb
index b26c0ea7a14..227ccf9459c 100644
--- a/spec/features/projects/main/download_buttons_spec.rb
+++ b/spec/features/projects/main/download_buttons_spec.rb
@@ -33,7 +33,11 @@ feature 'Download buttons in project main page', feature: true do
end
scenario 'shows download artifacts button' do
- expect(page).to have_link "Download '#{build.name}'"
+ href = latest_succeeded_namespace_project_artifacts_path(
+ project.namespace, project, "#{project.default_branch}/download",
+ job: 'build')
+
+ expect(page).to have_link "Download '#{build.name}'", href: href
end
end
end
diff --git a/spec/features/projects/tags/download_buttons_spec.rb b/spec/features/projects/tags/download_buttons_spec.rb
index 6e0022c179f..dd93d25c2c6 100644
--- a/spec/features/projects/tags/download_buttons_spec.rb
+++ b/spec/features/projects/tags/download_buttons_spec.rb
@@ -34,7 +34,11 @@ feature 'Download buttons in tags page', feature: true do
end
scenario 'shows download artifacts button' do
- expect(page).to have_link "Download '#{build.name}'"
+ href = latest_succeeded_namespace_project_artifacts_path(
+ project.namespace, project, "#{tag}/download",
+ job: 'build')
+
+ expect(page).to have_link "Download '#{build.name}'", href: href
end
end
end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index e00d85904d5..2242cb6236a 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -57,7 +57,7 @@ feature 'Project', feature: true do
describe 'removal', js: true do
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:project, namespace: user.namespace, name: 'project1') }
before do
login_with(user)
@@ -65,8 +65,12 @@ feature 'Project', feature: true do
visit edit_namespace_project_path(project.namespace, project)
end
- it 'removes project' do
+ it 'removes a project' do
expect { remove_with_confirm('Remove project', project.path) }.to change {Project.count}.by(-1)
+ expect(page).to have_content "Project 'project1' will be deleted."
+ expect(Project.all.count).to be_zero
+ expect(project.issues).to be_empty
+ expect(project.merge_requests).to be_empty
end
end
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index dcd3a2f17b0..1806200c82c 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe "Search", feature: true do
+ include WaitForAjax
+
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
let!(:issue) { create(:issue, project: project, assignee: user) }
@@ -16,6 +18,36 @@ describe "Search", feature: true do
expect(page).not_to have_selector('.search')
end
+ context 'search filters', js: true do
+ let(:group) { create(:group) }
+
+ before do
+ group.add_owner(user)
+ end
+
+ it 'shows group name after filtering' do
+ find('.js-search-group-dropdown').click
+ wait_for_ajax
+
+ page.within '.search-holder' do
+ click_link group.name
+ end
+
+ expect(find('.js-search-group-dropdown')).to have_content(group.name)
+ end
+
+ it 'shows project name after filtering' do
+ page.within('.project-filter') do
+ find('.js-search-project-dropdown').click
+ wait_for_ajax
+
+ click_link project.name_with_namespace
+ end
+
+ expect(find('.js-search-project-dropdown')).to have_content(project.name_with_namespace)
+ end
+ end
+
describe 'searching for Projects' do
it 'finds a project' do
page.within '.search-holder' do
diff --git a/spec/features/todos/todos_filtering_spec.rb b/spec/features/todos/todos_filtering_spec.rb
index 83cf306437d..b9e66243d84 100644
--- a/spec/features/todos/todos_filtering_spec.rb
+++ b/spec/features/todos/todos_filtering_spec.rb
@@ -29,8 +29,11 @@ describe 'Dashboard > User filters todos', feature: true, js: true do
fill_in 'Search projects', with: project_1.name_with_namespace
click_link project_1.name_with_namespace
end
+
wait_for_ajax
- expect('.prepend-top-default').not_to have_content project_2.name_with_namespace
+
+ expect(page).to have_content project_1.name_with_namespace
+ expect(page).not_to have_content project_2.name_with_namespace
end
it 'filters by author' do
@@ -39,8 +42,11 @@ describe 'Dashboard > User filters todos', feature: true, js: true do
fill_in 'Search authors', with: user_1.name
click_link user_1.name
end
+
wait_for_ajax
- expect('.prepend-top-default').not_to have_content user_2.name
+
+ expect(find('.todos-list')).to have_content user_1.name
+ expect(find('.todos-list')).not_to have_content user_2.name
end
it 'filters by type' do
@@ -48,8 +54,11 @@ describe 'Dashboard > User filters todos', feature: true, js: true do
within '.dropdown-menu-type' do
click_link 'Issue'
end
+
wait_for_ajax
- expect('.prepend-top-default').not_to have_content ' merge request !'
+
+ expect(find('.todos-list')).to have_content issue.to_reference
+ expect(find('.todos-list')).not_to have_content merge_request.to_reference
end
it 'filters by action' do
@@ -57,7 +66,10 @@ describe 'Dashboard > User filters todos', feature: true, js: true do
within '.dropdown-menu-action' do
click_link 'Assigned'
end
+
wait_for_ajax
- expect('.prepend-top-default').not_to have_content ' mentioned '
+
+ expect(find('.todos-list')).to have_content ' assigned you '
+ expect(find('.todos-list')).not_to have_content ' mentioned '
end
end
diff --git a/spec/features/unsubscribe_links_spec.rb b/spec/features/unsubscribe_links_spec.rb
new file mode 100644
index 00000000000..cc40671787c
--- /dev/null
+++ b/spec/features/unsubscribe_links_spec.rb
@@ -0,0 +1,75 @@
+require 'spec_helper'
+
+describe 'Unsubscribe links', feature: true do
+ include Warden::Test::Helpers
+
+ let(:recipient) { create(:user) }
+ let(:author) { create(:user) }
+ let(:project) { create(:empty_project, :public) }
+ let(:params) { { title: 'A bug!', description: 'Fix it!', assignee: recipient } }
+ let(:issue) { Issues::CreateService.new(project, author, params).execute }
+
+ let(:mail) { ActionMailer::Base.deliveries.last }
+ let(:body) { Capybara::Node::Simple.new(mail.default_part_body.to_s) }
+ let(:header_link) { mail.header['List-Unsubscribe'] }
+ let(:body_link) { body.find_link('unsubscribe')['href'] }
+
+ before do
+ perform_enqueued_jobs { issue }
+ end
+
+ context 'when logged out' do
+ context 'when visiting the link from the body' do
+ it 'shows the unsubscribe confirmation page and redirects to root path when confirming' do
+ visit body_link
+
+ expect(current_path).to eq unsubscribe_sent_notification_path(SentNotification.last)
+ expect(page).to have_text(%(Unsubscribe from issue #{issue.title} (#{issue.to_reference})))
+ expect(page).to have_text(%(Are you sure you want to unsubscribe from issue #{issue.title} (#{issue.to_reference})?))
+ expect(issue.subscribed?(recipient)).to be_truthy
+
+ click_link 'Unsubscribe'
+
+ expect(issue.subscribed?(recipient)).to be_falsey
+ expect(current_path).to eq new_user_session_path
+ end
+
+ it 'shows the unsubscribe confirmation page and redirects to root path when canceling' do
+ visit body_link
+
+ expect(current_path).to eq unsubscribe_sent_notification_path(SentNotification.last)
+ expect(issue.subscribed?(recipient)).to be_truthy
+
+ click_link 'Cancel'
+
+ expect(issue.subscribed?(recipient)).to be_truthy
+ expect(current_path).to eq new_user_session_path
+ end
+ end
+
+ it 'unsubscribes from the issue when visiting the link from the header' do
+ visit header_link
+
+ expect(page).to have_text('unsubscribed')
+ expect(issue.subscribed?(recipient)).to be_falsey
+ end
+ end
+
+ context 'when logged in' do
+ before { login_as(recipient) }
+
+ it 'unsubscribes from the issue when visiting the link from the email body' do
+ visit body_link
+
+ expect(page).to have_text('unsubscribed')
+ expect(issue.subscribed?(recipient)).to be_falsey
+ end
+
+ it 'unsubscribes from the issue when visiting the link from the header' do
+ visit header_link
+
+ expect(page).to have_text('unsubscribed')
+ expect(issue.subscribed?(recipient)).to be_falsey
+ end
+ end
+end
diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb
index 356a8d668b0..f00abd82fea 100644
--- a/spec/features/users/snippets_spec.rb
+++ b/spec/features/users/snippets_spec.rb
@@ -19,7 +19,10 @@ describe 'Snippets tab on a user profile', feature: true, js: true do
end
context 'clicking on the link to the second page' do
- before { click_link('2') }
+ before do
+ click_link('2')
+ wait_for_ajax
+ end
it 'shows the remaining snippets' do
expect(page.all('.snippets-list-holder .snippet-row').count).to eq(5)
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index ec8809e6926..40bccb8e50b 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -7,8 +7,8 @@ describe IssuesFinder do
let(:project2) { create(:empty_project) }
let(:milestone) { create(:milestone, project: project1) }
let(:label) { create(:label, project: project2) }
- let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone) }
- let(:issue2) { create(:issue, author: user, assignee: user, project: project2) }
+ let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone, title: 'gitlab') }
+ let(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'gitlab') }
let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2) }
let!(:label_link) { create(:label_link, label: label, target: issue2) }
@@ -127,6 +127,22 @@ describe IssuesFinder do
end
end
+ context 'filtering by issue term' do
+ let(:params) { { search: 'git' } }
+
+ it 'returns issues with title and description match for search term' do
+ expect(issues).to contain_exactly(issue1, issue2)
+ end
+ end
+
+ context 'filtering by issue iid' do
+ let(:params) { { search: issue3.to_reference } }
+
+ it 'returns issue with iid match' do
+ expect(issues).to contain_exactly(issue3)
+ end
+ end
+
context 'when the user is unauthorized' do
let(:search_user) { nil }
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 0807534720a..233d00534e5 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -18,4 +18,67 @@ describe GroupsHelper do
expect(group_icon(group.path)).to match('group_avatar.png')
end
end
+
+ describe 'group_lfs_status' do
+ let(:group) { create(:group) }
+ let!(:project) { create(:empty_project, namespace_id: group.id) }
+
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
+
+ context 'only one project in group' do
+ before do
+ group.update_attribute(:lfs_enabled, true)
+ end
+
+ it 'returns all projects as enabled' do
+ expect(group_lfs_status(group)).to include('Enabled for all projects')
+ end
+
+ it 'returns all projects as disabled' do
+ project.update_attribute(:lfs_enabled, false)
+
+ expect(group_lfs_status(group)).to include('Enabled for 0 out of 1 project')
+ end
+ end
+
+ context 'more than one project in group' do
+ before do
+ create(:empty_project, namespace_id: group.id)
+ end
+
+ context 'LFS enabled in group' do
+ before do
+ group.update_attribute(:lfs_enabled, true)
+ end
+
+ it 'returns both projects as enabled' do
+ expect(group_lfs_status(group)).to include('Enabled for all projects')
+ end
+
+ it 'returns only one as enabled' do
+ project.update_attribute(:lfs_enabled, false)
+
+ expect(group_lfs_status(group)).to include('Enabled for 1 out of 2 projects')
+ end
+ end
+
+ context 'LFS disabled in group' do
+ before do
+ group.update_attribute(:lfs_enabled, false)
+ end
+
+ it 'returns both projects as disabled' do
+ expect(group_lfs_status(group)).to include('Disabled for all projects')
+ end
+
+ it 'returns only one as disabled' do
+ project.update_attribute(:lfs_enabled, true)
+
+ expect(group_lfs_status(group)).to include('Disabled for 1 out of 2 projects')
+ end
+ end
+ end
+ end
end
diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb
new file mode 100644
index 00000000000..28c2268f8d0
--- /dev/null
+++ b/spec/helpers/milestones_helper_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe MilestonesHelper do
+ describe '#milestone_counts' do
+ let(:project) { FactoryGirl.create(:project) }
+ let(:counts) { helper.milestone_counts(project.milestones) }
+
+ context 'when there are milestones' do
+ let!(:milestone_1) { FactoryGirl.create(:active_milestone, project: project) }
+ let!(:milestone_2) { FactoryGirl.create(:active_milestone, project: project) }
+ let!(:milestone_3) { FactoryGirl.create(:closed_milestone, project: project) }
+
+ it 'returns the correct counts' do
+ expect(counts).to eq(opened: 2, closed: 1, all: 3)
+ end
+ end
+
+ context 'when there are only milestones of one type' do
+ let!(:milestone_1) { FactoryGirl.create(:active_milestone, project: project) }
+ let!(:milestone_2) { FactoryGirl.create(:active_milestone, project: project) }
+
+ it 'returns the correct counts' do
+ expect(counts).to eq(opened: 2, closed: 0, all: 2)
+ end
+ end
+
+ context 'when there are no milestones' do
+ it 'returns the correct counts' do
+ expect(counts).to eq(opened: 0, closed: 0, all: 0)
+ end
+ end
+ end
+end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 4b2ca3514f8..c5b5aa8c445 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -6,6 +6,38 @@ describe SearchHelper do
str
end
+ describe 'parsing result' do
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+ let(:results) { repository.search_files('feature', 'master') }
+ let(:search_result) { results.first }
+
+ subject { helper.parse_search_result(search_result) }
+
+ it "returns a valid OpenStruct object" do
+ is_expected.to be_an OpenStruct
+ expect(subject.filename).to eq('CHANGELOG')
+ expect(subject.basename).to eq('CHANGELOG')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(186)
+ expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n")
+ end
+
+ context "when filename has extension" do
+ let(:search_result) { "master:CONTRIBUTE.md:5:- [Contribute to GitLab](#contribute-to-gitlab)\n" }
+
+ it { expect(subject.filename).to eq('CONTRIBUTE.md') }
+ it { expect(subject.basename).to eq('CONTRIBUTE') }
+ end
+
+ context "when file under directory" do
+ let(:search_result) { "master:a/b/c.md:5:a b c\n" }
+
+ it { expect(subject.filename).to eq('a/b/c.md') }
+ it { expect(subject.basename).to eq('a/b/c') }
+ end
+ end
+
describe 'search_autocomplete_source' do
context "with no current user" do
before do
diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
index 51c89ac4889..ac9bde6baf1 100644
--- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
@@ -127,6 +127,13 @@ describe Banzai::Pipeline::WikiPipeline do
expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page.md\"")
end
+
+ it 'rewrites links with anchor' do
+ markdown = '[Link to Header](start-page#title)'
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/start-page#title\"")
+ end
end
describe "when creating root links" do
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index af192664b33..6dedd25e9d3 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -754,6 +754,20 @@ module Ci
it 'does return production' do
expect(builds.size).to eq(1)
expect(builds.first[:environment]).to eq(environment)
+ expect(builds.first[:options]).to include(environment: { name: environment })
+ end
+ end
+
+ context 'when hash is specified' do
+ let(:environment) do
+ { name: 'production',
+ url: 'http://production.gitlab.com' }
+ end
+
+ it 'does return production and URL' do
+ expect(builds.size).to eq(1)
+ expect(builds.first[:environment]).to eq(environment[:name])
+ expect(builds.first[:options]).to include(environment: environment)
end
end
@@ -770,15 +784,16 @@ module Ci
let(:environment) { 1 }
it 'raises error' do
- expect { builds }.to raise_error("jobs:deploy_to_production environment #{Gitlab::Regex.environment_name_regex_message}")
+ expect { builds }.to raise_error(
+ 'jobs:deploy_to_production:environment config should be a hash or a string')
end
end
context 'is not a valid string' do
- let(:environment) { 'production staging' }
+ let(:environment) { 'production:staging' }
it 'raises error' do
- expect { builds }.to raise_error("jobs:deploy_to_production environment #{Gitlab::Regex.environment_name_regex_message}")
+ expect { builds }.to raise_error("jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}")
end
end
end
diff --git a/spec/lib/ci/mask_secret_spec.rb b/spec/lib/ci/mask_secret_spec.rb
new file mode 100644
index 00000000000..3101bed20fb
--- /dev/null
+++ b/spec/lib/ci/mask_secret_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Ci::MaskSecret, lib: true do
+ subject { described_class }
+
+ describe '#mask' do
+ it 'masks exact number of characters' do
+ expect(mask('token', 'oke')).to eq('txxxn')
+ end
+
+ it 'masks multiple occurrences' do
+ expect(mask('token token token', 'oke')).to eq('txxxn txxxn txxxn')
+ end
+
+ it 'does not mask if not found' do
+ expect(mask('token', 'not')).to eq('token')
+ end
+
+ it 'does support null token' do
+ expect(mask('token', nil)).to eq('token')
+ end
+
+ def mask(value, token)
+ subject.mask!(value.dup, token)
+ end
+ end
+end
diff --git a/spec/lib/expand_variables_spec.rb b/spec/lib/expand_variables_spec.rb
new file mode 100644
index 00000000000..90bc7dad379
--- /dev/null
+++ b/spec/lib/expand_variables_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe ExpandVariables do
+ describe '#expand' do
+ subject { described_class.expand(value, variables) }
+
+ tests = [
+ { value: 'key',
+ result: 'key',
+ variables: []
+ },
+ { value: 'key$variable',
+ result: 'key',
+ variables: []
+ },
+ { value: 'key$variable',
+ result: 'keyvalue',
+ variables: [
+ { key: 'variable', value: 'value' }
+ ]
+ },
+ { value: 'key${variable}',
+ result: 'keyvalue',
+ variables: [
+ { key: 'variable', value: 'value' }
+ ]
+ },
+ { value: 'key$variable$variable2',
+ result: 'keyvalueresult',
+ variables: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: 'result' },
+ ]
+ },
+ { value: 'key${variable}${variable2}',
+ result: 'keyvalueresult',
+ variables: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: 'result' }
+ ]
+ },
+ { value: 'key$variable2$variable',
+ result: 'keyresultvalue',
+ variables: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: 'result' },
+ ]
+ },
+ { value: 'key${variable2}${variable}',
+ result: 'keyresultvalue',
+ variables: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: 'result' }
+ ]
+ },
+ { value: 'review/$CI_BUILD_REF_NAME',
+ result: 'review/feature/add-review-apps',
+ variables: [
+ { key: 'CI_BUILD_REF_NAME', value: 'feature/add-review-apps' }
+ ]
+ },
+ ]
+
+ tests.each do |test|
+ context "#{test[:value]} resolves to #{test[:result]}" do
+ let(:value) { test[:value] }
+ let(:variables) { test[:variables] }
+
+ it { is_expected.to eq(test[:result]) }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 7c23e02d05a..745fbc0df45 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -4,15 +4,53 @@ describe Gitlab::Auth, lib: true do
let(:gl_auth) { described_class }
describe 'find_for_git_client' do
- it 'recognizes CI' do
- token = '123'
+ context 'build token' do
+ subject { gl_auth.find_for_git_client('gitlab-ci-token', build.token, project: project, ip: 'ip') }
+
+ context 'for running build' do
+ let!(:build) { create(:ci_build, :running) }
+ let(:project) { build.project }
+
+ before do
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'gitlab-ci-token')
+ end
+
+ it 'recognises user-less build' do
+ expect(subject).to eq(Gitlab::Auth::Result.new(nil, build.project, :ci, build_authentication_abilities))
+ end
+
+ it 'recognises user token' do
+ build.update(user: create(:user))
+
+ expect(subject).to eq(Gitlab::Auth::Result.new(build.user, build.project, :build, build_authentication_abilities))
+ end
+ end
+
+ (HasStatus::AVAILABLE_STATUSES - ['running']).each do |build_status|
+ context "for #{build_status} build" do
+ let!(:build) { create(:ci_build, status: build_status) }
+ let(:project) { build.project }
+
+ before do
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'gitlab-ci-token')
+ end
+
+ it 'denies authentication' do
+ expect(subject).to eq(Gitlab::Auth::Result.new)
+ end
+ end
+ end
+ end
+
+ it 'recognizes other ci services' do
project = create(:empty_project)
- project.update_attributes(runners_token: token)
+ project.create_drone_ci_service(active: true)
+ project.drone_ci_service.update(token: 'token')
ip = 'ip'
- expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'gitlab-ci-token')
- expect(gl_auth.find_for_git_client('gitlab-ci-token', token, project: project, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, :ci))
+ expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'drone-ci-token')
+ expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities))
end
it 'recognizes master passwords' do
@@ -20,7 +58,25 @@ describe Gitlab::Auth, lib: true do
ip = 'ip'
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username)
- expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, :gitlab_or_ldap))
+ expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
+ end
+
+ it 'recognizes user lfs tokens' do
+ user = create(:user)
+ ip = 'ip'
+ token = Gitlab::LfsToken.new(user).generate
+
+ expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username)
+ expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities))
+ end
+
+ it 'recognizes deploy key lfs tokens' do
+ key = create(:deploy_key)
+ ip = 'ip'
+ token = Gitlab::LfsToken.new(key).generate
+
+ expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: "lfs+deploy-key-#{key.id}")
+ expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities))
end
it 'recognizes OAuth tokens' do
@@ -30,7 +86,7 @@ describe Gitlab::Auth, lib: true do
ip = 'ip'
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'oauth2')
- expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, :oauth))
+ expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities))
end
it 'returns double nil for invalid credentials' do
@@ -92,4 +148,30 @@ describe Gitlab::Auth, lib: true do
end
end
end
+
+ private
+
+ def build_authentication_abilities
+ [
+ :read_project,
+ :build_download_code,
+ :build_read_container_image,
+ :build_create_container_image
+ ]
+ end
+
+ def read_authentication_abilities
+ [
+ :read_project,
+ :download_code,
+ :read_container_image
+ ]
+ end
+
+ def full_authentication_abilities
+ read_authentication_abilities + [
+ :push_code,
+ :create_container_image
+ ]
+ end
end
diff --git a/spec/lib/gitlab/backend/shell_spec.rb b/spec/lib/gitlab/backend/shell_spec.rb
index 6e5ba211382..07407f212aa 100644
--- a/spec/lib/gitlab/backend/shell_spec.rb
+++ b/spec/lib/gitlab/backend/shell_spec.rb
@@ -1,4 +1,5 @@
require 'spec_helper'
+require 'stringio'
describe Gitlab::Shell, lib: true do
let(:project) { double('Project', id: 7, path: 'diaspora') }
@@ -44,15 +45,38 @@ describe Gitlab::Shell, lib: true do
end
end
+ describe '#add_key' do
+ it 'removes trailing garbage' do
+ allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path)
+ expect(Gitlab::Utils).to receive(:system_silent).with(
+ [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar']
+ )
+
+ gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage')
+ end
+ end
+
describe Gitlab::Shell::KeyAdder, lib: true do
describe '#add_key' do
- it 'normalizes space characters in the key' do
- io = spy
+ it 'removes trailing garbage' do
+ io = spy(:io)
adder = described_class.new(io)
- adder.add_key('key-42', "sha-rsa foo\tbar\tbaz")
+ adder.add_key('key-42', "ssh-rsa foo bar\tbaz")
+
+ expect(io).to have_received(:puts).with("key-42\tssh-rsa foo")
+ end
+
+ it 'raises an exception if the key contains a tab' do
+ expect do
+ described_class.new(StringIO.new).add_key('key-42', "ssh-rsa\tfoobar")
+ end.to raise_error(Gitlab::Shell::Error)
+ end
- expect(io).to have_received(:puts).with("key-42\tsha-rsa foo bar baz")
+ it 'raises an exception if the key contains a newline' do
+ expect do
+ described_class.new(StringIO.new).add_key('key-42', "ssh-rsa foobar\nssh-rsa pawned")
+ end.to raise_error(Gitlab::Shell::Error)
end
end
end
diff --git a/spec/lib/gitlab/ci/config/node/environment_spec.rb b/spec/lib/gitlab/ci/config/node/environment_spec.rb
new file mode 100644
index 00000000000..df453223da7
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/environment_spec.rb
@@ -0,0 +1,155 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Environment do
+ let(:entry) { described_class.new(config) }
+
+ before { entry.compose! }
+
+ context 'when configuration is a string' do
+ let(:config) { 'production' }
+
+ describe '#string?' do
+ it 'is string configuration' do
+ expect(entry).to be_string
+ end
+ end
+
+ describe '#hash?' do
+ it 'is not hash configuration' do
+ expect(entry).not_to be_hash
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ describe '#value' do
+ it 'returns valid hash' do
+ expect(entry.value).to eq(name: 'production')
+ end
+ end
+
+ describe '#name' do
+ it 'returns environment name' do
+ expect(entry.name).to eq 'production'
+ end
+ end
+
+ describe '#url' do
+ it 'returns environment url' do
+ expect(entry.url).to be_nil
+ end
+ end
+ end
+
+ context 'when configuration is a hash' do
+ let(:config) do
+ { name: 'development', url: 'https://example.gitlab.com' }
+ end
+
+ describe '#string?' do
+ it 'is not string configuration' do
+ expect(entry).not_to be_string
+ end
+ end
+
+ describe '#hash?' do
+ it 'is hash configuration' do
+ expect(entry).to be_hash
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ describe '#value' do
+ it 'returns valid hash' do
+ expect(entry.value).to eq config
+ end
+ end
+
+ describe '#name' do
+ it 'returns environment name' do
+ expect(entry.name).to eq 'development'
+ end
+ end
+
+ describe '#url' do
+ it 'returns environment url' do
+ expect(entry.url).to eq 'https://example.gitlab.com'
+ end
+ end
+ end
+
+ context 'when variables are used for environment' do
+ let(:config) do
+ { name: 'review/$CI_BUILD_REF_NAME',
+ url: 'https://$CI_BUILD_REF_NAME.review.gitlab.com' }
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when configuration is invalid' do
+ context 'when configuration is an array' do
+ let(:config) { ['env'] }
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'contains error about invalid type' do
+ expect(entry.errors)
+ .to include 'environment config should be a hash or a string'
+ end
+ end
+ end
+
+ context 'when environment name is not present' do
+ let(:config) { { url: 'https://example.gitlab.com' } }
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ describe '#errors?' do
+ it 'contains error about missing environment name' do
+ expect(entry.errors)
+ .to include "environment name can't be blank"
+ end
+ end
+ end
+
+ context 'when invalid URL is used' do
+ let(:config) { { name: 'test', url: 'invalid-example.gitlab.com' } }
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ describe '#errors?' do
+ it 'contains error about invalid URL' do
+ expect(entry.errors)
+ .to include "environment url must be a valid url"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 4ec3f19e03f..7fd25b9e5bf 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -91,63 +91,80 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
describe '#add_column_with_default' do
context 'outside of a transaction' do
- before do
- expect(model).to receive(:transaction_open?).and_return(false)
+ context 'when a column limit is not set' do
+ before do
+ expect(model).to receive(:transaction_open?).and_return(false)
- expect(model).to receive(:transaction).and_yield
+ expect(model).to receive(:transaction).and_yield
- expect(model).to receive(:add_column).
- with(:projects, :foo, :integer, default: nil)
+ expect(model).to receive(:add_column).
+ with(:projects, :foo, :integer, default: nil)
- expect(model).to receive(:change_column_default).
- with(:projects, :foo, 10)
- end
+ expect(model).to receive(:change_column_default).
+ with(:projects, :foo, 10)
+ end
- it 'adds the column while allowing NULL values' do
- expect(model).to receive(:update_column_in_batches).
- with(:projects, :foo, 10)
+ it 'adds the column while allowing NULL values' do
+ expect(model).to receive(:update_column_in_batches).
+ with(:projects, :foo, 10)
- expect(model).not_to receive(:change_column_null)
+ expect(model).not_to receive(:change_column_null)
- model.add_column_with_default(:projects, :foo, :integer,
- default: 10,
- allow_null: true)
- end
+ model.add_column_with_default(:projects, :foo, :integer,
+ default: 10,
+ allow_null: true)
+ end
- it 'adds the column while not allowing NULL values' do
- expect(model).to receive(:update_column_in_batches).
- with(:projects, :foo, 10)
+ it 'adds the column while not allowing NULL values' do
+ expect(model).to receive(:update_column_in_batches).
+ with(:projects, :foo, 10)
- expect(model).to receive(:change_column_null).
- with(:projects, :foo, false)
+ expect(model).to receive(:change_column_null).
+ with(:projects, :foo, false)
- model.add_column_with_default(:projects, :foo, :integer, default: 10)
- end
+ model.add_column_with_default(:projects, :foo, :integer, default: 10)
+ end
- it 'removes the added column whenever updating the rows fails' do
- expect(model).to receive(:update_column_in_batches).
- with(:projects, :foo, 10).
- and_raise(RuntimeError)
+ it 'removes the added column whenever updating the rows fails' do
+ expect(model).to receive(:update_column_in_batches).
+ with(:projects, :foo, 10).
+ and_raise(RuntimeError)
- expect(model).to receive(:remove_column).
- with(:projects, :foo)
+ expect(model).to receive(:remove_column).
+ with(:projects, :foo)
- expect do
- model.add_column_with_default(:projects, :foo, :integer, default: 10)
- end.to raise_error(RuntimeError)
+ expect do
+ model.add_column_with_default(:projects, :foo, :integer, default: 10)
+ end.to raise_error(RuntimeError)
+ end
+
+ it 'removes the added column whenever changing a column NULL constraint fails' do
+ expect(model).to receive(:change_column_null).
+ with(:projects, :foo, false).
+ and_raise(RuntimeError)
+
+ expect(model).to receive(:remove_column).
+ with(:projects, :foo)
+
+ expect do
+ model.add_column_with_default(:projects, :foo, :integer, default: 10)
+ end.to raise_error(RuntimeError)
+ end
end
- it 'removes the added column whenever changing a column NULL constraint fails' do
- expect(model).to receive(:change_column_null).
- with(:projects, :foo, false).
- and_raise(RuntimeError)
+ context 'when a column limit is set' do
+ it 'adds the column with a limit' do
+ allow(model).to receive(:transaction_open?).and_return(false)
+ allow(model).to receive(:transaction).and_yield
+ allow(model).to receive(:update_column_in_batches).with(:projects, :foo, 10)
+ allow(model).to receive(:change_column_null).with(:projects, :foo, false)
+ allow(model).to receive(:change_column_default).with(:projects, :foo, 10)
- expect(model).to receive(:remove_column).
- with(:projects, :foo)
+ expect(model).to receive(:add_column).
+ with(:projects, :foo, :integer, default: nil, limit: 8)
- expect do
- model.add_column_with_default(:projects, :foo, :integer, default: 10)
- end.to raise_error(RuntimeError)
+ model.add_column_with_default(:projects, :foo, :integer, default: 10, limit: 8)
+ end
end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index f12c9a370f7..de68e32e5b4 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -1,10 +1,17 @@
require 'spec_helper'
describe Gitlab::GitAccess, lib: true do
- let(:access) { Gitlab::GitAccess.new(actor, project, 'web') }
+ let(:access) { Gitlab::GitAccess.new(actor, project, 'web', authentication_abilities: authentication_abilities) }
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:actor) { user }
+ let(:authentication_abilities) do
+ [
+ :read_project,
+ :download_code,
+ :push_code
+ ]
+ end
describe '#check with single protocols allowed' do
def disable_protocol(protocol)
@@ -15,7 +22,7 @@ describe Gitlab::GitAccess, lib: true do
context 'ssh disabled' do
before do
disable_protocol('ssh')
- @acc = Gitlab::GitAccess.new(actor, project, 'ssh')
+ @acc = Gitlab::GitAccess.new(actor, project, 'ssh', authentication_abilities: authentication_abilities)
end
it 'blocks ssh git push' do
@@ -30,7 +37,7 @@ describe Gitlab::GitAccess, lib: true do
context 'http disabled' do
before do
disable_protocol('http')
- @acc = Gitlab::GitAccess.new(actor, project, 'http')
+ @acc = Gitlab::GitAccess.new(actor, project, 'http', authentication_abilities: authentication_abilities)
end
it 'blocks http push' do
@@ -111,6 +118,36 @@ describe Gitlab::GitAccess, lib: true do
end
end
end
+
+ describe 'build authentication_abilities permissions' do
+ let(:authentication_abilities) { build_authentication_abilities }
+
+ describe 'reporter user' do
+ before { project.team << [user, :reporter] }
+
+ context 'pull code' do
+ it { expect(subject).to be_allowed }
+ end
+ end
+
+ describe 'admin user' do
+ let(:user) { create(:admin) }
+
+ context 'when member of the project' do
+ before { project.team << [user, :reporter] }
+
+ context 'pull code' do
+ it { expect(subject).to be_allowed }
+ end
+ end
+
+ context 'when is not member of the project' do
+ context 'pull code' do
+ it { expect(subject).not_to be_allowed }
+ end
+ end
+ end
+ end
end
describe 'push_access_check' do
@@ -283,38 +320,71 @@ describe Gitlab::GitAccess, lib: true do
end
end
- describe 'deploy key permissions' do
- let(:key) { create(:deploy_key) }
- let(:actor) { key }
+ shared_examples 'can not push code' do
+ subject { access.check('git-receive-pack', '_any') }
+
+ context 'when project is authorized' do
+ before { authorize }
- context 'push code' do
- subject { access.check('git-receive-pack', '_any') }
+ it { expect(subject).not_to be_allowed }
+ end
- context 'when project is authorized' do
- before { key.projects << project }
+ context 'when unauthorized' do
+ context 'to public project' do
+ let(:project) { create(:project, :public) }
it { expect(subject).not_to be_allowed }
end
- context 'when unauthorized' do
- context 'to public project' do
- let(:project) { create(:project, :public) }
+ context 'to internal project' do
+ let(:project) { create(:project, :internal) }
- it { expect(subject).not_to be_allowed }
- end
+ it { expect(subject).not_to be_allowed }
+ end
- context 'to internal project' do
- let(:project) { create(:project, :internal) }
+ context 'to private project' do
+ let(:project) { create(:project) }
- it { expect(subject).not_to be_allowed }
- end
+ it { expect(subject).not_to be_allowed }
+ end
+ end
+ end
- context 'to private project' do
- let(:project) { create(:project, :internal) }
+ describe 'build authentication abilities' do
+ let(:authentication_abilities) { build_authentication_abilities }
- it { expect(subject).not_to be_allowed }
- end
+ it_behaves_like 'can not push code' do
+ def authorize
+ project.team << [user, :reporter]
end
end
end
+
+ describe 'deploy key permissions' do
+ let(:key) { create(:deploy_key) }
+ let(:actor) { key }
+
+ it_behaves_like 'can not push code' do
+ def authorize
+ key.projects << project
+ end
+ end
+ end
+
+ private
+
+ def build_authentication_abilities
+ [
+ :read_project,
+ :build_download_code
+ ]
+ end
+
+ def full_authentication_abilities
+ [
+ :read_project,
+ :download_code,
+ :push_code
+ ]
+ end
end
diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb
index 4244b807d41..576cda595bb 100644
--- a/spec/lib/gitlab/git_access_wiki_spec.rb
+++ b/spec/lib/gitlab/git_access_wiki_spec.rb
@@ -1,9 +1,16 @@
require 'spec_helper'
describe Gitlab::GitAccessWiki, lib: true do
- let(:access) { Gitlab::GitAccessWiki.new(user, project, 'web') }
+ let(:access) { Gitlab::GitAccessWiki.new(user, project, 'web', authentication_abilities: authentication_abilities) }
let(:project) { create(:project) }
let(:user) { create(:user) }
+ let(:authentication_abilities) do
+ [
+ :read_project,
+ :download_code,
+ :push_code
+ ]
+ end
describe 'push_allowed?' do
before do
diff --git a/spec/lib/gitlab/git_spec.rb b/spec/lib/gitlab/git_spec.rb
new file mode 100644
index 00000000000..219198eff60
--- /dev/null
+++ b/spec/lib/gitlab/git_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Gitlab::Git, lib: true do
+ let(:committer_email) { FFaker::Internet.email }
+
+ # I have to remove periods from the end of the name
+ # This happened when the user's name had a suffix (i.e. "Sr.")
+ # This seems to be what git does under the hood. For example, this commit:
+ #
+ # $ git commit --author='Foo Sr. <foo@example.com>' -m 'Where's my trailing period?'
+ #
+ # results in this:
+ #
+ # $ git show --pretty
+ # ...
+ # Author: Foo Sr <foo@example.com>
+ # ...
+ let(:committer_name) { FFaker::Name.name.chomp("\.") }
+
+ describe 'committer_hash' do
+ it "returns a hash containing the given email and name" do
+ committer_hash = Gitlab::Git::committer_hash(email: committer_email, name: committer_name)
+
+ expect(committer_hash[:email]).to eq(committer_email)
+ expect(committer_hash[:name]).to eq(committer_name)
+ expect(committer_hash[:time]).to be_a(Time)
+ end
+
+ context 'when email is nil' do
+ it "returns nil" do
+ committer_hash = Gitlab::Git::committer_hash(email: nil, name: committer_name)
+
+ expect(committer_hash).to be_nil
+ end
+ end
+
+ context 'when name is nil' do
+ it "returns nil" do
+ committer_hash = Gitlab::Git::committer_hash(email: committer_email, name: nil)
+
+ expect(committer_hash).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/project_creator_spec.rb b/spec/lib/gitlab/github_import/project_creator_spec.rb
index 014ee462e5c..ab06b7bc5bb 100644
--- a/spec/lib/gitlab/github_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/github_import/project_creator_spec.rb
@@ -13,7 +13,7 @@ describe Gitlab::GithubImport::ProjectCreator, lib: true do
)
end
- subject(:service) { described_class.new(repo, namespace, user, github_access_token: 'asdffg') }
+ subject(:service) { described_class.new(repo, repo.name, namespace, user, github_access_token: 'asdffg') }
before do
namespace.add_owner(user)
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
new file mode 100644
index 00000000000..006569254a6
--- /dev/null
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -0,0 +1,187 @@
+---
+issues:
+- subscriptions
+- award_emoji
+- author
+- assignee
+- updated_by
+- milestone
+- notes
+- label_links
+- labels
+- todos
+- user_agent_detail
+- moved_to
+- events
+- merge_requests_closing_issues
+- metrics
+events:
+- author
+- project
+- target
+notes:
+- award_emoji
+- project
+- noteable
+- author
+- updated_by
+- resolved_by
+- todos
+- events
+label_links:
+- target
+- label
+label:
+- subscriptions
+- project
+- lists
+- label_links
+- issues
+- merge_requests
+milestone:
+- project
+- issues
+- labels
+- merge_requests
+- participants
+- events
+snippets:
+- author
+- project
+- notes
+- award_emoji
+releases:
+- project
+project_members:
+- created_by
+- user
+- source
+- project
+merge_requests:
+- subscriptions
+- award_emoji
+- author
+- assignee
+- updated_by
+- milestone
+- notes
+- label_links
+- labels
+- todos
+- target_project
+- source_project
+- merge_user
+- merge_request_diffs
+- merge_request_diff
+- events
+- merge_requests_closing_issues
+- metrics
+merge_request_diff:
+- merge_request
+pipelines:
+- project
+- user
+- statuses
+- builds
+- trigger_requests
+statuses:
+- project
+- pipeline
+- user
+variables:
+- project
+triggers:
+- project
+- trigger_requests
+deploy_keys:
+- user
+- deploy_keys_projects
+- projects
+services:
+- project
+- service_hook
+hooks:
+- project
+protected_branches:
+- project
+- merge_access_levels
+- push_access_levels
+merge_access_levels:
+- protected_branch
+push_access_levels:
+- protected_branch
+project:
+- taggings
+- base_tags
+- tag_taggings
+- tags
+- creator
+- group
+- namespace
+- board
+- last_event
+- services
+- campfire_service
+- drone_ci_service
+- emails_on_push_service
+- builds_email_service
+- irker_service
+- pivotaltracker_service
+- hipchat_service
+- flowdock_service
+- assembla_service
+- asana_service
+- gemnasium_service
+- slack_service
+- buildkite_service
+- bamboo_service
+- teamcity_service
+- pushover_service
+- jira_service
+- redmine_service
+- custom_issue_tracker_service
+- bugzilla_service
+- gitlab_issue_tracker_service
+- external_wiki_service
+- forked_project_link
+- forked_from_project
+- forked_project_links
+- forks
+- merge_requests
+- fork_merge_requests
+- issues
+- labels
+- events
+- milestones
+- notes
+- snippets
+- hooks
+- protected_branches
+- project_members
+- users
+- requesters
+- deploy_keys_projects
+- deploy_keys
+- users_star_projects
+- starrers
+- releases
+- lfs_objects_projects
+- lfs_objects
+- project_group_links
+- invited_groups
+- todos
+- notification_settings
+- import_data
+- commit_statuses
+- pipelines
+- builds
+- runner_projects
+- runners
+- variables
+- triggers
+- environments
+- deployments
+- project_feature
+award_emoji:
+- awardable
+- user \ No newline at end of file
diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
new file mode 100644
index 00000000000..2ba344092ce
--- /dev/null
+++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+# Part of the test security suite for the Import/Export feature
+# Checks whether there are new attributes in models that are currently being exported as part of the
+# project Import/Export feature.
+# If there are new attributes, these will have to either be added to this spec in case we want them
+# to be included as part of the export, or blacklist them using the import_export.yml configuration file.
+# Likewise, new models added to import_export.yml, will need to be added with their correspondent attributes
+# to this spec.
+describe 'Import/Export attribute configuration', lib: true do
+ include ConfigurationHelper
+
+ let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
+ let(:relation_names) do
+ names = names_from_tree(config_hash['project_tree'])
+
+ # Remove duplicated or add missing models
+ # - project is not part of the tree, so it has to be added manually.
+ # - milestone, labels have both singular and plural versions in the tree, so remove the duplicates.
+ names.flatten.uniq - ['milestones', 'labels'] + ['project']
+ end
+
+ let(:safe_attributes_file) { 'spec/lib/gitlab/import_export/safe_model_attributes.yml' }
+ let(:safe_model_attributes) { YAML.load_file(safe_attributes_file) }
+
+ it 'has no new columns' do
+ relation_names.each do |relation_name|
+ relation_class = relation_class_for_name(relation_name)
+
+ expect(safe_model_attributes[relation_class.to_s]).not_to be_nil, "Expected exported class #{relation_class.to_s} to exist in safe_model_attributes"
+
+ current_attributes = parsed_attributes(relation_name, relation_class.attribute_names)
+ safe_attributes = safe_model_attributes[relation_class.to_s]
+ new_attributes = current_attributes - safe_attributes
+
+ expect(new_attributes).to be_empty, failure_message(relation_class.to_s, new_attributes)
+ end
+ end
+
+ def failure_message(relation_class, new_attributes)
+ <<-MSG
+ It looks like #{relation_class}, which is exported using the project Import/Export, has new attributes: #{new_attributes.join(',')}
+
+ Please add the attribute(s) to SAFE_MODEL_ATTRIBUTES if you consider this can be exported.
+ Otherwise, please blacklist the attribute(s) in IMPORT_EXPORT_CONFIG by adding it to its correspondent
+ model in the +excluded_attributes+ section.
+
+ SAFE_MODEL_ATTRIBUTES: #{File.expand_path(safe_attributes_file)}
+ IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file}
+ MSG
+ end
+
+ class Author < User
+ end
+end
diff --git a/spec/lib/gitlab/import_export/model_configuration_spec.rb b/spec/lib/gitlab/import_export/model_configuration_spec.rb
new file mode 100644
index 00000000000..9b492d1b9c7
--- /dev/null
+++ b/spec/lib/gitlab/import_export/model_configuration_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+# Part of the test security suite for the Import/Export feature
+# Finds if a new model has been added that can potentially be part of the Import/Export
+# If it finds a new model, it will show a +failure_message+ with the options available.
+describe 'Import/Export model configuration', lib: true do
+ include ConfigurationHelper
+
+ let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
+ let(:model_names) do
+ names = names_from_tree(config_hash['project_tree'])
+
+ # Remove duplicated or add missing models
+ # - project is not part of the tree, so it has to be added manually.
+ # - milestone, labels have both singular and plural versions in the tree, so remove the duplicates.
+ # - User, Author... Models we do not care about for checking models
+ names.flatten.uniq - ['milestones', 'labels', 'user', 'author'] + ['project']
+ end
+
+ let(:all_models_yml) { 'spec/lib/gitlab/import_export/all_models.yml' }
+ let(:all_models) { YAML.load_file(all_models_yml) }
+ let(:current_models) { setup_models }
+
+ it 'has no new models' do
+ model_names.each do |model_name|
+ new_models = Array(current_models[model_name]) - Array(all_models[model_name])
+ expect(new_models).to be_empty, failure_message(model_name.classify, new_models)
+ end
+ end
+
+ # List of current models between models, in the format of
+ # {model: [model_2, model3], ...}
+ def setup_models
+ all_models_hash = {}
+
+ model_names.each do |model_name|
+ model_class = relation_class_for_name(model_name)
+
+ all_models_hash[model_name] = associations_for(model_class) - ['project']
+ end
+
+ all_models_hash
+ end
+
+ def failure_message(parent_model_name, new_models)
+ <<-MSG
+ New model(s) <#{new_models.join(',')}> have been added, related to #{parent_model_name}, which is exported by
+ the Import/Export feature.
+
+ If you think this model should be included in the export, please add it to IMPORT_EXPORT_CONFIG.
+ Definitely add it to MODELS_JSON to signal that you've handled this error and to prevent it from showing up in the future.
+
+ MODELS_JSON: #{File.expand_path(all_models_yml)}
+ IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file}
+ MSG
+ end
+end
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 5114f9c55e1..281f6cf1177 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -24,7 +24,7 @@
"test_ee_field": "test",
"milestone": {
"id": 1,
- "title": "v0.0",
+ "title": "test milestone",
"project_id": 8,
"description": "test milestone",
"due_date": null,
@@ -51,7 +51,7 @@
{
"id": 2,
"label_id": 2,
- "target_id": 3,
+ "target_id": 40,
"target_type": "Issue",
"created_at": "2016-07-22T08:57:02.840Z",
"updated_at": "2016-07-22T08:57:02.840Z",
@@ -281,6 +281,31 @@
"deleted_at": null,
"due_date": null,
"moved_to_id": null,
+ "milestone": {
+ "id": 1,
+ "title": "test milestone",
+ "project_id": 8,
+ "description": "test milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1,
+ "events": [
+ {
+ "id": 487,
+ "target_type": "Milestone",
+ "target_id": 1,
+ "title": null,
+ "data": null,
+ "project_id": 46,
+ "created_at": "2016-06-14T15:02:04.418Z",
+ "updated_at": "2016-06-14T15:02:04.418Z",
+ "action": 1,
+ "author_id": 18
+ }
+ ]
+ },
"notes": [
{
"id": 359,
@@ -494,6 +519,27 @@
"deleted_at": null,
"due_date": null,
"moved_to_id": null,
+ "label_links": [
+ {
+ "id": 99,
+ "label_id": 2,
+ "target_id": 38,
+ "target_type": "Issue",
+ "created_at": "2016-07-22T08:57:02.840Z",
+ "updated_at": "2016-07-22T08:57:02.840Z",
+ "label": {
+ "id": 2,
+ "title": "test2",
+ "color": "#428bca",
+ "project_id": 8,
+ "created_at": "2016-07-22T08:55:44.161Z",
+ "updated_at": "2016-07-22T08:55:44.161Z",
+ "template": false,
+ "description": "",
+ "priority": null
+ }
+ }
+ ],
"notes": [
{
"id": 367,
@@ -6478,7 +6524,7 @@
{
"id": 37,
"project_id": 5,
- "ref": "master",
+ "ref": null,
"sha": "048721d90c449b244b7b4c53a9186b04330174ec",
"before_sha": null,
"push_data": null,
@@ -7301,6 +7347,30 @@
],
"protected_branches": [
-
+ {
+ "id": 1,
+ "project_id": 9,
+ "name": "master",
+ "created_at": "2016-08-30T07:32:52.426Z",
+ "updated_at": "2016-08-30T07:32:52.426Z",
+ "merge_access_levels": [
+ {
+ "id": 1,
+ "protected_branch_id": 1,
+ "access_level": 40,
+ "created_at": "2016-08-30T07:32:52.458Z",
+ "updated_at": "2016-08-30T07:32:52.458Z"
+ }
+ ],
+ "push_access_levels": [
+ {
+ "id": 1,
+ "protected_branch_id": 1,
+ "access_level": 40,
+ "created_at": "2016-08-30T07:32:52.490Z",
+ "updated_at": "2016-08-30T07:32:52.490Z"
+ }
+ ]
+ }
]
-}
+} \ No newline at end of file
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index a07ef279e68..feacb295231 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -29,12 +29,30 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::ENABLED)
end
+ it 'has the same label associated to two issues' do
+ restored_project_json
+
+ expect(Label.first.issues.count).to eq(2)
+ end
+
+ it 'has milestones associated to two separate issues' do
+ restored_project_json
+
+ expect(Milestone.find_by_description('test milestone').issues.count).to eq(2)
+ end
+
it 'creates a valid pipeline note' do
restored_project_json
expect(Ci::Pipeline.first.notes).not_to be_empty
end
+ it 'restores pipelines with missing ref' do
+ restored_project_json
+
+ expect(Ci::Pipeline.where(ref: nil)).not_to be_empty
+ end
+
it 'restores the correct event with symbolised data' do
restored_project_json
@@ -49,6 +67,18 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(issue.reload.updated_at.to_s).to eq('2016-06-14 15:02:47 UTC')
end
+ it 'contains the merge access levels on a protected branch' do
+ restored_project_json
+
+ expect(ProtectedBranch.first.merge_access_levels).not_to be_empty
+ end
+
+ it 'contains the push access levels on a protected branch' do
+ restored_project_json
+
+ expect(ProtectedBranch.first.push_access_levels).not_to be_empty
+ end
+
context 'event at forth level of the tree' do
let(:event) { Event.where(title: 'test levels').first }
@@ -77,12 +107,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(Label.first.label_links.first.target).not_to be_nil
end
- it 'has milestones associated to issues' do
- restored_project_json
-
- expect(Milestone.find_by_description('test milestone').issues).not_to be_empty
- end
-
context 'Merge requests' do
before do
restored_project_json
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
new file mode 100644
index 00000000000..8bccd313d6c
--- /dev/null
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -0,0 +1,330 @@
+---
+Issue:
+- id
+- title
+- assignee_id
+- author_id
+- project_id
+- created_at
+- updated_at
+- position
+- branch_name
+- description
+- state
+- iid
+- updated_by_id
+- confidential
+- deleted_at
+- due_date
+- moved_to_id
+- lock_version
+- milestone_id
+- weight
+Event:
+- id
+- target_type
+- target_id
+- title
+- data
+- project_id
+- created_at
+- updated_at
+- action
+- author_id
+Note:
+- id
+- note
+- noteable_type
+- author_id
+- created_at
+- updated_at
+- project_id
+- attachment
+- line_code
+- commit_id
+- noteable_id
+- system
+- st_diff
+- updated_by_id
+- type
+- position
+- original_position
+- resolved_at
+- resolved_by_id
+- discussion_id
+- original_discussion_id
+LabelLink:
+- id
+- label_id
+- target_id
+- target_type
+- created_at
+- updated_at
+Label:
+- id
+- title
+- color
+- project_id
+- created_at
+- updated_at
+- template
+- description
+- priority
+Milestone:
+- id
+- title
+- project_id
+- description
+- due_date
+- created_at
+- updated_at
+- state
+- iid
+ProjectSnippet:
+- id
+- title
+- content
+- author_id
+- project_id
+- created_at
+- updated_at
+- file_name
+- type
+- visibility_level
+Release:
+- id
+- tag
+- description
+- project_id
+- created_at
+- updated_at
+ProjectMember:
+- id
+- access_level
+- source_id
+- source_type
+- user_id
+- notification_level
+- type
+- created_at
+- updated_at
+- created_by_id
+- invite_email
+- invite_token
+- invite_accepted_at
+- requested_at
+- expires_at
+User:
+- id
+- username
+- email
+MergeRequest:
+- id
+- target_branch
+- source_branch
+- source_project_id
+- author_id
+- assignee_id
+- title
+- created_at
+- updated_at
+- state
+- merge_status
+- target_project_id
+- iid
+- description
+- position
+- locked_at
+- updated_by_id
+- merge_error
+- merge_params
+- merge_when_build_succeeds
+- merge_user_id
+- merge_commit_sha
+- deleted_at
+- in_progress_merge_commit_sha
+- lock_version
+- milestone_id
+- approvals_before_merge
+- rebase_commit_sha
+MergeRequestDiff:
+- id
+- state
+- st_commits
+- merge_request_id
+- created_at
+- updated_at
+- base_commit_sha
+- real_size
+- head_commit_sha
+- start_commit_sha
+Ci::Pipeline:
+- id
+- project_id
+- ref
+- sha
+- before_sha
+- push_data
+- created_at
+- updated_at
+- tag
+- yaml_errors
+- committed_at
+- gl_project_id
+- status
+- started_at
+- finished_at
+- duration
+- user_id
+CommitStatus:
+- id
+- project_id
+- status
+- finished_at
+- trace
+- created_at
+- updated_at
+- started_at
+- runner_id
+- coverage
+- commit_id
+- commands
+- job_id
+- name
+- deploy
+- options
+- allow_failure
+- stage
+- trigger_request_id
+- stage_idx
+- tag
+- ref
+- user_id
+- type
+- target_url
+- description
+- artifacts_file
+- gl_project_id
+- artifacts_metadata
+- erased_by_id
+- erased_at
+- artifacts_expire_at
+- environment
+- artifacts_size
+- when
+- yaml_variables
+- queued_at
+- token
+Ci::Variable:
+- id
+- project_id
+- key
+- value
+- encrypted_value
+- encrypted_value_salt
+- encrypted_value_iv
+- gl_project_id
+Ci::Trigger:
+- id
+- token
+- project_id
+- deleted_at
+- created_at
+- updated_at
+- gl_project_id
+DeployKey:
+- id
+- user_id
+- created_at
+- updated_at
+- key
+- title
+- type
+- fingerprint
+- public
+Service:
+- id
+- type
+- title
+- project_id
+- created_at
+- updated_at
+- active
+- properties
+- template
+- push_events
+- issues_events
+- merge_requests_events
+- tag_push_events
+- note_events
+- pipeline_events
+- build_events
+- category
+- default
+- wiki_page_events
+- confidential_issues_events
+ProjectHook:
+- id
+- url
+- project_id
+- created_at
+- updated_at
+- type
+- service_id
+- push_events
+- issues_events
+- merge_requests_events
+- tag_push_events
+- note_events
+- pipeline_events
+- enable_ssl_verification
+- build_events
+- wiki_page_events
+- token
+- group_id
+- confidential_issues_events
+ProtectedBranch:
+- id
+- project_id
+- name
+- created_at
+- updated_at
+Project:
+- description
+- issues_enabled
+- merge_requests_enabled
+- wiki_enabled
+- snippets_enabled
+- visibility_level
+- archived
+Author:
+- name
+ProjectFeature:
+- id
+- project_id
+- merge_requests_access_level
+- issues_access_level
+- wiki_access_level
+- snippets_access_level
+- builds_access_level
+- created_at
+- updated_at
+ProtectedBranch::MergeAccessLevel:
+- id
+- protected_branch_id
+- access_level
+- created_at
+- updated_at
+ProtectedBranch::PushAccessLevel:
+- id
+- protected_branch_id
+- access_level
+- created_at
+- updated_at
+AwardEmoji:
+- id
+- user_id
+- name
+- awardable_type
+- created_at
+- updated_at
diff --git a/spec/lib/gitlab/import_export/version_checker_spec.rb b/spec/lib/gitlab/import_export/version_checker_spec.rb
index 90c6d1c67f6..c680e712b59 100644
--- a/spec/lib/gitlab/import_export/version_checker_spec.rb
+++ b/spec/lib/gitlab/import_export/version_checker_spec.rb
@@ -23,7 +23,7 @@ describe Gitlab::ImportExport::VersionChecker, services: true do
it 'shows the correct error message' do
described_class.check!(shared: shared)
- expect(shared.errors.first).to eq("Import version mismatch: Required <= #{Gitlab::ImportExport.version} but was #{version}")
+ expect(shared.errors.first).to eq("Import version mismatch: Required #{Gitlab::ImportExport.version} but was #{version}")
end
end
end
diff --git a/spec/lib/gitlab/lfs_token_spec.rb b/spec/lib/gitlab/lfs_token_spec.rb
new file mode 100644
index 00000000000..9f04f67e0a8
--- /dev/null
+++ b/spec/lib/gitlab/lfs_token_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe Gitlab::LfsToken, lib: true do
+ describe '#generate and #value' do
+ shared_examples 'an LFS token generator' do
+ it 'returns a randomly generated token' do
+ token = handler.generate
+
+ expect(token).not_to be_nil
+ expect(token).to be_a String
+ expect(token.length).to eq 50
+ end
+
+ it 'returns the correct token based on the key' do
+ token = handler.generate
+
+ expect(handler.value).to eq(token)
+ end
+ end
+
+ context 'when the actor is a user' do
+ let(:actor) { create(:user) }
+ let(:handler) { described_class.new(actor) }
+
+ it_behaves_like 'an LFS token generator'
+
+ it 'returns the correct username' do
+ expect(handler.actor_name).to eq(actor.username)
+ end
+
+ it 'returns the correct token type' do
+ expect(handler.type).to eq(:lfs_token)
+ end
+ end
+
+ context 'when the actor is a deploy key' do
+ let(:actor) { create(:deploy_key) }
+ let(:handler) { described_class.new(actor) }
+
+ it_behaves_like 'an LFS token generator'
+
+ it 'returns the correct username' do
+ expect(handler.actor_name).to eq("lfs+deploy-key-#{actor.id}")
+ end
+
+ it 'returns the correct token type' do
+ expect(handler.type).to eq(:lfs_deploy_token)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb
index e54f5ffb312..cb54c020b31 100644
--- a/spec/lib/gitlab/redis_spec.rb
+++ b/spec/lib/gitlab/redis_spec.rb
@@ -3,19 +3,27 @@ require 'spec_helper'
describe Gitlab::Redis do
let(:redis_config) { Rails.root.join('config', 'resque.yml').to_s }
- before(:each) { described_class.reset_params! }
- after(:each) { described_class.reset_params! }
+ before(:each) { clear_raw_config }
+ after(:each) { clear_raw_config }
describe '.params' do
subject { described_class.params }
+ it 'withstands mutation' do
+ params1 = described_class.params
+ params2 = described_class.params
+ params1[:foo] = :bar
+
+ expect(params2).not_to have_key(:foo)
+ end
+
context 'when url contains unix socket reference' do
let(:config_old) { Rails.root.join('spec/fixtures/config/redis_old_format_socket.yml').to_s }
let(:config_new) { Rails.root.join('spec/fixtures/config/redis_new_format_socket.yml').to_s }
context 'with old format' do
it 'returns path key instead' do
- expect_any_instance_of(described_class).to receive(:config_file) { config_old }
+ stub_const("#{described_class}::CONFIG_FILE", config_old)
is_expected.to include(path: '/path/to/old/redis.sock')
is_expected.not_to have_key(:url)
@@ -24,7 +32,7 @@ describe Gitlab::Redis do
context 'with new format' do
it 'returns path key instead' do
- expect_any_instance_of(described_class).to receive(:config_file) { config_new }
+ stub_const("#{described_class}::CONFIG_FILE", config_new)
is_expected.to include(path: '/path/to/redis.sock')
is_expected.not_to have_key(:url)
@@ -38,7 +46,7 @@ describe Gitlab::Redis do
context 'with old format' do
it 'returns hash with host, port, db, and password' do
- expect_any_instance_of(described_class).to receive(:config_file) { config_old }
+ stub_const("#{described_class}::CONFIG_FILE", config_old)
is_expected.to include(host: 'localhost', password: 'mypassword', port: 6379, db: 99)
is_expected.not_to have_key(:url)
@@ -47,7 +55,7 @@ describe Gitlab::Redis do
context 'with new format' do
it 'returns hash with host, port, db, and password' do
- expect_any_instance_of(described_class).to receive(:config_file) { config_new }
+ stub_const("#{described_class}::CONFIG_FILE", config_new)
is_expected.to include(host: 'localhost', password: 'mynewpassword', port: 6379, db: 99)
is_expected.not_to have_key(:url)
@@ -56,6 +64,30 @@ describe Gitlab::Redis do
end
end
+ describe '.url' do
+ it 'withstands mutation' do
+ url1 = described_class.url
+ url2 = described_class.url
+ url1 << 'foobar'
+
+ expect(url2).not_to end_with('foobar')
+ end
+ end
+
+ describe '._raw_config' do
+ subject { described_class._raw_config }
+
+ it 'should be frozen' do
+ expect(subject).to be_frozen
+ end
+
+ it 'returns false when the file does not exist' do
+ stub_const("#{described_class}::CONFIG_FILE", '/var/empty/doesnotexist')
+
+ expect(subject).to eq(false)
+ end
+ end
+
describe '#raw_config_hash' do
it 'returns default redis url when no config file is present' do
expect(subject).to receive(:fetch_config) { false }
@@ -71,9 +103,15 @@ describe Gitlab::Redis do
describe '#fetch_config' do
it 'returns false when no config file is present' do
- allow(File).to receive(:exist?).with(redis_config) { false }
+ allow(described_class).to receive(:_raw_config) { false }
expect(subject.send(:fetch_config)).to be_falsey
end
end
+
+ def clear_raw_config
+ described_class.remove_instance_variable(:@_raw_config)
+ rescue NameError
+ # raised if @_raw_config was not set; ignore
+ end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index eae9c060c38..0363bc74939 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -861,7 +861,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :create) }
it_behaves_like 'it should not have Gmail Actions links'
- it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with X-GitLab headers containing project details'
it_behaves_like 'an email that contains a header with author username'
@@ -914,7 +914,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :delete) }
it_behaves_like 'it should not have Gmail Actions links'
- it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with X-GitLab headers containing project details'
it_behaves_like 'an email that contains a header with author username'
@@ -936,7 +936,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) }
it_behaves_like 'it should not have Gmail Actions links'
- it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with X-GitLab headers containing project details'
it_behaves_like 'an email that contains a header with author username'
@@ -964,7 +964,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, diff_refs: diff_refs, send_from_committer_email: send_from_committer_email) }
it_behaves_like 'it should not have Gmail Actions links'
- it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with X-GitLab headers containing project details'
it_behaves_like 'an email that contains a header with author username'
@@ -1066,7 +1066,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, diff_refs: diff_refs) }
it_behaves_like 'it should show Gmail Actions View Commit link'
- it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with X-GitLab headers containing project details'
it_behaves_like 'an email that contains a header with author username'
diff --git a/spec/mailers/shared/notify.rb b/spec/mailers/shared/notify.rb
index 93de5850ba2..56872da9a8f 100644
--- a/spec/mailers/shared/notify.rb
+++ b/spec/mailers/shared/notify.rb
@@ -169,10 +169,18 @@ shared_examples 'it should show Gmail Actions View Commit link' do
end
shared_examples 'an unsubscribeable thread' do
+ it 'has a List-Unsubscribe header' do
+ is_expected.to have_header 'List-Unsubscribe', /unsubscribe/
+ end
+
it { is_expected.to have_body_text /unsubscribe/ }
end
shared_examples 'a user cannot unsubscribe through footer link' do
+ it 'does not have a List-Unsubscribe header' do
+ is_expected.not_to have_header 'List-Unsubscribe', /unsubscribe/
+ end
+
it { is_expected.not_to have_body_text /unsubscribe/ }
end
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index cee20234e1f..03d02b4d382 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -1,3 +1,4 @@
+# encoding: utf-8
require 'rails_helper'
describe Blob do
@@ -7,6 +8,25 @@ describe Blob do
end
end
+ describe '#data' do
+ context 'using a binary blob' do
+ it 'returns the data as-is' do
+ data = "\n\xFF\xB9\xC3"
+ blob = described_class.new(double(binary?: true, data: data))
+
+ expect(blob.data).to eq(data)
+ end
+ end
+
+ context 'using a text blob' do
+ it 'converts the data to UTF-8' do
+ blob = described_class.new(double(binary?: false, data: "\n\xFF\xB9\xC3"))
+
+ expect(blob.data).to eq("\n���")
+ end
+ end
+ end
+
describe '#svg?' do
it 'is falsey when not text' do
git_blob = double(text?: false)
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index 8eab4281bc7..e7864b7ad33 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -88,9 +88,7 @@ describe Ci::Build, models: true do
end
describe '#trace' do
- subject { build.trace_html }
-
- it { is_expected.to be_empty }
+ it { expect(build.trace).to be_nil }
context 'when build.trace contains text' do
let(:text) { 'example output' }
@@ -98,16 +96,80 @@ describe Ci::Build, models: true do
build.trace = text
end
- it { is_expected.to include(text) }
- it { expect(subject.length).to be >= text.length }
+ it { expect(build.trace).to eq(text) }
+ end
+
+ context 'when build.trace hides runners token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.update(trace: token)
+ build.project.update(runners_token: token)
+ end
+
+ it { expect(build.trace).not_to include(token) }
+ it { expect(build.raw_trace).to include(token) }
+ end
+
+ context 'when build.trace hides build token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.update(trace: token)
+ build.update(token: token)
+ end
+
+ it { expect(build.trace).not_to include(token) }
+ it { expect(build.raw_trace).to include(token) }
+ end
+ end
+
+ describe '#raw_trace' do
+ subject { build.raw_trace }
+
+ context 'when build.trace hides runners token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.project.update(runners_token: token)
+ build.update(trace: token)
+ end
+
+ it { is_expected.not_to include(token) }
+ end
+
+ context 'when build.trace hides build token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.update(token: token)
+ build.update(trace: token)
+ end
+
+ it { is_expected.not_to include(token) }
+ end
+ end
+
+ context '#append_trace' do
+ subject { build.trace_html }
+
+ context 'when build.trace hides runners token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.project.update(runners_token: token)
+ build.append_trace(token, 0)
+ end
+
+ it { is_expected.not_to include(token) }
end
- context 'when build.trace hides token' do
+ context 'when build.trace hides build token' do
let(:token) { 'my_secret_token' }
before do
- build.project.update_attributes(runners_token: token)
- build.update_attributes(trace: token)
+ build.update(token: token)
+ build.append_trace(token, 0)
end
it { is_expected.not_to include(token) }
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index bce18b4e99e..a37a00f461a 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -8,7 +8,7 @@ describe Ci::Build, models: true do
it 'obfuscates project runners token' do
allow(build).to receive(:raw_trace).and_return("Test: #{build.project.runners_token}")
- expect(build.trace).to eq("Test: xxxxxx")
+ expect(build.trace).to eq("Test: xxxxxxxxxxxxxxxxxxxx")
end
it 'empty project runners token' do
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index fbf945c757c..550a890797e 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -187,6 +187,37 @@ describe Ci::Pipeline, models: true do
end
end
+ describe "merge request metrics" do
+ let(:project) { FactoryGirl.create :project }
+ let(:pipeline) { FactoryGirl.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) }
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref) }
+
+ context 'when transitioning to running' do
+ it 'records the build start time' do
+ time = Time.now
+ Timecop.freeze(time) { build.run }
+
+ expect(merge_request.reload.metrics.latest_build_started_at).to be_within(1.second).of(time)
+ end
+
+ it 'clears the build end time' do
+ build.run
+
+ expect(merge_request.reload.metrics.latest_build_finished_at).to be_nil
+ end
+ end
+
+ context 'when transitioning to success' do
+ it 'records the build end time' do
+ build.run
+ time = Time.now
+ Timecop.freeze(time) { build.success }
+
+ expect(merge_request.reload.metrics.latest_build_finished_at).to be_within(1.second).of(time)
+ end
+ end
+ end
+
def create_build(name, queued_at = current, started_from = 0)
create(:ci_build,
name: name,
@@ -373,8 +404,8 @@ describe Ci::Pipeline, models: true do
end
describe '#execute_hooks' do
- let!(:build_a) { create_build('a') }
- let!(:build_b) { create_build('b') }
+ let!(:build_a) { create_build('a', 0) }
+ let!(:build_b) { create_build('b', 1) }
let!(:hook) do
create(:project_hook, project: project, pipeline_events: enabled)
@@ -398,7 +429,7 @@ describe Ci::Pipeline, models: true do
build_b.enqueue
end
- it 'receive a pending event once' do
+ it 'receives a pending event once' do
expect(WebMock).to have_requested_pipeline_hook('pending').once
end
end
@@ -411,7 +442,7 @@ describe Ci::Pipeline, models: true do
build_b.run
end
- it 'receive a running event once' do
+ it 'receives a running event once' do
expect(WebMock).to have_requested_pipeline_hook('running').once
end
end
@@ -422,11 +453,21 @@ describe Ci::Pipeline, models: true do
build_b.success
end
- it 'receive a success event once' do
+ it 'receives a success event once' do
expect(WebMock).to have_requested_pipeline_hook('success').once
end
end
+ context 'when stage one failed' do
+ before do
+ build_a.drop
+ end
+
+ it 'receives a failed event once' do
+ expect(WebMock).to have_requested_pipeline_hook('failed').once
+ end
+ end
+
def have_requested_pipeline_hook(status)
have_requested(:post, hook.url).with do |req|
json_body = JSON.parse(req.body)
@@ -450,8 +491,36 @@ describe Ci::Pipeline, models: true do
end
end
- def create_build(name)
- create(:ci_build, :created, pipeline: pipeline, name: name)
+ def create_build(name, stage_idx)
+ create(:ci_build,
+ :created,
+ pipeline: pipeline,
+ name: name,
+ stage_idx: stage_idx)
+ end
+ end
+
+ describe "#merge_requests" do
+ let(:project) { FactoryGirl.create :project }
+ let(:pipeline) { FactoryGirl.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) }
+
+ it "returns merge requests whose `diff_head_sha` matches the pipeline's SHA" do
+ merge_request = create(:merge_request, source_project: project, source_branch: pipeline.ref)
+
+ expect(pipeline.merge_requests).to eq([merge_request])
+ end
+
+ it "doesn't return merge requests whose source branch doesn't match the pipeline's ref" do
+ create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master')
+
+ expect(pipeline.merge_requests).to be_empty
+ end
+
+ it "doesn't return merge requests whose `diff_head_sha` doesn't match the pipeline's SHA" do
+ create(:merge_request, source_project: project, source_branch: pipeline.ref)
+ allow_any_instance_of(MergeRequest).to receive(:diff_head_sha) { '97de212e80737a608d939f648d959671fb0a0142b' }
+
+ expect(pipeline.merge_requests).to be_empty
end
end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index fcfa3138ce5..2f1baff5d66 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -40,7 +40,7 @@ describe CommitStatus, models: true do
it { is_expected.to be_falsey }
end
- %w(running success failed).each do |status|
+ %w[running success failed].each do |status|
context "if commit status is #{status}" do
before { commit_status.status = status }
@@ -48,7 +48,7 @@ describe CommitStatus, models: true do
end
end
- %w(pending canceled).each do |status|
+ %w[pending canceled].each do |status|
context "if commit status is #{status}" do
before { commit_status.status = status }
@@ -60,7 +60,7 @@ describe CommitStatus, models: true do
describe '#active?' do
subject { commit_status.active? }
- %w(pending running).each do |state|
+ %w[pending running].each do |state|
context "if commit_status.status is #{state}" do
before { commit_status.status = state }
@@ -68,7 +68,7 @@ describe CommitStatus, models: true do
end
end
- %w(success failed canceled).each do |state|
+ %w[success failed canceled].each do |state|
context "if commit_status.status is #{state}" do
before { commit_status.status = state }
@@ -80,7 +80,7 @@ describe CommitStatus, models: true do
describe '#complete?' do
subject { commit_status.complete? }
- %w(success failed canceled).each do |state|
+ %w[success failed canceled].each do |state|
context "if commit_status.status is #{state}" do
before { commit_status.status = state }
@@ -88,7 +88,7 @@ describe CommitStatus, models: true do
end
end
- %w(pending running).each do |state|
+ %w[pending running].each do |state|
context "if commit_status.status is #{state}" do
before { commit_status.status = state }
@@ -187,7 +187,7 @@ describe CommitStatus, models: true do
subject { CommitStatus.where(pipeline: pipeline).stages }
it 'returns ordered list of stages' do
- is_expected.to eq(%w(build test deploy))
+ is_expected.to eq(%w[build test deploy])
end
end
@@ -223,4 +223,33 @@ describe CommitStatus, models: true do
expect(commit_status.commit).to eq project.commit
end
end
+
+ describe '#group_name' do
+ subject { commit_status.group_name }
+
+ tests = {
+ 'rspec:windows' => 'rspec:windows',
+ 'rspec:windows 0' => 'rspec:windows 0',
+ 'rspec:windows 0 test' => 'rspec:windows 0 test',
+ 'rspec:windows 0 1' => 'rspec:windows',
+ 'rspec:windows 0 1 name' => 'rspec:windows name',
+ 'rspec:windows 0/1' => 'rspec:windows',
+ 'rspec:windows 0/1 name' => 'rspec:windows name',
+ 'rspec:windows 0:1' => 'rspec:windows',
+ 'rspec:windows 0:1 name' => 'rspec:windows name',
+ 'rspec:windows 10000 20000' => 'rspec:windows',
+ 'rspec:windows 0 : / 1' => 'rspec:windows',
+ 'rspec:windows 0 : / 1 name' => 'rspec:windows name',
+ '0 1 name ruby' => 'name ruby',
+ '0 :/ 1 name ruby' => 'name ruby'
+ }
+
+ tests.each do |name, group_name|
+ it "'#{name}' puts in '#{group_name}'" do
+ commit_status.name = name
+
+ is_expected.to eq(group_name)
+ end
+ end
+ end
end
diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb
new file mode 100644
index 00000000000..b9381e33914
--- /dev/null
+++ b/spec/models/cycle_analytics/code_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#code', feature: true do
+ extend CycleAnalyticsHelpers::TestGeneration
+
+ let(:project) { create(:project) }
+ let(:from_date) { 10.days.ago }
+ let(:user) { create(:user, :admin) }
+ subject { CycleAnalytics.new(project, from: from_date) }
+
+ generate_cycle_analytics_spec(
+ phase: :code,
+ data_fn: -> (context) { { issue: context.create(:issue, project: context.project) } },
+ start_time_conditions: [["issue mentioned in a commit",
+ -> (context, data) do
+ context.create_commit_referencing_issue(data[:issue])
+ end]],
+ end_time_conditions: [["merge request that closes issue is created",
+ -> (context, data) do
+ context.create_merge_request_closing_issue(data[:issue])
+ end]],
+ post_fn: -> (context, data) do
+ context.merge_merge_requests_closing_issue(data[:issue])
+ context.deploy_master
+ end)
+
+ context "when a regular merge request (that doesn't close the issue) is created" do
+ it "returns nil" do
+ 5.times do
+ issue = create(:issue, project: project)
+
+ create_commit_referencing_issue(issue)
+ create_merge_request_closing_issue(issue, message: "Closes nothing")
+
+ merge_merge_requests_closing_issue(issue)
+ deploy_master
+ end
+
+ expect(subject.code).to be_nil
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb
new file mode 100644
index 00000000000..e9cc71254ab
--- /dev/null
+++ b/spec/models/cycle_analytics/issue_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#issue', models: true do
+ extend CycleAnalyticsHelpers::TestGeneration
+
+ let(:project) { create(:project) }
+ let(:from_date) { 10.days.ago }
+ let(:user) { create(:user, :admin) }
+ subject { CycleAnalytics.new(project, from: from_date) }
+
+ generate_cycle_analytics_spec(
+ phase: :issue,
+ data_fn: -> (context) { { issue: context.build(:issue, project: context.project) } },
+ start_time_conditions: [["issue created", -> (context, data) { data[:issue].save }]],
+ end_time_conditions: [["issue associated with a milestone",
+ -> (context, data) do
+ if data[:issue].persisted?
+ data[:issue].update(milestone: context.create(:milestone, project: context.project))
+ end
+ end],
+ ["list label added to issue",
+ -> (context, data) do
+ if data[:issue].persisted?
+ data[:issue].update(label_ids: [context.create(:label, lists: [context.create(:list)]).id])
+ end
+ end]],
+ post_fn: -> (context, data) do
+ if data[:issue].persisted?
+ context.create_merge_request_closing_issue(data[:issue].reload)
+ context.merge_merge_requests_closing_issue(data[:issue])
+ context.deploy_master
+ end
+ end)
+
+ context "when a regular label (instead of a list label) is added to the issue" do
+ it "returns nil" do
+ 5.times do
+ regular_label = create(:label)
+ issue = create(:issue, project: project)
+ issue.update(label_ids: [regular_label.id])
+
+ create_merge_request_closing_issue(issue)
+ merge_merge_requests_closing_issue(issue)
+ deploy_master
+ end
+
+ expect(subject.issue).to be_nil
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb
new file mode 100644
index 00000000000..5b8c96dc992
--- /dev/null
+++ b/spec/models/cycle_analytics/plan_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#plan', feature: true do
+ extend CycleAnalyticsHelpers::TestGeneration
+
+ let(:project) { create(:project) }
+ let(:from_date) { 10.days.ago }
+ let(:user) { create(:user, :admin) }
+ subject { CycleAnalytics.new(project, from: from_date) }
+
+ generate_cycle_analytics_spec(
+ phase: :plan,
+ data_fn: -> (context) do
+ {
+ issue: context.create(:issue, project: context.project),
+ branch_name: context.random_git_name
+ }
+ end,
+ start_time_conditions: [["issue associated with a milestone",
+ -> (context, data) do
+ data[:issue].update(milestone: context.create(:milestone, project: context.project))
+ end],
+ ["list label added to issue",
+ -> (context, data) do
+ data[:issue].update(label_ids: [context.create(:label, lists: [context.create(:list)]).id])
+ end]],
+ end_time_conditions: [["issue mentioned in a commit",
+ -> (context, data) do
+ context.create_commit_referencing_issue(data[:issue], branch_name: data[:branch_name])
+ end]],
+ post_fn: -> (context, data) do
+ context.create_merge_request_closing_issue(data[:issue], source_branch: data[:branch_name])
+ context.merge_merge_requests_closing_issue(data[:issue])
+ context.deploy_master
+ end)
+
+ context "when a regular label (instead of a list label) is added to the issue" do
+ it "returns nil" do
+ branch_name = random_git_name
+ label = create(:label)
+ issue = create(:issue, project: project)
+ issue.update(label_ids: [label.id])
+ create_commit_referencing_issue(issue, branch_name: branch_name)
+
+ create_merge_request_closing_issue(issue, source_branch: branch_name)
+ merge_merge_requests_closing_issue(issue)
+ deploy_master
+
+ expect(subject.issue).to be_nil
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb
new file mode 100644
index 00000000000..1f5e5cab92d
--- /dev/null
+++ b/spec/models/cycle_analytics/production_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#production', feature: true do
+ extend CycleAnalyticsHelpers::TestGeneration
+
+ let(:project) { create(:project) }
+ let(:from_date) { 10.days.ago }
+ let(:user) { create(:user, :admin) }
+ subject { CycleAnalytics.new(project, from: from_date) }
+
+ generate_cycle_analytics_spec(
+ phase: :production,
+ data_fn: -> (context) { { issue: context.build(:issue, project: context.project) } },
+ start_time_conditions: [["issue is created", -> (context, data) { data[:issue].save }]],
+ before_end_fn: lambda do |context, data|
+ context.create_merge_request_closing_issue(data[:issue])
+ context.merge_merge_requests_closing_issue(data[:issue])
+ end,
+ end_time_conditions:
+ [["merge request that closes issue is deployed to production", -> (context, data) { context.deploy_master }],
+ ["production deploy happens after merge request is merged (along with other changes)",
+ lambda do |context, data|
+ # Make other changes on master
+ sha = context.project.repository.commit_file(context.user, context.random_git_name, "content", "commit message", 'master', false)
+ context.project.repository.commit(sha)
+
+ context.deploy_master
+ end]])
+
+ context "when a regular merge request (that doesn't close the issue) is merged and deployed" do
+ it "returns nil" do
+ 5.times do
+ merge_request = create(:merge_request)
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ deploy_master
+ end
+
+ expect(subject.production).to be_nil
+ end
+ end
+
+ context "when the deployment happens to a non-production environment" do
+ it "returns nil" do
+ 5.times do
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ deploy_master(environment: 'staging')
+ end
+
+ expect(subject.production).to be_nil
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb
new file mode 100644
index 00000000000..b6e26d8f261
--- /dev/null
+++ b/spec/models/cycle_analytics/review_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#review', feature: true do
+ extend CycleAnalyticsHelpers::TestGeneration
+
+ let(:project) { create(:project) }
+ let(:from_date) { 10.days.ago }
+ let(:user) { create(:user, :admin) }
+ subject { CycleAnalytics.new(project, from: from_date) }
+
+ generate_cycle_analytics_spec(
+ phase: :review,
+ data_fn: -> (context) { { issue: context.create(:issue, project: context.project) } },
+ start_time_conditions: [["merge request that closes issue is created",
+ -> (context, data) do
+ context.create_merge_request_closing_issue(data[:issue])
+ end]],
+ end_time_conditions: [["merge request that closes issue is merged",
+ -> (context, data) do
+ context.merge_merge_requests_closing_issue(data[:issue])
+ end]],
+ post_fn: -> (context, data) { context.deploy_master })
+
+ context "when a regular merge request (that doesn't close the issue) is created and merged" do
+ it "returns nil" do
+ 5.times do
+ MergeRequests::MergeService.new(project, user).execute(create(:merge_request))
+
+ deploy_master
+ end
+
+ expect(subject.review).to be_nil
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb
new file mode 100644
index 00000000000..af1c4477ddb
--- /dev/null
+++ b/spec/models/cycle_analytics/staging_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#staging', feature: true do
+ extend CycleAnalyticsHelpers::TestGeneration
+
+ let(:project) { create(:project) }
+ let(:from_date) { 10.days.ago }
+ let(:user) { create(:user, :admin) }
+ subject { CycleAnalytics.new(project, from: from_date) }
+
+ generate_cycle_analytics_spec(
+ phase: :staging,
+ data_fn: lambda do |context|
+ issue = context.create(:issue, project: context.project)
+ { issue: issue, merge_request: context.create_merge_request_closing_issue(issue) }
+ end,
+ start_time_conditions: [["merge request that closes issue is merged",
+ -> (context, data) do
+ context.merge_merge_requests_closing_issue(data[:issue])
+ end ]],
+ end_time_conditions: [["merge request that closes issue is deployed to production",
+ -> (context, data) do
+ context.deploy_master
+ end],
+ ["production deploy happens after merge request is merged (along with other changes)",
+ lambda do |context, data|
+ # Make other changes on master
+ sha = context.project.repository.commit_file(
+ context.user,
+ context.random_git_name,
+ "content",
+ "commit message",
+ 'master',
+ false)
+ context.project.repository.commit(sha)
+
+ context.deploy_master
+ end]])
+
+ context "when a regular merge request (that doesn't close the issue) is merged and deployed" do
+ it "returns nil" do
+ 5.times do
+ merge_request = create(:merge_request)
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ deploy_master
+ end
+
+ expect(subject.staging).to be_nil
+ end
+ end
+
+ context "when the deployment happens to a non-production environment" do
+ it "returns nil" do
+ 5.times do
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ deploy_master(environment: 'staging')
+ end
+
+ expect(subject.staging).to be_nil
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/summary_spec.rb b/spec/models/cycle_analytics/summary_spec.rb
new file mode 100644
index 00000000000..743bc2da33f
--- /dev/null
+++ b/spec/models/cycle_analytics/summary_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+describe CycleAnalytics::Summary, models: true do
+ let(:project) { create(:project) }
+ let(:from) { Time.now }
+ let(:user) { create(:user, :admin) }
+ subject { described_class.new(project, from: from) }
+
+ describe "#new_issues" do
+ it "finds the number of issues created after the 'from date'" do
+ Timecop.freeze(5.days.ago) { create(:issue, project: project) }
+ Timecop.freeze(5.days.from_now) { create(:issue, project: project) }
+
+ expect(subject.new_issues).to eq(1)
+ end
+
+ it "doesn't find issues from other projects" do
+ Timecop.freeze(5.days.from_now) { create(:issue, project: create(:project)) }
+
+ expect(subject.new_issues).to eq(0)
+ end
+ end
+
+ describe "#commits" do
+ it "finds the number of commits created after the 'from date'" do
+ Timecop.freeze(5.days.ago) { create_commit("Test message", project, user, 'master') }
+ Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master') }
+
+ expect(subject.commits).to eq(1)
+ end
+
+ it "doesn't find commits from other projects" do
+ Timecop.freeze(5.days.from_now) { create_commit("Test message", create(:project), user, 'master') }
+
+ expect(subject.commits).to eq(0)
+ end
+ end
+
+ describe "#deploys" do
+ it "finds the number of deploys made created after the 'from date'" do
+ Timecop.freeze(5.days.ago) { create(:deployment, project: project) }
+ Timecop.freeze(5.days.from_now) { create(:deployment, project: project) }
+
+ expect(subject.deploys).to eq(1)
+ end
+
+ it "doesn't find commits from other projects" do
+ Timecop.freeze(5.days.from_now) { create(:deployment, project: create(:project)) }
+
+ expect(subject.deploys).to eq(0)
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb
new file mode 100644
index 00000000000..89ace0b2742
--- /dev/null
+++ b/spec/models/cycle_analytics/test_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#test', feature: true do
+ extend CycleAnalyticsHelpers::TestGeneration
+
+ let(:project) { create(:project) }
+ let(:from_date) { 10.days.ago }
+ let(:user) { create(:user, :admin) }
+ subject { CycleAnalytics.new(project, from: from_date) }
+
+ generate_cycle_analytics_spec(
+ phase: :test,
+ data_fn: lambda do |context|
+ issue = context.create(:issue, project: context.project)
+ merge_request = context.create_merge_request_closing_issue(issue)
+ pipeline = context.create(:ci_pipeline, ref: merge_request.source_branch, sha: merge_request.diff_head_sha, project: context.project)
+ { pipeline: pipeline, issue: issue }
+ end,
+ start_time_conditions: [["pipeline is started", -> (context, data) { data[:pipeline].run! }]],
+ end_time_conditions: [["pipeline is finished", -> (context, data) { data[:pipeline].succeed! }]],
+ post_fn: -> (context, data) do
+ context.merge_merge_requests_closing_issue(data[:issue])
+ context.deploy_master
+ end)
+
+ context "when the pipeline is for a regular merge request (that doesn't close an issue)" do
+ it "returns nil" do
+ 5.times do
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
+
+ pipeline.run!
+ pipeline.succeed!
+
+ merge_merge_requests_closing_issue(issue)
+ deploy_master
+ end
+
+ expect(subject.test).to be_nil
+ end
+ end
+
+ context "when the pipeline is not for a merge request" do
+ it "returns nil" do
+ 5.times do
+ pipeline = create(:ci_pipeline, ref: "refs/heads/master", sha: project.repository.commit('master').sha)
+
+ pipeline.run!
+ pipeline.succeed!
+
+ deploy_master
+ end
+
+ expect(subject.test).to be_nil
+ end
+ end
+
+ context "when the pipeline is dropped (failed)" do
+ it "returns nil" do
+ 5.times do
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
+
+ pipeline.run!
+ pipeline.drop!
+
+ merge_merge_requests_closing_issue(issue)
+ deploy_master
+ end
+
+ expect(subject.test).to be_nil
+ end
+ end
+
+ context "when the pipeline is cancelled" do
+ it "returns nil" do
+ 5.times do
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
+
+ pipeline.run!
+ pipeline.cancel!
+
+ merge_merge_requests_closing_issue(issue)
+ deploy_master
+ end
+
+ expect(subject.test).to be_nil
+ end
+ end
+end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index c881897926e..6b1867a44e1 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -63,4 +63,20 @@ describe Environment, models: true do
end
end
end
+
+ describe '#environment_type' do
+ subject { environment.environment_type }
+
+ it 'sets a environment type if name has multiple segments' do
+ environment.update!(name: 'production/worker.gitlab.com')
+
+ is_expected.to eq('production')
+ end
+
+ it 'nullifies a type if it\'s a simple name' do
+ environment.update!(name: 'production')
+
+ is_expected.to be_nil
+ end
+ end
end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index b5d0d79e14e..8600eb4d2c4 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -16,18 +16,12 @@ describe Event, models: true do
describe 'Callbacks' do
describe 'after_create :reset_project_activity' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
- context "project's last activity was less than 5 minutes ago" do
- it 'does not update project.last_activity_at if it has been touched less than 5 minutes ago' do
- create_event(project, project.owner)
- project.update_column(:last_activity_at, 5.minutes.ago)
- project_last_activity_at = project.last_activity_at
+ it 'calls the reset_project_activity method' do
+ expect_any_instance_of(Event).to receive(:reset_project_activity)
- create_event(project, project.owner)
-
- expect(project.last_activity_at).to eq(project_last_activity_at)
- end
+ create_event(project, project.owner)
end
end
end
@@ -161,6 +155,35 @@ describe Event, models: true do
end
end
+ describe '#reset_project_activity' do
+ let(:project) { create(:empty_project) }
+
+ context 'when a project was updated less than 1 hour ago' do
+ it 'does not update the project' do
+ project.update(last_activity_at: Time.now)
+
+ expect(project).not_to receive(:update_column).
+ with(:last_activity_at, a_kind_of(Time))
+
+ create_event(project, project.owner)
+ end
+ end
+
+ context 'when a project was updated more than 1 hour ago' do
+ it 'updates the project' do
+ project.update(last_activity_at: 1.year.ago)
+
+ expect_any_instance_of(Gitlab::ExclusiveLease).
+ to receive(:try_obtain).and_return(true)
+
+ expect(project).to receive(:update_column).
+ with(:last_activity_at, a_kind_of(Time))
+
+ create_event(project, project.owner)
+ end
+ end
+ end
+
def create_event(project, user, attrs = {})
data = {
before: Gitlab::Git::BLANK_SHA,
diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb
index 92e0f7f27ce..dd033480527 100644
--- a/spec/models/global_milestone_spec.rb
+++ b/spec/models/global_milestone_spec.rb
@@ -50,8 +50,9 @@ describe GlobalMilestone, models: true do
milestone1_project2,
milestone1_project3,
]
+ milestones_relation = Milestone.where(id: milestones.map(&:id))
- @global_milestone = GlobalMilestone.new(milestone1_project1.title, milestones)
+ @global_milestone = GlobalMilestone.new(milestone1_project1.title, milestones_relation)
end
it 'has exactly one group milestone' do
@@ -67,7 +68,7 @@ describe GlobalMilestone, models: true do
let(:milestone) { create(:milestone, title: "git / test", project: project1) }
it 'strips out slashes and spaces' do
- global_milestone = GlobalMilestone.new(milestone.title, [milestone])
+ global_milestone = GlobalMilestone.new(milestone.title, Milestone.where(id: milestone.id))
expect(global_milestone.safe_title).to eq('git-test')
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index ea4b59c26b1..0b3ef9b98fd 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -187,6 +187,52 @@ describe Group, models: true do
it { expect(group.has_master?(@members[:requester])).to be_falsey }
end
+ describe '#lfs_enabled?' do
+ context 'LFS enabled globally' do
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
+
+ it 'returns true when nothing is set' do
+ expect(group.lfs_enabled?).to be_truthy
+ end
+
+ it 'returns false when set to false' do
+ group.update_attribute(:lfs_enabled, false)
+
+ expect(group.lfs_enabled?).to be_falsey
+ end
+
+ it 'returns true when set to true' do
+ group.update_attribute(:lfs_enabled, true)
+
+ expect(group.lfs_enabled?).to be_truthy
+ end
+ end
+
+ context 'LFS disabled globally' do
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(false)
+ end
+
+ it 'returns false when nothing is set' do
+ expect(group.lfs_enabled?).to be_falsey
+ end
+
+ it 'returns false when set to false' do
+ group.update_attribute(:lfs_enabled, false)
+
+ expect(group.lfs_enabled?).to be_falsey
+ end
+
+ it 'returns false when set to true' do
+ group.update_attribute(:lfs_enabled, true)
+
+ expect(group.lfs_enabled?).to be_falsey
+ end
+ end
+ end
+
describe '#owners' do
let(:owner) { create(:user) }
let(:developer) { create(:user) }
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index 4a457997a4f..474ae62ccec 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -1,21 +1,3 @@
-# == Schema Information
-#
-# Table name: web_hooks
-#
-# id :integer not null, primary key
-# url :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# type :string(255) default("ProjectHook")
-# service_id :integer
-# push_events :boolean default(TRUE), not null
-# issues_events :boolean default(FALSE), not null
-# merge_requests_events :boolean default(FALSE), not null
-# tag_push_events :boolean default(FALSE)
-# note_events :boolean default(FALSE), not null
-#
-
require 'spec_helper'
describe ProjectHook, models: true do
diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb
index 534e1b4f128..1a83c836652 100644
--- a/spec/models/hooks/service_hook_spec.rb
+++ b/spec/models/hooks/service_hook_spec.rb
@@ -1,21 +1,3 @@
-# == Schema Information
-#
-# Table name: web_hooks
-#
-# id :integer not null, primary key
-# url :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# type :string(255) default("ProjectHook")
-# service_id :integer
-# push_events :boolean default(TRUE), not null
-# issues_events :boolean default(FALSE), not null
-# merge_requests_events :boolean default(FALSE), not null
-# tag_push_events :boolean default(FALSE)
-# note_events :boolean default(FALSE), not null
-#
-
require "spec_helper"
describe ServiceHook, models: true do
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index cbdf7eec082..ad2b710041a 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -1,21 +1,3 @@
-# == Schema Information
-#
-# Table name: web_hooks
-#
-# id :integer not null, primary key
-# url :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# type :string(255) default("ProjectHook")
-# service_id :integer
-# push_events :boolean default(TRUE), not null
-# issues_events :boolean default(FALSE), not null
-# merge_requests_events :boolean default(FALSE), not null
-# tag_push_events :boolean default(FALSE)
-# note_events :boolean default(FALSE), not null
-#
-
require "spec_helper"
describe SystemHook, models: true do
@@ -48,7 +30,7 @@ describe SystemHook, models: true do
it "user_create hook" do
create(:user)
-
+
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_create/,
headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' }
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index f9bab487b96..e52b9d75cef 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -1,21 +1,3 @@
-# == Schema Information
-#
-# Table name: web_hooks
-#
-# id :integer not null, primary key
-# url :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# type :string(255) default("ProjectHook")
-# service_id :integer
-# push_events :boolean default(TRUE), not null
-# issues_events :boolean default(FALSE), not null
-# merge_requests_events :boolean default(FALSE), not null
-# tag_push_events :boolean default(FALSE)
-# note_events :boolean default(FALSE), not null
-#
-
require 'spec_helper'
describe WebHook, models: true do
diff --git a/spec/models/issue/metrics_spec.rb b/spec/models/issue/metrics_spec.rb
new file mode 100644
index 00000000000..e170b087ebc
--- /dev/null
+++ b/spec/models/issue/metrics_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe Issue::Metrics, models: true do
+ let(:project) { create(:project) }
+
+ subject { create(:issue, project: project) }
+
+ describe "when recording the default set of issue metrics on issue save" do
+ context "milestones" do
+ it "records the first time an issue is associated with a milestone" do
+ time = Time.now
+ Timecop.freeze(time) { subject.update(milestone: create(:milestone)) }
+ metrics = subject.metrics
+
+ expect(metrics).to be_present
+ expect(metrics.first_associated_with_milestone_at).to be_within(1.second).of(time)
+ end
+
+ it "does not record the second time an issue is associated with a milestone" do
+ time = Time.now
+ Timecop.freeze(time) { subject.update(milestone: create(:milestone)) }
+ Timecop.freeze(time + 2.hours) { subject.update(milestone: nil) }
+ Timecop.freeze(time + 6.hours) { subject.update(milestone: create(:milestone)) }
+ metrics = subject.metrics
+
+ expect(metrics).to be_present
+ expect(metrics.first_associated_with_milestone_at).to be_within(1.second).of(time)
+ end
+ end
+
+ context "list labels" do
+ it "records the first time an issue is associated with a list label" do
+ list_label = create(:label, lists: [create(:list)])
+ time = Time.now
+ Timecop.freeze(time) { subject.update(label_ids: [list_label.id]) }
+ metrics = subject.metrics
+
+ expect(metrics).to be_present
+ expect(metrics.first_added_to_board_at).to be_within(1.second).of(time)
+ end
+
+ it "does not record the second time an issue is associated with a list label" do
+ time = Time.now
+ first_list_label = create(:label, lists: [create(:list)])
+ Timecop.freeze(time) { subject.update(label_ids: [first_list_label.id]) }
+ second_list_label = create(:label, lists: [create(:list)])
+ Timecop.freeze(time + 5.hours) { subject.update(label_ids: [second_list_label.id]) }
+ metrics = subject.metrics
+
+ expect(metrics).to be_present
+ expect(metrics.first_added_to_board_at).to be_within(1.second).of(time)
+ end
+ end
+ end
+end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index fef90d9b5cb..0b1634f654a 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -57,7 +57,7 @@ describe Member, models: true do
describe 'Scopes & finders' do
before do
- project = create(:project)
+ project = create(:empty_project)
group = create(:group)
@owner_user = create(:user).tap { |u| group.add_owner(u) }
@owner = group.members.find_by(user_id: @owner_user.id)
@@ -65,6 +65,15 @@ describe Member, models: true do
@master_user = create(:user).tap { |u| project.team << [u, :master] }
@master = project.members.find_by(user_id: @master_user.id)
+ @blocked_user = create(:user).tap do |u|
+ project.team << [u, :master]
+ project.team << [u, :developer]
+
+ u.block!
+ end
+ @blocked_master = project.members.find_by(user_id: @blocked_user.id, access_level: Gitlab::Access::MASTER)
+ @blocked_developer = project.members.find_by(user_id: @blocked_user.id, access_level: Gitlab::Access::DEVELOPER)
+
Member.add_user(
project.members,
'toto1@example.com',
@@ -73,7 +82,7 @@ describe Member, models: true do
)
@invited_member = project.members.invite.find_by_invite_email('toto1@example.com')
- accepted_invite_user = build(:user)
+ accepted_invite_user = build(:user, state: :active)
Member.add_user(
project.members,
'toto2@example.com',
@@ -91,7 +100,7 @@ describe Member, models: true do
describe '.access_for_user_ids' do
it 'returns the right access levels' do
- users = [@owner_user.id, @master_user.id]
+ users = [@owner_user.id, @master_user.id, @blocked_user.id]
expected = {
@owner_user.id => Gitlab::Access::OWNER,
@master_user.id => Gitlab::Access::MASTER
@@ -125,6 +134,19 @@ describe Member, models: true do
it { expect(described_class.request).not_to include @accepted_request_member }
end
+ describe '.developers' do
+ subject { described_class.developers.to_a }
+
+ it { is_expected.not_to include @owner }
+ it { is_expected.not_to include @master }
+ it { is_expected.to include @invited_member }
+ it { is_expected.to include @accepted_invite_member }
+ it { is_expected.not_to include @requested_member }
+ it { is_expected.to include @accepted_request_member }
+ it { is_expected.not_to include @blocked_master }
+ it { is_expected.not_to include @blocked_developer }
+ end
+
describe '.owners_and_masters' do
it { expect(described_class.owners_and_masters).to include @owner }
it { expect(described_class.owners_and_masters).to include @master }
@@ -132,6 +154,20 @@ describe Member, models: true do
it { expect(described_class.owners_and_masters).not_to include @accepted_invite_member }
it { expect(described_class.owners_and_masters).not_to include @requested_member }
it { expect(described_class.owners_and_masters).not_to include @accepted_request_member }
+ it { expect(described_class.owners_and_masters).not_to include @blocked_master }
+ end
+
+ describe '.has_access' do
+ subject { described_class.has_access.to_a }
+
+ it { is_expected.to include @owner }
+ it { is_expected.to include @master }
+ it { is_expected.to include @invited_member }
+ it { is_expected.to include @accepted_invite_member }
+ it { is_expected.not_to include @requested_member }
+ it { is_expected.to include @accepted_request_member }
+ it { is_expected.not_to include @blocked_master }
+ it { is_expected.not_to include @blocked_developer }
end
end
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 4f875fd257a..56fa7fa6134 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -1,22 +1,3 @@
-# == Schema Information
-#
-# Table name: members
-#
-# id :integer not null, primary key
-# access_level :integer not null
-# source_id :integer not null
-# source_type :string(255) not null
-# user_id :integer
-# notification_level :integer not null
-# type :string(255)
-# created_at :datetime
-# updated_at :datetime
-# created_by_id :integer
-# invite_email :string(255)
-# invite_token :string(255)
-# invite_accepted_at :datetime
-#
-
require 'spec_helper'
describe GroupMember, models: true do
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index be57957b569..805c15a4e5e 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -1,22 +1,3 @@
-# == Schema Information
-#
-# Table name: members
-#
-# id :integer not null, primary key
-# access_level :integer not null
-# source_id :integer not null
-# source_type :string(255) not null
-# user_id :integer
-# notification_level :integer not null
-# type :string(255)
-# created_at :datetime
-# updated_at :datetime
-# created_by_id :integer
-# invite_email :string(255)
-# invite_token :string(255)
-# invite_accepted_at :datetime
-#
-
require 'spec_helper'
describe ProjectMember, models: true do
diff --git a/spec/models/merge_request/metrics_spec.rb b/spec/models/merge_request/metrics_spec.rb
new file mode 100644
index 00000000000..a79dd215d41
--- /dev/null
+++ b/spec/models/merge_request/metrics_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe MergeRequest::Metrics, models: true do
+ let(:project) { create(:project) }
+
+ subject { create(:merge_request, source_project: project) }
+
+ describe "when recording the default set of metrics on merge request save" do
+ it "records the merge time" do
+ time = Time.now
+ Timecop.freeze(time) { subject.mark_as_merged }
+ metrics = subject.metrics
+
+ expect(metrics).to be_present
+ expect(metrics.merged_at).to be_within(1.second).of(time)
+ end
+ end
+end
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index e5b185dc3f6..530a7def553 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -64,5 +64,27 @@ describe MergeRequestDiff, models: true do
end
end
end
+
+ describe '#commits_sha' do
+ shared_examples 'returning all commits SHA' do
+ it 'returns all commits SHA' do
+ commits_sha = subject.commits_sha
+
+ expect(commits_sha).to eq(subject.commits.map(&:sha))
+ end
+ end
+
+ context 'when commits were loaded' do
+ before do
+ subject.commits
+ end
+
+ it_behaves_like 'returning all commits SHA'
+ end
+
+ context 'when commits were not loaded' do
+ it_behaves_like 'returning all commits SHA'
+ end
+ end
end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 3b815ded2d3..12df6adde44 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -495,15 +495,77 @@ describe MergeRequest, models: true do
end
describe '#all_pipelines' do
- let!(:pipelines) do
- subject.merge_request_diff.commits.map do |commit|
- create(:ci_empty_pipeline, project: subject.source_project, sha: commit.id, ref: subject.source_branch)
+ shared_examples 'returning pipelines with proper ordering' do
+ let!(:all_pipelines) do
+ subject.all_commits_sha.map do |sha|
+ create(:ci_empty_pipeline,
+ project: subject.source_project,
+ sha: sha,
+ ref: subject.source_branch)
+ end
+ end
+
+ it 'returns all pipelines' do
+ expect(subject.all_pipelines).not_to be_empty
+ expect(subject.all_pipelines).to eq(all_pipelines.reverse)
+ end
+ end
+
+ context 'with single merge_request_diffs' do
+ it_behaves_like 'returning pipelines with proper ordering'
+ end
+
+ context 'with multiple irrelevant merge_request_diffs' do
+ before do
+ subject.update(target_branch: 'v1.0.0')
end
+
+ it_behaves_like 'returning pipelines with proper ordering'
end
- it 'returns a pipelines from source projects with proper ordering' do
- expect(subject.all_pipelines).not_to be_empty
- expect(subject.all_pipelines).to eq(pipelines.reverse)
+ context 'with unsaved merge request' do
+ subject { build(:merge_request) }
+
+ let!(:pipeline) do
+ create(:ci_empty_pipeline,
+ project: subject.project,
+ sha: subject.diff_head_sha,
+ ref: subject.source_branch)
+ end
+
+ it 'returns pipelines from diff_head_sha' do
+ expect(subject.all_pipelines).to contain_exactly(pipeline)
+ end
+ end
+ end
+
+ describe '#all_commits_sha' do
+ let(:all_commits_sha) do
+ subject.merge_request_diffs.flat_map(&:commits).map(&:sha).uniq
+ end
+
+ shared_examples 'returning all SHA' do
+ it 'returns all SHA from all merge_request_diffs' do
+ expect(subject.merge_request_diffs.size).to eq(2)
+ expect(subject.all_commits_sha).to eq(all_commits_sha)
+ end
+ end
+
+ context 'with a completely different branch' do
+ before do
+ subject.update(target_branch: 'v1.0.0')
+ end
+
+ it_behaves_like 'returning all SHA'
+ end
+
+ context 'with a branch having no difference' do
+ before do
+ subject.update(target_branch: 'v1.1.0')
+ subject.reload # make sure commits were not cached
+ end
+
+ it_behaves_like 'returning all SHA'
end
end
@@ -701,18 +763,67 @@ describe MergeRequest, models: true do
end
end
- describe "#environments" do
+ describe '#environments' do
let(:project) { create(:project) }
- let!(:environment) { create(:environment, project: project) }
- let!(:environment1) { create(:environment, project: project) }
- let!(:environment2) { create(:environment, project: project) }
let(:merge_request) { create(:merge_request, source_project: project) }
- it 'selects deployed environments' do
- create(:deployment, environment: environment, sha: project.commit('master').id)
- create(:deployment, environment: environment1, sha: project.commit('feature').id)
+ context 'with multiple environments' do
+ let(:environments) { create_list(:environment, 3, project: project) }
- expect(merge_request.environments).to eq [environment]
+ before do
+ create(:deployment, environment: environments.first, ref: 'master', sha: project.commit('master').id)
+ create(:deployment, environment: environments.second, ref: 'feature', sha: project.commit('feature').id)
+ end
+
+ it 'selects deployed environments' do
+ expect(merge_request.environments).to contain_exactly(environments.first)
+ end
+ end
+
+ context 'with environments on source project' do
+ let(:source_project) do
+ create(:project) do |fork_project|
+ fork_project.create_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
+ end
+ end
+
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: source_project, source_branch: 'feature',
+ target_project: project)
+ end
+
+ let(:source_environment) { create(:environment, project: source_project) }
+
+ before do
+ create(:deployment, environment: source_environment, ref: 'feature', sha: merge_request.diff_head_sha)
+ end
+
+ it 'selects deployed environments' do
+ expect(merge_request.environments).to contain_exactly(source_environment)
+ end
+
+ context 'with environments on target project' do
+ let(:target_environment) { create(:environment, project: project) }
+
+ before do
+ create(:deployment, environment: target_environment, tag: true, sha: merge_request.diff_head_sha)
+ end
+
+ it 'selects deployed environments' do
+ expect(merge_request.environments).to contain_exactly(source_environment, target_environment)
+ end
+ end
+ end
+
+ context 'without a diff_head_commit' do
+ before do
+ expect(merge_request).to receive(:diff_head_commit).and_return(nil)
+ end
+
+ it 'returns an empty array' do
+ expect(merge_request.environments).to be_empty
+ end
end
end
diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb
index dc702cfc42c..8e5145e824b 100644
--- a/spec/models/project_services/asana_service_spec.rb
+++ b/spec/models/project_services/asana_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-# template :boolean default(FALSE)
-# push_events :boolean default(TRUE)
-# issues_events :boolean default(TRUE)
-# merge_requests_events :boolean default(TRUE)
-# tag_push_events :boolean default(TRUE)
-# note_events :boolean default(TRUE), not null
-#
-
require 'spec_helper'
describe AsanaService, models: true do
diff --git a/spec/models/project_services/assembla_service_spec.rb b/spec/models/project_services/assembla_service_spec.rb
index d672d80156c..4c5acb7990b 100644
--- a/spec/models/project_services/assembla_service_spec.rb
+++ b/spec/models/project_services/assembla_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-# template :boolean default(FALSE)
-# push_events :boolean default(TRUE)
-# issues_events :boolean default(TRUE)
-# merge_requests_events :boolean default(TRUE)
-# tag_push_events :boolean default(TRUE)
-# note_events :boolean default(TRUE), not null
-#
-
require 'spec_helper'
describe AssemblaService, models: true do
diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb
index 9ae461f8c2d..d7e1a4e3b6c 100644
--- a/spec/models/project_services/bamboo_service_spec.rb
+++ b/spec/models/project_services/bamboo_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-# template :boolean default(FALSE)
-# push_events :boolean default(TRUE)
-# issues_events :boolean default(TRUE)
-# merge_requests_events :boolean default(TRUE)
-# tag_push_events :boolean default(TRUE)
-# note_events :boolean default(TRUE), not null
-#
-
require 'spec_helper'
describe BambooService, models: true do
diff --git a/spec/models/project_services/bugzilla_service_spec.rb b/spec/models/project_services/bugzilla_service_spec.rb
index a6d9717ccb5..739cc72b2ff 100644
--- a/spec/models/project_services/bugzilla_service_spec.rb
+++ b/spec/models/project_services/bugzilla_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-# template :boolean default(FALSE)
-# push_events :boolean default(TRUE)
-# issues_events :boolean default(TRUE)
-# merge_requests_events :boolean default(TRUE)
-# tag_push_events :boolean default(TRUE)
-# note_events :boolean default(TRUE), not null
-#
-
require 'spec_helper'
describe BugzillaService, models: true do
diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb
index 0866e1532dd..6f65beb79d0 100644
--- a/spec/models/project_services/buildkite_service_spec.rb
+++ b/spec/models/project_services/buildkite_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-# template :boolean default(FALSE)
-# push_events :boolean default(TRUE)
-# issues_events :boolean default(TRUE)
-# merge_requests_events :boolean default(TRUE)
-# tag_push_events :boolean default(TRUE)
-# note_events :boolean default(TRUE), not null
-#
-
require 'spec_helper'
describe BuildkiteService, models: true do
diff --git a/spec/models/project_services/campfire_service_spec.rb b/spec/models/project_services/campfire_service_spec.rb
index c76ae21421b..a3b9d084a75 100644
--- a/spec/models/project_services/campfire_service_spec.rb
+++ b/spec/models/project_services/campfire_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-# template :boolean default(FALSE)
-# push_events :boolean default(TRUE)
-# issues_events :boolean default(TRUE)
-# merge_requests_events :boolean default(TRUE)
-# tag_push_events :boolean default(TRUE)
-# note_events :boolean default(TRUE), not null
-#
-
require 'spec_helper'
describe CampfireService, models: true do
diff --git a/spec/models/project_services/custom_issue_tracker_service_spec.rb b/spec/models/project_services/custom_issue_tracker_service_spec.rb
index ff976f8ec59..7020667ea58 100644
--- a/spec/models/project_services/custom_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/custom_issue_tracker_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-# template :boolean default(FALSE)
-# push_events :boolean default(TRUE)
-# issues_events :boolean default(TRUE)
-# merge_requests_events :boolean default(TRUE)
-# tag_push_events :boolean default(TRUE)
-# note_events :boolean default(TRUE), not null
-#
-
require 'spec_helper'
describe CustomIssueTrackerService, models: true do
diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb
index 8ef892259f2..f13bb1e8adf 100644
--- a/spec/models/project_services/drone_ci_service_spec.rb
+++ b/spec/models/project_services/drone_ci_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-# template :boolean default(FALSE)
-# push_events :boolean default(TRUE)
-# issues_events :boolean default(TRUE)
-# merge_requests_events :boolean default(TRUE)
-# tag_push_events :boolean default(TRUE)
-# note_events :boolean default(TRUE), not null
-#
-
require 'spec_helper'
describe DroneCiService, models: true do
diff --git a/spec/models/project_services/external_wiki_service_spec.rb b/spec/models/project_services/external_wiki_service_spec.rb
index d7c5ea95d71..342d86aeca9 100644
--- a/spec/models/project_services/external_wiki_service_spec.rb
+++ b/spec/models/project_services/external_wiki_service_spec.rb
@@ -1,24 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-# template :boolean default(FALSE)
-# push_events :boolean default(TRUE)
-# issues_events :boolean default(TRUE)
-# merge_requests_events :boolean default(TRUE)
-# tag_push_events :boolean default(TRUE)
-# note_events :boolean default(TRUE), not null
-# build_events :boolean default(FALSE), not null
-#
-
require 'spec_helper'
describe ExternalWikiService, models: true do
diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb
index d2557019756..d6db02d6e76 100644
--- a/spec/models/project_services/flowdock_service_spec.rb
+++ b/spec/models/project_services/flowdock_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-# template :boolean default(FALSE)
-# push_events :boolean default(TRUE)
-# issues_events :boolean default(TRUE)
-# merge_requests_events :boolean default(TRUE)
-# tag_push_events :boolean default(TRUE)
-# note_events :boolean default(TRUE), not null
-#
-
require 'spec_helper'
describe FlowdockService, models: true do
diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb
index 3d0b6c9816b..529044d1d8b 100644
--- a/spec/models/project_services/gemnasium_service_spec.rb
+++ b/spec/models/project_services/gemnasium_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-# template :boolean default(FALSE)
-# push_events :boolean default(TRUE)
-# issues_events :boolean default(TRUE)
-# merge_requests_events :boolean default(TRUE)
-# tag_push_events :boolean default(TRUE)
-# note_events :boolean default(TRUE), not null
-#
-
require 'spec_helper'
describe GemnasiumService, models: true do
diff --git a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
index 8ef79a17d50..652804fb444 100644
--- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-# template :boolean default(FALSE)
-# push_events :boolean default(TRUE)
-# issues_events :boolean default(TRUE)
-# merge_requests_events :boolean default(TRUE)
-# tag_push_events :boolean default(TRUE)
-# note_events :boolean default(TRUE), not null
-#
-
require 'spec_helper'
describe GitlabIssueTrackerService, models: true do
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index 34eafbe555d..cf713684463 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-# template :boolean default(FALSE)
-# push_events :boolean default(TRUE)
-# issues_events :boolean default(TRUE)
-# merge_requests_events :boolean default(TRUE)
-# tag_push_events :boolean default(TRUE)
-# note_events :boolean default(TRUE), not null
-#
-
require 'spec_helper'
describe HipchatService, models: true do
diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb
index ffb17fd3259..f8c45b37561 100644
--- a/spec/models/project_services/irker_service_spec.rb
+++ b/spec/models/project_services/irker_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-# template :boolean default(FALSE)
-# push_events :boolean default(TRUE)
-# issues_events :boolean default(TRUE)
-# merge_requests_events :boolean default(TRUE)
-# tag_push_events :boolean default(TRUE)
-# note_events :boolean default(TRUE), not null
-#
-
require 'spec_helper'
require 'socket'
require 'json'
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 9037ca5cc20..b48a3176007 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-# template :boolean default(FALSE)
-# push_events :boolean default(TRUE)
-# issues_events :boolean default(TRUE)
-# merge_requests_events :boolean default(TRUE)
-# tag_push_events :boolean default(TRUE)
-# note_events :boolean default(TRUE), not null
-#
-
require 'spec_helper'
describe JiraService, models: true do
diff --git a/spec/models/project_services/pivotaltracker_service_spec.rb b/spec/models/project_services/pivotaltracker_service_spec.rb
index d098d988521..45b2f1068bf 100644
--- a/spec/models/project_services/pivotaltracker_service_spec.rb
+++ b/spec/models/project_services/pivotaltracker_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-# template :boolean default(FALSE)
-# push_events :boolean default(TRUE)
-# issues_events :boolean default(TRUE)
-# merge_requests_events :boolean default(TRUE)
-# tag_push_events :boolean default(TRUE)
-# note_events :boolean default(TRUE), not null
-#
-
require 'spec_helper'
describe PivotaltrackerService, models: true do
diff --git a/spec/models/project_services/pushover_service_spec.rb b/spec/models/project_services/pushover_service_spec.rb
index 5959c81577d..8fc92a9ab51 100644
--- a/spec/models/project_services/pushover_service_spec.rb
+++ b/spec/models/project_services/pushover_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-# template :boolean default(FALSE)
-# push_events :boolean default(TRUE)
-# issues_events :boolean default(TRUE)
-# merge_requests_events :boolean default(TRUE)
-# tag_push_events :boolean default(TRUE)
-# note_events :boolean default(TRUE), not null
-#
-
require 'spec_helper'
describe PushoverService, models: true do
diff --git a/spec/models/project_services/redmine_service_spec.rb b/spec/models/project_services/redmine_service_spec.rb
index 7d14f6e8280..b8679cd2563 100644
--- a/spec/models/project_services/redmine_service_spec.rb
+++ b/spec/models/project_services/redmine_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-# template :boolean default(FALSE)
-# push_events :boolean default(TRUE)
-# issues_events :boolean default(TRUE)
-# merge_requests_events :boolean default(TRUE)
-# tag_push_events :boolean default(TRUE)
-# note_events :boolean default(TRUE), not null
-#
-
require 'spec_helper'
describe RedmineService, models: true do
diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb
index 5afdc4b2f7b..c07a70a8069 100644
--- a/spec/models/project_services/slack_service_spec.rb
+++ b/spec/models/project_services/slack_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-# template :boolean default(FALSE)
-# push_events :boolean default(TRUE)
-# issues_events :boolean default(TRUE)
-# merge_requests_events :boolean default(TRUE)
-# tag_push_events :boolean default(TRUE)
-# note_events :boolean default(TRUE), not null
-#
-
require 'spec_helper'
describe SlackService, models: true do
diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb
index 474715d24c3..f7e878844dc 100644
--- a/spec/models/project_services/teamcity_service_spec.rb
+++ b/spec/models/project_services/teamcity_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-# template :boolean default(FALSE)
-# push_events :boolean default(TRUE)
-# issues_events :boolean default(TRUE)
-# merge_requests_events :boolean default(TRUE)
-# tag_push_events :boolean default(TRUE)
-# note_events :boolean default(TRUE), not null
-#
-
require 'spec_helper'
describe TeamcityService, models: true do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 74c1d93a460..83f61f0af0a 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -6,6 +6,7 @@ describe Project, models: true do
it { is_expected.to belong_to(:namespace) }
it { is_expected.to belong_to(:creator).class_name('User') }
it { is_expected.to have_many(:users) }
+ it { is_expected.to have_many(:services) }
it { is_expected.to have_many(:events).dependent(:destroy) }
it { is_expected.to have_many(:merge_requests).dependent(:destroy) }
it { is_expected.to have_many(:issues).dependent(:destroy) }
@@ -24,6 +25,30 @@ describe Project, models: true do
it { is_expected.to have_one(:pushover_service).dependent(:destroy) }
it { is_expected.to have_one(:asana_service).dependent(:destroy) }
it { is_expected.to have_one(:board).dependent(:destroy) }
+ it { is_expected.to have_one(:campfire_service).dependent(:destroy) }
+ it { is_expected.to have_one(:drone_ci_service).dependent(:destroy) }
+ it { is_expected.to have_one(:emails_on_push_service).dependent(:destroy) }
+ it { is_expected.to have_one(:builds_email_service).dependent(:destroy) }
+ it { is_expected.to have_one(:emails_on_push_service).dependent(:destroy) }
+ it { is_expected.to have_one(:irker_service).dependent(:destroy) }
+ it { is_expected.to have_one(:pivotaltracker_service).dependent(:destroy) }
+ it { is_expected.to have_one(:hipchat_service).dependent(:destroy) }
+ it { is_expected.to have_one(:flowdock_service).dependent(:destroy) }
+ it { is_expected.to have_one(:assembla_service).dependent(:destroy) }
+ it { is_expected.to have_one(:gemnasium_service).dependent(:destroy) }
+ it { is_expected.to have_one(:buildkite_service).dependent(:destroy) }
+ it { is_expected.to have_one(:bamboo_service).dependent(:destroy) }
+ it { is_expected.to have_one(:teamcity_service).dependent(:destroy) }
+ it { is_expected.to have_one(:jira_service).dependent(:destroy) }
+ it { is_expected.to have_one(:redmine_service).dependent(:destroy) }
+ it { is_expected.to have_one(:custom_issue_tracker_service).dependent(:destroy) }
+ it { is_expected.to have_one(:bugzilla_service).dependent(:destroy) }
+ it { is_expected.to have_one(:gitlab_issue_tracker_service).dependent(:destroy) }
+ it { is_expected.to have_one(:external_wiki_service).dependent(:destroy) }
+ it { is_expected.to have_one(:project_feature).dependent(:destroy) }
+ it { is_expected.to have_one(:import_data).class_name('ProjectImportData').dependent(:destroy) }
+ it { is_expected.to have_one(:last_event).class_name('Event') }
+ it { is_expected.to have_one(:forked_from_project).through(:forked_project_link) }
it { is_expected.to have_many(:commit_statuses) }
it { is_expected.to have_many(:pipelines) }
it { is_expected.to have_many(:builds) }
@@ -31,9 +56,16 @@ describe Project, models: true do
it { is_expected.to have_many(:runners) }
it { is_expected.to have_many(:variables) }
it { is_expected.to have_many(:triggers) }
+ it { is_expected.to have_many(:labels).dependent(:destroy) }
+ it { is_expected.to have_many(:users_star_projects).dependent(:destroy) }
it { is_expected.to have_many(:environments).dependent(:destroy) }
it { is_expected.to have_many(:deployments).dependent(:destroy) }
it { is_expected.to have_many(:todos).dependent(:destroy) }
+ it { is_expected.to have_many(:releases).dependent(:destroy) }
+ it { is_expected.to have_many(:lfs_objects_projects).dependent(:destroy) }
+ it { is_expected.to have_many(:project_group_links).dependent(:destroy) }
+ it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
+ it { is_expected.to have_many(:forks).through(:forked_project_links) }
describe '#members & #requesters' do
let(:project) { create(:project) }
@@ -178,7 +210,7 @@ describe Project, models: true do
expect(project.runners_token).not_to eq('')
end
- it 'does not set an random toke if one provided' do
+ it 'does not set an random token if one provided' do
project = FactoryGirl.create :empty_project, runners_token: 'my-token'
expect(project.runners_token).to eq('my-token')
end
@@ -276,20 +308,23 @@ describe Project, models: true do
end
describe 'last_activity methods' do
- let(:project) { create(:project) }
- let(:last_event) { double(created_at: Time.now) }
+ let(:timestamp) { Time.now - 2.hours }
+ let(:project) { create(:project, created_at: timestamp, updated_at: timestamp) }
describe 'last_activity' do
it 'alias last_activity to last_event' do
- allow(project).to receive(:last_event).and_return(last_event)
+ last_event = create(:event, project: project)
+
expect(project.last_activity).to eq(last_event)
end
end
describe 'last_activity_date' do
it 'returns the creation date of the project\'s last event if present' do
- create(:event, project: project)
- expect(project.last_activity_at.to_i).to eq(last_event.created_at.to_i)
+ expect_any_instance_of(Event).to receive(:try_obtain_lease).and_return(true)
+ new_event = create(:event, project: project, created_at: Time.now)
+
+ expect(project.last_activity_at.to_i).to eq(new_event.created_at.to_i)
end
it 'returns the project\'s last update date if it has no events' do
@@ -1385,6 +1420,68 @@ describe Project, models: true do
end
end
+ describe '#lfs_enabled?' do
+ let(:project) { create(:project) }
+
+ shared_examples 'project overrides group' do
+ it 'returns true when enabled in project' do
+ project.update_attribute(:lfs_enabled, true)
+
+ expect(project.lfs_enabled?).to be_truthy
+ end
+
+ it 'returns false when disabled in project' do
+ project.update_attribute(:lfs_enabled, false)
+
+ expect(project.lfs_enabled?).to be_falsey
+ end
+
+ it 'returns the value from the namespace, when no value is set in project' do
+ expect(project.lfs_enabled?).to eq(project.namespace.lfs_enabled?)
+ end
+ end
+
+ context 'LFS disabled in group' do
+ before do
+ project.namespace.update_attribute(:lfs_enabled, false)
+ enable_lfs
+ end
+
+ it_behaves_like 'project overrides group'
+ end
+
+ context 'LFS enabled in group' do
+ before do
+ project.namespace.update_attribute(:lfs_enabled, true)
+ enable_lfs
+ end
+
+ it_behaves_like 'project overrides group'
+ end
+
+ describe 'LFS disabled globally' do
+ shared_examples 'it always returns false' do
+ it do
+ expect(project.lfs_enabled?).to be_falsey
+ expect(project.namespace.lfs_enabled?).to be_falsey
+ end
+ end
+
+ context 'when no values are set' do
+ it_behaves_like 'it always returns false'
+ end
+
+ context 'when all values are set to true' do
+ before do
+ project.namespace.update_attribute(:lfs_enabled, true)
+ project.update_attribute(:lfs_enabled, true)
+ end
+
+ it_behaves_like 'it always returns false'
+ end
+ end
+ end
+
describe '.where_paths_in' do
context 'without any paths' do
it 'returns an empty relation' do
@@ -1549,4 +1646,49 @@ describe Project, models: true do
expect(project.pushes_since_gc).to eq(0)
end
end
+
+ describe '#environments_for' do
+ let(:project) { create(:project) }
+ let(:environment) { create(:environment, project: project) }
+
+ context 'tagged deployment' do
+ before do
+ create(:deployment, environment: environment, ref: '1.0', tag: true, sha: project.commit.id)
+ end
+
+ it 'returns environment when with_tags is set' do
+ expect(project.environments_for('master', project.commit, with_tags: true)).to contain_exactly(environment)
+ end
+
+ it 'does not return environment when no with_tags is set' do
+ expect(project.environments_for('master', project.commit)).to be_empty
+ end
+
+ it 'does not return environment when commit is not part of deployment' do
+ expect(project.environments_for('master', project.commit('feature'))).to be_empty
+ end
+ end
+
+ context 'branch deployment' do
+ before do
+ create(:deployment, environment: environment, ref: 'master', sha: project.commit.id)
+ end
+
+ it 'returns environment when ref is set' do
+ expect(project.environments_for('master', project.commit)).to contain_exactly(environment)
+ end
+
+ it 'does not environment when ref is different' do
+ expect(project.environments_for('feature', project.commit)).to be_empty
+ end
+
+ it 'does not return environment when commit is not part of deployment' do
+ expect(project.environments_for('master', project.commit('feature'))).to be_empty
+ end
+ end
+ end
+
+ def enable_lfs
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
end
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 5eaf0d3b7a6..f979d66c88c 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -73,6 +73,68 @@ describe ProjectTeam, models: true do
end
end
+ describe '#fetch_members' do
+ context 'personal project' do
+ let(:project) { create(:empty_project) }
+
+ it 'returns project members' do
+ user = create(:user)
+ project.team << [user, :guest]
+
+ expect(project.team.members).to contain_exactly(user)
+ end
+
+ it 'returns project members of a specified level' do
+ user = create(:user)
+ project.team << [user, :reporter]
+
+ expect(project.team.guests).to be_empty
+ expect(project.team.reporters).to contain_exactly(user)
+ end
+
+ it 'returns invited members of a group' do
+ group_member = create(:group_member)
+
+ project.project_group_links.create!(
+ group: group_member.group,
+ group_access: Gitlab::Access::GUEST
+ )
+
+ expect(project.team.members).to contain_exactly(group_member.user)
+ end
+
+ it 'returns invited members of a group of a specified level' do
+ group_member = create(:group_member)
+
+ project.project_group_links.create!(
+ group: group_member.group,
+ group_access: Gitlab::Access::REPORTER
+ )
+
+ expect(project.team.guests).to be_empty
+ expect(project.team.reporters).to contain_exactly(group_member.user)
+ end
+ end
+
+ context 'group project' do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, group: group) }
+
+ it 'returns project members' do
+ group_member = create(:group_member, group: group)
+
+ expect(project.team.members).to contain_exactly(group_member.user)
+ end
+
+ it 'returns project members of a specified level' do
+ group_member = create(:group_member, :reporter, group: group)
+
+ expect(project.team.guests).to be_empty
+ expect(project.team.reporters).to contain_exactly(group_member.user)
+ end
+ end
+ end
+
describe '#find_member' do
context 'personal project' do
let(:project) { create(:empty_project) }
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 7624050878e..db29f4d353b 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -16,6 +16,21 @@ describe Repository, models: true do
merge_commit_id = repository.merge(user, merge_request, commit_options)
repository.commit(merge_commit_id)
end
+ let(:author_email) { FFaker::Internet.email }
+
+ # I have to remove periods from the end of the name
+ # This happened when the user's name had a suffix (i.e. "Sr.")
+ # This seems to be what git does under the hood. For example, this commit:
+ #
+ # $ git commit --author='Foo Sr. <foo@example.com>' -m 'Where's my trailing period?'
+ #
+ # results in this:
+ #
+ # $ git show --pretty
+ # ...
+ # Author: Foo Sr <foo@example.com>
+ # ...
+ let(:author_name) { FFaker::Name.name.chomp("\.") }
describe '#branch_names_contains' do
subject { repository.branch_names_contains(sample_commit.id) }
@@ -132,7 +147,31 @@ describe Repository, models: true do
end
end
- describe :commit_file do
+ describe "#commit_dir" do
+ it "commits a change that creates a new directory" do
+ expect do
+ repository.commit_dir(user, 'newdir', 'Create newdir', 'master')
+ end.to change { repository.commits('master').count }.by(1)
+
+ newdir = repository.tree('master', 'newdir')
+ expect(newdir.path).to eq('newdir')
+ end
+
+ context "when an author is specified" do
+ it "uses the given email/name to set the commit's author" do
+ expect do
+ repository.commit_dir(user, "newdir", "Add newdir", 'master', author_email: author_email, author_name: author_name)
+ end.to change { repository.commits('master').count }.by(1)
+
+ last_commit = repository.commit
+
+ expect(last_commit.author_email).to eq(author_email)
+ expect(last_commit.author_name).to eq(author_name)
+ end
+ end
+ end
+
+ describe "#commit_file" do
it 'commits change to a file successfully' do
expect do
repository.commit_file(user, 'CHANGELOG', 'Changelog!',
@@ -144,9 +183,23 @@ describe Repository, models: true do
expect(blob.data).to eq('Changelog!')
end
+
+ context "when an author is specified" do
+ it "uses the given email/name to set the commit's author" do
+ expect do
+ repository.commit_file(user, "README", 'README!', 'Add README',
+ 'master', true, author_email: author_email, author_name: author_name)
+ end.to change { repository.commits('master').count }.by(1)
+
+ last_commit = repository.commit
+
+ expect(last_commit.author_email).to eq(author_email)
+ expect(last_commit.author_name).to eq(author_name)
+ end
+ end
end
- describe :update_file do
+ describe "#update_file" do
it 'updates filename successfully' do
expect do
repository.update_file(user, 'NEWLICENSE', 'Copyright!',
@@ -160,6 +213,85 @@ describe Repository, models: true do
expect(files).not_to include('LICENSE')
expect(files).to include('NEWLICENSE')
end
+
+ context "when an author is specified" do
+ it "uses the given email/name to set the commit's author" do
+ repository.commit_file(user, "README", 'README!', 'Add README', 'master', true)
+
+ expect do
+ repository.update_file(user, 'README', "Updated README!",
+ branch: 'master',
+ previous_path: 'README',
+ message: 'Update README',
+ author_email: author_email,
+ author_name: author_name)
+ end.to change { repository.commits('master').count }.by(1)
+
+ last_commit = repository.commit
+
+ expect(last_commit.author_email).to eq(author_email)
+ expect(last_commit.author_name).to eq(author_name)
+ end
+ end
+ end
+
+ describe "#remove_file" do
+ it 'removes file successfully' do
+ repository.commit_file(user, "README", 'README!', 'Add README', 'master', true)
+
+ expect do
+ repository.remove_file(user, "README", "Remove README", 'master')
+ end.to change { repository.commits('master').count }.by(1)
+
+ expect(repository.blob_at('master', 'README')).to be_nil
+ end
+
+ context "when an author is specified" do
+ it "uses the given email/name to set the commit's author" do
+ repository.commit_file(user, "README", 'README!', 'Add README', 'master', true)
+
+ expect do
+ repository.remove_file(user, "README", "Remove README", 'master', author_email: author_email, author_name: author_name)
+ end.to change { repository.commits('master').count }.by(1)
+
+ last_commit = repository.commit
+
+ expect(last_commit.author_email).to eq(author_email)
+ expect(last_commit.author_name).to eq(author_name)
+ end
+ end
+ end
+
+ describe '#get_committer_and_author' do
+ it 'returns the committer and author data' do
+ options = repository.get_committer_and_author(user)
+ expect(options[:committer][:email]).to eq(user.email)
+ expect(options[:author][:email]).to eq(user.email)
+ end
+
+ context 'when the email/name are given' do
+ it 'returns an object containing the email/name' do
+ options = repository.get_committer_and_author(user, email: author_email, name: author_name)
+ expect(options[:author][:email]).to eq(author_email)
+ expect(options[:author][:name]).to eq(author_name)
+ end
+ end
+
+ context 'when the email is given but the name is not' do
+ it 'returns the committer as the author' do
+ options = repository.get_committer_and_author(user, email: author_email)
+ expect(options[:author][:email]).to eq(user.email)
+ expect(options[:author][:name]).to eq(user.name)
+ end
+ end
+
+ context 'when the name is given but the email is not' do
+ it 'returns nil' do
+ options = repository.get_committer_and_author(user, name: author_name)
+ expect(options[:author][:email]).to eq(user.email)
+ expect(options[:author][:name]).to eq(user.name)
+ end
+ end
end
describe "search_files" do
@@ -186,32 +318,6 @@ describe Repository, models: true do
it { is_expected.to be_an String }
it { expect(subject.lines[2]).to eq("master:CHANGELOG:188: - Feature: Replace teams with group membership\n") }
end
-
- describe 'parsing result' do
- subject { repository.parse_search_result(search_result) }
- let(:search_result) { results.first }
-
- it { is_expected.to be_an OpenStruct }
- it { expect(subject.filename).to eq('CHANGELOG') }
- it { expect(subject.basename).to eq('CHANGELOG') }
- it { expect(subject.ref).to eq('master') }
- it { expect(subject.startline).to eq(186) }
- it { expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n") }
-
- context "when filename has extension" do
- let(:search_result) { "master:CONTRIBUTE.md:5:- [Contribute to GitLab](#contribute-to-gitlab)\n" }
-
- it { expect(subject.filename).to eq('CONTRIBUTE.md') }
- it { expect(subject.basename).to eq('CONTRIBUTE') }
- end
-
- context "when file under directory" do
- let(:search_result) { "master:a/b/c.md:5:a b c\n" }
-
- it { expect(subject.filename).to eq('a/b/c.md') }
- it { expect(subject.basename).to eq('a/b/c') }
- end
- end
end
describe "#changelog" do
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 0621c6a06ce..e6bc5296398 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -9,12 +9,14 @@ describe Snippet, models: true do
it { is_expected.to include_module(Participable) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
+ it { is_expected.to include_module(Awardable) }
end
describe 'associations' do
it { is_expected.to belong_to(:author).class_name('User') }
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
+ it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
end
describe 'validation' do
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index eda1cafd65e..a7a06744428 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -33,4 +33,17 @@ describe ProjectPolicy, models: true do
it 'returns increasing permissions for each level' do
expect(users_permissions).to eq(users_permissions.sort.uniq)
end
+
+ it 'does not include the read_issue permission when the issue author is not a member of the private project' do
+ project = create(:project, :private)
+ issue = create(:issue, project: project)
+ user = issue.author
+
+ expect(project.team.member?(issue.author)).to eq(false)
+
+ expect(BasePolicy.class_for(project).abilities(user, project).can_set).
+ not_to include(:read_issue)
+
+ expect(Ability.allowed?(user, :read_issue, project)).to be_falsy
+ end
end
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb
index bbdf8f03c2b..e66faeed705 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/api_helpers_spec.rb
@@ -36,11 +36,36 @@ describe API::Helpers, api: true do
params.delete(API::Helpers::SUDO_PARAM)
end
+ def warden_authenticate_returns(value)
+ warden = double("warden", authenticate: value)
+ env['warden'] = warden
+ end
+
+ def doorkeeper_guard_returns(value)
+ allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ value }
+ end
+
def error!(message, status)
raise Exception
end
describe ".current_user" do
+ subject { current_user }
+
+ describe "when authenticating via Warden" do
+ before { doorkeeper_guard_returns false }
+
+ context "fails" do
+ it { is_expected.to be_nil }
+ end
+
+ context "succeeds" do
+ before { warden_authenticate_returns user }
+
+ it { is_expected.to eq(user) }
+ end
+ end
+
describe "when authenticating using a user's private token" do
it "returns nil for an invalid token" do
env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index 981a6791881..5ad4fc4865a 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
- let!(:project) { create(:project) }
+ let!(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
let!(:award_emoji) { create(:award_emoji, awardable: issue, user: user) }
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
@@ -39,6 +39,19 @@ describe API::API, api: true do
end
end
+ context 'on a snippet' do
+ let(:snippet) { create(:project_snippet, :public, project: project) }
+ let!(:award) { create(:award_emoji, awardable: snippet) }
+
+ it 'returns the awarded emoji' do
+ get api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(award.name)
+ end
+ end
+
context 'when the user has no access' do
it 'returns a status code 404' do
user1 = create(:user)
@@ -91,6 +104,20 @@ describe API::API, api: true do
end
end
+ context 'on a snippet' do
+ let(:snippet) { create(:project_snippet, :public, project: project) }
+ let!(:award) { create(:award_emoji, awardable: snippet) }
+
+ it 'returns the awarded emoji' do
+ get api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(award.name)
+ expect(json_response['awardable_id']).to eq(snippet.id)
+ expect(json_response['awardable_type']).to eq("Snippet")
+ end
+ end
+
context 'when the user has no access' do
it 'returns a status code 404' do
user1 = create(:user)
@@ -160,6 +187,18 @@ describe API::API, api: true do
end
end
end
+
+ context 'on a snippet' do
+ it 'creates a new award emoji' do
+ snippet = create(:project_snippet, :public, project: project)
+
+ post api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user), name: 'blowfish'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['name']).to eq('blowfish')
+ expect(json_response['user']['username']).to eq(user.username)
+ end
+ end
end
describe "POST /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji" do
@@ -229,6 +268,19 @@ describe API::API, api: true do
expect(response).to have_http_status(404)
end
end
+
+ context 'when the awardable is a Snippet' do
+ let(:snippet) { create(:project_snippet, :public, project: project) }
+ let!(:award) { create(:award_emoji, awardable: snippet, user: user) }
+
+ it 'deletes the award' do
+ expect do
+ delete api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user)
+ end.to change { snippet.award_emoji.count }.from(1).to(0)
+
+ expect(response).to have_http_status(200)
+ end
+ end
end
describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_emoji_id' do
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 5b3dc60aba2..10f772c5b1a 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -110,7 +110,7 @@ describe API::API, api: true do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
expect(response).to have_http_status(200)
- expect(json_response['status']).to be_nil
+ expect(json_response['status']).to eq("created")
end
end
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 2d1213df8a7..050d0dd082d 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -5,6 +5,21 @@ describe API::API, api: true do
let(:user) { create(:user) }
let!(:project) { create(:project, namespace: user.namespace ) }
let(:file_path) { 'files/ruby/popen.rb' }
+ let(:author_email) { FFaker::Internet.email }
+
+ # I have to remove periods from the end of the name
+ # This happened when the user's name had a suffix (i.e. "Sr.")
+ # This seems to be what git does under the hood. For example, this commit:
+ #
+ # $ git commit --author='Foo Sr. <foo@example.com>' -m 'Where's my trailing period?'
+ #
+ # results in this:
+ #
+ # $ git show --pretty
+ # ...
+ # Author: Foo Sr <foo@example.com>
+ # ...
+ let(:author_name) { FFaker::Name.name.chomp("\.") }
before { project.team << [user, :developer] }
@@ -16,6 +31,7 @@ describe API::API, api: true do
}
get api("/projects/#{project.id}/repository/files", user), params
+
expect(response).to have_http_status(200)
expect(json_response['file_path']).to eq(file_path)
expect(json_response['file_name']).to eq('popen.rb')
@@ -25,6 +41,7 @@ describe API::API, api: true do
it "returns a 400 bad request if no params given" do
get api("/projects/#{project.id}/repository/files", user)
+
expect(response).to have_http_status(400)
end
@@ -35,6 +52,7 @@ describe API::API, api: true do
}
get api("/projects/#{project.id}/repository/files", user), params
+
expect(response).to have_http_status(404)
end
end
@@ -51,12 +69,17 @@ describe API::API, api: true do
it "creates a new file in project repo" do
post api("/projects/#{project.id}/repository/files", user), valid_params
+
expect(response).to have_http_status(201)
expect(json_response['file_path']).to eq('newfile.rb')
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(user.email)
+ expect(last_commit.author_name).to eq(user.name)
end
it "returns a 400 bad request if no params given" do
post api("/projects/#{project.id}/repository/files", user)
+
expect(response).to have_http_status(400)
end
@@ -65,8 +88,22 @@ describe API::API, api: true do
and_return(false)
post api("/projects/#{project.id}/repository/files", user), valid_params
+
expect(response).to have_http_status(400)
end
+
+ context "when specifying an author" do
+ it "creates a new file with the specified author" do
+ valid_params.merge!(author_email: author_email, author_name: author_name)
+
+ post api("/projects/#{project.id}/repository/files", user), valid_params
+
+ expect(response).to have_http_status(201)
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(author_email)
+ expect(last_commit.author_name).to eq(author_name)
+ end
+ end
end
describe "PUT /projects/:id/repository/files" do
@@ -81,14 +118,32 @@ describe API::API, api: true do
it "updates existing file in project repo" do
put api("/projects/#{project.id}/repository/files", user), valid_params
+
expect(response).to have_http_status(200)
expect(json_response['file_path']).to eq(file_path)
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(user.email)
+ expect(last_commit.author_name).to eq(user.name)
end
it "returns a 400 bad request if no params given" do
put api("/projects/#{project.id}/repository/files", user)
+
expect(response).to have_http_status(400)
end
+
+ context "when specifying an author" do
+ it "updates a file with the specified author" do
+ valid_params.merge!(author_email: author_email, author_name: author_name, content: "New content")
+
+ put api("/projects/#{project.id}/repository/files", user), valid_params
+
+ expect(response).to have_http_status(200)
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(author_email)
+ expect(last_commit.author_name).to eq(author_name)
+ end
+ end
end
describe "DELETE /projects/:id/repository/files" do
@@ -102,12 +157,17 @@ describe API::API, api: true do
it "deletes existing file in project repo" do
delete api("/projects/#{project.id}/repository/files", user), valid_params
+
expect(response).to have_http_status(200)
expect(json_response['file_path']).to eq(file_path)
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(user.email)
+ expect(last_commit.author_name).to eq(user.name)
end
it "returns a 400 bad request if no params given" do
delete api("/projects/#{project.id}/repository/files", user)
+
expect(response).to have_http_status(400)
end
@@ -115,8 +175,22 @@ describe API::API, api: true do
allow_any_instance_of(Repository).to receive(:remove_file).and_return(false)
delete api("/projects/#{project.id}/repository/files", user), valid_params
+
expect(response).to have_http_status(400)
end
+
+ context "when specifying an author" do
+ it "removes a file with the specified author" do
+ valid_params.merge!(author_email: author_email, author_name: author_name)
+
+ delete api("/projects/#{project.id}/repository/files", user), valid_params
+
+ expect(response).to have_http_status(200)
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(author_email)
+ expect(last_commit.author_name).to eq(author_name)
+ end
+ end
end
describe "POST /projects/:id/repository/files with binary file" do
@@ -143,6 +217,7 @@ describe API::API, api: true do
it "remains unchanged" do
get api("/projects/#{project.id}/repository/files", user), get_params
+
expect(response).to have_http_status(200)
expect(json_response['file_path']).to eq(file_path)
expect(json_response['file_name']).to eq(file_path)
diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb
index 06e3a2183c0..34f84f78952 100644
--- a/spec/requests/api/fork_spec.rb
+++ b/spec/requests/api/fork_spec.rb
@@ -94,7 +94,7 @@ describe API::API, api: true do
it 'fails if trying to fork to another user when not admin' do
post api("/projects/fork/#{project.id}", user2), namespace: admin.namespace.id
- expect(response).to have_http_status(409)
+ expect(response).to have_http_status(404)
end
it 'fails if trying to fork to non-existent namespace' do
@@ -114,7 +114,7 @@ describe API::API, api: true do
it 'fails to fork to not owned group' do
post api("/projects/fork/#{project.id}", user2), namespace: group.name
- expect(response).to have_http_status(409)
+ expect(response).to have_http_status(404)
end
it 'forks to not owned group when admin' do
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 4860b23c2ed..1f68ef1af8f 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -120,10 +120,11 @@ describe API::API, api: true do
context 'when authenticated as the group owner' do
it 'updates the group' do
- put api("/groups/#{group1.id}", user1), name: new_group_name
+ put api("/groups/#{group1.id}", user1), name: new_group_name, request_access_enabled: true
expect(response).to have_http_status(200)
expect(json_response['name']).to eq(new_group_name)
+ expect(json_response['request_access_enabled']).to eq(true)
end
it 'returns 404 for a non existing group' do
@@ -238,8 +239,14 @@ describe API::API, api: true do
context "when authenticated as user with group permissions" do
it "creates group" do
- post api("/groups", user3), attributes_for(:group)
+ group = attributes_for(:group, { request_access_enabled: false })
+
+ post api("/groups", user3), group
expect(response).to have_http_status(201)
+
+ expect(json_response["name"]).to eq(group[:name])
+ expect(json_response["path"]).to eq(group[:path])
+ expect(json_response["request_access_enabled"]).to eq(group[:request_access_enabled])
end
it "does not create group, duplicate" do
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 46d1b868782..46e8e6f1169 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -100,6 +100,43 @@ describe API::API, api: true do
end
end
+ describe "POST /internal/lfs_authenticate" do
+ before do
+ project.team << [user, :developer]
+ end
+
+ context 'user key' do
+ it 'returns the correct information about the key' do
+ lfs_auth(key.id, project)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['username']).to eq(user.username)
+ expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).value)
+
+ expect(json_response['repository_http_path']).to eq(project.http_url_to_repo)
+ end
+
+ it 'returns a 404 when the wrong key is provided' do
+ lfs_auth(nil, project)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'deploy key' do
+ let(:key) { create(:deploy_key) }
+
+ it 'returns the correct information about the key' do
+ lfs_auth(key.id, project)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['username']).to eq("lfs+deploy-key-#{key.id}")
+ expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).value)
+ expect(json_response['repository_http_path']).to eq(project.http_url_to_repo)
+ end
+ end
+ end
+
describe "GET /internal/discover" do
it do
get(api("/internal/discover"), key_id: key.id, secret_token: secret_token)
@@ -389,4 +426,13 @@ describe API::API, api: true do
protocol: 'ssh'
)
end
+
+ def lfs_auth(key_id, project)
+ post(
+ api("/internal/lfs_authenticate"),
+ key_id: key_id,
+ secret_token: secret_token,
+ project: project.path_with_namespace
+ )
+ end
end
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 1e365bf353a..92032f09b17 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -30,20 +30,29 @@ describe API::Members, api: true do
let(:route) { get api("/#{source_type.pluralize}/#{source.id}/members", stranger) }
end
- context 'when authenticated as a non-member' do
- %i[access_requester stranger].each do |type|
- context "as a #{type}" do
- it 'returns 200' do
- user = public_send(type)
- get api("/#{source_type.pluralize}/#{source.id}/members", user)
+ %i[master developer access_requester stranger].each do |type|
+ context "when authenticated as a #{type}" do
+ it 'returns 200' do
+ user = public_send(type)
+ get api("/#{source_type.pluralize}/#{source.id}/members", user)
- expect(response).to have_http_status(200)
- expect(json_response.size).to eq(2)
- end
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(2)
+ expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id]
end
end
end
+ it 'does not return invitees' do
+ create(:"#{source_type}_member", invite_token: '123', invite_email: 'test@abc.com', source: source, user: nil)
+
+ get api("/#{source_type.pluralize}/#{source.id}/members", developer)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(2)
+ expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id]
+ end
+
it 'finds members with query string' do
get api("/#{source_type.pluralize}/#{source.id}/members", developer), query: master.username
diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb
index d6a0c656e74..b89dac01040 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/requests/api/milestones_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
- let!(:project) { create(:project, namespace: user.namespace ) }
+ let!(:project) { create(:empty_project, namespace: user.namespace ) }
let!(:closed_milestone) { create(:closed_milestone, project: project) }
let!(:milestone) { create(:milestone, project: project) }
@@ -12,6 +12,7 @@ describe API::API, api: true do
describe 'GET /projects/:id/milestones' do
it 'returns project milestones' do
get api("/projects/#{project.id}/milestones", user)
+
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(milestone.title)
@@ -19,6 +20,7 @@ describe API::API, api: true do
it 'returns a 401 error if user not authenticated' do
get api("/projects/#{project.id}/milestones")
+
expect(response).to have_http_status(401)
end
@@ -44,6 +46,7 @@ describe API::API, api: true do
describe 'GET /projects/:id/milestones/:milestone_id' do
it 'returns a project milestone by id' do
get api("/projects/#{project.id}/milestones/#{milestone.id}", user)
+
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(milestone.title)
expect(json_response['iid']).to eq(milestone.iid)
@@ -60,11 +63,13 @@ describe API::API, api: true do
it 'returns 401 error if user not authenticated' do
get api("/projects/#{project.id}/milestones/#{milestone.id}")
+
expect(response).to have_http_status(401)
end
it 'returns a 404 error if milestone id not found' do
get api("/projects/#{project.id}/milestones/1234", user)
+
expect(response).to have_http_status(404)
end
end
@@ -72,6 +77,7 @@ describe API::API, api: true do
describe 'POST /projects/:id/milestones' do
it 'creates a new project milestone' do
post api("/projects/#{project.id}/milestones", user), title: 'new milestone'
+
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new milestone')
expect(json_response['description']).to be_nil
@@ -80,6 +86,7 @@ describe API::API, api: true do
it 'creates a new project milestone with description and due date' do
post api("/projects/#{project.id}/milestones", user),
title: 'new milestone', description: 'release', due_date: '2013-03-02'
+
expect(response).to have_http_status(201)
expect(json_response['description']).to eq('release')
expect(json_response['due_date']).to eq('2013-03-02')
@@ -87,6 +94,14 @@ describe API::API, api: true do
it 'returns a 400 error if title is missing' do
post api("/projects/#{project.id}/milestones", user)
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns a 400 error if params are invalid (duplicate title)' do
+ post api("/projects/#{project.id}/milestones", user),
+ title: milestone.title, description: 'release', due_date: '2013-03-02'
+
expect(response).to have_http_status(400)
end
end
@@ -95,6 +110,7 @@ describe API::API, api: true do
it 'updates a project milestone' do
put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
title: 'updated title'
+
expect(response).to have_http_status(200)
expect(json_response['title']).to eq('updated title')
end
@@ -102,6 +118,7 @@ describe API::API, api: true do
it 'returns a 404 error if milestone id not found' do
put api("/projects/#{project.id}/milestones/1234", user),
title: 'updated title'
+
expect(response).to have_http_status(404)
end
end
@@ -131,6 +148,7 @@ describe API::API, api: true do
end
it 'returns project issues for a particular milestone' do
get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user)
+
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['milestone']['title']).to eq(milestone.title)
@@ -138,11 +156,12 @@ describe API::API, api: true do
it 'returns a 401 error if user not authenticated' do
get api("/projects/#{project.id}/milestones/#{milestone.id}/issues")
+
expect(response).to have_http_status(401)
end
describe 'confidential issues' do
- let(:public_project) { create(:project, :public) }
+ let(:public_project) { create(:empty_project, :public) }
let(:milestone) { create(:milestone, project: public_project) }
let(:issue) { create(:issue, project: public_project) }
let(:confidential_issue) { create(:issue, confidential: true, project: public_project) }
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 223444ea39f..063a8706e76 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -220,6 +220,15 @@ describe API::API, api: true do
expect(Time.parse(json_response['created_at'])).to be_within(1.second).of(creation_time)
end
end
+
+ context 'when the user is posting an award emoji' do
+ it 'returns an award emoji' do
+ post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: ':+1:'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['awardable_id']).to eq issue.id
+ end
+ end
end
context "when noteable is a Snippet" do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 28aa56e8644..192c7d14c13 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -225,7 +225,8 @@ describe API::API, api: true do
issues_enabled: false,
merge_requests_enabled: false,
wiki_enabled: false,
- only_allow_merge_if_build_succeeds: false
+ only_allow_merge_if_build_succeeds: false,
+ request_access_enabled: true
})
post api('/projects', user), project
@@ -352,7 +353,8 @@ describe API::API, api: true do
description: FFaker::Lorem.sentence,
issues_enabled: false,
merge_requests_enabled: false,
- wiki_enabled: false
+ wiki_enabled: false,
+ request_access_enabled: true
})
post api("/projects/user/#{user.id}", admin), project
@@ -887,6 +889,15 @@ describe API::API, api: true do
expect(json_response['message']['name']).to eq(['has already been taken'])
end
+ it 'updates request_access_enabled' do
+ project_param = { request_access_enabled: false }
+
+ put api("/projects/#{project.id}", user), project_param
+
+ expect(response).to have_http_status(200)
+ expect(json_response['request_access_enabled']).to eq(false)
+ end
+
it 'updates path & name to existing path & name in different namespace' do
project_param = { path: project4.path, name: project4.name }
put api("/projects/#{project3.id}", user), project_param
@@ -948,7 +959,8 @@ describe API::API, api: true do
wiki_enabled: true,
snippets_enabled: true,
merge_requests_enabled: true,
- description: 'new description' }
+ description: 'new description',
+ request_access_enabled: true }
put api("/projects/#{project.id}", user3), project_param
expect(response).to have_http_status(403)
end
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index 780bd7f2859..df97f1bf7b6 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -254,7 +254,8 @@ describe Ci::API::API do
let(:get_url) { ci_api("/builds/#{build.id}/artifacts") }
let(:jwt_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:headers) { { "GitLab-Workhorse" => "1.0", Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt_token } }
- let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token) }
+ let(:token) { build.token }
+ let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => token) }
before { build.run! }
@@ -262,6 +263,7 @@ describe Ci::API::API do
context "should authorize posting artifact to running build" do
it "using token as parameter" do
post authorize_url, { token: build.token }, headers
+
expect(response).to have_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response["TempPath"]).not_to be_nil
@@ -269,6 +271,15 @@ describe Ci::API::API do
it "using token as header" do
post authorize_url, {}, headers_with_token
+
+ expect(response).to have_http_status(200)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(json_response["TempPath"]).not_to be_nil
+ end
+
+ it "using runners token" do
+ post authorize_url, { token: build.project.runners_token }, headers
+
expect(response).to have_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response["TempPath"]).not_to be_nil
@@ -276,7 +287,9 @@ describe Ci::API::API do
it "reject requests that did not go through gitlab-workhorse" do
headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
+
post authorize_url, { token: build.token }, headers
+
expect(response).to have_http_status(500)
end
end
@@ -284,13 +297,17 @@ describe Ci::API::API do
context "should fail to post too large artifact" do
it "using token as parameter" do
stub_application_setting(max_artifacts_size: 0)
+
post authorize_url, { token: build.token, filesize: 100 }, headers
+
expect(response).to have_http_status(413)
end
it "using token as header" do
stub_application_setting(max_artifacts_size: 0)
+
post authorize_url, { filesize: 100 }, headers_with_token
+
expect(response).to have_http_status(413)
end
end
@@ -358,6 +375,16 @@ describe Ci::API::API do
it_behaves_like 'successful artifacts upload'
end
+
+ context 'when using runners token' do
+ let(:token) { build.project.runners_token }
+
+ before do
+ upload_artifacts(file_upload, headers_with_token)
+ end
+
+ it_behaves_like 'successful artifacts upload'
+ end
end
context 'posts artifacts file and metadata file' do
@@ -497,19 +524,40 @@ describe Ci::API::API do
before do
delete delete_url, token: build.token
- build.reload
end
- it 'removes build artifacts' do
- expect(response).to have_http_status(200)
- expect(build.artifacts_file.exists?).to be_falsy
- expect(build.artifacts_metadata.exists?).to be_falsy
- expect(build.artifacts_size).to be_nil
+ shared_examples 'having removable artifacts' do
+ it 'removes build artifacts' do
+ build.reload
+
+ expect(response).to have_http_status(200)
+ expect(build.artifacts_file.exists?).to be_falsy
+ expect(build.artifacts_metadata.exists?).to be_falsy
+ expect(build.artifacts_size).to be_nil
+ end
+ end
+
+ context 'when using build token' do
+ before do
+ delete delete_url, token: build.token
+ end
+
+ it_behaves_like 'having removable artifacts'
+ end
+
+ context 'when using runnners token' do
+ before do
+ delete delete_url, token: build.project.runners_token
+ end
+
+ it_behaves_like 'having removable artifacts'
end
end
describe 'GET /builds/:id/artifacts' do
- before { get get_url, token: build.token }
+ before do
+ get get_url, token: token
+ end
context 'build has artifacts' do
let(:build) { create(:ci_build, :artifacts) }
@@ -518,13 +566,29 @@ describe Ci::API::API do
'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
end
- it 'downloads artifact' do
- expect(response).to have_http_status(200)
- expect(response.headers).to include download_headers
+ shared_examples 'having downloadable artifacts' do
+ it 'download artifacts' do
+ expect(response).to have_http_status(200)
+ expect(response.headers).to include download_headers
+ end
+ end
+
+ context 'when using build token' do
+ let(:token) { build.token }
+
+ it_behaves_like 'having downloadable artifacts'
+ end
+
+ context 'when using runnners token' do
+ let(:token) { build.project.runners_token }
+
+ it_behaves_like 'having downloadable artifacts'
end
end
context 'build does not has artifacts' do
+ let(:token) { build.token }
+
it 'responds with not found' do
expect(response).to have_http_status(404)
end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index b7001fede40..74516686921 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -300,25 +300,79 @@ describe 'Git HTTP requests', lib: true do
end
context "when a gitlab ci token is provided" do
- let(:token) { 123 }
- let(:project) { FactoryGirl.create :empty_project }
+ let(:build) { create(:ci_build, :running) }
+ let(:project) { build.project }
+ let(:other_project) { create(:empty_project) }
before do
- project.update_attributes(runners_token: token)
project.project_feature.update_attributes(builds_access_level: ProjectFeature::ENABLED)
end
- it "downloads get status 200" do
- clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token
+ context 'when build created by system is authenticated' do
+ it "downloads get status 200" do
+ clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
- expect(response).to have_http_status(200)
- expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(response).to have_http_status(200)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ end
+
+ it "uploads get status 401 (no project existence information leak)" do
+ push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
+
+ expect(response).to have_http_status(401)
+ end
+
+ it "downloads from other project get status 404" do
+ clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
+
+ expect(response).to have_http_status(404)
+ end
end
- it "uploads get status 401 (no project existence information leak)" do
- push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token
+ context 'and build created by' do
+ before do
+ build.update(user: user)
+ project.team << [user, :reporter]
+ end
- expect(response).to have_http_status(401)
+ shared_examples 'can download code only' do
+ it 'downloads get status 200' do
+ clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
+
+ expect(response).to have_http_status(200)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ end
+
+ it 'uploads get status 403' do
+ push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'administrator' do
+ let(:user) { create(:admin) }
+
+ it_behaves_like 'can download code only'
+
+ it 'downloads from other project get status 403' do
+ clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'regular user' do
+ let(:user) { create(:user) }
+
+ it_behaves_like 'can download code only'
+
+ it 'downloads from other project get status 404' do
+ clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
+
+ expect(response).to have_http_status(404)
+ end
+ end
end
end
end
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index fc42b534dca..6b956e63004 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -22,11 +22,13 @@ describe JwtController do
context 'when using authorized request' do
context 'using CI token' do
- let(:project) { create(:empty_project, runners_token: 'token') }
- let(:headers) { { authorization: credentials('gitlab-ci-token', project.runners_token) } }
+ let(:build) { create(:ci_build, :running) }
+ let(:project) { build.project }
+ let(:headers) { { authorization: credentials('gitlab-ci-token', build.token) } }
context 'project with enabled CI' do
subject! { get '/jwt/auth', parameters, headers }
+
it { expect(service_class).to have_received(:new).with(project, nil, parameters) }
end
@@ -43,13 +45,31 @@ describe JwtController do
context 'using User login' do
let(:user) { create(:user) }
- let(:headers) { { authorization: credentials('user', 'password') } }
-
- before { expect(Gitlab::Auth).to receive(:find_with_user_password).with('user', 'password').and_return(user) }
+ let(:headers) { { authorization: credentials(user.username, user.password) } }
subject! { get '/jwt/auth', parameters, headers }
it { expect(service_class).to have_received(:new).with(nil, user, parameters) }
+
+ context 'when user has 2FA enabled' do
+ let(:user) { create(:user, :two_factor) }
+
+ context 'without personal token' do
+ it 'rejects the authorization attempt' do
+ expect(response).to have_http_status(401)
+ expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
+ end
+ end
+
+ context 'with personal token' do
+ let(:access_token) { create(:personal_access_token, user: user) }
+ let(:headers) { { authorization: credentials(user.username, access_token.token) } }
+
+ it 'rejects the authorization attempt' do
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
end
context 'using invalid login' do
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 6e551bb65fa..09e4e265dd1 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -14,6 +14,7 @@ describe 'Git LFS API and storage' do
end
let(:authorization) { }
let(:sendfile) { }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:sample_oid) { lfs_object.oid }
let(:sample_size) { lfs_object.size }
@@ -244,15 +245,76 @@ describe 'Git LFS API and storage' do
end
end
- context 'when CI is authorized' do
- let(:authorization) { authorize_ci_project }
+ context 'when deploy key is authorized' do
+ let(:key) { create(:deploy_key) }
+ let(:authorization) { authorize_deploy_key }
let(:update_permissions) do
+ project.deploy_keys << key
project.lfs_objects << lfs_object
end
it_behaves_like 'responds with a file'
end
+
+ context 'when build is authorized as' do
+ let(:authorization) { authorize_ci_project }
+
+ shared_examples 'can download LFS only from own projects' do
+ context 'for own project' do
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+ let(:update_permissions) do
+ project.team << [user, :reporter]
+ project.lfs_objects << lfs_object
+ end
+
+ it_behaves_like 'responds with a file'
+ end
+
+ context 'for other project' do
+ let(:other_project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
+
+ let(:update_permissions) do
+ project.lfs_objects << lfs_object
+ end
+
+ it 'rejects downloading code' do
+ expect(response).to have_http_status(other_project_status)
+ end
+ end
+ end
+
+ context 'administrator' do
+ let(:user) { create(:admin) }
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+ it_behaves_like 'can download LFS only from own projects' do
+ # We render 403, because administrator does have normally access
+ let(:other_project_status) { 403 }
+ end
+ end
+
+ context 'regular user' do
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+ it_behaves_like 'can download LFS only from own projects' do
+ # We render 404, to prevent data leakage about existence of the project
+ let(:other_project_status) { 404 }
+ end
+ end
+
+ context 'does not have user' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline) }
+
+ it_behaves_like 'can download LFS only from own projects' do
+ # We render 404, to prevent data leakage about existence of the project
+ let(:other_project_status) { 404 }
+ end
+ end
+ end
end
context 'without required headers' do
@@ -431,10 +493,62 @@ describe 'Git LFS API and storage' do
end
end
- context 'when CI is authorized' do
+ context 'when build is authorized as' do
let(:authorization) { authorize_ci_project }
- it_behaves_like 'an authorized requests'
+ let(:update_lfs_permissions) do
+ project.lfs_objects << lfs_object
+ end
+
+ shared_examples 'can download LFS only from own projects' do
+ context 'for own project' do
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+ let(:update_user_permissions) do
+ project.team << [user, :reporter]
+ end
+
+ it_behaves_like 'an authorized requests'
+ end
+
+ context 'for other project' do
+ let(:other_project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
+
+ it 'rejects downloading code' do
+ expect(response).to have_http_status(other_project_status)
+ end
+ end
+ end
+
+ context 'administrator' do
+ let(:user) { create(:admin) }
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+ it_behaves_like 'can download LFS only from own projects' do
+ # We render 403, because administrator does have normally access
+ let(:other_project_status) { 403 }
+ end
+ end
+
+ context 'regular user' do
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+ it_behaves_like 'can download LFS only from own projects' do
+ # We render 404, to prevent data leakage about existence of the project
+ let(:other_project_status) { 404 }
+ end
+ end
+
+ context 'does not have user' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline) }
+
+ it_behaves_like 'can download LFS only from own projects' do
+ # We render 404, to prevent data leakage about existence of the project
+ let(:other_project_status) { 404 }
+ end
+ end
end
context 'when user is not authenticated' do
@@ -583,11 +697,37 @@ describe 'Git LFS API and storage' do
end
end
- context 'when CI is authorized' do
+ context 'when build is authorized' do
let(:authorization) { authorize_ci_project }
- it 'responds with 401' do
- expect(response).to have_http_status(401)
+ context 'build has an user' do
+ let(:user) { create(:user) }
+
+ context 'tries to push to own project' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+ it 'responds with 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'tries to push to other project' do
+ let(:other_project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+ it 'responds with 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ context 'does not have user' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline) }
+
+ it 'responds with 401' do
+ expect(response).to have_http_status(401)
+ end
end
end
end
@@ -609,14 +749,6 @@ describe 'Git LFS API and storage' do
end
end
end
-
- context 'when CI is authorized' do
- let(:authorization) { authorize_ci_project }
-
- it 'responds with status 403' do
- expect(response).to have_http_status(401)
- end
- end
end
describe 'unsupported' do
@@ -779,10 +911,51 @@ describe 'Git LFS API and storage' do
end
end
- context 'when CI is authenticated' do
+ context 'when build is authorized' do
let(:authorization) { authorize_ci_project }
- it_behaves_like 'unauthorized'
+ context 'build has an user' do
+ let(:user) { create(:user) }
+
+ context 'tries to push to own project' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+ before do
+ project.team << [user, :developer]
+ put_authorize
+ end
+
+ it 'responds with 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'tries to push to other project' do
+ let(:other_project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+ before do
+ put_authorize
+ end
+
+ it 'responds with 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ context 'does not have user' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline) }
+
+ before do
+ put_authorize
+ end
+
+ it 'responds with 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
end
context 'for unauthenticated' do
@@ -839,10 +1012,42 @@ describe 'Git LFS API and storage' do
end
end
- context 'when CI is authenticated' do
+ context 'when build is authorized' do
let(:authorization) { authorize_ci_project }
- it_behaves_like 'unauthorized'
+ before do
+ put_authorize
+ end
+
+ context 'build has an user' do
+ let(:user) { create(:user) }
+
+ context 'tries to push to own project' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+ it 'responds with 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'tries to push to other project' do
+ let(:other_project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+ it 'responds with 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ context 'does not have user' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline) }
+
+ it 'responds with 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
end
context 'for unauthenticated' do
@@ -897,13 +1102,17 @@ describe 'Git LFS API and storage' do
end
def authorize_ci_project
- ActionController::HttpAuthentication::Basic.encode_credentials('gitlab-ci-token', project.runners_token)
+ ActionController::HttpAuthentication::Basic.encode_credentials('gitlab-ci-token', build.token)
end
def authorize_user
ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
end
+ def authorize_deploy_key
+ ActionController::HttpAuthentication::Basic.encode_credentials("lfs+deploy-key-#{key.id}", Gitlab::LfsToken.new(key).generate)
+ end
+
def fork_project(project, user, object = nil)
allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
Projects::ForkService.new(project, user, {}).execute
diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb
index 7cc71f706ce..c64df4979b0 100644
--- a/spec/services/auth/container_registry_authentication_service_spec.rb
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -6,8 +6,14 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
let(:current_params) { {} }
let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) }
let(:payload) { JWT.decode(subject[:token], rsa_key).first }
+ let(:authentication_abilities) do
+ [
+ :read_container_image,
+ :create_container_image
+ ]
+ end
- subject { described_class.new(current_project, current_user, current_params).execute }
+ subject { described_class.new(current_project, current_user, current_params).execute(authentication_abilities: authentication_abilities) }
before do
allow(Gitlab.config.registry).to receive_messages(enabled: true, issuer: 'rspec', key: nil)
@@ -189,13 +195,22 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end
end
- context 'project authorization' do
+ context 'build authorized as user' do
let(:current_project) { create(:empty_project) }
+ let(:current_user) { create(:user) }
+ let(:authentication_abilities) do
+ [
+ :build_read_container_image,
+ :build_create_container_image
+ ]
+ end
- context 'allow to use scope-less authentication' do
- it_behaves_like 'a valid token'
+ before do
+ current_project.team << [current_user, :developer]
end
+ it_behaves_like 'a valid token'
+
context 'allow to pull and push images' do
let(:current_params) do
{ scope: "repository:#{current_project.path_with_namespace}:pull,push" }
@@ -214,12 +229,44 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
context 'allow for public' do
let(:project) { create(:empty_project, :public) }
+
it_behaves_like 'a pullable'
end
- context 'disallow for private' do
+ shared_examples 'pullable for being team member' do
+ context 'when you are not member' do
+ it_behaves_like 'an inaccessible'
+ end
+
+ context 'when you are member' do
+ before do
+ project.team << [current_user, :developer]
+ end
+
+ it_behaves_like 'a pullable'
+ end
+ end
+
+ context 'for private' do
let(:project) { create(:empty_project, :private) }
- it_behaves_like 'an inaccessible'
+
+ it_behaves_like 'pullable for being team member'
+
+ context 'when you are admin' do
+ let(:current_user) { create(:admin) }
+
+ context 'when you are not member' do
+ it_behaves_like 'an inaccessible'
+ end
+
+ context 'when you are member' do
+ before do
+ project.team << [current_user, :developer]
+ end
+
+ it_behaves_like 'a pullable'
+ end
+ end
end
end
@@ -230,6 +277,11 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
context 'disallow for all' do
let(:project) { create(:empty_project, :public) }
+
+ before do
+ project.team << [current_user, :developer]
+ end
+
it_behaves_like 'an inaccessible'
end
end
diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb
index 8da2a2b3c1b..343b4385bf2 100644
--- a/spec/services/create_deployment_service_spec.rb
+++ b/spec/services/create_deployment_service_spec.rb
@@ -41,7 +41,7 @@ describe CreateDeploymentService, services: true do
context 'for environment with invalid name' do
let(:params) do
- { environment: 'name with spaces',
+ { environment: 'name,with,commas',
ref: 'master',
tag: false,
sha: '97de212e80737a608d939f648d959671fb0a0142',
@@ -56,8 +56,36 @@ describe CreateDeploymentService, services: true do
expect(subject).not_to be_persisted
end
end
+
+ context 'when variables are used' do
+ let(:params) do
+ { environment: 'review-apps/$CI_BUILD_REF_NAME',
+ ref: 'master',
+ tag: false,
+ sha: '97de212e80737a608d939f648d959671fb0a0142',
+ options: {
+ name: 'review-apps/$CI_BUILD_REF_NAME',
+ url: 'http://$CI_BUILD_REF_NAME.review-apps.gitlab.com'
+ },
+ variables: [
+ { key: 'CI_BUILD_REF_NAME', value: 'feature-review-apps' }
+ ]
+ }
+ end
+
+ it 'does create a new environment' do
+ expect { subject }.to change { Environment.count }.by(1)
+
+ expect(subject.environment.name).to eq('review-apps/feature-review-apps')
+ expect(subject.environment.external_url).to eq('http://feature-review-apps.review-apps.gitlab.com')
+ end
+
+ it 'does create a new deployment' do
+ expect(subject).to be_persisted
+ end
+ end
end
-
+
describe 'processing of builds' do
let(:environment) { nil }
@@ -95,6 +123,12 @@ describe CreateDeploymentService, services: true do
expect(Deployment.last.deployable).to eq(deployable)
end
+
+ it 'create environment has URL set' do
+ subject
+
+ expect(Deployment.last.environment.external_url).not_to be_nil
+ end
end
context 'without environment specified' do
@@ -107,7 +141,10 @@ describe CreateDeploymentService, services: true do
context 'when environment is specified' do
let(:pipeline) { create(:ci_pipeline, project: project) }
- let(:build) { create(:ci_build, pipeline: pipeline, environment: 'production') }
+ let(:build) { create(:ci_build, pipeline: pipeline, environment: 'production', options: options) }
+ let(:options) do
+ { environment: { name: 'production', url: 'http://gitlab.com' } }
+ end
context 'when build succeeds' do
it_behaves_like 'does create environment and deployment' do
@@ -132,4 +169,83 @@ describe CreateDeploymentService, services: true do
end
end
end
+
+ describe "merge request metrics" do
+ let(:params) do
+ {
+ environment: 'production',
+ ref: 'master',
+ tag: false,
+ sha: '97de212e80737a608d939f648d959671fb0a0142b',
+ }
+ end
+
+ let(:merge_request) { create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: project) }
+
+ context "while updating the 'first_deployed_to_production_at' time" do
+ before { merge_request.mark_as_merged }
+
+ context "for merge requests merged before the current deploy" do
+ it "sets the time if the deploy's environment is 'production'" do
+ time = Time.now
+ Timecop.freeze(time) { service.execute }
+
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_within(1.second).of(time)
+ end
+
+ it "doesn't set the time if the deploy's environment is not 'production'" do
+ staging_params = params.merge(environment: 'staging')
+ service = described_class.new(project, user, staging_params)
+ service.execute
+
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil
+ end
+
+ it 'does not raise errors if the merge request does not have a metrics record' do
+ merge_request.metrics.destroy
+
+ expect(merge_request.reload.metrics).to be_nil
+ expect { service.execute }.not_to raise_error
+ end
+ end
+
+ context "for merge requests merged before the previous deploy" do
+ context "if the 'first_deployed_to_production_at' time is already set" do
+ it "does not overwrite the older 'first_deployed_to_production_at' time" do
+ # Previous deploy
+ time = Time.now
+ Timecop.freeze(time) { service.execute }
+
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_within(1.second).of(time)
+
+ # Current deploy
+ service = described_class.new(project, user, params)
+ Timecop.freeze(time + 12.hours) { service.execute }
+
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_within(1.second).of(time)
+ end
+ end
+
+ context "if the 'first_deployed_to_production_at' time is not already set" do
+ it "does not overwrite the older 'first_deployed_to_production_at' time" do
+ # Previous deploy
+ time = 5.minutes.from_now
+ Timecop.freeze(time) { service.execute }
+
+ expect(merge_request.reload.metrics.merged_at).to be < merge_request.reload.metrics.first_deployed_to_production_at
+
+ merge_request.reload.metrics.update(first_deployed_to_production_at: nil)
+
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil
+
+ # Current deploy
+ service = described_class.new(project, user, params)
+ Timecop.freeze(time + 12.hours) { service.execute }
+
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 6ac1fa8f182..22991c5bc86 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -253,6 +253,21 @@ describe GitPushService, services: true do
expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
end
+ it "when pushing a branch for the first time with an existing branch permission configured" do
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
+
+ create(:protected_branch, :no_one_can_push, :developers_can_merge, project: project, name: 'master')
+ expect(project).to receive(:execute_hooks)
+ expect(project.default_branch).to eq("master")
+ expect_any_instance_of(ProtectedBranches::CreateService).not_to receive(:execute)
+
+ execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
+
+ expect(project.protected_branches).not_to be_empty
+ expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::NO_ACCESS])
+ expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER])
+ end
+
it "when pushing a branch for the first time with default branch protection set to 'developers can merge'" do
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
@@ -324,6 +339,43 @@ describe GitPushService, services: true do
end
end
+ describe "issue metrics" do
+ let(:issue) { create :issue, project: project }
+ let(:commit_author) { create :user }
+ let(:commit) { project.commit }
+ let(:commit_time) { Time.now }
+
+ before do
+ project.team << [commit_author, :developer]
+ project.team << [user, :developer]
+
+ allow(commit).to receive_messages(
+ safe_message: "this commit \n mentions #{issue.to_reference}",
+ references: [issue],
+ author_name: commit_author.name,
+ author_email: commit_author.email,
+ committed_date: commit_time
+ )
+
+ allow(project.repository).to receive(:commits_between).and_return([commit])
+ end
+
+ context "while saving the 'first_mentioned_in_commit_at' metric for an issue" do
+ it 'sets the metric for referenced issues' do
+ execute_service(project, user, @oldrev, @newrev, @ref)
+
+ expect(issue.reload.metrics.first_mentioned_in_commit_at).to be_within(1.second).of(commit_time)
+ end
+
+ it 'does not set the metric for non-referenced issues' do
+ non_referenced_issue = create(:issue, project: project)
+ execute_service(project, user, @oldrev, @newrev, @ref)
+
+ expect(non_referenced_issue.reload.metrics.first_mentioned_in_commit_at).to be_nil
+ end
+ end
+ end
+
describe "closing issues from pushed commits containing a closing reference" do
let(:issue) { create :issue, project: project }
let(:other_issue) { create :issue, project: project }
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index c1e4f8bd96b..b8142889075 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -83,5 +83,34 @@ describe MergeRequests::CreateService, services: true do
}
end
end
+
+ context 'while saving references to issues that the created merge request closes' do
+ let(:first_issue) { create(:issue, project: project) }
+ let(:second_issue) { create(:issue, project: project) }
+
+ let(:opts) do
+ {
+ title: 'Awesome merge_request',
+ source_branch: 'feature',
+ target_branch: 'master',
+ force_remove_source_branch: '1'
+ }
+ end
+
+ before do
+ project.team << [user, :master]
+ project.team << [assignee, :developer]
+ end
+
+ it 'creates a `MergeRequestsClosingIssues` record for each issue' do
+ issue_closing_opts = opts.merge(description: "Closes #{first_issue.to_reference} and #{second_issue.to_reference}")
+ service = described_class.new(project, user, issue_closing_opts)
+ allow(service).to receive(:execute_hooks)
+ merge_request = service.execute
+
+ issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
+ expect(issue_ids).to match_array([first_issue.id, second_issue.id])
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index fff86480c6d..a162df5fc34 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -174,6 +174,58 @@ describe MergeRequests::RefreshService, services: true do
end
end
+ context 'merge request metrics' do
+ let(:issue) { create :issue, project: @project }
+ let(:commit_author) { create :user }
+ let(:commit) { project.commit }
+
+ before do
+ project.team << [commit_author, :developer]
+ project.team << [user, :developer]
+
+ allow(commit).to receive_messages(
+ safe_message: "Closes #{issue.to_reference}",
+ references: [issue],
+ author_name: commit_author.name,
+ author_email: commit_author.email,
+ committed_date: Time.now
+ )
+
+ allow_any_instance_of(MergeRequest).to receive(:commits).and_return([commit])
+ end
+
+ context 'when the merge request is sourced from the same project' do
+ it 'creates a `MergeRequestsClosingIssues` record for each issue closed by a commit' do
+ merge_request = create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: @project)
+ refresh_service = service.new(@project, @user)
+ allow(refresh_service).to receive(:execute_hooks)
+ refresh_service.execute(@oldrev, @newrev, 'refs/heads/feature')
+
+ issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
+ expect(issue_ids).to eq([issue.id])
+ end
+ end
+
+ context 'when the merge request is sourced from a different project' do
+ it 'creates a `MergeRequestsClosingIssues` record for each issue closed by a commit' do
+ forked_project = create(:project)
+ create(:forked_project_link, forked_to_project: forked_project, forked_from_project: @project)
+
+ merge_request = create(:merge_request,
+ target_branch: 'master',
+ source_branch: 'feature',
+ target_project: @project,
+ source_project: forked_project)
+ refresh_service = service.new(@project, @user)
+ allow(refresh_service).to receive(:execute_hooks)
+ refresh_service.execute(@oldrev, @newrev, 'refs/heads/feature')
+
+ issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
+ expect(issue_ids).to eq([issue.id])
+ end
+ end
+ end
+
def reload_mrs
@merge_request.reload
@fork_merge_request.reload
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 6dfeb581975..33db34c0f62 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -263,5 +263,42 @@ describe MergeRequests::UpdateService, services: true do
end
end
end
+
+ context 'while saving references to issues that the updated merge request closes' do
+ let(:first_issue) { create(:issue, project: project) }
+ let(:second_issue) { create(:issue, project: project) }
+
+ it 'creates a `MergeRequestsClosingIssues` record for each issue' do
+ issue_closing_opts = { description: "Closes #{first_issue.to_reference} and #{second_issue.to_reference}" }
+ service = described_class.new(project, user, issue_closing_opts)
+ allow(service).to receive(:execute_hooks)
+ service.execute(merge_request)
+
+ issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
+ expect(issue_ids).to match_array([first_issue.id, second_issue.id])
+ end
+
+ it 'removes `MergeRequestsClosingIssues` records when issues are not closed anymore' do
+ opts = {
+ title: 'Awesome merge_request',
+ description: "Closes #{first_issue.to_reference} and #{second_issue.to_reference}",
+ source_branch: 'feature',
+ target_branch: 'master',
+ force_remove_source_branch: '1'
+ }
+
+ merge_request = MergeRequests::CreateService.new(project, user, opts).execute
+
+ issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
+ expect(issue_ids).to match_array([first_issue.id, second_issue.id])
+
+ service = described_class.new(project, user, description: "not closing any issues")
+ allow(service).to receive(:execute_hooks)
+ service.execute(merge_request.reload)
+
+ issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
+ expect(issue_ids).to be_empty
+ end
+ end
end
end
diff --git a/spec/services/notes/slash_commands_service_spec.rb b/spec/services/notes/slash_commands_service_spec.rb
index 4f231aab161..d1099884a02 100644
--- a/spec/services/notes/slash_commands_service_spec.rb
+++ b/spec/services/notes/slash_commands_service_spec.rb
@@ -122,6 +122,75 @@ describe Notes::SlashCommandsService, services: true do
end
end
+ describe '.noteable_update_service' do
+ include_context 'note on noteable'
+
+ it 'returns Issues::UpdateService for a note on an issue' do
+ note = create(:note_on_issue, project: project)
+
+ expect(described_class.noteable_update_service(note)).to eq(Issues::UpdateService)
+ end
+
+ it 'returns Issues::UpdateService for a note on a merge request' do
+ note = create(:note_on_merge_request, project: project)
+
+ expect(described_class.noteable_update_service(note)).to eq(MergeRequests::UpdateService)
+ end
+
+ it 'returns nil for a note on a commit' do
+ note = create(:note_on_commit, project: project)
+
+ expect(described_class.noteable_update_service(note)).to be_nil
+ end
+ end
+
+ describe '.supported?' do
+ include_context 'note on noteable'
+
+ let(:note) { create(:note_on_issue, project: project) }
+
+ context 'with no current_user' do
+ it 'returns false' do
+ expect(described_class.supported?(note, nil)).to be_falsy
+ end
+ end
+
+ context 'when current_user cannot update the noteable' do
+ it 'returns false' do
+ user = create(:user)
+
+ expect(described_class.supported?(note, user)).to be_falsy
+ end
+ end
+
+ context 'when current_user can update the noteable' do
+ it 'returns true' do
+ expect(described_class.supported?(note, master)).to be_truthy
+ end
+
+ context 'with a note on a commit' do
+ let(:note) { create(:note_on_commit, project: project) }
+
+ it 'returns false' do
+ expect(described_class.supported?(note, nil)).to be_falsy
+ end
+ end
+ end
+ end
+
+ describe '#supported?' do
+ include_context 'note on noteable'
+
+ it 'delegates to the class method' do
+ service = described_class.new(project, master)
+ note = create(:note_on_issue, project: project)
+
+ expect(described_class).to receive(:supported?).with(note, master)
+
+ service.supported?(note)
+ end
+ end
+
describe '#execute' do
let(:service) { described_class.new(project, master) }
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index f81a58899fd..0d152534c38 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -379,6 +379,7 @@ describe NotificationService, services: true do
it "emails subscribers of the issue's labels" do
subscriber = create(:user)
label = create(:label, issues: [issue])
+ issue.reload
label.toggle_subscription(subscriber)
notification.new_issue(issue, @u_disabled)
@@ -399,6 +400,7 @@ describe NotificationService, services: true do
project.team << [guest, :guest]
label = create(:label, issues: [confidential_issue])
+ confidential_issue.reload
label.toggle_subscription(non_member)
label.toggle_subscription(author)
label.toggle_subscription(assignee)
diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb
index c6160f4fa57..cf90b33dfb4 100644
--- a/spec/services/projects/housekeeping_service_spec.rb
+++ b/spec/services/projects/housekeeping_service_spec.rb
@@ -4,6 +4,10 @@ describe Projects::HousekeepingService do
subject { Projects::HousekeepingService.new(project) }
let(:project) { create :project }
+ before do
+ project.reset_pushes_since_gc
+ end
+
after do
project.reset_pushes_since_gc
end
diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb
new file mode 100644
index 00000000000..7d4eff3b6ef
--- /dev/null
+++ b/spec/services/protected_branches/create_service_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe ProtectedBranches::CreateService, services: true do
+ let(:project) { create(:empty_project) }
+ let(:user) { project.owner }
+ let(:params) do
+ {
+ name: 'master',
+ merge_access_levels_attributes: [ { access_level: Gitlab::Access::MASTER } ],
+ push_access_levels_attributes: [ { access_level: Gitlab::Access::MASTER } ]
+ }
+ end
+
+ describe '#execute' do
+ subject(:service) { described_class.new(project, user, params) }
+
+ it 'creates a new protected branch' do
+ expect { service.execute }.to change(ProtectedBranch, :count).by(1)
+ expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
+ expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
+ end
+ end
+end
diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb
new file mode 100644
index 00000000000..e8e760a6187
--- /dev/null
+++ b/spec/support/cycle_analytics_helpers.rb
@@ -0,0 +1,64 @@
+module CycleAnalyticsHelpers
+ def create_commit_referencing_issue(issue, branch_name: random_git_name)
+ project.repository.add_branch(user, branch_name, 'master')
+ create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name)
+ end
+
+ def create_commit(message, project, user, branch_name)
+ filename = random_git_name
+ oldrev = project.repository.commit(branch_name).sha
+
+ options = {
+ committer: project.repository.user_to_committer(user),
+ author: project.repository.user_to_committer(user),
+ commit: { message: message, branch: branch_name, update_ref: true },
+ file: { content: "content", path: filename, update: false }
+ }
+
+ commit_sha = Gitlab::Git::Blob.commit(project.repository, options)
+ project.repository.commit(commit_sha)
+
+ GitPushService.new(project,
+ user,
+ oldrev: oldrev,
+ newrev: commit_sha,
+ ref: 'refs/heads/master').execute
+ end
+
+ def create_merge_request_closing_issue(issue, message: nil, source_branch: nil)
+ if !source_branch || project.repository.commit(source_branch).blank?
+ source_branch = random_git_name
+ project.repository.add_branch(user, source_branch, 'master')
+ end
+
+ sha = project.repository.commit_file(user, random_git_name, "content", "commit message", source_branch, false)
+ project.repository.commit(sha)
+
+ opts = {
+ title: 'Awesome merge_request',
+ description: message || "Fixes #{issue.to_reference}",
+ source_branch: source_branch,
+ target_branch: 'master'
+ }
+
+ MergeRequests::CreateService.new(project, user, opts).execute
+ end
+
+ def merge_merge_requests_closing_issue(issue)
+ merge_requests = issue.closed_by_merge_requests
+ merge_requests.each { |merge_request| MergeRequests::MergeService.new(project, user).execute(merge_request) }
+ end
+
+ def deploy_master(environment: 'production')
+ CreateDeploymentService.new(project, user, {
+ environment: environment,
+ ref: 'master',
+ tag: false,
+ sha: project.repository.commit('master').sha
+ }).execute
+ end
+end
+
+RSpec.configure do |config|
+ config.include CycleAnalyticsHelpers
+end
diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb
new file mode 100644
index 00000000000..8e19a6c92e2
--- /dev/null
+++ b/spec/support/cycle_analytics_helpers/test_generation.rb
@@ -0,0 +1,161 @@
+# rubocop:disable Metrics/AbcSize
+
+# Note: The ABC size is large here because we have a method generating test cases with
+# multiple nested contexts. This shouldn't count as a violation.
+
+module CycleAnalyticsHelpers
+ module TestGeneration
+ # Generate the most common set of specs that all cycle analytics phases need to have.
+ #
+ # Arguments:
+ #
+ # phase: Which phase are we testing? Will call `CycleAnalytics.new.send(phase)` for the final assertion
+ # data_fn: A function that returns a hash, constituting initial data for the test case
+ # start_time_conditions: An array of `conditions`. Each condition is an tuple of `condition_name` and `condition_fn`. `condition_fn` is called with
+ # `context` (no lexical scope, so need to do `context.create` for factories, for example) and `data` (from the `data_fn`).
+ # Each `condition_fn` is expected to implement a case which consitutes the start of the given cycle analytics phase.
+ # end_time_conditions: An array of `conditions`. Each condition is an tuple of `condition_name` and `condition_fn`. `condition_fn` is called with
+ # `context` (no lexical scope, so need to do `context.create` for factories, for example) and `data` (from the `data_fn`).
+ # Each `condition_fn` is expected to implement a case which consitutes the end of the given cycle analytics phase.
+ # before_end_fn: This function is run before calling the end time conditions. Used for setup that needs to be run between the start and end conditions.
+ # post_fn: Code that needs to be run after running the end time conditions.
+
+ def generate_cycle_analytics_spec(phase:, data_fn:, start_time_conditions:, end_time_conditions:, before_end_fn: nil, post_fn: nil)
+ combinations_of_start_time_conditions = (1..start_time_conditions.size).flat_map { |size| start_time_conditions.combination(size).to_a }
+ combinations_of_end_time_conditions = (1..end_time_conditions.size).flat_map { |size| end_time_conditions.combination(size).to_a }
+
+ scenarios = combinations_of_start_time_conditions.product(combinations_of_end_time_conditions)
+ scenarios.each do |start_time_conditions, end_time_conditions|
+ context "start condition: #{start_time_conditions.map(&:first).to_sentence}" do
+ context "end condition: #{end_time_conditions.map(&:first).to_sentence}" do
+ it "finds the median of available durations between the two conditions" do
+ time_differences = Array.new(5) do |index|
+ data = data_fn[self]
+ start_time = (index * 10).days.from_now
+ end_time = start_time + rand(1..5).days
+
+ start_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(start_time) { condition_fn[self, data] }
+ end
+
+ # Run `before_end_fn` at the midpoint between `start_time` and `end_time`
+ Timecop.freeze(start_time + (end_time - start_time) / 2) { before_end_fn[self, data] } if before_end_fn
+
+ end_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(end_time) { condition_fn[self, data] }
+ end
+
+ Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+
+ end_time - start_time
+ end
+
+ median_time_difference = time_differences.sort[2]
+ expect(subject.send(phase)).to be_within(5).of(median_time_difference)
+ end
+
+ context "when the data belongs to another project" do
+ let(:other_project) { create(:project) }
+
+ it "returns nil" do
+ # Use a stub to "trick" the data/condition functions
+ # into using another project. This saves us from having to
+ # define separate data/condition functions for this particular
+ # test case.
+ allow(self).to receive(:project) { other_project }
+
+ 5.times do
+ data = data_fn[self]
+ start_time = Time.now
+ end_time = rand(1..10).days.from_now
+
+ start_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(start_time) { condition_fn[self, data] }
+ end
+
+ end_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(end_time) { condition_fn[self, data] }
+ end
+
+ Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+ end
+
+ # Turn off the stub before checking assertions
+ allow(self).to receive(:project).and_call_original
+
+ expect(subject.send(phase)).to be_nil
+ end
+ end
+
+ context "when the end condition happens before the start condition" do
+ it 'returns nil' do
+ data = data_fn[self]
+ start_time = Time.now
+ end_time = start_time + rand(1..5).days
+
+ # Run `before_end_fn` at the midpoint between `start_time` and `end_time`
+ Timecop.freeze(start_time + (end_time - start_time) / 2) { before_end_fn[self, data] } if before_end_fn
+
+ end_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(start_time) { condition_fn[self, data] }
+ end
+
+ start_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(end_time) { condition_fn[self, data] }
+ end
+
+ Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+
+ expect(subject.send(phase)).to be_nil
+ end
+ end
+ end
+ end
+
+ context "start condition NOT PRESENT: #{start_time_conditions.map(&:first).to_sentence}" do
+ context "end condition: #{end_time_conditions.map(&:first).to_sentence}" do
+ it "returns nil" do
+ 5.times do
+ data = data_fn[self]
+ end_time = rand(1..10).days.from_now
+
+ end_time_conditions.each_with_index do |(condition_name, condition_fn), index|
+ Timecop.freeze(end_time + index.days) { condition_fn[self, data] }
+ end
+
+ Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+ end
+
+ expect(subject.send(phase)).to be_nil
+ end
+ end
+ end
+
+ context "start condition: #{start_time_conditions.map(&:first).to_sentence}" do
+ context "end condition NOT PRESENT: #{end_time_conditions.map(&:first).to_sentence}" do
+ it "returns nil" do
+ 5.times do
+ data = data_fn[self]
+ start_time = Time.now
+
+ start_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(start_time) { condition_fn[self, data] }
+ end
+
+ post_fn[self, data] if post_fn
+ end
+
+ expect(subject.send(phase)).to be_nil
+ end
+ end
+ end
+ end
+
+ context "when none of the start / end conditions are matched" do
+ it "returns nil" do
+ expect(subject.send(phase)).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/git_helpers.rb b/spec/support/git_helpers.rb
new file mode 100644
index 00000000000..93422390ef7
--- /dev/null
+++ b/spec/support/git_helpers.rb
@@ -0,0 +1,9 @@
+module GitHelpers
+ def random_git_name
+ "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}"
+ end
+end
+
+RSpec.configure do |config|
+ config.include GitHelpers
+end
diff --git a/spec/support/import_export/configuration_helper.rb b/spec/support/import_export/configuration_helper.rb
new file mode 100644
index 00000000000..f752508d48c
--- /dev/null
+++ b/spec/support/import_export/configuration_helper.rb
@@ -0,0 +1,29 @@
+module ConfigurationHelper
+ # Returns a list of models from hashes/arrays contained in +project_tree+
+ def names_from_tree(project_tree)
+ project_tree.map do |branch_or_model|
+ branch_or_model = branch_or_model.to_s if branch_or_model.is_a?(Symbol)
+
+ branch_or_model.is_a?(String) ? branch_or_model : names_from_tree(branch_or_model)
+ end
+ end
+
+ def relation_class_for_name(relation_name)
+ relation_name = Gitlab::ImportExport::RelationFactory::OVERRIDES[relation_name.to_sym] || relation_name
+ relation_name.to_s.classify.constantize
+ end
+
+ def parsed_attributes(relation_name, attributes)
+ excluded_attributes = config_hash['excluded_attributes'][relation_name]
+ included_attributes = config_hash['included_attributes'][relation_name]
+
+ attributes = attributes - JSON[excluded_attributes.to_json] if excluded_attributes
+ attributes = attributes & JSON[included_attributes.to_json] if included_attributes
+
+ attributes
+ end
+
+ def associations_for(safe_model)
+ safe_model.reflect_on_all_associations.map { |assoc| assoc.name.to_s }
+ end
+end
diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb
new file mode 100644
index 00000000000..be0772d6a4a
--- /dev/null
+++ b/spec/support/import_export/export_file_helper.rb
@@ -0,0 +1,133 @@
+require './spec/support/import_export/configuration_helper'
+
+module ExportFileHelper
+ include ConfigurationHelper
+
+ ObjectWithParent = Struct.new(:object, :parent, :key_found)
+
+ def setup_project
+ project = create(:project, :public)
+
+ create(:release, project: project)
+
+ issue = create(:issue, assignee: user, project: project)
+ snippet = create(:project_snippet, project: project)
+ label = create(:label, project: project)
+ milestone = create(:milestone, project: project)
+ merge_request = create(:merge_request, source_project: project, milestone: milestone)
+ commit_status = create(:commit_status, project: project)
+
+ create(:label_link, label: label, target: issue)
+
+ ci_pipeline = create(:ci_pipeline,
+ project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch,
+ statuses: [commit_status])
+
+ create(:ci_build, pipeline: ci_pipeline, project: project)
+ create(:milestone, project: project)
+ create(:note, noteable: issue, project: project)
+ create(:note, noteable: merge_request, project: project)
+ create(:note, noteable: snippet, project: project)
+ create(:note_on_commit,
+ author: user,
+ project: project,
+ commit_id: ci_pipeline.sha)
+
+ create(:event, target: milestone, project: project, action: Event::CREATED, author: user)
+ create(:project_member, :master, user: user, project: project)
+ create(:ci_variable, project: project)
+ create(:ci_trigger, project: project)
+ key = create(:deploy_key)
+ key.projects << project
+ create(:service, project: project)
+ create(:project_hook, project: project, token: 'token')
+ create(:protected_branch, project: project)
+
+ project
+ end
+
+ # Expands the compressed file for an exported project into +tmpdir+
+ def in_directory_with_expanded_export(project)
+ Dir.mktmpdir do |tmpdir|
+ export_file = project.export_project_path
+ _output, exit_status = Gitlab::Popen.popen(%W{tar -zxf #{export_file} -C #{tmpdir}})
+
+ yield(exit_status, tmpdir)
+ end
+ end
+
+ # Recursively finds key/values including +key+ as part of the key, inside a nested hash
+ def deep_find_with_parent(sensitive_key_word, object, found = nil)
+ sensitive_key_found = object_contains_key?(object, sensitive_key_word)
+
+ # Returns the parent object and the object found containing a sensitive word as part of the key
+ if sensitive_key_found && object[sensitive_key_found]
+ ObjectWithParent.new(object[sensitive_key_found], object, sensitive_key_found)
+ elsif object.is_a?(Enumerable)
+ # Recursively lookup for keys containing sensitive words in a Hash or Array
+ object_with_parent = nil
+
+ object.find do |*hash_or_array|
+ object_with_parent = deep_find_with_parent(sensitive_key_word, hash_or_array.last, found)
+ end
+
+ object_with_parent
+ end
+ end
+
+ # Return true if the hash has a key containing a sensitive word
+ def object_contains_key?(object, sensitive_key_word)
+ return false unless object.is_a?(Hash)
+
+ object.keys.find { |key| key.include?(sensitive_key_word) }
+ end
+
+ # Returns the offended ObjectWithParent object if a sensitive word is found inside a hash,
+ # excluding the whitelisted safe hashes.
+ def find_sensitive_attributes(sensitive_word, project_hash)
+ loop do
+ object_with_parent = deep_find_with_parent(sensitive_word, project_hash)
+
+ return nil unless object_with_parent && object_with_parent.object
+
+ if is_safe_hash?(object_with_parent.parent, sensitive_word)
+ # It's in the safe list, remove hash and keep looking
+ object_with_parent.parent.delete(object_with_parent.key_found)
+ else
+ return object_with_parent
+ end
+
+ nil
+ end
+ end
+
+ # Returns true if it's one of the excluded models in +safe_list+
+ def is_safe_hash?(parent, sensitive_word)
+ return false unless parent && safe_list[sensitive_word.to_sym]
+
+ # Extra attributes that appear in a model but not in the exported hash.
+ excluded_attributes = ['type']
+
+ safe_list[sensitive_word.to_sym].each do |model|
+ # Check whether this is a hash attribute inside a model
+ if model.is_a?(Symbol)
+ return true if (safe_hashes[model] - parent.keys).empty?
+ else
+ return true if safe_model?(model, excluded_attributes, parent)
+ end
+ end
+
+ false
+ end
+
+ # Compares model attributes with those those found in the hash
+ # and returns true if there is a match, ignoring some excluded attributes.
+ def safe_model?(model, excluded_attributes, parent)
+ excluded_attributes += associations_for(model)
+ parsed_model_attributes = parsed_attributes(model.name.underscore, model.attribute_names)
+
+ (parsed_model_attributes - parent.keys - excluded_attributes).empty?
+ end
+end
diff --git a/spec/support/wait_for_vue_resource.rb b/spec/support/wait_for_vue_resource.rb
new file mode 100644
index 00000000000..1029f84716f
--- /dev/null
+++ b/spec/support/wait_for_vue_resource.rb
@@ -0,0 +1,7 @@
+module WaitForVueResource
+ def wait_for_vue_resource(spinner: true)
+ Timeout.timeout(Capybara.default_max_wait_time) do
+ loop until page.evaluate_script('Vue.activeResources').zero?
+ end
+ end
+end
diff --git a/spec/views/projects/merge_requests/_heading.html.haml_spec.rb b/spec/views/projects/merge_requests/_heading.html.haml_spec.rb
index 733b2dfa7ff..21f49d396e7 100644
--- a/spec/views/projects/merge_requests/_heading.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/_heading.html.haml_spec.rb
@@ -15,6 +15,8 @@ describe 'projects/merge_requests/widget/_heading' do
assign(:merge_request, merge_request)
assign(:project, project)
+ allow(view).to receive(:can?).and_return(true)
+
render
end
diff --git a/spec/views/projects/notes/_form.html.haml_spec.rb b/spec/views/projects/notes/_form.html.haml_spec.rb
new file mode 100644
index 00000000000..932d6756ad2
--- /dev/null
+++ b/spec/views/projects/notes/_form.html.haml_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe 'projects/notes/_form' do
+ include Devise::TestHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+
+ before do
+ project.team << [user, :master]
+ assign(:project, project)
+ assign(:note, note)
+
+ allow(view).to receive(:current_user).and_return(user)
+
+ render
+ end
+
+ %w[issue merge_request].each do |noteable|
+ context "with a note on #{noteable}" do
+ let(:note) { build(:"note_on_#{noteable}", project: project) }
+
+ it 'says that only markdown is supported, not slash commands' do
+ expect(rendered).to have_content('Styling with Markdown and slash commands are supported')
+ end
+ end
+ end
+
+ context 'with a note on a commit' do
+ let(:note) { build(:note_on_commit, project: project) }
+
+ it 'says that only markdown is supported, not slash commands' do
+ expect(rendered).to have_content('Styling with Markdown is supported')
+ end
+ end
+end
diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb
new file mode 100644
index 00000000000..ac7f3ffb157
--- /dev/null
+++ b/spec/views/projects/pipelines/show.html.haml_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+describe 'projects/pipelines/show' do
+ include Devise::TestHelpers
+
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id) }
+
+ before do
+ controller.prepend_view_path('app/views/projects')
+
+ create_build('build', 0, 'build', :success)
+ create_build('test', 1, 'rspec 0:2', :pending)
+ create_build('test', 1, 'rspec 1:2', :running)
+ create_build('test', 1, 'spinach 0:2', :created)
+ create_build('test', 1, 'spinach 1:2', :created)
+ create_build('test', 1, 'audit', :created)
+ create_build('deploy', 2, 'production', :created)
+
+ create(:generic_commit_status, pipeline: pipeline, stage: 'external', name: 'jenkins', stage_idx: 3)
+
+ assign(:project, project)
+ assign(:pipeline, pipeline)
+
+ allow(view).to receive(:can?).and_return(true)
+ end
+
+ it 'shows a graph with grouped stages' do
+ render
+
+ expect(rendered).to have_css('.pipeline-graph')
+ expect(rendered).to have_css('.grouped-pipeline-dropdown')
+
+ # stages
+ expect(rendered).to have_text('Build')
+ expect(rendered).to have_text('Test')
+ expect(rendered).to have_text('Deploy')
+ expect(rendered).to have_text('External')
+
+ # builds
+ expect(rendered).to have_text('rspec')
+ expect(rendered).to have_text('spinach')
+ expect(rendered).to have_text('rspec 0:2')
+ expect(rendered).to have_text('production')
+ expect(rendered).to have_text('jenkins')
+ end
+
+ private
+
+ def create_build(stage, stage_idx, name, status)
+ create(:ci_build, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name, status: status)
+ end
+end