summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGrzegorz Bizon <grzesiek.bizon@gmail.com>2016-09-03 09:35:21 +0200
committerGrzegorz Bizon <grzesiek.bizon@gmail.com>2016-09-03 09:35:21 +0200
commit2436631dea9264045d8694705a95d90c30b4057d (patch)
tree53c55f8c7ec7d4695d863585fe4dc8aba4527c84
parentb3cd41b4014fa96780218f0f086de239731ec91a (diff)
parent2aaab34b67eb2a6593780eda33d501a715ef0c5f (diff)
downloadgitlab-ce-refactor/ci-config-add-logical-validation.tar.gz
Merge branch 'master' into refactor/ci-config-add-logical-validationrefactor/ci-config-add-logical-validation
* master: (414 commits) Remove suggested colors hover underline Fix markdown anchor icon interaction Fix expiration date picker after update Refactored code to rely less on IDs that could change Move CHANGELOG entry for !5858 from 8.11 to 8.12 Hides merge request section in edit project when disabled Fix a typo Change minimum Unicorns required to two Update memory requirements Added `.term-bold` declaration. Change the inline code to codeblocks for the new features doc guideline Fix GitLab import button Rename behaviour to behavior in bug issue template for consistency Convert datetime coffeescript spec to ES6 Align add button on repository view Update CHANGELOG with 8.11.4 entries. removed null return - renamed 'placeTop' to 'placeProfileAvatarsToTop' Refactor Ci::Build#raw_trace Move CHANGELOG entry to a proper version Change widths of content in MR pipeline tab ... Conflicts: lib/gitlab/ci/config/node/jobs.rb
-rw-r--r--.gitlab/issue_templates/Bug.md44
-rw-r--r--.gitlab/issue_templates/Feature Proposal.md7
-rw-r--r--.gitlab/merge_request_templates/Documentation.md14
-rw-r--r--.rubocop.yml7
-rw-r--r--CHANGELOG129
-rw-r--r--CONTRIBUTING.md61
-rw-r--r--Gemfile7
-rw-r--r--Gemfile.lock10
-rw-r--r--PROCESS.md2
-rw-r--r--app/assets/images/icon-link.pngbin729 -> 0 bytes
-rw-r--r--app/assets/images/icon_anchor.svg1
-rw-r--r--app/assets/javascripts/abuse_reports.js.es63
-rw-r--r--app/assets/javascripts/activities.js4
-rw-r--r--app/assets/javascripts/application.js4
-rw-r--r--app/assets/javascripts/awards_handler.js1
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js30
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js.es67
-rw-r--r--app/assets/javascripts/boards/components/board.js.es615
-rw-r--r--app/assets/javascripts/boards/components/board_list.js.es613
-rw-r--r--app/assets/javascripts/boards/models/list.js.es629
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js.es63
-rw-r--r--app/assets/javascripts/dispatcher.js1
-rw-r--r--app/assets/javascripts/gl_dropdown.js24
-rw-r--r--app/assets/javascripts/issue.js3
-rw-r--r--app/assets/javascripts/labels_select.js2
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js8
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js12
-rw-r--r--app/assets/javascripts/logo.js54
-rw-r--r--app/assets/javascripts/merge_conflict_resolver.js.es64
-rw-r--r--app/assets/javascripts/project.js8
-rw-r--r--app/assets/javascripts/project_new.js20
-rw-r--r--app/assets/javascripts/right_sidebar.js2
-rw-r--r--app/assets/javascripts/todos.js28
-rw-r--r--app/assets/javascripts/user.js31
-rw-r--r--app/assets/javascripts/user.js.es634
-rw-r--r--app/assets/javascripts/users/calendar.js51
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/buttons.scss6
-rw-r--r--app/assets/stylesheets/framework/common.scss6
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss19
-rw-r--r--app/assets/stylesheets/framework/files.scss2
-rw-r--r--app/assets/stylesheets/framework/forms.scss1
-rw-r--r--app/assets/stylesheets/framework/header.scss54
-rw-r--r--app/assets/stylesheets/framework/logo.scss118
-rw-r--r--app/assets/stylesheets/framework/mixins.scss56
-rw-r--r--app/assets/stylesheets/framework/nav.scss30
-rw-r--r--app/assets/stylesheets/framework/selects.scss2
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss2
-rw-r--r--app/assets/stylesheets/framework/typography.scss27
-rw-r--r--app/assets/stylesheets/framework/variables.scss139
-rw-r--r--app/assets/stylesheets/pages/admin.scss4
-rw-r--r--app/assets/stylesheets/pages/boards.scss60
-rw-r--r--app/assets/stylesheets/pages/builds.scss15
-rw-r--r--app/assets/stylesheets/pages/commits.scss6
-rw-r--r--app/assets/stylesheets/pages/environments.scss5
-rw-r--r--app/assets/stylesheets/pages/events.scss9
-rw-r--r--app/assets/stylesheets/pages/import.scss19
-rw-r--r--app/assets/stylesheets/pages/issues.scss8
-rw-r--r--app/assets/stylesheets/pages/labels.scss1
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss12
-rw-r--r--app/assets/stylesheets/pages/notes.scss16
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss96
-rw-r--r--app/assets/stylesheets/pages/projects.scss35
-rw-r--r--app/assets/stylesheets/pages/search.scss4
-rw-r--r--app/assets/stylesheets/pages/status.scss9
-rw-r--r--app/assets/stylesheets/pages/todos.scss2
-rw-r--r--app/assets/stylesheets/pages/tree.scss14
-rw-r--r--app/assets/stylesheets/pages/xterm.scss3
-rw-r--r--app/controllers/admin/system_info_controller.rb8
-rw-r--r--app/controllers/application_controller.rb14
-rw-r--r--app/controllers/concerns/toggle_award_emoji.rb10
-rw-r--r--app/controllers/import/gitorious_controller.rb47
-rw-r--r--app/controllers/jwt_controller.rb2
-rw-r--r--app/controllers/namespaces_controller.rb2
-rw-r--r--app/controllers/projects/application_controller.rb2
-rw-r--r--app/controllers/projects/artifacts_controller.rb44
-rw-r--r--app/controllers/projects/avatars_controller.rb2
-rw-r--r--app/controllers/projects/boards/issues_controller.rb15
-rw-r--r--app/controllers/projects/builds_controller.rb4
-rw-r--r--app/controllers/projects/discussions_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb4
-rw-r--r--app/controllers/projects/labels_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb16
-rw-r--r--app/controllers/projects/milestones_controller.rb2
-rw-r--r--app/controllers/projects/snippets_controller.rb2
-rw-r--r--app/controllers/projects/tags_controller.rb8
-rw-r--r--app/controllers/projects_controller.rb18
-rw-r--r--app/finders/issuable_finder.rb2
-rw-r--r--app/finders/tags_finder.rb29
-rw-r--r--app/finders/todos_finder.rb2
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/helpers/ci_status_helper.rb11
-rw-r--r--app/helpers/compare_helper.rb2
-rw-r--r--app/helpers/gitlab_routing_helper.rb16
-rw-r--r--app/helpers/issuables_helper.rb13
-rw-r--r--app/helpers/lfs_helper.rb4
-rw-r--r--app/helpers/merge_requests_helper.rb2
-rw-r--r--app/helpers/nav_helper.rb2
-rw-r--r--app/helpers/projects_helper.rb35
-rw-r--r--app/helpers/search_helper.rb2
-rw-r--r--app/helpers/sentry_helper.rb22
-rw-r--r--app/helpers/tags_helper.rb10
-rw-r--r--app/helpers/todos_helper.rb34
-rw-r--r--app/mailers/base_mailer.rb2
-rw-r--r--app/models/ability.rb587
-rw-r--r--app/models/application_setting.rb2
-rw-r--r--app/models/ci/build.rb29
-rw-r--r--app/models/ci/pipeline.rb8
-rw-r--r--app/models/commit.rb9
-rw-r--r--app/models/commit_range.rb7
-rw-r--r--app/models/commit_status.rb4
-rw-r--r--app/models/concerns/awardable.rb14
-rw-r--r--app/models/concerns/has_status.rb (renamed from app/models/concerns/statuseable.rb)2
-rw-r--r--app/models/concerns/issuable.rb4
-rw-r--r--app/models/concerns/note_on_diff.rb4
-rw-r--r--app/models/concerns/project_features_compatibility.rb37
-rw-r--r--app/models/concerns/taskable.rb4
-rw-r--r--app/models/diff_note.rb4
-rw-r--r--app/models/event.rb2
-rw-r--r--app/models/merge_request.rb110
-rw-r--r--app/models/merge_request_diff.rb170
-rw-r--r--app/models/note.rb4
-rw-r--r--app/models/project.rb52
-rw-r--r--app/models/project_feature.rb63
-rw-r--r--app/models/repository.rb19
-rw-r--r--app/models/user.rb8
-rw-r--r--app/policies/base_policy.rb116
-rw-r--r--app/policies/ci/build_policy.rb13
-rw-r--r--app/policies/ci/runner_policy.rb13
-rw-r--r--app/policies/commit_status_policy.rb5
-rw-r--r--app/policies/deployment_policy.rb5
-rw-r--r--app/policies/environment_policy.rb5
-rw-r--r--app/policies/external_issue_policy.rb5
-rw-r--r--app/policies/global_policy.rb8
-rw-r--r--app/policies/group_member_policy.rb19
-rw-r--r--app/policies/group_policy.rb45
-rw-r--r--app/policies/issuable_policy.rb14
-rw-r--r--app/policies/issue_policy.rb28
-rw-r--r--app/policies/merge_request_policy.rb3
-rw-r--r--app/policies/namespace_policy.rb10
-rw-r--r--app/policies/note_policy.rb19
-rw-r--r--app/policies/personal_snippet_policy.rb16
-rw-r--r--app/policies/project_member_policy.rb22
-rw-r--r--app/policies/project_policy.rb224
-rw-r--r--app/policies/project_snippet_policy.rb20
-rw-r--r--app/policies/user_policy.rb11
-rw-r--r--app/services/base_service.rb6
-rw-r--r--app/services/boards/issues/list_service.rb7
-rw-r--r--app/services/boards/lists/create_service.rb9
-rw-r--r--app/services/ci/process_pipeline_service.rb16
-rw-r--r--app/services/ci/register_build_service.rb8
-rw-r--r--app/services/ci/web_hook_service.rb35
-rw-r--r--app/services/issuable_base_service.rb20
-rw-r--r--app/services/merge_requests/build_service.rb2
-rw-r--r--app/services/merge_requests/get_urls_service.rb2
-rw-r--r--app/services/merge_requests/resolve_service.rb21
-rw-r--r--app/services/merge_requests/update_service.rb4
-rw-r--r--app/services/projects/create_service.rb4
-rw-r--r--app/services/projects/fork_service.rb4
-rw-r--r--app/services/system_note_service.rb10
-rw-r--r--app/services/todo_service.rb3
-rw-r--r--app/views/admin/appearances/_form.html.haml2
-rw-r--r--app/views/admin/background_jobs/_head.html.haml46
-rw-r--r--app/views/admin/dashboard/_head.html.haml54
-rw-r--r--app/views/admin/projects/show.html.haml6
-rw-r--r--app/views/admin/system_info/show.html.haml12
-rw-r--r--app/views/ci/lints/show.html.haml3
-rw-r--r--app/views/dashboard/todos/index.html.haml38
-rw-r--r--app/views/events/_event.html.haml2
-rw-r--r--app/views/explore/groups/index.html.haml2
-rw-r--r--app/views/groups/group_members/update.js.haml2
-rw-r--r--app/views/help/ui.html.haml2
-rw-r--r--app/views/import/gitorious/status.html.haml54
-rw-r--r--app/views/layouts/nav/_group.html.haml2
-rw-r--r--app/views/layouts/nav/_group_settings.html.haml38
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml2
-rw-r--r--app/views/projects/_merge_request_settings.html.haml25
-rw-r--r--app/views/projects/boards/components/_board.html.haml14
-rw-r--r--app/views/projects/branches/_branch.html.haml2
-rw-r--r--app/views/projects/builds/_sidebar.html.haml73
-rw-r--r--app/views/projects/buttons/_download.html.haml46
-rw-r--r--app/views/projects/buttons/_fork.html.haml4
-rw-r--r--app/views/projects/ci/builds/_build.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build_pipeline.html.haml5
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml4
-rw-r--r--app/views/projects/commit/_ci_stage.html.haml4
-rw-r--r--app/views/projects/commit/_pipelines_list.haml5
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/commits/_head.html.haml5
-rw-r--r--app/views/projects/deployments/_actions.haml4
-rw-r--r--app/views/projects/edit.html.haml88
-rw-r--r--app/views/projects/forks/index.html.haml4
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml5
-rw-r--r--app/views/projects/graphs/_head.html.haml32
-rw-r--r--app/views/projects/issues/_head.html.haml54
-rw-r--r--app/views/projects/issues/_new_branch.html.haml17
-rw-r--r--app/views/projects/issues/_related_branches.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_diffs.html.haml1
-rw-r--r--app/views/projects/merge_requests/show/_mr_title.html.haml4
-rw-r--r--app/views/projects/merge_requests/show/_versions.html.haml31
-rw-r--r--app/views/projects/new.html.haml7
-rw-r--r--app/views/projects/notes/_note.html.haml4
-rw-r--r--app/views/projects/pipelines/_head.html.haml36
-rw-r--r--app/views/projects/pipelines/index.html.haml5
-rw-r--r--app/views/projects/project_members/update.js.haml2
-rw-r--r--app/views/projects/repositories/_download_archive.html.haml37
-rw-r--r--app/views/projects/show.html.haml2
-rw-r--r--app/views/projects/tags/_download.html.haml14
-rw-r--r--app/views/projects/tags/_tag.html.haml3
-rw-r--r--app/views/projects/tags/index.html.haml17
-rw-r--r--app/views/projects/tags/show.html.haml3
-rw-r--r--app/views/projects/tree/_tree_content.html.haml11
-rw-r--r--app/views/projects/tree/show.html.haml3
-rw-r--r--app/views/projects/wikis/_nav.html.haml22
-rw-r--r--app/views/shared/_logo.svg16
-rw-r--r--app/views/shared/_nav_scroll.html.haml4
-rw-r--r--app/views/shared/icons/_icon_play.svg4
-rw-r--r--app/views/shared/icons/_icon_status_created.svg1
-rw-r--r--app/views/shared/issuable/_filter.html.haml21
-rw-r--r--app/views/shared/issuable/_form.html.haml4
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/users/show.html.haml2
-rw-r--r--config/initializers/1_settings.rb2
-rw-r--r--config/initializers/sentry.rb1
-rw-r--r--config/routes.rb14
-rw-r--r--db/migrate/20160725104020_merge_request_diff_remove_uniq.rb21
-rw-r--r--db/migrate/20160725104452_merge_request_diff_add_index.rb17
-rw-r--r--db/migrate/20160823213309_add_lfs_enabled_to_projects.rb29
-rw-r--r--db/migrate/20160824103857_drop_unused_ci_tables.rb11
-rw-r--r--db/migrate/20160827011312_ensure_lock_version_has_no_default.rb16
-rw-r--r--db/migrate/20160830232601_change_lock_version_not_null.rb13
-rw-r--r--db/migrate/20160831214002_create_project_features.rb16
-rw-r--r--db/migrate/20160831214543_migrate_project_features.rb44
-rw-r--r--db/migrate/20160831223750_remove_features_enabled_from_projects.rb29
-rw-r--r--db/schema.rb40
-rw-r--r--doc/README.md1
-rw-r--r--doc/api/broadcast_messages.md158
-rw-r--r--doc/api/commits.md14
-rw-r--r--doc/api/issues.md32
-rw-r--r--doc/api/merge_requests.md133
-rw-r--r--doc/api/projects.md79
-rw-r--r--doc/api/users.md3
-rw-r--r--doc/ci/pipelines.md2
-rw-r--r--doc/ci/yaml/README.md21
-rw-r--r--doc/development/README.md2
-rw-r--r--doc/development/doc_styleguide.md55
-rw-r--r--doc/development/merge_request_performance_guidelines.md171
-rw-r--r--doc/development/newlines_styleguide.md2
-rw-r--r--doc/install/installation.md7
-rw-r--r--doc/install/requirements.md30
-rw-r--r--doc/integration/README.md2
-rw-r--r--doc/integration/bitbucket.md215
-rw-r--r--doc/integration/img/bitbucket_oauth_keys.pngbin0 -> 12073 bytes
-rw-r--r--doc/integration/img/bitbucket_oauth_settings_page.pngbin0 -> 82818 bytes
-rw-r--r--doc/integration/omniauth.md4
-rw-r--r--doc/update/8.10-to-8.11.md2
-rw-r--r--doc/update/8.11-to-8.12.md199
-rw-r--r--doc/user/markdown.md20
-rw-r--r--doc/user/permissions.md9
-rw-r--r--doc/user/project/issue_board.md7
-rw-r--r--doc/user/project/koding.md2
-rw-r--r--doc/user/project/merge_requests/resolve_conflicts.md1
-rw-r--r--doc/user/project/slash_commands.md2
-rw-r--r--doc/workflow/gitlab_flow.md2
-rw-r--r--doc/workflow/merge_requests.md8
-rw-r--r--doc/workflow/merge_requests/versions.pngbin0 -> 100566 bytes
-rw-r--r--doc/workflow/project_features.md10
-rw-r--r--features/dashboard/todos.feature20
-rw-r--r--features/project/merge_requests.feature2
-rw-r--r--features/steps/dashboard/new_project.rb1
-rw-r--r--features/steps/dashboard/todos.rb24
-rw-r--r--features/steps/project/merge_requests.rb6
-rw-r--r--features/steps/project/project.rb2
-rw-r--r--features/steps/shared/issuable.rb2
-rw-r--r--features/steps/shared/project.rb6
-rw-r--r--lib/api/api.rb2
-rw-r--r--lib/api/award_emoji.rb6
-rw-r--r--lib/api/broadcast_messages.rb99
-rw-r--r--lib/api/commit_statuses.rb2
-rw-r--r--lib/api/entities.rb35
-rw-r--r--lib/api/groups.rb4
-rw-r--r--lib/api/helpers.rb12
-rw-r--r--lib/api/internal.rb14
-rw-r--r--lib/api/issues.rb34
-rw-r--r--lib/api/merge_request_diffs.rb45
-rw-r--r--lib/api/projects.rb14
-rw-r--r--lib/api/users.rb2
-rw-r--r--lib/backup/repository.rb2
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb10
-rw-r--r--lib/banzai/filter/commit_range_reference_filter.rb2
-rw-r--r--lib/banzai/filter/commit_reference_filter.rb4
-rw-r--r--lib/banzai/filter/label_reference_filter.rb5
-rw-r--r--lib/banzai/filter/milestone_reference_filter.rb4
-rw-r--r--lib/banzai/filter/reference_filter.rb2
-rw-r--r--lib/banzai/reference_parser/base_parser.rb2
-rw-r--r--lib/gitlab/badge/coverage/report.rb4
-rw-r--r--lib/gitlab/ci/config/node/hidden.rb (renamed from lib/gitlab/ci/config/node/hidden_job.rb)3
-rw-r--r--lib/gitlab/ci/config/node/jobs.rb2
-rw-r--r--lib/gitlab/conflict/file.rb11
-rw-r--r--lib/gitlab/conflict/parser.rb9
-rw-r--r--lib/gitlab/contributions_calendar.rb1
-rw-r--r--lib/gitlab/current_settings.rb2
-rw-r--r--lib/gitlab/diff/file_collection/merge_request_diff.rb (renamed from lib/gitlab/diff/file_collection/merge_request.rb)16
-rw-r--r--lib/gitlab/github_import/importer.rb16
-rw-r--r--lib/gitlab/github_import/issue_formatter.rb6
-rw-r--r--lib/gitlab/github_import/milestone_formatter.rb36
-rw-r--r--lib/gitlab/github_import/project_creator.rb14
-rw-r--r--lib/gitlab/github_import/pull_request_formatter.rb11
-rw-r--r--lib/gitlab/gitorious_import.rb5
-rw-r--r--lib/gitlab/gitorious_import/client.rb29
-rw-r--r--lib/gitlab/gitorious_import/project_creator.rb27
-rw-r--r--lib/gitlab/gitorious_import/repository.rb35
-rw-r--r--lib/gitlab/import_export/import_export.yml7
-rw-r--r--lib/gitlab/import_sources.rb13
-rw-r--r--lib/gitlab/metrics/rack_middleware.rb22
-rw-r--r--lib/gitlab/sentry.rb27
-rw-r--r--spec/controllers/import/gitorious_controller_spec.rb69
-rw-r--r--spec/controllers/projects/boards/issues_controller_spec.rb4
-rw-r--r--spec/controllers/projects/boards/lists_controller_spec.rb43
-rw-r--r--spec/controllers/projects/boards_controller_spec.rb4
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb4
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb29
-rw-r--r--spec/controllers/projects/snippets_controller_spec.rb2
-rw-r--r--spec/factories/projects.rb21
-rw-r--r--spec/features/admin/admin_system_info_spec.rb47
-rw-r--r--spec/features/boards/boards_spec.rb86
-rw-r--r--spec/features/issues/award_emoji_spec.rb1
-rw-r--r--spec/features/issues/filter_issues_spec.rb10
-rw-r--r--spec/features/issues/new_branch_button_spec.rb2
-rw-r--r--spec/features/merge_requests/conflicts_spec.rb3
-rw-r--r--spec/features/merge_requests/diff_notes_spec.rb31
-rw-r--r--spec/features/merge_requests/merge_request_versions_spec.rb37
-rw-r--r--spec/features/notes_on_merge_requests_spec.rb2
-rw-r--r--spec/features/projects/branches/download_buttons_spec.rb40
-rw-r--r--spec/features/projects/builds_spec.rb (renamed from spec/features/builds_spec.rb)114
-rw-r--r--spec/features/projects/edit_spec.rb57
-rw-r--r--spec/features/projects/features_visibility_spec.rb122
-rw-r--r--spec/features/projects/files/download_buttons_spec.rb41
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin687442 -> 676870 bytes
-rw-r--r--spec/features/projects/main/download_buttons_spec.rb40
-rw-r--r--spec/features/projects/tags/download_buttons_spec.rb41
-rw-r--r--spec/features/task_lists_spec.rb266
-rw-r--r--spec/features/todos/todos_filtering_spec.rb63
-rw-r--r--spec/features/todos/todos_spec.rb35
-rw-r--r--spec/finders/tags_finder_spec.rb79
-rw-r--r--spec/fixtures/api/schemas/issues.json15
-rw-r--r--spec/javascripts/application_spec.js10
-rw-r--r--spec/javascripts/awards_handler_spec.js32
-rw-r--r--spec/javascripts/boards/list_spec.js.es69
-rw-r--r--spec/javascripts/boards/mock_data.js.es615
-rw-r--r--spec/javascripts/datetime_utility_spec.js.coffee31
-rw-r--r--spec/javascripts/datetime_utility_spec.js.es664
-rw-r--r--spec/javascripts/fixtures/awards_handler.html.haml2
-rw-r--r--spec/lib/banzai/filter/commit_range_reference_filter_spec.rb6
-rw-r--r--spec/lib/banzai/filter/commit_reference_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/external_issue_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/label_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/merge_request_reference_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/milestone_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/snippet_reference_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/base_parser_spec.rb8
-rw-r--r--spec/lib/banzai/reference_parser/user_parser_spec.rb10
-rw-r--r--spec/lib/gitlab/auth_spec.rb3
-rw-r--r--spec/lib/gitlab/ci/config/node/hidden_spec.rb (renamed from spec/lib/gitlab/ci/config/node/hidden_job_spec.rb)17
-rw-r--r--spec/lib/gitlab/ci/config/node/jobs_spec.rb2
-rw-r--r--spec/lib/gitlab/conflict/parser_spec.rb5
-rw-r--r--spec/lib/gitlab/github_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/issue_formatter_spec.rb5
-rw-r--r--spec/lib/gitlab/github_import/milestone_formatter_spec.rb5
-rw-r--r--spec/lib/gitlab/github_import/project_creator_spec.rb54
-rw-r--r--spec/lib/gitlab/github_import/pull_request_formatter_spec.rb7
-rw-r--r--spec/lib/gitlab/gitorious_import/project_creator_spec.rb26
-rw-r--r--spec/lib/gitlab/import_export/project.json6
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb13
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb12
-rw-r--r--spec/lib/gitlab/import_export/reader_spec.rb6
-rw-r--r--spec/lib/gitlab/metrics/rack_middleware_spec.rb21
-rw-r--r--spec/models/ability_spec.rb77
-rw-r--r--spec/models/build_spec.rb8
-rw-r--r--spec/models/ci/build_spec.rb60
-rw-r--r--spec/models/ci/pipeline_spec.rb30
-rw-r--r--spec/models/commit_range_spec.rb10
-rw-r--r--spec/models/concerns/awardable_spec.rb10
-rw-r--r--spec/models/concerns/has_status_spec.rb (renamed from spec/models/concerns/statuseable_spec.rb)6
-rw-r--r--spec/models/concerns/project_features_compatibility_spec.rb25
-rw-r--r--spec/models/members/project_member_spec.rb7
-rw-r--r--spec/models/merge_request_diff_spec.rb21
-rw-r--r--spec/models/merge_request_spec.rb141
-rw-r--r--spec/models/note_spec.rb22
-rw-r--r--spec/models/project_feature_spec.rb91
-rw-r--r--spec/models/project_security_spec.rb112
-rw-r--r--spec/models/project_spec.rb83
-rw-r--r--spec/models/repository_spec.rb18
-rw-r--r--spec/models/user_spec.rb3
-rw-r--r--spec/policies/project_policy_spec.rb36
-rw-r--r--spec/requests/api/award_emoji_spec.rb18
-rw-r--r--spec/requests/api/broadcast_messages_spec.rb180
-rw-r--r--spec/requests/api/builds_spec.rb20
-rw-r--r--spec/requests/api/commits_spec.rb4
-rw-r--r--spec/requests/api/internal_spec.rb4
-rw-r--r--spec/requests/api/issues_spec.rb90
-rw-r--r--spec/requests/api/merge_request_diffs_spec.rb49
-rw-r--r--spec/requests/api/merge_requests_spec.rb9
-rw-r--r--spec/requests/api/notes_spec.rb2
-rw-r--r--spec/requests/api/projects_spec.rb9
-rw-r--r--spec/requests/api/users_spec.rb1
-rw-r--r--spec/requests/git_http_spec.rb3
-rw-r--r--spec/requests/jwt_controller_spec.rb13
-rw-r--r--spec/requests/lfs_http_spec.rb107
-rw-r--r--spec/requests/projects/artifacts_controller_spec.rb117
-rw-r--r--spec/routing/routing_spec.rb4
-rw-r--r--spec/services/boards/issues/list_service_spec.rb8
-rw-r--r--spec/services/boards/lists/create_service_spec.rb11
-rw-r--r--spec/services/ci/image_for_build_service_spec.rb2
-rw-r--r--spec/services/ci/register_build_service_spec.rb19
-rw-r--r--spec/services/merge_requests/build_service_spec.rb4
-rw-r--r--spec/services/merge_requests/get_urls_service_spec.rb2
-rw-r--r--spec/services/merge_requests/merge_request_diff_cache_service_spec.rb2
-rw-r--r--spec/services/merge_requests/resolve_service_spec.rb87
-rw-r--r--spec/services/projects/create_service_spec.rb6
-rw-r--r--spec/services/system_note_service_spec.rb10
-rw-r--r--spec/services/todo_service_spec.rb21
-rw-r--r--spec/spec_helper.rb6
-rw-r--r--spec/support/taskable_shared_examples.rb63
-rw-r--r--spec/support/test_env.rb50
-rw-r--r--spec/views/projects/merge_requests/edit.html.haml_spec.rb53
-rw-r--r--spec/views/projects/merge_requests/show.html.haml_spec.rb44
-rw-r--r--spec/workers/repository_check/single_repository_worker_spec.rb8
432 files changed, 7040 insertions, 3219 deletions
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md
new file mode 100644
index 00000000000..ac38f0c9521
--- /dev/null
+++ b/.gitlab/issue_templates/Bug.md
@@ -0,0 +1,44 @@
+### Summary
+
+(Summarize the bug encountered concisely)
+
+### Steps to reproduce
+
+(How one can reproduce the issue - this is very important)
+
+### Expected behavior
+
+(What you should see instead)
+
+### Actual behavior
+
+(What actually happens)
+
+### Relevant logs and/or screenshots
+
+(Paste any relevant logs - please use code blocks (```) to format console output,
+logs, and code as it's very hard to read otherwise.)
+
+### Output of checks
+
+#### Results of GitLab application Check
+
+(For installations with omnibus-gitlab package run and paste the output of:
+`sudo gitlab-rake gitlab:check SANITIZE=true`)
+
+(For installations from source run and paste the output of:
+`sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true`)
+
+(we will only investigate if the tests are passing)
+
+#### Results of GitLab environment info
+
+(For installations with omnibus-gitlab package run and paste the output of:
+`sudo gitlab-rake gitlab:env:info`)
+
+(For installations from source run and paste the output of:
+`sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
+
+### Possible fixes
+
+(If you can, link to the line of code that might be responsible for the problem)
diff --git a/.gitlab/issue_templates/Feature Proposal.md b/.gitlab/issue_templates/Feature Proposal.md
new file mode 100644
index 00000000000..ea895ee6275
--- /dev/null
+++ b/.gitlab/issue_templates/Feature Proposal.md
@@ -0,0 +1,7 @@
+### Description
+
+(Include problem, use cases, benefits, and/or goals)
+
+### Proposal
+
+### Links / references
diff --git a/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md
new file mode 100644
index 00000000000..d2a1eb56423
--- /dev/null
+++ b/.gitlab/merge_request_templates/Documentation.md
@@ -0,0 +1,14 @@
+See the general Documentation guidelines http://docs.gitlab.com/ce/development/doc_styleguide.html.
+
+## What does this MR do?
+
+(briefly describe what this MR is about)
+
+## Moving docs to a new location?
+
+See the guidelines: http://docs.gitlab.com/ce/development/doc_styleguide.html#changing-document-location
+
+- [ ] Make sure the old link is not removed and has its contents replaced with a link to the new location.
+- [ ] Make sure internal links pointing to the document in question are not broken.
+- [ ] Search and replace any links referring to old docs in GitLab Rails app, specifically under the `app/views/` directory.
+- [ ] If working on CE, submit an MR to EE with the changes as well.
diff --git a/.rubocop.yml b/.rubocop.yml
index 282f4539f03..5bd31ccf329 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -5,8 +5,8 @@ require:
inherit_from: .rubocop_todo.yml
AllCops:
- TargetRubyVersion: 2.1
- # Cop names are not displayed in offense messages by default. Change behavior
+ TargetRubyVersion: 2.3
+ # Cop names are not d§splayed in offense messages by default. Change behavior
# by overriding DisplayCopNames, or by giving the -D/--display-cop-names
# option.
DisplayCopNames: true
@@ -192,6 +192,9 @@ Style/FlipFlop:
Style/For:
Enabled: true
+# Checks if there is a magic comment to enforce string literals
+Style/FrozenStringLiteralComment:
+ Enabled: false
# Do not introduce global variables.
Style/GlobalVars:
Enabled: true
diff --git a/CHANGELOG b/CHANGELOG
index 518e80a360a..93e91c457a3 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,22 +1,126 @@
Please view this file on the master branch, on stable branches it's out of date.
v 8.12.0 (unreleased)
+ - Prepend blank line to `Closes` message on merge request linked to issue (lukehowell)
+ - Filter tags by name !6121
+ - Make push events have equal vertical spacing.
- Add two-factor recovery endpoint to internal API !5510
+ - Remove vendor prefixes for linear-gradient CSS (ClemMakesApps)
+ - Add font color contrast to external label in admin area (ClemMakesApps)
+ - Change logo animation to CSS (ClemMakesApps)
+ - Instructions for enabling Git packfile bitmaps !6104
- Change merge_error column from string to text type
+ - Reduce contributions calendar data payload (ClemMakesApps)
- Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel)
+ - Expose `sha` and `merge_commit_sha` in merge request API (Ben Boeckel)
+ - Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling)
+ - Fix bug where pagination is still displayed despite all todos marked as done (ClemMakesApps)
+ - Center build stage columns in pipeline overview (ClemMakesApps)
+ - Rename behaviour to behavior in bug issue template for consistency (ClemMakesApps)
+ - Remove suggested colors hover underline (ClemMakesApps)
+ - Shorten task status phrase (ClemMakesApps)
+ - Add hover color to emoji icon (ClemMakesApps)
+ - Fix branches page dropdown sort alignment (ClemMakesApps)
+ - Add white background for no readme container (ClemMakesApps)
+ - API: Expose issue confidentiality flag. (Robert Schilling)
+ - Fix markdown anchor icon interaction (ClemMakesApps)
- Optimistic locking for Issues and Merge Requests (title and description overriding prevention)
- Add `wiki_page_events` to project hook APIs (Ben Boeckel)
+ - Remove Gitorious import
+ - Fix inconsistent background color for filter input field (ClemMakesApps)
+ - Remove prefixes from transition CSS property (ClemMakesApps)
- Add Sentry logging to API calls
+ - Add BroadcastMessage API
+ - Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling)
+ - Remove unused mixins (ClemMakesApps)
+ - Add search to all issue board lists
+ - Fix groups sort dropdown alignment (ClemMakesApps)
+ - Add horizontal scrolling to all sub-navs on mobile viewports (ClemMakesApps)
+ - Use JavaScript tooltips for mentions !5301 (winniehell)
+ - Fix markdown help references (ClemMakesApps)
+ - Add last commit time to repo view (ClemMakesApps)
+ - Fix accessibility and visibility of project list dropdown button !6140
+ - Added project specific enable/disable setting for LFS !5997
+ - 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
+ - Remove inconsistent font weight for sidebar's labels (ClemMakesApps)
+ - Align add button on repository view (ClemMakesApps)
- Added tests for diff notes
+ - Add a button to download latest successful artifacts for branches and tags !5142
+ - Remove redundant pipeline tooltips (ClemMakesApps)
+ - Expire commit info views after one day, instead of two weeks, to allow for user email updates
+ - Add delimiter to project stars and forks count (ClemMakesApps)
+ - Fix badge count alignment (ClemMakesApps)
+ - Remove green outline from `New branch unavailable` button on issue page !5858 (winniehell)
+ - Fix repo title alignment (ClemMakesApps)
+ - Fix branch title trailing space on hover (ClemMakesApps)
+ - Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison)
+ - Fix duplicate "me" in award emoji tooltip !5218 (jlogandavison)
+ - Order award emoji tooltips in order they were added (EspadaV8)
+ - Fix spacing and vertical alignment on build status icon on commits page (ClemMakesApps)
+ - Update merge_requests.md with a simpler way to check out a merge request. !5944
+ - Fix button missing type (ClemMakesApps)
+ - Move to project dropdown with infinite scroll for better performance
+ - Fix leaking of submit buttons outside the width of a main container !18731 (originally by @pavelloz)
+ - Load branches asynchronously in Cherry Pick and Revert dialogs.
+ - Convert datetime coffeescript spec to ES6 (ClemMakesApps)
+ - Add merge request versions !5467
+ - Change using size to use count and caching it for number of group members. !5935
+ - Replace play icon font with svg (ClemMakesApps)
- Added 'only_allow_merge_if_build_succeeds' project setting in the API. !5930 (Duck)
-
-v 8.11.2 (unreleased)
- - Show "Create Merge Request" widget for push events to fork projects on the source project
-
-v 8.11.1 (unreleased)
- - Does not halt the GitHub import process when an error occurs
+ - Reduce number of database queries on builds tab
+ - Wrap text in commit message containers
+ - Capitalize mentioned issue timeline notes (ClemMakesApps)
+ - Fix inconsistent checkbox alignment (ClemMakesApps)
+ - Use the default branch for displaying the project icon instead of master !5792 (Hannes Rosenögger)
+ - Adds response mime type to transaction metric action when it's not HTML
+ - Fix hover leading space bug in pipeline graph !5980
+ - User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496
+ - Fixed invisible scroll controls on build page on iPhone
+ - Fix error on raw build trace download for old builds stored in database !4822
+
+v 8.11.5 (unreleased)
+ - Optimize branch lookups and force a repository reload for Repository#find_branch
+ - Fix member expiration date picker after update
+ - Fix suggested colors options for new labels in the admin area. !6138
+ - Fix GitLab import button
+
+v 8.11.4
+ - Fix resolving conflicts on forks. !6082
+ - Fix diff commenting on merge requests created prior to 8.10. !6029
+ - Fix pipelines tab layout regression. !5952
+ - Fix "Wiki" link not appearing in navigation for projects with external wiki. !6057
+ - Do not enforce using hash with hidden key in CI configuration. !6079
+ - Fix hover leading space bug in pipeline graph !5980
+ - Fix sorting issues by "last updated" doesn't work after import from GitHub
+ - GitHub importer use default project visibility for non-private projects
+ - Creating an issue through our API now emails label subscribers !5720
+ - Block concurrent updates for Pipeline
+ - Don't create groups for unallowed users when importing projects
+ - Fix issue boards leak private label names and descriptions
+ - Fix broken gitlab:backup:restore because of bad permissions on repo storage !6098 (Dirk Hörner)
+ - Remove gitorious. !5866
+
+v 8.11.3
+ - Allow system info page to handle case where info is unavailable
+ - Label list shows all issues (opened or closed) with that label
+ - Don't show resolve conflicts link before MR status is updated
+ - Fix IE11 fork button bug !5982
+ - Don't prevent viewing the MR when git refs for conflicts can't be found on disk
+ - Fix external issue tracker "Issues" link leading to 404s
+ - Don't try to show merge conflict resolution info if a merge conflict contains non-UTF-8 characters
+ - Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling)
+
+v 8.11.2
+ - Show "Create Merge Request" widget for push events to fork projects on the source project. !5978
+ - Use gitlab-workhorse 0.7.11 !5983
+ - Does not halt the GitHub import process when an error occurs. !5763
- Fix file links on project page when default view is Files !5933
- - Change using size to use count and caching it for number of group members
+ - Fixed enter key in search input not working !5888
+
+v 8.11.1
+ - Pulled due to packaging error.
v 8.11.0
- Use test coverage value from the latest successful pipeline in badge. !5862
@@ -25,7 +129,6 @@ v 8.11.0
- Add Koding (online IDE) integration
- Ability to specify branches for Pivotal Tracker integration (Egor Lynko)
- Fix don't pass a local variable called `i` to a partial. !20510 (herminiotorres)
- - Add delimiter to project stars and forks count (ClemMakesApps)
- Fix rename `add_users_into_project` and `projects_ids`. !20512 (herminiotorres)
- Fix adding line comments on the initial commit to a repo !5900
- Fix the title of the toggle dropdown button. !5515 (herminiotorres)
@@ -41,7 +144,6 @@ v 8.11.0
- Use long options for curl examples in documentation !5703 (winniehell)
- Added tooltip listing label names to the labels value in the collapsed issuable sidebar
- Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell)
- - Fix badge count alignment (ClemMakesApps)
- GitLab Performance Monitoring can now track custom events such as the number of tags pushed to a repository
- Add support for relative links starting with ./ or / to RelativeLinkFilter (winniehell)
- Allow naming U2F devices !5833
@@ -79,10 +181,10 @@ v 8.11.0
- Get issue and merge request description templates from repositories
- Add hover state to todos !5361 (winniehell)
- Fix icon alignment of star and fork buttons !5451 (winniehell)
+ - Fix alignment of icon buttons !5887 (winniehell)
- Enforce 2FA restrictions on API authentication endpoints !5820
- Limit git rev-list output count to one in forced push check
- Show deployment status on merge requests with external URLs
- - Fix branch title trailing space on hover (ClemMakesApps)
- Clean up unused routes (Josef Strzibny)
- Fix issue on empty project to allow developers to only push to protected branches if given permission
- API: Add enpoints for pipelines
@@ -99,7 +201,6 @@ v 8.11.0
- Fix devise deprecation warnings.
- Check for 2FA when using Git over HTTP and only allow PersonalAccessTokens as password in that case !5764
- Update version_sorter and use new interface for faster tag sorting
- - Load branches asynchronously in Cherry Pick and Revert dialogs.
- Optimize checking if a user has read access to a list of issues !5370
- Store all DB secrets in secrets.yml, under descriptive names !5274
- Fix syntax highlighting in file editor
@@ -120,8 +221,6 @@ v 8.11.0
- Remove `search_id` of labels dropdown filter to fix 'Missleading URI for labels in Merge Requests and Issues view'. !5368 (Scott Le)
- Load project invited groups and members eagerly in `ProjectTeam#fetch_members`
- Add pipeline events hook
- - Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison)
- - Fix duplicate "me" in award emoji tooltip !5218 (jlogandavison)
- Bump gitlab_git to speedup DiffCollection iterations
- Rewrite description of a blocked user in admin settings. (Elias Werberich)
- Make branches sortable without push permission !5462 (winniehell)
@@ -133,14 +232,12 @@ v 8.11.0
- Fix search for notes which belongs to deleted objects
- Allow Akismet to be trained by submitting issues as spam or ham !5538
- Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska)
- - Fix spacing and vertical alignment on build status icon on commits page (ClemMakesApps)
- Allow branch names ending with .json for graph and network page !5579 (winniehell)
- Add the `sprockets-es6` gem
- Improve OAuth2 client documentation (muteor)
- Fix diff comments inverted toggle bug (ClemMakesApps)
- Multiple trigger variables show in separate lines (Katarzyna Kobierska Ula Budziszewska)
- Profile requests when a header is passed
- - Fix button missing type (ClemMakesApps)
- Avoid calculation of line_code and position for _line partial when showing diff notes on discussion tab.
- Speedup DiffNote#active? on discussions, preloading noteables and avoid touching git repository to return diff_refs when possible
- Add commit stats in commit api. !5517 (dixpac)
@@ -149,7 +246,6 @@ v 8.11.0
- edit_blob_link will use blob passed onto the options parameter
- Make error pages responsive (Takuya Noguchi)
- The performance of the project dropdown used for moving issues has been improved
- - Move to project dropdown with infinite scroll for better performance
- Fix skip_repo parameter being ignored when destroying a namespace
- Add all builds into stage/job dropdowns on builds page
- Change requests_profiles resource constraint to catch virtually any file
@@ -181,7 +277,6 @@ v 8.11.0
- Eliminate unneeded calls to Repository#blob_at when listing commits with no path
- Update gitlab_git gem to 10.4.7
- Simplify SQL queries of marking a todo as done
- - Update merge_requests.md with a simpler way to check out a merge request. !5944
v 8.10.7
- Upgrade Hamlit to 2.6.1. !5873
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d8093a61b4c..c77dcd96a7d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -129,7 +129,7 @@ request that potentially fixes it.
### Feature proposals
-To create a feature proposal for CE and CI, open an issue on the
+To create a feature proposal for CE, open an issue on the
[issue tracker of CE][ce-tracker].
For feature proposals for EE, open an issue on the
@@ -144,16 +144,7 @@ code snippet right after your description in a new line: `~"feature proposal"`.
Please keep feature proposals as small and simple as possible, complex ones
might be edited to make them small and simple.
-You are encouraged to use the template below for feature proposals.
-
-```
-## Description
-Include problem, use cases, benefits, and/or goals
-
-## Proposal
-
-## Links / references
-```
+Please submit Feature Proposals using the ['Feature Proposal' issue template](.gitlab/issue_templates/Feature Proposal.md) provided on the issue tracker.
For changes in the interface, it can be helpful to create a mockup first.
If you want to create something yourself, consider opening an issue first to
@@ -166,55 +157,11 @@ submitting your own, there's a good chance somebody else had the same issue or
feature proposal. Show your support with an award emoji and/or join the
discussion.
-Please submit bugs using the following template in the issue description area.
+Please submit bugs using the ['Bug' issue template](.gitlab/issue_templates/Bug.md) provided on the issue tracker.
The text in the parenthesis is there to help you with what to include. Omit it
when submitting the actual issue. You can copy-paste it and then edit as you
see fit.
-```
-## Summary
-
-(Summarize your issue in one sentence - what goes wrong, what did you expect to happen)
-
-## Steps to reproduce
-
-(How one can reproduce the issue - this is very important)
-
-## Expected behavior
-
-(What you should see instead)
-
-## Relevant logs and/or screenshots
-
-(Paste any relevant logs - please use code blocks (```) to format console output,
-logs, and code as it's very hard to read otherwise.)
-
-## Output of checks
-
-### Results of GitLab Application Check
-
-(For installations with omnibus-gitlab package run and paste the output of:
-sudo gitlab-rake gitlab:check SANITIZE=true)
-
-(For installations from source run and paste the output of:
-sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true)
-
-(we will only investigate if the tests are passing)
-
-### Results of GitLab Environment Info
-
-(For installations with omnibus-gitlab package run and paste the output of:
-sudo gitlab-rake gitlab:env:info)
-
-(For installations from source run and paste the output of:
-sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production)
-
-## Possible fixes
-
-(If you can, link to the line of code that might be responsible for the problem)
-
-```
-
### Issue weight
Issue weight allows us to get an idea of the amount of work required to solve
@@ -340,6 +287,8 @@ request is as follows:
migrations on a fresh database before the MR is reviewed. If the review leads
to large changes in the MR, do this again once the review is complete.
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).
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
diff --git a/Gemfile b/Gemfile
index 68547b6fac8..620338e5997 100644
--- a/Gemfile
+++ b/Gemfile
@@ -53,7 +53,7 @@ gem 'browser', '~> 2.2'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
-gem 'gitlab_git', '~> 10.4.7'
+gem 'gitlab_git', '~> 10.5'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
@@ -97,9 +97,6 @@ gem 'fog-rackspace', '~> 0.1.1'
# for aws storage
gem 'unf', '~> 0.1.4'
-# Authorization
-gem 'six', '~> 0.2.0'
-
# Seed data
gem 'seed-fu', '~> 2.3.5'
@@ -349,5 +346,5 @@ gem 'paranoia', '~> 2.0'
gem 'health_check', '~> 2.1.0'
# System information
-gem 'vmstat', '~> 2.1.1'
+gem 'vmstat', '~> 2.2'
gem 'sys-filesystem', '~> 1.1.6'
diff --git a/Gemfile.lock b/Gemfile.lock
index 5511d718938..28ede86b3ba 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -279,7 +279,7 @@ GEM
diff-lcs (~> 1.1)
mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3)
- gitlab_git (10.4.7)
+ gitlab_git (10.5.0)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
@@ -683,7 +683,6 @@ GEM
rack (~> 1.5)
rack-protection (~> 1.4)
tilt (>= 1.3, < 3)
- six (0.2.0)
slack-notifier (1.2.1)
slop (3.6.0)
spinach (0.8.10)
@@ -772,7 +771,7 @@ GEM
coercible (~> 1.0)
descendants_tracker (~> 0.0, >= 0.0.3)
equalizer (~> 0.0, >= 0.0.9)
- vmstat (2.1.1)
+ vmstat (2.2.0)
warden (1.2.6)
rack (>= 1.0)
web-console (2.3.0)
@@ -859,7 +858,7 @@ DEPENDENCIES
github-linguist (~> 4.7.0)
github-markup (~> 1.4)
gitlab-flowdock-git-hook (~> 1.0.1)
- gitlab_git (~> 10.4.7)
+ gitlab_git (~> 10.5)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2)
@@ -954,7 +953,6 @@ DEPENDENCIES
sidekiq-cron (~> 0.4.0)
simplecov (= 0.12.0)
sinatra (~> 1.4.4)
- six (~> 0.2.0)
slack-notifier (~> 1.2.0)
spinach-rails (~> 0.2.1)
spinach-rerun-reporter (~> 0.0.2)
@@ -980,7 +978,7 @@ DEPENDENCIES
unicorn-worker-killer (~> 0.4.2)
version_sorter (~> 2.1.0)
virtus (~> 1.0.1)
- vmstat (~> 2.1.1)
+ vmstat (~> 2.2)
web-console (~> 2.0)
webmock (~> 1.21.0)
wikicloth (= 0.8.1)
diff --git a/PROCESS.md b/PROCESS.md
index 8e1a3f7360f..8af660fbdd1 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -50,7 +50,7 @@ etc.).
The most important thing is making sure valid issues receive feedback from the
development team. Therefore the priority is mentioning developers that can help
-on those issue. Please select someone with relevant experience from
+on those issues. Please select someone with relevant experience from
[GitLab core team][core-team]. If there is nobody mentioned with that expertise
look in the commit history for the affected files to find someone. Avoid
mentioning the lead developer, this is the person that is least likely to give a
diff --git a/app/assets/images/icon-link.png b/app/assets/images/icon-link.png
deleted file mode 100644
index 5b55e12571c..00000000000
--- a/app/assets/images/icon-link.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/icon_anchor.svg b/app/assets/images/icon_anchor.svg
new file mode 100644
index 00000000000..7e242586bad
--- /dev/null
+++ b/app/assets/images/icon_anchor.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#333" fill-rule="evenodd" d="M9.683 6.676l-.047-.048C8.27 5.26 6.07 5.243 4.726 6.588l-2.29 2.29c-1.344 1.344-1.328 3.544.04 4.91 1.366 1.368 3.564 1.385 4.908.04l1.753-1.752c-.695.074-1.457-.078-2.176-.444L5.934 12.66c-.634.634-1.67.625-2.312-.017-.642-.643-.65-1.677-.017-2.312L6.035 7.9c.634-.634 1.67-.625 2.312.017.024.024.048.05.07.075l.003-.002c.36.36.943.366 1.3.01.355-.356.35-.938-.01-1.3l-.027-.024zM6.58 9.586l.048.05c1.367 1.366 3.565 1.384 4.91.04l2.29-2.292c1.344-1.343 1.328-3.542-.04-4.91-1.366-1.366-3.564-1.384-4.908-.04L7.127 4.187c.695-.074 1.457.078 2.176.444l1.028-1.027c.635-.634 1.67-.624 2.313.017.643.644.652 1.678.018 2.312l-2.43 2.432c-.635.634-1.67.624-2.313-.018-.024-.024-.048-.05-.07-.075l-.003.004c-.36-.362-.943-.367-1.3-.01-.355.355-.35.937.01 1.3.01.007.018.015.027.023z"/></svg> \ No newline at end of file
diff --git a/app/assets/javascripts/abuse_reports.js.es6 b/app/assets/javascripts/abuse_reports.js.es6
index 748084b0307..2fe46b9fd06 100644
--- a/app/assets/javascripts/abuse_reports.js.es6
+++ b/app/assets/javascripts/abuse_reports.js.es6
@@ -1,4 +1,3 @@
-window.gl = window.gl || {};
((global) => {
const MAX_MESSAGE_LENGTH = 500;
const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
@@ -36,4 +35,4 @@ window.gl = window.gl || {};
}
global.AbuseReports = AbuseReports;
-})(window.gl);
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js
index 1ab3c2197d8..d5e11e22be5 100644
--- a/app/assets/javascripts/activities.js
+++ b/app/assets/javascripts/activities.js
@@ -12,7 +12,7 @@
}
Activities.prototype.updateTooltips = function() {
- return gl.utils.localTimeAgo($('.js-timeago', '#activity'));
+ return gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
};
Activities.prototype.reloadActivities = function() {
@@ -26,7 +26,7 @@
event_filters = $.cookie("event_filter");
filter = sender.attr("id").split("_")[0];
$.cookie("event_filter", (event_filters !== filter ? filter : ""), {
- path: '/'
+ path: gon.relative_url_root || '/'
});
if (event_filters !== filter) {
return sender.closest('li').toggleClass("active");
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index fc354dfd677..43a679501a7 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -288,7 +288,7 @@
new Aside();
if ($window.width() < 1024 && $.cookie('pin_nav') === 'true') {
$.cookie('pin_nav', 'false', {
- path: '/',
+ path: gon.relative_url_root || '/',
expires: 365 * 10
});
$('.page-with-sidebar').toggleClass('page-sidebar-collapsed page-sidebar-expanded').removeClass('page-sidebar-pinned');
@@ -313,7 +313,7 @@
$topNav.removeClass('header-pinned-nav').toggleClass('header-collapsed header-expanded');
}
$.cookie('pin_nav', doPinNav, {
- path: '/',
+ path: gon.relative_url_root || '/',
expires: 365 * 10
});
if ($.cookie('pin_nav') === 'true' || doPinNav) {
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index aee1c29eee3..ad12cb906e1 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -320,6 +320,7 @@
frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
frequentlyUsedEmojis.push(emoji);
return $.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), {
+ path: gon.relative_url_root || '/',
expires: 365
});
};
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index 1b7b63489ea..5467e3edc69 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -1,10 +1,26 @@
-(function() {
+(function(w) {
$(function() {
- return $("body").on("click", ".js-toggle-button", function(e) {
- $(this).find('i').toggleClass('fa fa-chevron-down').toggleClass('fa fa-chevron-up');
- $(this).closest(".js-toggle-container").find(".js-toggle-content").toggle();
- return e.preventDefault();
+ $('body').on('click', '.js-toggle-button', function(e) {
+ e.preventDefault();
+ $(this)
+ .find('.fa')
+ .toggleClass('fa-chevron-down fa-chevron-up')
+ .end()
+ .closest('.js-toggle-container')
+ .find('.js-toggle-content')
+ .toggle()
+ ;
});
- });
-}).call(this);
+ // If we're accessing a permalink, ensure it is not inside a
+ // closed js-toggle-container!
+ var hash = w.gl.utils.getLocationHash();
+ var anchor = hash && document.getElementById(hash);
+ var container = anchor && $(anchor).closest('.js-toggle-container');
+
+ if (container && container.find('.js-toggle-content').is(':hidden')) {
+ container.find('.js-toggle-button').trigger('click');
+ anchor.scrollIntoView();
+ }
+ });
+})(window);
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
index a612cf0f1ae..91c12570e09 100644
--- a/app/assets/javascripts/boards/boards_bundle.js.es6
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -54,4 +54,11 @@ $(() => {
});
}
});
+
+ gl.IssueBoardsSearch = new Vue({
+ el: '#js-boards-seach',
+ data: {
+ filters: Store.state.filters
+ }
+ });
});
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6
index d7f4107cb02..7e86f001f44 100644
--- a/app/assets/javascripts/boards/components/board.js.es6
+++ b/app/assets/javascripts/boards/components/board.js.es6
@@ -21,15 +21,10 @@
},
data () {
return {
- query: '',
filters: Store.state.filters
};
},
watch: {
- query () {
- this.list.filters = this.getFilterData();
- this.list.getIssues(true);
- },
filters: {
handler () {
this.list.page = 1;
@@ -38,16 +33,6 @@
deep: true
}
},
- methods: {
- getFilterData () {
- const filters = this.filters;
- let queryData = { search: this.query };
-
- Object.keys(filters).forEach((key) => { queryData[key] = filters[key]; });
-
- return queryData;
- }
- },
ready () {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled,
diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6
index a6644e9eb8c..50fc11d7737 100644
--- a/app/assets/javascripts/boards/components/board_list.js.es6
+++ b/app/assets/javascripts/boards/components/board_list.js.es6
@@ -20,7 +20,8 @@
data () {
return {
scrollOffset: 250,
- filters: Store.state.filters
+ filters: Store.state.filters,
+ showCount: false
};
},
watch: {
@@ -30,6 +31,15 @@
this.$els.list.scrollTop = 0;
},
deep: true
+ },
+ issues () {
+ this.$nextTick(() => {
+ if (this.scrollHeight() > this.listHeight()) {
+ this.showCount = true;
+ } else {
+ this.showCount = false;
+ }
+ });
}
},
methods: {
@@ -58,6 +68,7 @@
group: 'issues',
sort: false,
disabled: this.disabled,
+ filter: '.board-list-count',
onStart: (e) => {
const card = this.$refs.issue[e.oldIndex];
diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6
index be2b8c568a8..91fd620fdb3 100644
--- a/app/assets/javascripts/boards/models/list.js.es6
+++ b/app/assets/javascripts/boards/models/list.js.es6
@@ -11,6 +11,7 @@ class List {
this.loading = true;
this.loadingMore = false;
this.issues = [];
+ this.issuesSize = 0;
if (obj.label) {
this.label = new ListLabel(obj.label);
@@ -51,17 +52,13 @@ class List {
}
nextPage () {
- if (Math.floor(this.issues.length / 20) === this.page) {
+ if (this.issuesSize > this.issues.length) {
this.page++;
return this.getIssues(false);
}
}
- canSearch () {
- return this.type === 'backlog';
- }
-
getIssues (emptyIssues = true) {
const filters = this.filters;
let data = { page: this.page };
@@ -80,12 +77,13 @@ class List {
.then((resp) => {
const data = resp.json();
this.loading = false;
+ this.issuesSize = data.size;
if (emptyIssues) {
this.issues = [];
}
- this.createIssues(data);
+ this.createIssues(data.issues);
});
}
@@ -96,14 +94,20 @@ class List {
}
addIssue (issue, listFrom) {
- this.issues.push(issue);
+ if (!this.findIssue(issue.id)) {
+ this.issues.push(issue);
- if (this.label) {
- issue.addLabel(this.label);
- }
+ if (this.label) {
+ issue.addLabel(this.label);
+ }
- if (listFrom) {
- gl.boardService.moveIssue(issue.id, listFrom.id, this.id);
+ if (listFrom) {
+ this.issuesSize++;
+ gl.boardService.moveIssue(issue.id, listFrom.id, this.id)
+ .then(() => {
+ listFrom.getIssues(false);
+ });
+ }
}
}
@@ -116,6 +120,7 @@ class List {
const matchesRemove = removeIssue.id === issue.id;
if (matchesRemove) {
+ this.issuesSize--;
issue.removeLabel(this.label);
}
diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6
index 18f26a1f911..bd07ee0c161 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js.es6
+++ b/app/assets/javascripts/boards/stores/boards_store.js.es6
@@ -15,7 +15,8 @@
author_id: gl.utils.getParameterValues('author_id')[0],
assignee_id: gl.utils.getParameterValues('assignee_id')[0],
milestone_title: gl.utils.getParameterValues('milestone_title')[0],
- label_name: gl.utils.getParameterValues('label_name[]')
+ label_name: gl.utils.getParameterValues('label_name[]'),
+ search: ''
};
},
addList (listObj) {
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index ba64d2bcf0b..38cdc7b9fba 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -199,6 +199,7 @@
break;
case 'labels':
switch (path[2]) {
+ case 'new':
case 'edit':
new Labels();
}
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 0179b320a3b..77b2082cba0 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -117,7 +117,7 @@
}
});
} else {
- return elements.show();
+ return elements.show().removeClass('option-hidden');
}
}
};
@@ -190,9 +190,9 @@
currentIndex = -1;
- NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link, .option-hidden';
+ NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
- SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ")";
+ SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)";
CURSOR_SELECT_SCROLL_PADDING = 5
@@ -556,7 +556,7 @@
if (isInput) {
field = $(this.el);
} else {
- field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
+ field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + escape(value) + "']");
}
if (el.hasClass(ACTIVE_CLASS)) {
el.removeClass(ACTIVE_CLASS);
@@ -565,10 +565,6 @@
} else {
field.remove();
}
- if (this.options.toggleLabel) {
- this.updateLabel(selectedObject, el, this);
- }
- return selectedObject;
} else if (el.hasClass(INDETERMINATE_CLASS)) {
el.addClass(ACTIVE_CLASS);
el.removeClass(INDETERMINATE_CLASS);
@@ -578,7 +574,6 @@
if (!field.length && fieldName) {
this.addInput(fieldName, value, selectedObject);
}
- return selectedObject;
} else {
if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) {
this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS);
@@ -590,9 +585,6 @@
field.remove();
}
el.addClass(ACTIVE_CLASS);
- if (this.options.toggleLabel) {
- this.updateLabel(selectedObject, el, this);
- }
if (value != null) {
if (!field.length && fieldName) {
this.addInput(fieldName, value, selectedObject);
@@ -600,8 +592,14 @@
field.val(value).trigger('change');
}
}
- return selectedObject;
}
+
+ // Update label right after input has been added
+ if (this.options.toggleLabel) {
+ this.updateLabel(selectedObject, el, this);
+ }
+
+ return selectedObject;
};
GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 6838d9d8da1..e6422602ce8 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -127,7 +127,7 @@
Issue.prototype.initCanCreateBranch = function() {
var $container;
- $container = $('div#new-branch');
+ $container = $('#new-branch');
if ($container.length === 0) {
return;
}
@@ -139,7 +139,6 @@
if (data.can_create_branch) {
$container.find('.checking').hide();
$container.find('.available').show();
- return $container.find('a').attr('disabled', false);
} else {
$container.find('.checking').hide();
return $container.find('.unavailable').show();
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 565dbeacdb3..bab23ff5ac0 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -164,7 +164,7 @@
instance.addInput(this.fieldName, label.id);
}
}
- if ($form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + (this.id(label)) + "']").length) {
+ if ($form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + escape(this.id(label)) + "']").length) {
selectedClass.push('is-active');
}
if ($dropdown.hasClass('js-multiselect') && removesAll) {
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 10afa7e4329..d4d5927d3b0 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -67,6 +67,14 @@
$.timeago.settings.strings = tmpLocale;
};
+ w.gl.utils.getDayDifference = function(a, b) {
+ var millisecondsPerDay = 1000 * 60 * 60 * 24;
+ var date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
+ var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
+
+ return Math.floor((date2 - date1) / millisecondsPerDay);
+ }
+
})(window);
}).call(this);
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index fffbfd19745..533310cc87c 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -43,7 +43,7 @@
}
return newUrl;
};
- return w.gl.utils.removeParamQueryString = function(url, param) {
+ w.gl.utils.removeParamQueryString = function(url, param) {
var urlVariables, variables;
url = decodeURIComponent(url);
urlVariables = url.split('&');
@@ -59,6 +59,16 @@
return results;
})()).join('&');
};
+ w.gl.utils.getLocationHash = function(url) {
+ var hashIndex;
+ if (typeof url === 'undefined') {
+ // Note: We can't use window.location.hash here because it's
+ // not consistent across browsers - Firefox will pre-decode it
+ url = window.location.href;
+ }
+ hashIndex = url.indexOf('#');
+ return hashIndex === -1 ? null : url.substring(hashIndex + 1);
+ };
})(window);
}).call(this);
diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js
index 218f24fe908..7d8eef1b495 100644
--- a/app/assets/javascripts/logo.js
+++ b/app/assets/javascripts/logo.js
@@ -1,54 +1,12 @@
(function() {
- var clearHighlights, currentTimer, defaultClass, delay, firstPiece, pieceIndex, pieces, start, stop, work;
-
Turbolinks.enableProgressBar();
- defaultClass = 'tanuki-shape';
-
- pieces = ['path#tanuki-right-cheek', 'path#tanuki-right-eye, path#tanuki-right-ear', 'path#tanuki-nose', 'path#tanuki-left-eye, path#tanuki-left-ear', 'path#tanuki-left-cheek'];
-
- pieceIndex = 0;
-
- firstPiece = pieces[0];
-
- currentTimer = null;
-
- delay = 150;
-
- clearHighlights = function() {
- return $("." + defaultClass + ".highlight").attr('class', defaultClass);
- };
-
- start = function() {
- clearHighlights();
- pieceIndex = 0;
- if (pieces[0] !== firstPiece) {
- pieces.reverse();
- }
- if (currentTimer) {
- clearInterval(currentTimer);
- }
- return currentTimer = setInterval(work, delay);
- };
-
- stop = function() {
- clearInterval(currentTimer);
- return clearHighlights();
- };
-
- work = function() {
- clearHighlights();
- $(pieces[pieceIndex]).attr('class', defaultClass + " highlight");
- if (pieceIndex === pieces.length - 1) {
- pieceIndex = 0;
- return pieces.reverse();
- } else {
- return pieceIndex++;
- }
- };
-
- $(document).on('page:fetch', start);
+ $(document).on('page:fetch', function() {
+ $('.tanuki-logo').addClass('animate');
+ });
- $(document).on('page:change', stop);
+ $(document).on('page:change', function() {
+ $('.tanuki-logo').removeClass('animate');
+ });
}).call(this);
diff --git a/app/assets/javascripts/merge_conflict_resolver.js.es6 b/app/assets/javascripts/merge_conflict_resolver.js.es6
index 77bffbcb403..b56fd5aa658 100644
--- a/app/assets/javascripts/merge_conflict_resolver.js.es6
+++ b/app/assets/javascripts/merge_conflict_resolver.js.es6
@@ -75,10 +75,8 @@ class MergeConflictResolver {
window.location.href = data.redirect_to;
})
.error(() => {
- new Flash('Something went wrong!');
- })
- .always(() => {
this.vue.isSubmitting = false;
+ new Flash('Something went wrong!');
});
}
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index 4e1de4dfb72..66e097c0a28 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -17,19 +17,15 @@
return $(this).parents('form').submit();
});
$('.hide-no-ssh-message').on('click', function(e) {
- var path;
- path = '/';
$.cookie('hide_no_ssh_message', 'false', {
- path: path
+ path: gon.relative_url_root || '/'
});
$(this).parents('.no-ssh-key-message').remove();
return e.preventDefault();
});
$('.hide-no-password-message').on('click', function(e) {
- var path;
- path = '/';
$.cookie('hide_no_password_message', 'false', {
- path: path
+ path: gon.relative_url_root || '/'
});
$(this).parents('.no-password-message').remove();
return e.preventDefault();
diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js
index 798f15e40a0..a787b11f2a9 100644
--- a/app/assets/javascripts/project_new.js
+++ b/app/assets/javascripts/project_new.js
@@ -4,6 +4,8 @@
this.ProjectNew = (function() {
function ProjectNew() {
this.toggleSettings = bind(this.toggleSettings, this);
+ this.$selects = $('.features select');
+
$('.project-edit-container').on('ajax:before', (function(_this) {
return function() {
$('.project-edit-container').hide();
@@ -15,18 +17,24 @@
}
ProjectNew.prototype.toggleSettings = function() {
- this._showOrHide('#project_builds_enabled', '.builds-feature');
- return this._showOrHide('#project_merge_requests_enabled', '.merge-requests-feature');
+ var self = this;
+
+ this.$selects.each(function () {
+ var $select = $(this),
+ className = $select.data('field').replace(/_/g, '-')
+ .replace('access-level', 'feature');
+ self._showOrHide($select, '.' + className);
+ });
};
ProjectNew.prototype.toggleSettingsOnclick = function() {
- return $('#project_builds_enabled, #project_merge_requests_enabled').on('click', this.toggleSettings);
+ this.$selects.on('change', this.toggleSettings);
};
ProjectNew.prototype._showOrHide = function(checkElement, container) {
- var $container;
- $container = $(container);
- if ($(checkElement).prop('checked')) {
+ var $container = $(container);
+
+ if ($(checkElement).val() !== '0') {
return $container.show();
} else {
return $container.hide();
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index dc4d5113826..e3d5f413c77 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -30,7 +30,7 @@
}
if (!triggered) {
return $.cookie("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'), {
- path: '/'
+ path: gon.relative_url_root || '/'
});
}
});
diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js
index 6e677fa8cc6..23eda7d44ca 100644
--- a/app/assets/javascripts/todos.js
+++ b/app/assets/javascripts/todos.js
@@ -13,6 +13,7 @@
this.perPage = this.el.data('perPage');
this.clearListeners();
this.initBtnListeners();
+ this.initFilters();
}
Todos.prototype.clearListeners = function() {
@@ -27,6 +28,31 @@
return $('.todo').on('click', this.goToTodoUrl);
};
+ Todos.prototype.initFilters = function() {
+ new UsersSelect();
+ this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
+ this.initFilterDropdown($('.js-type-search'), 'type');
+ this.initFilterDropdown($('.js-action-search'), 'action_id');
+
+ $('form.filter-form').on('submit', function (event) {
+ event.preventDefault();
+ Turbolinks.visit(this.action + '&' + $(this).serialize());
+ });
+ };
+
+ Todos.prototype.initFilterDropdown = function($dropdown, fieldName, searchFields) {
+ $dropdown.glDropdown({
+ selectable: true,
+ filterable: searchFields ? true : false,
+ fieldName: fieldName,
+ search: { fields: searchFields },
+ data: $dropdown.data('data'),
+ clicked: function() {
+ return $dropdown.closest('form.filter-form').submit();
+ }
+ })
+ };
+
Todos.prototype.doneClicked = function(e) {
var $this;
e.preventDefault();
@@ -66,7 +92,7 @@
success: (function(_this) {
return function(data) {
$this.remove();
- $('.js-todos-list').remove();
+ $('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>');
return _this.updateBadges(data);
};
})(this)
diff --git a/app/assets/javascripts/user.js b/app/assets/javascripts/user.js
deleted file mode 100644
index b46390ad8f4..00000000000
--- a/app/assets/javascripts/user.js
+++ /dev/null
@@ -1,31 +0,0 @@
-(function() {
- this.User = (function() {
- function User(opts) {
- this.opts = opts;
- $('.profile-groups-avatars').tooltip({
- "placement": "top"
- });
- this.initTabs();
- $('.hide-project-limit-message').on('click', function(e) {
- var path;
- path = '/';
- $.cookie('hide_project_limit_message', 'false', {
- path: path
- });
- $(this).parents('.project-limit-message').remove();
- return e.preventDefault();
- });
- }
-
- User.prototype.initTabs = function() {
- return new UserTabs({
- parentEl: '.user-profile',
- action: this.opts.action
- });
- };
-
- return User;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js.es6
new file mode 100644
index 00000000000..6889d3a7491
--- /dev/null
+++ b/app/assets/javascripts/user.js.es6
@@ -0,0 +1,34 @@
+(global => {
+ global.User = class {
+ constructor(opts) {
+ this.opts = opts;
+ this.placeProfileAvatarsToTop();
+ this.initTabs();
+ this.hideProjectLimitMessage();
+ }
+
+ placeProfileAvatarsToTop() {
+ $('.profile-groups-avatars').tooltip({
+ placement: 'top'
+ });
+ }
+
+ initTabs() {
+ return new UserTabs({
+ parentEl: '.user-profile',
+ action: this.opts.action
+ });
+ }
+
+ hideProjectLimitMessage() {
+ $('.hide-project-limit-message').on('click', e => {
+ e.preventDefault();
+ const path = gon.relative_url_root || '/';
+ $.cookie('hide_project_limit_message', 'false', {
+ path: path
+ });
+ $(this).parents('.project-limit-message').remove();
+ });
+ }
+ }
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js
index 8b3dbf5f5ae..74ecf4f4cf9 100644
--- a/app/assets/javascripts/users/calendar.js
+++ b/app/assets/javascripts/users/calendar.js
@@ -3,7 +3,6 @@
this.Calendar = (function() {
function Calendar(timestamps, calendar_activities_path) {
- var group, i;
this.calendar_activities_path = calendar_activities_path;
this.clickDay = bind(this.clickDay, this);
this.currentSelectedDate = '';
@@ -13,26 +12,36 @@
this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
this.months = [];
this.timestampsTmp = [];
- i = 0;
- group = 0;
- _.each(timestamps, (function(_this) {
- return function(count, date) {
- var day, innerArray, newDate;
- newDate = new Date(parseInt(date) * 1000);
- day = newDate.getDay();
- if ((day === 0 && i !== 0) || i === 0) {
- _this.timestampsTmp.push([]);
- group++;
- }
- innerArray = _this.timestampsTmp[group - 1];
- innerArray.push({
- count: count,
- date: newDate,
- day: day
- });
- return i++;
- };
- })(this));
+ var group = 0;
+
+ var today = new Date()
+ today.setHours(0, 0, 0, 0, 0);
+
+ var oneYearAgo = new Date(today);
+ oneYearAgo.setFullYear(today.getFullYear() - 1);
+
+ var days = gl.utils.getDayDifference(oneYearAgo, today);
+
+ for(var i = 0; i <= days; i++) {
+ var date = new Date(oneYearAgo);
+ date.setDate(date.getDate() + i);
+
+ var day = date.getDay();
+ var count = timestamps[date.getTime() * 0.001];
+
+ if ((day === 0 && i !== 0) || i === 0) {
+ this.timestampsTmp.push([]);
+ group++;
+ }
+
+ var innerArray = this.timestampsTmp[group - 1];
+ innerArray.push({
+ count: count || 0,
+ date: date,
+ day: day
+ });
+ }
+
this.colorKey = this.initColorKey();
this.color = this.initColor();
this.renderSvg(group);
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index a306b8f3f29..d5cca1b10fb 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -24,6 +24,7 @@
@import "framework/issue_box.scss";
@import "framework/jquery.scss";
@import "framework/lists.scss";
+@import "framework/logo.scss";
@import "framework/markdown_area.scss";
@import "framework/mobile.scss";
@import "framework/modal.scss";
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 6c3786b49bb..4618687a4be 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -200,13 +200,15 @@
svg {
height: 15px;
- width: auto;
+ width: 15px;
position: relative;
top: 2px;
}
svg, .fa {
- margin-right: 3px;
+ &:not(:last-child) {
+ margin-right: 3px;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index c1e5305644b..5957dce89bc 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -53,7 +53,7 @@ pre {
&.well-pre {
border: 1px solid #eee;
- background: #f9f9f9;
+ background: $gray-light;
border-radius: 0;
color: #555;
}
@@ -225,7 +225,7 @@ li.note {
.milestone {
&.milestone-closed {
- background: #f9f9f9;
+ background: $gray-light;
}
.progress {
margin-bottom: 0;
@@ -248,7 +248,7 @@ li.note {
img.emoji {
height: 20px;
- vertical-align: middle;
+ vertical-align: top;
width: 20px;
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 7d3a063d6c2..b0ba112476b 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -17,6 +17,12 @@
.dropdown {
position: relative;
+
+ .btn-link {
+ &:hover {
+ cursor: pointer;
+ }
+ }
}
.open {
@@ -177,6 +183,13 @@
&.dropdown-menu-user-link {
line-height: 16px;
}
+
+ .icon-play {
+ fill: $table-text-gray;
+ margin-right: 6px;
+ height: 12px;
+ width: 11px;
+ }
}
.dropdown-header {
@@ -189,6 +202,12 @@
.separator + .dropdown-header {
padding-top: 2px;
}
+
+ .unclickable {
+ cursor: not-allowed;
+ padding: 5px 8px;
+ color: $dropdown-header-color;
+ }
}
.dropdown-menu-large {
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index d3e3fc50736..e3be45ba1dc 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -115,7 +115,7 @@
padding: 0;
}
td.blame-commit {
- background: #f9f9f9;
+ background: $gray-light;
min-width: 350px;
.commit-author-link {
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 43d55661541..37ff7e22ed1 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -19,7 +19,6 @@ input[type='text'].danger {
}
.form-actions {
- margin: -$gl-padding;
margin-top: 0;
margin-bottom: -$gl-padding;
padding: $gl-padding;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 0c607071840..1036219172e 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -2,16 +2,6 @@
* Application Header
*
*/
-@mixin tanuki-logo-colors($path-color) {
- fill: $path-color;
- transition: all 0.8s;
-
- &:hover,
- &.highlight {
- fill: lighten($path-color, 25%);
- transition: all 0.1s;
- }
-}
header {
transition: padding $sidebar-transition-duration;
@@ -25,7 +15,7 @@ header {
margin: 8px 0;
text-align: center;
- #tanuki-logo, img {
+ .tanuki-logo, img {
height: 36px;
}
}
@@ -94,7 +84,7 @@ header {
.side-nav-toggle {
position: absolute;
left: -10px;
- margin: 6px 0;
+ margin: 7px 0;
font-size: 18px;
padding: 6px 10px;
border: none;
@@ -146,6 +136,8 @@ header {
}
.title {
+ position: relative;
+ padding-right: 20px;
margin: 0;
font-size: 19px;
max-width: 400px;
@@ -158,7 +150,11 @@ header {
vertical-align: top;
white-space: nowrap;
- @media (max-width: $screen-sm-max) {
+ @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+ max-width: 300px;
+ }
+
+ @media (max-width: $screen-xs-max) {
max-width: 190px;
}
@@ -170,11 +166,15 @@ header {
}
.dropdown-toggle-caret {
- position: relative;
- top: -2px;
+ color: $gl-text-color;
+ border: transparent;
+ background: transparent;
+ position: absolute;
+ right: 3px;
width: 12px;
- line-height: 12px;
- margin-left: 5px;
+ line-height: 19px;
+ margin-top: (($header-height - 19) / 2);
+ padding: 0;
font-size: 10px;
text-align: center;
cursor: pointer;
@@ -205,26 +205,6 @@ header {
}
}
-#tanuki-logo {
-
- #tanuki-left-ear,
- #tanuki-right-ear,
- #tanuki-nose {
- @include tanuki-logo-colors($tanuki-red);
- }
-
- #tanuki-left-eye,
- #tanuki-right-eye {
- @include tanuki-logo-colors($tanuki-orange);
- }
-
- #tanuki-left-cheek,
- #tanuki-right-cheek {
- @include tanuki-logo-colors($tanuki-yellow);
- }
-
-}
-
@media (max-width: $screen-xs-max) {
header .container-fluid {
font-size: 18px;
diff --git a/app/assets/stylesheets/framework/logo.scss b/app/assets/stylesheets/framework/logo.scss
new file mode 100644
index 00000000000..3ee3fb4cee5
--- /dev/null
+++ b/app/assets/stylesheets/framework/logo.scss
@@ -0,0 +1,118 @@
+@mixin unique-keyframes {
+ $animation-name: unique-id();
+ @include webkit-prefix(animation-name, $animation-name);
+
+ @-webkit-keyframes #{$animation-name} {
+ @content;
+ }
+ @keyframes #{$animation-name} {
+ @content;
+ }
+}
+
+@mixin tanuki-logo-colors($path-color) {
+ fill: $path-color;
+ transition: all 0.8s;
+
+ &:hover {
+ fill: lighten($path-color, 25%);
+ transition: all 0.1s;
+ }
+}
+
+@mixin tanuki-second-highlight-animations($tanuki-color) {
+ @include unique-keyframes {
+ 10%, 80% {
+ fill: #{$tanuki-color}
+ }
+ 20%, 90% {
+ fill: lighten($tanuki-color, 25%);
+ }
+ }
+}
+
+@mixin tanuki-forth-highlight-animations($tanuki-color) {
+ @include unique-keyframes {
+ 30%, 60% {
+ fill: #{$tanuki-color};
+ }
+ 40%, 70% {
+ fill: lighten($tanuki-color, 25%);
+ }
+ }
+}
+
+.tanuki-logo {
+
+ .tanuki-left-ear,
+ .tanuki-right-ear,
+ .tanuki-nose {
+ @include tanuki-logo-colors($tanuki-red);
+ }
+
+ .tanuki-left-eye,
+ .tanuki-right-eye {
+ @include tanuki-logo-colors($tanuki-orange);
+ }
+
+ .tanuki-left-cheek,
+ .tanuki-right-cheek {
+ @include tanuki-logo-colors($tanuki-yellow);
+ }
+
+ &.animate {
+ .tanuki-shape {
+ @include webkit-prefix(animation-duration, 1.5s);
+ @include webkit-prefix(animation-iteration-count, infinite);
+ }
+
+ .tanuki-left-cheek {
+ @include unique-keyframes {
+ 0%, 10%, 100% {
+ fill: lighten($tanuki-yellow, 25%);
+ }
+ 90% {
+ fill: $tanuki-yellow;
+ }
+ }
+ }
+
+ .tanuki-left-eye {
+ @include tanuki-second-highlight-animations($tanuki-orange);
+ }
+
+ .tanuki-left-ear {
+ @include tanuki-second-highlight-animations($tanuki-red);
+ }
+
+ .tanuki-nose {
+ @include unique-keyframes {
+ 20%, 70% {
+ fill: $tanuki-red;
+ }
+ 30%, 80% {
+ fill: lighten($tanuki-red, 25%);
+ }
+ }
+ }
+
+ .tanuki-right-eye {
+ @include tanuki-forth-highlight-animations($tanuki-orange);
+ }
+
+ .tanuki-right-ear {
+ @include tanuki-forth-highlight-animations($tanuki-red);
+ }
+
+ .tanuki-right-cheek {
+ @include unique-keyframes {
+ 40% {
+ fill: $tanuki-yellow;
+ }
+ 60% {
+ fill: lighten($tanuki-yellow, 25%);
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index d2d60ed7196..00f92cef9a4 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -9,43 +9,11 @@
border-radius: $radius;
}
-@mixin border-radius-left($radius) {
- @include border-radius($radius 0 0 $radius)
-}
-
-@mixin border-radius-right($radius) {
- @include border-radius(0 0 $radius $radius)
-}
-
-@mixin linear-gradient($from, $to) {
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from($from), to($to));
- background-image: -webkit-linear-gradient($from, $to);
- background-image: -moz-linear-gradient($from, $to);
- background-image: -ms-linear-gradient($from, $to);
- background-image: -o-linear-gradient($from, $to);
-}
-
-@mixin transition($transition) {
- -webkit-transition: $transition;
- -moz-transition: $transition;
- -ms-transition: $transition;
- -o-transition: $transition;
- transition: $transition;
-}
-
/**
* Prefilled mixins
* Mixins with fixed values
*/
-@mixin shade {
- @include box-shadow(0 0 3px #ddd);
-}
-
-@mixin solid-shade {
- @include box-shadow(0 0 0 3px #f1f1f1);
-}
-
@mixin str-truncated($max_width: 82%) {
display: inline-block;
overflow: hidden;
@@ -76,7 +44,7 @@
}
&.active {
- background: #f9f9f9;
+ background: $gray-light;
a {
font-weight: 600;
}
@@ -94,23 +62,6 @@
}
}
-@mixin input-big {
- height: 36px;
- padding: 5px 10px;
- font-size: 16px;
- line-height: 24px;
- color: #7f8fa4;
- background-color: #fff;
- border-color: #e7e9ed;
-}
-
-@mixin btn-big {
- height: 36px;
- padding: 5px 10px;
- font-size: 16px;
- line-height: 24px;
-}
-
@mixin bulleted-list {
> ul {
list-style-type: disc;
@@ -129,3 +80,8 @@
color: rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.1);
}
+
+@mixin webkit-prefix($property, $value) {
+ #{'-webkit-' + $property}: $value;
+ #{$property}: $value;
+}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 9e924f99e9c..9efbaf54e90 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -1,4 +1,4 @@
-@mixin fade($gradient-direction, $rgba, $gradient-color) {
+@mixin fade($gradient-direction, $gradient-color) {
visibility: hidden;
opacity: 0;
z-index: 2;
@@ -8,10 +8,7 @@
height: 30px;
transition-duration: .3s;
-webkit-transform: translateZ(0);
- background: -webkit-linear-gradient($gradient-direction, $rgba, $gradient-color 45%);
- background: -o-linear-gradient($gradient-direction, $rgba, $gradient-color 45%);
- background: -moz-linear-gradient($gradient-direction, $rgba, $gradient-color 45%);
- background: linear-gradient($gradient-direction, $rgba, $gradient-color 45%);
+ background: linear-gradient(to $gradient-direction, $gradient-color 45%, rgba($gradient-color, 0.4));
&.scrolling {
visibility: visible;
@@ -71,7 +68,7 @@
.badge {
font-weight: normal;
background-color: #eee;
- color: #78a;
+ color: $btn-transparent-color;
vertical-align: baseline;
}
}
@@ -161,6 +158,7 @@
> .dropdown {
margin-right: $gl-padding-top;
display: inline-block;
+ vertical-align: top;
&:last-child {
margin-right: 0;
@@ -210,12 +208,6 @@
}
}
- .project-filter-form {
- input {
- background-color: $background-color;
- }
- }
-
@media (max-width: $screen-xs-max) {
padding-bottom: 0;
width: 100%;
@@ -335,10 +327,6 @@
}
}
- .badge {
- color: $gl-icon-color;
- }
-
&:hover {
a, i {
color: $black;
@@ -356,7 +344,7 @@
}
.fade-right {
- @include fade(left, rgba(255, 255, 255, 0.4), $background-color);
+ @include fade(left, $background-color);
right: -5px;
.fa {
@@ -365,7 +353,7 @@
}
.fade-left {
- @include fade(right, rgba(255, 255, 255, 0.4), $background-color);
+ @include fade(right, $background-color);
left: -5px;
.fa {
@@ -376,6 +364,7 @@
&.sub-nav-scroll {
.fade-right {
+ @include fade(left, $dark-background-color);
right: 0;
.fa {
@@ -384,6 +373,7 @@
}
.fade-left {
+ @include fade(right, $dark-background-color);
left: 0;
.fa {
@@ -400,7 +390,7 @@
@include scrolling-links();
.fade-right {
- @include fade(left, rgba(255, 255, 255, 0.4), $white-light);
+ @include fade(left, $white-light);
right: -5px;
.fa {
@@ -409,7 +399,7 @@
}
.fade-left {
- @include fade(right, rgba(255, 255, 255, 0.4), $white-light);
+ @include fade(right, $white-light);
left: -5px;
.fa {
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index b2e22b60440..c75dacf95d9 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -151,7 +151,7 @@
background-position: right 0 bottom 6px;
border: 1px solid $input-border;
@include border-radius($border-radius-default);
- @include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s);
+ transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
&:focus {
border-color: $input-border-focus;
diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index 371c1bf17e1..915aa631ef8 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -125,7 +125,7 @@ $panel-inner-border: $border-color;
//
//##
-$well-bg: #f9f9f9;
+$well-bg: $gray-light;
$well-border: #eee;
//== Code
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 06874a993fa..3f8433a0e7f 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -159,25 +159,18 @@
position: relative;
a.anchor {
- // Setting `display: none` would prevent the anchor being scrolled to, so
- // instead we set the height to 0 and it gets updated on hover.
- height: 0;
+ left: -16px;
+ position: absolute;
+ text-decoration: none;
+
+ &:after {
+ content: url('icon_anchor.svg');
+ visibility: hidden;
+ }
}
- &:hover > a.anchor {
- $size: 14px;
- position: absolute;
- right: 100%;
- top: 50%;
- margin-top: -11px;
- margin-right: 0;
- padding-right: 15px;
- display: inline-block;
- width: $size;
- height: $size;
- background-image: image-url("icon-link.png");
- background-size: contain;
- background-repeat: no-repeat;
+ &:hover > a.anchor:after {
+ visibility: visible;
}
}
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 5da390118c6..e4a3ca3a9ea 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -10,12 +10,78 @@ $sidebar-transition-duration: .15s;
$sidebar-breakpoint: 1024px;
/*
+ * Color schema
+ */
+$white-light: #fff;
+$white-normal: #ededed;
+$white-dark: #ececec;
+
+$gray-light: #fafafa;
+$gray-normal: #f5f5f5;
+$gray-dark: #ededed;
+$gray-darkest: #c9c9c9;
+
+$green-light: #38ae67;
+$green-normal: #2faa60;
+$green-dark: #2ca05b;
+
+$blue-light: #2ea8e5;
+$blue-normal: #2d9fd8;
+$blue-dark: #2897ce;
+
+$blue-medium-light: #3498cb;
+$blue-medium: #2f8ebf;
+$blue-medium-dark: #2d86b4;
+
+$orange-light: #fc8a51;
+$orange-normal: #e75e40;
+$orange-dark: #ce5237;
+
+$red-light: #e52c5a;
+$red-normal: #d22852;
+$red-dark: darken($red-normal, 5%);
+
+$black: #000;
+$black-transparent: rgba(0, 0, 0, 0.3);
+
+$border-white-light: #f1f2f4;
+$border-white-normal: #d6dae2;
+$border-white-dark: #c6cacf;
+
+$border-gray-light: #dcdcdc;
+$border-gray-normal: #d7d7d7;
+$border-gray-dark: #c6cacf;
+
+$border-green-light: #2faa60;
+$border-green-normal: #2ca05b;
+$border-green-dark: #279654;
+
+$border-blue-light: #2d9fd8;
+$border-blue-normal: #2897ce;
+$border-blue-dark: #258dc1;
+
+$border-orange-light: #fc6d26;
+$border-orange-normal: #ce5237;
+$border-orange-dark: #c14e35;
+
+$border-red-light: #d22852;
+$border-red-normal: #ca264f;
+$border-red-dark: darken($border-red-normal, 5%);
+
+$help-well-bg: $gray-light;
+$help-well-border: #e5e5e5;
+
+$warning-message-bg: #fbf2d9;
+$warning-message-color: #9e8e60;
+$warning-message-border: #f0e2bb;
+
+/*
* UI elements
*/
$border-color: #e5e5e5;
$focus-border-color: #3aabf0;
$table-border-color: #f0f0f0;
-$background-color: #fafafa;
+$background-color: $gray-light;
$dark-background-color: #f5f5f5;
$table-text-gray: #8f8f8f;
@@ -90,73 +156,6 @@ $btn-side-margin: 10px;
$btn-sm-side-margin: 7px;
$btn-xs-side-margin: 5px;
-/*
- * Color schema
- */
-
-$white-light: #fff;
-$white-normal: #ededed;
-$white-dark: #ececec;
-
-$gray-light: #faf9f9;
-$gray-normal: #f5f5f5;
-$gray-dark: #ededed;
-$gray-darkest: #c9c9c9;
-
-$green-light: #38ae67;
-$green-normal: #2faa60;
-$green-dark: #2ca05b;
-
-$blue-light: #2ea8e5;
-$blue-normal: #2d9fd8;
-$blue-dark: #2897ce;
-
-$blue-medium-light: #3498cb;
-$blue-medium: #2f8ebf;
-$blue-medium-dark: #2d86b4;
-
-$orange-light: #fc8a51;
-$orange-normal: #e75e40;
-$orange-dark: #ce5237;
-
-$red-light: #e52c5a;
-$red-normal: #d22852;
-$red-dark: darken($red-normal, 5%);
-
-$black: #000;
-$black-transparent: rgba(0, 0, 0, 0.3);
-
-$border-white-light: #f1f2f4;
-$border-white-normal: #d6dae2;
-$border-white-dark: #c6cacf;
-
-$border-gray-light: #dcdcdc;
-$border-gray-normal: #d7d7d7;
-$border-gray-dark: #c6cacf;
-
-$border-green-light: #2faa60;
-$border-green-normal: #2ca05b;
-$border-green-dark: #279654;
-
-$border-blue-light: #2d9fd8;
-$border-blue-normal: #2897ce;
-$border-blue-dark: #258dc1;
-
-$border-orange-light: #fc6d26;
-$border-orange-normal: #ce5237;
-$border-orange-dark: #c14e35;
-
-$border-red-light: #d22852;
-$border-red-normal: #ca264f;
-$border-red-dark: darken($border-red-normal, 5%);
-
-$help-well-bg: #fafafa;
-$help-well-border: #e5e5e5;
-
-$warning-message-bg: #fbf2d9;
-$warning-message-color: #9e8e60;
-$warning-message-border: #f0e2bb;
-
/* tanuki logo colors */
$tanuki-red: #e24329;
$tanuki-orange: #fc6d26;
@@ -186,7 +185,7 @@ $line-removed-dark: #fac5cd;
$line-number-old: #f9d7dc;
$line-number-new: #ddfbe6;
$line-number-select: #fbf2da;
-$match-line: #fafafa;
+$match-line: $gray-light;
$table-border-gray: #f0f0f0;
$line-target-blue: #eaf3fc;
$line-select-yellow: #fcf8e7;
@@ -267,7 +266,7 @@ $zen-control-hover-color: #111;
$calendar-header-color: #b8b8b8;
$calendar-hover-bg: #ecf3fe;
$calendar-border-color: rgba(#000, .1);
-$calendar-unselectable-bg: #faf9f9;
+$calendar-unselectable-bg: $gray-light;
/*
* Personal Access Tokens
diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss
index c9cdfdcd29c..8f71381f5c4 100644
--- a/app/assets/stylesheets/pages/admin.scss
+++ b/app/assets/stylesheets/pages/admin.scss
@@ -96,6 +96,10 @@
line-height: inherit;
}
}
+
+ .label-default {
+ color: $btn-transparent-color;
+ }
}
.abuse-reports {
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 9ac4d801ac4..037278bb083 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -10,7 +10,7 @@
.is-dragging {
// Important because plugin sets inline CSS
opacity: 1!important;
-
+
* {
// !important to make sure no style can override this when dragging
cursor: -webkit-grabbing!important;
@@ -142,11 +142,6 @@
}
}
-.board-header-loading-spinner {
- margin-right: 10px;
- color: $gray-darkest;
-}
-
.board-inner-container {
border-bottom: 1px solid $border-color;
padding: $gl-padding;
@@ -160,40 +155,6 @@
border-bottom: 1px solid $border-color;
}
-.board-search-container {
- position: relative;
- background-color: #fff;
-
- .form-control {
- padding-right: 30px;
- }
-}
-
-.board-search-icon,
-.board-search-clear-btn {
- position: absolute;
- right: $gl-padding + 10px;
- top: 50%;
- margin-top: -7px;
- font-size: 14px;
-}
-
-.board-search-icon {
- color: $gl-placeholder-color;
-}
-
-.board-search-clear-btn {
- padding: 0;
- line-height: 1;
- background: transparent;
- border: 0;
- outline: 0;
-
- &:hover {
- color: $gl-link-color;
- }
-}
-
.board-delete {
margin-right: 10px;
padding: 0;
@@ -304,3 +265,22 @@
margin-right: 8px;
font-weight: 500;
}
+
+.issue-boards-search {
+ width: 335px;
+
+ .form-control {
+ display: inline-block;
+ width: 210px;
+ }
+}
+
+.board-list-count {
+ padding: 10px 0;
+ color: $gl-placeholder-color;
+ font-size: 13px;
+
+ > .fa {
+ margin-right: 5px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index c1bb250b42d..23255f34710 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -36,6 +36,7 @@
&.affix {
right: 30px;
bottom: 15px;
+ z-index: 1;
@media (min-width: $screen-md-min) {
right: 26%;
@@ -107,7 +108,7 @@
}
.blocks-container {
- padding: $gl-padding;
+ padding: 0 $gl-padding;
}
.block {
@@ -122,6 +123,13 @@
}
}
+ .retry-link {
+ color: $gl-link-color;
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
.stage-item {
cursor: pointer;
@@ -131,7 +139,7 @@
}
.build-dropdown {
- padding: 0 $gl-padding;
+ padding: $gl-padding 0;
.dropdown-menu-toggle {
margin-top: 8px;
@@ -145,12 +153,11 @@
}
.builds-container {
- margin-top: $gl-padding;
background-color: $white-light;
border-top: 1px solid $border-color;
border-bottom: 1px solid $border-color;
max-height: 300px;
- overflow: scroll;
+ overflow: auto;
svg {
position: relative;
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 6a58b445afa..b369f5c0805 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -113,11 +113,13 @@
.commit-row-description {
font-size: 14px;
- border-left: 1px solid #eee;
+ border-left: 1px solid $btn-gray-hover;
padding: 10px 15px;
margin: 10px 0;
- background: #f9f9f9;
+ background: $gray-light;
display: none;
+ white-space: pre-line;
+ word-break: normal;
pre {
border: none;
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 55f9d4a0011..d01c60ee6ab 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -4,8 +4,9 @@
margin: 0;
}
- .fa-play {
- font-size: 14px;
+ .icon-play {
+ height: 13px;
+ width: 12px;
}
.dropdown-new {
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 5c336bb1c7e..1d00da1266c 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -60,7 +60,7 @@
pre {
border: none;
- background: #f9f9f9;
+ background: $gray-light;
border-radius: 0;
color: #777;
margin: 0 20px;
@@ -92,7 +92,7 @@
border: 1px solid #eee;
padding: 5px;
@include border-radius(5px);
- background: #f9f9f9;
+ background: $gray-light;
margin-left: 10px;
top: -6px;
img {
@@ -115,11 +115,8 @@
}
&.commits-stat {
- margin-top: 3px;
display: block;
- padding: 3px;
- padding-left: 0;
-
+ padding: 0 3px 0 0;
&:hover {
background: none;
}
diff --git a/app/assets/stylesheets/pages/import.scss b/app/assets/stylesheets/pages/import.scss
index 84cc35239f9..a4f76a9495a 100644
--- a/app/assets/stylesheets/pages/import.scss
+++ b/app/assets/stylesheets/pages/import.scss
@@ -1,22 +1,3 @@
-i.icon-gitorious {
- display: inline-block;
- background-position: 0 0;
- background-size: contain;
- background-repeat: no-repeat;
-}
-
-i.icon-gitorious-small {
- background-image: image-url('gitorious-logo-blue.png');
- width: 13px;
- height: 13px;
-}
-
-i.icon-gitorious-big {
- background-image: image-url('gitorious-logo-black.png');
- width: 18px;
- height: 18px;
-}
-
.import-jobs-from-col,
.import-jobs-to-col {
width: 40%;
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index dfe1e3075da..910700b0206 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -12,6 +12,10 @@
padding-right: 8px;
margin-bottom: 10px;
min-width: 15px;
+
+ .selected_issue {
+ vertical-align: text-top;
+ }
}
.issue-labels {
@@ -68,12 +72,12 @@ form.edit-issue {
}
&.closed {
- background: #f9f9f9;
+ background: $gray-light;
border-color: #e5e5e5;
}
&.merged {
- background: #f9f9f9;
+ background: $gray-light;
border-color: #e5e5e5;
}
}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 606459f82cd..38c7cd98e41 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -7,6 +7,7 @@
display: inline-block;
margin-right: 10px;
margin-bottom: 10px;
+ text-decoration: none;
}
&.suggest-colors-dropdown {
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
index 1f499897c16..5ec660799e3 100644
--- a/app/assets/stylesheets/pages/merge_conflicts.scss
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -16,7 +16,7 @@ $colors: (
white_button_origin_chosen : #268ced,
white_header_not_chosen : #f0f0f0,
- white_line_not_chosen : #f9f9f9,
+ white_line_not_chosen : $gray-light,
dark_header_head_neutral : rgba(#3f3, .2),
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index fcdaf671538..7fdd79fa8b9 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -269,7 +269,7 @@
.builds {
.table-holder {
- overflow-x: scroll;
+ overflow-x: auto;
}
}
@@ -375,6 +375,16 @@
}
}
+.mr-version-switch {
+ background: $background-color;
+ padding: $gl-btn-padding;
+ color: $gl-placeholder-color;
+
+ a.btn-link {
+ color: $gl-dark-link-color;
+ }
+}
+
.merge-request-details {
.title {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 08d1692c888..54124a3d658 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -281,19 +281,13 @@ ul.notes {
font-size: 17px;
}
- &.js-note-delete {
- i {
- &:hover {
- color: $gl-text-red;
- }
+ &:hover {
+ .danger-highlight {
+ color: $gl-text-red;
}
- }
- &.js-note-edit {
- i {
- &:hover {
- color: $gl-link-color;
- }
+ .link-highlight {
+ color: $gl-link-color;
}
}
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 6fa097e3bf1..ee5d9de66d8 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -2,6 +2,7 @@
.stage {
max-width: 90px;
width: 90px;
+ text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -146,6 +147,7 @@
}
.stage-cell {
+ text-align: center;
svg {
height: 18px;
@@ -153,10 +155,6 @@
vertical-align: middle;
overflow: visible;
}
-
- .light {
- width: 3px;
- }
}
.duration,
@@ -215,6 +213,13 @@
border-color: $border-white-normal;
}
}
+
+ .btn {
+ .icon-play {
+ height: 13px;
+ width: 12px;
+ }
+ }
}
}
@@ -254,7 +259,6 @@
width: 100%;
overflow: auto;
white-space: nowrap;
- max-height: 500px;
transition: max-height 0.3s, padding 0.3s;
&.graph-collapsed {
@@ -265,7 +269,6 @@
.pipeline-visualization {
position: relative;
- min-width: 1220px;
ul {
padding: 0;
@@ -275,7 +278,7 @@
.stage-column {
display: inline-block;
vertical-align: top;
- margin-right: 50px;
+ margin-right: 65px;
li {
list-style: none;
@@ -321,6 +324,14 @@
a {
color: $layout-link-gray;
+ text-decoration: none;
+
+ &:hover {
+ .ci-status-text {
+ text-decoration: underline;
+ }
+ }
+
}
}
@@ -336,9 +347,9 @@
content: '';
position: absolute;
top: 50%;
- right: -54px;
+ right: -69px;
border-top: 2px solid $border-color;
- width: 54px;
+ width: 69px;
height: 1px;
}
}
@@ -358,22 +369,25 @@
&::after {
right: -20px;
border-right: 2px solid $border-color;
- border-radius: 0 0 50px;
+ border-radius: 0 0 15px;
}
// Left connecting curves
&::before {
left: -20px;
border-left: 2px solid $border-color;
- border-radius: 0 0 0 50px;
+ border-radius: 0 0 0 15px;
}
}
// Connect second build to first build with smaller curved line
&:nth-child(2) {
&::after, &::before {
- height: 45px;
- top: -26px;
+ height: 29px;
+ top: -10px;
+ }
+ .curve {
+ display: block;
}
}
}
@@ -392,6 +406,12 @@
border: none;
}
}
+ // Remove opposite curve
+ .curve {
+ &::before {
+ display: none;
+ }
+ }
}
}
@@ -403,6 +423,39 @@
border: none;
}
}
+ // Remove opposite curve
+ .curve {
+ &::after {
+ display: none;
+ }
+ }
+ }
+ }
+
+ // Curve first child connecting lines in opposite direction
+ .curve {
+ display: none;
+
+ &::before,
+ &::after {
+ content: '';
+ width: 21px;
+ height: 25px;
+ position: absolute;
+ top: -28.5px;
+ border-top: 2px solid $border-color;
+ }
+
+ &::after {
+ left: -39px;
+ border-right: 2px solid $border-color;
+ border-radius: 0 15px;
+ }
+
+ &::before {
+ right: -39px;
+ border-left: 2px solid $border-color;
+ border-radius: 15px 0 0;
}
}
}
@@ -421,11 +474,22 @@
.pipelines.tab-pane {
.content-list.pipelines {
- overflow: scroll;
+ overflow: auto;
}
.stage {
- max-width: 60px;
- width: 60px;
+ max-width: 100px;
+ width: 100px;
+ }
+
+ .pipeline-actions {
+ min-width: initial;
+ }
+}
+
+.ci-status-icon-created {
+
+ svg {
+ fill: $table-text-gray;
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index eaf2d3270b3..f2db373da52 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -311,6 +311,14 @@ a.deploy-project-label {
color: $gl-success;
}
+.lfs-enabled {
+ color: $gl-success;
+}
+
+.lfs-disabled {
+ color: $gl-warning;
+}
+
.breadcrumb.repo-breadcrumb {
padding: 0;
background: transparent;
@@ -600,18 +608,25 @@ pre.light-well {
}
}
-.project-show-readme .readme-holder {
- padding: $gl-padding 0;
- border-top: 0;
-
- .edit-project-readme {
- z-index: 2;
- position: relative;
+.project-show-readme {
+ .row-content-block {
+ background-color: inherit;
+ border: none;
}
- .wiki h1 {
- border-bottom: none;
- padding: 0;
+ .readme-holder {
+ padding: $gl-padding 0;
+ border-top: 0;
+
+ .edit-project-readme {
+ z-index: 2;
+ position: relative;
+ }
+
+ .wiki h1 {
+ border-bottom: none;
+ padding: 0;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index c9d436d72ba..436fb00ba2e 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -80,7 +80,7 @@
.search-icon {
@extend .fa-search;
- @include transition(color .15s);
+ transition: color 0.15s;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
@@ -125,7 +125,7 @@
}
.location-badge {
- @include transition(all .15s);
+ transition: all 0.15s;
background-color: $location-badge-active-bg;
color: $white-light;
}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 587f2d9f3c1..0ee7ceecae5 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -43,6 +43,15 @@
border-color: $blue-normal;
}
+ &.ci-created {
+ color: $table-text-gray;
+ border-color: $table-text-gray;
+
+ svg {
+ fill: $table-text-gray;
+ }
+ }
+
svg {
height: 13px;
width: 13px;
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 0340526a53a..68a5d1ae06c 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -99,7 +99,7 @@
pre {
border: none;
- background: #f9f9f9;
+ background: $gray-light;
border-radius: 0;
color: #777;
margin: 0 20px;
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 9da40fe2b09..cdd38442550 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -11,6 +11,20 @@
}
}
+ .add-to-tree {
+ vertical-align: top;
+ }
+
+ .last-commit {
+ max-width: 506px;
+
+ .last-commit-content {
+ @include str-truncated;
+ width: calc(100% - 140px);
+ margin-left: 3px;
+ }
+ }
+
.tree-table {
margin-bottom: 0;
diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss
index 8d855ce99b0..c9846103762 100644
--- a/app/assets/stylesheets/pages/xterm.scss
+++ b/app/assets/stylesheets/pages/xterm.scss
@@ -20,6 +20,9 @@
$l-cyan: #8abeb7;
$l-white: $ci-text-color;
+ .term-bold {
+ font-weight: bold;
+ }
.term-italic {
font-style: italic;
}
diff --git a/app/controllers/admin/system_info_controller.rb b/app/controllers/admin/system_info_controller.rb
index e4c73008826..ca04a17caa1 100644
--- a/app/controllers/admin/system_info_controller.rb
+++ b/app/controllers/admin/system_info_controller.rb
@@ -29,7 +29,8 @@ class Admin::SystemInfoController < Admin::ApplicationController
]
def show
- system_info = Vmstat.snapshot
+ @cpus = Vmstat.cpu rescue nil
+ @memory = Vmstat.memory rescue nil
mounts = Sys::Filesystem.mounts
@disks = []
@@ -50,10 +51,5 @@ class Admin::SystemInfoController < Admin::ApplicationController
rescue Sys::Filesystem::Error
end
end
-
- @cpus = system_info.cpus.length
-
- @mem_used = system_info.memory.active_bytes
- @mem_total = system_info.memory.total_bytes
end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 70a2275592b..bd4ba384b29 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -24,8 +24,8 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
- helper_method :abilities, :can?, :current_application_settings
- helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
+ helper_method :can?, :current_application_settings
+ helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
@@ -97,12 +97,8 @@ class ApplicationController < ActionController::Base
current_application_settings.after_sign_out_path.presence || new_user_session_path
end
- def abilities
- Ability.abilities
- end
-
def can?(object, action, subject)
- abilities.allowed?(object, action, subject)
+ Ability.allowed?(object, action, subject)
end
def access_denied!
@@ -250,10 +246,6 @@ class ApplicationController < ActionController::Base
Gitlab::OAuth::Provider.enabled?(:bitbucket) && Gitlab::BitbucketImport.public_key.present?
end
- def gitorious_import_enabled?
- current_application_settings.import_sources.include?('gitorious')
- end
-
def google_code_import_enabled?
current_application_settings.import_sources.include?('google_code')
end
diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb
index 036777c80c1..172d5344b7a 100644
--- a/app/controllers/concerns/toggle_award_emoji.rb
+++ b/app/controllers/concerns/toggle_award_emoji.rb
@@ -8,10 +8,14 @@ module ToggleAwardEmoji
def toggle_award_emoji
name = params.require(:name)
- awardable.toggle_award_emoji(name, current_user)
- TodoService.new.new_award_emoji(to_todoable(awardable), current_user)
+ 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)
- render json: { ok: true }
+ render json: { ok: true }
+ else
+ render json: { ok: false }
+ end
end
private
diff --git a/app/controllers/import/gitorious_controller.rb b/app/controllers/import/gitorious_controller.rb
deleted file mode 100644
index a4c4ad23027..00000000000
--- a/app/controllers/import/gitorious_controller.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-class Import::GitoriousController < Import::BaseController
- before_action :verify_gitorious_import_enabled
-
- def new
- redirect_to client.authorize_url(callback_import_gitorious_url)
- end
-
- def callback
- session[:gitorious_repos] = params[:repos]
- redirect_to status_import_gitorious_path
- end
-
- def status
- @repos = client.repos
-
- @already_added_projects = current_user.created_projects.where(import_type: "gitorious")
- already_added_projects_names = @already_added_projects.pluck(:import_source)
-
- @repos.reject! { |repo| already_added_projects_names.include? repo.full_name }
- end
-
- def jobs
- jobs = current_user.created_projects.where(import_type: "gitorious").to_json(only: [:id, :import_status])
- render json: jobs
- end
-
- def create
- @repo_id = params[:repo_id]
- repo = client.repo(@repo_id)
- @target_namespace = params[:new_namespace].presence || repo.namespace
- @project_name = repo.name
-
- namespace = get_or_create_namespace || (render and return)
-
- @project = Gitlab::GitoriousImport::ProjectCreator.new(repo, namespace, current_user).execute
- end
-
- private
-
- def client
- @client ||= Gitlab::GitoriousImport::Client.new(session[:gitorious_repos])
- end
-
- def verify_gitorious_import_enabled
- render_404 unless gitorious_import_enabled?
- end
-end
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 014b9b43ff2..66ebdcc37a7 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -37,7 +37,7 @@ class JwtController < ApplicationController
def authenticate_project(login, password)
if login == 'gitlab-ci-token'
- Project.find_by(builds_enabled: true, runners_token: password)
+ Project.with_builds_enabled.find_by(runners_token: password)
end
end
diff --git a/app/controllers/namespaces_controller.rb b/app/controllers/namespaces_controller.rb
index 5a94dcb0dbd..83eec1bf4a2 100644
--- a/app/controllers/namespaces_controller.rb
+++ b/app/controllers/namespaces_controller.rb
@@ -14,7 +14,7 @@ class NamespacesController < ApplicationController
if user
redirect_to user_path(user)
- elsif group && can?(current_user, :read_group, namespace)
+ elsif group && can?(current_user, :read_group, group)
redirect_to group_path(group)
elsif current_user.nil?
authenticate_user!
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 91315a07deb..b2ff36f6538 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -88,6 +88,6 @@ class Projects::ApplicationController < ApplicationController
end
def builds_enabled
- return render_404 unless @project.builds_enabled?
+ return render_404 unless @project.feature_available?(:builds, current_user)
end
end
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index 7241949393b..59222637961 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -1,22 +1,25 @@
class Projects::ArtifactsController < Projects::ApplicationController
+ include ExtractsPath
+
layout 'project'
before_action :authorize_read_build!
before_action :authorize_update_build!, only: [:keep]
+ before_action :extract_ref_name_and_path
before_action :validate_artifacts!
def download
- unless artifacts_file.file_storage?
- return redirect_to artifacts_file.url
+ if artifacts_file.file_storage?
+ send_file artifacts_file.path, disposition: 'attachment'
+ else
+ redirect_to artifacts_file.url
end
-
- send_file artifacts_file.path, disposition: 'attachment'
end
def browse
directory = params[:path] ? "#{params[:path]}/" : ''
@entry = build.artifacts_metadata_entry(directory)
- return render_404 unless @entry.exists?
+ render_404 unless @entry.exists?
end
def file
@@ -34,14 +37,41 @@ class Projects::ArtifactsController < Projects::ApplicationController
redirect_to namespace_project_build_path(project.namespace, project, build)
end
+ def latest_succeeded
+ target_path = artifacts_action_path(@path, project, build)
+
+ if target_path
+ redirect_to(target_path)
+ else
+ render_404
+ end
+ end
+
private
+ def extract_ref_name_and_path
+ return unless params[:ref_name_and_path]
+
+ @ref_name, @path = extract_ref(params[:ref_name_and_path])
+ end
+
def validate_artifacts!
- render_404 unless build.artifacts?
+ render_404 unless build && build.artifacts?
end
def build
- @build ||= project.builds.find_by!(id: params[:build_id])
+ @build ||= build_from_id || build_from_ref
+ end
+
+ def build_from_id
+ project.builds.find_by(id: params[:build_id]) if params[:build_id]
+ end
+
+ def build_from_ref
+ return unless @ref_name
+
+ builds = project.latest_successful_builds_for(@ref_name)
+ builds.find_by(name: params[:job])
end
def artifacts_file
diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb
index 5962f74c39b..ada7db3c552 100644
--- a/app/controllers/projects/avatars_controller.rb
+++ b/app/controllers/projects/avatars_controller.rb
@@ -4,7 +4,7 @@ class Projects::AvatarsController < Projects::ApplicationController
before_action :authorize_admin_project!, only: [:destroy]
def show
- @blob = @repository.blob_at_branch('master', @project.avatar_in_git)
+ @blob = @repository.blob_at_branch(@repository.root_ref, @project.avatar_in_git)
if @blob
headers['X-Content-Type-Options'] = 'nosniff'
diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb
index 1a4f6b50e8f..9404612a993 100644
--- a/app/controllers/projects/boards/issues_controller.rb
+++ b/app/controllers/projects/boards/issues_controller.rb
@@ -8,12 +8,15 @@ module Projects
issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute
issues = issues.page(params[:page])
- render json: issues.as_json(
- only: [:iid, :title, :confidential],
- include: {
- assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
- labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
- })
+ render json: {
+ issues: issues.as_json(
+ only: [:iid, :title, :confidential],
+ include: {
+ assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
+ labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
+ }),
+ size: issues.total_count
+ }
end
def update
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index 12195c3cbb8..77934ff9962 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -78,8 +78,8 @@ class Projects::BuildsController < Projects::ApplicationController
end
def raw
- if @build.has_trace?
- send_file @build.path_to_trace, type: 'text/plain; charset=utf-8', disposition: 'inline'
+ if @build.has_trace_file?
+ send_file @build.trace_file_path, type: 'text/plain; charset=utf-8', disposition: 'inline'
else
render_404
end
diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb
index b2e8733ccb7..d174e1145a7 100644
--- a/app/controllers/projects/discussions_controller.rb
+++ b/app/controllers/projects/discussions_controller.rb
@@ -38,6 +38,6 @@ class Projects::DiscussionsController < Projects::ApplicationController
end
def module_enabled
- render_404 unless @project.merge_requests_enabled
+ render_404 unless @project.feature_available?(:merge_requests, current_user)
end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 7b0189150f8..72d2d361878 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -201,7 +201,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def module_enabled
- return render_404 unless @project.issues_enabled && @project.default_issues_tracker?
+ return render_404 unless @project.feature_available?(:issues, current_user) && @project.default_issues_tracker?
end
def redirect_to_external_issue_tracker
@@ -212,7 +212,7 @@ class Projects::IssuesController < Projects::ApplicationController
if action_name == 'new'
redirect_to external.new_issue_path
else
- redirect_to external.issues_url
+ redirect_to external.project_path
end
end
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 0ca675623e5..28fa4a5b141 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -99,7 +99,7 @@ class Projects::LabelsController < Projects::ApplicationController
protected
def module_enabled
- unless @project.issues_enabled || @project.merge_requests_enabled
+ unless @project.feature_available?(:issues, current_user) || @project.feature_available?(:merge_requests, current_user)
return render_404
end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 6a8c7166b39..4f9ca0097a1 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -83,12 +83,22 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def diffs
apply_diff_view_cookie!
- @merge_request_diff = @merge_request.merge_request_diff
+ @merge_request_diff =
+ if params[:diff_id]
+ @merge_request.merge_request_diffs.find(params[:diff_id])
+ else
+ @merge_request.merge_request_diff
+ end
respond_to do |format|
format.html { define_discussion_vars }
format.json do
- @diffs = @merge_request.diffs(diff_options)
+ unless @merge_request_diff.latest?
+ # Disable comments if browsing older version of the diff
+ @diff_notes_disabled = true
+ end
+
+ @diffs = @merge_request_diff.diffs(diff_options)
render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") }
end
@@ -403,7 +413,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def module_enabled
- return render_404 unless @project.merge_requests_enabled
+ return render_404 unless @project.feature_available?(:merge_requests, current_user)
end
def validates_merge_request
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index da2892bfb3f..ff63f22cb5b 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -106,7 +106,7 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def module_enabled
- unless @project.issues_enabled || @project.merge_requests_enabled
+ unless @project.feature_available?(:issues, current_user) || @project.feature_available?(:merge_requests, current_user)
return render_404
end
end
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 6d0a7ee1031..17ceefec3b8 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -94,7 +94,7 @@ class Projects::SnippetsController < Projects::ApplicationController
end
def module_enabled
- return render_404 unless @project.snippets_enabled
+ return render_404 unless @project.feature_available?(:snippets, current_user)
end
def snippet_params
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 8592579abbd..6ea8ee62bc5 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -1,4 +1,6 @@
class Projects::TagsController < Projects::ApplicationController
+ include SortingHelper
+
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
@@ -6,8 +8,10 @@ class Projects::TagsController < Projects::ApplicationController
before_action :authorize_admin_project!, only: [:destroy]
def index
- @sort = params[:sort] || 'name'
- @tags = @repository.tags_sorted_by(@sort)
+ params[:sort] = params[:sort].presence || 'name'
+
+ @sort = params[:sort]
+ @tags = TagsFinder.new(@repository, params).execute
@tags = Kaminari.paginate_array(@tags).page(params[:page])
@releases = project.releases.where(tag: @tags.map(&:name))
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index fc52cd2f367..eaa38fa6c98 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -303,13 +303,23 @@ class ProjectsController < Projects::ApplicationController
end
def project_params
+ project_feature_attributes =
+ {
+ project_feature_attributes:
+ [
+ :issues_access_level, :builds_access_level,
+ :wiki_access_level, :merge_requests_access_level, :snippets_access_level
+ ]
+ }
+
params.require(:project).permit(
:name, :path, :description, :issues_tracker, :tag_list, :runners_token,
- :issues_enabled, :merge_requests_enabled, :snippets_enabled, :container_registry_enabled,
+ :container_registry_enabled,
:issues_tracker_id, :default_branch,
- :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar,
- :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
- :public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled
+ :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar,
+ :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
+ :public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled,
+ :lfs_enabled, project_feature_attributes
)
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 33daac0399e..60996b181f2 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -64,7 +64,7 @@ class IssuableFinder
if project?
@project = Project.find(params[:project_id])
- unless Ability.abilities.allowed?(current_user, :read_project, @project)
+ unless Ability.allowed?(current_user, :read_project, @project)
@project = nil
end
else
diff --git a/app/finders/tags_finder.rb b/app/finders/tags_finder.rb
new file mode 100644
index 00000000000..b474f0805dc
--- /dev/null
+++ b/app/finders/tags_finder.rb
@@ -0,0 +1,29 @@
+class TagsFinder
+ def initialize(repository, params)
+ @repository = repository
+ @params = params
+ end
+
+ def execute
+ tags = @repository.tags_sorted_by(sort)
+ filter_by_name(tags)
+ end
+
+ private
+
+ def sort
+ @params[:sort].presence
+ end
+
+ def search
+ @params[:search].presence
+ end
+
+ def filter_by_name(tags)
+ if search
+ tags.select { |tag| tag.name.include?(search) }
+ else
+ tags
+ end
+ end
+end
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index 06b3e8a9502..a93a63bdb9b 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -83,7 +83,7 @@ class TodosFinder
if project?
@project = Project.find(params[:project_id])
- unless Ability.abilities.allowed?(current_user, :read_project, @project)
+ unless Ability.allowed?(current_user, :read_project, @project)
@project = nil
end
else
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index f3733b01721..5f3765cad0d 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -110,7 +110,7 @@ module ApplicationHelper
project = event.project
# Skip if project repo is empty or MR disabled
- return false unless project && !project.empty_repo? && project.merge_requests_enabled
+ return false unless project && !project.empty_repo? && project.feature_available?(:merge_requests, current_user)
# Skip if user already created appropriate MR
return false if project.merge_requests.where(source_branch: event.branch_name).opened.any?
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index bb285a17baf..639deb7c521 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -25,6 +25,11 @@ module CiStatusHelper
end
end
+ def ci_status_for_statuseable(subject)
+ status = subject.try(:status) || 'not found'
+ status.humanize
+ end
+
def ci_icon_for_status(status)
icon_name =
case status
@@ -41,7 +46,7 @@ module CiStatusHelper
when 'play'
'icon_play'
when 'created'
- 'icon_status_pending'
+ 'icon_status_created'
else
'icon_status_cancel'
end
@@ -66,10 +71,10 @@ module CiStatusHelper
Ci::Runner.shared.blank?
end
- def render_status_with_link(type, status, path = nil, tooltip_placement: 'auto left', cssclass: '')
+ def render_status_with_link(type, status, path = nil, tooltip_placement: 'auto left', cssclass: '', container: 'body')
klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}"
title = "#{type.titleize}: #{ci_label_for_status(status)}"
- data = { toggle: 'tooltip', placement: tooltip_placement }
+ data = { toggle: 'tooltip', placement: tooltip_placement, container: container }
if path
link_to ci_icon_for_status(status), path,
diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb
index f1dc906cab4..aa54ee07bdc 100644
--- a/app/helpers/compare_helper.rb
+++ b/app/helpers/compare_helper.rb
@@ -3,7 +3,7 @@ module CompareHelper
from.present? &&
to.present? &&
from != to &&
- project.merge_requests_enabled &&
+ project.feature_available?(:merge_requests, current_user) &&
project.repository.branch_names.include?(from) &&
project.repository.branch_names.include?(to)
end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 5386ddadc62..a322a90cc4e 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -149,4 +149,20 @@ module GitlabRoutingHelper
def resend_invite_group_member_path(group_member, *args)
resend_invite_group_group_member_path(group_member.source, group_member)
end
+
+ # Artifacts
+
+ def artifacts_action_path(path, project, build)
+ action, path_params = path.split('/', 2)
+ args = [project.namespace, project, build, path_params]
+
+ case action
+ when 'download'
+ download_namespace_project_build_artifacts_path(*args)
+ when 'browse'
+ browse_namespace_project_build_artifacts_path(*args)
+ when 'file'
+ file_namespace_project_build_artifacts_path(*args)
+ end
+ end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index b9baeb1d6c4..5c04bba323f 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -49,6 +49,19 @@ module IssuablesHelper
end
end
+ def project_dropdown_label(project_id, default_label)
+ return default_label if project_id.nil?
+ return "Any project" if project_id == "0"
+
+ project = Project.find_by(id: project_id)
+
+ if project
+ project.name_with_namespace
+ else
+ default_label
+ end
+ end
+
def milestone_dropdown_label(milestone_title, default_label = "Milestone")
if milestone_title == Milestone::Upcoming.name
milestone_title = Milestone::Upcoming.title
diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb
index eb651e3687e..5d82abfca79 100644
--- a/app/helpers/lfs_helper.rb
+++ b/app/helpers/lfs_helper.rb
@@ -23,10 +23,14 @@ module LfsHelper
end
def lfs_download_access?
+ return false unless project.lfs_enabled?
+
project.public? || ci? || (user && user.can?(:download_code, project))
end
def lfs_upload_access?
+ return false unless project.lfs_enabled?
+
user && user.can?(:push_code, project)
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index db6e731c744..a9e175c3f5c 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -98,6 +98,6 @@ module MergeRequestsHelper
end
def merge_request_button_visibility(merge_request, closed)
- return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?)
+ return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork?
end
end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 6c1cc6ef072..2b0ff6c0d00 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -25,6 +25,8 @@ module NavHelper
current_path?('merge_requests#commits') ||
current_path?('merge_requests#builds') ||
current_path?('merge_requests#conflicts') ||
+ current_path?('merge_requests#pipelines') ||
+
current_path?('issues#show')
if cookies[:collapsed_gutter] == 'true'
"page-gutter right-sidebar-collapsed"
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 356f27f2d5d..4c685b97c03 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -61,7 +61,9 @@ module ProjectsHelper
project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" }
if current_user
- project_link << icon("chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle", aria: { label: "Toggle switch project dropdown" }, data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" })
+ project_link << button_tag(type: 'button', class: "dropdown-toggle-caret js-projects-dropdown-toggle", aria: { label: "Toggle switch project dropdown" }, data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" }) do
+ icon("chevron-down")
+ end
end
full_title = "#{namespace_link} / #{project_link}".html_safe
@@ -187,6 +189,18 @@ module ProjectsHelper
nav_tabs.flatten
end
+ def project_lfs_status(project)
+ if project.lfs_enabled?
+ content_tag(:span, class: 'lfs-enabled') do
+ 'Enabled'
+ end
+ else
+ content_tag(:span, class: 'lfs-disabled') do
+ 'Disabled'
+ end
+ end
+ end
+
def git_user_name
if current_user
current_user.name
@@ -400,4 +414,23 @@ module ProjectsHelper
message.strip.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]")
end
+
+ def project_feature_options
+ {
+ 'Disabled' => ProjectFeature::DISABLED,
+ 'Only team members' => ProjectFeature::PRIVATE,
+ 'Everyone with access' => ProjectFeature::ENABLED
+ }
+ end
+
+ def project_feature_access_select(field)
+ # Don't show option "everyone with access" if project is private
+ options = project_feature_options
+ level = @project.project_feature.public_send(field)
+
+ options.delete('Everyone with access') if @project.private? && level != ProjectFeature::ENABLED
+
+ options = options_for_select(options, selected: @project.project_feature.public_send(field) || ProjectFeature::ENABLED)
+ content_tag(:select, options, name: "project[project_feature_attributes][#{field.to_s}]", id: "project_project_feature_attributes_#{field.to_s}", class: "pull-right form-control", data: { field: field }).html_safe
+ end
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index c0195713f4a..4549c2e5bb6 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -44,7 +44,7 @@ module SearchHelper
def help_autocomplete
[
{ category: "Help", label: "API Help", url: help_page_path("api/README") },
- { category: "Help", label: "Markdown Help", url: help_page_path("markdown/markdown") },
+ { category: "Help", label: "Markdown Help", url: help_page_path("user/markdown") },
{ category: "Help", label: "Permissions Help", url: help_page_path("user/permissions") },
{ category: "Help", label: "Public Access Help", url: help_page_path("public_access/public_access") },
{ category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks/README") },
diff --git a/app/helpers/sentry_helper.rb b/app/helpers/sentry_helper.rb
index f8cccade15b..3d255df66a0 100644
--- a/app/helpers/sentry_helper.rb
+++ b/app/helpers/sentry_helper.rb
@@ -1,27 +1,9 @@
module SentryHelper
def sentry_enabled?
- Rails.env.production? && current_application_settings.sentry_enabled?
+ Gitlab::Sentry.enabled?
end
def sentry_context
- return unless sentry_enabled?
-
- if current_user
- Raven.user_context(
- id: current_user.id,
- email: current_user.email,
- username: current_user.username,
- )
- end
-
- Raven.tags_context(program: sentry_program_context)
- end
-
- def sentry_program_context
- if Sidekiq.server?
- 'sidekiq'
- else
- 'rails'
- end
+ Gitlab::Sentry.context(current_user)
end
end
diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb
index fb85544df2d..c0ec1634cdb 100644
--- a/app/helpers/tags_helper.rb
+++ b/app/helpers/tags_helper.rb
@@ -3,6 +3,16 @@ module TagsHelper
"/tags/#{tag}"
end
+ def filter_tags_path(options = {})
+ exist_opts = {
+ search: params[:search],
+ sort: params[:sort]
+ }
+
+ options = exist_opts.merge(options)
+ namespace_project_tags_path(@project.namespace, @project, @id, options)
+ end
+
def tag_list(project)
html = ''
project.tag_list.each do |tag|
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 0465327060e..1e86f648203 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -78,13 +78,11 @@ module TodosHelper
end
def todo_actions_options
- actions = [
- OpenStruct.new(id: '', title: 'Any Action'),
- OpenStruct.new(id: Todo::ASSIGNED, title: 'Assigned'),
- OpenStruct.new(id: Todo::MENTIONED, title: 'Mentioned')
+ [
+ { id: '', text: 'Any Action' },
+ { id: Todo::ASSIGNED, text: 'Assigned' },
+ { id: Todo::MENTIONED, text: 'Mentioned' }
]
-
- options_from_collection_for_select(actions, 'id', 'title', params[:action_id])
end
def todo_projects_options
@@ -92,22 +90,28 @@ module TodosHelper
projects = projects.includes(:namespace)
projects = projects.map do |project|
- OpenStruct.new(id: project.id, title: project.name_with_namespace)
+ { id: project.id, text: project.name_with_namespace }
end
- projects.unshift(OpenStruct.new(id: '', title: 'Any Project'))
-
- options_from_collection_for_select(projects, 'id', 'title', params[:project_id])
+ projects.unshift({ id: '', text: 'Any Project' }).to_json
end
def todo_types_options
- types = [
- OpenStruct.new(title: 'Any Type', name: ''),
- OpenStruct.new(title: 'Issue', name: 'Issue'),
- OpenStruct.new(title: 'Merge Request', name: 'MergeRequest')
+ [
+ { id: '', text: 'Any Type' },
+ { id: 'Issue', text: 'Issue' },
+ { id: 'MergeRequest', text: 'Merge Request' }
]
+ end
+
+ def todo_actions_dropdown_label(selected_action_id, default_action)
+ selected_action = todo_actions_options.find { |action| action[:id] == selected_action_id.to_i}
+ selected_action ? selected_action[:text] : default_action
+ end
- options_from_collection_for_select(types, 'name', 'title', params[:type])
+ def todo_types_dropdown_label(selected_type, default_type)
+ selected_type = todo_types_options.find { |type| type[:id] == selected_type && type[:id] != '' }
+ selected_type ? selected_type[:text] : default_type
end
private
diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb
index 8b83bbd93b7..61a574d3dc0 100644
--- a/app/mailers/base_mailer.rb
+++ b/app/mailers/base_mailer.rb
@@ -9,7 +9,7 @@ class BaseMailer < ActionMailer::Base
default reply_to: Proc.new { default_reply_to_address.format }
def can?
- Ability.abilities.allowed?(current_user, action, subject)
+ Ability.allowed?(current_user, action, subject)
end
private
diff --git a/app/models/ability.rb b/app/models/ability.rb
index a49dd703926..fa8f8bc3a5f 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -1,34 +1,5 @@
class Ability
class << self
- # rubocop: disable Metrics/CyclomaticComplexity
- def allowed(user, subject)
- return anonymous_abilities(user, subject) if user.nil?
- return [] unless user.is_a?(User)
- return [] if user.blocked?
-
- abilities_by_subject_class(user: user, subject: subject)
- end
-
- def abilities_by_subject_class(user:, subject:)
- case subject
- when CommitStatus then commit_status_abilities(user, subject)
- when Project then project_abilities(user, subject)
- when Issue then issue_abilities(user, subject)
- when Note then note_abilities(user, subject)
- when ProjectSnippet then project_snippet_abilities(user, subject)
- when PersonalSnippet then personal_snippet_abilities(user, subject)
- when MergeRequest then merge_request_abilities(user, subject)
- when Group then group_abilities(user, subject)
- when Namespace then namespace_abilities(user, subject)
- when GroupMember then group_member_abilities(user, subject)
- when ProjectMember then project_member_abilities(user, subject)
- when User then user_abilities
- when ExternalIssue, Deployment, Environment then project_abilities(user, subject.project)
- when Ci::Runner then runner_abilities(user, subject)
- else []
- end.concat(global_abilities(user))
- end
-
# Given a list of users and a project this method returns the users that can
# read the given project.
def users_that_can_read_project(users, project)
@@ -61,359 +32,7 @@ class Ability
issues.select { |issue| issue.visible_to_user?(user) }
end
- # List of possible abilities for anonymous user
- def anonymous_abilities(user, subject)
- if subject.is_a?(PersonalSnippet)
- anonymous_personal_snippet_abilities(subject)
- elsif subject.is_a?(ProjectSnippet)
- anonymous_project_snippet_abilities(subject)
- elsif subject.is_a?(CommitStatus)
- anonymous_commit_status_abilities(subject)
- elsif subject.is_a?(Project) || subject.respond_to?(:project)
- anonymous_project_abilities(subject)
- elsif subject.is_a?(Group) || subject.respond_to?(:group)
- anonymous_group_abilities(subject)
- elsif subject.is_a?(User)
- anonymous_user_abilities
- else
- []
- end
- end
-
- def anonymous_project_abilities(subject)
- project = if subject.is_a?(Project)
- subject
- else
- subject.project
- end
-
- if project && project.public?
- rules = [
- :read_project,
- :read_board,
- :read_list,
- :read_wiki,
- :read_label,
- :read_milestone,
- :read_project_snippet,
- :read_project_member,
- :read_merge_request,
- :read_note,
- :read_pipeline,
- :read_commit_status,
- :read_container_image,
- :download_code
- ]
-
- # Allow to read builds by anonymous user if guests are allowed
- rules << :read_build if project.public_builds?
-
- # Allow to read issues by anonymous user if issue is not confidential
- rules << :read_issue unless subject.is_a?(Issue) && subject.confidential?
-
- rules - project_disabled_features_rules(project)
- else
- []
- end
- end
-
- def anonymous_commit_status_abilities(subject)
- rules = anonymous_project_abilities(subject.project)
- # If subject is Ci::Build which inherits from CommitStatus filter the abilities
- rules = filter_build_abilities(rules) if subject.is_a?(Ci::Build)
- rules
- end
-
- def anonymous_group_abilities(subject)
- rules = []
-
- group = if subject.is_a?(Group)
- subject
- else
- subject.group
- end
-
- rules << :read_group if group.public?
-
- rules
- end
-
- def anonymous_personal_snippet_abilities(snippet)
- if snippet.public?
- [:read_personal_snippet]
- else
- []
- end
- end
-
- def anonymous_project_snippet_abilities(snippet)
- if snippet.public?
- [:read_project_snippet]
- else
- []
- end
- end
-
- def anonymous_user_abilities
- [:read_user] unless restricted_public_level?
- end
-
- def global_abilities(user)
- rules = []
- rules << :create_group if user.can_create_group
- rules << :read_users_list
- rules
- end
-
- def project_abilities(user, project)
- key = "/user/#{user.id}/project/#{project.id}"
-
- if RequestStore.active?
- RequestStore.store[key] ||= uncached_project_abilities(user, project)
- else
- uncached_project_abilities(user, project)
- end
- end
-
- def uncached_project_abilities(user, project)
- rules = []
- # Push abilities on the users team role
- rules.push(*project_team_rules(project.team, user))
-
- owner = user.admin? ||
- project.owner == user ||
- (project.group && project.group.has_owner?(user))
-
- if owner
- rules.push(*project_owner_rules)
- end
-
- if project.public? || (project.internal? && !user.external?)
- rules.push(*public_project_rules)
-
- # Allow to read builds for internal projects
- rules << :read_build if project.public_builds?
-
- unless owner || project.team.member?(user) || project_group_member?(project, user)
- rules << :request_access if project.request_access_enabled
- end
- end
-
- if project.archived?
- rules -= project_archived_rules
- end
-
- (rules - project_disabled_features_rules(project)).uniq
- end
-
- def project_team_rules(team, user)
- # Rules based on role in project
- if team.master?(user)
- project_master_rules
- elsif team.developer?(user)
- project_dev_rules
- elsif team.reporter?(user)
- project_report_rules
- elsif team.guest?(user)
- project_guest_rules
- else
- []
- end
- end
-
- def public_project_rules
- @public_project_rules ||= project_guest_rules + [
- :download_code,
- :fork_project,
- :read_commit_status,
- :read_pipeline,
- :read_container_image
- ]
- end
-
- def project_guest_rules
- @project_guest_rules ||= [
- :read_project,
- :read_wiki,
- :read_issue,
- :read_board,
- :read_list,
- :read_label,
- :read_milestone,
- :read_project_snippet,
- :read_project_member,
- :read_merge_request,
- :read_note,
- :create_project,
- :create_issue,
- :create_note,
- :upload_file
- ]
- end
-
- def project_report_rules
- @project_report_rules ||= project_guest_rules + [
- :download_code,
- :fork_project,
- :create_project_snippet,
- :update_issue,
- :admin_issue,
- :admin_label,
- :admin_list,
- :read_commit_status,
- :read_build,
- :read_container_image,
- :read_pipeline,
- :read_environment,
- :read_deployment
- ]
- end
-
- def project_dev_rules
- @project_dev_rules ||= project_report_rules + [
- :admin_merge_request,
- :update_merge_request,
- :create_commit_status,
- :update_commit_status,
- :create_build,
- :update_build,
- :create_pipeline,
- :update_pipeline,
- :create_merge_request,
- :create_wiki,
- :push_code,
- :resolve_note,
- :create_container_image,
- :update_container_image,
- :create_environment,
- :create_deployment
- ]
- end
-
- def project_archived_rules
- @project_archived_rules ||= [
- :create_merge_request,
- :push_code,
- :push_code_to_protected_branches,
- :update_merge_request,
- :admin_merge_request
- ]
- end
-
- def project_master_rules
- @project_master_rules ||= project_dev_rules + [
- :push_code_to_protected_branches,
- :update_project_snippet,
- :update_environment,
- :update_deployment,
- :admin_milestone,
- :admin_project_snippet,
- :admin_project_member,
- :admin_merge_request,
- :admin_note,
- :admin_wiki,
- :admin_project,
- :admin_commit_status,
- :admin_build,
- :admin_container_image,
- :admin_pipeline,
- :admin_environment,
- :admin_deployment
- ]
- end
-
- def project_owner_rules
- @project_owner_rules ||= project_master_rules + [
- :change_namespace,
- :change_visibility_level,
- :rename_project,
- :remove_project,
- :archive_project,
- :remove_fork_project,
- :destroy_merge_request,
- :destroy_issue
- ]
- end
-
- def project_disabled_features_rules(project)
- rules = []
-
- unless project.issues_enabled
- rules += named_abilities('issue')
- end
-
- unless project.merge_requests_enabled
- rules += named_abilities('merge_request')
- end
-
- unless project.issues_enabled or project.merge_requests_enabled
- rules += named_abilities('label')
- rules += named_abilities('milestone')
- end
-
- unless project.snippets_enabled
- rules += named_abilities('project_snippet')
- end
-
- unless project.wiki_enabled
- rules += named_abilities('wiki')
- end
-
- unless project.builds_enabled
- rules += named_abilities('build')
- rules += named_abilities('pipeline')
- rules += named_abilities('environment')
- rules += named_abilities('deployment')
- end
-
- unless project.container_registry_enabled
- rules += named_abilities('container_image')
- end
-
- rules
- end
-
- def group_abilities(user, group)
- rules = []
- rules << :read_group if can_read_group?(user, group)
-
- owner = user.admin? || group.has_owner?(user)
- master = owner || group.has_master?(user)
-
- # Only group masters and group owners can create new projects
- if master
- rules += [
- :create_projects,
- :admin_milestones
- ]
- end
-
- # Only group owner and administrators can admin group
- if owner
- rules += [
- :admin_group,
- :admin_namespace,
- :admin_group_member,
- :change_visibility_level
- ]
- end
-
- if group.public? || (group.internal? && !user.external?)
- rules << :request_access if group.request_access_enabled && group.users.exclude?(user)
- end
-
- rules.flatten
- end
-
- def can_read_group?(user, group)
- return true if user.admin?
- return true if group.public?
- return true if group.internal? && !user.external?
- return true if group.users.include?(user)
-
- GroupProjectsFinder.new(group).execute(user).any?
- end
-
+ # TODO: make this private and use the actual abilities stuff for this
def can_edit_note?(user, note)
return false if !note.editable? || !user.present?
return true if note.author == user || user.admin?
@@ -426,207 +45,23 @@ class Ability
end
end
- def namespace_abilities(user, namespace)
- rules = []
-
- # Only namespace owner and administrators can admin it
- if namespace.owner == user || user.admin?
- rules += [
- :create_projects,
- :admin_namespace
- ]
- end
-
- rules.flatten
- end
-
- [:issue, :merge_request].each do |name|
- define_method "#{name}_abilities" do |user, subject|
- rules = []
-
- if subject.author == user || (subject.respond_to?(:assignee) && subject.assignee == user)
- rules += [
- :"read_#{name}",
- :"update_#{name}",
- ]
- end
-
- rules += project_abilities(user, subject.project)
- rules = filter_confidential_issues_abilities(user, subject, rules) if subject.is_a?(Issue)
- rules
- end
- end
-
- def note_abilities(user, note)
- rules = []
-
- if note.author == user
- rules += [
- :read_note,
- :update_note,
- :admin_note,
- :resolve_note
- ]
- end
-
- if note.respond_to?(:project) && note.project
- rules += project_abilities(user, note.project)
- end
-
- if note.for_merge_request? && note.noteable.author == user
- rules << :resolve_note
- end
-
- rules
- end
-
- def personal_snippet_abilities(user, snippet)
- rules = []
-
- if snippet.author == user
- rules += [
- :read_personal_snippet,
- :update_personal_snippet,
- :admin_personal_snippet
- ]
- end
-
- if snippet.public? || (snippet.internal? && !user.external?)
- rules << :read_personal_snippet
- end
-
- rules
+ def allowed?(user, action, subject)
+ allowed(user, subject).include?(action)
end
- def project_snippet_abilities(user, snippet)
- rules = []
-
- if snippet.author == user || user.admin?
- rules += [
- :read_project_snippet,
- :update_project_snippet,
- :admin_project_snippet
- ]
- end
-
- if snippet.public? || (snippet.internal? && !user.external?) || (snippet.private? && snippet.project.team.member?(user))
- rules << :read_project_snippet
- end
-
- rules
- end
-
- def group_member_abilities(user, subject)
- rules = []
- target_user = subject.user
- group = subject.group
-
- unless group.last_owner?(target_user)
- can_manage = group_abilities(user, group).include?(:admin_group_member)
-
- if can_manage
- rules << :update_group_member
- rules << :destroy_group_member
- elsif user == target_user
- rules << :destroy_group_member
- end
- end
-
- rules
- end
-
- def project_member_abilities(user, subject)
- rules = []
- target_user = subject.user
- project = subject.project
-
- unless target_user == project.owner
- can_manage = project_abilities(user, project).include?(:admin_project_member)
-
- if can_manage
- rules << :update_project_member
- rules << :destroy_project_member
- elsif user == target_user
- rules << :destroy_project_member
- end
- end
-
- rules
- end
-
- def commit_status_abilities(user, subject)
- rules = project_abilities(user, subject.project)
- # If subject is Ci::Build which inherits from CommitStatus filter the abilities
- rules = filter_build_abilities(rules) if subject.is_a?(Ci::Build)
- rules
- end
-
- def filter_build_abilities(rules)
- # If we can't read build we should also not have that
- # ability when looking at this in context of commit_status
- %w(read create update admin).each do |rule|
- rules.delete(:"#{rule}_commit_status") unless rules.include?(:"#{rule}_build")
- end
- rules
- end
-
- def runner_abilities(user, runner)
- if user.is_admin?
- [:assign_runner]
- elsif runner.is_shared? || runner.locked?
- []
- elsif user.ci_authorized_runners.include?(runner)
- [:assign_runner]
- else
- []
- end
- end
+ def allowed(user, subject)
+ return uncached_allowed(user, subject) unless RequestStore.active?
- def user_abilities
- [:read_user]
- end
-
- def abilities
- @abilities ||= begin
- abilities = Six.new
- abilities << self
- abilities
- end
+ user_key = user ? user.id : 'anonymous'
+ subject_key = subject ? "#{subject.class.name}/#{subject.id}" : 'global'
+ key = "/ability/#{user_key}/#{subject_key}"
+ RequestStore[key] ||= uncached_allowed(user, subject).freeze
end
private
- def restricted_public_level?
- current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC)
- end
-
- def named_abilities(name)
- [
- :"read_#{name}",
- :"create_#{name}",
- :"update_#{name}",
- :"admin_#{name}"
- ]
- end
-
- def filter_confidential_issues_abilities(user, issue, rules)
- return rules if user.admin? || !issue.confidential?
-
- unless issue.author == user || issue.assignee == user || issue.project.team.member?(user, Gitlab::Access::REPORTER)
- rules.delete(:admin_issue)
- rules.delete(:read_issue)
- rules.delete(:update_issue)
- end
-
- rules
- end
-
- def project_group_member?(project, user)
- project.group &&
- (
- project.group.members.exists?(user_id: user.id) ||
- project.group.requesters.exists?(user_id: user.id)
- )
+ def uncached_allowed(user, subject)
+ BasePolicy.class_for(subject).abilities(user, subject)
end
end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index f0bcb2d7cda..246477ffe88 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -146,7 +146,7 @@ class ApplicationSetting < ActiveRecord::Base
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
domain_whitelist: Settings.gitlab['domain_whitelist'],
- import_sources: %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project],
+ import_sources: %w[github bitbucket gitlab google_code fogbugz git gitlab_project],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 096b3b801af..61052437318 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -208,22 +208,31 @@ module Ci
end
end
+ def has_trace_file?
+ File.exist?(path_to_trace) || has_old_trace_file?
+ end
+
def has_trace?
raw_trace.present?
end
def raw_trace
- if File.file?(path_to_trace)
- File.read(path_to_trace)
- elsif project.ci_id && File.file?(old_path_to_trace)
- # Temporary fix for build trace data integrity
- File.read(old_path_to_trace)
+ if File.exist?(trace_file_path)
+ File.read(trace_file_path)
else
# backward compatibility
read_attribute :trace
end
end
+ ##
+ # Deprecated
+ #
+ # This is a hotfix for CI build data integrity, see #4246
+ def has_old_trace_file?
+ project.ci_id && File.exist?(old_path_to_trace)
+ end
+
def trace
trace = raw_trace
if project && trace.present? && project.runners_token.present?
@@ -262,6 +271,14 @@ module Ci
end
end
+ def trace_file_path
+ if has_old_trace_file?
+ old_path_to_trace
+ else
+ path_to_trace
+ end
+ end
+
def dir_to_trace
File.join(
Settings.gitlab_ci.builds_path,
@@ -352,7 +369,7 @@ module Ci
end
def artifacts?
- !artifacts_expired? && artifacts_file.exists?
+ !artifacts_expired? && self[:artifacts_file].present?
end
def artifacts_metadata?
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 087abe4cbb1..bd1737c7587 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -1,7 +1,7 @@
module Ci
class Pipeline < ActiveRecord::Base
extend Ci::Model
- include Statuseable
+ include HasStatus
self.table_name = 'ci_commits'
@@ -65,8 +65,8 @@ module Ci
end
# ref can't be HEAD or SHA, can only be branch/tag name
- scope :latest_successful_for, ->(ref = default_branch) do
- where(ref: ref).success.order(id: :desc).limit(1)
+ def self.latest_successful_for(ref)
+ where(ref: ref).order(id: :desc).success.first
end
def self.truncate_sha(sha)
@@ -83,7 +83,7 @@ module Ci
end
def stages_with_latest_statuses
- statuses.latest.order(:stage_idx).group_by(&:stage)
+ statuses.latest.includes(project: :namespace).order(:stage_idx).group_by(&:stage)
end
def project_id
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 817d063e4a2..e64fd1e0c1b 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -108,15 +108,6 @@ class Commit
@diff_line_count
end
- # Returns a string describing the commit for use in a link title
- #
- # Example
- #
- # "Commit: Alex Denisov - Project git clone panel"
- def link_title
- "Commit: #{author_name} - #{title}"
- end
-
# Returns the commits title.
#
# Usually, the commit title is the first line of the commit message.
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index 630ee9601e0..656a242c265 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -4,12 +4,10 @@
#
# range = CommitRange.new('f3f85602...e86e1013', project)
# range.exclude_start? # => false
-# range.reference_title # => "Commits f3f85602 through e86e1013"
# range.to_s # => "f3f85602...e86e1013"
#
# range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae', project)
# range.exclude_start? # => true
-# range.reference_title # => "Commits f3f85602^ through e86e1013"
# range.to_param # => {from: "f3f856029bc5f966c5a7ee24cf7efefdd20e6019^", to: "e86e1013709735be5bb767e2b228930c543f25ae"}
# range.to_s # => "f3f85602..e86e1013"
#
@@ -109,11 +107,6 @@ class CommitRange
reference
end
- # Returns a String for use in a link's title attribute
- def reference_title
- "Commits #{sha_start} through #{sha_to}"
- end
-
# Return a Hash of parameters for passing to a URL helper
#
# See `namespace_project_compare_url`
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 84ceeac7d3e..4a628924499 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -1,5 +1,5 @@
class CommitStatus < ActiveRecord::Base
- include Statuseable
+ include HasStatus
include Importable
self.table_name = 'ci_builds'
@@ -25,6 +25,8 @@ class CommitStatus < ActiveRecord::Base
scope :retried, -> { where.not(id: latest) }
scope :ordered, -> { order(:name) }
scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) }
+ scope :latest_ci_stages, -> { latest.ordered.includes(project: :namespace) }
+ scope :retried_ci_stages, -> { retried.ordered.includes(project: :namespace) }
state_machine :status do
event :enqueue do
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index 800a16ab246..d8d4575bb4d 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -2,7 +2,7 @@ module Awardable
extend ActiveSupport::Concern
included do
- has_many :award_emoji, -> { includes(:user) }, as: :awardable, dependent: :destroy
+ has_many :award_emoji, -> { includes(:user).order(:id) }, as: :awardable, dependent: :destroy
if self < Participable
# By default we always load award_emoji user association
@@ -59,6 +59,18 @@ module Awardable
true
end
+ def awardable_votes?(name)
+ AwardEmoji::UPVOTE_NAME == name || AwardEmoji::DOWNVOTE_NAME == name
+ end
+
+ def user_can_award?(current_user, name)
+ if user_authored?(current_user)
+ !awardable_votes?(normalize_name(name))
+ else
+ true
+ end
+ 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/statuseable.rb b/app/models/concerns/has_status.rb
index 750f937b724..f7b8352405c 100644
--- a/app/models/concerns/statuseable.rb
+++ b/app/models/concerns/has_status.rb
@@ -1,4 +1,4 @@
-module Statuseable
+module HasStatus
extend ActiveSupport::Concern
AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped]
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 8e11d4f57cf..22231b2e0f0 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -196,6 +196,10 @@ module Issuable
end
end
+ def user_authored?(user)
+ user == author
+ end
+
def subscribed_without_subscriptions?(user)
participants(user).include?(user)
end
diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb
index a881fb83b7f..b8dd27a7afe 100644
--- a/app/models/concerns/note_on_diff.rb
+++ b/app/models/concerns/note_on_diff.rb
@@ -28,4 +28,8 @@ module NoteOnDiff
def can_be_award_emoji?
false
end
+
+ def to_discussion
+ Discussion.new([self])
+ end
end
diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
new file mode 100644
index 00000000000..9216122923e
--- /dev/null
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -0,0 +1,37 @@
+# Makes api V3 compatible with old project features permissions methods
+#
+# After migrating issues_enabled merge_requests_enabled builds_enabled snippets_enabled and wiki_enabled
+# fields to a new table "project_features", support for the old fields is still needed in the API.
+
+module ProjectFeaturesCompatibility
+ extend ActiveSupport::Concern
+
+ def wiki_enabled=(value)
+ write_feature_attribute(:wiki_access_level, value)
+ end
+
+ def builds_enabled=(value)
+ write_feature_attribute(:builds_access_level, value)
+ end
+
+ def merge_requests_enabled=(value)
+ write_feature_attribute(:merge_requests_access_level, value)
+ end
+
+ def issues_enabled=(value)
+ write_feature_attribute(:issues_access_level, value)
+ end
+
+ def snippets_enabled=(value)
+ write_feature_attribute(:snippets_access_level, value)
+ end
+
+ private
+
+ def write_feature_attribute(field, value)
+ build_project_feature unless project_feature
+
+ access_level = value == "true" ? ProjectFeature::ENABLED : ProjectFeature::DISABLED
+ project_feature.update_attribute(field, access_level)
+ end
+end
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index df2a9e3e84b..a3ac577cf3e 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -52,11 +52,11 @@ module Taskable
end
# Return a string that describes the current state of this Taskable's task
- # list items, e.g. "20 tasks (12 completed, 8 remaining)"
+ # list items, e.g. "12 of 20 tasks completed"
def task_status
return '' if description.blank?
sum = tasks.summary
- "#{sum.item_count} tasks (#{sum.complete_count} completed, #{sum.incomplete_count} remaining)"
+ "#{sum.complete_count} of #{sum.item_count} #{'task'.pluralize(sum.item_count)} completed"
end
end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index c8320ff87fa..4442cefc7e9 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -107,10 +107,6 @@ class DiffNote < Note
self.noteable.find_diff_discussion(self.discussion_id)
end
- def to_discussion
- Discussion.new([self])
- end
-
private
def supported?
diff --git a/app/models/event.rb b/app/models/event.rb
index fd736d12359..a0b7b0dc2b5 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -65,7 +65,7 @@ class Event < ActiveRecord::Base
elsif created_project?
true
elsif issue? || issue_note?
- Ability.abilities.allowed?(user, :read_issue, note? ? note_target : target)
+ Ability.allowed?(user, :read_issue, note? ? note_target : target)
else
((merge_request? || note?) && target.present?) || milestone?
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 5330a07ee35..b0b1313f94a 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -10,14 +10,16 @@ class MergeRequest < ActiveRecord::Base
belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project"
belongs_to :merge_user, class_name: "User"
- has_one :merge_request_diff, dependent: :destroy
+ has_many :merge_request_diffs, dependent: :destroy
+ has_one :merge_request_diff,
+ -> { order('merge_request_diffs.id DESC') }
has_many :events, as: :target, dependent: :destroy
serialize :merge_params, Hash
- after_create :create_merge_request_diff, unless: :importing?
- after_update :update_merge_request_diff
+ after_create :ensure_merge_request_diff, unless: :importing?
+ after_update :reload_diff_if_branch_changed
delegate :commits, :real_size, to: :merge_request_diff, prefix: nil
@@ -89,13 +91,13 @@ class MergeRequest < ActiveRecord::Base
end
end
- validates :source_project, presence: true, unless: [:allow_broken, :importing?]
+ validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
validates :source_branch, presence: true
validates :target_project, presence: true
validates :target_branch, presence: true
validates :merge_user, presence: true, if: :merge_when_build_succeeds?
- validate :validate_branches, unless: [:allow_broken, :importing?]
- validate :validate_fork
+ validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
+ validate :validate_fork, unless: :closed_without_fork?
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
@@ -170,10 +172,10 @@ class MergeRequest < ActiveRecord::Base
end
def diffs(diff_options = nil)
- if self.compare
- self.compare.diffs(diff_options)
+ if compare
+ compare.diffs(diff_options)
else
- Gitlab::Diff::FileCollection::MergeRequest.new(self, diff_options: diff_options)
+ merge_request_diff.diffs(diff_options)
end
end
@@ -184,8 +186,8 @@ class MergeRequest < ActiveRecord::Base
def diff_base_commit
if persisted?
merge_request_diff.base_commit
- elsif diff_start_commit && diff_head_commit
- self.target_project.merge_base_commit(diff_start_sha, diff_head_sha)
+ else
+ branch_merge_base_commit
end
end
@@ -238,12 +240,21 @@ class MergeRequest < ActiveRecord::Base
def source_branch_head
source_branch_ref = @source_branch_sha || source_branch
- source_project.repository.commit(source_branch) if source_branch_ref
+ source_project.repository.commit(source_branch_ref) if source_branch_ref
end
def target_branch_head
target_branch_ref = @target_branch_sha || target_branch
- target_project.repository.commit(target_branch) if target_branch_ref
+ target_project.repository.commit(target_branch_ref) if target_branch_ref
+ end
+
+ def branch_merge_base_commit
+ start_sha = target_branch_sha
+ head_sha = source_branch_sha
+
+ if start_sha && head_sha
+ target_project.merge_base_commit(start_sha, head_sha)
+ end
end
def target_branch_sha
@@ -267,16 +278,16 @@ class MergeRequest < ActiveRecord::Base
# Return diff_refs instance trying to not touch the git repository
def diff_sha_refs
if merge_request_diff && merge_request_diff.diff_refs_by_sha?
- return Gitlab::Diff::DiffRefs.new(
- base_sha: merge_request_diff.base_commit_sha,
- start_sha: merge_request_diff.start_commit_sha,
- head_sha: merge_request_diff.head_commit_sha
- )
+ merge_request_diff.diff_refs
else
diff_refs
end
end
+ def branch_merge_base_sha
+ branch_merge_base_commit.try(:sha)
+ end
+
def validate_branches
if target_project == source_project && target_branch == source_branch
errors.add :branch_conflict, "You can not use same project/branch for source and target"
@@ -294,36 +305,49 @@ class MergeRequest < ActiveRecord::Base
def validate_fork
return true unless target_project && source_project
+ return true if target_project == source_project
+ return true unless forked_source_project_missing?
- if target_project == source_project
- true
- else
- # If source and target projects are different
- # we should check if source project is actually a fork of target project
- if source_project.forked_from?(target_project)
- true
- else
- errors.add :validate_fork,
- 'Source project is not a fork of target project'
- end
- end
+ errors.add :validate_fork,
+ 'Source project is not a fork of the target project'
end
- def update_merge_request_diff
+ def closed_without_fork?
+ closed? && forked_source_project_missing?
+ end
+
+ def forked_source_project_missing?
+ return false unless for_fork?
+ return true unless source_project
+
+ !source_project.forked_from?(target_project)
+ end
+
+ def ensure_merge_request_diff
+ merge_request_diff || create_merge_request_diff
+ end
+
+ def create_merge_request_diff
+ merge_request_diffs.create
+ reload_merge_request_diff
+ end
+
+ def reload_merge_request_diff
+ merge_request_diff(true)
+ end
+
+ def reload_diff_if_branch_changed
if source_branch_changed? || target_branch_changed?
reload_diff
end
end
def reload_diff
- return unless merge_request_diff && open?
+ return unless open?
old_diff_refs = self.diff_refs
-
- merge_request_diff.reload_content
-
+ create_merge_request_diff
MergeRequests::MergeRequestDiffCacheService.new.execute(self)
-
new_diff_refs = self.diff_refs
update_diff_notes_positions(
@@ -387,7 +411,7 @@ class MergeRequest < ActiveRecord::Base
def can_remove_source_branch?(current_user)
!source_project.protected_branch?(source_branch) &&
!source_project.root_ref?(source_branch) &&
- Ability.abilities.allowed?(current_user, :push_code, source_project) &&
+ Ability.allowed?(current_user, :push_code, source_project) &&
diff_head_commit == source_branch_head
end
@@ -705,7 +729,9 @@ class MergeRequest < ActiveRecord::Base
end
def pipeline
- @pipeline ||= source_project.pipeline(diff_head_sha, source_branch) if diff_head_sha && source_project
+ return unless diff_head_sha && source_project
+
+ @pipeline ||= source_project.pipeline_for(source_branch, diff_head_sha)
end
def all_pipelines
@@ -777,8 +803,12 @@ class MergeRequest < ActiveRecord::Base
return @conflicts_can_be_resolved_in_ui = false unless has_complete_diff_refs?
begin
- @conflicts_can_be_resolved_in_ui = conflicts.files.each(&:lines)
- rescue Gitlab::Conflict::Parser::ParserError, Gitlab::Conflict::FileCollection::ConflictSideMissing
+ # Try to parse each conflict. If the MR's mergeable status hasn't been updated,
+ # ensure that we don't say there are conflicts to resolve when there are no conflict
+ # files.
+ conflicts.files.each(&:lines)
+ @conflicts_can_be_resolved_in_ui = conflicts.files.length > 0
+ rescue Rugged::OdbError, Gitlab::Conflict::Parser::ParserError, Gitlab::Conflict::FileCollection::ConflictSideMissing
@conflicts_can_be_resolved_in_ui = false
end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 32cc6a3bfea..445179a4487 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -8,8 +8,6 @@ class MergeRequestDiff < ActiveRecord::Base
belongs_to :merge_request
- delegate :source_branch_sha, :target_branch_sha, :target_branch, :source_branch, to: :merge_request, prefix: nil
-
state_machine :state, initial: :empty do
state :collected
state :overflow
@@ -24,12 +22,47 @@ class MergeRequestDiff < ActiveRecord::Base
serialize :st_commits
serialize :st_diffs
- after_create :reload_content, unless: :importing?
- after_save :keep_around_commits, unless: :importing?
+ # All diff information is collected from repository after object is created.
+ # It allows you to override variables like head_commit_sha before getting diff.
+ after_create :save_git_content, unless: :importing?
+
+ def self.select_without_diff
+ select(column_names - ['st_diffs'])
+ end
- def reload_content
+ # Collect information about commits and diff from repository
+ # and save it to the database as serialized data
+ def save_git_content
+ ensure_commits_sha
+ save_commits
reload_commits
- reload_diffs
+ save_diffs
+ keep_around_commits
+ end
+
+ def ensure_commits_sha
+ merge_request.fetch_ref
+ self.start_commit_sha ||= merge_request.target_branch_sha
+ self.head_commit_sha ||= merge_request.source_branch_sha
+ self.base_commit_sha ||= find_base_sha
+ save
+ end
+
+ # Override head_commit_sha to keep compatibility with merge request diff
+ # created before version 8.4 that does not store head_commit_sha in separate db field.
+ def head_commit_sha
+ if persisted? && super.nil?
+ last_commit.try(:sha)
+ else
+ super
+ end
+ end
+
+ # This method will rely on repository branch sha
+ # in case start_commit_sha is nil. Its necesarry for old merge request diff
+ # created before version 8.4 to work
+ def safe_start_commit_sha
+ start_commit_sha || merge_request.target_branch_sha
end
def size
@@ -38,14 +71,11 @@ class MergeRequestDiff < ActiveRecord::Base
def raw_diffs(options = {})
if options[:ignore_whitespace_change]
- @raw_diffs_no_whitespace ||= begin
- compare = Gitlab::Git::Compare.new(
+ @diffs_no_whitespace ||=
+ Gitlab::Git::Compare.new(
repository.raw_repository,
- self.start_commit_sha || self.target_branch_sha,
- self.head_commit_sha || self.source_branch_sha,
- )
- compare.diffs(options)
- end
+ safe_start_commit_sha,
+ head_commit_sha).diffs(options)
else
@raw_diffs ||= {}
@raw_diffs[options] ||= load_diffs(st_diffs, options)
@@ -56,6 +86,11 @@ class MergeRequestDiff < ActiveRecord::Base
@commits ||= load_commits(st_commits || [])
end
+ def reload_commits
+ @commits = nil
+ commits
+ end
+
def last_commit
commits.first
end
@@ -65,55 +100,60 @@ class MergeRequestDiff < ActiveRecord::Base
end
def base_commit
- return unless self.base_commit_sha
+ return unless base_commit_sha
- project.commit(self.base_commit_sha)
+ project.commit(base_commit_sha)
end
def start_commit
- return unless self.start_commit_sha
+ return unless start_commit_sha
- project.commit(self.start_commit_sha)
+ project.commit(start_commit_sha)
end
def head_commit
- return last_commit unless self.head_commit_sha
+ return unless head_commit_sha
+
+ project.commit(head_commit_sha)
+ end
+
+ def diff_refs
+ return unless start_commit_sha || base_commit_sha
- project.commit(self.head_commit_sha)
+ Gitlab::Diff::DiffRefs.new(
+ base_sha: base_commit_sha,
+ start_sha: start_commit_sha,
+ head_sha: head_commit_sha
+ )
end
def diff_refs_by_sha?
base_commit_sha? && head_commit_sha? && start_commit_sha?
end
+ def diffs(diff_options = nil)
+ Gitlab::Diff::FileCollection::MergeRequestDiff.new(self, diff_options: diff_options)
+ end
+
+ def project
+ merge_request.target_project
+ end
+
def compare
@compare ||=
- begin
- # Update ref for merge request
- merge_request.fetch_ref
+ Gitlab::Git::Compare.new(
+ repository.raw_repository,
+ safe_start_commit_sha,
+ head_commit_sha
+ )
+ end
- Gitlab::Git::Compare.new(
- repository.raw_repository,
- self.target_branch_sha,
- self.source_branch_sha
- )
- end
+ def latest?
+ self == merge_request.merge_request_diff
end
private
- # Collect array of Git::Commit objects
- # between target and source branches
- def unmerged_commits
- commits = compare.commits
-
- if commits.present?
- commits = Commit.decorate(commits, merge_request.source_project).reverse
- end
-
- commits
- end
-
def dump_commits(commits)
commits.map(&:to_hash)
end
@@ -122,26 +162,21 @@ class MergeRequestDiff < ActiveRecord::Base
array.map { |hash| Commit.new(Gitlab::Git::Commit.new(hash), merge_request.source_project) }
end
- # Reload all commits related to current merge request from repo
+ # Load all commits related to current merge request diff from repo
# and save it as array of hashes in st_commits db field
- def reload_commits
+ def save_commits
new_attributes = {}
- commit_objects = unmerged_commits
+ commits = compare.commits
- if commit_objects.present?
- new_attributes[:st_commits] = dump_commits(commit_objects)
+ if commits.present?
+ commits = Commit.decorate(commits, merge_request.source_project).reverse
+ new_attributes[:st_commits] = dump_commits(commits)
end
update_columns_serialized(new_attributes)
end
- # Collect array of Git::Diff objects
- # between target and source branches
- def unmerged_diffs
- compare.diffs(Commit.max_diff_options)
- end
-
def dump_diffs(diffs)
if diffs.respond_to?(:map)
diffs.map(&:to_hash)
@@ -162,16 +197,16 @@ class MergeRequestDiff < ActiveRecord::Base
end
end
- # Reload diffs between branches related to current merge request from repo
+ # Load diffs between branches related to current merge request diff from repo
# and save it as array of hashes in st_diffs db field
- def reload_diffs
+ def save_diffs
new_attributes = {}
new_diffs = []
if commits.size.zero?
new_attributes[:state] = :empty
else
- diff_collection = unmerged_diffs
+ diff_collection = compare.diffs(Commit.max_diff_options)
if diff_collection.overflow?
# Set our state to 'overflow' to make the #empty? and #collected?
@@ -188,32 +223,17 @@ class MergeRequestDiff < ActiveRecord::Base
end
new_attributes[:st_diffs] = new_diffs
-
- new_attributes[:start_commit_sha] = self.target_branch_sha
- new_attributes[:head_commit_sha] = self.source_branch_sha
- new_attributes[:base_commit_sha] = branch_base_sha
-
update_columns_serialized(new_attributes)
-
- keep_around_commits
- end
-
- def project
- merge_request.target_project
end
def repository
project.repository
end
- def branch_base_commit
- return unless self.source_branch_sha && self.target_branch_sha
-
- project.merge_base_commit(self.source_branch_sha, self.target_branch_sha)
- end
+ def find_base_sha
+ return unless head_commit_sha && start_commit_sha
- def branch_base_sha
- branch_base_commit.try(:sha)
+ project.merge_base_commit(head_commit_sha, start_commit_sha).try(:sha)
end
def utf8_st_diffs
@@ -248,8 +268,8 @@ class MergeRequestDiff < ActiveRecord::Base
end
def keep_around_commits
- repository.keep_around(target_branch_sha)
- repository.keep_around(source_branch_sha)
- repository.keep_around(branch_base_sha)
+ repository.keep_around(start_commit_sha)
+ repository.keep_around(head_commit_sha)
+ repository.keep_around(base_commit_sha)
end
end
diff --git a/app/models/note.rb b/app/models/note.rb
index f2656df028b..b94e3cff2ce 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -223,6 +223,10 @@ 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 8cf093be4c3..a6de2c48071 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -11,24 +11,23 @@ class Project < ActiveRecord::Base
include AfterCommitQueue
include CaseSensitivity
include TokenAuthenticatable
+ include ProjectFeaturesCompatibility
extend Gitlab::ConfigHelper
UNKNOWN_IMPORT_URL = 'http://unknown.git'
+ delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true
+
default_value_for :archived, false
default_value_for :visibility_level, gitlab_config_features.visibility_level
- default_value_for :issues_enabled, gitlab_config_features.issues
- default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests
- default_value_for :builds_enabled, gitlab_config_features.builds
- default_value_for :wiki_enabled, gitlab_config_features.wiki
- default_value_for :snippets_enabled, gitlab_config_features.snippets
default_value_for :container_registry_enabled, gitlab_config_features.container_registry
default_value_for(:repository_storage) { current_application_settings.repository_storage }
default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled }
after_create :ensure_dir_exist
after_save :ensure_dir_exist, if: :namespace_id_changed?
+ after_initialize :setup_project_feature
# set last_activity_at to the same as created_at
after_create :set_last_activity_at
@@ -62,10 +61,10 @@ class Project < ActiveRecord::Base
belongs_to :group, -> { where(type: Group) }, foreign_key: 'namespace_id'
belongs_to :namespace
- has_one :board, dependent: :destroy
-
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id'
+ has_one :board, dependent: :destroy
+
# Project services
has_many :services
has_one :campfire_service, dependent: :destroy
@@ -130,6 +129,7 @@ class Project < ActiveRecord::Base
has_many :notification_settings, dependent: :destroy, as: :source
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
+ has_one :project_feature, dependent: :destroy
has_many :commit_statuses, dependent: :destroy, class_name: 'CommitStatus', foreign_key: :gl_project_id
has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id
@@ -142,6 +142,7 @@ class Project < ActiveRecord::Base
has_many :deployments, dependent: :destroy
accepts_nested_attributes_for :variables, allow_destroy: true
+ accepts_nested_attributes_for :project_feature
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
@@ -159,8 +160,6 @@ class Project < ActiveRecord::Base
length: { within: 0..255 },
format: { with: Gitlab::Regex.project_path_regex,
message: Gitlab::Regex.project_path_regex_message }
- validates :issues_enabled, :merge_requests_enabled,
- :wiki_enabled, inclusion: { in: [true, false] }
validates :namespace, presence: true
validates_uniqueness_of :name, scope: :namespace_id
validates_uniqueness_of :path, scope: :namespace_id
@@ -196,6 +195,9 @@ class Project < ActiveRecord::Base
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
+ scope :with_builds_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0') }
+ scope :with_issues_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.issues_access_level IS NULL or project_features.issues_access_level > 0') }
+
scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') }
scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) }
@@ -390,6 +392,13 @@ class Project < ActiveRecord::Base
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 repository_storage_path
Gitlab.config.repositories.storages[repository_storage]
end
@@ -436,7 +445,7 @@ class Project < ActiveRecord::Base
# ref can't be HEAD, can only be branch/tag name or SHA
def latest_successful_builds_for(ref = default_branch)
- latest_pipeline = pipelines.latest_successful_for(ref).first
+ latest_pipeline = pipelines.latest_successful_for(ref)
if latest_pipeline
latest_pipeline.builds.latest.with_artifacts
@@ -680,6 +689,10 @@ class Project < ActiveRecord::Base
update_column(:has_external_issue_tracker, services.external_issue_trackers.any?)
end
+ def has_wiki?
+ wiki_enabled? || has_external_wiki?
+ end
+
def external_wiki
if has_external_wiki.nil?
cache_has_external_wiki # Populate
@@ -1035,6 +1048,7 @@ class Project < ActiveRecord::Base
"refs/heads/#{branch}",
force: true)
repository.copy_gitattributes(branch)
+ repository.expire_avatar_cache(branch)
reload_default_branch
end
@@ -1095,16 +1109,21 @@ class Project < ActiveRecord::Base
!namespace.share_with_group_lock
end
- def pipeline(sha, ref)
+ def pipeline_for(ref, sha = nil)
+ sha ||= commit(ref).try(:sha)
+
+ return unless sha
+
pipelines.order(id: :desc).find_by(sha: sha, ref: ref)
end
- def ensure_pipeline(sha, ref, current_user = nil)
- pipeline(sha, ref) || pipelines.create(sha: sha, ref: ref, user: current_user)
+ def ensure_pipeline(ref, sha, current_user = nil)
+ pipeline_for(ref, sha) ||
+ pipelines.create(sha: sha, ref: ref, user: current_user)
end
def enable_ci
- self.builds_enabled = true
+ project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
end
def any_runners?(&block)
@@ -1271,6 +1290,11 @@ class Project < ActiveRecord::Base
private
+ # Prevents the creation of project_feature record for every project
+ def setup_project_feature
+ build_project_feature unless project_feature
+ end
+
def default_branch_protected?
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL ||
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
new file mode 100644
index 00000000000..9c602c582bd
--- /dev/null
+++ b/app/models/project_feature.rb
@@ -0,0 +1,63 @@
+class ProjectFeature < ActiveRecord::Base
+ # == Project features permissions
+ #
+ # Grants access level to project tools
+ #
+ # Tools can be enabled only for users, everyone or disabled
+ # Access control is made only for non private projects
+ #
+ # levels:
+ #
+ # Disabled: not enabled for anyone
+ # Private: enabled only for team members
+ # Enabled: enabled for everyone able to access the project
+ #
+
+ # Permision levels
+ DISABLED = 0
+ PRIVATE = 10
+ ENABLED = 20
+
+ FEATURES = %i(issues merge_requests wiki snippets builds)
+
+ belongs_to :project
+
+ def feature_available?(feature, user)
+ raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature)
+
+ get_permission(user, public_send("#{feature}_access_level"))
+ end
+
+ def builds_enabled?
+ return true unless builds_access_level
+
+ builds_access_level > DISABLED
+ end
+
+ def wiki_enabled?
+ return true unless wiki_access_level
+
+ wiki_access_level > DISABLED
+ end
+
+ def merge_requests_enabled?
+ return true unless merge_requests_access_level
+
+ merge_requests_access_level > DISABLED
+ end
+
+ private
+
+ def get_permission(user, level)
+ case level
+ when DISABLED
+ false
+ when PRIVATE
+ user && (project.team.member?(user) || user.admin?)
+ when ENABLED
+ true
+ else
+ true
+ end
+ end
+end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index bdc3b9d1c1c..f891e8374d2 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -120,8 +120,21 @@ class Repository
commits
end
- def find_branch(name)
- raw_repository.branches.find { |branch| branch.name == name }
+ def find_branch(name, fresh_repo: true)
+ # Since the Repository object may have in-memory index changes, invalidating the memoized Repository object may
+ # cause unintended side effects. Because finding a branch is a read-only operation, we can safely instantiate
+ # a new repo here to ensure a consistent state to avoid a libgit2 bug where concurrent access (e.g. via git gc)
+ # may cause the branch to "disappear" erroneously or have the wrong SHA.
+ #
+ # See: https://github.com/libgit2/libgit2/issues/1534 and https://gitlab.com/gitlab-org/gitlab-ce/issues/15392
+ raw_repo =
+ if fresh_repo
+ Gitlab::Git::Repository.new(path_to_repo)
+ else
+ raw_repository
+ end
+
+ raw_repo.find_branch(name)
end
def find_tag(name)
@@ -1065,7 +1078,7 @@ class Repository
@avatar ||= cache.fetch(:avatar) do
AVATAR_FILES.find do |file|
- blob_at_branch('master', file)
+ blob_at_branch(root_ref, file)
end
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index ad3cfbc03e4..6996740eebd 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -433,7 +433,7 @@ class User < ActiveRecord::Base
#
# This logic is duplicated from `Ability#project_abilities` into a SQL form.
def projects_where_can_admin_issues
- authorized_projects(Gitlab::Access::REPORTER).non_archived.where.not(issues_enabled: false)
+ authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
end
def is_admin?
@@ -460,16 +460,12 @@ class User < ActiveRecord::Base
can?(:create_group, nil)
end
- def abilities
- Ability.abilities
- end
-
def can_select_namespace?
several_namespaces? || admin
end
def can?(action, subject)
- abilities.allowed?(self, action, subject)
+ Ability.allowed?(self, action, subject)
end
def first_name
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
new file mode 100644
index 00000000000..118c100ca11
--- /dev/null
+++ b/app/policies/base_policy.rb
@@ -0,0 +1,116 @@
+class BasePolicy
+ class RuleSet
+ attr_reader :can_set, :cannot_set
+ def initialize(can_set, cannot_set)
+ @can_set = can_set
+ @cannot_set = cannot_set
+ end
+
+ def size
+ to_set.size
+ end
+
+ def self.empty
+ new(Set.new, Set.new)
+ end
+
+ def can?(ability)
+ @can_set.include?(ability) && !@cannot_set.include?(ability)
+ end
+
+ def include?(ability)
+ can?(ability)
+ end
+
+ def to_set
+ @can_set - @cannot_set
+ end
+
+ def merge(other)
+ @can_set.merge(other.can_set)
+ @cannot_set.merge(other.cannot_set)
+ end
+
+ def can!(*abilities)
+ @can_set.merge(abilities)
+ end
+
+ def cannot!(*abilities)
+ @cannot_set.merge(abilities)
+ end
+
+ def freeze
+ @can_set.freeze
+ @cannot_set.freeze
+ super
+ end
+ end
+
+ def self.abilities(user, subject)
+ new(user, subject).abilities
+ end
+
+ def self.class_for(subject)
+ return GlobalPolicy if subject.nil?
+
+ subject.class.ancestors.each do |klass|
+ next unless klass.name
+
+ begin
+ policy_class = "#{klass.name}Policy".constantize
+
+ # NOTE: the < operator here tests whether policy_class
+ # inherits from BasePolicy
+ return policy_class if policy_class < BasePolicy
+ rescue NameError
+ nil
+ end
+ end
+
+ raise "no policy for #{subject.class.name}"
+ end
+
+ attr_reader :user, :subject
+ def initialize(user, subject)
+ @user = user
+ @subject = subject
+ end
+
+ def abilities
+ return RuleSet.empty if @user && @user.blocked?
+ return anonymous_abilities if @user.nil?
+ collect_rules { rules }
+ end
+
+ def anonymous_abilities
+ collect_rules { anonymous_rules }
+ end
+
+ def anonymous_rules
+ rules
+ end
+
+ def delegate!(new_subject)
+ @rule_set.merge(Ability.allowed(@user, new_subject))
+ end
+
+ def can?(rule)
+ @rule_set.can?(rule)
+ end
+
+ def can!(*rules)
+ @rule_set.can!(*rules)
+ end
+
+ def cannot!(*rules)
+ @rule_set.cannot!(*rules)
+ end
+
+ private
+
+ def collect_rules(&b)
+ @rule_set = RuleSet.empty
+ yield
+ @rule_set
+ end
+end
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
new file mode 100644
index 00000000000..2232e231cf8
--- /dev/null
+++ b/app/policies/ci/build_policy.rb
@@ -0,0 +1,13 @@
+module Ci
+ class BuildPolicy < CommitStatusPolicy
+ def rules
+ super
+
+ # If we can't read build we should also not have that
+ # ability when looking at this in context of commit_status
+ %w(read create update admin).each do |rule|
+ cannot! :"#{rule}_commit_status" unless can? :"#{rule}_build"
+ end
+ end
+ end
+end
diff --git a/app/policies/ci/runner_policy.rb b/app/policies/ci/runner_policy.rb
new file mode 100644
index 00000000000..7edd383530d
--- /dev/null
+++ b/app/policies/ci/runner_policy.rb
@@ -0,0 +1,13 @@
+module Ci
+ class RunnerPolicy < BasePolicy
+ def rules
+ return unless @user
+
+ can! :assign_runner if @user.is_admin?
+
+ return if @subject.is_shared? || @subject.locked?
+
+ can! :assign_runner if @user.ci_authorized_runners.include?(@subject)
+ end
+ end
+end
diff --git a/app/policies/commit_status_policy.rb b/app/policies/commit_status_policy.rb
new file mode 100644
index 00000000000..593df738328
--- /dev/null
+++ b/app/policies/commit_status_policy.rb
@@ -0,0 +1,5 @@
+class CommitStatusPolicy < BasePolicy
+ def rules
+ delegate! @subject.project
+ end
+end
diff --git a/app/policies/deployment_policy.rb b/app/policies/deployment_policy.rb
new file mode 100644
index 00000000000..163d070ff90
--- /dev/null
+++ b/app/policies/deployment_policy.rb
@@ -0,0 +1,5 @@
+class DeploymentPolicy < BasePolicy
+ def rules
+ delegate! @subject.project
+ end
+end
diff --git a/app/policies/environment_policy.rb b/app/policies/environment_policy.rb
new file mode 100644
index 00000000000..f4219569161
--- /dev/null
+++ b/app/policies/environment_policy.rb
@@ -0,0 +1,5 @@
+class EnvironmentPolicy < BasePolicy
+ def rules
+ delegate! @subject.project
+ end
+end
diff --git a/app/policies/external_issue_policy.rb b/app/policies/external_issue_policy.rb
new file mode 100644
index 00000000000..d9e28bd107a
--- /dev/null
+++ b/app/policies/external_issue_policy.rb
@@ -0,0 +1,5 @@
+class ExternalIssuePolicy < BasePolicy
+ def rules
+ delegate! @subject.project
+ end
+end
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
new file mode 100644
index 00000000000..3c2fbe6b56b
--- /dev/null
+++ b/app/policies/global_policy.rb
@@ -0,0 +1,8 @@
+class GlobalPolicy < BasePolicy
+ def rules
+ return unless @user
+
+ can! :create_group if @user.can_create_group
+ can! :read_users_list
+ end
+end
diff --git a/app/policies/group_member_policy.rb b/app/policies/group_member_policy.rb
new file mode 100644
index 00000000000..62335527654
--- /dev/null
+++ b/app/policies/group_member_policy.rb
@@ -0,0 +1,19 @@
+class GroupMemberPolicy < BasePolicy
+ def rules
+ return unless @user
+
+ target_user = @subject.user
+ group = @subject.group
+
+ return if group.last_owner?(target_user)
+
+ can_manage = Ability.allowed?(@user, :admin_group_member, group)
+
+ if can_manage
+ can! :update_group_member
+ can! :destroy_group_member
+ elsif @user == target_user
+ can! :destroy_group_member
+ end
+ end
+end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
new file mode 100644
index 00000000000..97ff6233968
--- /dev/null
+++ b/app/policies/group_policy.rb
@@ -0,0 +1,45 @@
+class GroupPolicy < BasePolicy
+ def rules
+ can! :read_group if @subject.public?
+ return unless @user
+
+ globally_viewable = @subject.public? || (@subject.internal? && !@user.external?)
+ member = @subject.users.include?(@user)
+ owner = @user.admin? || @subject.has_owner?(@user)
+ master = owner || @subject.has_master?(@user)
+
+ can_read = false
+ can_read ||= globally_viewable
+ can_read ||= member
+ can_read ||= @user.admin?
+ can_read ||= GroupProjectsFinder.new(@subject).execute(@user).any?
+ can! :read_group if can_read
+
+ # Only group masters and group owners can create new projects
+ if master
+ can! :create_projects
+ can! :admin_milestones
+ end
+
+ # Only group owner and administrators can admin group
+ if owner
+ can! :admin_group
+ can! :admin_namespace
+ can! :admin_group_member
+ can! :change_visibility_level
+ end
+
+ if globally_viewable && @subject.request_access_enabled && !member
+ can! :request_access
+ end
+ end
+
+ def can_read_group?
+ return true if @subject.public?
+ return true if @user.admin?
+ return true if @subject.internal? && !@user.external?
+ return true if @subject.users.include?(@user)
+
+ GroupProjectsFinder.new(@subject).execute(@user).any?
+ end
+end
diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb
new file mode 100644
index 00000000000..c253f9a9399
--- /dev/null
+++ b/app/policies/issuable_policy.rb
@@ -0,0 +1,14 @@
+class IssuablePolicy < BasePolicy
+ def action_name
+ @subject.class.name.underscore
+ end
+
+ def rules
+ if @user && (@subject.author == @user || @subject.assignee == @user)
+ can! :"read_#{action_name}"
+ can! :"update_#{action_name}"
+ end
+
+ delegate! @subject.project
+ end
+end
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
new file mode 100644
index 00000000000..bd1811a3c54
--- /dev/null
+++ b/app/policies/issue_policy.rb
@@ -0,0 +1,28 @@
+class IssuePolicy < IssuablePolicy
+ def issue
+ @subject
+ end
+
+ def rules
+ super
+
+ if @subject.confidential? && !can_read_confidential?
+ cannot! :read_issue
+ cannot! :admin_issue
+ cannot! :update_issue
+ cannot! :read_issue
+ end
+ end
+
+ private
+
+ def can_read_confidential?
+ return false unless @user
+ return true if @user.admin?
+ return true if @subject.author == @user
+ return true if @subject.assignee == @user
+ return true if @subject.project.team.member?(@user, Gitlab::Access::REPORTER)
+
+ false
+ end
+end
diff --git a/app/policies/merge_request_policy.rb b/app/policies/merge_request_policy.rb
new file mode 100644
index 00000000000..bc3afc626fb
--- /dev/null
+++ b/app/policies/merge_request_policy.rb
@@ -0,0 +1,3 @@
+class MergeRequestPolicy < IssuablePolicy
+ # pass
+end
diff --git a/app/policies/namespace_policy.rb b/app/policies/namespace_policy.rb
new file mode 100644
index 00000000000..29bb357e00a
--- /dev/null
+++ b/app/policies/namespace_policy.rb
@@ -0,0 +1,10 @@
+class NamespacePolicy < BasePolicy
+ def rules
+ return unless @user
+
+ if @subject.owner == @user || @user.admin?
+ can! :create_projects
+ can! :admin_namespace
+ end
+ end
+end
diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb
new file mode 100644
index 00000000000..83847466ee2
--- /dev/null
+++ b/app/policies/note_policy.rb
@@ -0,0 +1,19 @@
+class NotePolicy < BasePolicy
+ def rules
+ delegate! @subject.project
+
+ return unless @user
+
+ if @subject.author == @user
+ can! :read_note
+ can! :update_note
+ can! :admin_note
+ can! :resolve_note
+ end
+
+ if @subject.for_merge_request? &&
+ @subject.noteable.author == @user
+ can! :resolve_note
+ end
+ end
+end
diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb
new file mode 100644
index 00000000000..46c5aa1a5be
--- /dev/null
+++ b/app/policies/personal_snippet_policy.rb
@@ -0,0 +1,16 @@
+class PersonalSnippetPolicy < BasePolicy
+ def rules
+ can! :read_personal_snippet if @subject.public?
+ return unless @user
+
+ if @subject.author == @user
+ can! :read_personal_snippet
+ can! :update_personal_snippet
+ can! :admin_personal_snippet
+ end
+
+ if @subject.internal? && !@user.external?
+ can! :read_personal_snippet
+ end
+ end
+end
diff --git a/app/policies/project_member_policy.rb b/app/policies/project_member_policy.rb
new file mode 100644
index 00000000000..1c038dddd4b
--- /dev/null
+++ b/app/policies/project_member_policy.rb
@@ -0,0 +1,22 @@
+class ProjectMemberPolicy < BasePolicy
+ def rules
+ # anonymous users have no abilities here
+ return unless @user
+
+ target_user = @subject.user
+ project = @subject.project
+
+ return if target_user == project.owner
+
+ can_manage = Ability.allowed?(@user, :admin_project_member, project)
+
+ if can_manage
+ can! :update_project_member
+ can! :destroy_project_member
+ end
+
+ if @user == target_user
+ can! :destroy_project_member
+ end
+ end
+end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
new file mode 100644
index 00000000000..acf36d422d1
--- /dev/null
+++ b/app/policies/project_policy.rb
@@ -0,0 +1,224 @@
+class ProjectPolicy < BasePolicy
+ def rules
+ team_access!(user)
+
+ owner = user.admin? ||
+ project.owner == user ||
+ (project.group && project.group.has_owner?(user))
+
+ owner_access! if owner
+
+ if project.public? || (project.internal? && !user.external?)
+ guest_access!
+ public_access!
+
+ # Allow to read builds for internal projects
+ can! :read_build if project.public_builds?
+
+ if project.request_access_enabled &&
+ !(owner || project.team.member?(user) || project_group_member?(user))
+ can! :request_access
+ end
+ end
+
+ archived_access! if project.archived?
+
+ disabled_features!
+ end
+
+ def project
+ @subject
+ end
+
+ def guest_access!
+ can! :read_project
+ can! :read_board
+ can! :read_list
+ can! :read_wiki
+ can! :read_issue
+ can! :read_label
+ can! :read_milestone
+ can! :read_project_snippet
+ can! :read_project_member
+ can! :read_merge_request
+ can! :read_note
+ can! :create_project
+ can! :create_issue
+ can! :create_note
+ can! :upload_file
+ end
+
+ def reporter_access!
+ can! :download_code
+ can! :fork_project
+ can! :create_project_snippet
+ can! :update_issue
+ can! :admin_issue
+ can! :admin_label
+ can! :admin_list
+ can! :read_commit_status
+ can! :read_build
+ can! :read_container_image
+ can! :read_pipeline
+ can! :read_environment
+ can! :read_deployment
+ end
+
+ def developer_access!
+ can! :admin_merge_request
+ can! :update_merge_request
+ can! :create_commit_status
+ can! :update_commit_status
+ can! :create_build
+ can! :update_build
+ can! :create_pipeline
+ can! :update_pipeline
+ can! :create_merge_request
+ can! :create_wiki
+ can! :push_code
+ can! :resolve_note
+ can! :create_container_image
+ can! :update_container_image
+ can! :create_environment
+ can! :create_deployment
+ end
+
+ def master_access!
+ can! :push_code_to_protected_branches
+ can! :update_project_snippet
+ can! :update_environment
+ can! :update_deployment
+ can! :admin_milestone
+ can! :admin_project_snippet
+ can! :admin_project_member
+ can! :admin_merge_request
+ can! :admin_note
+ can! :admin_wiki
+ can! :admin_project
+ can! :admin_commit_status
+ can! :admin_build
+ can! :admin_container_image
+ can! :admin_pipeline
+ can! :admin_environment
+ can! :admin_deployment
+ end
+
+ def public_access!
+ can! :download_code
+ can! :fork_project
+ can! :read_commit_status
+ can! :read_pipeline
+ can! :read_container_image
+ end
+
+ def owner_access!
+ guest_access!
+ reporter_access!
+ developer_access!
+ master_access!
+ can! :change_namespace
+ can! :change_visibility_level
+ can! :rename_project
+ can! :remove_project
+ can! :archive_project
+ can! :remove_fork_project
+ can! :destroy_merge_request
+ can! :destroy_issue
+ end
+
+ # Push abilities on the users team role
+ 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
+ end
+
+ def archived_access!
+ cannot! :create_merge_request
+ cannot! :push_code
+ cannot! :push_code_to_protected_branches
+ cannot! :update_merge_request
+ cannot! :admin_merge_request
+ end
+
+ def disabled_features!
+ unless project.feature_available?(:issues, user)
+ cannot!(*named_abilities(:issue))
+ end
+
+ unless project.feature_available?(:merge_requests, user)
+ cannot!(*named_abilities(:merge_request))
+ end
+
+ unless project.feature_available?(:issues, user) || project.feature_available?(:merge_requests, user)
+ cannot!(*named_abilities(:label))
+ cannot!(*named_abilities(:milestone))
+ end
+
+ unless project.feature_available?(:snippets, user)
+ cannot!(*named_abilities(:project_snippet))
+ end
+
+ unless project.feature_available?(:wiki, user) || project.has_external_wiki?
+ cannot!(*named_abilities(:wiki))
+ end
+
+ unless project.feature_available?(:builds, user)
+ cannot!(*named_abilities(:build))
+ cannot!(*named_abilities(:pipeline))
+ cannot!(*named_abilities(:environment))
+ cannot!(*named_abilities(:deployment))
+ end
+
+ unless project.container_registry_enabled
+ cannot!(*named_abilities(:container_image))
+ end
+ end
+
+ def anonymous_rules
+ return unless project.public?
+
+ can! :read_project
+ can! :read_board
+ can! :read_list
+ can! :read_wiki
+ can! :read_label
+ can! :read_milestone
+ can! :read_project_snippet
+ can! :read_project_member
+ can! :read_merge_request
+ can! :read_note
+ can! :read_pipeline
+ can! :read_commit_status
+ can! :read_container_image
+ can! :download_code
+
+ # NOTE: may be overridden by IssuePolicy
+ can! :read_issue
+
+ # Allow to read builds by anonymous user if guests are allowed
+ can! :read_build if project.public_builds?
+
+ disabled_features!
+ end
+
+ def project_group_member?(user)
+ project.group &&
+ (
+ project.group.members.exists?(user_id: user.id) ||
+ project.group.requesters.exists?(user_id: user.id)
+ )
+ end
+
+ def named_abilities(name)
+ [
+ :"read_#{name}",
+ :"create_#{name}",
+ :"update_#{name}",
+ :"admin_#{name}"
+ ]
+ end
+end
diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb
new file mode 100644
index 00000000000..57acccfafd9
--- /dev/null
+++ b/app/policies/project_snippet_policy.rb
@@ -0,0 +1,20 @@
+class ProjectSnippetPolicy < BasePolicy
+ def rules
+ can! :read_project_snippet if @subject.public?
+ return unless @user
+
+ if @user && @subject.author == @user || @user.admin?
+ can! :read_project_snippet
+ can! :update_project_snippet
+ can! :admin_project_snippet
+ end
+
+ if @subject.internal? && !@user.external?
+ can! :read_project_snippet
+ end
+
+ if @subject.private? && @subject.project.team.member?(@user)
+ can! :read_project_snippet
+ end
+ end
+end
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
new file mode 100644
index 00000000000..03a2499e263
--- /dev/null
+++ b/app/policies/user_policy.rb
@@ -0,0 +1,11 @@
+class UserPolicy < BasePolicy
+ include Gitlab::CurrentSettings
+
+ def rules
+ can! :read_user if @user || !restricted_public_level?
+ end
+
+ def restricted_public_level?
+ current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC)
+ end
+end
diff --git a/app/services/base_service.rb b/app/services/base_service.rb
index 0d55ba5a981..0c208150fb8 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -7,12 +7,8 @@ class BaseService
@project, @current_user, @params = project, user, params.dup
end
- def abilities
- Ability.abilities
- end
-
def can?(object, action, subject)
- abilities.allowed?(object, action, subject)
+ Ability.allowed?(object, action, subject)
end
def notification_service
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index 435a8c6e681..34efd09ed9f 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -36,7 +36,12 @@ module Boards
end
def set_state
- params[:state] = list.done? ? 'closed' : 'opened'
+ params[:state] =
+ case list.list_type.to_sym
+ when :backlog then 'opened'
+ when :done then 'closed'
+ else 'all'
+ end
end
def board_label_ids
diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb
index 5cb408b9d20..b1887820bd4 100644
--- a/app/services/boards/lists/create_service.rb
+++ b/app/services/boards/lists/create_service.rb
@@ -3,7 +3,10 @@ module Boards
class CreateService < Boards::BaseService
def execute
List.transaction do
- create_list_at(next_position)
+ label = project.labels.find(params[:label_id])
+ position = next_position
+
+ create_list(label, position)
end
end
@@ -14,8 +17,8 @@ module Boards
max_position.nil? ? 0 : max_position.succ
end
- def create_list_at(position)
- board.lists.create(params.merge(list_type: :label, position: position))
+ def create_list(label, position)
+ board.lists.create(label: label, list_type: :label, position: position)
end
end
end
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index 6f7610d42ba..de48a50774e 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -10,13 +10,15 @@ module Ci
create_builds!
end
- new_builds =
- stage_indexes_of_created_builds.map do |index|
- process_stage(index)
- end
+ @pipeline.with_lock do
+ new_builds =
+ stage_indexes_of_created_builds.map do |index|
+ process_stage(index)
+ end
- # Return a flag if a when builds got enqueued
- new_builds.flatten.any?
+ # Return a flag if a when builds got enqueued
+ new_builds.flatten.any?
+ end
end
private
@@ -34,7 +36,7 @@ module Ci
end
def process_build(build, current_status)
- return false unless Statuseable::COMPLETED_STATUSES.include?(current_status)
+ return false unless HasStatus::COMPLETED_STATUSES.include?(current_status)
if valid_statuses_for_when(build.when).include?(current_status)
build.enqueue
diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb
index 9a187f5d694..6973191b203 100644
--- a/app/services/ci/register_build_service.rb
+++ b/app/services/ci/register_build_service.rb
@@ -8,16 +8,18 @@ module Ci
builds =
if current_runner.shared?
builds.
- # don't run projects which have not enabled shared runners
- joins(:project).where(projects: { builds_enabled: true, shared_runners_enabled: true }).
+ # don't run projects which have not enabled shared runners and builds
+ joins(:project).where(projects: { shared_runners_enabled: true }).
+ joins('LEFT JOIN project_features ON ci_builds.gl_project_id = project_features.project_id').
# this returns builds that are ordered by number of running builds
# we prefer projects that don't use shared runners at all
joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id").
+ where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
else
# do run projects which are only assigned to this runner (FIFO)
- builds.where(project: current_runner.projects.where(builds_enabled: true)).order('created_at ASC')
+ builds.where(project: current_runner.projects.with_builds_enabled).order('created_at ASC')
end
build = builds.find do |build|
diff --git a/app/services/ci/web_hook_service.rb b/app/services/ci/web_hook_service.rb
deleted file mode 100644
index 92e6df442b4..00000000000
--- a/app/services/ci/web_hook_service.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-module Ci
- class WebHookService
- def build_end(build)
- execute_hooks(build.project, build_data(build))
- end
-
- def execute_hooks(project, data)
- project.web_hooks.each do |web_hook|
- async_execute_hook(web_hook, data)
- end
- end
-
- def async_execute_hook(hook, data)
- Sidekiq::Client.enqueue(Ci::WebHookWorker, hook.id, data)
- end
-
- def build_data(build)
- project = build.project
- data = {}
- data.merge!({
- build_id: build.id,
- build_name: build.name,
- build_status: build.status,
- build_started_at: build.started_at,
- build_finished_at: build.finished_at,
- project_id: project.id,
- project_name: project.name,
- gitlab_url: project.gitlab_url,
- ref: build.ref,
- before_sha: build.before_sha,
- sha: build.sha,
- })
- end
- end
-end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index e06c37c323e..4c8d93999a7 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -45,6 +45,7 @@ class IssuableBaseService < BaseService
unless can?(current_user, ability, project)
params.delete(:milestone_id)
+ params.delete(:labels)
params.delete(:add_label_ids)
params.delete(:remove_label_ids)
params.delete(:label_ids)
@@ -72,6 +73,7 @@ class IssuableBaseService < BaseService
filter_labels_in_param(:add_label_ids)
filter_labels_in_param(:remove_label_ids)
filter_labels_in_param(:label_ids)
+ find_or_create_label_ids
end
def filter_labels_in_param(key)
@@ -80,6 +82,17 @@ class IssuableBaseService < BaseService
params[key] = project.labels.where(id: params[key]).pluck(:id)
end
+ def find_or_create_label_ids
+ labels = params.delete(:labels)
+ return unless labels
+
+ params[:label_ids] = labels.split(",").map do |label_name|
+ project.labels.create_with(color: Label::DEFAULT_COLOR)
+ .find_or_create_by(title: label_name.strip)
+ .id
+ end
+ end
+
def process_label_ids(attributes, existing_label_ids: nil)
label_ids = attributes.delete(:label_ids)
add_label_ids = attributes.delete(:add_label_ids)
@@ -162,7 +175,12 @@ class IssuableBaseService < BaseService
if params.present? && update_issuable(issuable, params)
issuable.reset_events_cache
- handle_common_system_notes(issuable, old_labels: old_labels)
+
+ # We do not touch as it will affect a update on updated_at field
+ ActiveRecord::Base.no_touching do
+ handle_common_system_notes(issuable, old_labels: old_labels)
+ end
+
handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users)
issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update')
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 290742f1506..e57791f6818 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -83,7 +83,7 @@ module MergeRequests
closes_issue = "Closes ##{iid}"
if merge_request.description.present?
- merge_request.description += closes_issue.prepend("\n")
+ merge_request.description += closes_issue.prepend("\n\n")
else
merge_request.description = closes_issue
end
diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb
index 08c1f72d65a..1262ecbc29a 100644
--- a/app/services/merge_requests/get_urls_service.rb
+++ b/app/services/merge_requests/get_urls_service.rb
@@ -31,7 +31,7 @@ module MergeRequests
def get_branches(changes)
return [] if project.empty_repo?
- return [] unless project.merge_requests_enabled
+ return [] unless project.merge_requests_enabled?
changes_list = Gitlab::ChangesList.new(changes)
changes_list.map do |change|
diff --git a/app/services/merge_requests/resolve_service.rb b/app/services/merge_requests/resolve_service.rb
index adc71b0c2bc..19caa038c44 100644
--- a/app/services/merge_requests/resolve_service.rb
+++ b/app/services/merge_requests/resolve_service.rb
@@ -1,11 +1,14 @@
module MergeRequests
class ResolveService < MergeRequests::BaseService
- attr_accessor :conflicts, :rugged, :merge_index
+ attr_accessor :conflicts, :rugged, :merge_index, :merge_request
def execute(merge_request)
@conflicts = merge_request.conflicts
@rugged = project.repository.rugged
@merge_index = conflicts.merge_index
+ @merge_request = merge_request
+
+ fetch_their_commit!
conflicts.files.each do |file|
write_resolved_file_to_index(file, params[:sections])
@@ -27,5 +30,21 @@ module MergeRequests
merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode)
merge_index.conflict_remove(our_path)
end
+
+ # If their commit (in the target project) doesn't exist in the source project, it
+ # can't be a parent for the merge commit we're about to create. If that's the case,
+ # fetch the target branch ref into the source project so the commit exists in both.
+ #
+ def fetch_their_commit!
+ return if rugged.include?(conflicts.their_commit.oid)
+
+ random_string = SecureRandom.hex
+
+ project.repository.fetch_ref(
+ merge_request.target_project.repository.path_to_repo,
+ "refs/heads/#{merge_request.target_branch}",
+ "refs/tmp/#{random_string}/head"
+ )
+ end
end
end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 30c5f24988c..398ec47f0ea 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -11,6 +11,10 @@ module MergeRequests
params.except!(:target_project_id)
params.except!(:source_branch)
+ if merge_request.closed_without_fork?
+ params.except!(:target_branch, :force_remove_source_branch)
+ end
+
merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
update(merge_request)
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 55956be2844..be749ba4a1c 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -7,7 +7,6 @@ module Projects
def execute
forked_from_project_id = params.delete(:forked_from_project_id)
import_data = params.delete(:import_data)
-
@project = Project.new(params)
# Make sure that the user is allowed to use the specified visibility level
@@ -81,8 +80,7 @@ module Projects
log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"")
unless @project.gitlab_project_import?
- @project.create_wiki if @project.wiki_enabled?
-
+ @project.create_wiki if @project.feature_available?(:wiki, current_user)
@project.build_missing_services
@project.create_labels
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index de6dc38cc8e..a2de4dccece 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -8,7 +8,6 @@ module Projects
name: @project.name,
path: @project.path,
shared_runners_enabled: @project.shared_runners_enabled,
- builds_enabled: @project.builds_enabled,
namespace_id: @params[:namespace].try(:id) || current_user.namespace.id
}
@@ -17,6 +16,9 @@ module Projects
end
new_project = CreateService.new(current_user, new_params).execute
+ builds_access_level = @project.project_feature.builds_access_level
+ new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
+
new_project
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 546a8f11330..0c8446e7c3d 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -269,11 +269,11 @@ module SystemNoteService
#
# Example Note text:
#
- # "mentioned in #1"
+ # "Mentioned in #1"
#
- # "mentioned in !2"
+ # "Mentioned in !2"
#
- # "mentioned in 54f7727c"
+ # "Mentioned in 54f7727c"
#
# See cross_reference_note_content.
#
@@ -308,7 +308,7 @@ module SystemNoteService
# Check if a cross-reference is disallowed
#
- # This method prevents adding a "mentioned in !1" note on every single commit
+ # This method prevents adding a "Mentioned in !1" note on every single commit
# in a merge request. Additionally, it prevents the creation of references to
# external issues (which would fail).
#
@@ -417,7 +417,7 @@ module SystemNoteService
end
def cross_reference_note_prefix
- 'mentioned in '
+ 'Mentioned in '
end
def cross_reference_note_content(gfm_reference)
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index e0ccb654590..2aab8c736d6 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -148,7 +148,8 @@ class TodoService
def mark_todos_as_done_by_ids(ids, current_user)
todos = current_user.todos.where(id: ids)
- marked_todos = todos.update_all(state: :done)
+ # Only return those that are not really on that state
+ marked_todos = todos.where.not(state: :done).update_all(state: :done)
current_user.update_todos_count_cache
marked_todos
end
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index 92e2dae4842..9175b3d3f96 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -13,7 +13,7 @@
.col-sm-10
= f.text_area :description, class: "form-control", rows: 10
.hint
- Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('markdown/markdown'), target: '_blank'}.
+ Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('user/markdown'), target: '_blank'}.
.form-group
= f.label :logo, class: 'control-label'
.col-sm-10
diff --git a/app/views/admin/background_jobs/_head.html.haml b/app/views/admin/background_jobs/_head.html.haml
index 89d7a40d6b0..107fc25244a 100644
--- a/app/views/admin/background_jobs/_head.html.haml
+++ b/app/views/admin/background_jobs/_head.html.haml
@@ -1,22 +1,24 @@
-.nav-links.sub-nav
- %ul{ class: (container_class) }
- = nav_link(controller: :system_info) do
- = link_to admin_system_info_path, title: 'System Info' do
- %span
- System Info
- = nav_link(controller: :background_jobs) do
- = link_to admin_background_jobs_path, title: 'Background Jobs' do
- %span
- Background Jobs
- = nav_link(controller: :logs) do
- = link_to admin_logs_path, title: 'Logs' do
- %span
- Logs
- = nav_link(controller: :health_check) do
- = link_to admin_health_check_path, title: 'Health Check' do
- %span
- Health Check
- = nav_link(controller: :requests_profiles) do
- = link_to admin_requests_profiles_path, title: 'Requests Profiles' do
- %span
- Requests Profiles
+.scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ = nav_link(controller: :system_info) do
+ = link_to admin_system_info_path, title: 'System Info' do
+ %span
+ System Info
+ = nav_link(controller: :background_jobs) do
+ = link_to admin_background_jobs_path, title: 'Background Jobs' do
+ %span
+ Background Jobs
+ = nav_link(controller: :logs) do
+ = link_to admin_logs_path, title: 'Logs' do
+ %span
+ Logs
+ = nav_link(controller: :health_check) do
+ = link_to admin_health_check_path, title: 'Health Check' do
+ %span
+ Health Check
+ = nav_link(controller: :requests_profiles) do
+ = link_to admin_requests_profiles_path, title: 'Requests Profiles' do
+ %span
+ Requests Profiles
diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml
index b74da64f82e..c91ab4cb946 100644
--- a/app/views/admin/dashboard/_head.html.haml
+++ b/app/views/admin/dashboard/_head.html.haml
@@ -1,26 +1,28 @@
-.nav-links.sub-nav
- %ul{ class: (container_class) }
- = nav_link(controller: :dashboard, html_options: {class: 'home'}) do
- = link_to admin_root_path, title: 'Overview' do
- %span
- Overview
- = nav_link(controller: [:admin, :projects]) do
- = link_to admin_namespaces_projects_path, title: 'Projects' do
- %span
- Projects
- = nav_link(controller: :users) do
- = link_to admin_users_path, title: 'Users' do
- %span
- Users
- = nav_link(controller: :groups) do
- = link_to admin_groups_path, title: 'Groups' do
- %span
- Groups
- = nav_link path: 'builds#index' do
- = link_to admin_builds_path, title: 'Builds' do
- %span
- Builds
- = nav_link path: ['runners#index', 'runners#show'] do
- = link_to admin_runners_path, title: 'Runners' do
- %span
- Runners
+.scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ = nav_link(controller: :dashboard, html_options: {class: 'home'}) do
+ = link_to admin_root_path, title: 'Overview' do
+ %span
+ Overview
+ = nav_link(controller: [:admin, :projects]) do
+ = link_to admin_namespaces_projects_path, title: 'Projects' do
+ %span
+ Projects
+ = nav_link(controller: :users) do
+ = link_to admin_users_path, title: 'Users' do
+ %span
+ Users
+ = nav_link(controller: :groups) do
+ = link_to admin_groups_path, title: 'Groups' do
+ %span
+ Groups
+ = nav_link path: 'builds#index' do
+ = link_to admin_builds_path, title: 'Builds' do
+ %span
+ Builds
+ = nav_link path: ['runners#index', 'runners#show'] do
+ = link_to admin_runners_path, title: 'Runners' do
+ %span
+ Runners
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index b2c607361b3..6c7c3c48604 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -73,6 +73,12 @@
%span.light last commit:
%strong
= last_commit(@project)
+
+ %li
+ %span.light Git LFS status:
+ %strong
+ = project_lfs_status(@project)
+ = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
- else
%li
%span.light repository:
diff --git a/app/views/admin/system_info/show.html.haml b/app/views/admin/system_info/show.html.haml
index 6956e5ab795..bfc6142067a 100644
--- a/app/views/admin/system_info/show.html.haml
+++ b/app/views/admin/system_info/show.html.haml
@@ -9,12 +9,20 @@
.light-well
%h4 CPU
.data
- %h1= "#{@cpus} cores"
+ - if @cpus
+ %h1= "#{@cpus.length} cores"
+ - else
+ = icon('warning', class: 'text-warning')
+ Unable to collect CPU info
.col-sm-4
.light-well
%h4 Memory
.data
- %h1= "#{number_to_human_size(@mem_used)} / #{number_to_human_size(@mem_total)}"
+ - if @memory
+ %h1= "#{number_to_human_size(@memory.active_bytes)} / #{number_to_human_size(@memory.total_bytes)}"
+ - else
+ = icon('warning', class: 'text-warning')
+ Unable to collect memory info
.col-sm-4
.light-well
%h4 Disks
diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml
index 0044d779c31..889086c62b1 100644
--- a/app/views/ci/lints/show.html.haml
+++ b/app/views/ci/lints/show.html.haml
@@ -1,3 +1,6 @@
+- page_title "CI Lint"
+- page_description "Validate your GitLab CI configuration file"
+
%h2 Check your .gitlab-ci.yml
%hr
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index d320d3bcc1e..9d31f31c639 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -28,21 +28,25 @@
.row-content-block.second-block
= form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form' do
.filter-item.inline
- = select_tag('project_id', todo_projects_options,
- class: 'select2 trigger-submit', include_blank: true,
- data: {placeholder: 'Project'})
+ - if params[:project_id].present?
+ = hidden_field_tag(:project_id, params[:project_id])
+ = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
+ placeholder: 'Search projects', data: { data: todo_projects_options } })
.filter-item.inline
- = users_select_tag(:author_id, selected: params[:author_id],
- placeholder: 'Author', class: 'trigger-submit', any_user: "Any Author", first_user: true, current_user: true)
+ - if params[:author_id].present?
+ = hidden_field_tag(:author_id, params[:author_id])
+ = dropdown_tag(user_dropdown_label(params[:author_id], 'Author'), options: { toggle_class: 'js-user-search js-filter-submit js-author-search', title: 'Filter by author', filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit',
+ placeholder: 'Search authors', data: { any_user: 'Any Author', first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: 'author_id', default_label: 'Author' } })
.filter-item.inline
- = select_tag('type', todo_types_options,
- class: 'select2 trigger-submit', include_blank: true,
- data: {placeholder: 'Type'})
+ - if params[:type].present?
+ = hidden_field_tag(:type, params[:type])
+ = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit',
+ data: { data: todo_types_options } })
.filter-item.inline.actions-filter
- = select_tag('action_id', todo_actions_options,
- class: 'select2 trigger-submit', include_blank: true,
- data: {placeholder: 'Action'})
-
+ - if params[:action_id].present?
+ = hidden_field_tag(:action_id, params[:action_id])
+ = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit',
+ data: { data: todo_actions_options }})
.pull-right
.dropdown.inline.prepend-left-10
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
@@ -66,7 +70,7 @@
- if @todos.any?
.js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} }
- @todos.group_by(&:project).each do |group|
- .panel.panel-default.panel-small.js-todos-list
+ .panel.panel-default.panel-small
- project = group[0]
.panel-heading
= link_to project.name_with_namespace, namespace_project_path(project.namespace, project)
@@ -76,11 +80,3 @@
= paginate @todos, theme: "gitlab"
- else
.nothing-here-block You're all done!
-
-:javascript
- new UsersSelect();
-
- $('form.filter-form').on('submit', function (event) {
- event.preventDefault();
- Turbolinks.visit(this.action + '&' + $(this).serialize());
- });
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 5c318cd3b8b..31fdcc5e21b 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -1,7 +1,7 @@
- if event.visible_to_user?(current_user)
.event-item{ class: event_row_class(event) }
.event-item-timestamp
- #{time_ago_with_tooltip(event.created_at)}
+ #{time_ago_with_tooltip(event.created_at, skip_js: true)}
= cache [event, current_application_settings, "v2.2"] do
= author_avatar(event, size: 40)
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index 57f6e7e0612..b8248a80a27 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -24,7 +24,7 @@
- else
= sort_title_recently_created
%b.caret
- %ul.dropdown-menu
+ %ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to explore_groups_path(sort: sort_value_recently_created) do
= sort_title_recently_created
diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml
index 742f9d7a433..3be7ed8432c 100644
--- a/app/views/groups/group_members/update.js.haml
+++ b/app/views/groups/group_members/update.js.haml
@@ -1,3 +1,3 @@
:plain
$("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @group_member))}');
- new MemberExpirationDate();
+ new gl.MemberExpirationDate();
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index 85e188d6f8b..d16bd61b779 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -549,4 +549,4 @@
%li wiki page
%li help page
- You can check how markdown rendered at #{link_to 'Markdown help page', help_page_path("markdown/markdown")}.
+ You can check how markdown rendered at #{link_to 'Markdown help page', help_page_path("user/markdown")}.
diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml
deleted file mode 100644
index ed3afb0ce33..00000000000
--- a/app/views/import/gitorious/status.html.haml
+++ /dev/null
@@ -1,54 +0,0 @@
-- page_title "Gitorious import"
-- header_title "Projects", root_path
-%h3.page-title
- %i.icon-gitorious.icon-gitorious-big
- Import projects from Gitorious.org
-
-%p.light
- Select projects you want to import.
-%hr
-%p
- = button_tag class: "btn btn-import btn-success js-import-all" do
- Import all projects
- = icon("spinner spin", class: "loading-icon")
-
-.table-responsive
- %table.table.import-jobs
- %colgroup.import-jobs-from-col
- %colgroup.import-jobs-to-col
- %colgroup.import-jobs-status-col
- %thead
- %tr
- %th From Gitorious.org
- %th To GitLab
- %th Status
- %tbody
- - @already_added_projects.each do |project|
- %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
- %td
- = link_to project.import_source, "https://gitorious.org/#{project.import_source}", target: "_blank"
- %td
- = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
- %td.job-status
- - if project.import_status == 'finished'
- %span
- %i.fa.fa-check
- done
- - elsif project.import_status == 'started'
- %i.fa.fa-spinner.fa-spin
- started
- - else
- = project.human_import_status_name
-
- - @repos.each do |repo|
- %tr{id: "repo_#{repo.id}"}
- %td
- = link_to repo.full_name, "https://gitorious.org/#{repo.full_name}", target: "_blank"
- %td.import-target
- = repo.full_name
- %td.import-actions.job-status
- = button_tag class: "btn btn-import js-add-to-import" do
- Import
- = icon("spinner spin", class: "loading-icon")
-
-.js-importer-status{ data: { jobs_import_path: "#{jobs_import_gitorious_path}", import_path: "#{import_gitorious_path}" } }
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index d7d36c84b6c..27ac1760166 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -1,5 +1,5 @@
+= render 'layouts/nav/group_settings'
.scrolling-tabs-container{ class: nav_control_class }
- = render 'layouts/nav/group_settings'
.fade-left
= icon('angle-left')
.fade-right
diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml
index bf9a7ecb786..75275afc0f3 100644
--- a/app/views/layouts/nav/_group_settings.html.haml
+++ b/app/views/layouts/nav/_group_settings.html.haml
@@ -1,22 +1,26 @@
- if current_user
+ - can_admin_group = can?(current_user, :admin_group, @group)
- can_edit = can?(current_user, :admin_group, @group)
- member = @group.members.find_by(user_id: current_user.id)
- can_leave = member && can?(current_user, :destroy_group_member, member)
- .controls
- .dropdown.group-settings-dropdown
- %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'}
- = icon('cog')
- = icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-align-right
- = nav_link(path: 'groups#projects') do
- = link_to 'Projects', projects_group_path(@group), title: 'Projects'
- %li.divider
- - if can_edit
- %li
- = link_to 'Edit Group', edit_group_path(@group)
- - if can_leave
- %li
- = link_to polymorphic_path([:leave, @group, :members]),
- data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do
- Leave Group
+ - if can_admin_group || can_edit || can_leave
+ .controls
+ .dropdown.group-settings-dropdown
+ %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'}
+ = icon('cog')
+ = icon('caret-down')
+ %ul.dropdown-menu.dropdown-menu-align-right
+ - if can_admin_group
+ = nav_link(path: 'groups#projects') do
+ = link_to 'Projects', projects_group_path(@group), title: 'Projects'
+ - if can_edit || can_leave
+ %li.divider
+ - if can_edit
+ %li
+ = link_to 'Edit Group', edit_group_path(@group)
+ - if can_leave
+ %li
+ = link_to polymorphic_path([:leave, @group, :members]),
+ data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do
+ Leave Group
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index 52a5bdc1a1b..613b8b7d301 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -26,7 +26,7 @@
%span
Protected Branches
- - if @project.builds_enabled?
+ - if @project.feature_available?(:builds, current_user)
= nav_link(controller: :runners) do
= link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do
%span
diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml
index 19b4249374b..14eb47089b1 100644
--- a/app/views/projects/_merge_request_settings.html.haml
+++ b/app/views/projects/_merge_request_settings.html.haml
@@ -1,11 +1,14 @@
-%fieldset.builds-feature
- %h5.prepend-top-0
- Merge Requests
- .form-group
- .checkbox
- = f.label :only_allow_merge_if_build_succeeds do
- = f.check_box :only_allow_merge_if_build_succeeds
- %strong Only allow merge requests to be merged if the build succeeds
- .help-block
- Builds need to be configured to enable this feature.
- = link_to icon('question-circle'), help_page_path('workflow/merge_requests', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds')
+.merge-requests-feature
+ %fieldset.builds-feature
+ %hr
+ %h5.prepend-top-0
+ Merge Requests
+ .form-group
+ .checkbox
+ = f.label :only_allow_merge_if_build_succeeds do
+ = f.check_box :only_allow_merge_if_build_succeeds
+ %strong Only allow merge requests to be merged if the build succeeds
+ %br
+ %span.descr
+ Builds need to be configured to enable this feature.
+ = link_to icon('question-circle'), help_page_path('workflow/merge_requests', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds')
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
index de53a298f84..73066150fb3 100644
--- a/app/views/projects/boards/components/_board.html.haml
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -13,19 +13,13 @@
%h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" }
{{ list.title }}
%span.pull-right{ "v-if" => "list.type !== 'blank'" }
- {{ list.issues.length }}
+ {{ list.issuesSize }}
- if can?(current_user, :admin_list, @project)
%board-delete{ "inline-template" => true,
":list" => "list",
"v-if" => "!list.preset && list.id" }
%button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
= icon("trash")
- = icon("spinner spin", class: "board-header-loading-spinner pull-right", "v-show" => "list.loadingMore")
- .board-inner-container.board-search-container{ "v-if" => "list.canSearch()" }
- %input.form-control{ type: "text", placeholder: "Search issues", "v-model" => "query", "debounce" => "250" }
- = icon("search", class: "board-search-icon", "v-show" => "!query")
- %button.board-search-clear-btn{ type: "button", role: "button", "aria-label" => "Clear search", "@click" => "query = ''", "v-show" => "query" }
- = icon("times", class: "board-search-clear")
%board-list{ "inline-template" => true,
"v-if" => "list.type !== 'blank'",
":list" => "list",
@@ -39,5 +33,11 @@
"v-show" => "!loading",
":data-board" => "list.id" }
= render "projects/boards/components/card"
+ %li.board-list-count.text-center{ "v-if" => "showCount" }
+ = icon("spinner spin", "v-show" => "list.loadingMore" )
+ %span{ "v-if" => "list.issues.length === list.issuesSize" }
+ Showing all issues
+ %span{ "v-else" => true }
+ Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
- if can?(current_user, :admin_list, @project)
= render "projects/boards/components/blank_state"
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 6192ccb710b..808e6b95746 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -27,6 +27,8 @@
= link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-default', method: :post, title: "Compare" do
Compare
+ = render 'projects/buttons/download', project: @project, ref: branch.name
+
- if can_remove_branch?(@project, branch.name)
= link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do
= icon("trash-o")
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index 5b0b58e087b..5ce36a475a9 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -1,3 +1,6 @@
+- builds = @build.pipeline.builds.latest.to_a
+- statuses = ["failed", "pending", "running", "canceled", "success", "skipped"]
+
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar
.block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default
Build
@@ -11,40 +14,6 @@
%p.build-detail-row
#{@build.coverage}%
- - builds = @build.pipeline.builds.latest.to_a
- - statuses = ["failed", "pending", "running", "canceled", "success", "skipped"]
- - if builds.size > 1
- .dropdown.build-dropdown
- .build-light-text 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|
- %li
- %a.stage-item= stage
-
- .builds-container
- - statuses.each do |build_status|
- - builds.select{|build| build.status == build_status}.each do |build|
- .build-job{class: ('active' if build == @build), data: {stage: build.stage}}
- = link_to namespace_project_build_path(@project.namespace, @project, build) do
- = icon('check')
- = ci_icon_for_status(build.status)
- %span
- - if build.name
- = build.name
- - else
- = build.id
-
- - if @build.retried?
- %li.active
- %a
- Build ##{@build.id}
- &middot;
- %i.fa.fa-warning
- This build was retried.
-
.blocks-container
- if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
.block{ class: ("block-first" if !@build.coverage) }
@@ -76,7 +45,7 @@
.title
Build details
- if can?(current_user, :update_build, @build) && @build.retryable?
- = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right', method: :post
+ = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post
- if @build.merge_request
%p.build-detail-row
%span.build-light-text Merge Request:
@@ -100,7 +69,7 @@
- elsif @build.runner
\##{@build.runner.id}
.btn-group.btn-group-justified{ role: :group }
- - if @build.has_trace?
+ - if @build.has_trace_file?
= link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default'
- if @build.active?
= link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post
@@ -141,3 +110,35 @@
- @build.tag_list.each do |tag|
%span.label.label-primary
= tag
+
+ - if builds.size > 1
+ .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|
+ %li
+ %a.stage-item= stage
+
+ .builds-container
+ - statuses.each do |build_status|
+ - builds.select{|build| build.status == build_status}.each do |build|
+ .build-job{class: ('active' if build == @build), data: {stage: build.stage}}
+ = link_to namespace_project_build_path(@project.namespace, @project, build) do
+ = icon('check')
+ = ci_icon_for_status(build.status)
+ %span
+ - if build.name
+ = build.name
+ - else
+ = build.id
+
+ - if @build.retried?
+ %li.active
+ %a
+ Build ##{@build.id}
+ &middot;
+ %i.fa.fa-warning
+ This build was retried.
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 58f43ecb5d5..5f5e071eb40 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -1,4 +1,42 @@
-- unless @project.empty_repo?
- - if can? current_user, :download_code, @project
- = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has-tooltip', data: {container: "body"}, rel: 'nofollow', title: "Download ZIP" do
- = icon('download')
+- if !project.empty_repo? && can?(current_user, :download_code, project)
+ %span.btn-group{class: 'hidden-xs hidden-sm btn-grouped'}
+ .dropdown.inline
+ %button.btn{ 'data-toggle' => 'dropdown' }
+ = icon('download')
+ %span.caret
+ %span.sr-only
+ Select Archive Format
+ %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
+ %li.dropdown-header Source code
+ %li
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do
+ %i.fa.fa-download
+ %span Download zip
+ %li
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do
+ %i.fa.fa-download
+ %span Download tar.gz
+ %li
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do
+ %i.fa.fa-download
+ %span Download tar.bz2
+ %li
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow' do
+ %i.fa.fa-download
+ %span Download tar
+
+ - pipeline = project.pipelines.latest_successful_for(ref)
+ - if pipeline
+ - artifacts = pipeline.builds.latest.with_artifacts
+ - if artifacts.any?
+ %li.dropdown-header Artifacts
+ - unless pipeline.latest?
+ - latest_pipeline = project.pipeline_for(ref)
+ %li
+ .unclickable= ci_status_for_statuseable(latest_pipeline)
+ %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
+ %i.fa.fa-download
+ %span Download '#{job.name}'
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index d78888e9fe4..22db33498f1 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -3,11 +3,11 @@
- if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
= link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn has-tooltip' do
= custom_icon('icon_fork')
- Fork
+ %span Fork
- else
= link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has-tooltip' do
= custom_icon('icon_fork')
- Fork
+ %span Fork
%div.count-with-arrow
%span.arrow
= link_to namespace_project_forks_path(@project.namespace, @project), class: "count" do
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 1fdf32466f2..73de8abe55b 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -89,4 +89,4 @@
= icon('repeat')
- elsif build.playable?
= link_to play_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do
- = icon('play')
+ = custom_icon('icon_play')
diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml
index 04cbd0c3591..36fb0300aeb 100644
--- a/app/views/projects/ci/builds/_build_pipeline.html.haml
+++ b/app/views/projects/ci/builds/_build_pipeline.html.haml
@@ -1,14 +1,15 @@
- 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')
- = subject.name
+ %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)
- = subject.name
+ %span.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 b119f6edf14..bb9493f5158 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -66,13 +66,13 @@
- if actions.any?
.btn-group
%a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
- = icon("play")
+ = custom_icon('icon_play')
%b.caret
%ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |build|
%li
= link_to play_namespace_project_build_path(pipeline.project.namespace, pipeline.project, build), method: :post, rel: 'nofollow' do
- = icon("play")
+ = custom_icon('icon_play')
%span= build.name.humanize
- if artifacts.present?
.btn-group
diff --git a/app/views/projects/commit/_ci_stage.html.haml b/app/views/projects/commit/_ci_stage.html.haml
index 9d925cacc0d..6bb900e3fc1 100644
--- a/app/views/projects/commit/_ci_stage.html.haml
+++ b/app/views/projects/commit/_ci_stage.html.haml
@@ -8,8 +8,8 @@
- if stage
&nbsp;
= stage.titleize
- = render statuses.latest.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, allow_retry: true
- = render statuses.retried.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true
+ = render statuses.latest_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, allow_retry: true
+ = render statuses.retried_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true
%tr
%td{colspan: 10}
&nbsp;
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index 29f4ef8f49e..f41a11a056d 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -10,7 +10,10 @@
%th Commit
- pipelines.stages.each do |stage|
%th.stage
- %span.has-tooltip{ title: "#{stage.titleize}" }
+ - if stage.titleize.length > 12
+ %span.has-tooltip{ title: "#{stage.titleize}" }
+ = stage.titleize
+ - else
= stage.titleize
%th
%th
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index fd888f41b1e..389477d0927 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -7,7 +7,7 @@
- cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count]
- cache_key.push(commit.status) if commit.status
-= cache(cache_key) do
+= cache(cache_key, expires_in: 1.day) do
%li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
= author_avatar(commit, size: 36)
diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml
index 61152649907..4d1ee1c5318 100644
--- a/app/views/projects/commits/_head.html.haml
+++ b/app/views/projects/commits/_head.html.haml
@@ -1,8 +1,5 @@
.scrolling-tabs-container.sub-nav-scroll
- .fade-left
- = icon('angle-left')
- .fade-right
- = icon('angle-right')
+ = render 'shared/nav_scroll'
.nav-links.sub-nav.scrolling-tabs
%ul{ class: (container_class) }
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml
index f7bf3b834ef..16d134eb6b6 100644
--- a/app/views/projects/deployments/_actions.haml
+++ b/app/views/projects/deployments/_actions.haml
@@ -5,13 +5,13 @@
.inline
.dropdown
%a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
- = icon("play")
+ = custom_icon('icon_play')
%b.caret
%ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |action|
%li
= link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do
- = icon("play")
+ = custom_icon('icon_play')
%span= action.name.humanize
- if local_assigns.fetch(:allow_rollback, false)
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index b282aa52b25..f6d751a343e 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -44,42 +44,56 @@
%hr
%fieldset.features.append-bottom-0
%h5.prepend-top-0
- Features
- .form-group
- .checkbox
- = f.label :issues_enabled do
- = f.check_box :issues_enabled
- %strong Issues
- %br
- %span.descr Lightweight issue tracking system for this project
- .form-group
- .checkbox
- = f.label :merge_requests_enabled do
- = f.check_box :merge_requests_enabled
- %strong Merge Requests
- %br
- %span.descr Submit changes to be merged upstream
- .form-group
- .checkbox
- = f.label :builds_enabled do
- = f.check_box :builds_enabled
- %strong Builds
- %br
- %span.descr Test and deploy your changes before merge
- .form-group
- .checkbox
- = f.label :wiki_enabled do
- = f.check_box :wiki_enabled
- %strong Wiki
- %br
- %span.descr Pages for project documentation
- .form-group
- .checkbox
- = f.label :snippets_enabled do
- = f.check_box :snippets_enabled
- %strong Snippets
- %br
- %span.descr Share code pastes with others out of git repository
+ Feature Visibility
+
+ = f.fields_for :project_feature do |feature_fields|
+ .form_group.prepend-top-20
+ .row
+ .col-md-9
+ = feature_fields.label :issues_access_level, "Issues", class: 'label-light'
+ %span.help-block Lightweight issue tracking system for this project
+ .col-md-3
+ = project_feature_access_select(:issues_access_level)
+
+ .row
+ .col-md-9
+ = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
+ %span.help-block Submit changes to be merged upstream
+ .col-md-3
+ = project_feature_access_select(:merge_requests_access_level)
+
+ .row
+ .col-md-9
+ = feature_fields.label :builds_access_level, "Builds", class: 'label-light'
+ %span.help-block Submit Test and deploy your changes before merge
+ .col-md-3
+ = project_feature_access_select(:builds_access_level)
+
+ .row
+ .col-md-9
+ = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light'
+ %span.help-block Pages for project documentation
+ .col-md-3
+ = project_feature_access_select(:wiki_access_level)
+
+ .row
+ .col-md-9
+ = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
+ %span.help-block Share code pastes with others out of Git repository
+ .col-md-3
+ = 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')
+
- if Gitlab.config.registry.enabled
.form-group
.checkbox
@@ -88,7 +102,7 @@
%strong Container Registry
%br
%span.descr Enable Container Registry for this repository
- %hr
+
= render 'merge_request_settings', f: f
%hr
%fieldset.features.append-bottom-default
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index a1d79bdabda..bacc5708e4b 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -32,11 +32,11 @@
- if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
= link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-new' do
= custom_icon('icon_fork')
- Fork
+ %span Fork
- else
= link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn btn-new' do
= custom_icon('icon_fork')
- Fork
+ %span Fork
= render 'projects', projects: @forks
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 584c0fa18ae..576d0bec51b 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,9 +1,10 @@
%li.build
+ .curve
.build-content
- if subject.target_url
- link_to subject.target_url do
= render_status_with_link('commit status', subject.status)
- = subject.name
+ %span.ci-status-text= subject.name
- else
= render_status_with_link('commit status', subject.status)
- = subject.name
+ %span.ci-status-text= subject.name
diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml
index 45e51389c00..082e2cb4d8c 100644
--- a/app/views/projects/graphs/_head.html.haml
+++ b/app/views/projects/graphs/_head.html.haml
@@ -1,16 +1,18 @@
-.nav-links.sub-nav
- %ul{ class: (container_class) }
+.scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
- - content_for :page_specific_javascripts do
- = page_specific_javascript_tag('lib/chart.js')
- = page_specific_javascript_tag('graphs/graphs_bundle.js')
- = nav_link(action: :show) do
- = link_to 'Contributors', namespace_project_graph_path
- = nav_link(action: :commits) do
- = link_to 'Commits', commits_namespace_project_graph_path
- = nav_link(action: :languages) do
- = link_to 'Languages', languages_namespace_project_graph_path
- - if @project.builds_enabled?
- = nav_link(action: :ci) do
- = link_to ci_namespace_project_graph_path do
- Continuous Integration
+ - content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/chart.js')
+ = page_specific_javascript_tag('graphs/graphs_bundle.js')
+ = nav_link(action: :show) do
+ = link_to 'Contributors', namespace_project_graph_path
+ = nav_link(action: :commits) do
+ = link_to 'Commits', commits_namespace_project_graph_path
+ = nav_link(action: :languages) do
+ = link_to 'Languages', languages_namespace_project_graph_path
+ - if @project.feature_available?(:builds, current_user)
+ = nav_link(action: :ci) do
+ = link_to ci_namespace_project_graph_path do
+ Continuous Integration
diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml
index b6cb559afcb..f88b33018d0 100644
--- a/app/views/projects/issues/_head.html.haml
+++ b/app/views/projects/issues/_head.html.haml
@@ -1,30 +1,32 @@
-.nav-links.sub-nav
- %ul{ class: (container_class) }
- - if project_nav_tab?(:issues) && !current_controller?(:merge_requests)
- = nav_link(controller: :issues) do
- = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do
- %span
- Issues
+.scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ - if project_nav_tab?(:issues) && !current_controller?(:merge_requests)
+ = nav_link(controller: :issues) do
+ = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do
+ %span
+ Issues
- = nav_link(controller: :boards) do
- = link_to namespace_project_board_path(@project.namespace, @project), title: 'Board' do
- %span
- Board
+ = nav_link(controller: :boards) do
+ = link_to namespace_project_board_path(@project.namespace, @project), title: 'Board' do
+ %span
+ Board
- - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests)
- = nav_link(controller: :merge_requests) do
- = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do
- %span
- Merge Requests
+ - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests)
+ = nav_link(controller: :merge_requests) do
+ = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do
+ %span
+ Merge Requests
- - if project_nav_tab? :labels
- = nav_link(controller: :labels) do
- = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do
- %span
- Labels
+ - if project_nav_tab? :labels
+ = nav_link(controller: :labels) do
+ = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do
+ %span
+ Labels
- - if project_nav_tab? :milestones
- = nav_link(controller: :milestones) do
- = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do
- %span
- Milestones
+ - if project_nav_tab? :milestones
+ = nav_link(controller: :milestones) do
+ = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do
+ %span
+ Milestones \ No newline at end of file
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index 24749699c6d..33556a1a2b3 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -1,13 +1,12 @@
- if can?(current_user, :push_code, @project)
.pull-right
#new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)}
+ = link_to '#', class: 'checking btn btn-grouped', disabled: 'disabled' do
+ = icon('spinner spin')
+ Checking branches
= link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid),
- method: :post, class: 'btn btn-new btn-inverted has-tooltip', title: @issue.to_branch_name, disabled: 'disabled' do
- .checking
- = icon('spinner spin')
- Checking branches
- .available.hide
- New branch
- .unavailable.hide
- = icon('exclamation-triangle')
- New branch unavailable
+ method: :post, class: 'btn btn-new btn-inverted btn-grouped has-tooltip available hide', title: @issue.to_branch_name do
+ New branch
+ = link_to '#', class: 'unavailable btn btn-grouped hide', disabled: 'disabled' do
+ = icon('exclamation-triangle')
+ New branch unavailable
diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml
index 6ea9f612d13..a8eeab3e55e 100644
--- a/app/views/projects/issues/_related_branches.html.haml
+++ b/app/views/projects/issues/_related_branches.html.haml
@@ -5,7 +5,7 @@
- @related_branches.each do |branch|
%li
- target = @project.repository.find_branch(branch).target
- - pipeline = @project.pipeline(target.sha, branch) if target
+ - pipeline = @project.pipeline_for(branch, target.sha) if target
- if pipeline
%span.related-branch-ci-status
= render_pipeline_status(pipeline)
diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml
index 013b05628fa..99c71e1454a 100644
--- a/app/views/projects/merge_requests/show/_diffs.html.haml
+++ b/app/views/projects/merge_requests/show/_diffs.html.haml
@@ -1,4 +1,5 @@
- if @merge_request_diff.collected?
+ = render 'projects/merge_requests/show/versions'
= render "projects/diffs/diffs", diffs: @diffs
- elsif @merge_request_diff.empty?
.nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch}
diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml
index 098ce19da21..e35291dff7d 100644
--- a/app/views/projects/merge_requests/show/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_title.html.haml
@@ -1,3 +1,7 @@
+- if @merge_request.closed_without_fork?
+ .alert.alert-danger
+ %p The source project of this merge request has been removed.
+
.clearfix.detail-page-header
.issuable-header
.issuable-status-box.status-box{ class: status_box_class(@merge_request) }
diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml
new file mode 100644
index 00000000000..2da70ce7137
--- /dev/null
+++ b/app/views/projects/merge_requests/show/_versions.html.haml
@@ -0,0 +1,31 @@
+- merge_request_diffs = @merge_request.merge_request_diffs.select_without_diff
+
+- if merge_request_diffs.size > 1
+ .mr-version-switch
+ Version:
+ %span.dropdown.inline
+ %a.btn-link.dropdown-toggle{ data: {toggle: :dropdown} }
+ %strong.monospace<
+ - if @merge_request_diff.latest?
+ #{"latest"}
+ - else
+ #{@merge_request_diff.head_commit.short_id}
+ %span.caret
+ %ul.dropdown-menu.dropdown-menu-selectable
+ - merge_request_diffs.each do |merge_request_diff|
+ %li
+ = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, diff_id: merge_request_diff.id), class: ('is-active' if merge_request_diff == @merge_request_diff) do
+ %strong.monospace
+ #{merge_request_diff.head_commit.short_id}
+ %br
+ %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)
+
+ - unless @merge_request_diff.latest?
+ %span.prepend-left-default
+ = icon('info-circle')
+ This version is not the latest one. Comments are disabled
+ .pull-right
+ %span.monospace
+ #{@merge_request_diff.base_commit.short_id}..#{@merge_request_diff.head_commit.short_id}
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index ea4898f2107..fda0592dd41 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -55,16 +55,11 @@
= render 'bitbucket_import_modal'
%div
- if gitlab_import_enabled?
- = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless bitbucket_import_configured?}" do
+ = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
= icon('gitlab', text: 'GitLab.com')
- unless gitlab_import_configured?
= render 'gitlab_import_modal'
%div
- - if gitorious_import_enabled?
- = link_to new_import_gitorious_path, class: 'btn import_gitorious' do
- %i.icon-gitorious.icon-gitorious-small
- Gitorious.org
- %div
- if google_code_import_enabled?
= link_to new_import_google_code_path, class: 'btn import_google_code' do
= icon('google', text: 'Google Code')
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index d2ac1ce2b9a..7c82177f9ea 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -52,11 +52,11 @@
- if note.emoji_awardable?
= link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do
= icon('spinner spin')
- = icon('smile-o')
+ = icon('smile-o', class: 'link-highlight')
- if note_editable
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
- = icon('pencil')
+ = icon('pencil', class: 'link-highlight')
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do
= icon('trash-o')
.note-body{class: note_editable ? 'js-task-list-container' : ''}
diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml
index d65faf86d4e..f611ddc8f5f 100644
--- a/app/views/projects/pipelines/_head.html.haml
+++ b/app/views/projects/pipelines/_head.html.haml
@@ -1,19 +1,21 @@
-.nav-links.sub-nav
- %ul{ class: (container_class) }
- - if project_nav_tab? :pipelines
- = nav_link(controller: :pipelines) do
- = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
- %span
- Pipelines
+.scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ - if project_nav_tab? :pipelines
+ = nav_link(controller: :pipelines) do
+ = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
+ %span
+ Pipelines
- - if project_nav_tab? :builds
- = nav_link(controller: %w(builds)) do
- = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
- %span
- Builds
+ - if project_nav_tab? :builds
+ = nav_link(controller: %w(builds)) do
+ = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
+ %span
+ Builds
- - if project_nav_tab? :environments
- = nav_link(controller: %w(environments)) do
- = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
- %span
- Environments
+ - if project_nav_tab? :environments
+ = nav_link(controller: %w(environments)) do
+ = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
+ %span
+ Environments
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 5f466bdbac2..4d957e0d890 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -49,7 +49,10 @@
%th Commit
- stages.each do |stage|
%th.stage
- %span.has-tooltip{ title: "#{stage.titleize}" }
+ - if stage.titleize.length > 12
+ %span.has-tooltip{ title: "#{stage.titleize}" }
+ = stage.titleize
+ - else
= stage.titleize
%th
%th
diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml
index 833954bc039..37e55dc72a3 100644
--- a/app/views/projects/project_members/update.js.haml
+++ b/app/views/projects/project_members/update.js.haml
@@ -1,3 +1,3 @@
:plain
$("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @project_member))}');
- new MemberExpirationDate();
+ new gl.MemberExpirationDate();
diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml
deleted file mode 100644
index 24658319060..00000000000
--- a/app/views/projects/repositories/_download_archive.html.haml
+++ /dev/null
@@ -1,37 +0,0 @@
-- ref = ref || nil
-- btn_class = btn_class || ''
-- split_button = split_button || false
-- if split_button == true
- %span.btn-group{class: btn_class}
- = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn col-xs-10', rel: 'nofollow' do
- %i.fa.fa-download
- %span Download zip
- %a.col-xs-2.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' }
- %span.caret
- %span.sr-only
- Select Archive Format
- %ul.col-xs-10.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
- %li
- = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), rel: 'nofollow' do
- %i.fa.fa-download
- %span Download zip
- %li
- = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do
- %i.fa.fa-download
- %span Download tar.gz
- %li
- = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do
- %i.fa.fa-download
- %span Download tar.bz2
- %li
- = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar'), rel: 'nofollow' do
- %i.fa.fa-download
- %span Download tar
-- else
- %span.btn-group{class: btn_class}
- = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn', rel: 'nofollow' do
- %i.fa.fa-download
- %span zip
- = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.gz'), class: 'btn', rel: 'nofollow' do
- %i.fa.fa-download
- %span tar.gz
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 340e159c874..9adce776c1c 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -72,7 +72,7 @@
= render "projects/buttons/koding"
.btn-group.project-repo-btn-group
- = render "projects/buttons/download"
+ = render 'projects/buttons/download', project: @project, ref: @ref
= render 'projects/buttons/dropdown'
= render 'shared/notifications/button', notification_setting: @notification_setting
diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml
deleted file mode 100644
index 8a11dbfa9f4..00000000000
--- a/app/views/projects/tags/_download.html.haml
+++ /dev/null
@@ -1,14 +0,0 @@
-%span.btn-group
- = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), class: 'btn btn-default', rel: 'nofollow' do
- %span Source code
- %a.btn.btn-default.dropdown-toggle{ 'data-toggle' => 'dropdown' }
- %span.caret
- %span.sr-only
- Select Archive Format
- %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
- %li
- = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do
- %span Download zip
- %li
- = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do
- %span Download tar.gz
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 2c11c0e5b21..a156d98bab8 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -11,8 +11,7 @@
= strip_gpg_signature(tag.message)
.controls
- - if can?(current_user, :download_code, @project)
- = render 'projects/tags/download', ref: tag.name, project: @project
+ = render 'projects/buttons/download', project: @project, ref: tag.name
- if can?(current_user, :push_code, @project)
= link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index 368231e73fe..6adbe9351dc 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -8,21 +8,24 @@
Tags give the ability to mark specific points in history as being important
.nav-controls
- - if can? current_user, :push_code, @project
- = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do
- New tag
+ = form_tag(filter_tags_path, method: :get) do
+ = search_field_tag :search, params[:search], { placeholder: 'Filter by tag name', id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false }
.dropdown.inline
%button.dropdown-toggle.btn{ type: 'button', data: { toggle: 'dropdown'} }
- %span.light= @sort.humanize
+ %span.light
+ = @sort.humanize
%b.caret
%ul.dropdown-menu.dropdown-menu-align-right
%li
- = link_to namespace_project_tags_path(sort: nil) do
+ = link_to filter_tags_path(sort: nil) do
Name
- = link_to namespace_project_tags_path(sort: sort_value_recently_updated) do
+ = link_to filter_tags_path(sort: sort_value_recently_updated) do
= sort_title_recently_updated
- = link_to namespace_project_tags_path(sort: sort_value_oldest_updated) do
+ = link_to filter_tags_path(sort: sort_value_oldest_updated) do
= sort_title_oldest_updated
+ - if can?(current_user, :push_code, @project)
+ = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do
+ New tag
.tags
- if @tags.any?
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index 395d7af6cbb..4dd7439b2d0 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -12,8 +12,7 @@
= icon('files-o')
= link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Browse commits' do
= icon('history')
- - if can? current_user, :download_code, @project
- = render 'projects/tags/download', ref: @tag.name, project: @project
+ = render 'projects/buttons/download', project: @project, ref: @tag.name
- if can?(current_user, :admin_project, @project)
.pull-right
= link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
index 558e6146ae9..ca5d2d7722a 100644
--- a/app/views/projects/tree/_tree_content.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -5,16 +5,17 @@
%tr
%th Name
%th Last Update
- %th.hidden-xs
- .pull-left Last Commit
- .last-commit.hidden-sm.pull-left
- &nbsp;
+ %th.hidden-xs.last-commit
+ Last Commit
+ .last-commit-content.hidden-sm
%i.fa.fa-angle-right
&nbsp;
%small.light
= link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
&ndash;
- = truncate(@commit.title, length: 50)
+ = time_ago_with_tooltip(@commit.committed_date)
+ &ndash;
+ = @commit.full_title
= link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'pull-right'
- if @path.present?
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index bf5360b4dee..37d341212af 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -10,8 +10,7 @@
%div{ class: container_class }
.tree-controls
= render 'projects/find_file_link'
- - if can? current_user, :download_code, @project
- = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true
+ = render 'projects/buttons/download', project: @project, ref: @ref
#tree-holder.tree-holder.clearfix
.nav-block
diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml
index f8ea479e0b1..551a20c1044 100644
--- a/app/views/projects/wikis/_nav.html.haml
+++ b/app/views/projects/wikis/_nav.html.haml
@@ -1,13 +1,15 @@
-.nav-links.sub-nav
- %ul{ class: (container_class) }
- = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
- = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
+.scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
+ = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
- = nav_link(path: 'wikis#pages') do
- = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project)
+ = nav_link(path: 'wikis#pages') do
+ = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project)
- = nav_link(path: 'wikis#git_access') do
- = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do
- Git Access
+ = nav_link(path: 'wikis#git_access') do
+ = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do
+ Git Access
- = render 'projects/wikis/new'
+ = render 'projects/wikis/new'
diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg
index b07f1c5603e..9b67422da2c 100644
--- a/app/views/shared/_logo.svg
+++ b/app/views/shared/_logo.svg
@@ -1,9 +1,9 @@
-<svg width="36" height="36" id="tanuki-logo">
- <path id="tanuki-right-ear" class="tanuki-shape" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/>
- <path id="tanuki-left-ear" class="tanuki-shape" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/>
- <path id="tanuki-nose" class="tanuki-shape" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/>
- <path id="tanuki-right-eye" class="tanuki-shape" fill="#fc6d26" d="M18,34.38 11.38,14 2,14 6,25Z"/>
- <path id="tanuki-left-eye" class="tanuki-shape" fill="#fc6d26" d="M18,34.38 24.62,14 34,14 30,25Z"/>
- <path id="tanuki-right-cheek" class="tanuki-shape" fill="#fca326" d="M2 14L.1 20.16c-.18.565 0 1.2.5 1.56l17.42 12.66z"/>
- <path id="tanuki-left-cheek" class="tanuki-shape" fill="#fca326" d="M34 14l1.9 6.16c.18.565 0 1.2-.5 1.56L18 34.38z"/>
+<svg width="36" height="36" class="tanuki-logo">
+ <path class="tanuki-shape tanuki-left-ear" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/>
+ <path class="tanuki-shape tanuki-right-ear" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/>
+ <path class="tanuki-shape tanuki-nose" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/>
+ <path class="tanuki-shape tanuki-left-eye" fill="#fc6d26" d="M18,34.38 11.38,14 2,14 6,25Z"/>
+ <path class="tanuki-shape tanuki-right-eye" fill="#fc6d26" d="M18,34.38 24.62,14 34,14 30,25Z"/>
+ <path class="tanuki-shape tanuki-left-cheek" fill="#fca326" d="M2 14L.1 20.16c-.18.565 0 1.2.5 1.56l17.42 12.66z"/>
+ <path class="tanuki-shape tanuki-right-cheek" fill="#fca326" d="M34 14l1.9 6.16c.18.565 0 1.2-.5 1.56L18 34.38z"/>
</svg>
diff --git a/app/views/shared/_nav_scroll.html.haml b/app/views/shared/_nav_scroll.html.haml
new file mode 100644
index 00000000000..4e3b1b3a571
--- /dev/null
+++ b/app/views/shared/_nav_scroll.html.haml
@@ -0,0 +1,4 @@
+.fade-left
+ = icon('angle-left')
+.fade-right
+ = icon('angle-right') \ No newline at end of file
diff --git a/app/views/shared/icons/_icon_play.svg b/app/views/shared/icons/_icon_play.svg
index 80a6d41dbf6..e965afa9a56 100644
--- a/app/views/shared/icons/_icon_play.svg
+++ b/app/views/shared/icons/_icon_play.svg
@@ -1 +1,3 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 11"><path fill-rule="evenodd" d="m9.283 6.47l-7.564 4.254c-.949.534-1.719.266-1.719-.576v-9.292c0-.852.756-1.117 1.719-.576l7.564 4.254c.949.534.963 1.392 0 1.934"/></svg> \ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 11" class="icon-play">
+ <path fill-rule="evenodd" d="m9.283 6.47l-7.564 4.254c-.949.534-1.719.266-1.719-.576v-9.292c0-.852.756-1.117 1.719-.576l7.564 4.254c.949.534.963 1.392 0 1.934"/>
+ </svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_icon_status_created.svg b/app/views/shared/icons/_icon_status_created.svg
new file mode 100644
index 00000000000..1f5c3b51b03
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_created.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" enable-background="new 0 0 14 14"><path d="M12.5,7 C12.5,4 10,1.5 7,1.5 C4,1.5 1.5,4 1.5,7 C1.5,10 4,12.5 7,12.5 C10,12.5 12.5,10 12.5,7 L12.5,7 Z M0,7 C0,3.1 3.1,0 7,0 C10.9,0 14,3.1 14,7 C14,10.9 10.9,14 7,14 C3.1,14 0,10.9 0,7 L0,7 Z" /><circle cx="7" cy="7" r="3.25"/></svg>
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 4f8ea7e7cef..0f4f744a71f 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -27,15 +27,18 @@
= render "shared/issuable/label_dropdown"
.pull-right
- - if controller.controller_name == 'boards' && can?(current_user, :admin_list, @project)
- .dropdown
- %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, project_id: @project.try(:id) } }
- Create new list
- .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
- = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Create a new list" }
- - if can?(current_user, :admin_label, @project)
- = render partial: "shared/issuable/label_page_create"
- = dropdown_loading
+ - if controller.controller_name == 'boards'
+ #js-boards-seach.issue-boards-search
+ %input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" }
+ - if can?(current_user, :admin_list, @project)
+ .dropdown.pull-right
+ %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, project_id: @project.try(:id) } }
+ Create new list
+ .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
+ = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Create a new list" }
+ - if can?(current_user, :admin_label, @project)
+ = render partial: "shared/issuable/label_page_create"
+ = dropdown_loading
- else
= render 'shared/sort_dropdown'
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 22594b46443..3856a4917b4 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -134,7 +134,7 @@
title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' }
= icon('question-circle')
-- if issuable.is_a?(MergeRequest)
+- if issuable.is_a?(MergeRequest) && !issuable.closed_without_fork?
%hr
- if @merge_request.new_record?
.form-group
@@ -175,7 +175,7 @@
= link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel'
- else
.pull-right
- - if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project)
+ - if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project)
= link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" },
method: :delete, class: 'btn btn-danger btn-grouped'
= link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel'
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index d34d28f6736..24a1a616919 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[]", label, id: nil
+ = hidden_field_tag "label_name[]", u(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/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index c1b50e65af5..b13daaf43c9 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -118,7 +118,7 @@
= icon('spinner spin', class: 'block-loading')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
- .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels_array.any?) }
+ .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels_array.any?) }
- if issuable.labels_array.any?
- issuable.labels_array.each do |label|
= link_to_label(label, type: issuable.to_ability_name)
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index c7f39868e71..9a052abe40a 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -123,6 +123,6 @@
:javascript
var userProfile;
- userProfile = new User({
+ userProfile = new gl.User({
action: "#{controller.action_name}"
});
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 7a9376def02..4a01b9e40fb 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -212,7 +212,7 @@ Settings.gitlab.default_projects_features['builds'] = true if Settin
Settings.gitlab.default_projects_features['container_registry'] = true if Settings.gitlab.default_projects_features['container_registry'].nil?
Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE)
Settings.gitlab['domain_whitelist'] ||= []
-Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project]
+Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab google_code fogbugz git gitlab_project]
Settings.gitlab['trusted_proxies'] ||= []
#
diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb
index 74fef7cadfe..5892c1de024 100644
--- a/config/initializers/sentry.rb
+++ b/config/initializers/sentry.rb
@@ -18,6 +18,7 @@ if Rails.env.production?
# Sanitize fields based on those sanitized from Rails.
config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s)
+ config.tags = { program: Gitlab::Sentry.program_context }
end
end
end
diff --git a/config/routes.rb b/config/routes.rb
index e93b640fbc0..262a174437a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -157,12 +157,6 @@ Rails.application.routes.draw do
get :jobs
end
- resource :gitorious, only: [:create, :new], controller: :gitorious do
- get :status
- get :callback
- get :jobs
- end
-
resource :google_code, only: [:create, :new], controller: :google_code do
get :status
post :callback
@@ -788,6 +782,14 @@ Rails.application.routes.draw do
resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
collection do
post :cancel_all
+
+ resources :artifacts, only: [] do
+ collection do
+ get :latest_succeeded,
+ path: '*ref_name_and_path',
+ format: false
+ end
+ end
end
member do
diff --git a/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb b/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb
new file mode 100644
index 00000000000..c8cbd2718ff
--- /dev/null
+++ b/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb
@@ -0,0 +1,21 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MergeRequestDiffRemoveUniq < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ if index_exists?(:merge_request_diffs, :merge_request_id)
+ remove_index :merge_request_diffs, :merge_request_id
+ end
+ end
+
+ def down
+ unless index_exists?(:merge_request_diffs, :merge_request_id)
+ add_concurrent_index :merge_request_diffs, :merge_request_id, unique: true
+ end
+ end
+end
diff --git a/db/migrate/20160725104452_merge_request_diff_add_index.rb b/db/migrate/20160725104452_merge_request_diff_add_index.rb
new file mode 100644
index 00000000000..6d04242dd25
--- /dev/null
+++ b/db/migrate/20160725104452_merge_request_diff_add_index.rb
@@ -0,0 +1,17 @@
+class MergeRequestDiffAddIndex < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ add_concurrent_index :merge_request_diffs, :merge_request_id
+ end
+
+ def down
+ if index_exists?(:merge_request_diffs, :merge_request_id)
+ remove_index :merge_request_diffs, :merge_request_id
+ end
+ end
+end
diff --git a/db/migrate/20160823213309_add_lfs_enabled_to_projects.rb b/db/migrate/20160823213309_add_lfs_enabled_to_projects.rb
new file mode 100644
index 00000000000..c169084e976
--- /dev/null
+++ b/db/migrate/20160823213309_add_lfs_enabled_to_projects.rb
@@ -0,0 +1,29 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddLfsEnabledToProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # 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 = ''
+
+ # 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
+ add_column :projects, :lfs_enabled, :boolean
+ end
+end
diff --git a/db/migrate/20160824103857_drop_unused_ci_tables.rb b/db/migrate/20160824103857_drop_unused_ci_tables.rb
new file mode 100644
index 00000000000..65cf46308d9
--- /dev/null
+++ b/db/migrate/20160824103857_drop_unused_ci_tables.rb
@@ -0,0 +1,11 @@
+class DropUnusedCiTables < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ drop_table(:ci_services)
+ drop_table(:ci_web_hooks)
+ end
+end
diff --git a/db/migrate/20160827011312_ensure_lock_version_has_no_default.rb b/db/migrate/20160827011312_ensure_lock_version_has_no_default.rb
new file mode 100644
index 00000000000..7c55bc23cf2
--- /dev/null
+++ b/db/migrate/20160827011312_ensure_lock_version_has_no_default.rb
@@ -0,0 +1,16 @@
+class EnsureLockVersionHasNoDefault < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ change_column_default :issues, :lock_version, nil
+ change_column_default :merge_requests, :lock_version, nil
+
+ execute('UPDATE issues SET lock_version = 1 WHERE lock_version = 0')
+ execute('UPDATE merge_requests SET lock_version = 1 WHERE lock_version = 0')
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20160830232601_change_lock_version_not_null.rb b/db/migrate/20160830232601_change_lock_version_not_null.rb
new file mode 100644
index 00000000000..01c58ed5bdc
--- /dev/null
+++ b/db/migrate/20160830232601_change_lock_version_not_null.rb
@@ -0,0 +1,13 @@
+class ChangeLockVersionNotNull < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ change_column_null :issues, :lock_version, true
+ change_column_null :merge_requests, :lock_version, true
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20160831214002_create_project_features.rb b/db/migrate/20160831214002_create_project_features.rb
new file mode 100644
index 00000000000..2d76a015a08
--- /dev/null
+++ b/db/migrate/20160831214002_create_project_features.rb
@@ -0,0 +1,16 @@
+class CreateProjectFeatures < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ create_table :project_features do |t|
+ t.belongs_to :project, index: true
+ t.integer :merge_requests_access_level
+ t.integer :issues_access_level
+ t.integer :wiki_access_level
+ t.integer :snippets_access_level
+ t.integer :builds_access_level
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20160831214543_migrate_project_features.rb b/db/migrate/20160831214543_migrate_project_features.rb
new file mode 100644
index 00000000000..93f9821bc76
--- /dev/null
+++ b/db/migrate/20160831214543_migrate_project_features.rb
@@ -0,0 +1,44 @@
+class MigrateProjectFeatures < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON =
+ <<-EOT
+ Migrating issues_enabled, merge_requests_enabled, wiki_enabled, builds_enabled, snippets_enabled fields from projects to
+ a new table called project_features.
+ EOT
+
+ def up
+ sql =
+ %Q{
+ INSERT INTO project_features(project_id, issues_access_level, merge_requests_access_level, wiki_access_level,
+ builds_access_level, snippets_access_level, created_at, updated_at)
+ SELECT
+ id AS project_id,
+ CASE WHEN issues_enabled IS true THEN 20 ELSE 0 END AS issues_access_level,
+ CASE WHEN merge_requests_enabled IS true THEN 20 ELSE 0 END AS merge_requests_access_level,
+ CASE WHEN wiki_enabled IS true THEN 20 ELSE 0 END AS wiki_access_level,
+ CASE WHEN builds_enabled IS true THEN 20 ELSE 0 END AS builds_access_level,
+ CASE WHEN snippets_enabled IS true THEN 20 ELSE 0 END AS snippets_access_level,
+ created_at,
+ updated_at
+ FROM projects
+ }
+
+ execute(sql)
+ end
+
+ def down
+ sql = %Q{
+ UPDATE projects
+ SET
+ issues_enabled = COALESCE((SELECT CASE WHEN issues_access_level = 20 THEN true ELSE false END AS issues_enabled FROM project_features WHERE project_features.project_id = projects.id), true),
+ merge_requests_enabled = COALESCE((SELECT CASE WHEN merge_requests_access_level = 20 THEN true ELSE false END AS merge_requests_enabled FROM project_features WHERE project_features.project_id = projects.id),true),
+ wiki_enabled = COALESCE((SELECT CASE WHEN wiki_access_level = 20 THEN true ELSE false END AS wiki_enabled FROM project_features WHERE project_features.project_id = projects.id), true),
+ builds_enabled = COALESCE((SELECT CASE WHEN builds_access_level = 20 THEN true ELSE false END AS builds_enabled FROM project_features WHERE project_features.project_id = projects.id), true),
+ snippets_enabled = COALESCE((SELECT CASE WHEN snippets_access_level = 20 THEN true ELSE false END AS snippets_enabled FROM project_features WHERE project_features.project_id = projects.id),true)
+ }
+
+ execute(sql)
+ end
+end
diff --git a/db/migrate/20160831223750_remove_features_enabled_from_projects.rb b/db/migrate/20160831223750_remove_features_enabled_from_projects.rb
new file mode 100644
index 00000000000..a2c207b49ea
--- /dev/null
+++ b/db/migrate/20160831223750_remove_features_enabled_from_projects.rb
@@ -0,0 +1,29 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveFeaturesEnabledFromProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = true
+ DOWNTIME_REASON = "Removing fields from database requires downtine."
+
+ def up
+ remove_column :projects, :issues_enabled
+ remove_column :projects, :merge_requests_enabled
+ remove_column :projects, :builds_enabled
+ remove_column :projects, :wiki_enabled
+ remove_column :projects, :snippets_enabled
+ end
+
+ # Ugly SQL but the only way i found to make it work on both Postgres and Mysql
+ # It will be slow but it is ok since it is a revert method
+ def down
+ add_column_with_default(:projects, :issues_enabled, :boolean, default: true, allow_null: false)
+ add_column_with_default(:projects, :merge_requests_enabled, :boolean, default: true, allow_null: false)
+ add_column_with_default(:projects, :builds_enabled, :boolean, default: true, allow_null: false)
+ add_column_with_default(:projects, :wiki_enabled, :boolean, default: true, allow_null: false)
+ add_column_with_default(:projects, :snippets_enabled, :boolean, default: true, allow_null: false)
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 802c928b2fc..af6e74a4e25 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: 20160823081327) do
+ActiveRecord::Schema.define(version: 20160831223750) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -295,16 +295,6 @@ ActiveRecord::Schema.define(version: 20160823081327) do
add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree
add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree
- create_table "ci_services", force: :cascade do |t|
- t.string "type"
- t.string "title"
- t.integer "project_id", null: false
- t.datetime "created_at"
- t.datetime "updated_at"
- t.boolean "active", default: false, null: false
- t.text "properties"
- end
-
create_table "ci_sessions", force: :cascade do |t|
t.string "session_id", null: false
t.text "data"
@@ -360,13 +350,6 @@ ActiveRecord::Schema.define(version: 20160823081327) do
add_index "ci_variables", ["gl_project_id"], name: "index_ci_variables_on_gl_project_id", using: :btree
- create_table "ci_web_hooks", force: :cascade do |t|
- t.string "url", null: false
- t.integer "project_id", null: false
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
create_table "deploy_keys_projects", force: :cascade do |t|
t.integer "deploy_key_id", null: false
t.integer "project_id", null: false
@@ -593,7 +576,7 @@ ActiveRecord::Schema.define(version: 20160823081327) do
t.string "start_commit_sha"
end
- add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", unique: true, using: :btree
+ add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", using: :btree
create_table "merge_requests", force: :cascade do |t|
t.string "target_branch", null: false
@@ -783,6 +766,19 @@ ActiveRecord::Schema.define(version: 20160823081327) do
add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree
+ create_table "project_features", force: :cascade do |t|
+ t.integer "project_id"
+ t.integer "merge_requests_access_level"
+ t.integer "issues_access_level"
+ t.integer "wiki_access_level"
+ t.integer "snippets_access_level"
+ t.integer "builds_access_level"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", using: :btree
+
create_table "project_group_links", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "group_id", null: false
@@ -807,11 +803,7 @@ ActiveRecord::Schema.define(version: 20160823081327) do
t.datetime "created_at"
t.datetime "updated_at"
t.integer "creator_id"
- t.boolean "issues_enabled", default: true, null: false
- t.boolean "merge_requests_enabled", default: true, null: false
- t.boolean "wiki_enabled", default: true, null: false
t.integer "namespace_id"
- t.boolean "snippets_enabled", default: true, null: false
t.datetime "last_activity_at"
t.string "import_url"
t.integer "visibility_level", default: 0, null: false
@@ -825,7 +817,6 @@ ActiveRecord::Schema.define(version: 20160823081327) do
t.integer "commit_count", default: 0
t.text "import_error"
t.integer "ci_id"
- t.boolean "builds_enabled", default: true, null: false
t.boolean "shared_runners_enabled", default: true, null: false
t.string "runners_token"
t.string "build_coverage_regex"
@@ -842,6 +833,7 @@ ActiveRecord::Schema.define(version: 20160823081327) do
t.string "repository_storage", default: "default", null: false
t.boolean "request_access_enabled", default: true, null: false
t.boolean "has_external_wiki"
+ t.boolean "lfs_enabled"
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
diff --git a/doc/README.md b/doc/README.md
index 047035dfb09..254394eb63e 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -19,7 +19,6 @@
- [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects.
- [Webhooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project.
- [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN.
-- [Koding](user/project/koding.md) Learn how to use Koding, the online IDE.
## Administrator documentation
diff --git a/doc/api/broadcast_messages.md b/doc/api/broadcast_messages.md
new file mode 100644
index 00000000000..c3a9207a3ae
--- /dev/null
+++ b/doc/api/broadcast_messages.md
@@ -0,0 +1,158 @@
+# Broadcast Messages
+
+> **Note:** This feature was introduced in GitLab 8.12.
+
+The broadcast message API is only accessible to administrators. All requests by
+guests will respond with `401 Unauthorized`, and all requests by normal users
+will respond with `403 Forbidden`.
+
+## Get all broadcast messages
+
+```
+GET /broadcast_messages
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages
+```
+
+Example response:
+
+```json
+[
+ {
+ "message":"Example broadcast message",
+ "starts_at":"2016-08-24T23:21:16.078Z",
+ "ends_at":"2016-08-26T23:21:16.080Z",
+ "color":"#E75E40",
+ "font":"#FFFFFF",
+ "id":1,
+ "active": false
+ }
+]
+```
+
+## Get a specific broadcast message
+
+```
+GET /broadcast_messages/:id
+```
+
+| Attribute | Type | Required | Description |
+| ----------- | -------- | -------- | ------------------------- |
+| `id` | integer | yes | Broadcast message ID |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1
+```
+
+Example response:
+
+```json
+{
+ "message":"Deploy in progress",
+ "starts_at":"2016-08-24T23:21:16.078Z",
+ "ends_at":"2016-08-26T23:21:16.080Z",
+ "color":"#cecece",
+ "font":"#FFFFFF",
+ "id":1,
+ "active":false
+}
+```
+
+## Create a broadcast message
+
+Responds with `400 Bad request` when the `message` parameter is missing or the
+`color` or `font` values are invalid, and `201 Created` when the broadcast
+message was successfully created.
+
+```
+POST /broadcast_messages
+```
+
+| Attribute | Type | Required | Description |
+| ----------- | -------- | -------- | ---------------------------------------------------- |
+| `message` | string | yes | Message to display |
+| `starts_at` | datetime | no | Starting time (defaults to current time) |
+| `ends_at` | datetime | no | Ending time (defaults to one hour from current time) |
+| `color` | string | no | Background color hex code |
+| `font` | string | no | Foreground color hex code |
+
+```bash
+curl --data "message=Deploy in progress&color=#cecece" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages
+```
+
+Example response:
+
+```json
+{
+ "message":"Deploy in progress",
+ "starts_at":"2016-08-26T00:41:35.060Z",
+ "ends_at":"2016-08-26T01:41:35.060Z",
+ "color":"#cecece",
+ "font":"#FFFFFF",
+ "id":1,
+ "active": true
+}
+```
+
+## Update a broadcast message
+
+```
+PUT /broadcast_messages/:id
+```
+
+| Attribute | Type | Required | Description |
+| ----------- | -------- | -------- | ------------------------- |
+| `id` | integer | yes | Broadcast message ID |
+| `message` | string | no | Message to display |
+| `starts_at` | datetime | no | Starting time |
+| `ends_at` | datetime | no | Ending time |
+| `color` | string | no | Background color hex code |
+| `font` | string | no | Foreground color hex code |
+
+```bash
+curl --request PUT --data "message=Update message&color=#000" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1
+```
+
+Example response:
+
+```json
+{
+ "message":"Update message",
+ "starts_at":"2016-08-26T00:41:35.060Z",
+ "ends_at":"2016-08-26T01:41:35.060Z",
+ "color":"#000",
+ "font":"#FFFFFF",
+ "id":1,
+ "active": true
+}
+```
+
+## Delete a broadcast message
+
+```
+DELETE /broadcast_messages/:id
+```
+
+| Attribute | Type | Required | Description |
+| ----------- | -------- | -------- | ------------------------- |
+| `id` | integer | yes | Broadcast message ID |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1
+```
+
+Example response:
+
+```json
+{
+ "message":"Update message",
+ "starts_at":"2016-08-26T00:41:35.060Z",
+ "ends_at":"2016-08-26T01:41:35.060Z",
+ "color":"#000",
+ "font":"#FFFFFF",
+ "id":1,
+ "active": true
+}
+```
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 5c98c5d7565..682151d4b1d 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -10,7 +10,7 @@ GET /projects/:id/repository/commits
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `ref_name` | string | no | The name of a repository branch or tag or if not given the default branch |
| `since` | string | no | Only commits after or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
| `until` | string | no | Only commits before or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
@@ -58,7 +58,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `sha` | string | yes | The commit hash or name of a repository branch or tag |
```bash
@@ -102,7 +102,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `sha` | string | yes | The commit hash or name of a repository branch or tag |
```bash
@@ -138,7 +138,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `sha` | string | yes | The commit hash or name of a repository branch or tag |
```bash
@@ -187,7 +187,7 @@ POST /projects/:id/repository/commits/:sha/comments
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `sha` | string | yes | The commit SHA or name of a repository branch or tag |
| `note` | string | yes | The text of the comment |
| `path` | string | no | The file path relative to the repository |
@@ -232,7 +232,7 @@ GET /projects/:id/repository/commits/:sha/statuses
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project
+| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `sha` | string | yes | The commit SHA
| `ref_name`| string | no | The name of a repository branch or tag or, if not given, the default branch
| `stage` | string | no | Filter by [build stage](../ci/yaml/README.md#stages), e.g., `test`
@@ -306,7 +306,7 @@ POST /projects/:id/statuses/:sha
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project
+| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `sha` | string | yes | The commit SHA
| `state` | string | yes | The state of the status. Can be one of the following: `pending`, `running`, `success`, `failed`, `canceled`
| `ref` | string | no | The `ref` (branch or tag) to which the status refers
diff --git a/doc/api/issues.md b/doc/api/issues.md
index b194799ccbf..eed0d2fce51 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -80,7 +80,8 @@ Example response:
"subscribed" : false,
"user_notes_count": 1,
"due_date": "2016-07-22",
- "web_url": "http://example.com/example/example/issues/6"
+ "web_url": "http://example.com/example/example/issues/6",
+ "confidential": false
}
]
```
@@ -158,7 +159,8 @@ Example response:
"subscribed" : false,
"user_notes_count": 1,
"due_date": null,
- "web_url": "http://example.com/example/example/issues/1"
+ "web_url": "http://example.com/example/example/issues/1",
+ "confidential": false
}
]
```
@@ -238,7 +240,8 @@ Example response:
"subscribed" : false,
"user_notes_count": 1,
"due_date": "2016-07-22",
- "web_url": "http://example.com/example/example/issues/1"
+ "web_url": "http://example.com/example/example/issues/1",
+ "confidential": false
}
]
```
@@ -303,7 +306,8 @@ Example response:
"subscribed": false,
"user_notes_count": 1,
"due_date": null,
- "web_url": "http://example.com/example/example/issues/1"
+ "web_url": "http://example.com/example/example/issues/1",
+ "confidential": false
}
```
@@ -324,6 +328,7 @@ POST /projects/:id/issues
| `id` | integer | yes | The ID of a project |
| `title` | string | yes | The title of an issue |
| `description` | string | no | The description of an issue |
+| `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. |
| `assignee_id` | integer | no | The ID of a user to assign issue |
| `milestone_id` | integer | no | The ID of a milestone to assign issue |
| `labels` | string | no | Comma-separated label names for an issue |
@@ -362,7 +367,8 @@ Example response:
"subscribed" : true,
"user_notes_count": 0,
"due_date": null,
- "web_url": "http://example.com/example/example/issues/14"
+ "web_url": "http://example.com/example/example/issues/14",
+ "confidential": false
}
```
@@ -385,6 +391,7 @@ PUT /projects/:id/issues/:issue_id
| `issue_id` | integer | yes | The ID of a project's issue |
| `title` | string | no | The title of an issue |
| `description` | string | no | The description of an issue |
+| `confidential` | boolean | no | Updates an issue to be confidential |
| `assignee_id` | integer | no | The ID of a user to assign the issue to |
| `milestone_id` | integer | no | The ID of a milestone to assign the issue to |
| `labels` | string | no | Comma-separated label names for an issue |
@@ -424,7 +431,8 @@ Example response:
"subscribed" : true,
"user_notes_count": 0,
"due_date": "2016-07-22",
- "web_url": "http://example.com/example/example/issues/15"
+ "web_url": "http://example.com/example/example/issues/15",
+ "confidential": false
}
```
@@ -503,7 +511,8 @@ Example response:
"web_url": "https://gitlab.example.com/u/solon.cremin"
},
"due_date": null,
- "web_url": "http://example.com/example/example/issues/11"
+ "web_url": "http://example.com/example/example/issues/11",
+ "confidential": false
}
```
@@ -559,7 +568,8 @@ Example response:
"web_url": "https://gitlab.example.com/u/solon.cremin"
},
"due_date": null,
- "web_url": "http://example.com/example/example/issues/11"
+ "web_url": "http://example.com/example/example/issues/11",
+ "confidential": false
}
```
@@ -616,7 +626,8 @@ Example response:
},
"subscribed": false,
"due_date": null,
- "web_url": "http://example.com/example/example/issues/12"
+ "web_url": "http://example.com/example/example/issues/12",
+ "confidential": false
}
```
@@ -704,7 +715,8 @@ Example response:
"upvotes": 0,
"downvotes": 0,
"due_date": null,
- "web_url": "http://example.com/example/example/issues/110"
+ "web_url": "http://example.com/example/example/issues/110",
+ "confidential": false
},
"target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/issues/10",
"body": "Vel voluptas atque dicta mollitia adipisci qui at.",
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index f275762da3e..494040a1ce8 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -68,6 +68,8 @@ Parameters:
"merge_when_build_succeeds": true,
"merge_status": "can_be_merged",
"subscribed" : false,
+ "sha": "8888888888888888888888888888888888888888",
+ "merge_commit_sha": null,
"user_notes_count": 1,
"should_remove_source_branch": true,
"force_remove_source_branch": false,
@@ -135,6 +137,8 @@ Parameters:
"merge_when_build_succeeds": true,
"merge_status": "can_be_merged",
"subscribed" : true,
+ "sha": "8888888888888888888888888888888888888888",
+ "merge_commit_sha": "9999999999999999999999999999999999999999",
"user_notes_count": 1,
"should_remove_source_branch": true,
"force_remove_source_branch": false,
@@ -238,6 +242,8 @@ Parameters:
"merge_when_build_succeeds": true,
"merge_status": "can_be_merged",
"subscribed" : true,
+ "sha": "8888888888888888888888888888888888888888",
+ "merge_commit_sha": null,
"user_notes_count": 1,
"should_remove_source_branch": true,
"force_remove_source_branch": false,
@@ -322,6 +328,8 @@ Parameters:
"merge_when_build_succeeds": true,
"merge_status": "can_be_merged",
"subscribed" : true,
+ "sha": "8888888888888888888888888888888888888888",
+ "merge_commit_sha": null,
"user_notes_count": 0,
"should_remove_source_branch": true,
"force_remove_source_branch": false,
@@ -397,6 +405,8 @@ Parameters:
"merge_when_build_succeeds": true,
"merge_status": "can_be_merged",
"subscribed" : true,
+ "sha": "8888888888888888888888888888888888888888",
+ "merge_commit_sha": null,
"user_notes_count": 1,
"should_remove_source_branch": true,
"force_remove_source_branch": false,
@@ -499,6 +509,8 @@ Parameters:
"merge_when_build_succeeds": true,
"merge_status": "can_be_merged",
"subscribed" : true,
+ "sha": "8888888888888888888888888888888888888888",
+ "merge_commit_sha": "9999999999999999999999999999999999999999",
"user_notes_count": 1,
"should_remove_source_branch": true,
"force_remove_source_branch": false,
@@ -569,6 +581,8 @@ Parameters:
"merge_when_build_succeeds": true,
"merge_status": "can_be_merged",
"subscribed" : true,
+ "sha": "8888888888888888888888888888888888888888",
+ "merge_commit_sha": null,
"user_notes_count": 1,
"should_remove_source_branch": true,
"force_remove_source_branch": false,
@@ -724,7 +738,9 @@ Example response:
},
"merge_when_build_succeeds": false,
"merge_status": "cannot_be_merged",
- "subscribed": true
+ "subscribed": true,
+ "sha": "8888888888888888888888888888888888888888",
+ "merge_commit_sha": null
}
```
@@ -798,7 +814,9 @@ Example response:
},
"merge_when_build_succeeds": false,
"merge_status": "cannot_be_merged",
- "subscribed": false
+ "subscribed": false,
+ "sha": "8888888888888888888888888888888888888888",
+ "merge_commit_sha": null
}
```
@@ -891,6 +909,8 @@ Example response:
"merge_when_build_succeeds": false,
"merge_status": "unchecked",
"subscribed": true,
+ "sha": "8888888888888888888888888888888888888888",
+ "merge_commit_sha": null,
"user_notes_count": 7,
"should_remove_source_branch": true,
"force_remove_source_branch": false,
@@ -902,3 +922,112 @@ Example response:
"created_at": "2016-07-01T11:14:15.530Z"
}
```
+
+## Get MR diff versions
+
+Get a list of merge request diff versions.
+
+```
+GET /projects/:id/merge_requests/:merge_request_id/versions
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | --------------------- |
+| `id` | String | yes | The ID of the project |
+| `merge_request_id` | integer | yes | The ID of the merge request |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/merge_requests/1/versions
+```
+
+Example response:
+
+```json
+[{
+ "id": 110,
+ "head_commit_sha": "33e2ee8579fda5bc36accc9c6fbd0b4fefda9e30",
+ "base_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd",
+ "start_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd",
+ "created_at": "2016-07-26T14:44:48.926Z",
+ "merge_request_id": 105,
+ "state": "collected",
+ "real_size": "1"
+}, {
+ "id": 108,
+ "head_commit_sha": "3eed087b29835c48015768f839d76e5ea8f07a24",
+ "base_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd",
+ "start_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd",
+ "created_at": "2016-07-25T14:21:33.028Z",
+ "merge_request_id": 105,
+ "state": "collected",
+ "real_size": "1"
+}]
+```
+
+## Get a single MR diff version
+
+Get a single merge request diff version.
+
+```
+GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | --------------------- |
+| `id` | String | yes | The ID of the project |
+| `merge_request_id` | integer | yes | The ID of the merge request |
+| `version_id` | integer | yes | The ID of the merge request diff version |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/merge_requests/1/versions/1
+```
+
+Example response:
+
+```json
+{
+ "id": 110,
+ "head_commit_sha": "33e2ee8579fda5bc36accc9c6fbd0b4fefda9e30",
+ "base_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd",
+ "start_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd",
+ "created_at": "2016-07-26T14:44:48.926Z",
+ "merge_request_id": 105,
+ "state": "collected",
+ "real_size": "1",
+ "commits": [{
+ "id": "33e2ee8579fda5bc36accc9c6fbd0b4fefda9e30",
+ "short_id": "33e2ee85",
+ "title": "Change year to 2018",
+ "author_name": "Administrator",
+ "author_email": "admin@example.com",
+ "created_at": "2016-07-26T17:44:29.000+03:00",
+ "message": "Change year to 2018"
+ }, {
+ "id": "aa24655de48b36335556ac8a3cd8bb521f977cbd",
+ "short_id": "aa24655d",
+ "title": "Update LICENSE",
+ "author_name": "Administrator",
+ "author_email": "admin@example.com",
+ "created_at": "2016-07-25T17:21:53.000+03:00",
+ "message": "Update LICENSE"
+ }, {
+ "id": "3eed087b29835c48015768f839d76e5ea8f07a24",
+ "short_id": "3eed087b",
+ "title": "Add license",
+ "author_name": "Administrator",
+ "author_email": "admin@example.com",
+ "created_at": "2016-07-25T17:21:20.000+03:00",
+ "message": "Add license"
+ }],
+ "diffs": [{
+ "old_path": "LICENSE",
+ "new_path": "LICENSE",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "diff": "--- /dev/null\n+++ b/LICENSE\n@@ -0,0 +1,21 @@\n+The MIT License (MIT)\n+\n+Copyright (c) 2018 Administrator\n+\n+Permission is hereby granted, free of charge, to any person obtaining a copy\n+of this software and associated documentation files (the \"Software\"), to deal\n+in the Software without restriction, including without limitation the rights\n+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n+copies of the Software, and to permit persons to whom the Software is\n+furnished to do so, subject to the following conditions:\n+\n+The above copyright notice and this permission notice shall be included in all\n+copies or substantial portions of the Software.\n+\n+THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n+SOFTWARE.\n",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false
+ }]
+}
+```
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 0e4806e31c5..a62aaee14d7 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -452,6 +452,7 @@ Parameters:
- `import_url` (optional)
- `public_builds` (optional)
- `only_allow_merge_if_build_succeeds` (optional)
+- `lfs_enabled` (optional)
### Create project for user
@@ -478,6 +479,7 @@ Parameters:
- `import_url` (optional)
- `public_builds` (optional)
- `only_allow_merge_if_build_succeeds` (optional)
+- `lfs_enabled` (optional)
### Edit project
@@ -489,7 +491,7 @@ PUT /projects/:id
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
- `name` (optional) - project name
- `path` (optional) - repository name for project
- `description` (optional) - short project description
@@ -505,6 +507,7 @@ Parameters:
- `visibility_level` (optional)
- `public_builds` (optional)
- `only_allow_merge_if_build_succeeds` (optional)
+- `lfs_enabled` (optional)
On success, method returns 200 with the updated project. If parameters are
invalid, 400 is returned.
@@ -519,7 +522,7 @@ POST /projects/fork/:id
Parameters:
-- `id` (required) - The ID of the project to be forked
+- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
### Star a project
@@ -532,7 +535,7 @@ POST /projects/:id/star
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the project |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
@@ -599,7 +602,7 @@ DELETE /projects/:id/star
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the project |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
@@ -670,7 +673,7 @@ POST /projects/:id/archive
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the project |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/archive"
@@ -757,7 +760,7 @@ POST /projects/:id/unarchive
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the project |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/unarchive"
@@ -839,7 +842,7 @@ DELETE /projects/:id
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
## Uploads
@@ -853,7 +856,7 @@ POST /projects/:id/uploads
Parameters:
-- `id` (required) - The ID of the project
+- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
- `file` (required) - The file to be uploaded
```json
@@ -882,7 +885,7 @@ POST /projects/:id/share
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
- `group_id` (required) - The ID of a group
- `group_access` (required) - Level of permissions for sharing
@@ -998,6 +1001,8 @@ is available before it is returned in the JSON response or an empty response is
## Branches
+For more information please consult the [Branches](branches.md) documentation.
+
### List branches
Lists all branches of a project.
@@ -1016,56 +1021,46 @@ Parameters:
"name": "async",
"commit": {
"id": "a2b702edecdf41f07b42653eb1abe30ce98b9fca",
- "parents": [
- {
- "id": "3f94fc7c85061973edc9906ae170cc269b07ca55"
- }
+ "parent_ids": [
+ "3f94fc7c85061973edc9906ae170cc269b07ca55"
],
- "tree": "c68537c6534a02cc2b176ca1549f4ffa190b58ee",
"message": "give Caolan credit where it's due (up top)",
- "author": {
- "name": "Jeremy Ashkenas",
- "email": "jashkenas@example.com"
- },
- "committer": {
- "name": "Jeremy Ashkenas",
- "email": "jashkenas@example.com"
- },
+ "author_name": "Jeremy Ashkenas",
+ "author_email": "jashkenas@example.com",
"authored_date": "2010-12-08T21:28:50+00:00",
+ "committer_name": "Jeremy Ashkenas",
+ "committer_email": "jashkenas@example.com",
"committed_date": "2010-12-08T21:28:50+00:00"
},
- "protected": false
+ "protected": false,
+ "developers_can_push": false,
+ "developers_can_merge": false
},
{
"name": "gh-pages",
"commit": {
"id": "101c10a60019fe870d21868835f65c25d64968fc",
- "parents": [
- {
- "id": "9c15d2e26945a665131af5d7b6d30a06ba338aaa"
- }
+ "parent_ids": [
+ "9c15d2e26945a665131af5d7b6d30a06ba338aaa"
],
- "tree": "fb5cc9d45da3014b17a876ad539976a0fb9b352a",
"message": "Underscore.js 1.5.2",
- "author": {
- "name": "Jeremy Ashkenas",
- "email": "jashkenas@example.com"
- },
- "committer": {
- "name": "Jeremy Ashkenas",
- "email": "jashkenas@example.com"
- },
+ "author_name": "Jeremy Ashkenas",
+ "author_email": "jashkenas@example.com",
"authored_date": "2013-09-07T12:58:21+00:00",
+ "committer_name": "Jeremy Ashkenas",
+ "committer_email": "jashkenas@example.com",
"committed_date": "2013-09-07T12:58:21+00:00"
},
- "protected": false
+ "protected": false,
+ "developers_can_push": false,
+ "developers_can_merge": false
}
]
```
-### List single branch
+### Single branch
-Lists a specific branch of a project.
+A specific branch of a project.
```
GET /projects/:id/repository/branches/:branch
@@ -1075,6 +1070,8 @@ Parameters:
- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
- `branch` (required) - The name of the branch.
+- `developers_can_push` - Flag if developers can push to the branch.
+- `developers_can_merge` - Flag if developers can merge to the branch.
### Protect single branch
@@ -1114,7 +1111,7 @@ POST /projects/:id/fork/:forked_from_id
Parameters:
-- `id` (required) - The ID of the project
+- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
- `forked_from_id:` (required) - The ID of the project that was forked from
### Delete an existing forked from relationship
@@ -1125,7 +1122,7 @@ DELETE /projects/:id/fork
Parameter:
-- `id` (required) - The ID of the project
+- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
## Search for projects by name
diff --git a/doc/api/users.md b/doc/api/users.md
index 7e848586dbd..54f7a2a2ace 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -310,8 +310,7 @@ GET /user
"can_create_group": true,
"can_create_project": true,
"two_factor_enabled": true,
- "external": false,
- "private_token": "dd34asd13as"
+ "external": false
}
```
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index 20cd88c8d20..ca9b986a060 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -5,7 +5,7 @@ Introduced in GitLab 8.8.
## Pipelines
-A pipeline is a group of [builds] that get executed in [stages] (batches). All
+A pipeline is a group of [builds] that get executed in [stages] \(batches). All
of the builds in a stage are executed in parallel (if there are enough
concurrent [runners]), and if they all succeed, the pipeline moves on to the
next stage. If one of the builds fails, the next stage is not (usually)
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index e7850aa2c9d..58d5306f12a 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -39,7 +39,7 @@ If you want a quick introduction to GitLab CI, follow our
- [before_script and after_script](#before_script-and-after_script)
- [Git Strategy](#git-strategy)
- [Shallow cloning](#shallow-cloning)
-- [Hidden jobs](#hidden-jobs)
+- [Hidden keys](#hidden-keys)
- [Special YAML features](#special-yaml-features)
- [Anchors](#anchors)
- [Validate the .gitlab-ci.yml](#validate-the-gitlab-ci-yml)
@@ -934,24 +934,27 @@ variables:
GIT_DEPTH: "3"
```
-## Hidden jobs
+## Hidden keys
>**Note:**
Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
-Jobs that start with a dot (`.`) will be not processed by GitLab CI. You can
+Keys that start with a dot (`.`) will be not processed by GitLab CI. You can
use this feature to ignore jobs, or use the
-[special YAML features](#special-yaml-features) and transform the hidden jobs
+[special YAML features](#special-yaml-features) and transform the hidden keys
into templates.
-In the following example, `.job_name` will be ignored:
+In the following example, `.key_name` will be ignored:
```yaml
-.job_name:
+.key_name:
script:
- rake spec
```
+Hidden keys can be hashes like normal CI jobs, but you are also allowed to use
+different types of structures to leverage special YAML features.
+
## Special YAML features
It's possible to use special YAML features like anchors (`&`), aliases (`*`)
@@ -967,7 +970,7 @@ 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
-properties, and is a perfect example to be used with [hidden jobs](#hidden-jobs)
+properties, and is a perfect example to be used with [hidden keys](#hidden-keys)
to provide templates for your jobs.
The following example uses anchors and map merging. It will create two jobs,
@@ -975,7 +978,7 @@ The following example uses anchors and map merging. It will create two jobs,
having their own custom `script` defined:
```yaml
-.job_template: &job_definition # Hidden job that defines an anchor named 'job_definition'
+.job_template: &job_definition # Hidden key that defines an anchor named 'job_definition'
image: ruby:2.1
services:
- postgres
@@ -1081,7 +1084,7 @@ test:mysql:
- ruby
```
-You can see that the hidden jobs are conveniently used as templates.
+You can see that the hidden keys are conveniently used as templates.
## Validate the .gitlab-ci.yml
diff --git a/doc/development/README.md b/doc/development/README.md
index 57f37da6f80..58c00f618fa 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -18,6 +18,8 @@
## Process
- [Code review guidelines](code_review.md) for reviewing code and having code reviewed.
+- [Merge request performance guidelines](merge_request_performance_guidelines.md)
+ for ensuring merge requests do not negatively impact GitLab performance
## Backend howtos
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index 927a1872413..39b801f761d 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -6,7 +6,7 @@ it organized and easy to find.
## Location and naming of documents
>**Note:**
-These guidelines derive from the discussion taken place in issue [#3349](ce-3349).
+These guidelines derive from the discussion taken place in issue [#3349][ce-3349].
The documentation hierarchy can be vastly improved by providing a better layout
and organization of directories.
@@ -155,15 +155,30 @@ Inside the document:
- Every piece of documentation that comes with a new feature should declare the
GitLab version that feature got introduced. Right below the heading add a
- note: `> Introduced in GitLab 8.3.`.
+ note:
+
+ ```
+ > Introduced in GitLab 8.3.
+ ```
+
- If possible every feature should have a link to the MR that introduced it.
The above note would be then transformed to:
- `> [Introduced][ce-1242] in GitLab 8.3.`, where
- the [link identifier](#links) is named after the repository (CE) and the MR
- number.
-- If the feature is only in GitLab EE, don't forget to mention it, like:
- `> Introduced in GitLab EE 8.3.`. Otherwise, leave
- this mention out.
+
+ ```
+ > [Introduced][ce-1242] in GitLab 8.3.
+ ```
+
+ , where the [link identifier](#links) is named after the repository (CE) and
+ the MR number.
+
+- If the feature is only in GitLab Enterprise Edition, don't forget to mention
+ it, like:
+
+ ```
+ > Introduced in GitLab Enterprise Edition 8.3.
+ ```
+
+ Otherwise, leave this mention out.
## References
@@ -222,18 +237,26 @@ For example, if you were to move `doc/workflow/lfs/lfs_administration.md` to
```
1. Find and replace any occurrences of the old location with the new one.
- A quick way to find them is to use `grep`:
+ A quick way to find them is to use `git grep`. First go to the root directory
+ where you cloned the `gitlab-ce` repository and then do:
```
- grep -nR "lfs_administration.md" doc/
+ git grep -n "workflow/lfs/lfs_administration"
+ git grep -n "lfs/lfs_administration"
```
- The above command will search in the `doc/` directory for
- `lfs_administration.md` recursively and will print the file and the line
- where this file is mentioned. Note that we used just the filename
- (`lfs_administration.md`) and not the whole the relative path
- (`workflow/lfs/lfs_administration.md`).
+Things to note:
+- Since we also use inline documentation, except for the documentation itself,
+ the document might also be referenced in the views of GitLab (`app/`) which will
+ render when visiting `/help`, and sometimes in the testing suite (`spec/`).
+- The above `git grep` command will search recursively in the directory you run
+ it in for `workflow/lfs/lfs_administration` and `lfs/lfs_administration`
+ and will print the file and the line where this file is mentioned.
+ You may ask why the two greps. Since we use relative paths to link to
+ documentation, sometimes it might be useful to search a path deeper.
+- The `*.md` extension is not used when a document is linked to GitLab's
+ built-in help page, that's why we omit it in `git grep`.
## Configuration documentation for source and Omnibus installations
@@ -422,7 +445,7 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "domain
[cURL]: http://curl.haxx.se/ "cURL website"
[single spaces]: http://www.slate.com/articles/technology/technology/2011/01/space_invaders.html
-[gfm]: http://docs.gitlab.com/ce/markdown/markdown.html#newlines "GitLab flavored markdown documentation"
+[gfm]: http://docs.gitlab.com/ce/user/markdown.html#newlines "GitLab flavored markdown documentation"
[doc-restart]: ../administration/restart_gitlab.md "GitLab restart documentation"
[ce-3349]: https://gitlab.com/gitlab-org/gitlab-ce/issues/3349 "Documentation restructure"
[graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/d8d39f4a87b90fb9ae89ca12dc565347b4900d5e/production/resources/gitlab-map.graffle
diff --git a/doc/development/merge_request_performance_guidelines.md b/doc/development/merge_request_performance_guidelines.md
new file mode 100644
index 00000000000..0363bf8c1d5
--- /dev/null
+++ b/doc/development/merge_request_performance_guidelines.md
@@ -0,0 +1,171 @@
+# Merge Request Performance Guidelines
+
+To ensure a merge request does not negatively impact performance of GitLab
+_every_ merge request **must** adhere to the guidelines outlined in this
+document. There are no exceptions to this rule unless specifically discussed
+with and agreed upon by merge request endbosses and performance specialists.
+
+To measure the impact of a merge request you can use
+[Sherlock](profiling.md#sherlock). It's also highly recommended that you read
+the following guides:
+
+* [Performance Guidelines](performance.md)
+* [What requires downtime?](what_requires_downtime.md)
+
+## Impact Analysis
+
+**Summary:** think about the impact your merge request may have on performance
+and those maintaining a GitLab setup.
+
+Any change submitted can have an impact not only on the application itself but
+also those maintaining it and those keeping it up and running (e.g. production
+engineers). As a result you should think carefully about the impact of your
+merge request on not only the application but also on the people keeping it up
+and running.
+
+Can the queries used potentially take down any critical services and result in
+engineers being woken up in the night? Can a malicious user abuse the code to
+take down a GitLab instance? Will my changes simply make loading a certain page
+slower? Will execution time grow exponentially given enough load or data in the
+database?
+
+These are all questions one should ask themselves before submitting a merge
+request. It may sometimes be difficult to assess the impact, in which case you
+should ask a performance specialist to review your code. See the "Reviewing"
+section below for more information.
+
+## Performance Review
+
+**Summary:** ask performance specialists to review your code if you're not sure
+about the impact.
+
+Sometimes it's hard to assess the impact of a merge request. In this case you
+should ask one of the merge request (mini) endbosses to review your changes. You
+can find a list of these endbosses at <https://about.gitlab.com/team/>. An
+endboss in turn can request a performance specialist to review the changes.
+
+## Query Counts
+
+**Summary:** a merge request **should not** increase the number of executed SQL
+queries unless absolutely necessary.
+
+The number of queries executed by the code modified or added by a merge request
+must not increase unless absolutely necessary. When building features it's
+entirely possible you will need some extra queries, but you should try to keep
+this at a minimum.
+
+As an example, say you introduce a feature that updates a number of database
+rows with the same value. It may be very tempting (and easy) to write this using
+the following pseudo code:
+
+```ruby
+objects_to_update.each do |object|
+ object.some_field = some_value
+ object.save
+end
+```
+
+This will end up running one query for every object to update. This code can
+easily overload a database given enough rows to update or many instances of this
+code running in parallel. This particular problem is known as the
+["N+1 query problem"](http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations).
+
+In this particular case the workaround is fairly easy:
+
+```ruby
+objects_to_update.update_all(some_field: some_value)
+```
+
+This uses ActiveRecord's `update_all` method to update all rows in a single
+query. This in turn makes it much harder for this code to overload a database.
+
+## Executing Queries in Loops
+
+**Summary:** SQL queries **must not** be executed in a loop unless absolutely
+necessary.
+
+Executing SQL queries in a loop can result in many queries being executed
+depending on the number of iterations in a loop. This may work fine for a
+development environment with little data, but in a production environment this
+can quickly spiral out of control.
+
+There are some cases where this may be needed. If this is the case this should
+be clearly mentioned in the merge request description.
+
+## Eager Loading
+
+**Summary:** always eager load associations when retrieving more than one row.
+
+When retrieving multiple database records for which you need to use any
+associations you **must** eager load these associations. For example, if you're
+retrieving a list of blog posts and you want to display their authors you
+**must** eager load the author associations.
+
+In other words, instead of this:
+
+```ruby
+Post.all.each do |post|
+ puts post.author.name
+end
+```
+
+You should use this:
+
+```ruby
+Post.all.includes(:author).each do |post|
+ puts post.author.name
+end
+```
+
+## Memory Usage
+
+**Summary:** merge requests **must not** increase memory usage unless absolutely
+necessary.
+
+A merge request must not increase the memory usage of GitLab by more than the
+absolute bare minimum required by the code. This means that if you have to parse
+some large document (e.g. an HTML document) it's best to parse it as a stream
+whenever possible, instead of loading the entire input into memory. Sometimes
+this isn't possible, in that case this should be stated explicitly in the merge
+request.
+
+## Lazy Rendering of UI Elements
+
+**Summary:** only render UI elements when they're actually needed.
+
+Certain UI elements may not always be needed. For example, when hovering over a
+diff line there's a small icon displayed that can be used to create a new
+comment. Instead of always rendering these kind of elements they should only be
+rendered when actually needed. This ensures we don't spend time generating
+Haml/HTML when it's not going to be used.
+
+## Instrumenting New Code
+
+**Summary:** always add instrumentation for new classes, modules, and methods.
+
+Newly added classes, modules, and methods must be instrumented. This ensures
+we can track the performance of this code over time.
+
+For more information see [Instrumentation](instrumentation.md). This guide
+describes how to add instrumentation and where to add it.
+
+## Use of Caching
+
+**Summary:** cache data in memory or in Redis when it's needed multiple times in
+a transaction or has to be kept around for a certain time period.
+
+Sometimes certain bits of data have to be re-used in different places during a
+transaction. In these cases this data should be cached in memory to remove the
+need for running complex operations to fetch the data. You should use Redis if
+data should be cached for a certain time period instead of the duration of the
+transaction.
+
+For example, say you process multiple snippets of text containiner username
+mentions (e.g. `Hello @alice` and `How are you doing @alice?`). By caching the
+user objects for every username we can remove the need for running the same
+query for every mention of `@alice`.
+
+Caching data per transaction can be done using
+[RequestStore](https://github.com/steveklabnik/request_store). Caching data in
+Redis can be done using [Rails' caching
+system](http://guides.rubyonrails.org/caching_with_rails.html).
diff --git a/doc/development/newlines_styleguide.md b/doc/development/newlines_styleguide.md
index e03adcaadea..32aac2529a4 100644
--- a/doc/development/newlines_styleguide.md
+++ b/doc/development/newlines_styleguide.md
@@ -2,7 +2,7 @@
This style guide recommends best practices for newlines in Ruby code.
-## Rule: separate code with newlines only when it makes sense from logic perspectice
+## Rule: separate code with newlines only to group together related logic
```ruby
# bad
diff --git a/doc/install/installation.md b/doc/install/installation.md
index d4b89fa8345..9522c3e7170 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -268,9 +268,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-11-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-12-stable gitlab
-**Note:** You can change `8-11-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `8-12-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
@@ -331,6 +331,9 @@ sudo usermod -aG redis git
# Disable 'git gc --auto' because GitLab already runs 'git gc' when needed
sudo -u git -H git config --global gc.auto 0
+ # Enable packfile bitmaps
+ sudo -u git -H git config --global repack.writeBitmaps true
+
# Configure Redis connection settings
sudo -u git -H cp config/resque.yml.example config/resque.yml
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 571f1a38358..9799e0a3730 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -63,24 +63,24 @@ If you have enough RAM memory and a recent CPU the speed of GitLab is mainly lim
### Memory
-You need at least 2GB of addressable memory (RAM + swap) to install and use GitLab!
+You need at least 4GB of addressable memory (RAM + swap) to install and use GitLab!
The operating system and any other running applications will also be using memory
-so keep in mind that you need at least 2GB available before running GitLab. With
+so keep in mind that you need at least 4GB available before running GitLab. With
less memory GitLab will give strange errors during the reconfigure run and 500
errors during usage.
-- 512MB RAM + 1.5GB of swap is the absolute minimum but we strongly **advise against** this amount of memory. See the unicorn worker section below for more advice.
-- 1GB RAM + 1GB swap supports up to 100 users but it will be very slow
-- **2GB RAM** is the **recommended** memory size for all installations and supports up to 100 users
-- 4GB RAM supports up to 1,000 users
-- 8GB RAM supports up to 2,000 users
-- 16GB RAM supports up to 4,000 users
-- 32GB RAM supports up to 8,000 users
-- 64GB RAM supports up to 16,000 users
-- 128GB RAM supports up to 32,000 users
+- 1GB RAM + 3GB of swap is the absolute minimum but we strongly **advise against** this amount of memory. See the unicorn worker section below for more advice.
+- 2GB RAM + 2GB swap supports up to 100 users but it will be very slow
+- **4GB RAM** is the **recommended** memory size for all installations and supports up to 100 users
+- 8GB RAM supports up to 1,000 users
+- 16GB RAM supports up to 2,000 users
+- 32GB RAM supports up to 4,000 users
+- 64GB RAM supports up to 8,000 users
+- 128GB RAM supports up to 16,000 users
+- 256GB RAM supports up to 32,000 users
- More users? Run it on [multiple application servers](https://about.gitlab.com/high-availability/)
-We recommend having at least 1GB of swap on your server, even if you currently have
+We recommend having at least 2GB of swap on your server, even if you currently have
enough available RAM. Having swap will help reduce the chance of errors occurring
if your available memory changes.
@@ -113,10 +113,8 @@ It's possible to increase the amount of unicorn workers and this will usually he
For most instances we recommend using: CPU cores + 1 = unicorn workers.
So for a machine with 2 cores, 3 unicorn workers is ideal.
-For all machines that have 1GB and up we recommend a minimum of three unicorn workers.
-If you have a 512MB machine with a magnetic (non-SSD) swap drive we recommend to configure only one Unicorn worker to prevent excessive swapping.
-With one Unicorn worker only git over ssh access will work because the git over HTTP access requires two running workers (one worker to receive the user request and one worker for the authorization check).
-If you have a 512MB machine with a SSD drive you can use two Unicorn workers, this will allow HTTP access although it will be slow due to swapping.
+For all machines that have 2GB and up we recommend a minimum of three unicorn workers.
+If you have a 1GB machine we recommend to configure only two Unicorn workers to prevent excessive swapping.
To change the Unicorn workers when you have the Omnibus package please see [the Unicorn settings in the Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md#unicorn-settings).
diff --git a/doc/integration/README.md b/doc/integration/README.md
index 70895abbcad..c2fd299db07 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -15,7 +15,7 @@ See the documentation below for details on how to configure these services.
- [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages
- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users
- [Akismet](akismet.md) Configure Akismet to stop spam
-- [Koding](koding.md) Configure Koding to use IDE integration
+- [Koding](../administration/integration/koding.md) Configure Koding to use IDE integration
GitLab Enterprise Edition contains [advanced Jenkins support][jenkins].
diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md
index 2eb6266ebe7..556d71b8b76 100644
--- a/doc/integration/bitbucket.md
+++ b/doc/integration/bitbucket.md
@@ -1,111 +1,164 @@
-# Integrate your server with Bitbucket
+# Integrate your GitLab server with Bitbucket
-Import projects from Bitbucket and login to your GitLab instance with your Bitbucket account.
+Import projects from Bitbucket.org and login to your GitLab instance with your
+Bitbucket.org account.
-To enable the Bitbucket OmniAuth provider you must register your application with Bitbucket.
-Bitbucket will generate an application ID and secret key for you to use.
+## Overview
-1. Sign in to Bitbucket.
+You can set up Bitbucket.org as an OAuth provider so that you can use your
+credentials to authenticate into GitLab or import your projects from
+Bitbucket.org.
-1. Navigate to your individual user settings or a team's settings, depending on how you want the application registered. It does not matter if the application is registered as an individual or a team - that is entirely up to you.
+- To use Bitbucket.org as an OmniAuth provider, follow the [Bitbucket OmniAuth
+ provider](#bitbucket-omniauth-provider) section.
+- To import projects from Bitbucket, follow both the
+ [Bitbucket OmniAuth provider](#bitbucket-omniauth-provider) and
+ [Bitbucket project import](#bitbucket-project-import) sections.
-1. Select "OAuth" in the left menu.
+## Bitbucket OmniAuth provider
-1. Select "Add consumer".
+> **Note:**
+Make sure to first follow the [Initial OmniAuth configuration][init-oauth]
+before proceeding with setting up the Bitbucket integration.
-1. Provide the required details.
- - Name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive.
- - Application description: Fill this in if you wish.
- - URL: The URL to your GitLab installation. 'https://gitlab.company.com'
-1. Select "Save".
+To enable the Bitbucket OmniAuth provider you must register your application
+with Bitbucket.org. Bitbucket will generate an application ID and secret key for
+you to use.
-1. You should now see a Key and Secret in the list of OAuth customers.
- Keep this page open as you continue configuration.
+1. Sign in to [Bitbucket.org](https://bitbucket.org).
+1. Navigate to your individual user settings (**Bitbucket settings**) or a team's
+ settings (**Manage team**), depending on how you want the application registered.
+ It does not matter if the application is registered as an individual or a
+ team, that is entirely up to you.
+1. Select **OAuth** in the left menu under "Access Management".
+1. Select **Add consumer**.
+1. Provide the required details:
-1. On your GitLab server, open the configuration file.
+ | Item | Description |
+ | :--- | :---------- |
+ | **Name** | This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive. |
+ | **Application description** | Fill this in if you wish. |
+ | **Callback URL** | Leave blank. |
+ | **URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. |
- For omnibus package:
+ And grant at least the following permissions:
- ```sh
- sudo editor /etc/gitlab/gitlab.rb
+ ```
+ Account: Email
+ Repositories: Read, Admin
```
- For installations from source:
+ >**Note:**
+ It may seem a little odd to giving GitLab admin permissions to repositories,
+ but this is needed in order for GitLab to be able to clone the repositories.
- ```sh
- cd /home/git/gitlab
+ ![Bitbucket OAuth settings page](img/bitbucket_oauth_settings_page.png)
+
+1. Select **Save**.
+1. Select your newly created OAuth consumer and you should now see a Key and
+ Secret in the list of OAuth customers. Keep this page open as you continue
+ the configuration.
+
+ ![Bitbucket OAuth key](img/bitbucket_oauth_keys.png)
+
+1. On your GitLab server, open the configuration file:
- sudo -u git -H editor config/gitlab.yml
```
+ # For Omnibus packages
+ sudo editor /etc/gitlab/gitlab.rb
-1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
+ # For installations from source
+ sudo -u git -H editor /home/git/gitlab/config/gitlab.yml
+ ```
-1. Add the provider configuration:
+1. Follow the [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration)
+ for initial settings.
+1. Add the Bitbucket provider configuration:
- For omnibus package:
+ For Omnibus packages:
```ruby
- gitlab_rails['omniauth_providers'] = [
- {
- "name" => "bitbucket",
- "app_id" => "YOUR_KEY",
- "app_secret" => "YOUR_APP_SECRET",
- "url" => "https://bitbucket.org/"
- }
- ]
+ gitlab_rails['omniauth_providers'] = [
+ {
+ "name" => "bitbucket",
+ "app_id" => "BITBUCKET_APP_KEY",
+ "app_secret" => "BITBUCKET_APP_SECRET",
+ "url" => "https://bitbucket.org/"
+ }
+ ]
```
- For installation from source:
+ For installations from source:
- ```
- - { name: 'bitbucket', app_id: 'YOUR_KEY',
- app_secret: 'YOUR_APP_SECRET' }
+ ```yaml
+ - { name: 'bitbucket',
+ app_id: 'BITBUCKET_APP_KEY',
+ app_secret: 'BITBUCKET_APP_SECRET' }
```
-1. Change 'YOUR_APP_ID' to the key from the Bitbucket application page from step 7.
+ ---
-1. Change 'YOUR_APP_SECRET' to the secret from the Bitbucket application page from step 7.
+ Where `BITBUCKET_APP_KEY` is the Key and `BITBUCKET_APP_SECRET` the Secret
+ from the Bitbucket application page.
1. Save the configuration file.
+1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
+ installed GitLab via Omnibus or from source respectively.
-1. If you're using the omnibus package, reconfigure GitLab (```gitlab-ctl reconfigure```).
-
-1. Restart GitLab for the changes to take effect.
-
-On the sign in page there should now be a Bitbucket icon below the regular sign in form.
-Click the icon to begin the authentication process. Bitbucket will ask the user to sign in and authorize the GitLab application.
-If everything goes well the user will be returned to GitLab and will be signed in.
+On the sign in page there should now be a Bitbucket icon below the regular sign
+in form. Click the icon to begin the authentication process. Bitbucket will ask
+the user to sign in and authorize the GitLab application. If everything goes
+well, the user will be returned to GitLab and will be signed in.
## Bitbucket project import
-To allow projects to be imported directly into GitLab, Bitbucket requires two extra setup steps compared to GitHub and GitLab.com.
+To allow projects to be imported directly into GitLab, Bitbucket requires two
+extra setup steps compared to [GitHub](github.md) and [GitLab.com](gitlab.md).
-Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and instead requires GitLab to use SSH and identify itself using your GitLab server's SSH key.
+Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and
+instead requires GitLab to use SSH and identify itself using your GitLab
+server's SSH key.
-### Step 1: Public key
+To be able to access repositories on Bitbucket, GitLab will automatically
+register your public key with Bitbucket as a deploy key for the repositories to
+be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa` which
+translates to `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages and to
+`/home/git/.ssh/bitbucket_rsa.pub` for installations from source.
-To be able to access repositories on Bitbucket, GitLab will automatically register your public key with Bitbucket as a deploy key for the repositories to be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa.pub`, which will expand to `/home/git/.ssh/bitbucket_rsa.pub` in most configurations.
+---
-If you have that file in place, you're all set and should see the "Import projects from Bitbucket" option enabled. If you don't, do the following:
+Below are the steps that will allow GitLab to be able to import your projects
+from Bitbucket.
-1. Create a new SSH key:
+1. Make sure you [have enabled the Bitbucket OAuth support](#bitbucket-omniauth-provider).
+1. Create a new SSH key with an **empty passphrase**:
```sh
sudo -u git -H ssh-keygen
```
- When asked `Enter file in which to save the key` specify the correct path, eg. `/home/git/.ssh/bitbucket_rsa`.
- Make sure to use an **empty passphrase**.
+ When asked to 'Enter file in which to save the key' enter:
+ `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages or
+ `/home/git/.ssh/bitbucket_rsa` for installations from source. The name is
+ important so make sure to get it right.
-1. Configure SSH client to use your new key:
+ > **Warning:**
+ This key must NOT be associated with ANY existing Bitbucket accounts. If it
+ is, the import will fail with an `Access denied! Please verify you can add
+ deploy keys to this repository.` error.
- Open the SSH configuration file of the git user.
+1. Next, you need to to configure the SSH client to use your new key. Open the
+ SSH configuration file of the `git` user:
- ```sh
- sudo editor /home/git/.ssh/config
+ ```
+ # For Omnibus packages
+ sudo editor /var/opt/gitlab/.ssh/config
+
+ # For installations from source
+ sudo editor /home/git/.ssh/config
```
- Add a host configuration for `bitbucket.org`.
+1. Add a host configuration for `bitbucket.org`:
```sh
Host bitbucket.org
@@ -113,28 +166,46 @@ If you have that file in place, you're all set and should see the "Import projec
User git
```
-### Step 2: Known hosts
-
-To allow GitLab to connect to Bitbucket over SSH, you need to add 'bitbucket.org' to your GitLab server's known SSH hosts. Take the following steps to do so:
-
-1. Manually connect to 'bitbucket.org' over SSH, while logged in as the `git` account that GitLab will use:
+1. Save the file and exit.
+1. Manually connect to `bitbucket.org` over SSH, while logged in as the `git`
+ user that GitLab will use:
```sh
sudo -u git -H ssh bitbucket.org
```
-1. Verify the RSA key fingerprint you'll see in the response matches the one in the [Bitbucket documentation](https://confluence.atlassian.com/display/BITBUCKET/Use+the+SSH+protocol+with+Bitbucket#UsetheSSHprotocolwithBitbucket-KnownhostorBitbucket'spublickeyfingerprints) (the specific IP address doesn't matter):
+ That step is performed because GitLab needs to connect to Bitbucket over SSH,
+ in order to add `bitbucket.org` to your GitLab server's known SSH hosts.
+
+1. Verify the RSA key fingerprint you'll see in the response matches the one
+ in the [Bitbucket documentation][bitbucket-docs] (the specific IP address
+ doesn't matter):
```sh
- The authenticity of host 'bitbucket.org (207.223.240.182)' can't be established.
- RSA key fingerprint is 97:8c:1b:f2:6f:14:6b:5c:3b:ec:aa:46:46:74:7c:40.
+ The authenticity of host 'bitbucket.org (104.192.143.1)' can't be established.
+ RSA key fingerprint is SHA256:zzXQOXSRBEiUtuE8AikJYKwbHaxvSc0ojez9YXaGp1A.
Are you sure you want to continue connecting (yes/no)?
```
-1. If the fingerprint matches, type `yes` to continue connecting and have 'bitbucket.org' be added to your known hosts.
+1. If the fingerprint matches, type `yes` to continue connecting and have
+ `bitbucket.org` be added to your known SSH hosts. After confirming you should
+ see a permission denied message. If you see an authentication successful
+ message you have done something wrong. The key you are using has already been
+ added to a Bitbucket account and will cause the import script to fail. Ensure
+ the key you are using CANNOT authenticate with Bitbucket.
+1. Restart GitLab to allow it to find the new public key.
-1. Your GitLab server is now able to connect to Bitbucket over SSH.
+Your GitLab server is now able to connect to Bitbucket over SSH. You should be
+able to see the "Import projects from Bitbucket" option on the New Project page
+enabled.
-1. Restart GitLab to allow it to find the new public key.
+## Acknowledgemts
+
+Special thanks to the writer behind the following article:
+
+- http://stratus3d.com/blog/2015/09/06/migrating-from-bitbucket-to-local-gitlab-server/
-You should now see the "Import projects from Bitbucket" option on the New Project page enabled.
+[init-oauth]: omniauth.md#initial-omniauth-configuration
+[bitbucket-docs]: https://confluence.atlassian.com/bitbucket/use-the-ssh-protocol-with-bitbucket-cloud-221449711.html#UsetheSSHprotocolwithBitbucketCloud-KnownhostorBitbucket%27spublickeyfingerprints
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/integration/img/bitbucket_oauth_keys.png b/doc/integration/img/bitbucket_oauth_keys.png
new file mode 100644
index 00000000000..3fb2f7524a3
--- /dev/null
+++ b/doc/integration/img/bitbucket_oauth_keys.png
Binary files differ
diff --git a/doc/integration/img/bitbucket_oauth_settings_page.png b/doc/integration/img/bitbucket_oauth_settings_page.png
new file mode 100644
index 00000000000..a3047712d8c
--- /dev/null
+++ b/doc/integration/img/bitbucket_oauth_settings_page.png
Binary files differ
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index 46b260e7033..8a55fce96fe 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -102,8 +102,8 @@ To change these settings:
block_auto_created_users: true
```
-Now we can choose one or more of the Supported Providers listed above to continue
-the configuration process.
+Now we can choose one or more of the [Supported Providers](#supported-providers)
+listed above to continue the configuration process.
## Enable OmniAuth for an Existing User
diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md
index 98721763566..b058f8e2a03 100644
--- a/doc/update/8.10-to-8.11.md
+++ b/doc/update/8.10-to-8.11.md
@@ -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.7.8
+sudo -u git -H git checkout v0.7.11
sudo -u git -H make
```
diff --git a/doc/update/8.11-to-8.12.md b/doc/update/8.11-to-8.12.md
new file mode 100644
index 00000000000..c8ca42f97bc
--- /dev/null
+++ b/doc/update/8.11-to-8.12.md
@@ -0,0 +1,199 @@
+# From 8.11 to 8.12
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+ sudo service gitlab stop
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+If you are you running Ruby 2.1.x, you do not _need_ to upgrade Ruby yet, but you should note that support for 2.1.x is deprecated and we will require 2.3.x in 8.13. It's strongly recommended that you upgrade as soon as possible.
+
+You can check which version you are running with `ruby -v`.
+
+Download and compile Ruby:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.1.tar.gz
+echo 'c39b4001f7acb4e334cb60a0f4df72d434bef711 ruby-2.3.1.tar.gz' | shasum --check - && tar xzf ruby-2.3.1.tar.gz
+cd ruby-2.3.1
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Get latest code
+
+```bash
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+```
+
+For GitLab Community Edition:
+
+```bash
+sudo -u git -H git checkout 8-12-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+sudo -u git -H git checkout 8-12-stable-ee
+```
+
+### 5. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v3.4.0
+```
+
+### 6. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. This requires
+[Go 1.5](https://golang.org/dl) which should already be on your system from
+GitLab 8.1.
+
+```bash
+cd /home/git/gitlab-workhorse
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout v0.7.11
+sudo -u git -H make
+```
+
+### 7. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Clean up assets and cache
+sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
+```
+
+### 8. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+git diff origin/8-11-stable:config/gitlab.yml.example origin/8-12-stable:config/gitlab.yml.example
+```
+
+#### Git configuration
+
+Configure Git to generate packfile bitmaps (introduced in Git 2.0) on
+the GitLab server during `git gc`.
+
+```sh
+sudo -u git -H git config --global repack.writeBitmaps true
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+# For HTTPS configurations
+git diff origin/8-11-stable:lib/support/nginx/gitlab-ssl origin/8-12-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-11-stable:lib/support/nginx/gitlab origin/8-12-stable:lib/support/nginx/gitlab
+```
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-12-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-12-stable/config/initializers/smtp_settings.rb.sample#L13?
+
+#### Init script
+
+Ensure you're still up-to-date with the latest init script changes:
+
+ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+### 9. Start application
+
+ sudo service gitlab start
+ sudo service nginx restart
+
+### 10. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+ sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+
+To make sure you didn't miss anything run a more thorough check:
+
+ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (8.11)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.10 to 8.11](8.10-to-8.11.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 7fe96e67dbb..c7fda8a497f 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -66,7 +66,7 @@ dependency to do so. Please see the [github-markup gem readme](https://github.co
## Newlines
> If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#newlines
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#newlines
GFM honors the markdown specification in how [paragraphs and line breaks are handled](https://daringfireball.net/projects/markdown/syntax#p).
@@ -86,7 +86,7 @@ Sugar is sweet
## Multiple underscores in words
> If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#multiple-underscores-in-words
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#multiple-underscores-in-words
It is not reasonable to italicize just _part_ of a word, especially when you're dealing with code and names that often appear with multiple underscores. Therefore, GFM ignores multiple underscores in words:
@@ -101,7 +101,7 @@ do_this_and_do_that_and_another_thing
## URL auto-linking
> If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#url-auto-linking
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#url-auto-linking
GFM will autolink almost any URL you copy and paste into your text:
@@ -122,7 +122,7 @@ GFM will autolink almost any URL you copy and paste into your text:
## Multiline Blockquote
> If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#multiline-blockquote
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#multiline-blockquote
On top of standard Markdown [blockquotes](#blockquotes), which require prepending `>` to quoted lines,
GFM supports multiline blockquotes fenced by <code>>>></code>:
@@ -156,7 +156,7 @@ you can quote that without having to manually prepend `>` to every line!
## Code and Syntax Highlighting
> If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#code-and-syntax-highlighting
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#code-and-syntax-highlighting
_GitLab uses the [Rouge Ruby library][rouge] for syntax highlighting. For a
list of supported languages visit the Rouge website._
@@ -226,7 +226,7 @@ But let's throw in a <b>tag</b>.
## Inline Diff
> If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#inline-diff
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#inline-diff
With inline diffs tags you can display {+ additions +} or [- deletions -].
@@ -242,7 +242,7 @@ However the wrapping tags cannot be mixed as such:
## Emoji
> If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#emoji
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#emoji
Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you:
@@ -307,7 +307,7 @@ GFM also recognizes certain cross-project references:
## Task Lists
> If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#task-lists
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#task-lists
You can add task lists to issues, merge requests and comments. To create a task list, add a specially-formatted Markdown list, like so:
@@ -330,7 +330,7 @@ Task lists can only be created in descriptions, not in titles. Task item state c
## Videos
> If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#videos
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#videos
Image tags with a video extension are automatically converted to a video player.
@@ -780,7 +780,7 @@ A link starting with a `/` is relative to the wiki root.
- The [Markdown Syntax Guide](https://daringfireball.net/projects/markdown/syntax) at Daring Fireball is an excellent resource for a detailed explanation of standard markdown.
- [Dillinger.io](http://dillinger.io) is a handy tool for testing standard markdown.
-[markdown.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md
+[markdown.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md
[rouge]: http://rouge.jneen.net/ "Rouge website"
[redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website"
[^1]: This link will be broken if you see this document from the Help page or docs.gitlab.com
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 66542861781..1498cb361c8 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -104,6 +104,15 @@ will find the option to flag the user as external.
By default new users are not set as external users. This behavior can be changed
by an administrator under **Admin > Application Settings**.
+## Project features
+
+Project features like wiki and issues can be hidden from users depending on
+which visibility level you select on project settings.
+
+- Disabled: disabled for everyone
+- Only team members: only team members will see even if your project is public or internal
+- Everyone with access: everyone can see depending on your project visibility level
+
## GitLab CI
GitLab CI permissions rely on the role the user has in GitLab. There are four
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index 9df9ed9c9da..cac926b3e28 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -31,10 +31,9 @@ Below is a table of the definitions used for GitLab's Issue Board.
There are three types of lists, the ones you create based on your labels, and
two default:
-- **Backlog** (default): shows all issues that do not fall in one of the other
- lists. Always appears on the very left.
-- **Done** (default): shows all closed issues. Always appears on the very right.
-- Label list: a list based on a label. It shows all issues with that label.
+- **Backlog** (default): shows all opened issues that do not fall in one of the other lists. Always appears on the very left.
+- **Done** (default): shows all closed issues that do not fall in one of the other lists. Always appears on the very right.
+- Label list: a list based on a label. It shows all opened or closed issues with that label.
![GitLab Issue Board](img/issue_board.png)
diff --git a/doc/user/project/koding.md b/doc/user/project/koding.md
index e54587fab68..c56a1efe3c2 100644
--- a/doc/user/project/koding.md
+++ b/doc/user/project/koding.md
@@ -68,7 +68,7 @@ GitLab instance. For details about what's next you can follow
[this guide](https://www.koding.com/docs/creating-an-aws-stack) from step 8.
Once stack initialized you will see the `README.md` content from your project
-in `Stack Build` wizard, this wizard will let you to build the stack and import
+in `Stack Build` wizard, this wizard will let you build the stack and import
your project into it. **Once it's completed it will automatically open the
related vm instead of importing from scratch**.
diff --git a/doc/user/project/merge_requests/resolve_conflicts.md b/doc/user/project/merge_requests/resolve_conflicts.md
index 44b76ffc8e6..4d7225bd820 100644
--- a/doc/user/project/merge_requests/resolve_conflicts.md
+++ b/doc/user/project/merge_requests/resolve_conflicts.md
@@ -26,6 +26,7 @@ this is similar to performing `git checkout feature; git merge master` locally.
GitLab allows resolving conflicts in a file where all of the below are true:
- The file is text, not binary
+- The file is in a UTF-8 compatible encoding
- The file does not already contain conflict markers
- The file, with conflict markers added, is not over 200 KB in size
- The file exists under the same path in both branches
diff --git a/doc/user/project/slash_commands.md b/doc/user/project/slash_commands.md
index 11e1574f772..1792a0c501d 100644
--- a/doc/user/project/slash_commands.md
+++ b/doc/user/project/slash_commands.md
@@ -26,5 +26,5 @@ do.
| `/done` | Mark todo as done |
| `/subscribe` | Subscribe |
| `/unsubscribe` | Unsubscribe |
-| `/due <in 2 days or this Friday or December 31st>` | Set due date |
+| <code>/due &lt;in 2 days &#124; this Friday &#124; December 31st&gt;</code> | Set due date |
| `/remove_due_date` | Remove due date |
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index 8119324bb62..7c0eb90d540 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -115,7 +115,7 @@ In this flow it is not common to have a production branch (or git flow master br
Merge or pull requests are created in a git management application and ask an assigned person to merge two branches.
Tools such as GitHub and Bitbucket choose the name pull request since the first manual action would be to pull the feature branch.
-Tools such as GitLab and Gitorious choose the name merge request since that is the final action that is requested of the assignee.
+Tools such as GitLab and others choose the name merge request since that is the final action that is requested of the assignee.
In this article we'll refer to them as merge requests.
If you work on a feature branch for more than a few hours it is good to share the intermediate result with the rest of the team.
diff --git a/doc/workflow/merge_requests.md b/doc/workflow/merge_requests.md
index 334a119e522..40a5e4476be 100644
--- a/doc/workflow/merge_requests.md
+++ b/doc/workflow/merge_requests.md
@@ -80,3 +80,11 @@ If you click the "Hide whitespace changes" button, you can see the diff without
It is also working on commits compare view.
![Commit Compare](merge_requests/commit_compare.png)
+
+## Merge Requests versions
+
+Every time you push to merge request branch, a new version of merge request diff
+is created. When you visit the merge request page you see latest version of changes.
+However you can select an older one from version dropdown
+
+![Merge Request Versions](merge_requests/versions.png)
diff --git a/doc/workflow/merge_requests/versions.png b/doc/workflow/merge_requests/versions.png
new file mode 100644
index 00000000000..c0a6dfe6806
--- /dev/null
+++ b/doc/workflow/merge_requests/versions.png
Binary files differ
diff --git a/doc/workflow/project_features.md b/doc/workflow/project_features.md
index a523b3facbe..f19e7df8c9a 100644
--- a/doc/workflow/project_features.md
+++ b/doc/workflow/project_features.md
@@ -32,4 +32,12 @@ Snippets are little bits of code or text.
This is a nice place to put code or text that is used semi-regularly within the project, but does not belong in source control.
-For example, a specific config file that is used by > the team that is only valid for the people that work on the code.
+For example, a specific config file that is used by the team that is only valid for the people that work on the code.
+
+## Git LFS
+
+>**Note:** Project-specific LFS setting was added on 8.12 and is available only to admins.
+
+Git Large File Storage allows you to easily manage large binary files with Git.
+With this setting admins can better control which projects are allowed to use
+LFS.
diff --git a/features/dashboard/todos.feature b/features/dashboard/todos.feature
index 42f5d6d2af7..0b23bbb7951 100644
--- a/features/dashboard/todos.feature
+++ b/features/dashboard/todos.feature
@@ -23,26 +23,6 @@ Feature: Dashboard Todos
Then I should see all todos marked as done
@javascript
- Scenario: I filter by project
- Given I filter by "Enterprise"
- Then I should not see todos
-
- @javascript
- Scenario: I filter by author
- Given I filter by "John Doe"
- Then I should not see todos related to "Mary Jane" in the list
-
- @javascript
- Scenario: I filter by type
- Given I filter by "Issue"
- Then I should not see todos related to "Merge Requests" in the list
-
- @javascript
- Scenario: I filter by action
- Given I filter by "Mentioned"
- Then I should not see todos related to "Assignments" in the list
-
- @javascript
Scenario: I click on a todo row
Given I click on the todo
Then I should be directed to the corresponding page
diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature
index 967f2edb243..5aa592e9067 100644
--- a/features/project/merge_requests.feature
+++ b/features/project/merge_requests.feature
@@ -24,7 +24,7 @@ Feature: Project Merge Requests
Scenario: I should see target branch when it is different from default
Given project "Shop" have "Bug NS-06" open merge request
When I visit project "Shop" merge requests page
- Then I should see "other_branch" branch
+ Then I should see "feature_conflict" branch
Scenario: I should not see the numbers of diverged commits if the branch is rebased on the target
Given project "Shop" have "Bug NS-07" open merge request with rebased branch
diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb
index f0d8d498e46..2f0941e4113 100644
--- a/features/steps/dashboard/new_project.rb
+++ b/features/steps/dashboard/new_project.rb
@@ -18,7 +18,6 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
expect(page).to have_link('GitHub')
expect(page).to have_link('Bitbucket')
expect(page).to have_link('GitLab.com')
- expect(page).to have_link('Gitorious.org')
expect(page).to have_link('Google Code')
expect(page).to have_link('Repo by URL')
end
diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb
index 60152d3da55..344b6fda9a6 100644
--- a/features/steps/dashboard/todos.rb
+++ b/features/steps/dashboard/todos.rb
@@ -3,7 +3,6 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
include SharedPaths
include SharedProject
include SharedUser
- include Select2Helper
step '"John Doe" is a developer of project "Shop"' do
project.team << [john_doe, :developer]
@@ -54,7 +53,8 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
page.within('.todos-pending-count') { expect(page).to have_content '0' }
expect(page).to have_content 'To do 0'
expect(page).to have_content 'Done 4'
- expect(page).not_to have_link project.name_with_namespace
+ expect(page).to have_content "You're all done!"
+ expect('.prepend-top-default').not_to have_link project.name_with_namespace
should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}"
should_not_see_todo "John Doe mentioned you on issue #{issue.to_reference}"
should_not_see_todo "John Doe assigned you issue #{issue.to_reference}"
@@ -79,19 +79,31 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
end
step 'I filter by "Enterprise"' do
- select2(enterprise.id, from: "#project_id")
+ click_button 'Project'
+ page.within '.dropdown-menu-project' do
+ click_link enterprise.name_with_namespace
+ end
end
step 'I filter by "John Doe"' do
- select2(john_doe.id, from: "#author_id")
+ click_button 'Author'
+ page.within '.dropdown-menu-author' do
+ click_link john_doe.username
+ end
end
step 'I filter by "Issue"' do
- select2('Issue', from: "#type")
+ click_button 'Type'
+ page.within '.dropdown-menu-type' do
+ click_link 'Issue'
+ end
end
step 'I filter by "Mentioned"' do
- select2("#{Todo::MENTIONED}", from: '#action_id')
+ click_button 'Action'
+ page.within '.dropdown-menu-action' do
+ click_link 'Mentioned'
+ end
end
step 'I should not see todos' do
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index 9778ff4a6c7..56b28949585 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -58,8 +58,8 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
expect(find('.merge-request-info')).not_to have_content "master"
end
- step 'I should see "other_branch" branch' do
- expect(page).to have_content "other_branch"
+ step 'I should see "feature_conflict" branch' do
+ expect(page).to have_content "feature_conflict"
end
step 'I should see "Bug NS-04" in merge requests' do
@@ -124,7 +124,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
source_project: project,
target_project: project,
source_branch: 'fix',
- target_branch: 'other_branch',
+ target_branch: 'feature_conflict',
author: project.users.first,
description: "# Description header"
)
diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb
index 76fefee9254..975c879149e 100644
--- a/features/steps/project/project.rb
+++ b/features/steps/project/project.rb
@@ -5,7 +5,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps
step 'change project settings' do
fill_in 'project_name_edit', with: 'NewName'
- uncheck 'project_issues_enabled'
+ select 'Disabled', from: 'project_project_feature_attributes_issues_access_level'
end
step 'I save project' do
diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb
index aa666a954bc..df9845ba569 100644
--- a/features/steps/shared/issuable.rb
+++ b/features/steps/shared/issuable.rb
@@ -179,7 +179,7 @@ module SharedIssuable
project = Project.find_by(name: from_project_name)
expect(page).to have_content(user_name)
- expect(page).to have_content("mentioned in #{issuable.class.to_s.titleize.downcase} #{issuable.to_reference(project)}")
+ expect(page).to have_content("Mentioned in #{issuable.class.to_s.titleize.downcase} #{issuable.to_reference(project)}")
end
def expect_sidebar_content(content)
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index 0b4920883b8..afbd8ef1233 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -15,7 +15,7 @@ module SharedProject
# Create a specific project called "Shop"
step 'I own project "Shop"' do
@project = Project.find_by(name: "Shop")
- @project ||= create(:project, name: "Shop", namespace: @user.namespace, snippets_enabled: true)
+ @project ||= create(:project, name: "Shop", namespace: @user.namespace)
@project.team << [@user, :master]
end
@@ -41,6 +41,8 @@ module SharedProject
step 'I own project "Forum"' do
@project = Project.find_by(name: "Forum")
@project ||= create(:project, name: "Forum", namespace: @user.namespace, path: 'forum_project')
+ @project.build_project_feature
+ @project.project_feature.save
@project.team << [@user, :master]
end
@@ -95,7 +97,7 @@ module SharedProject
step 'I should see project settings' do
expect(current_path).to eq edit_namespace_project_path(@project.namespace, @project)
expect(page).to have_content("Project name")
- expect(page).to have_content("Features")
+ expect(page).to have_content("Feature Visibility")
end
def current_project
diff --git a/lib/api/api.rb b/lib/api/api.rb
index ecbd5a6e2fa..e14464c1b0d 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -31,6 +31,7 @@ module API
mount ::API::AccessRequests
mount ::API::AwardEmoji
mount ::API::Branches
+ mount ::API::BroadcastMessages
mount ::API::Builds
mount ::API::CommitStatuses
mount ::API::Commits
@@ -67,5 +68,6 @@ module API
mount ::API::Triggers
mount ::API::Users
mount ::API::Variables
+ mount ::API::MergeRequestDiffs
end
end
diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb
index 2efe7e3adf3..7c22b17e4e5 100644
--- a/lib/api/award_emoji.rb
+++ b/lib/api/award_emoji.rb
@@ -54,7 +54,7 @@ module API
post endpoint do
required_attributes! [:name]
- not_found!('Award Emoji') unless can_read_awardable?
+ not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable?
award = awardable.create_award_emoji(params[:name], current_user)
@@ -92,6 +92,10 @@ module API
can?(current_user, ability, awardable)
end
+ def can_award_awardable?
+ awardable.user_can_award?(current_user, params[:name])
+ end
+
def awardable
@awardable ||=
begin
diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb
new file mode 100644
index 00000000000..fb2a4148011
--- /dev/null
+++ b/lib/api/broadcast_messages.rb
@@ -0,0 +1,99 @@
+module API
+ class BroadcastMessages < Grape::API
+ before { authenticate! }
+ before { authenticated_as_admin! }
+
+ resource :broadcast_messages do
+ helpers do
+ def find_message
+ BroadcastMessage.find(params[:id])
+ end
+ end
+
+ desc 'Get all broadcast messages' do
+ detail 'This feature was introduced in GitLab 8.12.'
+ success Entities::BroadcastMessage
+ end
+ params do
+ optional :page, type: Integer, desc: 'Current page number'
+ optional :per_page, type: Integer, desc: 'Number of messages per page'
+ end
+ get do
+ messages = BroadcastMessage.all
+
+ present paginate(messages), with: Entities::BroadcastMessage
+ end
+
+ desc 'Create a broadcast message' do
+ detail 'This feature was introduced in GitLab 8.12.'
+ success Entities::BroadcastMessage
+ end
+ params do
+ requires :message, type: String, desc: 'Message to display'
+ optional :starts_at, type: DateTime, desc: 'Starting time', default: -> { Time.zone.now }
+ optional :ends_at, type: DateTime, desc: 'Ending time', default: -> { 1.hour.from_now }
+ optional :color, type: String, desc: 'Background color'
+ optional :font, type: String, desc: 'Foreground color'
+ end
+ post do
+ create_params = declared(params, include_missing: false).to_h
+ message = BroadcastMessage.create(create_params)
+
+ if message.persisted?
+ present message, with: Entities::BroadcastMessage
+ else
+ render_validation_error!(message)
+ end
+ end
+
+ desc 'Get a specific broadcast message' do
+ detail 'This feature was introduced in GitLab 8.12.'
+ success Entities::BroadcastMessage
+ end
+ params do
+ requires :id, type: Integer, desc: 'Broadcast message ID'
+ end
+ get ':id' do
+ message = find_message
+
+ present message, with: Entities::BroadcastMessage
+ end
+
+ desc 'Update a broadcast message' do
+ detail 'This feature was introduced in GitLab 8.12.'
+ success Entities::BroadcastMessage
+ end
+ params do
+ requires :id, type: Integer, desc: 'Broadcast message ID'
+ optional :message, type: String, desc: 'Message to display'
+ optional :starts_at, type: DateTime, desc: 'Starting time'
+ optional :ends_at, type: DateTime, desc: 'Ending time'
+ optional :color, type: String, desc: 'Background color'
+ optional :font, type: String, desc: 'Foreground color'
+ end
+ put ':id' do
+ message = find_message
+ update_params = declared(params, include_missing: false).to_h
+
+ if message.update(update_params)
+ present message, with: Entities::BroadcastMessage
+ else
+ render_validation_error!(message)
+ end
+ end
+
+ desc 'Delete a broadcast message' do
+ detail 'This feature was introduced in GitLab 8.12.'
+ success Entities::BroadcastMessage
+ end
+ params do
+ requires :id, type: Integer, desc: 'Broadcast message ID'
+ end
+ delete ':id' do
+ message = find_message
+
+ present message.destroy, with: Entities::BroadcastMessage
+ end
+ end
+ end
+end
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index 4df6ca8333e..5e3c9563703 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -64,7 +64,7 @@ module API
ref = branches.first
end
- pipeline = @project.ensure_pipeline(commit.sha, ref, current_user)
+ pipeline = @project.ensure_pipeline(ref, commit.sha, current_user)
name = params[:name] || params[:context]
status = GenericCommitStatus.running_or_pending.find_by(pipeline: pipeline, name: name, ref: params[:ref])
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index aaeb3d4800b..3faba79415b 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -76,15 +76,23 @@ module API
expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group }
expose :name, :name_with_namespace
expose :path, :path_with_namespace
- expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :container_registry_enabled
+ expose :container_registry_enabled
+
+ # Expose old field names with the new permissions methods to keep API compatible
+ expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:user]) }
+ expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:user]) }
+ expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:user]) }
+ expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:user]) }
+ expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:user]) }
+
expose :created_at, :last_activity_at
- expose :shared_runners_enabled
+ expose :shared_runners_enabled, :lfs_enabled
expose :creator_id
expose :namespace
expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? }
expose :avatar_url
expose :star_count, :forks_count
- expose :open_issues_count, if: lambda { |project, options| project.issues_enabled? && project.default_issues_tracker? }
+ expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:user]) && project.default_issues_tracker? }
expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
expose :public_builds
expose :shared_with_groups do |project, options|
@@ -211,6 +219,7 @@ module API
expose :user_notes_count
expose :upvotes, :downvotes
expose :due_date
+ expose :confidential
expose :web_url do |issue, options|
Gitlab::UrlBuilder.build(issue)
@@ -232,6 +241,8 @@ module API
expose :milestone, using: Entities::Milestone
expose :merge_when_build_succeeds
expose :merge_status
+ expose :diff_head_sha, as: :sha
+ expose :merge_commit_sha
expose :subscribed do |merge_request, options|
merge_request.subscribed?(options[:current_user])
end
@@ -250,6 +261,19 @@ module API
end
end
+ class MergeRequestDiff < Grape::Entity
+ expose :id, :head_commit_sha, :base_commit_sha, :start_commit_sha,
+ :created_at, :merge_request_id, :state, :real_size
+ end
+
+ class MergeRequestDiffFull < MergeRequestDiff
+ expose :commits, using: Entities::RepoCommit
+
+ expose :diffs, using: Entities::RepoDiff do |compare, _|
+ compare.raw_diffs(all_diffs: true).to_a
+ end
+ end
+
class SSHKey < Grape::Entity
expose :id, :title, :key, :created_at
end
@@ -561,5 +585,10 @@ module API
class Template < Grape::Entity
expose :name, :content
end
+
+ class BroadcastMessage < Grape::Entity
+ expose :id, :message, :starts_at, :ends_at, :color, :font
+ expose :active?, as: :active
+ end
end
end
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 9d8b8d737a9..d2df77238d5 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -30,7 +30,7 @@ module API
# Example Request:
# POST /groups
post do
- authorize! :create_group, current_user
+ authorize! :create_group
required_attributes! [:name, :path]
attrs = attributes_for_keys [:name, :path, :description, :visibility_level]
@@ -97,7 +97,7 @@ module API
group = find_group(params[:id])
projects = GroupProjectsFinder.new(group).execute(current_user)
projects = paginate projects
- present projects, with: Entities::Project
+ present projects, with: Entities::Project, user: current_user
end
# Transfer a project to the Group namespace
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index da4b1bf9902..6a20ba95a79 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -129,7 +129,7 @@ module API
forbidden! unless current_user.is_admin?
end
- def authorize!(action, subject)
+ def authorize!(action, subject = nil)
forbidden! unless can?(current_user, action, subject)
end
@@ -148,7 +148,7 @@ module API
end
def can?(object, action, subject)
- abilities.allowed?(object, action, subject)
+ Ability.allowed?(object, action, subject)
end
# Checks the occurrences of required attributes, each attribute must be present in the params hash
@@ -408,14 +408,6 @@ module API
links.join(', ')
end
- def abilities
- @abilities ||= begin
- abilities = Six.new
- abilities << Ability
- abilities
- end
- end
-
def secret_token
File.read(Gitlab.config.gitlab_shell.secret_file).chomp
end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 5b54c11ef62..6e6efece7c4 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -105,15 +105,19 @@ module API
post '/two_factor_recovery_codes' do
status 200
- key = Key.find(params[:key_id])
- user = key.user
+ key = Key.find_by(id: params[:key_id])
+
+ unless key
+ return { 'success' => false, 'message' => 'Could not find the given key' }
+ end
- # Make sure this isn't a deploy key
- unless key.type.nil?
+ if key.is_a?(DeployKey)
return { success: false, message: 'Deploy keys cannot be used to retrieve recovery codes' }
end
- unless user.present?
+ user = key.user
+
+ unless user
return { success: false, message: 'Could not find a user for the given key' }
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 077258faee1..556684187d8 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -140,12 +140,13 @@ module API
# labels (optional) - The labels of an issue
# created_at (optional) - Date time string, ISO 8601 formatted
# due_date (optional) - Date time string in the format YEAR-MONTH-DAY
+ # confidential (optional) - Boolean parameter if the issue should be confidential
# Example Request:
# POST /projects/:id/issues
post ':id/issues' do
required_attributes! [:title]
- keys = [:title, :description, :assignee_id, :milestone_id, :due_date]
+ keys = [:title, :description, :assignee_id, :milestone_id, :due_date, :confidential]
keys << :created_at if current_user.admin? || user_project.owner == current_user
attrs = attributes_for_keys(keys)
@@ -154,21 +155,19 @@ module API
render_api_error!({ labels: errors }, 400)
end
- project = user_project
+ attrs[:labels] = params[:labels] if params[:labels]
- issue = ::Issues::CreateService.new(project, current_user, attrs.merge(request: request, api: true)).execute
+ # Convert and filter out invalid confidential flags
+ attrs['confidential'] = to_boolean(attrs['confidential'])
+ attrs.delete('confidential') if attrs['confidential'].nil?
+
+ issue = ::Issues::CreateService.new(user_project, current_user, attrs.merge(request: request, api: true)).execute
if issue.spam?
render_api_error!({ error: 'Spam detected' }, 400)
end
if issue.valid?
- # Find or create labels and attach to issue. Labels are valid because
- # we already checked its name, so there can't be an error here
- if params[:labels].present?
- issue.add_labels_by_names(params[:labels].split(','))
- end
-
present issue, with: Entities::Issue, current_user: current_user
else
render_validation_error!(issue)
@@ -188,12 +187,13 @@ module API
# state_event (optional) - The state event of an issue (close|reopen)
# updated_at (optional) - Date time string, ISO 8601 formatted
# due_date (optional) - Date time string in the format YEAR-MONTH-DAY
+ # confidential (optional) - Boolean parameter if the issue should be confidential
# Example Request:
# PUT /projects/:id/issues/:issue_id
put ':id/issues/:issue_id' do
issue = user_project.issues.find(params[:issue_id])
authorize! :update_issue, issue
- keys = [:title, :description, :assignee_id, :milestone_id, :state_event, :due_date]
+ keys = [:title, :description, :assignee_id, :milestone_id, :state_event, :due_date, :confidential]
keys << :updated_at if current_user.admin? || user_project.owner == current_user
attrs = attributes_for_keys(keys)
@@ -202,17 +202,15 @@ module API
render_api_error!({ labels: errors }, 400)
end
+ attrs[:labels] = params[:labels] if params[:labels]
+
+ # Convert and filter out invalid confidential flags
+ attrs['confidential'] = to_boolean(attrs['confidential'])
+ attrs.delete('confidential') if attrs['confidential'].nil?
+
issue = ::Issues::UpdateService.new(user_project, current_user, attrs).execute(issue)
if issue.valid?
- # Find or create labels and attach to issue. Labels are valid because
- # we already checked its name, so there can't be an error here
- if params[:labels] && can?(current_user, :admin_issue, user_project)
- issue.remove_labels
- # Create and add labels to the new created issue
- issue.add_labels_by_names(params[:labels].split(','))
- end
-
present issue, with: Entities::Issue, current_user: current_user
else
render_validation_error!(issue)
diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb
new file mode 100644
index 00000000000..07435d78468
--- /dev/null
+++ b/lib/api/merge_request_diffs.rb
@@ -0,0 +1,45 @@
+module API
+ # MergeRequestDiff API
+ class MergeRequestDiffs < Grape::API
+ before { authenticate! }
+
+ resource :projects do
+ desc 'Get a list of merge request diff versions' do
+ detail 'This feature was introduced in GitLab 8.12.'
+ success Entities::MergeRequestDiff
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ end
+
+ get ":id/merge_requests/:merge_request_id/versions" do
+ merge_request = user_project.merge_requests.
+ find(params[:merge_request_id])
+
+ authorize! :read_merge_request, merge_request
+ present merge_request.merge_request_diffs, with: Entities::MergeRequestDiff
+ end
+
+ desc 'Get a single merge request diff version' do
+ detail 'This feature was introduced in GitLab 8.12.'
+ success Entities::MergeRequestDiffFull
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ requires :version_id, type: Integer, desc: 'The ID of a merge request diff version'
+ end
+
+ get ":id/merge_requests/:merge_request_id/versions/:version_id" do
+ merge_request = user_project.merge_requests.
+ find(params[:merge_request_id])
+
+ authorize! :read_merge_request, merge_request
+ present merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull
+ end
+ end
+ end
+end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 71efd4f33ca..a1fd598414a 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -51,7 +51,7 @@ module API
@projects = current_user.viewable_starred_projects
@projects = filter_projects(@projects)
@projects = paginate @projects
- present @projects, with: Entities::Project
+ present @projects, with: Entities::Project, user: current_user
end
# Get all projects for admin user
@@ -105,6 +105,7 @@ module API
# visibility_level (optional) - 0 by default
# import_url (optional)
# public_builds (optional)
+ # lfs_enabled (optional)
# Example Request
# POST /projects
post do
@@ -124,7 +125,8 @@ module API
:visibility_level,
:import_url,
:public_builds,
- :only_allow_merge_if_build_succeeds]
+ :only_allow_merge_if_build_succeeds,
+ :lfs_enabled]
attrs = map_public_to_visibility_level(attrs)
@project = ::Projects::CreateService.new(current_user, attrs).execute
if @project.saved?
@@ -156,6 +158,7 @@ module API
# visibility_level (optional)
# import_url (optional)
# public_builds (optional)
+ # lfs_enabled (optional)
# Example Request
# POST /projects/user/:user_id
post "user/:user_id" do
@@ -174,7 +177,8 @@ module API
:visibility_level,
:import_url,
:public_builds,
- :only_allow_merge_if_build_succeeds]
+ :only_allow_merge_if_build_succeeds,
+ :lfs_enabled]
attrs = map_public_to_visibility_level(attrs)
@project = ::Projects::CreateService.new(user, attrs).execute
if @project.saved?
@@ -220,6 +224,7 @@ module API
# public (optional) - if true same as setting visibility_level = 20
# visibility_level (optional) - visibility level of a project
# public_builds (optional)
+ # lfs_enabled (optional)
# Example Request
# PUT /projects/:id
put ':id' do
@@ -237,7 +242,8 @@ module API
:public,
:visibility_level,
:public_builds,
- :only_allow_merge_if_build_succeeds]
+ :only_allow_merge_if_build_succeeds,
+ :lfs_enabled]
attrs = map_public_to_visibility_level(attrs)
authorize_admin_project
authorize! :rename_project, user_project if attrs[:name].present?
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 8a376d3c2a3..c440305ff0f 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -327,7 +327,7 @@ module API
# Example Request:
# GET /user
get do
- present @current_user, with: Entities::UserLogin
+ present @current_user, with: Entities::UserFull
end
# Get currently authenticated user's keys
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index f117fc3d37d..9fcd9a3f999 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -55,7 +55,7 @@ module Backup
bk_repos_path = File.join(path, '..', 'repositories.old.' + Time.now.to_i.to_s)
FileUtils.mv(path, bk_repos_path)
# This is expected from gitlab:check
- FileUtils.mkdir_p(path, mode: 2770)
+ FileUtils.mkdir_p(path, mode: 02770)
end
Project.find_each(batch_size: 1000) do |project|
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index d77a5e3ff09..16cd774c81a 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -18,10 +18,6 @@ module Banzai
@object_sym ||= object_name.to_sym
end
- def self.object_class_title
- @object_title ||= object_class.name.titleize
- end
-
# Public: Find references in text (like `!123` for merge requests)
#
# AnyReferenceFilter.references_in(text) do |match, id, project_ref, matches|
@@ -49,10 +45,6 @@ module Banzai
self.class.object_sym
end
- def object_class_title
- self.class.object_class_title
- end
-
def references_in(*args, &block)
self.class.references_in(*args, &block)
end
@@ -198,7 +190,7 @@ module Banzai
end
def object_link_title(object)
- "#{object_class_title}: #{object.title}"
+ object.title
end
def object_link_text(object, matches)
diff --git a/lib/banzai/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb
index bbb88c979cc..4358bf45549 100644
--- a/lib/banzai/filter/commit_range_reference_filter.rb
+++ b/lib/banzai/filter/commit_range_reference_filter.rb
@@ -35,7 +35,7 @@ module Banzai
end
def object_link_title(range)
- range.reference_title
+ nil
end
end
end
diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb
index 2ce1816672b..a26dd09c25a 100644
--- a/lib/banzai/filter/commit_reference_filter.rb
+++ b/lib/banzai/filter/commit_reference_filter.rb
@@ -28,10 +28,6 @@ module Banzai
only_path: context[:only_path])
end
- def object_link_title(commit)
- commit.link_title
- end
-
def object_link_text_extras(object, matches)
extras = super
diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb
index e258dc8e2bf..8f262ef3d8d 100644
--- a/lib/banzai/filter/label_reference_filter.rb
+++ b/lib/banzai/filter/label_reference_filter.rb
@@ -70,6 +70,11 @@ module Banzai
def unescape_html_entities(text)
CGI.unescapeHTML(text.to_s)
end
+
+ def object_link_title(object)
+ # use title of wrapped element instead
+ nil
+ end
end
end
end
diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb
index ca686c87d97..58fff496d00 100644
--- a/lib/banzai/filter/milestone_reference_filter.rb
+++ b/lib/banzai/filter/milestone_reference_filter.rb
@@ -59,6 +59,10 @@ module Banzai
html_safe
end
end
+
+ def object_link_title(object)
+ nil
+ end
end
end
end
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index bf058241cda..2d221290f7e 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -52,7 +52,7 @@ module Banzai
end
def reference_class(type)
- "gfm gfm-#{type}"
+ "gfm gfm-#{type} has-tooltip"
end
# Ensure that a :project key exists in context
diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb
index 6cf218aaa0d..e8e03e4a98f 100644
--- a/lib/banzai/reference_parser/base_parser.rb
+++ b/lib/banzai/reference_parser/base_parser.rb
@@ -211,7 +211,7 @@ module Banzai
end
def can?(user, permission, subject)
- Ability.abilities.allowed?(user, permission, subject)
+ Ability.allowed?(user, permission, subject)
end
def find_projects_for_hash_keys(hash)
diff --git a/lib/gitlab/badge/coverage/report.rb b/lib/gitlab/badge/coverage/report.rb
index 95d925dc7f3..9a0482306b7 100644
--- a/lib/gitlab/badge/coverage/report.rb
+++ b/lib/gitlab/badge/coverage/report.rb
@@ -12,9 +12,7 @@ module Gitlab
@ref = ref
@job = job
- @pipeline = @project.pipelines
- .latest_successful_for(@ref)
- .first
+ @pipeline = @project.pipelines.latest_successful_for(@ref)
end
def entity
diff --git a/lib/gitlab/ci/config/node/hidden_job.rb b/lib/gitlab/ci/config/node/hidden.rb
index 073044b66f8..fe4ee8a7fc6 100644
--- a/lib/gitlab/ci/config/node/hidden_job.rb
+++ b/lib/gitlab/ci/config/node/hidden.rb
@@ -5,11 +5,10 @@ module Gitlab
##
# Entry that represents a hidden CI/CD job.
#
- class HiddenJob < Entry
+ class Hidden < Entry
include Validatable
validations do
- validates :config, type: Hash
validates :config, presence: true
end
diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb
index 45865825e46..d10e80d1a7d 100644
--- a/lib/gitlab/ci/config/node/jobs.rb
+++ b/lib/gitlab/ci/config/node/jobs.rb
@@ -29,7 +29,7 @@ module Gitlab
def compose!(deps = nil)
super do
@config.each do |name, config|
- node = hidden?(name) ? Node::HiddenJob : Node::Job
+ node = hidden?(name) ? Node::Hidden : Node::Job
factory = Node::Factory.new(node)
.value(config || {})
diff --git a/lib/gitlab/conflict/file.rb b/lib/gitlab/conflict/file.rb
index 0a1fd27ced5..dff9e29c6a5 100644
--- a/lib/gitlab/conflict/file.rb
+++ b/lib/gitlab/conflict/file.rb
@@ -181,6 +181,17 @@ module Gitlab
sections: sections
}
end
+
+ # Don't try to print merge_request or repository.
+ def inspect
+ instance_variables = [:merge_file_result, :their_path, :our_path, :our_mode].map do |instance_variable|
+ value = instance_variable_get("@#{instance_variable}")
+
+ "#{instance_variable}=\"#{value}\""
+ end
+
+ "#<#{self.class} #{instance_variables.join(' ')}>"
+ end
end
end
end
diff --git a/lib/gitlab/conflict/parser.rb b/lib/gitlab/conflict/parser.rb
index 6eccded7872..2d4d55daeeb 100644
--- a/lib/gitlab/conflict/parser.rb
+++ b/lib/gitlab/conflict/parser.rb
@@ -13,10 +13,19 @@ module Gitlab
class UnmergeableFile < ParserError
end
+ class UnsupportedEncoding < ParserError
+ end
+
def parse(text, our_path:, their_path:, parent_file: nil)
raise UnmergeableFile if text.blank? # Typically a binary file
raise UnmergeableFile if text.length > 102400
+ begin
+ text.to_json
+ rescue Encoding::UndefinedConversionError
+ raise UnsupportedEncoding
+ end
+
line_obj_index = 0
line_old = 1
line_new = 1
diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb
index 9dc2602867e..bd681f03173 100644
--- a/lib/gitlab/contributions_calendar.rb
+++ b/lib/gitlab/contributions_calendar.rb
@@ -23,7 +23,6 @@ module Gitlab
dates.each do |date|
date_id = date.to_time.to_i.to_s
- @timestamps[date_id] = 0
day_events = events.find { |day_events| day_events["date"] == date }
if day_events
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 27acd817e51..12fbb78c53e 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -41,7 +41,7 @@ module Gitlab
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
domain_whitelist: Settings.gitlab['domain_whitelist'],
- import_sources: %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project],
+ import_sources: %w[github bitbucket gitlab google_code fogbugz git gitlab_project],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
diff --git a/lib/gitlab/diff/file_collection/merge_request.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb
index 4f946908e2f..36348b33943 100644
--- a/lib/gitlab/diff/file_collection/merge_request.rb
+++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb
@@ -1,14 +1,14 @@
module Gitlab
module Diff
module FileCollection
- class MergeRequest < Base
- def initialize(merge_request, diff_options:)
- @merge_request = merge_request
+ class MergeRequestDiff < Base
+ def initialize(merge_request_diff, diff_options:)
+ @merge_request_diff = merge_request_diff
- super(merge_request,
- project: merge_request.project,
+ super(merge_request_diff,
+ project: merge_request_diff.project,
diff_options: diff_options,
- diff_refs: merge_request.diff_refs)
+ diff_refs: merge_request_diff.diff_refs)
end
def diff_files
@@ -61,11 +61,11 @@ module Gitlab
end
def cacheable?
- @merge_request.merge_request_diff.present?
+ @merge_request_diff.present?
end
def cache_key
- [@merge_request.merge_request_diff, 'highlighted-diff-files', diff_options]
+ [@merge_request_diff, 'highlighted-diff-files', diff_options]
end
end
end
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index 02ffb43d89b..4fdc2f46be0 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -152,12 +152,14 @@ module Gitlab
end
def create_comments(issuable, comments)
- comments.each do |raw|
- begin
- comment = CommentFormatter.new(project, raw)
- issuable.notes.create!(comment.attributes)
- rescue => e
- errors << { type: :comment, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ ActiveRecord::Base.no_touching do
+ comments.each do |raw|
+ begin
+ comment = CommentFormatter.new(project, raw)
+ issuable.notes.create!(comment.attributes)
+ rescue => e
+ errors << { type: :comment, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ end
end
end
end
@@ -166,7 +168,7 @@ module Gitlab
unless project.wiki_enabled?
wiki = WikiFormatter.new(project)
gitlab_shell.import_repository(project.repository_storage_path, wiki.path_with_namespace, wiki.import_url)
- project.update_attribute(:wiki_enabled, true)
+ project.project.update_attribute(:wiki_access_level, ProjectFeature::ENABLED)
end
rescue Gitlab::Shell::Error => e
# GitHub error message when the wiki repo has not been created,
diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb
index 835ec858b35..07edbe37a13 100644
--- a/lib/gitlab/github_import/issue_formatter.rb
+++ b/lib/gitlab/github_import/issue_formatter.rb
@@ -12,7 +12,7 @@ module Gitlab
author_id: author_id,
assignee_id: assignee_id,
created_at: raw_data.created_at,
- updated_at: updated_at
+ updated_at: raw_data.updated_at
}
end
@@ -69,10 +69,6 @@ module Gitlab
def state
raw_data.state == 'closed' ? 'closed' : 'opened'
end
-
- def updated_at
- state == 'closed' ? raw_data.closed_at : raw_data.updated_at
- end
end
end
end
diff --git a/lib/gitlab/github_import/milestone_formatter.rb b/lib/gitlab/github_import/milestone_formatter.rb
index 53d4b3102d1..b2fa524cf5b 100644
--- a/lib/gitlab/github_import/milestone_formatter.rb
+++ b/lib/gitlab/github_import/milestone_formatter.rb
@@ -3,14 +3,14 @@ module Gitlab
class MilestoneFormatter < BaseFormatter
def attributes
{
- iid: number,
+ iid: raw_data.number,
project: project,
- title: title,
- description: description,
- due_date: due_date,
+ title: raw_data.title,
+ description: raw_data.description,
+ due_date: raw_data.due_on,
state: state,
- created_at: created_at,
- updated_at: updated_at
+ created_at: raw_data.created_at,
+ updated_at: raw_data.updated_at
}
end
@@ -20,33 +20,9 @@ module Gitlab
private
- def number
- raw_data.number
- end
-
- def title
- raw_data.title
- end
-
- def description
- raw_data.description
- end
-
- def due_date
- raw_data.due_on
- end
-
def state
raw_data.state == 'closed' ? 'closed' : 'active'
end
-
- def created_at
- raw_data.created_at
- end
-
- def updated_at
- state == 'closed' ? raw_data.closed_at : raw_data.updated_at
- end
end
end
end
diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb
index f4221003db5..e9725880c5e 100644
--- a/lib/gitlab/github_import/project_creator.rb
+++ b/lib/gitlab/github_import/project_creator.rb
@@ -11,18 +11,24 @@ module Gitlab
end
def execute
- ::Projects::CreateService.new(
+ project = ::Projects::CreateService.new(
current_user,
name: repo.name,
path: repo.name,
description: repo.description,
namespace_id: namespace.id,
- visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC,
+ visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : ApplicationSetting.current.default_project_visibility,
import_type: "github",
import_source: repo.full_name,
- import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@"),
- wiki_enabled: !repo.has_wiki? # If repo has wiki we'll import it later
+ import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@")
).execute
+
+ # If repo has wiki we'll import it later
+ if repo.has_wiki? && project
+ project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED)
+ end
+
+ project
end
end
end
diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb
index 04aa3664f64..d9d436d7490 100644
--- a/lib/gitlab/github_import/pull_request_formatter.rb
+++ b/lib/gitlab/github_import/pull_request_formatter.rb
@@ -20,7 +20,7 @@ module Gitlab
author_id: author_id,
assignee_id: assignee_id,
created_at: raw_data.created_at,
- updated_at: updated_at
+ updated_at: raw_data.updated_at
}
end
@@ -103,15 +103,6 @@ module Gitlab
'opened'
end
end
-
- def updated_at
- case state
- when 'merged' then raw_data.merged_at
- when 'closed' then raw_data.closed_at
- else
- raw_data.updated_at
- end
- end
end
end
end
diff --git a/lib/gitlab/gitorious_import.rb b/lib/gitlab/gitorious_import.rb
deleted file mode 100644
index 8d0132a744c..00000000000
--- a/lib/gitlab/gitorious_import.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-module Gitlab
- module GitoriousImport
- GITORIOUS_HOST = "https://gitorious.org"
- end
-end
diff --git a/lib/gitlab/gitorious_import/client.rb b/lib/gitlab/gitorious_import/client.rb
deleted file mode 100644
index 99fe5bdebfc..00000000000
--- a/lib/gitlab/gitorious_import/client.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-module Gitlab
- module GitoriousImport
- class Client
- attr_reader :repo_list
-
- def initialize(repo_list)
- @repo_list = repo_list
- end
-
- def authorize_url(redirect_uri)
- "#{GITORIOUS_HOST}/gitlab-import?callback_url=#{redirect_uri}"
- end
-
- def repos
- @repos ||= repo_names.map { |full_name| GitoriousImport::Repository.new(full_name) }
- end
-
- def repo(id)
- repos.find { |repo| repo.id == id }
- end
-
- private
-
- def repo_names
- repo_list.to_s.split(',').map(&:strip).reject(&:blank?)
- end
- end
- end
-end
diff --git a/lib/gitlab/gitorious_import/project_creator.rb b/lib/gitlab/gitorious_import/project_creator.rb
deleted file mode 100644
index 8e22aa9286d..00000000000
--- a/lib/gitlab/gitorious_import/project_creator.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module Gitlab
- module GitoriousImport
- class ProjectCreator
- attr_reader :repo, :namespace, :current_user
-
- def initialize(repo, namespace, current_user)
- @repo = repo
- @namespace = namespace
- @current_user = current_user
- end
-
- def execute
- ::Projects::CreateService.new(
- current_user,
- name: repo.name,
- path: repo.path,
- description: repo.description,
- namespace_id: namespace.id,
- visibility_level: Gitlab::VisibilityLevel::PUBLIC,
- import_type: "gitorious",
- import_source: repo.full_name,
- import_url: repo.import_url
- ).execute
- end
- end
- end
-end
diff --git a/lib/gitlab/gitorious_import/repository.rb b/lib/gitlab/gitorious_import/repository.rb
deleted file mode 100644
index c88f1ae358d..00000000000
--- a/lib/gitlab/gitorious_import/repository.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-module Gitlab
- module GitoriousImport
- Repository = Struct.new(:full_name) do
- def id
- Digest::SHA1.hexdigest(full_name)
- end
-
- def namespace
- segments.first
- end
-
- def path
- segments.last
- end
-
- def name
- path.titleize
- end
-
- def description
- ""
- end
-
- def import_url
- "#{GITORIOUS_HOST}/#{full_name}.git"
- end
-
- private
-
- def segments
- full_name.split('/')
- end
- end
- end
-end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 1da51043611..c2e8a1ca5dd 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -39,15 +39,12 @@ project_tree:
- :labels
- milestones:
- :events
+ - :project_feature
# Only include the following attributes for the models specified.
included_attributes:
project:
- :description
- - :issues_enabled
- - :merge_requests_enabled
- - :wiki_enabled
- - :snippets_enabled
- :visibility_level
- :archived
user:
@@ -72,4 +69,4 @@ methods:
statuses:
- :type
merge_request_diff:
- - :utf8_st_diffs \ No newline at end of file
+ - :utf8_st_diffs
diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb
index 59a05411fe9..94261b7eeed 100644
--- a/lib/gitlab/import_sources.rb
+++ b/lib/gitlab/import_sources.rb
@@ -14,13 +14,12 @@ module Gitlab
def options
{
- 'GitHub' => 'github',
- 'Bitbucket' => 'bitbucket',
- 'GitLab.com' => 'gitlab',
- 'Gitorious.org' => 'gitorious',
- 'Google Code' => 'google_code',
- 'FogBugz' => 'fogbugz',
- 'Repo by URL' => 'git',
+ 'GitHub' => 'github',
+ 'Bitbucket' => 'bitbucket',
+ 'GitLab.com' => 'gitlab',
+ 'Google Code' => 'google_code',
+ 'FogBugz' => 'fogbugz',
+ 'Repo by URL' => 'git',
'GitLab export' => 'gitlab_project'
}
end
diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
index b4493bf44d2..01c96a6fe96 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -4,6 +4,17 @@ module Gitlab
class RackMiddleware
CONTROLLER_KEY = 'action_controller.instance'
ENDPOINT_KEY = 'api.endpoint'
+ CONTENT_TYPES = {
+ 'text/html' => :html,
+ 'text/plain' => :txt,
+ 'application/json' => :json,
+ 'text/js' => :js,
+ 'application/atom+xml' => :atom,
+ 'image/png' => :png,
+ 'image/jpeg' => :jpeg,
+ 'image/gif' => :gif,
+ 'image/svg+xml' => :svg
+ }
def initialize(app)
@app = app
@@ -46,8 +57,15 @@ module Gitlab
end
def tag_controller(trans, env)
- controller = env[CONTROLLER_KEY]
- trans.action = "#{controller.class.name}##{controller.action_name}"
+ controller = env[CONTROLLER_KEY]
+ action = "#{controller.class.name}##{controller.action_name}"
+ suffix = CONTENT_TYPES[controller.content_type]
+
+ if suffix && suffix != :html
+ action += ".#{suffix}"
+ end
+
+ trans.action = action
end
def tag_endpoint(trans, env)
diff --git a/lib/gitlab/sentry.rb b/lib/gitlab/sentry.rb
new file mode 100644
index 00000000000..117fc508135
--- /dev/null
+++ b/lib/gitlab/sentry.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module Sentry
+ def self.enabled?
+ Rails.env.production? && current_application_settings.sentry_enabled?
+ end
+
+ def self.context(current_user = nil)
+ return unless self.enabled?
+
+ if current_user
+ Raven.user_context(
+ id: current_user.id,
+ email: current_user.email,
+ username: current_user.username,
+ )
+ end
+ end
+
+ def self.program_context
+ if Sidekiq.server?
+ 'sidekiq'
+ else
+ 'rails'
+ end
+ end
+ end
+end
diff --git a/spec/controllers/import/gitorious_controller_spec.rb b/spec/controllers/import/gitorious_controller_spec.rb
deleted file mode 100644
index 4ae2b78e11c..00000000000
--- a/spec/controllers/import/gitorious_controller_spec.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-require 'spec_helper'
-
-describe Import::GitoriousController do
- include ImportSpecHelper
-
- let(:user) { create(:user) }
-
- before do
- sign_in(user)
- end
-
- describe "GET new" do
- it "redirects to import endpoint on gitorious.org" do
- get :new
-
- expect(controller).to redirect_to("https://gitorious.org/gitlab-import?callback_url=http://test.host/import/gitorious/callback")
- end
- end
-
- describe "GET callback" do
- it "stores repo list in session" do
- get :callback, repos: 'foo/bar,baz/qux'
-
- expect(session[:gitorious_repos]).to eq('foo/bar,baz/qux')
- end
- end
-
- describe "GET status" do
- before do
- @repo = OpenStruct.new(full_name: 'asd/vim')
- end
-
- it "assigns variables" do
- @project = create(:project, import_type: 'gitorious', creator_id: user.id)
- stub_client(repos: [@repo])
-
- get :status
-
- expect(assigns(:already_added_projects)).to eq([@project])
- expect(assigns(:repos)).to eq([@repo])
- end
-
- it "does not show already added project" do
- @project = create(:project, import_type: 'gitorious', creator_id: user.id, import_source: 'asd/vim')
- stub_client(repos: [@repo])
-
- get :status
-
- expect(assigns(:already_added_projects)).to eq([@project])
- expect(assigns(:repos)).to eq([])
- end
- end
-
- describe "POST create" do
- before do
- @repo = Gitlab::GitoriousImport::Repository.new('asd/vim')
- end
-
- it "takes already existing namespace" do
- namespace = create(:namespace, name: "asd", owner: user)
- expect(Gitlab::GitoriousImport::ProjectCreator).
- to receive(:new).with(@repo, namespace, user).
- and_return(double(execute: true))
- stub_client(repo: @repo)
-
- post :create, format: :js
- end
- end
-end
diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb
index d0ad5e26dbd..2896636db5a 100644
--- a/spec/controllers/projects/boards/issues_controller_spec.rb
+++ b/spec/controllers/projects/boards/issues_controller_spec.rb
@@ -41,8 +41,8 @@ describe Projects::Boards::IssuesController do
context 'with unauthorized user' do
before do
- allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true)
- allow(Ability.abilities).to receive(:allowed?).with(user, :read_issue, project).and_return(false)
+ allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+ allow(Ability).to receive(:allowed?).with(user, :read_issue, project).and_return(false)
end
it 'returns a successful 403 response' do
diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb
index 9496636e3cc..d687dea3c3b 100644
--- a/spec/controllers/projects/boards/lists_controller_spec.rb
+++ b/spec/controllers/projects/boards/lists_controller_spec.rb
@@ -35,11 +35,11 @@ describe Projects::Boards::ListsController do
context 'with unauthorized user' do
before do
- allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true)
- allow(Ability.abilities).to receive(:allowed?).with(user, :read_list, project).and_return(false)
+ allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+ allow(Ability).to receive(:allowed?).with(user, :read_list, project).and_return(false)
end
- it 'returns a successful 403 response' do
+ it 'returns a forbidden 403 response' do
read_board_list user: user
expect(response).to have_http_status(403)
@@ -56,9 +56,9 @@ describe Projects::Boards::ListsController do
end
describe 'POST create' do
- let(:label) { create(:label, project: project, name: 'Development') }
-
context 'with valid params' do
+ let(:label) { create(:label, project: project, name: 'Development') }
+
it 'returns a successful 200 response' do
create_board_list user: user, label_id: label.id
@@ -73,20 +73,29 @@ describe Projects::Boards::ListsController do
end
context 'with invalid params' do
- it 'returns an error' do
- create_board_list user: user, label_id: nil
+ context 'when label is nil' do
+ it 'returns a not found 404 response' do
+ create_board_list user: user, label_id: nil
+
+ expect(response).to have_http_status(404)
+ end
+ end
- parsed_response = JSON.parse(response.body)
+ context 'when label that does not belongs to project' do
+ it 'returns a not found 404 response' do
+ label = create(:label, name: 'Development')
- expect(parsed_response['label']).to contain_exactly "can't be blank"
- expect(response).to have_http_status(422)
+ create_board_list user: user, label_id: label.id
+
+ expect(response).to have_http_status(404)
+ end
end
end
context 'with unauthorized user' do
- let(:label) { create(:label, project: project, name: 'Development') }
+ it 'returns a forbidden 403 response' do
+ label = create(:label, project: project, name: 'Development')
- it 'returns a successful 403 response' do
create_board_list user: guest, label_id: label.id
expect(response).to have_http_status(403)
@@ -122,7 +131,7 @@ describe Projects::Boards::ListsController do
end
context 'with invalid position' do
- it 'returns a unprocessable entity 422 response' do
+ it 'returns an unprocessable entity 422 response' do
move user: user, list: planning, position: 6
expect(response).to have_http_status(422)
@@ -138,7 +147,7 @@ describe Projects::Boards::ListsController do
end
context 'with unauthorized user' do
- it 'returns a successful 403 response' do
+ it 'returns a forbidden 403 response' do
move user: guest, list: planning, position: 6
expect(response).to have_http_status(403)
@@ -180,7 +189,7 @@ describe Projects::Boards::ListsController do
end
context 'with unauthorized user' do
- it 'returns a successful 403 response' do
+ it 'returns a forbidden 403 response' do
remove_board_list user: guest, list: planning
expect(response).to have_http_status(403)
@@ -213,7 +222,7 @@ describe Projects::Boards::ListsController do
end
context 'when board lists is not empty' do
- it 'returns a unprocessable entity 422 response' do
+ it 'returns an unprocessable entity 422 response' do
create(:list, board: board)
generate_default_board_lists user: user
@@ -223,7 +232,7 @@ describe Projects::Boards::ListsController do
end
context 'with unauthorized user' do
- it 'returns a successful 403 response' do
+ it 'returns a forbidden 403 response' do
generate_default_board_lists user: guest
expect(response).to have_http_status(403)
diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb
index 75a6d39e82c..6f6e608e1f3 100644
--- a/spec/controllers/projects/boards_controller_spec.rb
+++ b/spec/controllers/projects/boards_controller_spec.rb
@@ -23,8 +23,8 @@ describe Projects::BoardsController do
context 'with unauthorized user' do
before do
- allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true)
- allow(Ability.abilities).to receive(:allowed?).with(user, :read_board, project).and_return(false)
+ allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+ allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false)
end
it 'returns a successful 404 response' do
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 0836b71056c..16929767ddf 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -8,13 +8,13 @@ describe Projects::IssuesController do
describe "GET #index" do
context 'external issue tracker' do
it 'redirects to the external issue tracker' do
- external = double(issues_url: 'https://example.com/issues')
+ external = double(project_path: 'https://example.com/project')
allow(project).to receive(:external_issue_tracker).and_return(external)
controller.instance_variable_set(:@project, project)
get :index, namespace_id: project.namespace.path, project_id: project
- expect(response).to redirect_to('https://example.com/issues')
+ expect(response).to redirect_to('https://example.com/project')
end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index c64c2b075c5..a219400d75f 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -170,6 +170,35 @@ describe Projects::MergeRequestsController do
expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request])
expect(merge_request.reload.closed?).to be_truthy
end
+
+ it 'allows editing of a closed merge request' do
+ merge_request.close!
+
+ put :update,
+ namespace_id: project.namespace.path,
+ project_id: project.path,
+ id: merge_request.iid,
+ merge_request: {
+ title: 'New title'
+ }
+
+ expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request])
+ expect(merge_request.reload.title).to eq 'New title'
+ end
+
+ it 'does not allow to update target branch closed merge request' do
+ merge_request.close!
+
+ put :update,
+ namespace_id: project.namespace.path,
+ project_id: project.path,
+ id: merge_request.iid,
+ merge_request: {
+ target_branch: 'new_branch'
+ }
+
+ expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch }
+ end
end
end
diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb
index b8a28f43707..72a3ebf2ebd 100644
--- a/spec/controllers/projects/snippets_controller_spec.rb
+++ b/spec/controllers/projects/snippets_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Projects::SnippetsController do
- let(:project) { create(:project_empty_repo, :public, snippets_enabled: true) }
+ let(:project) { create(:project_empty_repo, :public) }
let(:user) { create(:user) }
let(:user2) { create(:user) }
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index f82d68a1816..fb84ba07d25 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -8,7 +8,6 @@ FactoryGirl.define do
path { name.downcase.gsub(/\s/, '_') }
namespace
creator
- snippets_enabled true
trait :public do
visibility_level Gitlab::VisibilityLevel::PUBLIC
@@ -27,6 +26,26 @@ FactoryGirl.define do
project.create_repository
end
end
+
+ # Nest Project Feature attributes
+ transient do
+ wiki_access_level ProjectFeature::ENABLED
+ builds_access_level ProjectFeature::ENABLED
+ snippets_access_level ProjectFeature::ENABLED
+ issues_access_level ProjectFeature::ENABLED
+ merge_requests_access_level ProjectFeature::ENABLED
+ end
+
+ after(:create) do |project, evaluator|
+ project.project_feature.
+ update_attributes(
+ wiki_access_level: evaluator.wiki_access_level,
+ builds_access_level: evaluator.builds_access_level,
+ snippets_access_level: evaluator.snippets_access_level,
+ issues_access_level: evaluator.issues_access_level,
+ merge_requests_access_level: evaluator.merge_requests_access_level,
+ )
+ end
end
# Project with empty repository
diff --git a/spec/features/admin/admin_system_info_spec.rb b/spec/features/admin/admin_system_info_spec.rb
index f4e5c26b519..1df972843e2 100644
--- a/spec/features/admin/admin_system_info_spec.rb
+++ b/spec/features/admin/admin_system_info_spec.rb
@@ -6,12 +6,49 @@ describe 'Admin System Info' do
end
describe 'GET /admin/system_info' do
- it 'shows system info page' do
- visit admin_system_info_path
+ let(:cpu) { double(:cpu, length: 2) }
+ let(:memory) { double(:memory, active_bytes: 4294967296, total_bytes: 17179869184) }
- expect(page).to have_content 'CPU'
- expect(page).to have_content 'Memory'
- expect(page).to have_content 'Disks'
+ context 'when all info is available' do
+ before do
+ allow(Vmstat).to receive(:cpu).and_return(cpu)
+ allow(Vmstat).to receive(:memory).and_return(memory)
+ visit admin_system_info_path
+ end
+
+ it 'shows system info page' do
+ expect(page).to have_content 'CPU 2 cores'
+ expect(page).to have_content 'Memory 4 GB / 16 GB'
+ expect(page).to have_content 'Disks'
+ end
+ end
+
+ context 'when CPU info is not available' do
+ before do
+ allow(Vmstat).to receive(:cpu).and_raise(Errno::ENOENT)
+ allow(Vmstat).to receive(:memory).and_return(memory)
+ visit admin_system_info_path
+ end
+
+ it 'shows system info page with no CPU info' do
+ expect(page).to have_content 'CPU Unable to collect CPU info'
+ expect(page).to have_content 'Memory 4 GB / 16 GB'
+ expect(page).to have_content 'Disks'
+ end
+ end
+
+ context 'when memory info is not available' do
+ before do
+ allow(Vmstat).to receive(:cpu).and_return(cpu)
+ allow(Vmstat).to receive(:memory).and_raise(Errno::ENOENT)
+ visit admin_system_info_path
+ end
+
+ it 'shows system info page with no CPU info' do
+ expect(page).to have_content 'CPU 2 cores'
+ expect(page).to have_content 'Memory Unable to collect memory info'
+ expect(page).to have_content 'Disks'
+ end
end
end
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 5d777895542..c6c2e2095df 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -110,6 +110,45 @@ describe 'Issue Boards', feature: true, js: true do
end
end
+ it 'search backlog list' do
+ page.within('#js-boards-seach') do
+ find('.form-control').set(issue1.title)
+ end
+
+ wait_for_vue_resource
+
+ expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0)
+ expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
+ expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0)
+ end
+
+ it 'search done list' do
+ page.within('#js-boards-seach') do
+ find('.form-control').set(issue8.title)
+ end
+
+ wait_for_vue_resource
+
+ expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0)
+ expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0)
+ expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
+ expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1)
+ end
+
+ it 'search list' do
+ page.within('#js-boards-seach') do
+ find('.form-control').set(issue5.title)
+ end
+
+ wait_for_vue_resource
+
+ expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0)
+ expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
+ expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0)
+ end
+
it 'allows user to delete board' do
page.within(find('.board:nth-child(2)')) do
find('.board-delete').click
@@ -143,14 +182,21 @@ 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('20')
+ expect(page.find('.board-header')).to have_content('56')
expect(page).to have_selector('.card', count: 20)
+ 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)
- expect(page.find('.board-header')).to have_content('40')
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)
+
+ expect(page).to have_selector('.card', count: 56)
+ expect(page).to have_content('Showing all issues')
end
end
@@ -162,32 +208,6 @@ describe 'Issue Boards', feature: true, js: true do
end
end
- it 'is searchable' do
- page.within(find('.board', match: :first)) do
- find('.form-control').set issue1.title
-
- wait_for_vue_resource(spinner: false)
-
- expect(page).to have_selector('.card', count: 1)
- end
- end
-
- it 'clears search' do
- page.within(find('.board', match: :first)) do
- find('.form-control').set issue1.title
-
- expect(page).to have_selector('.card', count: 1)
-
- find('.board-search-clear-btn').click
- end
-
- wait_for_vue_resource
-
- page.within(find('.board', match: :first)) do
- expect(page).to have_selector('.card', count: 6)
- end
- end
-
it 'moves issue from backlog into list' do
drag_to(list_to_index: 1)
@@ -466,13 +486,19 @@ 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('20')
+ expect(page.find('.board-header')).to have_content('51')
expect(page).to have_selector('.card', count: 20)
+ expect(page).to have_content('Showing 20 of 51 issues')
evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
- expect(page.find('.board-header')).to have_content('40')
expect(page).to have_selector('.card', count: 40)
+ expect(page).to have_content('Showing 40 of 51 issues')
+
+ evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
+
+ expect(page).to have_selector('.card', count: 51)
+ expect(page).to have_content('Showing all issues')
end
end
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index 6eb04cf74c5..79cc50bc18e 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -12,7 +12,6 @@ describe 'Awards Emoji', feature: true do
describe 'Click award emoji from issue#show' do
let!(:issue) do
create(:issue,
- author: @user,
assignee: @user,
project: project)
end
diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb
index e262f285868..0e9f814044e 100644
--- a/spec/features/issues/filter_issues_spec.rb
+++ b/spec/features/issues/filter_issues_spec.rb
@@ -8,6 +8,7 @@ describe 'Filter issues', feature: true do
let!(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) }
let!(:issue1) { create(:issue, project: project) }
+ let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
before do
project.team << [user, :master]
@@ -107,6 +108,15 @@ describe 'Filter issues', feature: true do
end
expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
end
+
+ it 'filters by wont fix labels' do
+ find('.dropdown-menu-labels a', text: label.title).click
+ page.within '.labels-filter' do
+ expect(page).to have_content wontfix.title
+ click_link wontfix.title
+ end
+ expect(find('.js-label-select .dropdown-toggle-text')).to have_content(wontfix.title)
+ end
end
describe 'Filter issues for assignee and label from issues#index' do
diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb
index e528aff4d41..fb0c4704285 100644
--- a/spec/features/issues/new_branch_button_spec.rb
+++ b/spec/features/issues/new_branch_button_spec.rb
@@ -20,7 +20,7 @@ feature 'Start new branch from an issue', feature: true do
context "when there is a referenced merge request" do
let(:note) do
create(:note, :on_issue, :system, project: project,
- note: "mentioned in !#{referenced_mr.iid}")
+ note: "Mentioned in !#{referenced_mr.iid}")
end
let(:referenced_mr) do
create(:merge_request, :simple, source_project: project, target_project: project,
diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb
index 930c36ade2b..759edf8ec80 100644
--- a/spec/features/merge_requests/conflicts_spec.rb
+++ b/spec/features/merge_requests/conflicts_spec.rb
@@ -43,7 +43,8 @@ feature 'Merge request conflict resolution', js: true, feature: true do
'conflict-too-large' => 'when the conflicts contain a large file',
'conflict-binary-file' => 'when the conflicts contain a binary file',
'conflict-contains-conflict-markers' => 'when the conflicts contain a file with ambiguous conflict markers',
- 'conflict-missing-side' => 'when the conflicts contain a file edited in one branch and deleted in another'
+ 'conflict-missing-side' => 'when the conflicts contain a file edited in one branch and deleted in another',
+ 'conflict-non-utf8' => 'when the conflicts contain a non-UTF-8 file',
}
UNRESOLVABLE_CONFLICTS.each do |source_branch, description|
diff --git a/spec/features/merge_requests/diff_notes_spec.rb b/spec/features/merge_requests/diff_notes_spec.rb
index a818679a874..06fad1007e8 100644
--- a/spec/features/merge_requests/diff_notes_spec.rb
+++ b/spec/features/merge_requests/diff_notes_spec.rb
@@ -147,6 +147,37 @@ feature 'Diff notes', js: true, feature: true do
end
end
+ context 'when the MR only supports legacy diff notes' do
+ before do
+ @merge_request.merge_request_diff.update_attributes(start_commit_sha: nil)
+ visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline')
+ end
+
+ context 'with a new line' do
+ it 'should allow commenting' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
+ end
+ end
+
+ context 'with an old line' do
+ it 'should allow commenting' do
+ should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
+ end
+ end
+
+ context 'with an unchanged line' do
+ it 'should allow commenting' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
+ end
+ end
+
+ context 'with a match line' do
+ it 'should not allow commenting' do
+ should_not_allow_commenting(find('.match', match: :first))
+ end
+ end
+ end
+
def should_allow_commenting(line_holder, diff_side = nil)
line = get_line_components(line_holder, diff_side)
line[:content].hover
diff --git a/spec/features/merge_requests/merge_request_versions_spec.rb b/spec/features/merge_requests/merge_request_versions_spec.rb
new file mode 100644
index 00000000000..577c910f11b
--- /dev/null
+++ b/spec/features/merge_requests/merge_request_versions_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+feature 'Merge Request versions', js: true, feature: true do
+ before do
+ login_as :admin
+ merge_request = create(:merge_request, importing: true)
+ merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+ merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+ project = merge_request.source_project
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'show the latest version of the diff' do
+ page.within '.mr-version-switch' do
+ expect(page).to have_content 'Version: latest'
+ end
+
+ expect(page).to have_content '8 changed files'
+ end
+
+ describe 'switch between versions' do
+ before do
+ page.within '.mr-version-switch' do
+ find('.btn-link').click
+ click_link '6f6d7e7e'
+ end
+ end
+
+ it 'should show older version' do
+ page.within '.mr-version-switch' do
+ expect(page).to have_content 'Version: 6f6d7e7e'
+ end
+
+ expect(page).to have_content '5 changed files'
+ end
+ end
+end
diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb
index 7a9edbbe339..f1c522155d3 100644
--- a/spec/features/notes_on_merge_requests_spec.rb
+++ b/spec/features/notes_on_merge_requests_spec.rb
@@ -141,7 +141,7 @@ describe 'Comments', feature: true do
let(:project2) { create(:project, :private) }
let(:issue) { create(:issue, project: project2) }
let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'markdown') }
- let!(:note) { create(:note_on_merge_request, :system, noteable: merge_request, project: project, note: "mentioned in #{issue.to_reference(project)}") }
+ let!(:note) { create(:note_on_merge_request, :system, noteable: merge_request, project: project, note: "Mentioned in #{issue.to_reference(project)}") }
it 'shows the system note' do
login_as :admin
diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb
new file mode 100644
index 00000000000..04058300570
--- /dev/null
+++ b/spec/features/projects/branches/download_buttons_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+feature 'Download buttons in branches page', feature: true do
+ given(:user) { create(:user) }
+ given(:role) { :developer }
+ given(:status) { 'success' }
+ given(:project) { create(:project) }
+
+ given(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ sha: project.commit('binary-encoding').sha,
+ ref: 'binary-encoding', # make sure the branch is in the 1st page!
+ status: status)
+ end
+
+ given!(:build) do
+ create(:ci_build, :success, :artifacts,
+ pipeline: pipeline,
+ status: pipeline.status,
+ name: 'build')
+ end
+
+ background do
+ login_as(user)
+ project.team << [user, role]
+ end
+
+ describe 'when checking branches' do
+ context 'with artifacts' do
+ before do
+ visit namespace_project_branches_path(project.namespace, project)
+ end
+
+ scenario 'shows download artifacts button' do
+ expect(page).to have_link "Download '#{build.name}'"
+ end
+ end
+ end
+end
diff --git a/spec/features/builds_spec.rb b/spec/features/projects/builds_spec.rb
index 0cfeb2e57d8..d5d571e49bc 100644
--- a/spec/features/builds_spec.rb
+++ b/spec/features/projects/builds_spec.rb
@@ -1,4 +1,5 @@
require 'spec_helper'
+require 'tempfile'
describe "Builds" do
let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
@@ -6,7 +7,7 @@ describe "Builds" do
before do
login_as(:user)
@commit = FactoryGirl.create :ci_pipeline
- @build = FactoryGirl.create :ci_build, pipeline: @commit
+ @build = FactoryGirl.create :ci_build, :trace, pipeline: @commit
@build2 = FactoryGirl.create :ci_build
@project = @commit.project
@project.team << [@user, :developer]
@@ -156,7 +157,6 @@ describe "Builds" do
context 'Build raw trace' do
before do
@build.run!
- @build.trace = 'BUILD TRACE'
visit namespace_project_build_path(@project.namespace, @project, @build)
end
@@ -255,35 +255,101 @@ describe "Builds" do
end
end
- describe "GET /:project/builds/:id/raw" do
- context "Build from project" do
- before do
- Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
- @build.run!
- @build.trace = 'BUILD TRACE'
- visit namespace_project_build_path(@project.namespace, @project, @build)
- page.within('.js-build-sidebar') { click_link 'Raw' }
+ describe 'GET /:project/builds/:id/raw' do
+ context 'access source' do
+ context 'build from project' do
+ before do
+ Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+ @build.run!
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ page.within('.js-build-sidebar') { click_link 'Raw' }
+ end
+
+ it 'sends the right headers' do
+ expect(page.status_code).to eq(200)
+ expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace)
+ end
end
- it 'sends the right headers' do
- expect(page.status_code).to eq(200)
- expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
- expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace)
+ context 'build from other project' do
+ before do
+ Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+ @build2.run!
+ visit raw_namespace_project_build_path(@project.namespace, @project, @build2)
+ end
+
+ it 'sends the right headers' do
+ expect(page.status_code).to eq(404)
+ end
end
end
- context "Build from other project" do
- before do
- Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
- @build2.run!
- @build2.trace = 'BUILD TRACE'
- visit raw_namespace_project_build_path(@project.namespace, @project, @build2)
- puts page.status_code
- puts current_url
+ context 'storage form' do
+ let(:existing_file) { Tempfile.new('existing-trace-file').path }
+ let(:non_existing_file) do
+ file = Tempfile.new('non-existing-trace-file')
+ path = file.path
+ file.unlink
+ path
end
- it 'sends the right headers' do
- expect(page.status_code).to eq(404)
+ context 'when build has trace in file' do
+ before do
+ Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+ @build.run!
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+
+ allow_any_instance_of(Project).to receive(:ci_id).and_return(nil)
+ allow_any_instance_of(Ci::Build).to receive(:path_to_trace).and_return(existing_file)
+ allow_any_instance_of(Ci::Build).to receive(:old_path_to_trace).and_return(non_existing_file)
+
+ page.within('.js-build-sidebar') { click_link 'Raw' }
+ end
+
+ it 'sends the right headers' do
+ expect(page.status_code).to eq(200)
+ expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(page.response_headers['X-Sendfile']).to eq(existing_file)
+ end
+ end
+
+ context 'when build has trace in old file' do
+ before do
+ Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+ @build.run!
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+
+ allow_any_instance_of(Project).to receive(:ci_id).and_return(999)
+ allow_any_instance_of(Ci::Build).to receive(:path_to_trace).and_return(non_existing_file)
+ allow_any_instance_of(Ci::Build).to receive(:old_path_to_trace).and_return(existing_file)
+
+ page.within('.js-build-sidebar') { click_link 'Raw' }
+ end
+
+ it 'sends the right headers' do
+ expect(page.status_code).to eq(200)
+ expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(page.response_headers['X-Sendfile']).to eq(existing_file)
+ end
+ end
+
+ context 'when build has trace in DB' do
+ before do
+ Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+ @build.run!
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+
+ allow_any_instance_of(Project).to receive(:ci_id).and_return(nil)
+ allow_any_instance_of(Ci::Build).to receive(:path_to_trace).and_return(non_existing_file)
+ allow_any_instance_of(Ci::Build).to receive(:old_path_to_trace).and_return(non_existing_file)
+
+ page.within('.js-build-sidebar') { click_link 'Raw' }
+ end
+
+ it 'sends the right headers' do
+ expect(page.status_code).to eq(404)
+ end
end
end
end
diff --git a/spec/features/projects/edit_spec.rb b/spec/features/projects/edit_spec.rb
new file mode 100644
index 00000000000..a1643fd1f43
--- /dev/null
+++ b/spec/features/projects/edit_spec.rb
@@ -0,0 +1,57 @@
+require 'rails_helper'
+
+feature 'Project edit', feature: true, js: true do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+
+ visit edit_namespace_project_path(project.namespace, project)
+ end
+
+ context 'feature visibility' do
+ context 'merge requests select' do
+ it 'hides merge requests section' do
+ select('Disabled', from: 'project_project_feature_attributes_merge_requests_access_level')
+
+ expect(page).to have_selector('.merge-requests-feature', visible: false)
+ end
+
+ it 'hides merge requests section after save' do
+ select('Disabled', from: 'project_project_feature_attributes_merge_requests_access_level')
+
+ expect(page).to have_selector('.merge-requests-feature', visible: false)
+
+ click_button 'Save changes'
+
+ wait_for_ajax
+
+ expect(page).to have_selector('.merge-requests-feature', visible: false)
+ end
+ end
+
+ context 'builds select' do
+ it 'hides merge requests section' do
+ select('Disabled', from: 'project_project_feature_attributes_builds_access_level')
+
+ expect(page).to have_selector('.builds-feature', visible: false)
+ end
+
+ it 'hides merge requests section after save' do
+ select('Disabled', from: 'project_project_feature_attributes_builds_access_level')
+
+ expect(page).to have_selector('.builds-feature', visible: false)
+
+ click_button 'Save changes'
+
+ wait_for_ajax
+
+ expect(page).to have_selector('.builds-feature', visible: false)
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
new file mode 100644
index 00000000000..9b487e350f2
--- /dev/null
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -0,0 +1,122 @@
+require 'spec_helper'
+include WaitForAjax
+
+describe 'Edit Project Settings', feature: true do
+ let(:member) { create(:user) }
+ let!(:project) { create(:project, :public, path: 'gitlab', name: 'sample') }
+ let(:non_member) { create(:user) }
+
+ describe 'project features visibility selectors', js: true do
+ before do
+ project.team << [member, :master]
+ login_as(member)
+ end
+
+ tools = { builds: "pipelines", issues: "issues", wiki: "wiki", snippets: "snippets", merge_requests: "merge_requests" }
+
+ tools.each do |tool_name, shortcut_name|
+ describe "feature #{tool_name}" do
+ it 'toggles visibility' do
+ visit edit_namespace_project_path(project.namespace, project)
+
+ select 'Disabled', from: "project_project_feature_attributes_#{tool_name}_access_level"
+ click_button 'Save changes'
+ wait_for_ajax
+ expect(page).not_to have_selector(".shortcuts-#{shortcut_name}")
+
+ select 'Everyone with access', from: "project_project_feature_attributes_#{tool_name}_access_level"
+ click_button 'Save changes'
+ wait_for_ajax
+ expect(page).to have_selector(".shortcuts-#{shortcut_name}")
+
+ select 'Only team members', from: "project_project_feature_attributes_#{tool_name}_access_level"
+ click_button 'Save changes'
+ wait_for_ajax
+ expect(page).to have_selector(".shortcuts-#{shortcut_name}")
+
+ sleep 0.1
+ end
+ end
+ end
+ end
+
+ describe 'project features visibility pages' do
+ before do
+ @tools =
+ {
+ builds: namespace_project_pipelines_path(project.namespace, project),
+ issues: namespace_project_issues_path(project.namespace, project),
+ wiki: namespace_project_wiki_path(project.namespace, project, :home),
+ snippets: namespace_project_snippets_path(project.namespace, project),
+ merge_requests: namespace_project_merge_requests_path(project.namespace, project),
+ }
+ end
+
+ context 'normal user' do
+ it 'renders 200 if tool is enabled' do
+ @tools.each do |method_name, url|
+ project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::ENABLED)
+ visit url
+ expect(page.status_code).to eq(200)
+ end
+ end
+
+ it 'renders 404 if feature is disabled' do
+ @tools.each do |method_name, url|
+ project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::DISABLED)
+ visit url
+ expect(page.status_code).to eq(404)
+ end
+ end
+
+ it 'renders 404 if feature is enabled only for team members' do
+ project.team.truncate
+
+ @tools.each do |method_name, url|
+ project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE)
+ visit url
+ expect(page.status_code).to eq(404)
+ end
+ end
+
+ it 'renders 200 if users is member of group' do
+ group = create(:group)
+ project.group = group
+ project.save
+
+ group.add_owner(member)
+
+ @tools.each do |method_name, url|
+ project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE)
+ visit url
+ expect(page.status_code).to eq(200)
+ end
+ end
+ end
+
+ context 'admin user' do
+ before do
+ non_member.update_attribute(:admin, true)
+ login_as(non_member)
+ end
+
+ it 'renders 404 if feature is disabled' do
+ @tools.each do |method_name, url|
+ project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::DISABLED)
+ visit url
+ expect(page.status_code).to eq(404)
+ end
+ end
+
+ it 'renders 200 if feature is enabled only for team members' do
+ project.team.truncate
+
+ @tools.each do |method_name, url|
+ project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE)
+ visit url
+ expect(page.status_code).to eq(200)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/files/download_buttons_spec.rb b/spec/features/projects/files/download_buttons_spec.rb
new file mode 100644
index 00000000000..be5cebcd7c9
--- /dev/null
+++ b/spec/features/projects/files/download_buttons_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+feature 'Download buttons in files tree', feature: true do
+ given(:user) { create(:user) }
+ given(:role) { :developer }
+ given(:status) { 'success' }
+ given(:project) { create(:project) }
+
+ given(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ sha: project.commit.sha,
+ ref: project.default_branch,
+ status: status)
+ end
+
+ given!(:build) do
+ create(:ci_build, :success, :artifacts,
+ pipeline: pipeline,
+ status: pipeline.status,
+ name: 'build')
+ end
+
+ background do
+ login_as(user)
+ project.team << [user, role]
+ end
+
+ describe 'when files tree' do
+ context 'with artifacts' do
+ before do
+ visit namespace_project_tree_path(
+ project.namespace, project, project.default_branch)
+ end
+
+ scenario 'shows download artifacts button' do
+ expect(page).to have_link "Download '#{build.name}'"
+ end
+ end
+ end
+end
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 7bb0d26b21c..e14b2705704 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/main/download_buttons_spec.rb b/spec/features/projects/main/download_buttons_spec.rb
new file mode 100644
index 00000000000..b26c0ea7a14
--- /dev/null
+++ b/spec/features/projects/main/download_buttons_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+feature 'Download buttons in project main page', feature: true do
+ given(:user) { create(:user) }
+ given(:role) { :developer }
+ given(:status) { 'success' }
+ given(:project) { create(:project) }
+
+ given(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ sha: project.commit.sha,
+ ref: project.default_branch,
+ status: status)
+ end
+
+ given!(:build) do
+ create(:ci_build, :success, :artifacts,
+ pipeline: pipeline,
+ status: pipeline.status,
+ name: 'build')
+ end
+
+ background do
+ login_as(user)
+ project.team << [user, role]
+ end
+
+ describe 'when checking project main page' do
+ context 'with artifacts' do
+ before do
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ scenario 'shows download artifacts button' do
+ expect(page).to have_link "Download '#{build.name}'"
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/tags/download_buttons_spec.rb b/spec/features/projects/tags/download_buttons_spec.rb
new file mode 100644
index 00000000000..6e0022c179f
--- /dev/null
+++ b/spec/features/projects/tags/download_buttons_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+feature 'Download buttons in tags page', feature: true do
+ given(:user) { create(:user) }
+ given(:role) { :developer }
+ given(:status) { 'success' }
+ given(:tag) { 'v1.0.0' }
+ given(:project) { create(:project) }
+
+ given(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ sha: project.commit(tag).sha,
+ ref: tag,
+ status: status)
+ end
+
+ given!(:build) do
+ create(:ci_build, :success, :artifacts,
+ pipeline: pipeline,
+ status: pipeline.status,
+ name: 'build')
+ end
+
+ background do
+ login_as(user)
+ project.team << [user, role]
+ end
+
+ describe 'when checking tags' do
+ context 'with artifacts' do
+ before do
+ visit namespace_project_tags_path(project.namespace, project)
+ end
+
+ scenario 'shows download artifacts button' do
+ expect(page).to have_link "Download '#{build.name}'"
+ end
+ end
+ end
+end
diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb
index 6ed279ef9be..abb27c90e0a 100644
--- a/spec/features/task_lists_spec.rb
+++ b/spec/features/task_lists_spec.rb
@@ -20,6 +20,22 @@ feature 'Task Lists', feature: true do
MARKDOWN
end
+ let(:singleIncompleteMarkdown) do
+ <<-MARKDOWN.strip_heredoc
+ This is a task list:
+
+ - [ ] Incomplete entry 1
+ MARKDOWN
+ end
+
+ let(:singleCompleteMarkdown) do
+ <<-MARKDOWN.strip_heredoc
+ This is a task list:
+
+ - [x] Incomplete entry 1
+ MARKDOWN
+ end
+
before do
Warden.test_mode!
@@ -34,77 +50,145 @@ feature 'Task Lists', feature: true do
end
describe 'for Issues' do
- let!(:issue) { create(:issue, description: markdown, author: user, project: project) }
+ describe 'multiple tasks' do
+ let!(:issue) { create(:issue, description: markdown, author: user, project: project) }
- it 'renders' do
- visit_issue(project, issue)
+ it 'renders' do
+ visit_issue(project, issue)
- expect(page).to have_selector('ul.task-list', count: 1)
- expect(page).to have_selector('li.task-list-item', count: 6)
- expect(page).to have_selector('ul input[checked]', count: 2)
- end
+ expect(page).to have_selector('ul.task-list', count: 1)
+ expect(page).to have_selector('li.task-list-item', count: 6)
+ expect(page).to have_selector('ul input[checked]', count: 2)
+ end
+
+ it 'contains the required selectors' do
+ visit_issue(project, issue)
+
+ container = '.detail-page-description .description.js-task-list-container'
- it 'contains the required selectors' do
- visit_issue(project, issue)
+ expect(page).to have_selector(container)
+ expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox")
+ expect(page).to have_selector("#{container} .js-task-list-field")
+ expect(page).to have_selector('form.js-issuable-update')
+ expect(page).to have_selector('a.btn-close')
+ end
- container = '.detail-page-description .description.js-task-list-container'
+ it 'is only editable by author' do
+ visit_issue(project, issue)
+ expect(page).to have_selector('.js-task-list-container')
- expect(page).to have_selector(container)
- expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox")
- expect(page).to have_selector("#{container} .js-task-list-field")
- expect(page).to have_selector('form.js-issuable-update')
- expect(page).to have_selector('a.btn-close')
+ logout(:user)
+
+ login_as(user2)
+ visit current_path
+ expect(page).not_to have_selector('.js-task-list-container')
+ end
+
+ it 'provides a summary on Issues#index' do
+ visit namespace_project_issues_path(project.namespace, project)
+ expect(page).to have_content("2 of 6 tasks completed")
+ end
end
- it 'is only editable by author' do
- visit_issue(project, issue)
- expect(page).to have_selector('.js-task-list-container')
+ describe 'single incomplete task' do
+ let!(:issue) { create(:issue, description: singleIncompleteMarkdown, author: user, project: project) }
- logout(:user)
+ it 'renders' do
+ visit_issue(project, issue)
- login_as(user2)
- visit current_path
- expect(page).not_to have_selector('.js-task-list-container')
+ expect(page).to have_selector('ul.task-list', count: 1)
+ expect(page).to have_selector('li.task-list-item', count: 1)
+ expect(page).to have_selector('ul input[checked]', count: 0)
+ end
+
+ it 'provides a summary on Issues#index' do
+ visit namespace_project_issues_path(project.namespace, project)
+ expect(page).to have_content("0 of 1 task completed")
+ end
end
- it 'provides a summary on Issues#index' do
- visit namespace_project_issues_path(project.namespace, project)
- expect(page).to have_content("6 tasks (2 completed, 4 remaining)")
+ describe 'single complete task' do
+ let!(:issue) { create(:issue, description: singleCompleteMarkdown, author: user, project: project) }
+
+ it 'renders' do
+ visit_issue(project, issue)
+
+ expect(page).to have_selector('ul.task-list', count: 1)
+ expect(page).to have_selector('li.task-list-item', count: 1)
+ expect(page).to have_selector('ul input[checked]', count: 1)
+ end
+
+ it 'provides a summary on Issues#index' do
+ visit namespace_project_issues_path(project.namespace, project)
+ expect(page).to have_content("1 of 1 task completed")
+ end
end
end
describe 'for Notes' do
let!(:issue) { create(:issue, author: user, project: project) }
- let!(:note) do
- create(:note, note: markdown, noteable: issue,
- project: project, author: user)
+ describe 'multiple tasks' do
+ let!(:note) do
+ create(:note, note: markdown, noteable: issue,
+ project: project, author: user)
+ end
+
+ it 'renders for note body' do
+ visit_issue(project, issue)
+
+ expect(page).to have_selector('.note ul.task-list', count: 1)
+ expect(page).to have_selector('.note li.task-list-item', count: 6)
+ expect(page).to have_selector('.note ul input[checked]', count: 2)
+ end
+
+ it 'contains the required selectors' do
+ visit_issue(project, issue)
+
+ expect(page).to have_selector('.note .js-task-list-container')
+ expect(page).to have_selector('.note .js-task-list-container .task-list .task-list-item .task-list-item-checkbox')
+ expect(page).to have_selector('.note .js-task-list-container .js-task-list-field')
+ end
+
+ it 'is only editable by author' do
+ visit_issue(project, issue)
+ expect(page).to have_selector('.js-task-list-container')
+
+ logout(:user)
+
+ login_as(user2)
+ visit current_path
+ expect(page).not_to have_selector('.js-task-list-container')
+ end
end
- it 'renders for note body' do
- visit_issue(project, issue)
-
- expect(page).to have_selector('.note ul.task-list', count: 1)
- expect(page).to have_selector('.note li.task-list-item', count: 6)
- expect(page).to have_selector('.note ul input[checked]', count: 2)
- end
+ describe 'single incomplete task' do
+ let!(:note) do
+ create(:note, note: singleIncompleteMarkdown, noteable: issue,
+ project: project, author: user)
+ end
- it 'contains the required selectors' do
- visit_issue(project, issue)
+ it 'renders for note body' do
+ visit_issue(project, issue)
- expect(page).to have_selector('.note .js-task-list-container')
- expect(page).to have_selector('.note .js-task-list-container .task-list .task-list-item .task-list-item-checkbox')
- expect(page).to have_selector('.note .js-task-list-container .js-task-list-field')
+ expect(page).to have_selector('.note ul.task-list', count: 1)
+ expect(page).to have_selector('.note li.task-list-item', count: 1)
+ expect(page).to have_selector('.note ul input[checked]', count: 0)
+ end
end
- it 'is only editable by author' do
- visit_issue(project, issue)
- expect(page).to have_selector('.js-task-list-container')
+ describe 'single complete task' do
+ let!(:note) do
+ create(:note, note: singleCompleteMarkdown, noteable: issue,
+ project: project, author: user)
+ end
- logout(:user)
+ it 'renders for note body' do
+ visit_issue(project, issue)
- login_as(user2)
- visit current_path
- expect(page).not_to have_selector('.js-task-list-container')
+ expect(page).to have_selector('.note ul.task-list', count: 1)
+ expect(page).to have_selector('.note li.task-list-item', count: 1)
+ expect(page).to have_selector('.note ul input[checked]', count: 1)
+ end
end
end
@@ -113,42 +197,78 @@ feature 'Task Lists', feature: true do
visit namespace_project_merge_request_path(project.namespace, project, merge)
end
- let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) }
+ describe 'multiple tasks' do
+ let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) }
- it 'renders for description' do
- visit_merge_request(project, merge)
+ it 'renders for description' do
+ visit_merge_request(project, merge)
- expect(page).to have_selector('ul.task-list', count: 1)
- expect(page).to have_selector('li.task-list-item', count: 6)
- expect(page).to have_selector('ul input[checked]', count: 2)
- end
+ expect(page).to have_selector('ul.task-list', count: 1)
+ expect(page).to have_selector('li.task-list-item', count: 6)
+ expect(page).to have_selector('ul input[checked]', count: 2)
+ end
- it 'contains the required selectors' do
- visit_merge_request(project, merge)
+ it 'contains the required selectors' do
+ visit_merge_request(project, merge)
- container = '.detail-page-description .description.js-task-list-container'
+ container = '.detail-page-description .description.js-task-list-container'
- expect(page).to have_selector(container)
- expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox")
- expect(page).to have_selector("#{container} .js-task-list-field")
- expect(page).to have_selector('form.js-issuable-update')
- expect(page).to have_selector('a.btn-close')
- end
+ expect(page).to have_selector(container)
+ expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox")
+ expect(page).to have_selector("#{container} .js-task-list-field")
+ expect(page).to have_selector('form.js-issuable-update')
+ expect(page).to have_selector('a.btn-close')
+ end
- it 'is only editable by author' do
- visit_merge_request(project, merge)
- expect(page).to have_selector('.js-task-list-container')
+ it 'is only editable by author' do
+ visit_merge_request(project, merge)
+ expect(page).to have_selector('.js-task-list-container')
- logout(:user)
+ logout(:user)
- login_as(user2)
- visit current_path
- expect(page).not_to have_selector('.js-task-list-container')
+ login_as(user2)
+ visit current_path
+ expect(page).not_to have_selector('.js-task-list-container')
+ end
+
+ it 'provides a summary on MergeRequests#index' do
+ visit namespace_project_merge_requests_path(project.namespace, project)
+ expect(page).to have_content("2 of 6 tasks completed")
+ end
+ end
+
+ describe 'single incomplete task' do
+ let!(:merge) { create(:merge_request, :simple, description: singleIncompleteMarkdown, author: user, source_project: project) }
+
+ it 'renders for description' do
+ visit_merge_request(project, merge)
+
+ expect(page).to have_selector('ul.task-list', count: 1)
+ expect(page).to have_selector('li.task-list-item', count: 1)
+ expect(page).to have_selector('ul input[checked]', count: 0)
+ end
+
+ it 'provides a summary on MergeRequests#index' do
+ visit namespace_project_merge_requests_path(project.namespace, project)
+ expect(page).to have_content("0 of 1 task completed")
+ end
end
- it 'provides a summary on MergeRequests#index' do
- visit namespace_project_merge_requests_path(project.namespace, project)
- expect(page).to have_content("6 tasks (2 completed, 4 remaining)")
+ describe 'single complete task' do
+ let!(:merge) { create(:merge_request, :simple, description: singleCompleteMarkdown, author: user, source_project: project) }
+
+ it 'renders for description' do
+ visit_merge_request(project, merge)
+
+ expect(page).to have_selector('ul.task-list', count: 1)
+ expect(page).to have_selector('li.task-list-item', count: 1)
+ expect(page).to have_selector('ul input[checked]', count: 1)
+ end
+
+ it 'provides a summary on MergeRequests#index' do
+ visit namespace_project_merge_requests_path(project.namespace, project)
+ expect(page).to have_content("1 of 1 task completed")
+ end
end
end
end
diff --git a/spec/features/todos/todos_filtering_spec.rb b/spec/features/todos/todos_filtering_spec.rb
new file mode 100644
index 00000000000..83cf306437d
--- /dev/null
+++ b/spec/features/todos/todos_filtering_spec.rb
@@ -0,0 +1,63 @@
+require 'spec_helper'
+
+describe 'Dashboard > User filters todos', feature: true, js: true do
+ include WaitForAjax
+
+ let(:user_1) { create(:user, username: 'user_1', name: 'user_1') }
+ let(:user_2) { create(:user, username: 'user_2', name: 'user_2') }
+
+ let(:project_1) { create(:empty_project, name: 'project_1') }
+ let(:project_2) { create(:empty_project, name: 'project_2') }
+
+ let(:issue) { create(:issue, title: 'issue', project: project_1) }
+
+ let!(:merge_request) { create(:merge_request, source_project: project_2, title: 'merge_request') }
+
+ before do
+ create(:todo, user: user_1, author: user_2, project: project_1, target: issue, action: 1)
+ create(:todo, user: user_1, author: user_1, project: project_2, target: merge_request, action: 2)
+
+ project_1.team << [user_1, :developer]
+ project_2.team << [user_1, :developer]
+ login_as(user_1)
+ visit dashboard_todos_path
+ end
+
+ it 'filters by project' do
+ click_button 'Project'
+ within '.dropdown-menu-project' 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
+ end
+
+ it 'filters by author' do
+ click_button 'Author'
+ within '.dropdown-menu-author' 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
+ end
+
+ it 'filters by type' do
+ click_button 'Type'
+ within '.dropdown-menu-type' do
+ click_link 'Issue'
+ end
+ wait_for_ajax
+ expect('.prepend-top-default').not_to have_content ' merge request !'
+ end
+
+ it 'filters by action' do
+ click_button 'Action'
+ within '.dropdown-menu-action' do
+ click_link 'Assigned'
+ end
+ wait_for_ajax
+ expect('.prepend-top-default').not_to have_content ' mentioned '
+ end
+end
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index 0342f4f1d97..fc555a74f30 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -41,6 +41,27 @@ describe 'Dashboard Todos', feature: true do
expect(page).to have_content("You're all done!")
end
end
+
+ context 'todo is stale on the page' do
+ before do
+ todos = TodosFinder.new(user, state: :pending).execute
+ TodoService.new.mark_todos_as_done(todos, user)
+ end
+
+ describe 'deleting the todo' do
+ before do
+ first('.done-todo').click
+ end
+
+ it 'is removed from the list' do
+ expect(page).not_to have_selector('.todos-list .todo')
+ end
+
+ it 'shows "All done" message' do
+ expect(page).to have_content("You're all done!")
+ end
+ end
+ end
end
context 'User has Todos with labels spanning multiple projects' do
@@ -97,6 +118,20 @@ describe 'Dashboard Todos', feature: true do
expect(page).to have_css("#todo_#{Todo.first.id}")
end
end
+
+ describe 'mark all as done', js: true do
+ before do
+ visit dashboard_todos_path
+ click_link('Mark all as done')
+ end
+
+ it 'shows "All done" message!' do
+ within('.todos-pending-count') { expect(page).to have_content '0' }
+ expect(page).to have_content 'To do 0'
+ expect(page).to have_content "You're all done!"
+ expect(page).not_to have_selector('.gl-pagination')
+ end
+ end
end
context 'User has a Todo in a project pending deletion' do
diff --git a/spec/finders/tags_finder_spec.rb b/spec/finders/tags_finder_spec.rb
new file mode 100644
index 00000000000..2ac810e478a
--- /dev/null
+++ b/spec/finders/tags_finder_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe TagsFinder do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+
+ describe '#execute' do
+ context 'sort only' do
+ it 'sorts by name' do
+ tags_finder = described_class.new(repository, {})
+
+ result = tags_finder.execute
+
+ expect(result.first.name).to eq("v1.0.0")
+ end
+
+ it 'sorts by recently_updated' do
+ tags_finder = described_class.new(repository, { sort: 'updated_desc' })
+
+ result = tags_finder.execute
+ recently_updated_tag = repository.tags.max do |a, b|
+ repository.commit(a.target).committed_date <=> repository.commit(b.target).committed_date
+ end
+
+ expect(result.first.name).to eq(recently_updated_tag.name)
+ end
+
+ it 'sorts by last_updated' do
+ tags_finder = described_class.new(repository, { sort: 'updated_asc' })
+
+ result = tags_finder.execute
+
+ expect(result.first.name).to eq('v1.0.0')
+ end
+ end
+
+ context 'filter only' do
+ it 'filters tags by name' do
+ tags_finder = described_class.new(repository, { search: '1.0.0' })
+
+ result = tags_finder.execute
+
+ expect(result.first.name).to eq('v1.0.0')
+ expect(result.count).to eq(1)
+ end
+
+ it 'does not find any tags with that name' do
+ tags_finder = described_class.new(repository, { search: 'hey' })
+
+ result = tags_finder.execute
+
+ expect(result.count).to eq(0)
+ end
+ end
+
+ context 'filter and sort' do
+ it 'filters tags by name and sorts by recently_updated' do
+ params = { sort: 'updated_desc', search: 'v1' }
+ tags_finder = described_class.new(repository, params)
+
+ result = tags_finder.execute
+
+ expect(result.first.name).to eq('v1.1.0')
+ expect(result.count).to eq(2)
+ end
+
+ it 'filters tags by name and sorts by last_updated' do
+ params = { sort: 'updated_asc', search: 'v1' }
+ tags_finder = described_class.new(repository, params)
+
+ result = tags_finder.execute
+
+ expect(result.first.name).to eq('v1.0.0')
+ expect(result.count).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/fixtures/api/schemas/issues.json b/spec/fixtures/api/schemas/issues.json
index 0d2067f704a..70771b21c96 100644
--- a/spec/fixtures/api/schemas/issues.json
+++ b/spec/fixtures/api/schemas/issues.json
@@ -1,4 +1,15 @@
{
- "type": "array",
- "items": { "$ref": "issue.json" }
+ "type": "object",
+ "required" : [
+ "issues",
+ "size"
+ ],
+ "properties" : {
+ "issues": {
+ "type": "array",
+ "items": { "$ref": "issue.json" }
+ },
+ "size": { "type": "integer" }
+ },
+ "additionalProperties": false
}
diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js
index b48026c3b77..56b98856614 100644
--- a/spec/javascripts/application_spec.js
+++ b/spec/javascripts/application_spec.js
@@ -13,17 +13,21 @@
gl.utils.preventDisabledButtons();
isClicked = false;
$button = $('#test-button');
+ expect($button).toExist();
$button.click(function() {
return isClicked = true;
});
$button.trigger('click');
return expect(isClicked).toBe(false);
});
- return it('should be on the same page if a disabled link clicked', function() {
- var locationBeforeLinkClick;
+
+ it('should be on the same page if a disabled link clicked', function() {
+ var locationBeforeLinkClick, $link;
locationBeforeLinkClick = window.location.href;
gl.utils.preventDisabledButtons();
- $('#test-link').click();
+ $link = $('#test-link');
+ expect($link).toExist();
+ $link.click();
return expect(window.location.href).toBe(locationBeforeLinkClick);
});
});
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index fa32d0d7da5..c1c12b57b53 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -11,7 +11,7 @@
/*= require ./fixtures/emoji_menu */
(function() {
- var awardsHandler, lazyAssert;
+ var awardsHandler, lazyAssert, urlRoot;
awardsHandler = null;
@@ -27,6 +27,7 @@
};
gon.award_menu_url = '/emojis';
+ urlRoot = gon.relative_url_root;
lazyAssert = function(done, assertFn) {
return setTimeout(function() {
@@ -45,9 +46,14 @@
return cb();
};
})(this));
- return spyOn(jQuery, 'get').and.callFake(function(req, cb) {
+ spyOn(jQuery, 'get').and.callFake(function(req, cb) {
return cb(window.emojiMenu);
});
+ spyOn(jQuery, 'cookie');
+ });
+ afterEach(function() {
+ // restore original url root value
+ gon.relative_url_root = urlRoot;
});
describe('::showEmojiMenu', function() {
it('should show emoji menu when Add emoji button clicked', function(done) {
@@ -189,6 +195,28 @@
return expect($thumbsUpEmoji.data("original-title")).toBe('sam');
});
});
+ describe('::addEmojiToFrequentlyUsedList', function() {
+ it('should set a cookie with the correct default path', function() {
+ gon.relative_url_root = '';
+ awardsHandler.addEmojiToFrequentlyUsedList('sunglasses');
+ expect(jQuery.cookie)
+ .toHaveBeenCalledWith('frequently_used_emojis', 'sunglasses', {
+ path: '/',
+ expires: 365
+ })
+ ;
+ });
+ it('should set a cookie with the correct custom root path', function() {
+ gon.relative_url_root = '/gitlab/subdir';
+ awardsHandler.addEmojiToFrequentlyUsedList('alien');
+ expect(jQuery.cookie)
+ .toHaveBeenCalledWith('frequently_used_emojis', 'alien', {
+ path: '/gitlab/subdir',
+ expires: 365
+ })
+ ;
+ });
+ });
describe('search', function() {
return it('should filter the emoji', function() {
$('.js-add-award').eq(0).click();
diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6
index c206b794442..1688b996162 100644
--- a/spec/javascripts/boards/list_spec.js.es6
+++ b/spec/javascripts/boards/list_spec.js.es6
@@ -60,15 +60,6 @@ describe('List model', () => {
}, 0);
});
- it('can\'t search when not backlog', () => {
- expect(list.canSearch()).toBe(false);
- });
-
- it('can search when backlog', () => {
- list.type = 'backlog';
- expect(list.canSearch()).toBe(true);
- });
-
it('gets issue from list', (done) => {
setTimeout(() => {
const issue = list.findIssue(1);
diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6
index 0c37ec8354f..f3797ed44d4 100644
--- a/spec/javascripts/boards/mock_data.js.es6
+++ b/spec/javascripts/boards/mock_data.js.es6
@@ -26,12 +26,15 @@ const listObjDuplicate = {
const BoardsMockData = {
'GET': {
- '/test/issue-boards/board/lists{/id}/issues': [{
- title: 'Testing',
- iid: 1,
- confidential: false,
- labels: []
- }]
+ '/test/issue-boards/board/lists{/id}/issues': {
+ issues: [{
+ title: 'Testing',
+ iid: 1,
+ confidential: false,
+ labels: []
+ }],
+ size: 1
+ }
},
'POST': {
'/test/issue-boards/board/lists{/id}': listObj
diff --git a/spec/javascripts/datetime_utility_spec.js.coffee b/spec/javascripts/datetime_utility_spec.js.coffee
deleted file mode 100644
index 6b9617341fe..00000000000
--- a/spec/javascripts/datetime_utility_spec.js.coffee
+++ /dev/null
@@ -1,31 +0,0 @@
-#= require lib/utils/datetime_utility
-
-describe 'Date time utils', ->
- describe 'get day name', ->
- it 'should return Sunday', ->
- day = gl.utils.getDayName(new Date('07/17/2016'))
- expect(day).toBe('Sunday')
-
- it 'should return Monday', ->
- day = gl.utils.getDayName(new Date('07/18/2016'))
- expect(day).toBe('Monday')
-
- it 'should return Tuesday', ->
- day = gl.utils.getDayName(new Date('07/19/2016'))
- expect(day).toBe('Tuesday')
-
- it 'should return Wednesday', ->
- day = gl.utils.getDayName(new Date('07/20/2016'))
- expect(day).toBe('Wednesday')
-
- it 'should return Thursday', ->
- day = gl.utils.getDayName(new Date('07/21/2016'))
- expect(day).toBe('Thursday')
-
- it 'should return Friday', ->
- day = gl.utils.getDayName(new Date('07/22/2016'))
- expect(day).toBe('Friday')
-
- it 'should return Saturday', ->
- day = gl.utils.getDayName(new Date('07/23/2016'))
- expect(day).toBe('Saturday')
diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js.es6
new file mode 100644
index 00000000000..a2d1b0a7732
--- /dev/null
+++ b/spec/javascripts/datetime_utility_spec.js.es6
@@ -0,0 +1,64 @@
+//= require lib/utils/datetime_utility
+(() => {
+ describe('Date time utils', () => {
+ describe('get day name', () => {
+ it('should return Sunday', () => {
+ const day = gl.utils.getDayName(new Date('07/17/2016'));
+ expect(day).toBe('Sunday');
+ });
+
+ it('should return Monday', () => {
+ const day = gl.utils.getDayName(new Date('07/18/2016'));
+ expect(day).toBe('Monday');
+ });
+
+ it('should return Tuesday', () => {
+ const day = gl.utils.getDayName(new Date('07/19/2016'));
+ expect(day).toBe('Tuesday');
+ });
+
+ it('should return Wednesday', () => {
+ const day = gl.utils.getDayName(new Date('07/20/2016'));
+ expect(day).toBe('Wednesday');
+ });
+
+ it('should return Thursday', () => {
+ const day = gl.utils.getDayName(new Date('07/21/2016'));
+ expect(day).toBe('Thursday');
+ });
+
+ it('should return Friday', () => {
+ const day = gl.utils.getDayName(new Date('07/22/2016'));
+ expect(day).toBe('Friday');
+ });
+
+ it('should return Saturday', () => {
+ const day = gl.utils.getDayName(new Date('07/23/2016'));
+ expect(day).toBe('Saturday');
+ });
+ });
+
+ describe('get day difference', () => {
+ it('should return 7', () => {
+ const firstDay = new Date('07/01/2016');
+ const secondDay = new Date('07/08/2016');
+ const difference = gl.utils.getDayDifference(firstDay, secondDay);
+ expect(difference).toBe(7);
+ });
+
+ it('should return 31', () => {
+ const firstDay = new Date('07/01/2016');
+ const secondDay = new Date('08/01/2016');
+ const difference = gl.utils.getDayDifference(firstDay, secondDay);
+ expect(difference).toBe(31);
+ });
+
+ it('should return 365', () => {
+ const firstDay = new Date('07/02/2015');
+ const secondDay = new Date('07/01/2016');
+ const difference = gl.utils.getDayDifference(firstDay, secondDay);
+ expect(difference).toBe(365);
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/fixtures/awards_handler.html.haml b/spec/javascripts/fixtures/awards_handler.html.haml
index d55936ee4f9..1ef2e8f8624 100644
--- a/spec/javascripts/fixtures/awards_handler.html.haml
+++ b/spec/javascripts/fixtures/awards_handler.html.haml
@@ -39,7 +39,7 @@
%span.note-role Reporter
%a.note-action-button.note-emoji-button.js-add-award.js-note-emoji{"data-position" => "right", :href => "#", :title => "Award Emoji"}
%i.fa.fa-spinner.fa-spin
- %i.fa.fa-smile-o
+ %i.fa.fa-smile-o.link-highlight
.js-task-list-container.note-body.is-task-list-enabled
.note-text
%p Suscipit sunt quia quisquam sed eveniet ipsam.
diff --git a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
index 593bd6d5cac..e6c90ad87ee 100644
--- a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
@@ -65,14 +65,14 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
expect(reference_filter(act).to_html).to eq exp
end
- it 'includes a title attribute' do
+ it 'includes no title attribute' do
doc = reference_filter("See #{reference}")
- expect(doc.css('a').first.attr('title')).to eq range.reference_title
+ expect(doc.css('a').first.attr('title')).to eq ""
end
it 'includes default classes' do
doc = reference_filter("See #{reference}")
- expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range'
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range has-tooltip'
end
it 'includes a data-project attribute' do
diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb
index d46d3f1489e..e0f08282551 100644
--- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb
@@ -55,7 +55,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
it 'includes a title attribute' do
doc = reference_filter("See #{reference}")
- expect(doc.css('a').first.attr('title')).to eq commit.link_title
+ expect(doc.css('a').first.attr('title')).to eq commit.title
end
it 'escapes the title attribute' do
@@ -67,7 +67,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
it 'includes default classes' do
doc = reference_filter("See #{reference}")
- expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit'
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit has-tooltip'
end
it 'includes a data-project attribute' do
diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
index 953466679e4..7116c09fb21 100644
--- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
@@ -64,7 +64,7 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do
it 'includes default classes' do
doc = filter("Issue #{reference}")
- expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue'
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip'
end
it 'supports an :only_path context' do
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index a005b4990e7..fce86a9b6ad 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -54,7 +54,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
it 'includes a title attribute' do
doc = reference_filter("Issue #{reference}")
- expect(doc.css('a').first.attr('title')).to eq "Issue: #{issue.title}"
+ expect(doc.css('a').first.attr('title')).to eq issue.title
end
it 'escapes the title attribute' do
@@ -66,7 +66,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
it 'includes default classes' do
doc = reference_filter("Issue #{reference}")
- expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue'
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip'
end
it 'includes a data-project attribute' do
diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb
index 9276a154007..908ccebbf87 100644
--- a/spec/lib/banzai/filter/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb
@@ -21,7 +21,7 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
it 'includes default classes' do
doc = reference_filter("Label #{reference}")
- expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label'
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label has-tooltip'
end
it 'includes a data-project attribute' do
diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
index 805acf1c8b3..274258a045c 100644
--- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
@@ -46,7 +46,7 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
it 'includes a title attribute' do
doc = reference_filter("Merge #{reference}")
- expect(doc.css('a').first.attr('title')).to eq "Merge Request: #{merge.title}"
+ expect(doc.css('a').first.attr('title')).to eq merge.title
end
it 'escapes the title attribute' do
@@ -58,7 +58,7 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
it 'includes default classes' do
doc = reference_filter("Merge #{reference}")
- expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request'
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request has-tooltip'
end
it 'includes a data-project attribute' do
diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
index 9424f2363e1..7419863d848 100644
--- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
@@ -20,7 +20,7 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do
it 'includes default classes' do
doc = reference_filter("Milestone #{reference}")
- expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone'
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone has-tooltip'
end
it 'includes a data-project attribute' do
diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
index 5068ddd7faa..9b92d1a3926 100644
--- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
@@ -39,7 +39,7 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do
it 'includes a title attribute' do
doc = reference_filter("Snippet #{reference}")
- expect(doc.css('a').first.attr('title')).to eq "Snippet: #{snippet.title}"
+ expect(doc.css('a').first.attr('title')).to eq snippet.title
end
it 'escapes the title attribute' do
@@ -51,7 +51,7 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do
it 'includes default classes' do
doc = reference_filter("Snippet #{reference}")
- expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet'
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet has-tooltip'
end
it 'includes a data-project attribute' do
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index 108b36a97cc..fdbdb21eac1 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -104,7 +104,7 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
it 'includes default classes' do
doc = reference_filter("Hey #{reference}")
- expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member'
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member has-tooltip'
end
it 'supports an :only_path context' do
diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb
index ac9c66e2663..9095d2b1345 100644
--- a/spec/lib/banzai/reference_parser/base_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb
@@ -30,7 +30,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do
it 'returns the nodes if the attribute value equals the current project ID' do
link['data-project'] = project.id.to_s
- expect(Ability.abilities).not_to receive(:allowed?)
+ expect(Ability).not_to receive(:allowed?)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
@@ -39,7 +39,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do
link['data-project'] = other_project.id.to_s
- expect(Ability.abilities).to receive(:allowed?).
+ expect(Ability).to receive(:allowed?).
with(user, :read_project, other_project).
and_return(true)
@@ -57,7 +57,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do
link['data-project'] = other_project.id.to_s
- expect(Ability.abilities).to receive(:allowed?).
+ expect(Ability).to receive(:allowed?).
with(user, :read_project, other_project).
and_return(false)
@@ -221,7 +221,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do
it 'delegates the permissions check to the Ability class' do
user = double(:user)
- expect(Ability.abilities).to receive(:allowed?).
+ expect(Ability).to receive(:allowed?).
with(user, :read_project, project)
subject.can?(user, :read_project, project)
diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb
index 9a82891297d..4e7f82a6e09 100644
--- a/spec/lib/banzai/reference_parser/user_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb
@@ -82,7 +82,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do
end
it 'returns the nodes if the user can read the group' do
- expect(Ability.abilities).to receive(:allowed?).
+ expect(Ability).to receive(:allowed?).
with(user, :read_group, group).
and_return(true)
@@ -90,7 +90,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do
end
it 'returns an empty Array if the user can not read the group' do
- expect(Ability.abilities).to receive(:allowed?).
+ expect(Ability).to receive(:allowed?).
with(user, :read_group, group).
and_return(false)
@@ -103,7 +103,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do
it 'returns the nodes if the attribute value equals the current project ID' do
link['data-project'] = project.id.to_s
- expect(Ability.abilities).not_to receive(:allowed?)
+ expect(Ability).not_to receive(:allowed?)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
@@ -113,7 +113,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do
link['data-project'] = other_project.id.to_s
- expect(Ability.abilities).to receive(:allowed?).
+ expect(Ability).to receive(:allowed?).
with(user, :read_project, other_project).
and_return(true)
@@ -125,7 +125,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do
link['data-project'] = other_project.id.to_s
- expect(Ability.abilities).to receive(:allowed?).
+ expect(Ability).to receive(:allowed?).
with(user, :read_project, other_project).
and_return(false)
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index b0772cad312..7c23e02d05a 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -7,7 +7,8 @@ describe Gitlab::Auth, lib: true do
it 'recognizes CI' do
token = '123'
project = create(:empty_project)
- project.update_attributes(runners_token: token, builds_enabled: true)
+ project.update_attributes(runners_token: token)
+
ip = 'ip'
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'gitlab-ci-token')
diff --git a/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb b/spec/lib/gitlab/ci/config/node/hidden_spec.rb
index cc44e2cc054..61e2a554419 100644
--- a/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/hidden_spec.rb
@@ -1,15 +1,15 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::HiddenJob do
+describe Gitlab::Ci::Config::Node::Hidden do
let(:entry) { described_class.new(config) }
describe 'validations' do
context 'when entry config value is correct' do
- let(:config) { { image: 'ruby:2.2' } }
+ let(:config) { [:some, :array] }
describe '#value' do
it 'returns key value' do
- expect(entry.value).to eq(image: 'ruby:2.2')
+ expect(entry.value).to eq [:some, :array]
end
end
@@ -21,17 +21,6 @@ describe Gitlab::Ci::Config::Node::HiddenJob do
end
context 'when entry value is not correct' do
- context 'incorrect config value type' do
- let(:config) { ['incorrect'] }
-
- describe '#errors' do
- it 'saves errors' do
- expect(entry.errors)
- .to include 'hidden job config should be a hash'
- end
- end
- end
-
context 'when config is empty' do
let(:config) { {} }
diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb
index 420d137270a..929809339ef 100644
--- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb
@@ -76,7 +76,7 @@ describe Gitlab::Ci::Config::Node::Jobs do
expect(entry.descendants.first(2))
.to all(be_an_instance_of(Gitlab::Ci::Config::Node::Job))
expect(entry.descendants.last)
- .to be_an_instance_of(Gitlab::Ci::Config::Node::HiddenJob)
+ .to be_an_instance_of(Gitlab::Ci::Config::Node::Hidden)
end
end
diff --git a/spec/lib/gitlab/conflict/parser_spec.rb b/spec/lib/gitlab/conflict/parser_spec.rb
index 65a828accde..a1d2ca1e272 100644
--- a/spec/lib/gitlab/conflict/parser_spec.rb
+++ b/spec/lib/gitlab/conflict/parser_spec.rb
@@ -183,6 +183,11 @@ CONFLICT
expect { parse_text('a' * 102401) }.
to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
end
+
+ it 'raises UnsupportedEncoding when the file contains non-UTF-8 characters' do
+ expect { parse_text("a\xC4\xFC".force_encoding(Encoding::ASCII_8BIT)) }.
+ to raise_error(Gitlab::Conflict::Parser::UnsupportedEncoding)
+ end
end
end
end
diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb
index b7c3bc4e1a7..3fb8de81545 100644
--- a/spec/lib/gitlab/github_import/importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::GithubImport::Importer, lib: true do
describe '#execute' do
context 'when an error occurs' do
- let(:project) { create(:project, import_url: 'https://github.com/octocat/Hello-World.git', wiki_enabled: false) }
+ let(:project) { create(:project, import_url: 'https://github.com/octocat/Hello-World.git', wiki_access_level: ProjectFeature::DISABLED) }
let(:octocat) { double(id: 123456, login: 'octocat') }
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
index 0e7ffbe9b8e..d60c4111e99 100644
--- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
@@ -48,8 +48,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
end
context 'when issue is closed' do
- let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') }
- let(:raw_data) { double(base_data.merge(state: 'closed', closed_at: closed_at)) }
+ let(:raw_data) { double(base_data.merge(state: 'closed')) }
it 'returns formatted attributes' do
expected = {
@@ -62,7 +61,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
author_id: project.creator_id,
assignee_id: nil,
created_at: created_at,
- updated_at: closed_at
+ updated_at: updated_at
}
expect(issue.attributes).to eq(expected)
diff --git a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb b/spec/lib/gitlab/github_import/milestone_formatter_spec.rb
index 5a421e50581..09337c99a07 100644
--- a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/milestone_formatter_spec.rb
@@ -40,8 +40,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do
end
context 'when milestone is closed' do
- let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') }
- let(:raw_data) { double(base_data.merge(state: 'closed', closed_at: closed_at)) }
+ let(:raw_data) { double(base_data.merge(state: 'closed')) }
it 'returns formatted attributes' do
expected = {
@@ -52,7 +51,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do
state: 'closed',
due_date: nil,
created_at: created_at,
- updated_at: closed_at
+ updated_at: updated_at
}
expect(formatter.attributes).to eq(expected)
diff --git a/spec/lib/gitlab/github_import/project_creator_spec.rb b/spec/lib/gitlab/github_import/project_creator_spec.rb
index 0f363b8b0aa..014ee462e5c 100644
--- a/spec/lib/gitlab/github_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/github_import/project_creator_spec.rb
@@ -2,33 +2,59 @@ require 'spec_helper'
describe Gitlab::GithubImport::ProjectCreator, lib: true do
let(:user) { create(:user) }
+ let(:namespace) { create(:group, owner: user) }
+
let(:repo) do
OpenStruct.new(
login: 'vim',
name: 'vim',
- private: true,
full_name: 'asd/vim',
- clone_url: "https://gitlab.com/asd/vim.git",
- owner: OpenStruct.new(login: "john")
+ clone_url: 'https://gitlab.com/asd/vim.git'
)
end
- let(:namespace) { create(:group, owner: user) }
- let(:token) { "asdffg" }
- let(:access_params) { { github_access_token: token } }
+
+ subject(:service) { described_class.new(repo, namespace, user, github_access_token: 'asdffg') }
before do
namespace.add_owner(user)
+ allow_any_instance_of(Project).to receive(:add_import_job)
end
- it 'creates project' do
- allow_any_instance_of(Project).to receive(:add_import_job)
+ describe '#execute' do
+ it 'creates a project' do
+ expect { service.execute }.to change(Project, :count).by(1)
+ end
+
+ it 'handle GitHub credentials' do
+ project = service.execute
+
+ expect(project.import_url).to eq('https://asdffg@gitlab.com/asd/vim.git')
+ expect(project.safe_import_url).to eq('https://*****@gitlab.com/asd/vim.git')
+ expect(project.import_data.credentials).to eq(user: 'asdffg', password: nil)
+ end
+
+ context 'when Github project is private' do
+ it 'sets project visibility to private' do
+ repo.private = true
+
+ project = service.execute
+
+ expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
+
+ context 'when Github project is public' do
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:default_project_visibility).and_return(Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it 'sets project visibility to the default project visibility' do
+ repo.private = false
- project_creator = Gitlab::GithubImport::ProjectCreator.new(repo, namespace, user, access_params)
- project = project_creator.execute
+ project = service.execute
- expect(project.import_url).to eq("https://asdffg@gitlab.com/asd/vim.git")
- expect(project.safe_import_url).to eq("https://*****@gitlab.com/asd/vim.git")
- expect(project.import_data.credentials).to eq(user: "asdffg", password: nil)
- expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
index b667abf063d..edfc6ad81c6 100644
--- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
@@ -62,8 +62,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
context 'when pull request is closed' do
- let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') }
- let(:raw_data) { double(base_data.merge(state: 'closed', closed_at: closed_at)) }
+ let(:raw_data) { double(base_data.merge(state: 'closed')) }
it 'returns formatted attributes' do
expected = {
@@ -81,7 +80,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
author_id: project.creator_id,
assignee_id: nil,
created_at: created_at,
- updated_at: closed_at
+ updated_at: updated_at
}
expect(pull_request.attributes).to eq(expected)
@@ -108,7 +107,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
author_id: project.creator_id,
assignee_id: nil,
created_at: created_at,
- updated_at: merged_at
+ updated_at: updated_at
}
expect(pull_request.attributes).to eq(expected)
diff --git a/spec/lib/gitlab/gitorious_import/project_creator_spec.rb b/spec/lib/gitlab/gitorious_import/project_creator_spec.rb
deleted file mode 100644
index 946712ca38e..00000000000
--- a/spec/lib/gitlab/gitorious_import/project_creator_spec.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::GitoriousImport::ProjectCreator, lib: true do
- let(:user) { create(:user) }
- let(:repo) { Gitlab::GitoriousImport::Repository.new('foo/bar-baz-qux') }
- let(:namespace){ create(:group, owner: user) }
-
- before do
- namespace.add_owner(user)
- end
-
- it 'creates project' do
- allow_any_instance_of(Project).to receive(:add_import_job)
-
- project_creator = Gitlab::GitoriousImport::ProjectCreator.new(repo, namespace, user)
- project = project_creator.execute
-
- expect(project.name).to eq("Bar Baz Qux")
- expect(project.path).to eq("bar-baz-qux")
- expect(project.namespace).to eq(namespace)
- expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
- expect(project.import_type).to eq("gitorious")
- expect(project.import_source).to eq("foo/bar-baz-qux")
- expect(project.import_url).to eq("https://gitorious.org/foo/bar-baz-qux.git")
- end
-end
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index cbbf98dca94..5114f9c55e1 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -1,9 +1,5 @@
{
"description": "Nisi et repellendus ut enim quo accusamus vel magnam.",
- "issues_enabled": true,
- "merge_requests_enabled": true,
- "wiki_enabled": true,
- "snippets_enabled": false,
"visibility_level": 10,
"archived": false,
"issues": [
@@ -7307,4 +7303,4 @@
"protected_branches": [
]
-} \ 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 4d857945fde..a07ef279e68 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
let(:user) { create(:user) }
let(:namespace) { create(:namespace, owner: user) }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') }
- let(:project) { create(:empty_project, name: 'project', path: 'project') }
+ let!(:project) { create(:empty_project, name: 'project', path: 'project', builds_access_level: ProjectFeature::DISABLED, issues_access_level: ProjectFeature::DISABLED) }
let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
let(:restored_project_json) { project_tree_restorer.restore }
@@ -18,6 +18,17 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(restored_project_json).to be true
end
+ it 'restore correct project features' do
+ restored_project_json
+ project = Project.find_by_path('project')
+
+ expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED)
+ expect(project.project_feature.builds_access_level).to eq(ProjectFeature::DISABLED)
+ expect(project.project_feature.snippets_access_level).to eq(ProjectFeature::ENABLED)
+ expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::ENABLED)
+ expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::ENABLED)
+ end
+
it 'creates a valid pipeline note' do
restored_project_json
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index 3a86a4ce07c..d891c2d0cc6 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -111,6 +111,14 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
expect(saved_project_json['issues'].first['label_links'].first['label']).not_to be_empty
end
+ it 'has project feature' do
+ project_feature = saved_project_json['project_feature']
+ expect(project_feature).not_to be_empty
+ expect(project_feature["issues_access_level"]).to eq(ProjectFeature::DISABLED)
+ expect(project_feature["wiki_access_level"]).to eq(ProjectFeature::ENABLED)
+ expect(project_feature["builds_access_level"]).to eq(ProjectFeature::PRIVATE)
+ end
+
it 'does not complain about non UTF-8 characters in MR diffs' do
ActiveRecord::Base.connection.execute("UPDATE merge_request_diffs SET st_diffs = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'")
@@ -154,6 +162,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
create(:event, target: milestone, project: project, action: Event::CREATED, author: user)
+ project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
+ project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::ENABLED)
+ project.project_feature.update_attribute(:builds_access_level, ProjectFeature::PRIVATE)
+
project
end
diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb
index b6dec41d218..3ceb1e7e803 100644
--- a/spec/lib/gitlab/import_export/reader_spec.rb
+++ b/spec/lib/gitlab/import_export/reader_spec.rb
@@ -32,6 +32,12 @@ describe Gitlab::ImportExport::Reader, lib: true do
expect(described_class.new(shared: shared).project_tree).to match(include: [:issues])
end
+ it 'generates the correct hash for a single project feature relation' do
+ setup_yaml(project_tree: [:project_feature])
+
+ expect(described_class.new(shared: shared).project_tree).to match(include: [:project_feature])
+ end
+
it 'generates the correct hash for a multiple project relation' do
setup_yaml(project_tree: [:issues, :snippets])
diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
index a30cb2a5e38..bcaffd27909 100644
--- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
@@ -19,7 +19,7 @@ describe Gitlab::Metrics::RackMiddleware do
end
it 'tags a transaction with the name and action of a controller' do
- klass = double(:klass, name: 'TestController')
+ klass = double(:klass, name: 'TestController', content_type: 'text/html')
controller = double(:controller, class: klass, action_name: 'show')
env['action_controller.instance'] = controller
@@ -32,7 +32,7 @@ describe Gitlab::Metrics::RackMiddleware do
middleware.call(env)
end
- it 'tags a transaction with the method andpath of the route in the grape endpoint' do
+ it 'tags a transaction with the method and path of the route in the grape endpoint' do
route = double(:route, route_method: "GET", route_path: "/:version/projects/:id/archive(.:format)")
endpoint = double(:endpoint, route: route)
@@ -87,17 +87,30 @@ describe Gitlab::Metrics::RackMiddleware do
describe '#tag_controller' do
let(:transaction) { middleware.transaction_from_env(env) }
+ let(:content_type) { 'text/html' }
- it 'tags a transaction with the name and action of a controller' do
+ before do
klass = double(:klass, name: 'TestController')
- controller = double(:controller, class: klass, action_name: 'show')
+ controller = double(:controller, class: klass, action_name: 'show', content_type: content_type)
env['action_controller.instance'] = controller
+ end
+ it 'tags a transaction with the name and action of a controller' do
middleware.tag_controller(transaction, env)
expect(transaction.action).to eq('TestController#show')
end
+
+ context 'when the response content type is not :html' do
+ let(:content_type) { 'application/json' }
+
+ it 'appends the mime type to the transaction action' do
+ middleware.tag_controller(transaction, env)
+
+ expect(transaction.action).to eq('TestController#show.json')
+ end
+ end
end
describe '#tag_endpoint' do
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index aa3b2bbf471..1bdf005c823 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -171,70 +171,6 @@ describe Ability, lib: true do
end
end
- shared_examples_for ".project_abilities" do |enable_request_store|
- before do
- RequestStore.begin! if enable_request_store
- end
-
- after do
- if enable_request_store
- RequestStore.end!
- RequestStore.clear!
- end
- end
-
- describe '.project_abilities' do
- let!(:project) { create(:empty_project, :public) }
- let!(:user) { create(:user) }
-
- it 'returns permissions for admin user' do
- admin = create(:admin)
-
- results = described_class.project_abilities(admin, project)
-
- expect(results.count).to eq(68)
- end
-
- it 'returns permissions for an owner' do
- results = described_class.project_abilities(project.owner, project)
-
- expect(results.count).to eq(68)
- end
-
- it 'returns permissions for a master' do
- project.team << [user, :master]
-
- results = described_class.project_abilities(user, project)
-
- expect(results.count).to eq(60)
- end
-
- it 'returns permissions for a developer' do
- project.team << [user, :developer]
-
- results = described_class.project_abilities(user, project)
-
- expect(results.count).to eq(44)
- end
-
- it 'returns permissions for a guest' do
- project.team << [user, :guest]
-
- results = described_class.project_abilities(user, project)
-
- expect(results.count).to eq(21)
- end
- end
- end
-
- describe '.project_abilities with RequestStore' do
- it_behaves_like ".project_abilities", true
- end
-
- describe '.project_abilities without RequestStore' do
- it_behaves_like ".project_abilities", false
- end
-
describe '.issues_readable_by_user' do
context 'with an admin user' do
it 'returns all given issues' do
@@ -282,4 +218,17 @@ describe Ability, lib: true do
end
end
end
+
+ describe '.project_disabled_features_rules' do
+ let(:project) { create(:project, wiki_access_level: ProjectFeature::DISABLED) }
+
+ subject { described_class.allowed(project.owner, project) }
+
+ context 'wiki named abilities' do
+ it 'disables wiki abilities if the project has no wiki' do
+ expect(project).to receive(:has_external_wiki?).and_return(false)
+ expect(subject).not_to include(:read_wiki, :create_wiki, :update_wiki, :admin_wiki)
+ end
+ end
+ end
end
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index ee2c3d04984..c45c2635cf4 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -948,15 +948,17 @@ describe Ci::Build, models: true do
before { build.run! }
it 'returns false' do
- expect(build.retryable?).to be false
+ expect(build).not_to be_retryable
end
end
context 'when build is finished' do
- before { build.success! }
+ before do
+ build.success!
+ end
it 'returns true' do
- expect(build.retryable?).to be true
+ expect(build).to be_retryable
end
end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 36d10636ae9..bce18b4e99e 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -19,4 +19,64 @@ describe Ci::Build, models: true do
expect(build.trace).to eq(test_trace)
end
end
+
+ describe '#has_trace_file?' do
+ context 'when there is no trace' do
+ it { expect(build.has_trace_file?).to be_falsey }
+ it { expect(build.trace).to be_nil }
+ end
+
+ context 'when there is a trace' do
+ context 'when trace is stored in file' do
+ let(:build_with_trace) { create(:ci_build, :trace) }
+
+ it { expect(build_with_trace.has_trace_file?).to be_truthy }
+ it { expect(build_with_trace.trace).to eq('BUILD TRACE') }
+ end
+
+ context 'when trace is stored in old file' do
+ before do
+ allow(build.project).to receive(:ci_id).and_return(999)
+ allow(File).to receive(:exist?).with(build.path_to_trace).and_return(false)
+ allow(File).to receive(:exist?).with(build.old_path_to_trace).and_return(true)
+ allow(File).to receive(:read).with(build.old_path_to_trace).and_return(test_trace)
+ end
+
+ it { expect(build.has_trace_file?).to be_truthy }
+ it { expect(build.trace).to eq(test_trace) }
+ end
+
+ context 'when trace is stored in DB' do
+ before do
+ allow(build.project).to receive(:ci_id).and_return(nil)
+ allow(build).to receive(:read_attribute).with(:trace).and_return(test_trace)
+ allow(File).to receive(:exist?).with(build.path_to_trace).and_return(false)
+ allow(File).to receive(:exist?).with(build.old_path_to_trace).and_return(false)
+ end
+
+ it { expect(build.has_trace_file?).to be_falsey }
+ it { expect(build.trace).to eq(test_trace) }
+ end
+ end
+ end
+
+ describe '#trace_file_path' do
+ context 'when trace is stored in file' do
+ before do
+ allow(build).to receive(:has_trace_file?).and_return(true)
+ allow(build).to receive(:has_old_trace_file?).and_return(false)
+ end
+
+ it { expect(build.trace_file_path).to eq(build.path_to_trace) }
+ end
+
+ context 'when trace is stored in old file' do
+ before do
+ allow(build).to receive(:has_trace_file?).and_return(true)
+ allow(build).to receive(:has_old_trace_file?).and_return(true)
+ end
+
+ it { expect(build.trace_file_path).to eq(build.old_path_to_trace) }
+ end
+ end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 721b20e0cb2..598df576001 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -195,6 +195,36 @@ describe Ci::Pipeline, models: true do
end
end
+ context 'with non-empty project' do
+ let(:project) { create(:project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ ref: project.default_branch,
+ sha: project.commit.sha)
+ end
+
+ describe '#latest?' do
+ context 'with latest sha' do
+ it 'returns true' do
+ expect(pipeline).to be_latest
+ end
+ end
+
+ context 'with not latest sha' do
+ before do
+ pipeline.update(
+ sha: project.commit("#{project.default_branch}~1").sha)
+ end
+
+ it 'returns false' do
+ expect(pipeline).not_to be_latest
+ end
+ end
+ end
+ end
+
describe '#manual_actions' do
subject { pipeline.manual_actions }
diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb
index 384a38ebc69..c41359b55a3 100644
--- a/spec/models/commit_range_spec.rb
+++ b/spec/models/commit_range_spec.rb
@@ -76,16 +76,6 @@ describe CommitRange, models: true do
end
end
- describe '#reference_title' do
- it 'returns the correct String for three-dot ranges' do
- expect(range.reference_title).to eq "Commits #{full_sha_from} through #{full_sha_to}"
- end
-
- it 'returns the correct String for two-dot ranges' do
- expect(range2.reference_title).to eq "Commits #{full_sha_from}^ through #{full_sha_to}"
- end
- end
-
describe '#to_param' do
it 'includes the correct keys' do
expect(range.to_param.keys).to eq %i(from to)
diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb
index a371c4a18a9..de791abdf3d 100644
--- a/spec/models/concerns/awardable_spec.rb
+++ b/spec/models/concerns/awardable_spec.rb
@@ -45,4 +45,14 @@ describe Issue, "Awardable" do
expect { issue.toggle_award_emoji("thumbsdown", award_emoji.user) }.to change { AwardEmoji.count }.by(-1)
end
end
+
+ describe 'querying award_emoji on an Awardable' do
+ let(:issue) { create(:issue) }
+
+ it 'sorts in ascending fashion' do
+ create_list(:award_emoji, 3, awardable: issue)
+
+ expect(issue.award_emoji).to eq issue.award_emoji.sort_by(&:id)
+ end
+ end
end
diff --git a/spec/models/concerns/statuseable_spec.rb b/spec/models/concerns/has_status_spec.rb
index 8e0a2a2cbde..e118432d098 100644
--- a/spec/models/concerns/statuseable_spec.rb
+++ b/spec/models/concerns/has_status_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
-describe Statuseable do
+describe HasStatus do
before do
@object = Object.new
- @object.extend(Statuseable::ClassMethods)
+ @object.extend(HasStatus::ClassMethods)
end
describe '.status' do
@@ -12,7 +12,7 @@ describe Statuseable do
end
subject { @object.status }
-
+
shared_examples 'build status summary' do
context 'all successful' do
let(:statuses) { Array.new(2) { create(type, status: :success) } }
diff --git a/spec/models/concerns/project_features_compatibility_spec.rb b/spec/models/concerns/project_features_compatibility_spec.rb
new file mode 100644
index 00000000000..5363aea4d22
--- /dev/null
+++ b/spec/models/concerns/project_features_compatibility_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe ProjectFeaturesCompatibility do
+ let(:project) { create(:project) }
+ let(:features) { %w(issues wiki builds merge_requests snippets) }
+
+ # We had issues_enabled, snippets_enabled, builds_enabled, merge_requests_enabled and issues_enabled fields on projects table
+ # All those fields got moved to a new table called project_feature and are now integers instead of booleans
+ # This spec tests if the described concern makes sure parameters received by the API are correctly parsed to the new table
+ # So we can keep it compatible
+
+ it "converts fields from 'true' to ProjectFeature::ENABLED" do
+ features.each do |feature|
+ project.update_attribute("#{feature}_enabled".to_sym, "true")
+ expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::ENABLED)
+ end
+ end
+
+ it "converts fields from 'false' to ProjectFeature::DISABLED" do
+ features.each do |feature|
+ project.update_attribute("#{feature}_enabled".to_sym, "false")
+ expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::DISABLED)
+ end
+ end
+end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index 913d74645a7..be57957b569 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -71,9 +71,6 @@ describe ProjectMember, models: true do
describe :import_team do
before do
- @abilities = Six.new
- @abilities << Ability
-
@project_1 = create :project
@project_2 = create :project
@@ -92,8 +89,8 @@ describe ProjectMember, models: true do
it { expect(@project_2.users).to include(@user_1) }
it { expect(@project_2.users).to include(@user_2) }
- it { expect(@abilities.allowed?(@user_1, :create_project, @project_2)).to be_truthy }
- it { expect(@abilities.allowed?(@user_2, :read_project, @project_2)).to be_truthy }
+ it { expect(Ability.allowed?(@user_1, :create_project, @project_2)).to be_truthy }
+ it { expect(Ability.allowed?(@user_2, :read_project, @project_2)).to be_truthy }
end
describe 'project 1 should not be changed' do
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 29f7396f862..e5b185dc3f6 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -1,6 +1,27 @@
require 'spec_helper'
describe MergeRequestDiff, models: true do
+ describe 'create new record' do
+ subject { create(:merge_request).merge_request_diff }
+
+ it { expect(subject).to be_valid }
+ it { expect(subject).to be_persisted }
+ it { expect(subject.commits.count).to eq(5) }
+ it { expect(subject.diffs.count).to eq(8) }
+ it { expect(subject.head_commit_sha).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+ it { expect(subject.base_commit_sha).to eq('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }
+ it { expect(subject.start_commit_sha).to eq('0b4bc9a49b562e85de7cc9e834518ea6828729b9') }
+ end
+
+ describe '#latest' do
+ let!(:mr) { create(:merge_request, :with_diffs) }
+ let!(:first_diff) { mr.merge_request_diff }
+ let!(:last_diff) { mr.create_merge_request_diff }
+
+ it { expect(last_diff.latest?).to be_truthy }
+ it { expect(first_diff.latest?).to be_falsey }
+ end
+
describe '#diffs' do
let(:mr) { create(:merge_request, :with_diffs) }
let(:mr_diff) { mr.merge_request_diff }
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 64c56d922ff..5bf3b8e609e 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -9,7 +9,7 @@ describe MergeRequest, models: true do
it { is_expected.to belong_to(:target_project).with_foreign_key(:target_project_id).class_name('Project') }
it { is_expected.to belong_to(:source_project).with_foreign_key(:source_project_id).class_name('Project') }
it { is_expected.to belong_to(:merge_user).class_name("User") }
- it { is_expected.to have_one(:merge_request_diff).dependent(:destroy) }
+ it { is_expected.to have_many(:merge_request_diffs).dependent(:destroy) }
end
describe 'modules' do
@@ -159,7 +159,7 @@ describe MergeRequest, models: true do
context 'when there are MR diffs' do
it 'delegates to the MR diffs' do
- merge_request.merge_request_diff = MergeRequestDiff.new
+ merge_request.save
expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(hash_including(options))
@@ -316,7 +316,7 @@ describe MergeRequest, models: true do
end
it "can be removed if the last commit is the head of the source branch" do
- allow(subject.source_project).to receive(:commit).and_return(subject.diff_head_commit)
+ allow(subject).to receive(:source_branch_head).and_return(subject.diff_head_commit)
expect(subject.can_remove_source_branch?(user)).to be_truthy
end
@@ -477,8 +477,8 @@ describe MergeRequest, models: true do
allow(subject).to receive(:diff_head_sha).and_return('123abc')
- expect(subject.source_project).to receive(:pipeline).
- with('123abc', 'master').
+ expect(subject.source_project).to receive(:pipeline_for).
+ with('master', '123abc').
and_return(pipeline)
expect(subject.pipeline).to eq(pipeline)
@@ -721,12 +721,15 @@ describe MergeRequest, models: true do
let(:commit) { subject.project.commit(sample_commit.id) }
- it "reloads the diff content" do
- expect(subject.merge_request_diff).to receive(:reload_content)
-
+ it "does not change existing merge request diff" do
+ expect(subject.merge_request_diff).not_to receive(:save_git_content)
subject.reload_diff
end
+ it "creates new merge request diff" do
+ expect { subject.reload_diff }.to change { subject.merge_request_diffs.count }.by(1)
+ end
+
it "executs diff cache service" do
expect_any_instance_of(MergeRequests::MergeRequestDiffCacheService).to receive(:execute).with(subject)
@@ -736,13 +739,15 @@ describe MergeRequest, models: true do
it "updates diff note positions" do
old_diff_refs = subject.diff_refs
- merge_request_diff = subject.merge_request_diff
-
# Update merge_request_diff so that #diff_refs will return commit.diff_refs
- allow(merge_request_diff).to receive(:reload_content) do
- merge_request_diff.base_commit_sha = commit.parent_id
- merge_request_diff.start_commit_sha = commit.parent_id
- merge_request_diff.head_commit_sha = commit.sha
+ allow(subject).to receive(:create_merge_request_diff) do
+ subject.merge_request_diffs.create(
+ base_commit_sha: commit.parent_id,
+ start_commit_sha: commit.parent_id,
+ head_commit_sha: commit.sha
+ )
+
+ subject.merge_request_diff(true)
end
expect(Notes::DiffPositionUpdateService).to receive(:new).with(
@@ -752,14 +757,31 @@ describe MergeRequest, models: true do
new_diff_refs: commit.diff_refs,
paths: note.position.paths
).and_call_original
- expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(note)
+ expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(note)
expect_any_instance_of(DiffNote).to receive(:save).once
subject.reload_diff
end
end
+ describe '#branch_merge_base_commit' do
+ context 'source and target branch exist' do
+ it { expect(subject.branch_merge_base_commit.sha).to eq('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }
+ it { expect(subject.branch_merge_base_commit).to be_a(Commit) }
+ end
+
+ context 'when the target branch does not exist' do
+ before do
+ subject.project.repository.raw_repository.delete_branch(subject.target_branch)
+ end
+
+ it 'returns nil' do
+ expect(subject.branch_merge_base_commit).to be_nil
+ end
+ end
+ end
+
describe "#diff_sha_refs" do
context "with diffs" do
subject { create(:merge_request, :with_diffs) }
@@ -890,6 +912,19 @@ describe MergeRequest, models: true do
expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
end
+ it 'returns a falsey value when the MR is marked as having conflicts, but has none' do
+ merge_request = create_merge_request('master')
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the MR has a missing ref after a force push' do
+ merge_request = create_merge_request('conflict-resolvable')
+ allow(merge_request.conflicts).to receive(:merge_index).and_raise(Rugged::OdbError)
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
it 'returns a falsey value when the MR does not support new diff notes' do
merge_request = create_merge_request('conflict-resolvable')
merge_request.merge_request_diff.update_attributes(start_commit_sha: nil)
@@ -927,4 +962,80 @@ describe MergeRequest, models: true do
expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy
end
end
+
+ describe "#forked_source_project_missing?" do
+ let(:project) { create(:project) }
+ let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:user) { create(:user) }
+ let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
+
+ context "when the fork exists" do
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: fork_project,
+ target_project: project)
+ end
+
+ it { expect(merge_request.forked_source_project_missing?).to be_falsey }
+ end
+
+ context "when the source project is the same as the target project" do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ it { expect(merge_request.forked_source_project_missing?).to be_falsey }
+ end
+
+ context "when the fork does not exist" do
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: fork_project,
+ target_project: project)
+ end
+
+ it "returns true" do
+ unlink_project.execute
+ merge_request.reload
+
+ expect(merge_request.forked_source_project_missing?).to be_truthy
+ end
+ end
+ end
+
+ describe "#closed_without_fork?" do
+ let(:project) { create(:project) }
+ let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:user) { create(:user) }
+ let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
+
+ context "when the merge request is closed" do
+ let(:closed_merge_request) do
+ create(:closed_merge_request,
+ source_project: fork_project,
+ target_project: project)
+ end
+
+ it "returns false if the fork exist" do
+ expect(closed_merge_request.closed_without_fork?).to be_falsey
+ end
+
+ it "returns true if the fork does not exist" do
+ unlink_project.execute
+ closed_merge_request.reload
+
+ expect(closed_merge_request.closed_without_fork?).to be_truthy
+ end
+ end
+
+ context "when the merge request is open" do
+ let(:open_merge_request) do
+ create(:merge_request,
+ source_project: fork_project,
+ target_project: project)
+ end
+
+ it "returns false" do
+ expect(open_merge_request.closed_without_fork?).to be_falsey
+ end
+ end
+ end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index ef2747046b9..e6b6e7c0634 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -85,8 +85,6 @@ describe Note, models: true do
@u1 = create(:user)
@u2 = create(:user)
@u3 = create(:user)
- @abilities = Six.new
- @abilities << Ability
end
describe 'read' do
@@ -95,9 +93,9 @@ describe Note, models: true do
@p2.project_members.create(user: @u3, access_level: ProjectMember::GUEST)
end
- it { expect(@abilities.allowed?(@u1, :read_note, @p1)).to be_falsey }
- it { expect(@abilities.allowed?(@u2, :read_note, @p1)).to be_truthy }
- it { expect(@abilities.allowed?(@u3, :read_note, @p1)).to be_falsey }
+ it { expect(Ability.allowed?(@u1, :read_note, @p1)).to be_falsey }
+ it { expect(Ability.allowed?(@u2, :read_note, @p1)).to be_truthy }
+ it { expect(Ability.allowed?(@u3, :read_note, @p1)).to be_falsey }
end
describe 'write' do
@@ -106,9 +104,9 @@ describe Note, models: true do
@p2.project_members.create(user: @u3, access_level: ProjectMember::DEVELOPER)
end
- it { expect(@abilities.allowed?(@u1, :create_note, @p1)).to be_falsey }
- it { expect(@abilities.allowed?(@u2, :create_note, @p1)).to be_truthy }
- it { expect(@abilities.allowed?(@u3, :create_note, @p1)).to be_falsey }
+ it { expect(Ability.allowed?(@u1, :create_note, @p1)).to be_falsey }
+ it { expect(Ability.allowed?(@u2, :create_note, @p1)).to be_truthy }
+ it { expect(Ability.allowed?(@u3, :create_note, @p1)).to be_falsey }
end
describe 'admin' do
@@ -118,9 +116,9 @@ describe Note, models: true do
@p2.project_members.create(user: @u3, access_level: ProjectMember::MASTER)
end
- it { expect(@abilities.allowed?(@u1, :admin_note, @p1)).to be_falsey }
- it { expect(@abilities.allowed?(@u2, :admin_note, @p1)).to be_truthy }
- it { expect(@abilities.allowed?(@u3, :admin_note, @p1)).to be_falsey }
+ it { expect(Ability.allowed?(@u1, :admin_note, @p1)).to be_falsey }
+ it { expect(Ability.allowed?(@u2, :admin_note, @p1)).to be_truthy }
+ it { expect(Ability.allowed?(@u3, :admin_note, @p1)).to be_falsey }
end
end
@@ -225,7 +223,7 @@ describe Note, models: true do
let(:note) do
create :note,
noteable: ext_issue, project: ext_proj,
- note: "mentioned in issue #{private_issue.to_reference(ext_proj)}",
+ note: "Mentioned in issue #{private_issue.to_reference(ext_proj)}",
system: true
end
diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb
new file mode 100644
index 00000000000..8d554a01be5
--- /dev/null
+++ b/spec/models/project_feature_spec.rb
@@ -0,0 +1,91 @@
+require 'spec_helper'
+
+describe ProjectFeature do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ describe '#feature_available?' do
+ let(:features) { %w(issues wiki builds merge_requests snippets) }
+
+ context 'when features are disabled' do
+ it "returns false" do
+ features.each do |feature|
+ project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::DISABLED)
+ expect(project.feature_available?(:issues, user)).to eq(false)
+ end
+ end
+ end
+
+ context 'when features are enabled only for team members' do
+ it "returns false when user is not a team member" do
+ features.each do |feature|
+ project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
+ expect(project.feature_available?(:issues, user)).to eq(false)
+ end
+ end
+
+ it "returns true when user is a team member" do
+ project.team << [user, :developer]
+
+ features.each do |feature|
+ project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
+ expect(project.feature_available?(:issues, user)).to eq(true)
+ end
+ end
+
+ it "returns true when user is a member of project group" do
+ group = create(:group)
+ project = create(:project, namespace: group)
+ group.add_developer(user)
+
+ features.each do |feature|
+ project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
+ expect(project.feature_available?(:issues, user)).to eq(true)
+ end
+ end
+
+ it "returns true if user is an admin" do
+ user.update_attribute(:admin, true)
+
+ features.each do |feature|
+ project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
+ expect(project.feature_available?(:issues, user)).to eq(true)
+ end
+ end
+ end
+
+ context 'when feature is enabled for everyone' do
+ it "returns true" do
+ features.each do |feature|
+ project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::ENABLED)
+ expect(project.feature_available?(:issues, user)).to eq(true)
+ end
+ end
+ end
+ end
+
+ describe '#*_enabled?' do
+ let(:features) { %w(wiki builds merge_requests) }
+
+ it "returns false when feature is disabled" do
+ features.each do |feature|
+ project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::DISABLED)
+ expect(project.public_send("#{feature}_enabled?")).to eq(false)
+ end
+ end
+
+ it "returns true when feature is enabled only for team members" do
+ features.each do |feature|
+ project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
+ expect(project.public_send("#{feature}_enabled?")).to eq(true)
+ end
+ end
+
+ it "returns true when feature is enabled for everyone" do
+ features.each do |feature|
+ project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::ENABLED)
+ expect(project.public_send("#{feature}_enabled?")).to eq(true)
+ end
+ end
+ end
+end
diff --git a/spec/models/project_security_spec.rb b/spec/models/project_security_spec.rb
deleted file mode 100644
index 36379074ea0..00000000000
--- a/spec/models/project_security_spec.rb
+++ /dev/null
@@ -1,112 +0,0 @@
-require 'spec_helper'
-
-describe Project, models: true do
- describe 'authorization' do
- before do
- @p1 = create(:project)
-
- @u1 = create(:user)
- @u2 = create(:user)
- @u3 = create(:user)
- @u4 = @p1.owner
-
- @abilities = Six.new
- @abilities << Ability
- end
-
- let(:guest_actions) { Ability.project_guest_rules }
- let(:report_actions) { Ability.project_report_rules }
- let(:dev_actions) { Ability.project_dev_rules }
- let(:master_actions) { Ability.project_master_rules }
- let(:owner_actions) { Ability.project_owner_rules }
-
- describe "Non member rules" do
- it "denies for non-project users any actions" do
- owner_actions.each do |action|
- expect(@abilities.allowed?(@u1, action, @p1)).to be_falsey
- end
- end
- end
-
- describe "Guest Rules" do
- before do
- @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::GUEST)
- end
-
- it "allows for project user any guest actions" do
- guest_actions.each do |action|
- expect(@abilities.allowed?(@u2, action, @p1)).to be_truthy
- end
- end
- end
-
- describe "Report Rules" do
- before do
- @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::REPORTER)
- end
-
- it "allows for project user any report actions" do
- report_actions.each do |action|
- expect(@abilities.allowed?(@u2, action, @p1)).to be_truthy
- end
- end
- end
-
- describe "Developer Rules" do
- before do
- @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::REPORTER)
- @p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::DEVELOPER)
- end
-
- it "denies for developer master-specific actions" do
- [dev_actions - report_actions].each do |action|
- expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey
- end
- end
-
- it "allows for project user any dev actions" do
- dev_actions.each do |action|
- expect(@abilities.allowed?(@u3, action, @p1)).to be_truthy
- end
- end
- end
-
- describe "Master Rules" do
- before do
- @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::DEVELOPER)
- @p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::MASTER)
- end
-
- it "denies for developer master-specific actions" do
- [master_actions - dev_actions].each do |action|
- expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey
- end
- end
-
- it "allows for project user any master actions" do
- master_actions.each do |action|
- expect(@abilities.allowed?(@u3, action, @p1)).to be_truthy
- end
- end
- end
-
- describe "Owner Rules" do
- before do
- @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::DEVELOPER)
- @p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::MASTER)
- end
-
- it "denies for masters admin-specific actions" do
- [owner_actions - master_actions].each do |action|
- expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey
- end
- end
-
- it "allows for project owner any admin actions" do
- owner_actions.each do |action|
- expect(@abilities.allowed?(@u4, action, @p1)).to be_truthy
- end
- end
- end
- end
-end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 9a3660012f9..4a41fafb84d 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -506,6 +506,18 @@ describe Project, models: true do
end
end
+ describe '#has_wiki?' do
+ let(:no_wiki_project) { build(:project, wiki_enabled: false, has_external_wiki: false) }
+ let(:wiki_enabled_project) { build(:project) }
+ let(:external_wiki_project) { build(:project, has_external_wiki: true) }
+
+ it 'returns true if project is wiki enabled or has external wiki' do
+ expect(wiki_enabled_project).to have_wiki
+ expect(external_wiki_project).to have_wiki
+ expect(no_wiki_project).not_to have_wiki
+ end
+ end
+
describe '#external_wiki' do
let(:project) { create(:project) }
@@ -685,31 +697,43 @@ describe Project, models: true do
end
end
- describe '#pipeline' do
- let(:project) { create :project }
- let(:pipeline) { create :ci_pipeline, project: project, ref: 'master' }
-
- subject { project.pipeline(pipeline.sha, 'master') }
+ describe '#pipeline_for' do
+ let(:project) { create(:project) }
+ let!(:pipeline) { create_pipeline }
- it { is_expected.to eq(pipeline) }
+ shared_examples 'giving the correct pipeline' do
+ it { is_expected.to eq(pipeline) }
- context 'return latest' do
- let(:pipeline2) { create :ci_pipeline, project: project, ref: 'master' }
+ context 'return latest' do
+ let!(:pipeline2) { create_pipeline }
- before do
- pipeline
- pipeline2
+ it { is_expected.to eq(pipeline2) }
end
+ end
+
+ context 'with explicit sha' do
+ subject { project.pipeline_for('master', pipeline.sha) }
+
+ it_behaves_like 'giving the correct pipeline'
+ end
+
+ context 'with implicit sha' do
+ subject { project.pipeline_for('master') }
+
+ it_behaves_like 'giving the correct pipeline'
+ end
- it { is_expected.to eq(pipeline2) }
+ def create_pipeline
+ create(:ci_pipeline,
+ project: project,
+ ref: 'master',
+ sha: project.commit('master').sha)
end
end
describe '#builds_enabled' do
let(:project) { create :project }
- before { project.builds_enabled = true }
-
subject { project.builds_enabled }
it { expect(project.builds_enabled?).to be_truthy }
@@ -1442,4 +1466,35 @@ describe Project, models: true do
expect(shared_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(true)
end
end
+
+ describe 'change_head' do
+ let(:project) { create(:project) }
+
+ it 'calls the before_change_head method' do
+ expect(project.repository).to receive(:before_change_head)
+ project.change_head(project.default_branch)
+ end
+
+ it 'creates the new reference with rugged' do
+ expect(project.repository.rugged.references).to receive(:create).with('HEAD',
+ "refs/heads/#{project.default_branch}",
+ force: true)
+ project.change_head(project.default_branch)
+ end
+
+ it 'copies the gitattributes' do
+ expect(project.repository).to receive(:copy_gitattributes).with(project.default_branch)
+ project.change_head(project.default_branch)
+ end
+
+ it 'expires the avatar cache' do
+ expect(project.repository).to receive(:expire_avatar_cache).with(project.default_branch)
+ project.change_head(project.default_branch)
+ end
+
+ it 'reloads the default branch' do
+ expect(project).to receive(:reload_default_branch)
+ project.change_head(project.default_branch)
+ end
+ end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 1fea50ad42c..812c72c48cb 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -382,6 +382,24 @@ describe Repository, models: true do
end
end
+ describe '#find_branch' do
+ it 'loads a branch with a fresh repo' do
+ expect(Gitlab::Git::Repository).to receive(:new).twice.and_call_original
+
+ 2.times do
+ expect(repository.find_branch('feature')).not_to be_nil
+ end
+ end
+
+ it 'loads a branch with a cached repo' do
+ expect(Gitlab::Git::Repository).to receive(:new).once.and_call_original
+
+ 2.times do
+ expect(repository.find_branch('feature', fresh_repo: false)).not_to be_nil
+ end
+ end
+ end
+
describe '#rm_branch' do
let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
let(:blank_sha) { '0000000000000000000000000000000000000000' }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 8eb0c5033c9..a1770d96f83 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1006,8 +1006,7 @@ describe User, models: true do
end
it 'does not include projects for which issues are disabled' do
- project = create(:project)
- project.update_attributes(issues_enabled: false)
+ project = create(:project, issues_access_level: ProjectFeature::DISABLED)
expect(user.projects_where_can_admin_issues.to_a).to be_empty
expect(user.can?(:admin_issue, project)).to eq(false)
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
new file mode 100644
index 00000000000..eda1cafd65e
--- /dev/null
+++ b/spec/policies/project_policy_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe ProjectPolicy, models: true do
+ let(:project) { create(:empty_project, :public) }
+ let(:guest) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:dev) { create(:user) }
+ let(:master) { create(:user) }
+ let(:owner) { create(:user) }
+ let(:admin) { create(:admin) }
+
+ let(:users_ordered_by_permissions) do
+ [nil, guest, reporter, dev, master, owner, admin]
+ end
+
+ let(:users_permissions) do
+ users_ordered_by_permissions.map { |u| Ability.allowed(u, project).size }
+ end
+
+ before do
+ project.team << [guest, :guest]
+ project.team << [master, :master]
+ project.team << [dev, :developer]
+ project.team << [reporter, :reporter]
+
+ group = create(:group)
+ project.project_group_links.create(
+ group: group,
+ group_access: Gitlab::Access::MASTER)
+ group.add_owner(owner)
+ end
+
+ it 'returns increasing permissions for each level' do
+ expect(users_permissions).to eq(users_permissions.sort.uniq)
+ end
+end
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index 73c268c0d1e..981a6791881 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -4,7 +4,7 @@ describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
let!(:project) { create(:project) }
- let(:issue) { create(:issue, project: project, author: user) }
+ 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) }
let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) }
@@ -115,6 +115,8 @@ describe API::API, api: true do
end
describe "POST /projects/:id/awardable/:awardable_id/award_emoji" do
+ let(:issue2) { create(:issue, project: project, author: user) }
+
context "on an issue" do
it "creates a new award emoji" do
post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'blowfish'
@@ -136,6 +138,12 @@ describe API::API, api: true do
expect(response).to have_http_status(401)
end
+ it "returns a 404 error if the user authored issue" do
+ post api("/projects/#{project.id}/issues/#{issue2.id}/award_emoji", user), name: 'thumbsup'
+
+ expect(response).to have_http_status(404)
+ end
+
it "normalizes +1 as thumbsup award" do
post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: '+1'
@@ -155,6 +163,8 @@ describe API::API, api: true do
end
describe "POST /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji" do
+ let(:note2) { create(:note, project: project, noteable: issue, author: user) }
+
it 'creates a new award emoji' do
expect do
post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
@@ -164,6 +174,12 @@ describe API::API, api: true do
expect(json_response['user']['username']).to eq(user.username)
end
+ it "it returns 404 error when user authored note" do
+ post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup'
+
+ expect(response).to have_http_status(404)
+ end
+
it "normalizes +1 as thumbsup award" do
post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: '+1'
diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb
new file mode 100644
index 00000000000..7c9078b2864
--- /dev/null
+++ b/spec/requests/api/broadcast_messages_spec.rb
@@ -0,0 +1,180 @@
+require 'spec_helper'
+
+describe API::BroadcastMessages, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
+
+ describe 'GET /broadcast_messages' do
+ it 'returns a 401 for anonymous users' do
+ get api('/broadcast_messages')
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns a 403 for users' do
+ get api('/broadcast_messages', user)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it 'returns an Array of BroadcastMessages for admins' do
+ create(:broadcast_message)
+
+ get api('/broadcast_messages', admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_kind_of(Array)
+ expect(json_response.first.keys)
+ .to match_array(%w(id message starts_at ends_at color font active))
+ end
+ end
+
+ describe 'GET /broadcast_messages/:id' do
+ let!(:message) { create(:broadcast_message) }
+
+ it 'returns a 401 for anonymous users' do
+ get api("/broadcast_messages/#{message.id}")
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns a 403 for users' do
+ get api("/broadcast_messages/#{message.id}", user)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it 'returns the specified message for admins' do
+ get api("/broadcast_messages/#{message.id}", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq message.id
+ expect(json_response.keys)
+ .to match_array(%w(id message starts_at ends_at color font active))
+ end
+ end
+
+ describe 'POST /broadcast_messages' do
+ it 'returns a 401 for anonymous users' do
+ post api('/broadcast_messages'), attributes_for(:broadcast_message)
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns a 403 for users' do
+ post api('/broadcast_messages', user), attributes_for(:broadcast_message)
+
+ expect(response).to have_http_status(403)
+ end
+
+ context 'as an admin' do
+ it 'requires the `message` parameter' do
+ attrs = attributes_for(:broadcast_message)
+ attrs.delete(:message)
+
+ post api('/broadcast_messages', admin), attrs
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq 'message is missing'
+ end
+
+ it 'defines sane default start and end times' do
+ time = Time.zone.parse('2016-07-02 10:11:12')
+ travel_to(time) do
+ post api('/broadcast_messages', admin), message: 'Test message'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['starts_at']).to eq '2016-07-02T10:11:12.000Z'
+ expect(json_response['ends_at']).to eq '2016-07-02T11:11:12.000Z'
+ end
+ end
+
+ it 'accepts a custom background and foreground color' do
+ attrs = attributes_for(:broadcast_message, color: '#000000', font: '#cecece')
+
+ post api('/broadcast_messages', admin), attrs
+
+ expect(response).to have_http_status(201)
+ expect(json_response['color']).to eq attrs[:color]
+ expect(json_response['font']).to eq attrs[:font]
+ end
+ end
+ end
+
+ describe 'PUT /broadcast_messages/:id' do
+ let!(:message) { create(:broadcast_message) }
+
+ it 'returns a 401 for anonymous users' do
+ put api("/broadcast_messages/#{message.id}"),
+ attributes_for(:broadcast_message)
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns a 403 for users' do
+ put api("/broadcast_messages/#{message.id}", user),
+ attributes_for(:broadcast_message)
+
+ expect(response).to have_http_status(403)
+ end
+
+ context 'as an admin' do
+ it 'accepts new background and foreground colors' do
+ attrs = { color: '#000000', font: '#cecece' }
+
+ put api("/broadcast_messages/#{message.id}", admin), attrs
+
+ expect(response).to have_http_status(200)
+ expect(json_response['color']).to eq attrs[:color]
+ expect(json_response['font']).to eq attrs[:font]
+ end
+
+ it 'accepts new start and end times' do
+ time = Time.zone.parse('2016-07-02 10:11:12')
+ travel_to(time) do
+ attrs = { starts_at: Time.zone.now, ends_at: 3.hours.from_now }
+
+ put api("/broadcast_messages/#{message.id}", admin), attrs
+
+ expect(response).to have_http_status(200)
+ expect(json_response['starts_at']).to eq '2016-07-02T10:11:12.000Z'
+ expect(json_response['ends_at']).to eq '2016-07-02T13:11:12.000Z'
+ end
+ end
+
+ it 'accepts a new message' do
+ attrs = { message: 'new message' }
+
+ put api("/broadcast_messages/#{message.id}", admin), attrs
+
+ expect(response).to have_http_status(200)
+ expect { message.reload }.to change { message.message }.to('new message')
+ end
+ end
+ end
+
+ describe 'DELETE /broadcast_messages/:id' do
+ let!(:message) { create(:broadcast_message) }
+
+ it 'returns a 401 for anonymous users' do
+ delete api("/broadcast_messages/#{message.id}"),
+ attributes_for(:broadcast_message)
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns a 403 for users' do
+ delete api("/broadcast_messages/#{message.id}", user),
+ attributes_for(:broadcast_message)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it 'deletes the broadcast message for admins' do
+ expect { delete api("/broadcast_messages/#{message.id}", admin) }
+ .to change { BroadcastMessage.count }.by(-1)
+ end
+ end
+end
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index 9a17a705b1e..ee0b61e2ca4 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -15,7 +15,9 @@ describe API::API, api: true do
describe 'GET /projects/:id/builds ' do
let(:query) { '' }
- before { get api("/projects/#{project.id}/builds?#{query}", api_user) }
+ before do
+ get api("/projects/#{project.id}/builds?#{query}", api_user)
+ end
context 'authorized user' do
it 'returns project builds' do
@@ -122,7 +124,9 @@ describe API::API, api: true do
end
describe 'GET /projects/:id/builds/:build_id' do
- before { get api("/projects/#{project.id}/builds/#{build.id}", api_user) }
+ before do
+ get api("/projects/#{project.id}/builds/#{build.id}", api_user)
+ end
context 'authorized user' do
it 'returns specific build data' do
@@ -141,7 +145,9 @@ describe API::API, api: true do
end
describe 'GET /projects/:id/builds/:build_id/artifacts' do
- before { get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) }
+ before do
+ get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user)
+ end
context 'build with artifacts' do
let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
@@ -292,7 +298,9 @@ describe API::API, api: true do
end
describe 'POST /projects/:id/builds/:build_id/cancel' do
- before { post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user) }
+ before do
+ post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user)
+ end
context 'authorized user' do
context 'user with :update_build persmission' do
@@ -323,7 +331,9 @@ describe API::API, api: true do
describe 'POST /projects/:id/builds/:build_id/retry' do
let(:build) { create(:ci_build, :canceled, pipeline: pipeline) }
- before { post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) }
+ before do
+ post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user)
+ end
context 'authorized user' do
context 'user with :update_build permission' do
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 7ca75d77673..5b3dc60aba2 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -95,7 +95,7 @@ describe API::API, api: true do
end
it "returns status for CI" do
- pipeline = project.ensure_pipeline(project.repository.commit.sha, 'master')
+ pipeline = project.ensure_pipeline('master', project.repository.commit.sha)
pipeline.update(status: 'success')
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
@@ -105,7 +105,7 @@ describe API::API, api: true do
end
it "returns status for CI when pipeline is created" do
- project.ensure_pipeline(project.repository.commit.sha, 'master')
+ project.ensure_pipeline('master', project.repository.commit.sha)
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 5d06abcfeb3..46d1b868782 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -44,8 +44,8 @@ describe API::API, api: true do
secret_token: secret_token,
key_id: 12345
- expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Not found')
+ expect(json_response['success']).to be_falsey
+ expect(json_response['message']).to eq('Could not find the given key')
end
it 'returns an error message when the key is a deploy key' do
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index b8038fc85a1..47344a13b5e 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
+
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:non_member) { create(:user) }
@@ -404,6 +405,7 @@ describe API::API, api: true do
expect(json_response['milestone']).to be_a Hash
expect(json_response['assignee']).to be_a Hash
expect(json_response['author']).to be_a Hash
+ expect(json_response['confidential']).to be_falsy
end
it "returns a project issue by id" do
@@ -469,13 +471,63 @@ describe API::API, api: true do
end
describe "POST /projects/:id/issues" do
- it "creates a new project issue" do
+ it 'creates a new project issue' do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2'
+
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['description']).to be_nil
expect(json_response['labels']).to eq(['label', 'label2'])
+ expect(json_response['confidential']).to be_falsy
+ end
+
+ it 'creates a new confidential project issue' do
+ post api("/projects/#{project.id}/issues", user),
+ title: 'new issue', confidential: true
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['confidential']).to be_truthy
+ end
+
+ it 'creates a new confidential project issue with a different param' do
+ post api("/projects/#{project.id}/issues", user),
+ title: 'new issue', confidential: 'y'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['confidential']).to be_truthy
+ end
+
+ it 'creates a public issue when confidential param is false' do
+ post api("/projects/#{project.id}/issues", user),
+ title: 'new issue', confidential: false
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['confidential']).to be_falsy
+ end
+
+ it 'creates a public issue when confidential param is invalid' do
+ post api("/projects/#{project.id}/issues", user),
+ title: 'new issue', confidential: 'foo'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['confidential']).to be_falsy
+ end
+
+ it "sends notifications for subscribers of newly added labels" do
+ label = project.labels.first
+ label.toggle_subscription(user2)
+
+ perform_enqueued_jobs do
+ post api("/projects/#{project.id}/issues", user),
+ title: 'new issue', labels: label.title
+ end
+
+ should_email(user2)
end
it "returns a 400 bad request if title not given" do
@@ -619,6 +671,30 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
expect(json_response['title']).to eq('updated title')
end
+
+ it 'sets an issue to confidential' do
+ put api("/projects/#{project.id}/issues/#{issue.id}", user),
+ confidential: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response['confidential']).to be_truthy
+ end
+
+ it 'makes a confidential issue public' do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
+ confidential: false
+
+ expect(response).to have_http_status(200)
+ expect(json_response['confidential']).to be_falsy
+ end
+
+ it 'does not update a confidential issue with wrong confidential flag' do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
+ confidential: 'foo'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['confidential']).to be_truthy
+ end
end
end
@@ -633,6 +709,18 @@ describe API::API, api: true do
expect(json_response['labels']).to eq([label.title])
end
+ it "sends notifications for subscribers of newly added labels when issue is updated" do
+ label = create(:label, title: 'foo', color: '#FFAABB', project: project)
+ label.toggle_subscription(user2)
+
+ perform_enqueued_jobs do
+ put api("/projects/#{project.id}/issues/#{issue.id}", user),
+ title: 'updated title', labels: label.title
+ end
+
+ should_email(user2)
+ end
+
it 'removes all labels' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
labels: ''
diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb
new file mode 100644
index 00000000000..8f1e5ac9891
--- /dev/null
+++ b/spec/requests/api/merge_request_diffs_spec.rb
@@ -0,0 +1,49 @@
+require "spec_helper"
+
+describe API::API, 'MergeRequestDiffs', api: true do
+ include ApiHelpers
+
+ let!(:user) { create(:user) }
+ let!(:merge_request) { create(:merge_request, importing: true) }
+ let!(:project) { merge_request.target_project }
+
+ before do
+ merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+ merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+ project.team << [user, :master]
+ end
+
+ describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do
+ context 'valid merge request' do
+ before { get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user) }
+ let(:merge_request_diff) { merge_request.merge_request_diffs.first }
+
+ it { expect(response.status).to eq 200 }
+ it { expect(json_response.size).to eq(merge_request.merge_request_diffs.size) }
+ it { expect(json_response.first['id']).to eq(merge_request_diff.id) }
+ it { expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) }
+ end
+
+ it 'returns a 404 when merge_request_id not found' do
+ get api("/projects/#{project.id}/merge_requests/999/versions", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id' do
+ context 'valid merge request' do
+ before { get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user) }
+ let(:merge_request_diff) { merge_request.merge_request_diffs.first }
+
+ it { expect(response.status).to eq 200 }
+ it { expect(json_response['id']).to eq(merge_request_diff.id) }
+ it { expect(json_response['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) }
+ it { expect(json_response['diffs'].size).to eq(merge_request_diff.diffs.size) }
+ end
+
+ it 'returns a 404 when merge_request_id not found' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/999", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index baff872e28e..a7930c59df9 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -9,7 +9,7 @@ describe API::API, api: true do
let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) }
let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) }
- let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds) }
+ let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
@@ -34,6 +34,13 @@ describe API::API, api: true do
expect(json_response.length).to eq(3)
expect(json_response.last['title']).to eq(merge_request.title)
expect(json_response.last).to have_key('web_url')
+ expect(json_response.last['sha']).to eq(merge_request.diff_head_sha)
+ expect(json_response.last['merge_commit_sha']).to be_nil
+ expect(json_response.last['merge_commit_sha']).to eq(merge_request.merge_commit_sha)
+ expect(json_response.first['title']).to eq(merge_request_merged.title)
+ expect(json_response.first['sha']).to eq(merge_request_merged.diff_head_sha)
+ expect(json_response.first['merge_commit_sha']).not_to be_nil
+ expect(json_response.first['merge_commit_sha']).to eq(merge_request_merged.merge_commit_sha)
end
it "returns an array of all merge_requests" do
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 737fa14cbb0..223444ea39f 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -25,7 +25,7 @@ describe API::API, api: true do
let!(:cross_reference_note) do
create :note,
noteable: ext_issue, project: ext_proj,
- note: "mentioned in issue #{private_issue.to_reference(ext_proj)}",
+ note: "Mentioned in issue #{private_issue.to_reference(ext_proj)}",
system: true
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 63f2467be63..28aa56e8644 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -73,7 +73,7 @@ describe API::API, api: true do
end
it 'does not include open_issues_count' do
- project.update_attributes( { issues_enabled: false } )
+ project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
get api('/projects', user)
expect(response.status).to eq 200
@@ -231,8 +231,15 @@ describe API::API, api: true do
post api('/projects', user), project
project.each_pair do |k, v|
+ next if %i{ issues_enabled merge_requests_enabled wiki_enabled }.include?(k)
expect(json_response[k.to_s]).to eq(v)
end
+
+ # Check feature permissions attributes
+ project = Project.find_by_path(project[:path])
+ expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED)
+ expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::DISABLED)
+ expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED)
end
it 'sets a project as public' do
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 0bbba64a6d5..ef73778efa9 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -605,6 +605,7 @@ describe API::API, api: true do
expect(json_response['can_create_project']).to eq(user.can_create_project?)
expect(json_response['can_create_group']).to eq(user.can_create_group?)
expect(json_response['projects_limit']).to eq(user.projects_limit)
+ expect(json_response['private_token']).to be_blank
end
it "returns 401 error if user is unauthenticated" do
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index afaf4b7cefb..9ca3b021aa2 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -289,7 +289,8 @@ describe 'Git HTTP requests', lib: true do
let(:project) { FactoryGirl.create :empty_project }
before do
- project.update_attributes(runners_token: token, builds_enabled: true)
+ project.update_attributes(runners_token: token)
+ project.project_feature.update_attributes(builds_access_level: ProjectFeature::ENABLED)
end
it "downloads get status 200" do
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index c6172b9cc7d..fc42b534dca 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -22,19 +22,20 @@ describe JwtController do
context 'when using authorized request' do
context 'using CI token' do
- let(:project) { create(:empty_project, runners_token: 'token', builds_enabled: builds_enabled) }
+ let(:project) { create(:empty_project, runners_token: 'token') }
let(:headers) { { authorization: credentials('gitlab-ci-token', project.runners_token) } }
- subject! { get '/jwt/auth', parameters, headers }
-
context 'project with enabled CI' do
- let(:builds_enabled) { true }
-
+ subject! { get '/jwt/auth', parameters, headers }
it { expect(service_class).to have_received(:new).with(project, nil, parameters) }
end
context 'project with disabled CI' do
- let(:builds_enabled) { false }
+ before do
+ project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
+ end
+
+ subject! { get '/jwt/auth', parameters, headers }
it { expect(response).to have_http_status(403) }
end
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 4c9b4a8ba42..fcd6521317a 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -44,6 +44,113 @@ describe 'Git LFS API and storage' do
end
end
+ context 'project specific LFS settings' do
+ let(:project) { create(:empty_project) }
+ let(:body) do
+ {
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ },
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }
+ ],
+ 'operation' => 'upload'
+ }
+ end
+ let(:authorization) { authorize_user }
+
+ context 'with LFS disabled globally' do
+ before do
+ project.team << [user, :master]
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(false)
+ end
+
+ describe 'LFS disabled in project' do
+ before do
+ project.update_attribute(:lfs_enabled, false)
+ end
+
+ it 'responds with a 501 message on upload' do
+ post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
+
+ expect(response).to have_http_status(501)
+ end
+
+ it 'responds with a 501 message on download' do
+ get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers
+
+ expect(response).to have_http_status(501)
+ end
+ end
+
+ describe 'LFS enabled in project' do
+ before do
+ project.update_attribute(:lfs_enabled, true)
+ end
+
+ it 'responds with a 501 message on upload' do
+ post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
+
+ expect(response).to have_http_status(501)
+ end
+
+ it 'responds with a 501 message on download' do
+ get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers
+
+ expect(response).to have_http_status(501)
+ end
+ end
+ end
+
+ context 'with LFS enabled globally' do
+ before do
+ project.team << [user, :master]
+ enable_lfs
+ end
+
+ describe 'LFS disabled in project' do
+ before do
+ project.update_attribute(:lfs_enabled, false)
+ end
+
+ it 'responds with a 403 message on upload' do
+ post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
+
+ expect(response).to have_http_status(403)
+ expect(json_response).to include('message' => 'Access forbidden. Check your access level.')
+ end
+
+ it 'responds with a 403 message on download' do
+ get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers
+
+ expect(response).to have_http_status(403)
+ expect(json_response).to include('message' => 'Access forbidden. Check your access level.')
+ end
+ end
+
+ describe 'LFS enabled in project' do
+ before do
+ project.update_attribute(:lfs_enabled, true)
+ end
+
+ it 'responds with a 200 message on upload' do
+ post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
+
+ expect(response).to have_http_status(200)
+ expect(json_response['objects'].first['size']).to eq(1575078)
+ end
+
+ it 'responds with a 200 message on download' do
+ get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers
+
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
+ end
+
describe 'deprecated API' do
let(:project) { create(:empty_project) }
diff --git a/spec/requests/projects/artifacts_controller_spec.rb b/spec/requests/projects/artifacts_controller_spec.rb
new file mode 100644
index 00000000000..e02f0eacc93
--- /dev/null
+++ b/spec/requests/projects/artifacts_controller_spec.rb
@@ -0,0 +1,117 @@
+require 'spec_helper'
+
+describe Projects::ArtifactsController do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ sha: project.commit.sha,
+ ref: project.default_branch,
+ status: 'success')
+ end
+
+ let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
+
+ describe 'GET /:project/builds/artifacts/:ref_name/browse?job=name' do
+ before do
+ project.team << [user, :developer]
+
+ login_as(user)
+ end
+
+ def path_from_ref(
+ ref = pipeline.ref, job = build.name, path = 'browse')
+ latest_succeeded_namespace_project_artifacts_path(
+ project.namespace,
+ project,
+ [ref, path].join('/'),
+ job: job)
+ end
+
+ context 'cannot find the build' do
+ shared_examples 'not found' do
+ it { expect(response).to have_http_status(:not_found) }
+ end
+
+ context 'has no such ref' do
+ before do
+ get path_from_ref('TAIL', build.name)
+ end
+
+ it_behaves_like 'not found'
+ end
+
+ context 'has no such build' do
+ before do
+ get path_from_ref(pipeline.ref, 'NOBUILD')
+ end
+
+ it_behaves_like 'not found'
+ end
+
+ context 'has no path' do
+ before do
+ get path_from_ref(pipeline.sha, build.name, '')
+ end
+
+ it_behaves_like 'not found'
+ end
+ end
+
+ context 'found the build and redirect' do
+ shared_examples 'redirect to the build' do
+ it 'redirects' do
+ path = browse_namespace_project_build_artifacts_path(
+ project.namespace,
+ project,
+ build)
+
+ expect(response).to redirect_to(path)
+ end
+ end
+
+ context 'with regular branch' do
+ before do
+ pipeline.update(ref: 'master',
+ sha: project.commit('master').sha)
+
+ get path_from_ref('master')
+ end
+
+ it_behaves_like 'redirect to the build'
+ end
+
+ context 'with branch name containing slash' do
+ before do
+ pipeline.update(ref: 'improve/awesome',
+ sha: project.commit('improve/awesome').sha)
+
+ get path_from_ref('improve/awesome')
+ end
+
+ it_behaves_like 'redirect to the build'
+ end
+
+ context 'with branch name and path containing slashes' do
+ before do
+ pipeline.update(ref: 'improve/awesome',
+ sha: project.commit('improve/awesome').sha)
+
+ get path_from_ref('improve/awesome', build.name, 'file/README.md')
+ end
+
+ it 'redirects' do
+ path = file_namespace_project_build_artifacts_path(
+ project.namespace,
+ project,
+ build,
+ 'README.md')
+
+ expect(response).to redirect_to(path)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index d65648dd0b2..4bc3cddd9c2 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -107,9 +107,9 @@ describe HelpController, "routing" do
end
it 'to #show' do
- path = '/help/markdown/markdown.md'
+ path = '/help/user/markdown.md'
expect(get(path)).to route_to('help#show',
- path: 'markdown/markdown',
+ path: 'user/markdown',
format: 'md')
path = '/help/workflow/protected_branches/protected_branches1.png'
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
index f7f45983d26..cf4c5f13635 100644
--- a/spec/services/boards/issues/list_service_spec.rb
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -30,7 +30,7 @@ describe Boards::Issues::ListService, services: true do
let!(:closed_issue1) { create(:labeled_issue, :closed, project: project, labels: [bug]) }
let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3]) }
let!(:closed_issue3) { create(:issue, :closed, project: project) }
- let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1]) }
+ let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1, development]) }
before do
project.team << [user, :developer]
@@ -58,15 +58,15 @@ describe Boards::Issues::ListService, services: true do
issues = described_class.new(project, user, params).execute
- expect(issues).to eq [closed_issue4, closed_issue2, closed_issue3, closed_issue1]
+ expect(issues).to eq [closed_issue2, closed_issue3, closed_issue1]
end
- it 'returns opened issues that have label list applied when listing issues from a label list' do
+ it 'returns opened/closed issues that have label list applied when listing issues from a label list' do
params = { id: list1.id }
issues = described_class.new(project, user, params).execute
- expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2]
+ expect(issues).to eq [closed_issue4, list1_issue3, list1_issue1, list1_issue2]
end
end
end
diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb
index 5e7e145065e..90764b86b16 100644
--- a/spec/services/boards/lists/create_service_spec.rb
+++ b/spec/services/boards/lists/create_service_spec.rb
@@ -5,7 +5,7 @@ describe Boards::Lists::CreateService, services: true do
let(:project) { create(:project_with_board) }
let(:board) { project.board }
let(:user) { create(:user) }
- let(:label) { create(:label, name: 'in-progress') }
+ let(:label) { create(:label, project: project, name: 'in-progress') }
subject(:service) { described_class.new(project, user, label_id: label.id) }
@@ -50,5 +50,14 @@ describe Boards::Lists::CreateService, services: true do
expect(list2.reload.position).to eq 1
end
end
+
+ context 'when provided label does not belongs to the project' do
+ it 'raises an error' do
+ label = create(:label, name: 'in-development')
+ service = described_class.new(project, user, label_id: label.id)
+
+ expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
end
end
diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb
index c931c3e4829..b3e0a7b9b58 100644
--- a/spec/services/ci/image_for_build_service_spec.rb
+++ b/spec/services/ci/image_for_build_service_spec.rb
@@ -5,7 +5,7 @@ module Ci
let(:service) { ImageForBuildService.new }
let(:project) { FactoryGirl.create(:empty_project) }
let(:commit_sha) { '01234567890123456789' }
- let(:pipeline) { project.ensure_pipeline(commit_sha, 'master') }
+ let(:pipeline) { project.ensure_pipeline('master', commit_sha) }
let(:build) { FactoryGirl.create(:ci_build, pipeline: pipeline) }
describe '#execute' do
diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb
index 026d0ca6534..1e21a32a062 100644
--- a/spec/services/ci/register_build_service_spec.rb
+++ b/spec/services/ci/register_build_service_spec.rb
@@ -151,6 +151,25 @@ module Ci
it { expect(build.runner).to eq(specific_runner) }
end
end
+
+ context 'disallow when builds are disabled' do
+ before do
+ project.update(shared_runners_enabled: true)
+ project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
+ end
+
+ context 'and uses shared runner' do
+ let(:build) { service.execute(shared_runner) }
+
+ it { expect(build).to be_nil }
+ end
+
+ context 'and uses specific runner' do
+ let(:build) { service.execute(specific_runner) }
+
+ it { expect(build).to be_nil }
+ end
+ end
end
end
end
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 232508cda23..0d586e2216b 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -99,14 +99,14 @@ describe MergeRequests::BuildService, services: true do
let(:source_branch) { "#{issue.iid}-fix-issue" }
it 'appends "Closes #$issue-iid" to the description' do
- expect(merge_request.description).to eq("#{commit_1.safe_message.split(/\n+/, 2).last}\nCloses ##{issue.iid}")
+ expect(merge_request.description).to eq("#{commit_1.safe_message.split(/\n+/, 2).last}\n\nCloses ##{issue.iid}")
end
context 'merge request already has a description set' do
let(:description) { 'Merge request description' }
it 'appends "Closes #$issue-iid" to the description' do
- expect(merge_request.description).to eq("#{description}\nCloses ##{issue.iid}")
+ expect(merge_request.description).to eq("#{description}\n\nCloses ##{issue.iid}")
end
end
diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb
index 8a4b76367e3..3a71776e81f 100644
--- a/spec/services/merge_requests/get_urls_service_spec.rb
+++ b/spec/services/merge_requests/get_urls_service_spec.rb
@@ -50,7 +50,7 @@ describe MergeRequests::GetUrlsService do
let(:changes) { new_branch_changes }
before do
- project.merge_requests_enabled = false
+ project.project_feature.update_attribute(:merge_requests_access_level, ProjectFeature::DISABLED)
end
it_behaves_like 'no_merge_request_url'
diff --git a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
index c4b87468275..807f89e80b7 100644
--- a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
+++ b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
@@ -6,7 +6,7 @@ describe MergeRequests::MergeRequestDiffCacheService do
describe '#execute' do
it 'retrieves the diff files to cache the highlighted result' do
merge_request = create(:merge_request)
- cache_key = [merge_request.merge_request_diff, 'highlighted-diff-files', Gitlab::Diff::FileCollection::MergeRequest.default_options]
+ cache_key = [merge_request.merge_request_diff, 'highlighted-diff-files', Gitlab::Diff::FileCollection::MergeRequestDiff.default_options]
expect(Rails.cache).to receive(:read).with(cache_key).and_return({})
expect(Rails.cache).to receive(:write).with(cache_key, anything)
diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb
new file mode 100644
index 00000000000..d71932458fa
--- /dev/null
+++ b/spec/services/merge_requests/resolve_service_spec.rb
@@ -0,0 +1,87 @@
+require 'spec_helper'
+
+describe MergeRequests::ResolveService do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ let(:fork_project) do
+ create(:forked_project_with_submodules) do |fork_project|
+ fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
+ fork_project.save
+ end
+ end
+
+ let(:merge_request) do
+ create(:merge_request,
+ source_branch: 'conflict-resolvable', source_project: project,
+ target_branch: 'conflict-start')
+ end
+
+ let(:merge_request_from_fork) do
+ create(:merge_request,
+ source_branch: 'conflict-resolvable-fork', source_project: fork_project,
+ target_branch: 'conflict-start', target_project: project)
+ end
+
+ describe '#execute' do
+ context 'with valid params' do
+ let(:params) do
+ {
+ sections: {
+ '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
+ },
+ commit_message: 'This is a commit message!'
+ }
+ end
+
+ context 'when the source and target project are the same' do
+ before do
+ MergeRequests::ResolveService.new(project, user, params).execute(merge_request)
+ end
+
+ it 'creates a commit with the message' do
+ expect(merge_request.source_branch_head.message).to eq(params[:commit_message])
+ end
+
+ it 'creates a commit with the correct parents' do
+ expect(merge_request.source_branch_head.parents.map(&:id)).
+ to eq(['1450cd639e0bc6721eb02800169e464f212cde06',
+ '75284c70dd26c87f2a3fb65fd5a1f0b0138d3a6b'])
+ end
+ end
+
+ context 'when the source project is a fork and does not contain the HEAD of the target branch' do
+ let!(:target_head) do
+ project.repository.commit_file(user, 'new-file-in-target', '', 'Add new file in target', 'conflict-start', false)
+ end
+
+ before do
+ MergeRequests::ResolveService.new(fork_project, user, params).execute(merge_request_from_fork)
+ end
+
+ it 'creates a commit with the message' do
+ expect(merge_request_from_fork.source_branch_head.message).to eq(params[:commit_message])
+ end
+
+ it 'creates a commit with the correct parents' do
+ expect(merge_request_from_fork.source_branch_head.parents.map(&:id)).
+ to eq(['404fa3fc7c2c9b5dacff102f353bdf55b1be2813',
+ target_head])
+ end
+ end
+ end
+
+ context 'when a resolution is missing' do
+ let(:invalid_params) { { sections: { '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' } } }
+ let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) }
+
+ it 'raises a MissingResolution error' do
+ expect { service.execute(merge_request) }.
+ to raise_error(Gitlab::Conflict::File::MissingResolution)
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index bbced59ff02..3ea1273abc3 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -69,7 +69,7 @@ describe Projects::CreateService, services: true do
context 'wiki_enabled false does not create wiki repository directory' do
before do
- @opts.merge!(wiki_enabled: false)
+ @opts.merge!( { project_feature_attributes: { wiki_access_level: ProjectFeature::DISABLED } })
@project = create_project(@user, @opts)
@path = ProjectWiki.new(@project, @user).send(:path_to_repo)
end
@@ -85,7 +85,7 @@ describe Projects::CreateService, services: true do
context 'global builds_enabled false does not enable CI by default' do
before do
- @opts.merge!(builds_enabled: false)
+ project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
end
it { is_expected.to be_falsey }
@@ -93,7 +93,7 @@ describe Projects::CreateService, services: true do
context 'global builds_enabled true does enable CI by default' do
before do
- @opts.merge!(builds_enabled: true)
+ project.project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
end
it { is_expected.to be_truthy }
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 00427d6db2a..3d854a959f3 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -330,13 +330,13 @@ describe SystemNoteService, services: true do
let(:mentioner) { project2.repository.commit }
it 'references the mentioning commit' do
- expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference(project)}"
+ expect(subject.note).to eq "Mentioned in commit #{mentioner.to_reference(project)}"
end
end
context 'from non-Commit' do
it 'references the mentioning object' do
- expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference(project)}"
+ expect(subject.note).to eq "Mentioned in issue #{mentioner.to_reference(project)}"
end
end
end
@@ -346,13 +346,13 @@ describe SystemNoteService, services: true do
let(:mentioner) { project.repository.commit }
it 'references the mentioning commit' do
- expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference}"
+ expect(subject.note).to eq "Mentioned in commit #{mentioner.to_reference}"
end
end
context 'from non-Commit' do
it 'references the mentioning object' do
- expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference}"
+ expect(subject.note).to eq "Mentioned in issue #{mentioner.to_reference}"
end
end
end
@@ -362,7 +362,7 @@ describe SystemNoteService, services: true do
describe '.cross_reference?' do
it 'is truthy when text begins with expected text' do
- expect(described_class.cross_reference?('mentioned in something')).to be_truthy
+ expect(described_class.cross_reference?('Mentioned in something')).to be_truthy
end
it 'is falsey when text does not begin with expected text' do
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 296fd1bd5a4..cafcad3e3c0 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -496,6 +496,7 @@ describe TodoService, services: true do
describe '#mark_todos_as_done' do
let(:issue) { create(:issue, project: project, author: author, assignee: john_doe) }
+ let(:another_issue) { create(:issue, project: project, author: author, assignee: john_doe) }
it 'marks a relation of todos as done' do
create(:todo, :mentioned, user: john_doe, target: issue, project: project)
@@ -518,6 +519,26 @@ describe TodoService, services: true do
expect(TodoService.new.mark_todos_as_done([todo], john_doe)).to eq(1)
end
+ context 'when some of the todos are done already' do
+ before do
+ create(:todo, :mentioned, user: john_doe, target: issue, project: project)
+ create(:todo, :mentioned, user: john_doe, target: another_issue, project: project)
+ end
+
+ it 'returns the number of those still pending' do
+ TodoService.new.mark_pending_todos_as_done(issue, john_doe)
+
+ expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq(1)
+ end
+
+ it 'returns 0 if all are done' do
+ TodoService.new.mark_pending_todos_as_done(issue, john_doe)
+ TodoService.new.mark_pending_todos_as_done(another_issue, john_doe)
+
+ expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq(0)
+ end
+ end
+
it 'caches the number of todos of a user', :caching do
create(:todo, :mentioned, user: john_doe, target: issue, project: project)
todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project)
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index c144cd85487..02b2b3ca101 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -26,9 +26,9 @@ RSpec.configure do |config|
config.verbose_retry = true
config.display_try_failure_messages = true
- config.include Devise::TestHelpers, type: :controller
- config.include LoginHelpers, type: :feature
- config.include LoginHelpers, type: :request
+ config.include Devise::TestHelpers, type: :controller
+ config.include Warden::Test::Helpers, type: :request
+ config.include LoginHelpers, type: :feature
config.include StubConfiguration
config.include EmailHelpers
config.include TestEnv
diff --git a/spec/support/taskable_shared_examples.rb b/spec/support/taskable_shared_examples.rb
index 927c72c7409..201614e45a4 100644
--- a/spec/support/taskable_shared_examples.rb
+++ b/spec/support/taskable_shared_examples.rb
@@ -3,30 +3,57 @@
# Requires a context containing:
# subject { Issue or MergeRequest }
shared_examples 'a Taskable' do
- before do
- subject.description = <<-EOT.strip_heredoc
- * [ ] Task 1
- * [x] Task 2
- * [x] Task 3
- * [ ] Task 4
- * [ ] Task 5
- EOT
+ describe 'with multiple tasks' do
+ before do
+ subject.description = <<-EOT.strip_heredoc
+ * [ ] Task 1
+ * [x] Task 2
+ * [x] Task 3
+ * [ ] Task 4
+ * [ ] Task 5
+ EOT
+ end
+
+ it 'returns the correct task status' do
+ expect(subject.task_status).to match('2 of')
+ expect(subject.task_status).to match('5 tasks completed')
+ end
+
+ describe '#tasks?' do
+ it 'returns true when object has tasks' do
+ expect(subject.tasks?).to eq true
+ end
+
+ it 'returns false when object has no tasks' do
+ subject.description = 'Now I have no tasks'
+ expect(subject.tasks?).to eq false
+ end
+ end
end
- it 'returns the correct task status' do
- expect(subject.task_status).to match('5 tasks')
- expect(subject.task_status).to match('2 completed')
- expect(subject.task_status).to match('3 remaining')
+ describe 'with an incomplete task' do
+ before do
+ subject.description = <<-EOT.strip_heredoc
+ * [ ] Task 1
+ EOT
+ end
+
+ it 'returns the correct task status' do
+ expect(subject.task_status).to match('0 of')
+ expect(subject.task_status).to match('1 task completed')
+ end
end
- describe '#tasks?' do
- it 'returns true when object has tasks' do
- expect(subject.tasks?).to eq true
+ describe 'with a complete task' do
+ before do
+ subject.description = <<-EOT.strip_heredoc
+ * [x] Task 1
+ EOT
end
- it 'returns false when object has no tasks' do
- subject.description = 'Now I have no tasks'
- expect(subject.tasks?).to eq false
+ it 'returns the correct task status' do
+ expect(subject.task_status).to match('1 of')
+ expect(subject.task_status).to match('1 task completed')
end
end
end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index edbbfc3c9e5..0097dbf8fad 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -6,7 +6,7 @@ module TestEnv
# When developing the seed repository, comment out the branch you will modify.
BRANCH_SHA = {
'empty-branch' => '7efb185',
- 'ends-with.json' => '98b0d8b3',
+ 'ends-with.json' => '98b0d8b',
'flatten-dir' => 'e56497b',
'feature' => '0b4bc9a',
'feature_conflict' => 'bb5206f',
@@ -24,11 +24,12 @@ module TestEnv
'expand-collapse-lines' => '238e82d',
'video' => '8879059',
'crlf-diff' => '5938907',
- 'conflict-start' => '14fa46b',
+ 'conflict-start' => '75284c7',
'conflict-resolvable' => '1450cd6',
'conflict-binary-file' => '259a6fb',
'conflict-contains-conflict-markers' => '5e0964c',
'conflict-missing-side' => 'eb227b3',
+ 'conflict-non-utf8' => 'd0a293c',
'conflict-too-large' => '39fa04f',
}
@@ -36,9 +37,10 @@ module TestEnv
# need to keep all the branches in sync.
# We currently only need a subset of the branches
FORKED_BRANCH_SHA = {
- 'add-submodule-version-bump' => '3f547c08',
- 'master' => '5937ac0',
- 'remove-submodule' => '2a33e0c0'
+ 'add-submodule-version-bump' => '3f547c0',
+ 'master' => '5937ac0',
+ 'remove-submodule' => '2a33e0c',
+ 'conflict-resolvable-fork' => '404fa3f'
}
# Test environment
@@ -116,22 +118,7 @@ module TestEnv
system(*%W(#{Gitlab.config.git.bin_path} clone -q #{clone_url} #{repo_path}))
end
- Dir.chdir(repo_path) do
- branch_sha.each do |branch, sha|
- # Try to reset without fetching to avoid using the network.
- reset = %W(#{Gitlab.config.git.bin_path} update-ref refs/heads/#{branch} #{sha})
- unless system(*reset)
- if system(*%W(#{Gitlab.config.git.bin_path} fetch origin))
- unless system(*reset)
- raise 'The fetched test seed '\
- 'does not contain the required revision.'
- end
- else
- raise 'Could not fetch test seed repository.'
- end
- end
- end
- end
+ set_repo_refs(repo_path, branch_sha)
# We must copy bare repositories because we will push to them.
system(git_env, *%W(#{Gitlab.config.git.bin_path} clone -q --bare #{repo_path} #{repo_path_bare}))
@@ -143,6 +130,7 @@ module TestEnv
FileUtils.mkdir_p(target_repo_path)
FileUtils.cp_r("#{base_repo_path}/.", target_repo_path)
FileUtils.chmod_R 0755, target_repo_path
+ set_repo_refs(target_repo_path, BRANCH_SHA)
end
def repos_path
@@ -159,6 +147,7 @@ module TestEnv
FileUtils.mkdir_p(target_repo_path)
FileUtils.cp_r("#{base_repo_path}/.", target_repo_path)
FileUtils.chmod_R 0755, target_repo_path
+ set_repo_refs(target_repo_path, FORKED_BRANCH_SHA)
end
# When no cached assets exist, manually hit the root path to create them
@@ -208,4 +197,23 @@ module TestEnv
def git_env
{ 'GIT_TEMPLATE_DIR' => '' }
end
+
+ def set_repo_refs(repo_path, branch_sha)
+ Dir.chdir(repo_path) do
+ branch_sha.each do |branch, sha|
+ # Try to reset without fetching to avoid using the network.
+ reset = %W(#{Gitlab.config.git.bin_path} update-ref refs/heads/#{branch} #{sha})
+ unless system(*reset)
+ if system(*%W(#{Gitlab.config.git.bin_path} fetch origin))
+ unless system(*reset)
+ raise 'The fetched test seed '\
+ 'does not contain the required revision.'
+ end
+ else
+ raise 'Could not fetch test seed repository.'
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
new file mode 100644
index 00000000000..31bbb150698
--- /dev/null
+++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+describe 'projects/merge_requests/edit.html.haml' do
+ include Devise::TestHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
+
+ let(:closed_merge_request) do
+ create(:closed_merge_request,
+ source_project: fork_project,
+ target_project: project,
+ author: user)
+ end
+
+ before do
+ assign(:project, project)
+ assign(:merge_request, closed_merge_request)
+
+ allow(view).to receive(:can?).and_return(true)
+ allow(view).to receive(:current_user)
+ .and_return(User.find(closed_merge_request.author_id))
+ end
+
+ context 'when a merge request without fork' do
+ it "shows editable fields" do
+ unlink_project.execute
+ closed_merge_request.reload
+
+ render
+
+ expect(rendered).to have_field('merge_request[title]')
+ expect(rendered).to have_field('merge_request[description]')
+ expect(rendered).to have_selector('#merge_request_assignee_id', visible: false)
+ expect(rendered).to have_selector('#merge_request_milestone_id', visible: false)
+ expect(rendered).not_to have_selector('#merge_request_target_branch', visible: false)
+ end
+ end
+
+ context 'when a merge request with an existing source project is closed' do
+ it "shows editable fields" do
+ render
+
+ expect(rendered).to have_field('merge_request[title]')
+ expect(rendered).to have_field('merge_request[description]')
+ expect(rendered).to have_selector('#merge_request_assignee_id', visible: false)
+ expect(rendered).to have_selector('#merge_request_milestone_id', visible: false)
+ expect(rendered).to have_selector('#merge_request_target_branch', visible: false)
+ end
+ end
+end
diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb
new file mode 100644
index 00000000000..fe0780e72df
--- /dev/null
+++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe 'projects/merge_requests/show.html.haml' do
+ include Devise::TestHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
+
+ let(:closed_merge_request) do
+ create(:closed_merge_request,
+ source_project: fork_project,
+ target_project: project,
+ author: user)
+ end
+
+ before do
+ assign(:project, project)
+ assign(:merge_request, closed_merge_request)
+ assign(:commits_count, 0)
+
+ allow(view).to receive(:can?).and_return(true)
+ end
+
+ context 'when the merge request is closed' do
+ it 'shows the "Reopen" button' do
+ render
+
+ expect(rendered).to have_css('a', visible: true, text: 'Reopen')
+ expect(rendered).to have_css('a', visible: false, text: 'Close')
+ end
+
+ it 'does not show the "Reopen" button when the source project does not exist' do
+ unlink_project.execute
+ closed_merge_request.reload
+
+ render
+
+ expect(rendered).to have_css('a', visible: false, text: 'Reopen')
+ expect(rendered).to have_css('a', visible: false, text: 'Close')
+ end
+ end
+end
diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb
index 05e07789dac..59cfb2c8e3a 100644
--- a/spec/workers/repository_check/single_repository_worker_spec.rb
+++ b/spec/workers/repository_check/single_repository_worker_spec.rb
@@ -5,7 +5,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
subject { described_class.new }
it 'passes when the project has no push events' do
- project = create(:project_empty_repo, wiki_enabled: false)
+ project = create(:project_empty_repo, wiki_access_level: ProjectFeature::DISABLED)
project.events.destroy_all
break_repo(project)
@@ -25,7 +25,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
end
it 'fails if the wiki repository is broken' do
- project = create(:project_empty_repo, wiki_enabled: true)
+ project = create(:project_empty_repo, wiki_access_level: ProjectFeature::ENABLED)
project.create_wiki
# Test sanity: everything should be fine before the wiki repo is broken
@@ -39,7 +39,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
end
it 'skips wikis when disabled' do
- project = create(:project_empty_repo, wiki_enabled: false)
+ project = create(:project_empty_repo, wiki_access_level: ProjectFeature::DISABLED)
# Make sure the test would fail if the wiki repo was checked
break_wiki(project)
@@ -49,7 +49,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
end
it 'creates missing wikis' do
- project = create(:project_empty_repo, wiki_enabled: true)
+ project = create(:project_empty_repo, wiki_access_level: ProjectFeature::ENABLED)
FileUtils.rm_rf(wiki_path(project))
subject.perform(project.id)