summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml74
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile17
-rw-r--r--Gemfile.lock61
-rw-r--r--README.md7
-rw-r--r--app/assets/javascripts/blob/blob_ci_yaml.js42
-rw-r--r--app/assets/javascripts/blob/blob_dockerfile_selector.js19
-rw-r--r--app/assets/javascripts/blob/blob_dockerfile_selectors.js27
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js121
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selector.js23
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selectors.js26
-rw-r--r--app/assets/javascripts/blob/blob_license_selector.js28
-rw-r--r--app/assets/javascripts/blob/blob_license_selectors.js23
-rw-r--r--app/assets/javascripts/blob/template_selector.js101
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selector.js9
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selectors.js23
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_dockerfile_selector.js9
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_dockerfile_selectors.js23
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_gitignore_selector.js9
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_gitignore_selectors.js23
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_license_selector.js13
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_license_selectors.js24
-rw-r--r--app/assets/javascripts/blob/template_selectors/template_selector.js92
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js32
-rw-r--r--app/assets/javascripts/blob_edit/blob_edit_bundle.js15
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js177
-rw-r--r--app/assets/javascripts/boards/components/modal/filters.js2
-rw-r--r--app/assets/javascripts/boards/components/modal/index.js15
-rw-r--r--app/assets/javascripts/boards/stores/modal_store.js1
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.js31
-rw-r--r--app/assets/javascripts/commons/polyfills.js2
-rw-r--r--app/assets/javascripts/cycle_analytics/components/limit_warning_component.js17
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_code_component.js1
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_issue_component.js1
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_plan_component.js7
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_production_component.js1
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_review_component.js1
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_staging_component.js1
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_test_component.js1
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js2
-rw-r--r--app/assets/javascripts/diff.js11
-rw-r--r--app/assets/javascripts/droplab/droplab_filter.js2
-rw-r--r--app/assets/javascripts/group_name.js42
-rw-r--r--app/assets/javascripts/lib/utils/poll.js21
-rw-r--r--app/assets/javascripts/main.js9
-rw-r--r--app/assets/javascripts/merge_request_tabs.js3
-rw-r--r--app/assets/javascripts/right_sidebar.js2
-rw-r--r--app/assets/javascripts/templates/issuable_template_selector.js14
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/empty_state.js33
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/error_state.js19
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/nav_controls.js52
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js68
-rw-r--r--app/assets/javascripts/vue_pipelines_index/index.js8
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipelines.js191
-rw-r--r--app/assets/stylesheets/framework/buttons.scss27
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss2
-rw-r--r--app/assets/stylesheets/framework/forms.scss14
-rw-r--r--app/assets/stylesheets/framework/header.scss56
-rw-r--r--app/assets/stylesheets/framework/icons.scss12
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss2
-rw-r--r--app/assets/stylesheets/framework/layout.scss20
-rw-r--r--app/assets/stylesheets/framework/lists.scss2
-rw-r--r--app/assets/stylesheets/framework/nav.scss12
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss4
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss7
-rw-r--r--app/assets/stylesheets/framework/variables.scss220
-rw-r--r--app/assets/stylesheets/pages/boards.scss4
-rw-r--r--app/assets/stylesheets/pages/builds.scss6
-rw-r--r--app/assets/stylesheets/pages/commits.scss4
-rw-r--r--app/assets/stylesheets/pages/groups.scss7
-rw-r--r--app/assets/stylesheets/pages/issuable.scss4
-rw-r--r--app/assets/stylesheets/pages/issues.scss10
-rw-r--r--app/assets/stylesheets/pages/login.scss4
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss2
-rw-r--r--app/assets/stylesheets/pages/milestone.scss2
-rw-r--r--app/assets/stylesheets/pages/notes.scss7
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss57
-rw-r--r--app/assets/stylesheets/pages/profile.scss1
-rw-r--r--app/assets/stylesheets/pages/projects.scss69
-rw-r--r--app/assets/stylesheets/pages/sherlock.scss2
-rw-r--r--app/assets/stylesheets/pages/status.scss68
-rw-r--r--app/assets/stylesheets/pages/todos.scss30
-rw-r--r--app/controllers/admin/users_controller.rb12
-rw-r--r--app/controllers/projects/builds_controller.rb6
-rw-r--r--app/controllers/projects/issues_controller.rb9
-rwxr-xr-x[-rw-r--r--]app/controllers/projects/merge_requests_controller.rb12
-rw-r--r--app/controllers/projects/milestones_controller.rb9
-rw-r--r--app/controllers/projects/pipelines_controller.rb6
-rw-r--r--app/controllers/registrations_controller.rb11
-rw-r--r--app/finders/labels_finder.rb13
-rw-r--r--app/helpers/milestones_helper.rb2
-rw-r--r--app/helpers/namespaces_helper.rb8
-rw-r--r--app/helpers/nav_helper.rb6
-rw-r--r--app/helpers/sorting_helper.rb28
-rw-r--r--app/helpers/users_helper.rb7
-rw-r--r--app/models/merge_request.rb6
-rw-r--r--app/models/milestone.rb15
-rw-r--r--app/models/project.rb13
-rw-r--r--app/models/user.rb25
-rw-r--r--app/serializers/build_entity.rb7
-rw-r--r--app/serializers/build_serializer.rb8
-rw-r--r--app/serializers/pipeline_entity.rb11
-rw-r--r--app/serializers/pipeline_serializer.rb7
-rw-r--r--app/serializers/status_entity.rb2
-rw-r--r--app/services/ci/retry_pipeline_service.rb4
-rw-r--r--app/services/users/create_service.rb110
-rw-r--r--app/views/admin/users/_access_levels.html.haml2
-rw-r--r--app/views/dashboard/todos/index.html.haml3
-rw-r--r--app/views/explore/projects/_filter.html.haml21
-rw-r--r--app/views/layouts/_page.html.haml3
-rw-r--r--app/views/layouts/header/_default.html.haml13
-rw-r--r--app/views/projects/blob/_upload.html.haml9
-rw-r--r--app/views/projects/blob/edit.html.haml2
-rw-r--r--app/views/projects/blob/new.html.haml2
-rw-r--r--app/views/projects/builds/_sidebar.html.haml5
-rw-r--r--app/views/projects/builds/show.html.haml2
-rw-r--r--app/views/projects/commit/_pipelines_list.haml1
-rw-r--r--app/views/projects/environments/terminal.html.haml2
-rw-r--r--app/views/projects/merge_requests/merge.js.haml3
-rw-r--r--app/views/projects/milestones/index.html.haml1
-rw-r--r--app/views/projects/new.html.haml2
-rw-r--r--app/views/projects/pipelines/_head.html.haml2
-rw-r--r--app/views/projects/pipelines/_info.html.haml10
-rw-r--r--app/views/projects/pipelines/index.html.haml60
-rw-r--r--app/views/projects/wikis/_sidebar.html.haml5
-rw-r--r--app/views/shared/_group_form.html.haml4
-rw-r--r--app/views/shared/_milestones_filter.html.haml4
-rw-r--r--app/views/shared/_milestones_sort_dropdown.html.haml22
-rw-r--r--app/views/shared/empty_states/icons/_pipelines_empty.svg1
-rw-r--r--app/views/shared/empty_states/icons/_pipelines_failed.svg1
-rw-r--r--app/views/shared/groups/_group.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/projects/_list.html.haml2
-rw-r--r--app/views/shared/projects/_project.html.haml60
-rw-r--r--app/views/users/show.html.haml9
-rw-r--r--changelogs/unreleased/12818-expose-simple-cicd-status-endpoints-with-status-serializer-gitlab-ci-status-for-pipeline-job-and-merge-request.yml5
-rw-r--r--changelogs/unreleased/22850-404-when-requesting-build-trace.yml4
-rw-r--r--changelogs/unreleased/23862-fix-group-project-count.yml4
-rw-r--r--changelogs/unreleased/25188-polyfill-es-symbol.yml4
-rw-r--r--changelogs/unreleased/27574-pipelines-empty-state.yml4
-rw-r--r--changelogs/unreleased/27878-new-service-for-creating-user.yml4
-rw-r--r--changelogs/unreleased/27910-admin-can-create-project-in-all-groups.yml4
-rw-r--r--changelogs/unreleased/28614-harmonious-color-palette.yml4
-rw-r--r--changelogs/unreleased/28634-todos-margin.yml4
-rw-r--r--changelogs/unreleased/29116-maxint-error.yml4
-rw-r--r--changelogs/unreleased/29897-remove-force-scroll-for-mr-changes-diff.yml4
-rw-r--r--changelogs/unreleased/29930-fix-profile-cover-button-a11y.yml4
-rw-r--r--changelogs/unreleased/30035-milestone-with-due-date-shows-escaped-html.yml4
-rw-r--r--changelogs/unreleased/4195-add-sorting-to-project-milestones.yml4
-rw-r--r--changelogs/unreleased/add-issue-modal-loading-indicator.yml4
-rw-r--r--changelogs/unreleased/fix-ci-api-regression-for-after-script.yml4
-rw-r--r--changelogs/unreleased/fix-gb-pipeline-retry-only-latest-jobs.yml5
-rw-r--r--changelogs/unreleased/fix-issue-23237.yml4
-rw-r--r--changelogs/unreleased/labels-finder-optimize-project.yml4
-rw-r--r--changelogs/unreleased/projects-list-line-breaks.yml4
-rw-r--r--changelogs/unreleased/sh-remove-tags-from-explore.yml4
-rw-r--r--changelogs/unreleased/slow-search-changelog.yml4
-rw-r--r--changelogs/unreleased/tc-pipeline-show-trigger-date.yml4
-rw-r--r--changelogs/unreleased/user-callout-showing-on-all-profiles.yml4
-rw-r--r--changelogs/unreleased/user-profile-join-date.yml4
-rw-r--r--config/routes/project.rb2
-rw-r--r--config/webpack.config.js2
-rw-r--r--db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb2
-rw-r--r--doc/administration/high_availability/database.md2
-rw-r--r--doc/api/README.md135
-rw-r--r--doc/api/users.md121
-rw-r--r--doc/api/v3_to_v4.md1
-rw-r--r--doc/ci/triggers/README.md24
-rw-r--r--doc/ci/triggers/img/triggers_page.pngbin5116 -> 110560 bytes
-rw-r--r--doc/user/project/new_ci_build_permissions_model.md10
-rw-r--r--features/steps/project/issues/award_emoji.rb2
-rw-r--r--features/steps/shared/diff_note.rb4
-rw-r--r--features/steps/shared/markdown.rb2
-rw-r--r--features/steps/shared/note.rb2
-rw-r--r--lib/api/users.rb27
-rw-r--r--lib/api/v3/users.rb53
-rw-r--r--lib/gitlab/ci/build/step.rb9
-rw-r--r--lib/gitlab/ci/status/canceled.rb4
-rw-r--r--lib/gitlab/ci/status/core.rb4
-rw-r--r--lib/gitlab/ci/status/created.rb4
-rw-r--r--lib/gitlab/ci/status/failed.rb4
-rw-r--r--lib/gitlab/ci/status/manual.rb4
-rw-r--r--lib/gitlab/ci/status/pending.rb4
-rw-r--r--lib/gitlab/ci/status/running.rb4
-rw-r--r--lib/gitlab/ci/status/skipped.rb4
-rw-r--r--lib/gitlab/ci/status/success.rb4
-rw-r--r--lib/gitlab/ee_compat_check.rb295
-rw-r--r--lib/gitlab/o_auth/user.rb6
-rw-r--r--package.json1
-rwxr-xr-xscripts/merge-reports1
-rwxr-xr-xscripts/prepare_build.sh2
-rwxr-xr-xscripts/sync-reports95
-rw-r--r--spec/controllers/projects/builds_controller_spec.rb33
-rw-r--r--spec/controllers/projects/builds_controller_specs.rb47
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb1
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb38
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb20
-rw-r--r--spec/controllers/registrations_controller_spec.rb9
-rw-r--r--spec/factories/ci/builds.rb2
-rw-r--r--spec/features/admin/admin_broadcast_messages_spec.rb2
-rw-r--r--spec/features/commits_spec.rb13
-rw-r--r--spec/features/explore/groups_list_spec.rb20
-rw-r--r--spec/features/groups/group_name_toggle_spec.rb47
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb27
-rw-r--r--spec/features/merge_requests/toggler_behavior_spec.rb2
-rw-r--r--spec/features/projects/compare_spec.rb3
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb2
-rw-r--r--spec/features/projects/milestones/milestones_sorting_spec.rb52
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb2
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb2
-rw-r--r--spec/features/user_callout_spec.rb8
-rw-r--r--spec/helpers/avatars_helper_spec.rb21
-rw-r--r--spec/helpers/namespaces_helper_spec.rb33
-rw-r--r--spec/helpers/users_helper_spec.rb17
-rw-r--r--spec/javascripts/boards/boards_store_spec.js14
-rw-r--r--spec/javascripts/boards/list_spec.js10
-rw-r--r--spec/javascripts/boards/mock_data.js8
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js7
-rw-r--r--spec/javascripts/cycle_analytics/limit_warning_component_spec.js39
-rw-r--r--spec/javascripts/fixtures/pipelines.html.haml14
-rw-r--r--spec/javascripts/fixtures/pipelines_table.html.haml3
-rw-r--r--spec/javascripts/lib/utils/poll_spec.js36
-rw-r--r--spec/javascripts/test_bundle.js3
-rw-r--r--spec/javascripts/vue_pipelines_index/empty_state_spec.js38
-rw-r--r--spec/javascripts/vue_pipelines_index/error_state_spec.js23
-rw-r--r--spec/javascripts/vue_pipelines_index/mock_data.js107
-rw-r--r--spec/javascripts/vue_pipelines_index/nav_controls_spec.js93
-rw-r--r--spec/javascripts/vue_pipelines_index/pipelines_spec.js114
-rw-r--r--spec/lib/gitlab/ci/build/step_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/factory_spec.rb9
-rw-r--r--spec/lib/gitlab/ci/status/canceled_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/created_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/failed_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/manual_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/pending_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/running_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/skipped_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/success_spec.rb4
-rw-r--r--spec/lib/gitlab/git/blob_snippet_spec.rb2
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb8
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb28
-rw-r--r--spec/lib/gitlab/git/compare_spec.rb10
-rw-r--r--spec/lib/gitlab/git/diff_collection_spec.rb4
-rw-r--r--spec/lib/gitlab/git/tree_spec.rb10
-rw-r--r--spec/lib/gitlab/git/util_spec.rb2
-rw-r--r--spec/lib/gitlab/ldap/user_spec.rb2
-rw-r--r--spec/models/hooks/system_hook_spec.rb5
-rw-r--r--spec/models/milestone_spec.rb2
-rw-r--r--spec/models/namespace_spec.rb2
-rw-r--r--spec/models/pages_domain_spec.rb14
-rw-r--r--spec/models/repository_spec.rb2
-rw-r--r--spec/models/user_spec.rb13
-rw-r--r--spec/requests/api/projects_spec.rb2
-rw-r--r--spec/requests/api/v3/projects_spec.rb2
-rw-r--r--spec/requests/api/v3/users_spec.rb14
-rw-r--r--spec/serializers/build_entity_spec.rb13
-rw-r--r--spec/serializers/build_serializer_spec.rb45
-rw-r--r--spec/serializers/deployment_entity_spec.rb9
-rw-r--r--spec/serializers/pipeline_entity_spec.rb2
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb16
-rw-r--r--spec/serializers/status_entity_spec.rb2
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb13
-rw-r--r--spec/services/milestones/close_service_spec.rb2
-rw-r--r--spec/services/projects/fork_service_spec.rb4
-rw-r--r--spec/services/users/create_service_spec.rb182
-rw-r--r--spec/support/test_env.rb5
-rw-r--r--spec/views/projects/pipelines/show.html.haml_spec.rb10
-rw-r--r--yarn.lock4
270 files changed, 3639 insertions, 1508 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 997d8268008..ad643b41a8f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -12,6 +12,8 @@ variables:
GIT_DEPTH: "20"
PHANTOMJS_VERSION: "2.1.1"
GET_SOURCES_ATTEMPTS: "3"
+ KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-${CI_COMMIT_REF_SLUG}.json
+ KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-${CI_COMMIT_REF_SLUG}.json
before_script:
- bundle --version
@@ -34,14 +36,15 @@ stages:
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
+ KNAPSACK_S3_BUCKET: "gitlab-ce-cache"
cache:
key: "knapsack"
paths:
- - knapsack/
+ - knapsack/
artifacts:
expire_in: 31d
paths:
- - knapsack/
+ - knapsack/
.use-pg: &use-pg
services:
@@ -74,17 +77,17 @@ stages:
- JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[-2]}
- export CI_NODE_TOTAL=${JOB_NAME[-1]}
- - export KNAPSACK_REPORT_PATH=knapsack/rspec_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
+ - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
- - cp knapsack/rspec_report.json ${KNAPSACK_REPORT_PATH}
+ - cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- knapsack rspec "--color --format documentation"
artifacts:
expire_in: 31d
when: always
paths:
- - coverage/
- - knapsack/
- - tmp/capybara/
+ - coverage/
+ - knapsack/
+ - tmp/capybara/
.rspec-knapsack-pg: &rspec-knapsack-pg
<<: *rspec-knapsack
@@ -102,17 +105,17 @@ stages:
- JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[-2]}
- export CI_NODE_TOTAL=${JOB_NAME[-1]}
- - export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
+ - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
- - cp knapsack/spinach_report.json ${KNAPSACK_REPORT_PATH}
+ - cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
artifacts:
expire_in: 31d
when: always
paths:
- - coverage/
- - knapsack/
- - tmp/capybara/
+ - coverage/
+ - knapsack/
+ - tmp/capybara/
.spinach-knapsack-pg: &spinach-knapsack-pg
<<: *spinach-knapsack
@@ -124,15 +127,31 @@ stages:
<<: *only-master-and-ee-or-mysql
# Prepare and merge knapsack tests
-
knapsack:
<<: *knapsack-state
<<: *dedicated-runner
stage: prepare
script:
- - mkdir -p knapsack/
- - '[[ -f knapsack/rspec_report.json ]] || echo "{}" > knapsack/rspec_report.json'
- - '[[ -f knapsack/spinach_report.json ]] || echo "{}" > knapsack/spinach_report.json'
+ - mkdir -p knapsack/${CI_PROJECT_NAME}/
+ - wget -O $KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${KNAPSACK_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_RSPEC_SUITE_REPORT_PATH || true
+ - wget -O $KNAPSACK_SPINACH_SUITE_REPORT_PATH http://${KNAPSACK_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_SPINACH_SUITE_REPORT_PATH || true
+ - '[[ -f $KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_RSPEC_SUITE_REPORT_PATH}'
+ - '[[ -f $KNAPSACK_SPINACH_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_SPINACH_SUITE_REPORT_PATH}'
+
+update-knapsack:
+ <<: *knapsack-state
+ <<: *dedicated-runner
+ stage: post-test
+ script:
+ - scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec_node_*.json
+ - scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach_node_*.json
+ - '[[ -z ${KNAPSACK_S3_BUCKET} ]] || scripts/sync-reports put $KNAPSACK_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
+ - rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
+ only:
+ - master@gitlab-org/gitlab-ce
+ - master@gitlab-org/gitlab-ee
+ - master@gitlab/gitlabhq
+ - master@gitlab/gitlab-ee
setup-test-env:
<<: *use-pg
@@ -152,20 +171,6 @@ setup-test-env:
- public/assets
- tmp/tests
-update-knapsack:
- <<: *knapsack-state
- <<: *dedicated-runner
- stage: post-test
- script:
- - scripts/merge-reports knapsack/rspec_report.json knapsack/rspec_node_*.json
- - scripts/merge-reports knapsack/spinach_report.json knapsack/spinach_node_*.json
- - rm -f knapsack/*_node_*.json
- only:
- - master@gitlab-org/gitlab-ce
- - master@gitlab-org/gitlab-ee
- - master@gitlab/gitlabhq
- - master@gitlab/gitlab-ee
-
rspec pg 0 20: *rspec-knapsack-pg
rspec pg 1 20: *rspec-knapsack-pg
rspec pg 2 20: *rspec-knapsack-pg
@@ -268,10 +273,9 @@ rake ee_compat_check:
- /^[\d-]+-stable(-ee)?$/
allow_failure: yes
cache:
- key: "ruby233-ee_compat_check_repo"
+ key: "ee_compat_check_repo"
paths:
- - ee_compat_check/repo/
- - vendor/ruby
+ - ee_compat_check/ee-repo/
artifacts:
name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}"
when: on_failure
@@ -371,6 +375,7 @@ rake karma:
BABEL_ENV: "coverage"
script:
- bundle exec rake karma
+ coverage: '/^Statements *: (\d+\.\d+%)/'
artifacts:
name: coverage-javascript
expire_in: 31d
@@ -402,7 +407,7 @@ bundler:audit:
- master@gitlab/gitlabhq
- master@gitlab/gitlab-ee
script:
- - "bundle exec bundle-audit check --update"
+ - "bundle exec bundle-audit check --update --ignore CVE-2016-4658"
.migration-paths: &migration-paths
stage: test
@@ -447,6 +452,7 @@ coverage:
USE_BUNDLE_INSTALL: "true"
script:
- bundle exec scripts/merge-simplecov
+ coverage: '/LOC \((\d+\.\d+%)\) covered.$/'
artifacts:
name: coverage
expire_in: 31d
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 0d91a54c7d4..1d0ba9ea182 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.3.0
+0.4.0
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 347f5833ee6..9df886c42a1 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-1.4.1
+1.4.2
diff --git a/Gemfile b/Gemfile
index 38158387642..c4368d79d5d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -244,7 +244,7 @@ gem 'net-ssh', '~> 3.0.1'
gem 'base32', '~> 0.3.0'
# Sentry integration
-gem 'sentry-raven', '~> 2.0.0'
+gem 'sentry-raven', '~> 2.4.0'
gem 'premailer-rails', '~> 1.9.0'
@@ -257,15 +257,14 @@ end
group :development do
gem 'foreman', '~> 0.78.0'
- gem 'brakeman', '~> 3.4.0', require: false
+ gem 'brakeman', '~> 3.6.0', require: false
gem 'letter_opener_web', '~> 1.3.0'
- gem 'bullet', '~> 5.2.0', require: false
+ gem 'bullet', '~> 5.5.0', require: false
gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false
- gem 'web-console', '~> 2.0'
# Better errors handler
- gem 'better_errors', '~> 1.0.1'
+ gem 'better_errors', '~> 2.1.0'
gem 'binding_of_caller', '~> 0.7.2'
# thin instead webrick
@@ -297,7 +296,7 @@ group :development, :test do
gem 'capybara-screenshot', '~> 1.0.0'
gem 'poltergeist', '~> 1.9.0'
- gem 'spring', '~> 1.7.0'
+ gem 'spring', '~> 2.0.0'
gem 'spring-commands-rspec', '~> 1.0.4'
gem 'spring-commands-spinach', '~> 1.1.0'
@@ -305,8 +304,8 @@ group :development, :test do
gem 'rubocop-rspec', '~> 1.12.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false
gem 'haml_lint', '~> 0.21.0', require: false
- gem 'simplecov', '0.12.0', require: false
- gem 'flay', '~> 2.6.1', require: false
+ gem 'simplecov', '~> 0.14.0', require: false
+ gem 'flay', '~> 2.8.0', require: false
gem 'bundler-audit', '~> 0.5.0', require: false
gem 'benchmark-ips', '~> 2.3.0', require: false
@@ -323,7 +322,7 @@ group :test do
gem 'shoulda-matchers', '~> 2.8.0', require: false
gem 'email_spec', '~> 1.6.0'
gem 'json-schema', '~> 2.6.2'
- gem 'webmock', '~> 1.21.0'
+ gem 'webmock', '~> 1.24.0'
gem 'test_after_commit', '~> 1.1'
gem 'sham_rack', '~> 1.3.6'
gem 'timecop', '~> 0.8.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 07be5d7aded..2cb0e88962a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -75,19 +75,20 @@ GEM
base32 (0.3.2)
bcrypt (3.1.11)
benchmark-ips (2.3.0)
- better_errors (1.0.1)
+ better_errors (2.1.1)
coderay (>= 1.0.0)
erubis (>= 2.6.6)
+ rack (>= 0.9.0)
bindata (2.3.5)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
- brakeman (3.4.1)
+ brakeman (3.6.1)
browser (2.2.0)
builder (3.2.3)
- bullet (5.2.0)
+ bullet (5.5.1)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.10.0)
bundler-audit (0.5.0)
@@ -101,7 +102,7 @@ GEM
rack (>= 1.0.0)
rack-test (>= 0.5.4)
xpath (~> 2.0)
- capybara-screenshot (1.0.11)
+ capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3)
launchy
carrierwave (0.11.2)
@@ -117,7 +118,7 @@ GEM
numerizer (~> 0.1.1)
chunky_png (1.3.5)
cliver (0.3.2)
- coderay (1.1.0)
+ coderay (1.1.1)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
coffee-rails (4.1.1)
@@ -200,7 +201,9 @@ GEM
multi_json
ffaker (2.4.0)
ffi (1.9.10)
- flay (2.6.1)
+ flay (2.8.1)
+ erubis (~> 2.7.0)
+ path_expander (~> 1.0)
ruby_parser (~> 3.0)
sexp_processor (~> 4.0)
flowdock (0.7.1)
@@ -340,6 +343,7 @@ GEM
temple (~> 0.7.6)
thor
tilt
+ hashdiff (0.3.2)
hashie (3.5.5)
health_check (2.6.0)
rails (>= 4.0)
@@ -518,6 +522,7 @@ GEM
activerecord (>= 4.0, < 5.1)
parser (2.4.0.0)
ast (~> 2.2)
+ path_expander (1.0.1)
pg (0.18.4)
poltergeist (1.9.0)
capybara (~> 2.1)
@@ -532,14 +537,14 @@ GEM
premailer-rails (1.9.2)
actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9)
- pry (0.10.3)
+ pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
- pry-byebug (3.4.1)
+ pry-byebug (3.4.2)
byebug (~> 9.0)
pry (~> 0.10)
- pry-rails (0.3.4)
+ pry-rails (0.3.5)
pry (>= 0.9.10)
pyu-ruby-sasl (0.0.3.3)
rack (1.6.5)
@@ -671,7 +676,7 @@ GEM
ruby-progressbar (1.8.1)
ruby-saml (1.4.1)
nokogiri (>= 1.5.10)
- ruby_parser (3.8.2)
+ ruby_parser (3.8.4)
sexp_processor (~> 4.1)
rubyntlm (0.5.2)
rubypants (0.2.0)
@@ -700,10 +705,10 @@ GEM
activesupport (>= 3.1)
select2-rails (3.5.9.3)
thor (~> 0.14)
- sentry-raven (2.0.2)
- faraday (>= 0.7.6, < 0.10.x)
+ sentry-raven (2.4.0)
+ faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9)
- sexp_processor (4.7.0)
+ sexp_processor (4.8.0)
sham_rack (1.3.6)
rack
shoulda-matchers (2.8.0)
@@ -724,7 +729,7 @@ GEM
faraday (~> 0.9)
jwt (~> 1.5)
multi_json (~> 1.10)
- simplecov (0.12.0)
+ simplecov (0.14.1)
docile (~> 1.1.0)
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
@@ -741,7 +746,8 @@ GEM
spinach (>= 0.4)
spinach-rerun-reporter (0.0.2)
spinach (~> 0.8)
- spring (1.7.2)
+ spring (2.0.1)
+ activesupport (>= 4.2)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
spring-commands-spinach (1.1.0)
@@ -813,14 +819,10 @@ GEM
vmstat (2.3.0)
warden (1.2.6)
rack (>= 1.0)
- web-console (2.3.0)
- activemodel (>= 4.0)
- binding_of_caller (>= 0.7.2)
- railties (>= 4.0)
- sprockets-rails (>= 2.0, < 4.0)
- webmock (1.21.0)
+ webmock (1.24.6)
addressable (>= 2.3.6)
crack (>= 0.3.2)
+ hashdiff
webpack-rails (0.9.9)
rails (>= 3.2.0)
websocket-driver (0.6.3)
@@ -854,12 +856,12 @@ DEPENDENCIES
babosa (~> 1.0.2)
base32 (~> 0.3.0)
benchmark-ips (~> 2.3.0)
- better_errors (~> 1.0.1)
+ better_errors (~> 2.1.0)
binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0)
- brakeman (~> 3.4.0)
+ brakeman (~> 3.6.0)
browser (~> 2.2)
- bullet (~> 5.2.0)
+ bullet (~> 5.5.0)
bundler-audit (~> 0.5.0)
capybara (~> 2.6.2)
capybara-screenshot (~> 1.0.0)
@@ -885,7 +887,7 @@ DEPENDENCIES
email_spec (~> 1.6.0)
factory_girl_rails (~> 4.7.0)
ffaker (~> 2.4)
- flay (~> 2.6.1)
+ flay (~> 2.8.0)
fog-aws (~> 0.9)
fog-core (~> 1.40)
fog-google (~> 0.5)
@@ -991,18 +993,18 @@ DEPENDENCIES
scss_lint (~> 0.47.0)
seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9)
- sentry-raven (~> 2.0.0)
+ sentry-raven (~> 2.4.0)
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0)
sidekiq (~> 4.2.7)
sidekiq-cron (~> 0.4.4)
sidekiq-limit_fetch (~> 3.4)
- simplecov (= 0.12.0)
+ simplecov (~> 0.14.0)
slack-notifier (~> 1.5.1)
spinach-rails (~> 0.2.1)
spinach-rerun-reporter (~> 0.0.2)
- spring (~> 1.7.0)
+ spring (~> 2.0.0)
spring-commands-rspec (~> 1.0.4)
spring-commands-spinach (~> 1.1.0)
sprockets (~> 3.7.0)
@@ -1023,8 +1025,7 @@ DEPENDENCIES
version_sorter (~> 2.1.0)
virtus (~> 1.0.1)
vmstat (~> 2.3.0)
- web-console (~> 2.0)
- webmock (~> 1.21.0)
+ webmock (~> 1.24.0)
webpack-rails (~> 0.9.9)
wikicloth (= 0.8.1)
diff --git a/README.md b/README.md
index 09e08adbb73..f0e3b52ef6f 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,15 @@
# GitLab
[![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
-[![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby)
+[![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ce/pipelines)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
+## Test coverage
+
+- [![Ruby coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby) Ruby
+- [![JavaScript coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=rake+karma)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-javascript) JavaScript
+
## Canonical source
The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/).
diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js b/app/assets/javascripts/blob/blob_ci_yaml.js
deleted file mode 100644
index ec1c018424d..00000000000
--- a/app/assets/javascripts/blob/blob_ci_yaml.js
+++ /dev/null
@@ -1,42 +0,0 @@
-/* eslint-disable no-param-reassign, comma-dangle */
-/* global Api */
-
-require('./template_selector');
-
-((global) => {
- class BlobCiYamlSelector extends gl.TemplateSelector {
- requestFile(query) {
- return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this));
- }
-
- requestFileSuccess(file) {
- return super.requestFileSuccess(file);
- }
- }
-
- global.BlobCiYamlSelector = BlobCiYamlSelector;
-
- class BlobCiYamlSelectors {
- constructor({ editor, $dropdowns } = {}) {
- this.editor = editor;
- this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector');
- this.initSelectors();
- }
-
- initSelectors() {
- const editor = this.editor;
- this.$dropdowns.each((i, dropdown) => {
- const $dropdown = $(dropdown);
- return new BlobCiYamlSelector({
- editor,
- pattern: /(.gitlab-ci.yml)/,
- data: $dropdown.data('data'),
- wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
- dropdown: $dropdown
- });
- });
- }
- }
-
- global.BlobCiYamlSelectors = BlobCiYamlSelectors;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/blob/blob_dockerfile_selector.js b/app/assets/javascripts/blob/blob_dockerfile_selector.js
deleted file mode 100644
index d4f60cc6ecd..00000000000
--- a/app/assets/javascripts/blob/blob_dockerfile_selector.js
+++ /dev/null
@@ -1,19 +0,0 @@
-/* global Api */
-
-require('./template_selector');
-
-(() => {
- const global = window.gl || (window.gl = {});
-
- class BlobDockerfileSelector extends gl.TemplateSelector {
- requestFile(query) {
- return Api.dockerfileYml(query.name, this.requestFileSuccess.bind(this));
- }
-
- requestFileSuccess(file) {
- return super.requestFileSuccess(file);
- }
- }
-
- global.BlobDockerfileSelector = BlobDockerfileSelector;
-})();
diff --git a/app/assets/javascripts/blob/blob_dockerfile_selectors.js b/app/assets/javascripts/blob/blob_dockerfile_selectors.js
deleted file mode 100644
index 9cee79fa5d5..00000000000
--- a/app/assets/javascripts/blob/blob_dockerfile_selectors.js
+++ /dev/null
@@ -1,27 +0,0 @@
-(() => {
- const global = window.gl || (window.gl = {});
-
- class BlobDockerfileSelectors {
- constructor({ editor, $dropdowns } = {}) {
- this.editor = editor;
- this.$dropdowns = $dropdowns || $('.js-dockerfile-selector');
- this.initSelectors();
- }
-
- initSelectors() {
- const editor = this.editor;
- this.$dropdowns.each((i, dropdown) => {
- const $dropdown = $(dropdown);
- return new gl.BlobDockerfileSelector({
- editor,
- pattern: /(Dockerfile)/,
- data: $dropdown.data('data'),
- wrapper: $dropdown.closest('.js-dockerfile-selector-wrap'),
- dropdown: $dropdown,
- });
- });
- }
- }
-
- global.BlobDockerfileSelectors = BlobDockerfileSelectors;
-})();
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index 8f6bf162d6e..c9fe23aec75 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -1,66 +1,63 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, camelcase, object-shorthand, quotes, comma-dangle, prefer-arrow-callback, no-unused-vars, prefer-template, no-useless-escape, no-alert, max-len */
+/* eslint-disable func-names, object-shorthand, prefer-arrow-callback */
/* global Dropzone */
-(function() {
- this.BlobFileDropzone = (function() {
- function BlobFileDropzone(form, method) {
- var dropzone, form_dropzone, submitButton;
- form_dropzone = form.find('.dropzone');
- Dropzone.autoDiscover = false;
- dropzone = form_dropzone.dropzone({
- autoDiscover: false,
- autoProcessQueue: false,
- url: form.attr('action'),
- // Rails uses a hidden input field for PUT
- // http://stackoverflow.com/questions/21056482/how-to-set-method-put-in-form-tag-in-rails
- method: method,
- clickable: true,
- uploadMultiple: false,
- paramName: "file",
- maxFilesize: gon.max_file_size || 10,
- parallelUploads: 1,
- maxFiles: 1,
- addRemoveLinks: true,
- previewsContainer: '.dropzone-previews',
- headers: {
- "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
- },
- init: function() {
- this.on('addedfile', function(file) {
- $('.dropzone-alerts').html('').hide();
- });
- this.on('success', function(header, response) {
- window.location.href = response.filePath;
- });
- this.on('maxfilesexceeded', function(file) {
- this.removeFile(file);
- });
- return this.on('sending', function(file, xhr, formData) {
- formData.append('target_branch', form.find('input[name="target_branch"]').val());
- formData.append('create_merge_request', form.find('.js-create-merge-request').val());
- formData.append('commit_message', form.find('.js-commit-message').val());
- });
- },
- // Override behavior of adding error underneath preview
- error: function(file, errorMessage) {
- var stripped;
- stripped = $("<div/>").html(errorMessage).text();
- $('.dropzone-alerts').html('Error uploading file: \"' + stripped + '\"').show();
+export default class BlobFileDropzone {
+ constructor(form, method) {
+ const formDropzone = form.find('.dropzone');
+ Dropzone.autoDiscover = false;
+
+ const dropzone = formDropzone.dropzone({
+ autoDiscover: false,
+ autoProcessQueue: false,
+ url: form.attr('action'),
+ // Rails uses a hidden input field for PUT
+ // http://stackoverflow.com/questions/21056482/how-to-set-method-put-in-form-tag-in-rails
+ method: method,
+ clickable: true,
+ uploadMultiple: false,
+ paramName: 'file',
+ maxFilesize: gon.max_file_size || 10,
+ parallelUploads: 1,
+ maxFiles: 1,
+ addRemoveLinks: true,
+ previewsContainer: '.dropzone-previews',
+ headers: {
+ 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content'),
+ },
+ init: function () {
+ this.on('addedfile', function () {
+ $('.dropzone-alerts').html('').hide();
+ });
+ this.on('success', function (header, response) {
+ window.location.href = response.filePath;
+ });
+ this.on('maxfilesexceeded', function (file) {
this.removeFile(file);
- }
- });
- submitButton = form.find('#submit-all')[0];
- submitButton.addEventListener('click', function(e) {
- e.preventDefault();
- e.stopPropagation();
- if (dropzone[0].dropzone.getQueuedFiles().length === 0) {
- alert("Please select a file");
- }
- dropzone[0].dropzone.processQueue();
- return false;
- });
- }
+ });
+ this.on('sending', function (file, xhr, formData) {
+ formData.append('target_branch', form.find('input[name="target_branch"]').val());
+ formData.append('create_merge_request', form.find('.js-create-merge-request').val());
+ formData.append('commit_message', form.find('.js-commit-message').val());
+ });
+ },
+ // Override behavior of adding error underneath preview
+ error: function (file, errorMessage) {
+ const stripped = $('<div/>').html(errorMessage).text();
+ $('.dropzone-alerts').html(`Error uploading file: "${stripped}"`).show();
+ this.removeFile(file);
+ },
+ });
- return BlobFileDropzone;
- })();
-}).call(window);
+ const submitButton = form.find('#submit-all')[0];
+ submitButton.addEventListener('click', function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ if (dropzone[0].dropzone.getQueuedFiles().length === 0) {
+ // eslint-disable-next-line no-alert
+ alert('Please select a file');
+ }
+ dropzone[0].dropzone.processQueue();
+ return false;
+ });
+ }
+}
diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js
deleted file mode 100644
index de20eab9cd1..00000000000
--- a/app/assets/javascripts/blob/blob_gitignore_selector.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params */
-/* global Api */
-
-require('./template_selector');
-
-(function() {
- var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
- hasProp = {}.hasOwnProperty;
-
- this.BlobGitignoreSelector = (function(superClass) {
- extend(BlobGitignoreSelector, superClass);
-
- function BlobGitignoreSelector() {
- return BlobGitignoreSelector.__super__.constructor.apply(this, arguments);
- }
-
- BlobGitignoreSelector.prototype.requestFile = function(query) {
- return Api.gitignoreText(query.name, this.requestFileSuccess.bind(this));
- };
-
- return BlobGitignoreSelector;
- })(gl.TemplateSelector);
-}).call(window);
diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js
deleted file mode 100644
index 43e5c0a5641..00000000000
--- a/app/assets/javascripts/blob/blob_gitignore_selectors.js
+++ /dev/null
@@ -1,26 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-cond-assign, no-sequences, comma-dangle, max-len */
-/* global BlobGitignoreSelector */
-
-(function() {
- this.BlobGitignoreSelectors = (function() {
- function BlobGitignoreSelectors(opts) {
- var ref;
- this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-gitignore-selector'), this.editor = opts.editor;
- this.$dropdowns.each((function(_this) {
- return function(i, dropdown) {
- var $dropdown;
- $dropdown = $(dropdown);
- return new BlobGitignoreSelector({
- pattern: /(.gitignore)/,
- data: $dropdown.data('data'),
- wrapper: $dropdown.closest('.js-gitignore-selector-wrap'),
- dropdown: $dropdown,
- editor: _this.editor
- });
- };
- })(this));
- }
-
- return BlobGitignoreSelectors;
- })();
-}).call(window);
diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js
deleted file mode 100644
index b582052a76e..00000000000
--- a/app/assets/javascripts/blob/blob_license_selector.js
+++ /dev/null
@@ -1,28 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, comma-dangle */
-/* global Api */
-
-require('./template_selector');
-
-(function() {
- var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
- hasProp = {}.hasOwnProperty;
-
- this.BlobLicenseSelector = (function(superClass) {
- extend(BlobLicenseSelector, superClass);
-
- function BlobLicenseSelector() {
- return BlobLicenseSelector.__super__.constructor.apply(this, arguments);
- }
-
- BlobLicenseSelector.prototype.requestFile = function(query) {
- var data;
- data = {
- project: this.dropdown.data('project'),
- fullname: this.dropdown.data('fullname')
- };
- return Api.licenseText(query.id, data, this.requestFileSuccess.bind(this));
- };
-
- return BlobLicenseSelector;
- })(gl.TemplateSelector);
-}).call(window);
diff --git a/app/assets/javascripts/blob/blob_license_selectors.js b/app/assets/javascripts/blob/blob_license_selectors.js
deleted file mode 100644
index c5067b0feae..00000000000
--- a/app/assets/javascripts/blob/blob_license_selectors.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/* eslint-disable no-unused-vars, no-param-reassign */
-/* global BlobLicenseSelector */
-
-((global) => {
- class BlobLicenseSelectors {
- constructor({ $dropdowns, editor }) {
- this.$dropdowns = $('.js-license-selector');
- this.editor = editor;
- this.$dropdowns.each((i, dropdown) => {
- const $dropdown = $(dropdown);
- return new BlobLicenseSelector({
- editor,
- pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
- data: $dropdown.data('data'),
- wrapper: $dropdown.closest('.js-license-selector-wrap'),
- dropdown: $dropdown,
- });
- });
- }
- }
-
- global.BlobLicenseSelectors = BlobLicenseSelectors;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js
deleted file mode 100644
index 7e03ec3b391..00000000000
--- a/app/assets/javascripts/blob/template_selector.js
+++ /dev/null
@@ -1,101 +0,0 @@
-/* eslint-disable comma-dangle, object-shorthand, func-names, space-before-function-paren, arrow-parens, no-unused-vars, class-methods-use-this, no-var, consistent-return, no-param-reassign, max-len */
-
-((global) => {
- class TemplateSelector {
- constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) {
- this.onClick = this.onClick.bind(this);
- this.dropdown = dropdown;
- this.data = data;
- this.pattern = pattern;
- this.wrapper = wrapper;
- this.editor = editor;
- this.fileEndpoint = fileEndpoint;
- this.$input = $input || $('#file_name');
- this.dropdownIcon = $('.fa-chevron-down', this.dropdown);
- this.buildDropdown();
- this.bindEvents();
- this.onFilenameUpdate();
-
- this.autosizeUpdateEvent = document.createEvent('Event');
- this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
- }
-
- buildDropdown() {
- return this.dropdown.glDropdown({
- data: this.data,
- filterable: true,
- selectable: true,
- toggleLabel: this.toggleLabel,
- search: {
- fields: ['name']
- },
- clicked: this.onClick,
- text: function(item) {
- return item.name;
- }
- });
- }
-
- bindEvents() {
- return this.$input.on('keyup blur', (e) => this.onFilenameUpdate());
- }
-
- toggleLabel(item) {
- return item.name;
- }
-
- onFilenameUpdate() {
- var filenameMatches;
- if (!this.$input.length) {
- return;
- }
- filenameMatches = this.pattern.test(this.$input.val().trim());
- if (!filenameMatches) {
- this.wrapper.addClass('hidden');
- return;
- }
- return this.wrapper.removeClass('hidden');
- }
-
- onClick(item, el, e) {
- e.preventDefault();
- return this.requestFile(item);
- }
-
- requestFile(item) {
- // This `requestFile` method is an abstract method that should
- // be added by all subclasses.
- }
-
- // To be implemented on the extending class
- // e.g.
- // Api.gitignoreText item.name, @requestFileSuccess.bind(@)
- requestFileSuccess(file, { skipFocus } = {}) {
- if (!file) return;
-
- const oldValue = this.editor.getValue();
- const newValue = file.content;
-
- this.editor.setValue(newValue, 1);
- if (!skipFocus) this.editor.focus();
-
- if (this.editor instanceof jQuery) {
- this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
- }
- }
-
- startLoadingSpinner() {
- this.dropdownIcon
- .addClass('fa-spinner fa-spin')
- .removeClass('fa-chevron-down');
- }
-
- stopLoadingSpinner() {
- this.dropdownIcon
- .addClass('fa-chevron-down')
- .removeClass('fa-spinner fa-spin');
- }
- }
-
- global.TemplateSelector = TemplateSelector;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selector.js
new file mode 100644
index 00000000000..5a5954e7751
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selector.js
@@ -0,0 +1,9 @@
+/* global Api */
+
+import TemplateSelector from './template_selector';
+
+export default class BlobCiYamlSelector extends TemplateSelector {
+ requestFile(query) {
+ return Api.gitlabCiYml(query.name, (file, config) => this.setEditorContent(file, config));
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selectors.js
new file mode 100644
index 00000000000..7a4d6a42a03
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selectors.js
@@ -0,0 +1,23 @@
+/* global Api */
+
+import BlobCiYamlSelector from './blob_ci_yaml_selector';
+
+export default class BlobCiYamlSelectors {
+ constructor({ editor, $dropdowns }) {
+ this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector');
+ this.initSelectors(editor);
+ }
+
+ initSelectors(editor) {
+ this.$dropdowns.each((i, dropdown) => {
+ const $dropdown = $(dropdown);
+ return new BlobCiYamlSelector({
+ editor,
+ pattern: /(.gitlab-ci.yml)/,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
+ dropdown: $dropdown,
+ });
+ });
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selector.js
new file mode 100644
index 00000000000..19f8820a0cb
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selector.js
@@ -0,0 +1,9 @@
+/* global Api */
+
+import TemplateSelector from './template_selector';
+
+export default class BlobDockerfileSelector extends TemplateSelector {
+ requestFile(query) {
+ return Api.dockerfileYml(query.name, (file, config) => this.setEditorContent(file, config));
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selectors.js
new file mode 100644
index 00000000000..da067035b43
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selectors.js
@@ -0,0 +1,23 @@
+import BlobDockerfileSelector from './blob_dockerfile_selector';
+
+export default class BlobDockerfileSelectors {
+ constructor({ editor, $dropdowns }) {
+ this.editor = editor;
+ this.$dropdowns = $dropdowns || $('.js-dockerfile-selector');
+ this.initSelectors();
+ }
+
+ initSelectors() {
+ const editor = this.editor;
+ this.$dropdowns.each((i, dropdown) => {
+ const $dropdown = $(dropdown);
+ return new BlobDockerfileSelector({
+ editor,
+ pattern: /(Dockerfile)/,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-dockerfile-selector-wrap'),
+ dropdown: $dropdown,
+ });
+ });
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_gitignore_selector.js b/app/assets/javascripts/blob/template_selectors/blob_gitignore_selector.js
new file mode 100644
index 00000000000..0b6b02fc2b3
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/blob_gitignore_selector.js
@@ -0,0 +1,9 @@
+/* global Api */
+
+import TemplateSelector from './template_selector';
+
+export default class BlobGitignoreSelector extends TemplateSelector {
+ requestFile(query) {
+ return Api.gitignoreText(query.name, (file, config) => this.setEditorContent(file, config));
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_gitignore_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_gitignore_selectors.js
new file mode 100644
index 00000000000..dc485d97677
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/blob_gitignore_selectors.js
@@ -0,0 +1,23 @@
+import BlobGitignoreSelector from './blob_gitignore_selector';
+
+export default class BlobGitignoreSelectors {
+ constructor({ editor, $dropdowns }) {
+ this.$dropdowns = $dropdowns || $('.js-gitignore-selector');
+ this.editor = editor;
+ this.initSelectors();
+ }
+
+ initSelectors() {
+ this.$dropdowns.each((i, dropdown) => {
+ const $dropdown = $(dropdown);
+
+ return new BlobGitignoreSelector({
+ pattern: /(.gitignore)/,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-gitignore-selector-wrap'),
+ dropdown: $dropdown,
+ editor: this.editor,
+ });
+ });
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_license_selector.js b/app/assets/javascripts/blob/template_selectors/blob_license_selector.js
new file mode 100644
index 00000000000..e9cb31cc2dc
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/blob_license_selector.js
@@ -0,0 +1,13 @@
+/* global Api */
+
+import TemplateSelector from './template_selector';
+
+export default class BlobLicenseSelector extends TemplateSelector {
+ requestFile(query) {
+ const data = {
+ project: this.dropdown.data('project'),
+ fullname: this.dropdown.data('fullname'),
+ };
+ return Api.licenseText(query.id, data, (file, config) => this.setEditorContent(file, config));
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_license_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_license_selectors.js
new file mode 100644
index 00000000000..a44f4f78b2d
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/blob_license_selectors.js
@@ -0,0 +1,24 @@
+/* eslint-disable no-unused-vars, no-param-reassign */
+
+import BlobLicenseSelector from './blob_license_selector';
+
+export default class BlobLicenseSelectors {
+ constructor({ $dropdowns, editor }) {
+ this.$dropdowns = $dropdowns || $('.js-license-selector');
+ this.initSelectors(editor);
+ }
+
+ initSelectors(editor) {
+ this.$dropdowns.each((i, dropdown) => {
+ const $dropdown = $(dropdown);
+
+ return new BlobLicenseSelector({
+ editor,
+ pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-license-selector-wrap'),
+ dropdown: $dropdown,
+ });
+ });
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/template_selector.js b/app/assets/javascripts/blob/template_selectors/template_selector.js
new file mode 100644
index 00000000000..d7c1c32efbd
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/template_selector.js
@@ -0,0 +1,92 @@
+/* eslint-disable class-methods-use-this, no-unused-vars */
+
+export default class TemplateSelector {
+ constructor({ dropdown, data, pattern, wrapper, editor, $input } = {}) {
+ this.pattern = pattern;
+ this.editor = editor;
+ this.dropdown = dropdown;
+ this.$dropdownContainer = wrapper;
+ this.$filenameInput = $input || $('#file_name');
+ this.$dropdownIcon = $('.fa-chevron-down', dropdown);
+
+ this.initDropdown(dropdown, data);
+ this.listenForFilenameInput();
+ this.renderMatchedDropdown();
+ this.initAutosizeUpdateEvent();
+ }
+
+ initDropdown(dropdown, data) {
+ return $(dropdown).glDropdown({
+ data,
+ filterable: true,
+ selectable: true,
+ toggleLabel: item => item.name,
+ search: {
+ fields: ['name'],
+ },
+ clicked: (item, el, e) => this.fetchFileTemplate(item, el, e),
+ text: item => item.name,
+ });
+ }
+
+ initAutosizeUpdateEvent() {
+ this.autosizeUpdateEvent = document.createEvent('Event');
+ this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
+ }
+
+ listenForFilenameInput() {
+ return this.$filenameInput.on('keyup blur', e => this.renderMatchedDropdown(e));
+ }
+
+ renderMatchedDropdown() {
+ if (!this.$filenameInput.length) {
+ return null;
+ }
+
+ const filenameMatches = this.pattern.test(this.$filenameInput.val().trim());
+
+ if (!filenameMatches) {
+ return this.$dropdownContainer.addClass('hidden');
+ }
+ return this.$dropdownContainer.removeClass('hidden');
+ }
+
+ fetchFileTemplate(item, el, e) {
+ e.preventDefault();
+ return this.requestFile(item);
+ }
+
+ requestFile(item) {
+ // This `requestFile` method is an abstract method that should
+ // be added by all subclasses.
+ }
+
+ // To be implemented on the extending class
+ // e.g. Api.gitlabCiYml(query.name, file => this.setEditorContent(file));
+
+ setEditorContent(file, { skipFocus } = {}) {
+ if (!file) return;
+
+ const newValue = file.content;
+
+ this.editor.setValue(newValue, 1);
+
+ if (!skipFocus) this.editor.focus();
+
+ if (this.editor instanceof jQuery) {
+ this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
+ }
+ }
+
+ startLoadingSpinner() {
+ this.$dropdownIcon
+ .addClass('fa-spinner fa-spin')
+ .removeClass('fa-chevron-down');
+ }
+
+ stopLoadingSpinner() {
+ this.$dropdownIcon
+ .addClass('fa-chevron-down')
+ .removeClass('fa-spinner fa-spin');
+ }
+}
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
new file mode 100644
index 00000000000..c5deccf631e
--- /dev/null
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -0,0 +1,32 @@
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */
+/* global EditBlob */
+/* global NewCommitForm */
+
+import EditBlob from './edit_blob';
+import BlobFileDropzone from '../blob/blob_file_dropzone';
+
+$(() => {
+ const editBlobForm = $('.js-edit-blob-form');
+ const uploadBlobForm = $('.js-upload-blob-form');
+
+ if (editBlobForm.length) {
+ const urlRoot = editBlobForm.data('relative-url-root');
+ const assetsPath = editBlobForm.data('assets-prefix');
+ const blobLanguage = editBlobForm.data('blob-language');
+
+ new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage);
+ new NewCommitForm(editBlobForm);
+ }
+
+ if (uploadBlobForm.length) {
+ const method = uploadBlobForm.data('method');
+
+ new BlobFileDropzone(uploadBlobForm, method);
+ new NewCommitForm(uploadBlobForm);
+
+ window.gl.utils.disableButtonIfEmptyField(
+ uploadBlobForm.find('.js-commit-message'),
+ '.btn-upload-file',
+ );
+ }
+});
diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
deleted file mode 100644
index 0436bbb0eaf..00000000000
--- a/app/assets/javascripts/blob_edit/blob_edit_bundle.js
+++ /dev/null
@@ -1,15 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */
-/* global EditBlob */
-/* global NewCommitForm */
-
-require('./edit_blob');
-
-(function() {
- $(function() {
- var url = $(".js-edit-blob-form").data("relative-url-root");
- url += $(".js-edit-blob-form").data("assets-prefix");
-
- var blob = new EditBlob(url, $('.js-edit-blob-form').data('blob-language'));
- new NewCommitForm($('.js-edit-blob-form'));
- });
-}).call(window);
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index a1127b9e30e..d3560d5df3b 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -1,88 +1,99 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, camelcase, no-param-reassign, quotes, prefer-template, no-new, comma-dangle, one-var, one-var-declaration-per-line, prefer-arrow-callback, no-else-return, no-unused-vars, max-len */
/* global ace */
-/* global BlobGitignoreSelectors */
-
-(function() {
- var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
-
- this.EditBlob = (function() {
- function EditBlob(assets_path, ace_mode) {
- if (ace_mode == null) {
- ace_mode = null;
- }
- this.editModeLinkClickHandler = bind(this.editModeLinkClickHandler, this);
- ace.config.set("modePath", assets_path + "/ace");
- ace.config.loadModule("ace/ext/searchbox");
- this.editor = ace.edit("editor");
- this.editor.focus();
- if (ace_mode) {
- this.editor.getSession().setMode("ace/mode/" + ace_mode);
- }
- $('form').submit((function(_this) {
- return function() {
- return $("#file-content").val(_this.editor.getValue());
- };
- // Before a form submission, move the content from the Ace editor into the
- // submitted textarea
- })(this));
- this.initModePanesAndLinks();
- this.initSoftWrap();
- new gl.BlobLicenseSelectors({
- editor: this.editor
- });
+
+import BlobLicenseSelectors from '../blob/template_selectors/blob_license_selectors';
+import BlobGitignoreSelectors from '../blob/template_selectors/blob_gitignore_selectors';
+import BlobCiYamlSelectors from '../blob/template_selectors/blob_ci_yaml_selectors';
+import BlobDockerfileSelectors from '../blob/template_selectors/blob_dockerfile_selectors';
+
+export default class EditBlob {
+ constructor(assetsPath, aceMode) {
+ this.configureAceEditor(aceMode, assetsPath);
+ this.prepFileContentForSubmit();
+ this.initModePanesAndLinks();
+ this.initSoftWrap();
+ this.initFileSelectors();
+ }
+
+ configureAceEditor(aceMode, assetsPath) {
+ ace.config.set('modePath', `${assetsPath}/ace`);
+ ace.config.loadModule('ace/ext/searchbox');
+
+ this.editor = ace.edit('editor');
+ this.editor.focus();
+
+ if (aceMode) {
+ this.editor.getSession().setMode(`ace/mode/${aceMode}`);
+ }
+ }
+
+ prepFileContentForSubmit() {
+ $('form').submit(() => {
+ $('#file-content').val(this.editor.getValue());
+ });
+ }
+
+ initFileSelectors() {
+ this.blobTemplateSelectors = [
+ new BlobLicenseSelectors({
+ editor: this.editor,
+ }),
new BlobGitignoreSelectors({
- editor: this.editor
- });
- new gl.BlobCiYamlSelectors({
- editor: this.editor
- });
- new gl.BlobDockerfileSelectors({
- editor: this.editor
+ editor: this.editor,
+ }),
+ new BlobCiYamlSelectors({
+ editor: this.editor,
+ }),
+ new BlobDockerfileSelectors({
+ editor: this.editor,
+ }),
+ ];
+ }
+
+ initModePanesAndLinks() {
+ this.$editModePanes = $('.js-edit-mode-pane');
+ this.$editModeLinks = $('.js-edit-mode a');
+ this.$editModeLinks.on('click', e => this.editModeLinkClickHandler(e));
+ }
+
+ editModeLinkClickHandler(e) {
+ e.preventDefault();
+
+ const currentLink = $(e.target);
+ const paneId = currentLink.attr('href');
+ const currentPane = this.$editModePanes.filter(paneId);
+
+ this.$editModeLinks.parent().removeClass('active hover');
+
+ currentLink.parent().addClass('active hover');
+
+ this.$editModePanes.hide();
+
+ currentPane.fadeIn(200);
+
+ if (paneId === '#preview') {
+ this.$toggleButton.hide();
+ return $.post(currentLink.data('preview-url'), {
+ content: this.editor.getValue(),
+ }, (response) => {
+ currentPane.empty().append(response);
+ return currentPane.renderGFM();
});
}
- EditBlob.prototype.initModePanesAndLinks = function() {
- this.$editModePanes = $(".js-edit-mode-pane");
- this.$editModeLinks = $(".js-edit-mode a");
- return this.$editModeLinks.click(this.editModeLinkClickHandler);
- };
-
- EditBlob.prototype.editModeLinkClickHandler = function(event) {
- var currentLink, currentPane, paneId;
- event.preventDefault();
- currentLink = $(event.target);
- paneId = currentLink.attr("href");
- currentPane = this.$editModePanes.filter(paneId);
- this.$editModeLinks.parent().removeClass("active hover");
- currentLink.parent().addClass("active hover");
- this.$editModePanes.hide();
- currentPane.fadeIn(200);
- if (paneId === "#preview") {
- this.$toggleButton.hide();
- return $.post(currentLink.data("preview-url"), {
- content: this.editor.getValue()
- }, function(response) {
- currentPane.empty().append(response);
- return currentPane.renderGFM();
- });
- } else {
- this.$toggleButton.show();
- return this.editor.focus();
- }
- };
-
- EditBlob.prototype.initSoftWrap = function() {
- this.isSoftWrapped = false;
- this.$toggleButton = $('.soft-wrap-toggle');
- this.$toggleButton.on('click', this.toggleSoftWrap.bind(this));
- };
-
- EditBlob.prototype.toggleSoftWrap = function(e) {
- this.isSoftWrapped = !this.isSoftWrapped;
- this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped);
- this.editor.getSession().setUseWrapMode(this.isSoftWrapped);
- };
-
- return EditBlob;
- })();
-}).call(window);
+ this.$toggleButton.show();
+
+ return this.editor.focus();
+ }
+
+ initSoftWrap() {
+ this.isSoftWrapped = false;
+ this.$toggleButton = $('.soft-wrap-toggle');
+ this.$toggleButton.on('click', () => this.toggleSoftWrap());
+ }
+
+ toggleSoftWrap() {
+ this.isSoftWrapped = !this.isSoftWrapped;
+ this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped);
+ this.editor.getSession().setUseWrapMode(this.isSoftWrapped);
+ }
+}
diff --git a/app/assets/javascripts/boards/components/modal/filters.js b/app/assets/javascripts/boards/components/modal/filters.js
index 2e22b1eca47..b214b5a7199 100644
--- a/app/assets/javascripts/boards/components/modal/filters.js
+++ b/app/assets/javascripts/boards/components/modal/filters.js
@@ -17,7 +17,7 @@ export default {
this.filteredSearch.handleInputPlaceholder();
this.filteredSearch.toggleClearSearchButton();
},
- beforeDestroy() {
+ destroyed() {
this.filteredSearch.cleanup();
FilteredSearchContainer.container = document;
this.store.path = '';
diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js
index 4800407be1c..91c08cde13a 100644
--- a/app/assets/javascripts/boards/components/modal/index.js
+++ b/app/assets/javascripts/boards/components/modal/index.js
@@ -65,8 +65,15 @@ require('./empty_state');
},
filter: {
handler() {
- this.page = 1;
- this.loadIssues(true);
+ if (this.$el.tagName) {
+ this.page = 1;
+ this.filterLoading = true;
+
+ this.loadIssues(true)
+ .then(() => {
+ this.filterLoading = false;
+ });
+ }
},
deep: true,
},
@@ -140,14 +147,14 @@ require('./empty_state');
:image="blankStateImage"
:issue-link-base="issueLinkBase"
:root-path="rootPath"
- v-if="!loading && showList"></modal-list>
+ v-if="!loading && showList && !filterLoading"></modal-list>
<empty-state
v-if="showEmptyState"
:image="blankStateImage"
:new-issue-path="newIssuePath"></empty-state>
<section
class="add-issues-list text-center"
- v-if="loading">
+ v-if="loading || filterLoading">
<div class="add-issues-list-loading">
<i class="fa fa-spinner fa-spin"></i>
</div>
diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js
index 7ee266a831f..9b009483a3c 100644
--- a/app/assets/javascripts/boards/stores/modal_store.js
+++ b/app/assets/javascripts/boards/stores/modal_store.js
@@ -15,6 +15,7 @@
searchTerm: '',
loading: false,
loadingNewPage: false,
+ filterLoading: false,
page: 1,
perPage: 50,
filter: {
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js
index 360fb39dc9c..a20e5bc3b1b 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js
@@ -1,10 +1,10 @@
-/* eslint-disable no-new*/
-/* global Flash */
import Vue from 'vue';
import PipelinesTableComponent from '../../vue_shared/components/pipelines_table';
import PipelinesService from '../../vue_pipelines_index/services/pipelines_service';
import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store';
import eventHub from '../../vue_pipelines_index/event_hub';
+import EmptyState from '../../vue_pipelines_index/components/empty_state';
+import ErrorState from '../../vue_pipelines_index/components/error_state';
import '../../lib/utils/common_utils';
import '../../vue_shared/vue_resource_interceptor';
@@ -22,6 +22,8 @@ import '../../vue_shared/vue_resource_interceptor';
export default Vue.component('pipelines-table', {
components: {
'pipelines-table-component': PipelinesTableComponent,
+ 'error-state': ErrorState,
+ 'empty-state': EmptyState,
},
/**
@@ -36,12 +38,24 @@ export default Vue.component('pipelines-table', {
return {
endpoint: pipelinesTableData.endpoint,
+ helpPagePath: pipelinesTableData.helpPagePath,
store,
state: store.state,
isLoading: false,
+ hasError: false,
};
},
+ computed: {
+ shouldRenderErrorState() {
+ return this.hasError && !this.isLoading;
+ },
+
+ shouldRenderEmptyState() {
+ return !this.state.pipelines.length && !this.isLoading;
+ },
+ },
+
/**
* When the component is about to be mounted, tell the service to fetch the data
*
@@ -80,8 +94,8 @@ export default Vue.component('pipelines-table', {
this.isLoading = false;
})
.catch(() => {
+ this.hasError = true;
this.isLoading = false;
- new Flash('An error occurred while fetching the pipelines, please reload the page again.');
});
},
},
@@ -92,12 +106,11 @@ export default Vue.component('pipelines-table', {
<i class="fa fa-spinner fa-spin"></i>
</div>
- <div class="blank-state blank-state-no-icon"
- v-if="!isLoading && state.pipelines.length === 0">
- <h2 class="blank-state-title js-blank-state-title">
- No pipelines to show
- </h2>
- </div>
+ <empty-state
+ v-if="shouldRenderEmptyState"
+ :help-page-path="helpPagePath" />
+
+ <error-state v-if="shouldRenderErrorState" />
<div class="table-holder"
v-if="!isLoading && state.pipelines.length > 0">
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index fbd0db64ca7..3253eebd9b5 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -1,9 +1,11 @@
// ECMAScript polyfills
import 'core-js/fn/array/find';
+import 'core-js/fn/array/from';
import 'core-js/fn/object/assign';
import 'core-js/fn/promise';
import 'core-js/fn/string/code-point-at';
import 'core-js/fn/string/from-code-point';
+import 'core-js/fn/symbol';
// Browser polyfills
import './polyfills/custom_event';
diff --git a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js
new file mode 100644
index 00000000000..abe48572347
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js
@@ -0,0 +1,17 @@
+export default {
+ props: {
+ count: {
+ type: Number,
+ required: true,
+ },
+ },
+ template: `
+ <span v-if="count === 50" class="events-info pull-right">
+ <i class="fa fa-warning has-tooltip"
+ aria-hidden="true"
+ title="Limited to showing 50 events at most"
+ data-placement="top"></i>
+ Showing 50 events
+ </span>
+ `,
+};
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
index 9947f355aca..3f419a96ff9 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
@@ -14,6 +14,7 @@ import Vue from 'vue';
<div>
<div class="events-description">
{{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
index 6ad4805e8c5..7ffa38edd9e 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
@@ -14,6 +14,7 @@ import Vue from 'vue';
<div>
<div class="events-description">
{{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
index 42e1bbce744..d736c8b0c28 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
@@ -19,12 +19,7 @@ import iconCommit from '../svg/icon_commit.svg';
<div>
<div class="events-description">
{{ stage.description }}
- <span v-if="items.length === 50" class="events-info pull-right">
- <i class="fa fa-warning has-tooltip"
- title="Limited to showing 50 events at most"
- data-placement="top"></i>
- Showing 50 events
- </span>
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="commit in items" class="stage-event-item">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
index da80450a32c..698a79ca68c 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
@@ -14,6 +14,7 @@ import Vue from 'vue';
<div>
<div class="events-description">
{{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
index 2200f43914f..e63c41f2a57 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
@@ -14,6 +14,7 @@ import Vue from 'vue';
<div>
<div class="events-description">
{{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
index 8fa63734cf1..d51f7134e25 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
@@ -17,6 +17,7 @@ import iconBranch from '../svg/icon_branch.svg';
<div>
<div class="events-description">
{{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
index 0015249cfaa..17ae3a9ddc1 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
@@ -18,6 +18,7 @@ import iconBranch from '../svg/icon_branch.svg';
<div>
<div class="events-description">
{{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index ae17d05e679..b099b39e58f 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -2,6 +2,7 @@
import Vue from 'vue';
import Cookies from 'js-cookie';
+import LimitWarningComponent from './components/limit_warning_component';
require('./components/stage_code_component');
require('./components/stage_issue_component');
@@ -130,5 +131,6 @@ $(() => {
});
// Register global components
+ Vue.component('limit-warning', LimitWarningComponent);
Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent);
});
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index cfa60325fcc..88180149715 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -33,11 +33,7 @@ class Diff {
handleClickUnfold(e) {
const $target = $(e.target);
- // current babel config relies on iterators implementation, so we cannot simply do:
- // const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent());
- const ref = this.lineNumbers($target.parent());
- const oldLineNumber = ref[0];
- const newLineNumber = ref[1];
+ const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent());
const offset = newLineNumber - oldLineNumber;
const bottom = $target.hasClass('js-unfold-bottom');
let since;
@@ -105,10 +101,11 @@ class Diff {
}
lineNumbers(line) {
- if (!line.children().length) {
+ const children = line.find('.diff-line-num').toArray();
+ if (children.length !== 2) {
return [0, 0];
}
- return line.find('.diff-line-num').map((i, elm) => parseInt($(elm).data('linenumber'), 10));
+ return children.map(elm => parseInt($(elm).data('linenumber'), 10) || 0);
}
highlightSelectedLine() {
diff --git a/app/assets/javascripts/droplab/droplab_filter.js b/app/assets/javascripts/droplab/droplab_filter.js
index 9b40a3f20a4..7f7d93f3e27 100644
--- a/app/assets/javascripts/droplab/droplab_filter.js
+++ b/app/assets/javascripts/droplab/droplab_filter.js
@@ -56,10 +56,12 @@ require('../window')(function(w){
this.hookInput = hookInput;
this.hookInput.trigger.addEventListener('keyup.dl', this.keydownWrapper);
+ this.hookInput.trigger.addEventListener('mousedown.dl', this.keydownWrapper);
},
destroy: function destroy(){
this.hookInput.trigger.removeEventListener('keyup.dl', this.keydownWrapper);
+ this.hookInput.trigger.removeEventListener('mousedown.dl', this.keydownWrapper);
}
};
});
diff --git a/app/assets/javascripts/group_name.js b/app/assets/javascripts/group_name.js
index 6a028f299b1..62675d7e67e 100644
--- a/app/assets/javascripts/group_name.js
+++ b/app/assets/javascripts/group_name.js
@@ -1,40 +1,64 @@
-const GROUP_LIMIT = 2;
+
+import _ from 'underscore';
export default class GroupName {
constructor() {
- this.titleContainer = document.querySelector('.title');
- this.groups = document.querySelectorAll('.group-path');
+ this.titleContainer = document.querySelector('.title-container');
+ this.title = document.querySelector('.title');
+ this.titleWidth = this.title.offsetWidth;
this.groupTitle = document.querySelector('.group-title');
+ this.groups = document.querySelectorAll('.group-path');
this.toggle = null;
this.isHidden = false;
this.init();
}
init() {
- if (this.groups.length > GROUP_LIMIT) {
+ if (this.groups.length > 0) {
this.groups[this.groups.length - 1].classList.remove('hidable');
- this.addToggle();
+ this.toggleHandler();
+ window.addEventListener('resize', _.debounce(this.toggleHandler.bind(this), 100));
}
this.render();
}
- addToggle() {
- const header = document.querySelector('.header-content');
+ toggleHandler() {
+ if (this.titleWidth > this.titleContainer.offsetWidth) {
+ if (!this.toggle) this.createToggle();
+ this.showToggle();
+ } else if (this.toggle) {
+ this.hideToggle();
+ }
+ }
+
+ createToggle() {
this.toggle = document.createElement('button');
this.toggle.className = 'text-expander group-name-toggle';
this.toggle.setAttribute('aria-label', 'Toggle full path');
this.toggle.innerHTML = '...';
this.toggle.addEventListener('click', this.toggleGroups.bind(this));
- header.insertBefore(this.toggle, this.titleContainer);
+ this.titleContainer.insertBefore(this.toggle, this.title);
this.toggleGroups();
}
+ showToggle() {
+ this.title.classList.add('wrap');
+ this.toggle.classList.remove('hidden');
+ if (this.isHidden) this.groupTitle.classList.add('is-hidden');
+ }
+
+ hideToggle() {
+ this.title.classList.remove('wrap');
+ this.toggle.classList.add('hidden');
+ if (this.isHidden) this.groupTitle.classList.remove('is-hidden');
+ }
+
toggleGroups() {
this.isHidden = !this.isHidden;
this.groupTitle.classList.toggle('is-hidden');
}
render() {
- this.titleContainer.classList.remove('initializing');
+ this.title.classList.remove('initializing');
}
}
diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js
index 938cf9912a8..c30a1fcb5da 100644
--- a/app/assets/javascripts/lib/utils/poll.js
+++ b/app/assets/javascripts/lib/utils/poll.js
@@ -36,20 +36,21 @@ export default class Poll {
this.options.data = options.data || {};
this.intervalHeader = 'POLL-INTERVAL';
+ this.timeoutID = null;
+ this.canPoll = true;
}
checkConditions(response) {
const headers = gl.utils.normalizeHeaders(response.headers);
const pollInterval = headers[this.intervalHeader];
- if (pollInterval > 0 && response.status === httpStatusCodes.OK) {
- this.options.successCallback(response);
- setTimeout(() => {
+ if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) {
+ this.timeoutID = setTimeout(() => {
this.makeRequest();
}, pollInterval);
- } else {
- this.options.successCallback(response);
}
+
+ this.options.successCallback(response);
}
makeRequest() {
@@ -59,4 +60,14 @@ export default class Poll {
.then(response => this.checkConditions(response))
.catch(error => errorCallback(error));
}
+
+ /**
+ * Stops the polling recursive chain
+ * and guarantees if the timeout is already running it won't make another request by
+ * cancelling the previously established timeout.
+ */
+ stop() {
+ this.canPoll = false;
+ clearTimeout(this.timeoutID);
+ }
}
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index b1ca0dc091d..9007d661d01 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -47,15 +47,6 @@ import { installGlEmojiElement } from './behaviors/gl_emoji';
installGlEmojiElement();
// blob
-import './blob/blob_ci_yaml';
-import './blob/blob_dockerfile_selector';
-import './blob/blob_dockerfile_selectors';
-import './blob/blob_file_dropzone';
-import './blob/blob_gitignore_selector';
-import './blob/blob_gitignore_selectors';
-import './blob/blob_license_selector';
-import './blob/blob_license_selectors';
-import './blob/template_selector';
import './blob/create_branch_dropdown';
import './blob/target_branch_dropdown';
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index d9692269c38..811f90c5a87 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -127,9 +127,6 @@ require('./flash');
if (this.diffViewType() === 'parallel') {
this.expandViewContainer();
}
- $.scrollTo('.merge-request-details .merge-request-tabs', {
- offset: 0,
- });
} else if (action === 'pipelines') {
if (this.pipelinesLoaded) {
return;
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 7298a7d5347..64a68d56962 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -200,7 +200,7 @@ import Cookies from 'js-cookie';
Sidebar.prototype.setSidebarHeight = function() {
const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
const $rightSidebar = $('.js-right-sidebar');
- const diff = $navHeight - $('body').scrollTop();
+ const diff = $navHeight - $(window).scrollTop();
if (diff > 0) {
$rightSidebar.outerHeight($(window).height() - diff);
} else {
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/templates/issuable_template_selector.js
index e9e9aafd71a..32067ed1fee 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js
+++ b/app/assets/javascripts/templates/issuable_template_selector.js
@@ -1,15 +1,15 @@
/* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */
/* global Api */
-require('../blob/template_selector');
+import TemplateSelector from '../blob/template_selectors/template_selector';
((global) => {
- class IssuableTemplateSelector extends gl.TemplateSelector {
+ class IssuableTemplateSelector extends TemplateSelector {
constructor(...args) {
super(...args);
this.projectPath = this.dropdown.data('project-path');
this.namespacePath = this.dropdown.data('namespace-path');
- this.issuableType = this.wrapper.data('issuable-type');
+ this.issuableType = this.$dropdownContainer.data('issuable-type');
this.titleInput = $(`#${this.issuableType}_title`);
const initialQuery = {
@@ -41,16 +41,16 @@ require('../blob/template_selector');
}
setInputValueToTemplateContent() {
- // `this.requestFileSuccess` sets the value of the description input field
+ // `this.setEditorContent` sets the value of the description input field
// to the content of the template selected.
if (this.titleInput.val() === '') {
// If the title has not yet been set, focus the title input and
// skip focusing the description input by setting `true` as the
- // `skipFocus` option to `requestFileSuccess`.
- this.requestFileSuccess(this.currentTemplate, { skipFocus: true });
+ // `skipFocus` option to `setEditorContent`.
+ this.setEditorContent(this.currentTemplate, { skipFocus: true });
this.titleInput.focus();
} else {
- this.requestFileSuccess(this.currentTemplate, { skipFocus: false });
+ this.setEditorContent(this.currentTemplate, { skipFocus: false });
}
return;
}
diff --git a/app/assets/javascripts/vue_pipelines_index/components/empty_state.js b/app/assets/javascripts/vue_pipelines_index/components/empty_state.js
new file mode 100644
index 00000000000..56b4858f4b4
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/empty_state.js
@@ -0,0 +1,33 @@
+import pipelinesEmptyStateSVG from 'empty_states/icons/_pipelines_empty.svg';
+
+export default {
+ props: {
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+ },
+
+ template: `
+ <div class="row empty-state">
+ <div class="col-xs-12">
+ <div class="svg-content">
+ ${pipelinesEmptyStateSVG}
+ </div>
+ </div>
+
+ <div class="col-xs-12 text-center">
+ <div class="text-content">
+ <h4>Build with confidence</h4>
+ <p>
+ Continous Integration can help catch bugs by running your tests automatically,
+ while Continuous Deployment can help you deliver code to your product environment.
+ </p>
+ <a :href="helpPagePath" class="btn btn-info">
+ Get started with Pipelines
+ </a>
+ </div>
+ </div>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/error_state.js b/app/assets/javascripts/vue_pipelines_index/components/error_state.js
new file mode 100644
index 00000000000..e5d228bddf8
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/error_state.js
@@ -0,0 +1,19 @@
+import pipelinesErrorStateSVG from 'empty_states/icons/_pipelines_failed.svg';
+
+export default {
+ template: `
+ <div class="row empty-state js-pipelines-error-state">
+ <div class="col-xs-12">
+ <div class="svg-content">
+ ${pipelinesErrorStateSVG}
+ </div>
+ </div>
+
+ <div class="col-xs-12 text-center">
+ <div class="text-content">
+ <h4>The API failed to fetch the pipelines.</h4>
+ </div>
+ </div>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/nav_controls.js b/app/assets/javascripts/vue_pipelines_index/components/nav_controls.js
new file mode 100644
index 00000000000..6aa10531034
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/nav_controls.js
@@ -0,0 +1,52 @@
+export default {
+ props: {
+ newPipelinePath: {
+ type: String,
+ required: true,
+ },
+
+ hasCiEnabled: {
+ type: Boolean,
+ required: true,
+ },
+
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+
+ ciLintPath: {
+ type: String,
+ required: true,
+ },
+
+ canCreatePipeline: {
+ type: Boolean,
+ required: true,
+ },
+ },
+
+ template: `
+ <div class="nav-controls">
+ <a
+ v-if="canCreatePipeline"
+ :href="newPipelinePath"
+ class="btn btn-create">
+ Run Pipeline
+ </a>
+
+ <a
+ v-if="!hasCiEnabled"
+ :href="helpPagePath"
+ class="btn btn-info">
+ Get started with Pipelines
+ </a>
+
+ <a
+ :href="ciLintPath"
+ class="btn btn-default">
+ CI Lint
+ </a>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js b/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js
new file mode 100644
index 00000000000..b4480bd98c7
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js
@@ -0,0 +1,68 @@
+export default {
+ props: {
+ scope: {
+ type: String,
+ required: true,
+ },
+
+ count: {
+ type: Object,
+ required: true,
+ },
+
+ paths: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ template: `
+ <ul class="nav-links">
+ <li
+ class="js-pipelines-tab-all"
+ :class="{ 'active': scope === 'all'}">
+ <a :href="paths.allPath">
+ All
+ <span class="badge js-totalbuilds-count">
+ {{count.all}}
+ </span>
+ </a>
+ </li>
+ <li class="js-pipelines-tab-pending"
+ :class="{ 'active': scope === 'pending'}">
+ <a :href="paths.pendingPath">
+ Pending
+ <span class="badge">
+ {{count.pending}}
+ </span>
+ </a>
+ </li>
+ <li class="js-pipelines-tab-running"
+ :class="{ 'active': scope === 'running'}">
+ <a :href="paths.runningPath">
+ Running
+ <span class="badge">
+ {{count.running}}
+ </span>
+ </a>
+ </li>
+ <li class="js-pipelines-tab-finished"
+ :class="{ 'active': scope === 'finished'}">
+ <a :href="paths.finishedPath">
+ Finished
+ <span class="badge">
+ {{count.finished}}
+ </span>
+ </a>
+ </li>
+ <li class="js-pipelines-tab-branches"
+ :class="{ 'active': scope === 'branches'}">
+ <a :href="paths.branchesPath">Branches</a>
+ </li>
+ <li class="js-pipelines-tab-tags"
+ :class="{ 'active': scope === 'tags'}">
+ <a :href="paths.tagsPath">Tags</a>
+ </li>
+ </ul>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/index.js b/app/assets/javascripts/vue_pipelines_index/index.js
index 104154a715b..48f9181a8d9 100644
--- a/app/assets/javascripts/vue_pipelines_index/index.js
+++ b/app/assets/javascripts/vue_pipelines_index/index.js
@@ -4,23 +4,19 @@ import PipelinesComponent from './pipelines';
import '../vue_shared/vue_resource_interceptor';
$(() => new Vue({
- el: document.querySelector('.vue-pipelines-index'),
+ el: document.querySelector('#pipelines-list-vue'),
data() {
- const project = document.querySelector('.pipelines');
const store = new PipelinesStore();
return {
store,
- endpoint: project.dataset.url,
};
},
components: {
'vue-pipelines': PipelinesComponent,
},
template: `
- <vue-pipelines
- :endpoint="endpoint"
- :store="store" />
+ <vue-pipelines :store="store" />
`,
}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js b/app/assets/javascripts/vue_pipelines_index/pipelines.js
index f389e5e4950..48f0e9036e8 100644
--- a/app/assets/javascripts/vue_pipelines_index/pipelines.js
+++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js
@@ -1,19 +1,15 @@
-/* global Flash */
-/* eslint-disable no-new */
-import '~/flash';
import Vue from 'vue';
import PipelinesService from './services/pipelines_service';
import eventHub from './event_hub';
import PipelinesTableComponent from '../vue_shared/components/pipelines_table';
import TablePaginationComponent from '../vue_shared/components/table_pagination';
+import EmptyState from './components/empty_state';
+import ErrorState from './components/error_state';
+import NavigationTabs from './components/navigation_tabs';
+import NavigationControls from './components/nav_controls';
export default {
props: {
- endpoint: {
- type: String,
- required: true,
- },
-
store: {
type: Object,
required: true,
@@ -23,17 +19,109 @@ export default {
components: {
'gl-pagination': TablePaginationComponent,
'pipelines-table-component': PipelinesTableComponent,
+ 'empty-state': EmptyState,
+ 'error-state': ErrorState,
+ 'navigation-tabs': NavigationTabs,
+ 'navigation-controls': NavigationControls,
},
data() {
+ const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
+
return {
+ endpoint: pipelinesData.endpoint,
+ cssClass: pipelinesData.cssClass,
+ helpPagePath: pipelinesData.helpPagePath,
+ newPipelinePath: pipelinesData.newPipelinePath,
+ canCreatePipeline: pipelinesData.canCreatePipeline,
+ allPath: pipelinesData.allPath,
+ pendingPath: pipelinesData.pendingPath,
+ runningPath: pipelinesData.runningPath,
+ finishedPath: pipelinesData.finishedPath,
+ branchesPath: pipelinesData.branchesPath,
+ tagsPath: pipelinesData.tagsPath,
+ hasCi: pipelinesData.hasCi,
+ ciLintPath: pipelinesData.ciLintPath,
state: this.store.state,
apiScope: 'all',
pagenum: 1,
- pageRequest: false,
+ isLoading: false,
+ hasError: false,
};
},
+ computed: {
+ canCreatePipelineParsed() {
+ return gl.utils.convertPermissionToBoolean(this.canCreatePipeline);
+ },
+
+ scope() {
+ const scope = gl.utils.getParameterByName('scope');
+ return scope === null ? 'all' : scope;
+ },
+
+ shouldRenderErrorState() {
+ return this.hasError && !this.isLoading;
+ },
+
+ /**
+ * The empty state should only be rendered when the request is made to fetch all pipelines
+ * and none is returned.
+ *
+ * @return {Boolean}
+ */
+ shouldRenderEmptyState() {
+ return !this.isLoading &&
+ !this.hasError &&
+ !this.state.pipelines.length &&
+ (this.scope === 'all' || this.scope === null);
+ },
+
+ /**
+ * When a specific scope does not have pipelines we render a message.
+ *
+ * @return {Boolean}
+ */
+ shouldRenderNoPipelinesMessage() {
+ return !this.isLoading &&
+ !this.hasError &&
+ !this.state.pipelines.length &&
+ this.scope !== 'all' &&
+ this.scope !== null;
+ },
+
+ shouldRenderTable() {
+ return !this.hasError &&
+ !this.isLoading && this.state.pipelines.length;
+ },
+
+ /**
+ * Pagination should only be rendered when there is more than one page.
+ *
+ * @return {Boolean}
+ */
+ shouldRenderPagination() {
+ return !this.isLoading &&
+ this.state.pipelines.length &&
+ this.state.pageInfo.total > this.state.pageInfo.perPage;
+ },
+
+ hasCiEnabled() {
+ return this.hasCi !== undefined;
+ },
+
+ paths() {
+ return {
+ allPath: this.allPath,
+ pendingPath: this.pendingPath,
+ finishedPath: this.finishedPath,
+ runningPath: this.runningPath,
+ branchesPath: this.branchesPath,
+ tagsPath: this.tagsPath,
+ };
+ },
+ },
+
created() {
this.service = new PipelinesService(this.endpoint);
@@ -69,7 +157,7 @@ export default {
const pageNumber = gl.utils.getParameterByName('page') || this.pagenum;
const scope = gl.utils.getParameterByName('scope') || this.apiScope;
- this.pageRequest = true;
+ this.isLoading = true;
return this.service.getPipelines(scope, pageNumber)
.then(resp => ({
headers: resp.headers,
@@ -81,41 +169,72 @@ export default {
this.store.storePagination(response.headers);
})
.then(() => {
- this.pageRequest = false;
+ this.isLoading = false;
})
.catch(() => {
- this.pageRequest = false;
- new Flash('An error occurred while fetching the pipelines, please reload the page again.');
+ this.hasError = true;
+ this.isLoading = false;
});
},
},
- template: `
- <div>
- <div class="pipelines realtime-loading" v-if="pageRequest">
- <i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
- </div>
- <div class="blank-state blank-state-no-icon"
- v-if="!pageRequest && state.pipelines.length === 0">
- <h2 class="blank-state-title js-blank-state-title">
- No pipelines to show
- </h2>
+ template: `
+ <div :class="cssClass">
+
+ <div
+ class="top-area"
+ v-if="!isLoading && !shouldRenderEmptyState">
+ <navigation-tabs
+ :scope="scope"
+ :count="state.count"
+ :paths="paths" />
+
+ <navigation-controls
+ :new-pipeline-path="newPipelinePath"
+ :has-ci-enabled="hasCiEnabled"
+ :help-page-path="helpPagePath"
+ :ciLintPath="ciLintPath"
+ :can-create-pipeline="canCreatePipelineParsed " />
</div>
- <div class="table-holder" v-if="!pageRequest && state.pipelines.length">
- <pipelines-table-component
- :pipelines="state.pipelines"
- :service="service"/>
+ <div class="content-list pipelines">
+
+ <div
+ class="realtime-loading"
+ v-if="isLoading">
+ <i
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
+ </div>
+
+ <empty-state
+ v-if="shouldRenderEmptyState"
+ :help-page-path="helpPagePath" />
+
+ <error-state v-if="shouldRenderErrorState" />
+
+ <div
+ class="blank-state blank-state-no-icon"
+ v-if="shouldRenderNoPipelinesMessage">
+ <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
+ </div>
+
+ <div
+ class="table-holder"
+ v-if="shouldRenderTable">
+
+ <pipelines-table-component
+ :pipelines="state.pipelines"
+ :service="service"/>
+ </div>
+
+ <gl-pagination
+ v-if="shouldRenderPagination"
+ :pagenum="pagenum"
+ :change="change"
+ :count="state.count.all"
+ :pageInfo="state.pageInfo"/>
</div>
-
- <gl-pagination
- v-if="!pageRequest && state.pipelines.length && state.pageInfo.total > state.pageInfo.perPage"
- :pagenum="pagenum"
- :change="change"
- :count="state.count.all"
- :pageInfo="state.pageInfo"
- >
- </gl-pagination>
</div>
`,
};
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index cda46223492..50849e95541 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -68,23 +68,19 @@
}
@mixin btn-green {
- @include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, $white-light);
+ @include btn-color($green-500, $green-600, $green-600, $green-700, $green-700, $green-800, $white-light);
}
@mixin btn-blue {
- @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, $white-light);
-}
-
-@mixin btn-blue-medium {
- @include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, $white-light);
+ @include btn-color($blue-500, $blue-600, $blue-600, $blue-700, $blue-700, $blue-800, $white-light);
}
@mixin btn-orange {
- @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, $white-light);
+ @include btn-color($orange-500, $orange-600, $orange-600, $orange-700, $orange-700, $orange-800, $white-light);
}
@mixin btn-red {
- @include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, $white-light);
+ @include btn-color($red-500, $red-600, $red-600, $red-700, $red-700, $red-800, $white-light);
}
@mixin btn-gray {
@@ -145,11 +141,11 @@
&.btn-new,
&.btn-create,
&.btn-save {
- @include btn-outline($white-light, $border-green-light, $border-green-light, $green-light, $white-light, $border-green-light, $green-normal, $border-green-normal);
+ @include btn-outline($white-light, $green-600, $green-500, $green-500, $white-light, $green-600, $green-600, $green-700);
}
&.btn-remove {
- @include btn-outline($white-light, $border-red-light, $border-red-light, $red-light, $white-light, $border-red-light, $red-normal, $border-red-normal);
+ @include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700);
}
}
@@ -157,11 +153,8 @@
@include btn-gray;
}
- &.btn-primary {
- @include btn-blue-medium;
- }
-
&.btn-info,
+ &.btn-primary,
&.btn-register {
@include btn-blue;
}
@@ -171,11 +164,11 @@
}
&.btn-close {
- @include btn-outline($white-light, $border-orange-light, $border-orange-light, $orange-light, $white-light, $border-orange-light, $orange-normal, $border-orange-normal);
+ @include btn-outline($white-light, $orange-600, $orange-500, $orange-500, $white-light, $orange-600, $orange-600, $orange-700);
}
&.btn-spam {
- @include btn-outline($white-light, $border-red-light, $border-red-light, $red-light, $white-light, $border-red-light, $red-normal, $border-red-normal);
+ @include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700);
}
&.btn-danger,
@@ -360,7 +353,7 @@
.btn-inverted {
&-secondary {
- @include btn-outline($white-light, $border-blue-light, $border-blue-light, $blue-light, $white-light, $border-blue-light, $blue-normal, $border-blue-normal);
+ @include btn-outline($white-light, $blue-500, $blue-500, $blue-500, $white-light, $blue-600, $blue-600, $blue-700);
}
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 186bb9ac616..da5b754aec7 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -195,7 +195,7 @@
text-decoration: none;
.badge {
- background-color: darken($row-hover, 5%);
+ background-color: darken($dropdown-link-hover-bg, 5%);
}
}
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 2890fcd088b..432024779fd 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -177,34 +177,34 @@ label {
}
.gl-field-error {
- color: $red-normal;
+ color: $red-500;
}
.gl-show-field-errors {
.gl-field-success-outline {
- border: 1px solid $green-normal;
+ border: 1px solid $green-600;
&:focus {
- box-shadow: 0 0 0 1px $green-normal inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $green-normal;
+ box-shadow: 0 0 0 1px $green-600 inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $green-600;
border: 0 none;
}
}
.gl-field-error-outline {
- border: 1px solid $red-normal;
+ border: 1px solid $red-500;
&:focus {
- box-shadow: 0 0 0 1px $red-normal inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $gl-field-focus-shadow-error;
+ box-shadow: 0 0 0 1px $red-500 inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $gl-field-focus-shadow-error;
border: 0 none;
}
}
.gl-field-success-message {
- color: $green-normal;
+ color: $green-600;
}
.gl-field-error-message {
- color: $red-normal;
+ color: $red-500;
}
.gl-field-hint {
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 6660a022260..6f356369476 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -26,7 +26,7 @@ header {
padding: 0 16px;
z-index: 100;
margin-bottom: 0;
- height: $header-height;
+ min-height: $header-height;
background-color: $gray-light;
border: none;
border-bottom: 1px solid $border-color;
@@ -85,7 +85,7 @@ header {
.navbar-toggle {
color: $nav-toggle-gray;
- margin: 6px 0;
+ margin: 7px 0;
border-radius: 0;
position: absolute;
right: -10px;
@@ -135,12 +135,14 @@ header {
}
.header-content {
+ display: flex;
+ justify-content: space-between;
position: relative;
- height: $header-height;
+ min-height: $header-height;
padding-left: 30px;
- @media (min-width: $screen-sm-min) {
- padding-right: 0;
+ @media (max-width: $screen-sm-max) {
+ padding-right: 20px;
}
.dropdown-menu {
@@ -165,8 +167,7 @@ header {
}
.group-name-toggle {
- margin: 0 5px;
- vertical-align: sub;
+ margin: 3px 5px;
}
.group-title {
@@ -177,39 +178,32 @@ header {
}
}
+ .title-container {
+ display: flex;
+ align-items: flex-start;
+ flex: 1 1 auto;
+ padding-top: (($header-height - 19) / 2);
+ overflow: hidden;
+ }
+
.title {
position: relative;
padding-right: 20px;
margin: 0;
font-size: 18px;
- max-width: 385px;
+ line-height: 22px;
display: inline-block;
- line-height: $header-height;
font-weight: normal;
color: $gl-text-color;
- overflow: hidden;
- text-overflow: ellipsis;
vertical-align: top;
white-space: nowrap;
- &.initializing {
- display: none;
- }
-
- @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
- max-width: 300px;
- }
-
- @media (max-width: $screen-xs-max) {
- max-width: 190px;
- }
-
- @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
- max-width: 428px;
+ &.wrap {
+ white-space: normal;
}
- @media (min-width: $screen-lg-min) {
- max-width: 685px;
+ &.initializing {
+ opacity: 0;
}
a {
@@ -226,10 +220,10 @@ header {
border: transparent;
background: transparent;
position: absolute;
+ top: 2px;
right: 3px;
width: 12px;
line-height: 19px;
- margin-top: (($header-height - 19) / 2);
padding: 0;
font-size: 10px;
text-align: center;
@@ -247,7 +241,7 @@ header {
}
.navbar-collapse {
- float: right;
+ flex: 0 0 auto;
border-top: none;
@media (min-width: $screen-md-min) {
@@ -255,7 +249,7 @@ header {
}
@media (max-width: $screen-xs-max) {
- float: none;
+ flex: 1 1 auto;
}
}
}
@@ -265,7 +259,7 @@ header {
}
.impersonation i {
- color: $red-normal;
+ color: $red-500;
}
}
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index db8d231a82a..87667f39ab8 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -1,8 +1,8 @@
.ci-status-icon-success {
- color: $gl-success;
+ color: $green-500;
svg {
- fill: $gl-success;
+ fill: $green-500;
}
}
@@ -17,18 +17,18 @@
.ci-status-icon-pending,
.ci-status-icon-failed_with_warnings,
.ci-status-icon-success_with_warnings {
- color: $gl-warning;
+ color: $orange-500;
svg {
- fill: $gl-warning;
+ fill: $orange-500;
}
}
.ci-status-icon-running {
- color: $blue-normal;
+ color: $blue-400;
svg {
- fill: $blue-normal;
+ fill: $blue-400;
}
}
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index 46632f15f35..1537b0744cc 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -33,7 +33,7 @@
}
&.status-box-open {
- background-color: $green-light;
+ background-color: $green-500;
}
&.status-box-expired {
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 4d5a2ca52f0..20c7bc93c28 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -76,28 +76,28 @@ body {
/* Stripe the background colors so that adjacent alert-warnings are distinct from one another */
.alert-warning {
transition: background-color 0.15s, border-color 0.15s;
- background-color: lighten($gl-warning, 4%);
- border-color: lighten($gl-warning, 4%);
+ background-color: $orange-500;
+ border-color: $orange-500;
}
.alert-warning + .alert-warning {
- background-color: $gl-warning;
- border-color: $gl-warning;
+ background-color: $orange-600;
+ border-color: $orange-600;
}
.alert-warning + .alert-warning + .alert-warning {
- background-color: darken($gl-warning, 4%);
- border-color: darken($gl-warning, 4%);
+ background-color: $orange-700;
+ border-color: $orange-700;
}
.alert-warning + .alert-warning + .alert-warning + .alert-warning {
- background-color: darken($gl-warning, 8%);
- border-color: darken($gl-warning, 8%);
+ background-color: $orange-800;
+ border-color: $orange-800;
}
.alert-warning:only-of-type {
- background-color: $gl-warning;
- border-color: $gl-warning;
+ background-color: $orange-500;
+ border-color: $orange-500;
}
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 7adbb0a4188..15dc0aa6a52 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -122,7 +122,7 @@ ul.content-list {
}
.member-group-link {
- color: $blue-normal;
+ color: $blue-600;
}
.description {
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 205d23b1329..5ab505034b6 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -416,14 +416,16 @@
.page-with-layout-nav {
.right-sidebar {
- top: ($header-height * 2) + 2;
+ top: ($header-height + 1) * 2;
}
- .build-sidebar {
- top: ($header-height * 3) + 3;
+ &.page-with-sub-nav {
+ .right-sidebar {
+ top: ($header-height + 1) * 3;
- &.affix {
- top: 0;
+ &.affix {
+ top: 0;
+ }
}
}
}
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index 12a86a64645..e54cc2866a7 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap.scss
@@ -176,6 +176,10 @@ summary {
&.panel-without-border {
border: 0;
}
+
+ &.panel-without-margin {
+ margin: 0;
+ }
}
.panel-succes .panel-heading,
diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index 0fc89d5976a..c9f345d24be 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -31,6 +31,7 @@ $border-radius-small: 3px !default;
//
$text-color: $gl-text-color;
$link-color: $gl-link-color;
+$link-hover-color: $gl-link-hover-color;
//== Typography
@@ -73,7 +74,7 @@ $pagination-hover-color: $gl-text-color;
$pagination-hover-bg: $row-hover;
$pagination-hover-border: $border-color;
-$pagination-active-color: $blue-dark;
+$pagination-active-color: $blue-600;
$pagination-active-bg: $white-light;
$pagination-active-border: $border-color;
@@ -135,8 +136,8 @@ $well-border: #eee;
//
//##
-$code-color: #c7254e;
-$code-bg: #f9f2f4;
+$code-color: $red-600;
+$code-bg: lighten($red-50, 2%);
$kbd-color: $white-light;
$kbd-bg: #333;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 82c9c76c4c0..97794a47df8 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -26,27 +26,49 @@ $gray-dark: darken($gray-light, $darken-dark-factor);
$gray-darker: #eee;
$gray-darkest: #c4c4c4;
-$green-light: #3cbd70;
-$green-normal: darken($green-light, $darken-normal-factor);
-$green-dark: darken($green-light, $darken-dark-factor);
-
-$blue-light: #2ea8e5;
-$blue-normal: darken($blue-light, $darken-normal-factor);
-$blue-dark: darken($blue-light, $darken-dark-factor);
-
-$blue-medium-light: #3498cb;
-$blue-medium: darken($blue-medium-light, $darken-normal-factor);
-$blue-medium-dark: darken($blue-medium-light, $darken-dark-factor);
-
-$blue-light-transparent: rgba(44, 159, 216, 0.05);
-
-$orange-light: #fc8a51;
-$orange-normal: darken($orange-light, $darken-normal-factor);
-$orange-dark: darken($orange-light, $darken-dark-factor);
-
-$red-light: #eb4d5c;
-$red-normal: darken($red-light, $darken-normal-factor);
-$red-dark: darken($red-light, $darken-dark-factor);
+$green-50: #e4f5eb;
+$green-100: #bae6cc;
+$green-200: #8dd5aa;
+$green-300: #5fc488;
+$green-400: #3cb76f;
+$green-500: #1aaa55;
+$green-600: #168f48;
+$green-700: #12753a;
+$green-800: #0e5a2d;
+$green-900: #0a4020;
+
+$blue-50: #e4eff9;
+$blue-100: #bcd7f1;
+$blue-200: #8fbce8;
+$blue-300: #62a1df;
+$blue-400: #418cd8;
+$blue-500: #1f78d1;
+$blue-600: #1b69b6;
+$blue-700: #17599c;
+$blue-800: #134a81;
+$blue-900: #0f3b66;
+
+$orange-50: #fff2e1;
+$orange-100: #fedfb3;
+$orange-200: #feca81;
+$orange-300: #fdb44f;
+$orange-400: #fca429;
+$orange-500: #fc9403;
+$orange-600: #de7e00;
+$orange-700: #c26700;
+$orange-800: #a35100;
+$orange-900: #853b00;
+
+$red-50: #fbe7e4;
+$red-100: #f4c4bc;
+$red-200: #ed9d90;
+$red-300: #e67664;
+$red-400: #e05842;
+$red-500: #db3b21;
+$red-600: #c0341d;
+$red-700: #a62d19;
+$red-800: #8b2615;
+$red-900: #711e11;
$black: #000;
$black-transparent: rgba(0, 0, 0, 0.3);
@@ -58,32 +80,11 @@ $border-gray-light: darken($gray-light, $darken-border-factor);
$border-gray-normal: darken($gray-normal, $darken-border-factor);
$border-gray-dark: darken($white-normal, $darken-border-factor);
-$border-green-extra-light: #9adb84;
-$border-green-light: darken($green-light, $darken-border-factor);
-$border-green-normal: darken($green-normal, $darken-border-factor);
-$border-green-dark: darken($green-dark, $darken-border-factor);
-
-$border-blue-light: darken($blue-light, $darken-border-factor);
-$border-blue-normal: darken($blue-normal, $darken-border-factor);
-$border-blue-dark: darken($blue-dark, $darken-border-factor);
-
-$border-orange-light: darken($orange-light, $darken-border-factor);
-$border-orange-normal: darken($orange-normal, $darken-border-factor);
-$border-orange-dark: darken($orange-dark, $darken-border-factor);
-
-$border-red-light: darken($red-light, $darken-border-factor);
-$border-red-normal: darken($red-normal, $darken-border-factor);
-$border-red-dark: darken($red-dark, $darken-border-factor);
-
-$warning-message-bg: #fbf2d9;
-$warning-message-color: #9e8e60;
-$warning-message-border: #f0e2bb;
-
/*
* UI elements
*/
$border-color: #e5e5e5;
-$focus-border-color: #3aabf0;
+$focus-border-color: $blue-300;
$well-expand-item: #e8f2f7;
$well-inner-border: #eef0f2;
$well-light-border: #f1f1f1;
@@ -96,10 +97,11 @@ $gl-font-size: 14px;
$gl-text-color: rgba(0, 0, 0, .85);
$gl-text-color-secondary: rgba(0, 0, 0, .55);
$gl-text-color-disabled: rgba(0, 0, 0, .35);
-$gl-text-green: #4a2;
-$gl-text-red: #d12f19;
-$gl-text-orange: #d90;
-$gl-link-color: #3777b0;
+$gl-text-green: $green-600;
+$gl-text-red: $red-500;
+$gl-text-orange: $orange-600;
+$gl-link-color: $blue-600;
+$gl-link-hover-color: $blue-800;
$gl-grayish-blue: #7f8fa4;
$gl-gray: $gl-text-color;
$gl-gray-dark: #313236;
@@ -116,9 +118,9 @@ $list-text-disabled-color: $gl-text-color-disabled;
$list-border-light: #eee;
$list-border: rgba(0, 0, 0, 0.05);
$list-text-height: 42px;
-$list-warning-row-bg: #fcf8e3;
-$list-warning-row-border: #faebcc;
-$list-warning-row-color: #8a6d3b;
+$list-warning-row-bg: $orange-50;
+$list-warning-row-border: $orange-100;
+$list-warning-row-color: $orange-700;
/*
* Markdown
@@ -145,24 +147,24 @@ $gl-sidebar-padding: 22px;
/*
* Misc
*/
-$row-hover: #f7faff;
-$row-hover-border: #b2d7ff;
+$row-hover: lighten($blue-50, 2%);
+$row-hover-border: $blue-100;
$progress-color: #c0392b;
$header-height: 50px;
$fixed-layout-width: 1280px;
$limited-layout-width: 990px;
$gl-avatar-size: 40px;
-$error-exclamation-point: #e62958;
+$error-exclamation-point: $red-500;
$border-radius-default: 2px;
$settings-icon-size: 18px;
-$provider-btn-not-active-color: #4688f1;
-$link-underline-blue: #4a8bee;
-$active-item-blue: #4a8bee;
+$provider-btn-not-active-color: $blue-500;
+$link-underline-blue: $blue-500;
+$active-item-blue: $blue-500;
$layout-link-gray: #7e7c7c;
$btn-side-margin: 10px;
$btn-sm-side-margin: 7px;
$btn-xs-side-margin: 5px;
-$issue-status-expired: #cea61b;
+$issue-status-expired: $orange-500;
$issuable-sidebar-color: $gl-text-color-secondary;
$show-aside-bg: #eee;
$show-aside-color: #777;
@@ -191,10 +193,10 @@ $user-mention-color: #2fa0bb;
$time-color: #999;
$project-member-show-color: #aaa;
$gl-promo-color: #aaa;
-$error-bg: #c67;
-$warning-message-bg: #ffffe6;
-$warning-message-border: #ed9;
-$warning-message-color: #b90;
+$error-bg: $red-400;
+$warning-message-bg: $orange-50;
+$warning-message-border: $orange-100;
+$warning-message-color: $orange-700;
$control-group-descr-color: #666;
$table-permission-x-bg: #d9edf7;
$username-color: #666;
@@ -209,30 +211,30 @@ $tanuki-yellow: #fca326;
/*
* State colors:
*/
-$gl-primary: $blue-normal;
-$gl-success: $green-normal;
+$gl-primary: $blue-500;
+$gl-success: $green-500;
$gl-success-focus: rgba($gl-success, .4);
-$gl-info: $blue-normal;
-$gl-warning: $orange-normal;
-$gl-danger: $red-normal;
+$gl-info: $blue-500;
+$gl-warning: $orange-500;
+$gl-danger: $red-500;
$gl-btn-active-background: rgba(0, 0, 0, 0.16);
$gl-btn-active-gradient: inset 0 2px 3px $gl-btn-active-background;
/*
* Commit Diff Colors
*/
-$added: #63c363;
-$deleted: #f77;
-$line-added: #ecfdf0;
-$line-added-dark: #c7f0d2;
-$line-removed: #fbe9eb;
-$line-removed-dark: #fac5cd;
-$line-number-old: #f9d7dc;
-$line-number-new: #ddfbe6;
-$line-number-select: #fbf2da;
-$line-target-blue: #f6faff;
-$line-select-yellow: #fcf8e7;
-$line-select-yellow-dark: #f0e2bd;
+$added: $green-300;
+$deleted: $red-300;
+$line-added: $green-50;
+$line-added-dark: $green-100;
+$line-removed: $red-50;
+$line-removed-dark: $red-100;
+$line-number-old: lighten($red-100, 5%);
+$line-number-new: lighten($green-100, 5%);
+$line-number-select: lighten($orange-100, 5%);
+$line-target-blue: $blue-50;
+$line-select-yellow: $orange-50;
+$line-select-yellow-dark: $orange-100;
$dark-diff-match-bg: rgba(255, 255, 255, 0.3);
$dark-diff-match-color: rgba(255, 255, 255, 0.1);
$file-mode-changed: #777;
@@ -272,7 +274,7 @@ $dropdown-toggle-active-border-color: darken($border-color, 14%);
/*
* Filtered Search
*/
-$dropdown-hover-color: #3b86ff;
+$dropdown-hover-color: $blue-400;
/*
* Buttons
@@ -295,10 +297,10 @@ $award-emoji-menu-shadow: rgba(0,0,0,.175);
/*
* Search Box
*/
-$search-input-border-color: rgba(#4688f1, .8);
+$search-input-border-color: rgba($blue-400, .8);
$search-input-focus-shadow-color: $dropdown-input-focus-shadow;
$search-input-width: 220px;
-$location-badge-active-bg: #4f91f8;
+$location-badge-active-bg: $blue-500;
$location-icon-color: #e7e9ed;
/*
@@ -360,18 +362,18 @@ $builds-trace-bg: #111;
/*
* Callout
*/
-$callout-danger-bg: #fdf7f7;
-$callout-danger-border: #eed3d7;
-$callout-danger-color: #b94a48;
-$callout-warning-bg: #faf8f0;
-$callout-warning-border: #faebcc;
-$callout-warning-color: #8a6d3b;
-$callout-info-bg: #f4f8fa;
-$callout-info-border: #bce8f1;
-$callout-info-color: #34789a;
-$callout-success-bg: #dff0d8;
-$callout-success-border: #5ca64d;
-$callout-success-color: #3c763d;
+$callout-danger-bg: $red-50;
+$callout-danger-border: $red-100;
+$callout-danger-color: $red-700;
+$callout-warning-bg: $orange-50;
+$callout-warning-border: $orange-100;
+$callout-warning-color: $orange-700;
+$callout-info-bg: $blue-50;
+$callout-info-border: $blue-100;
+$callout-info-color: $blue-700;
+$callout-success-bg: $green-50;
+$callout-success-border: $green-100;
+$callout-success-color: $green-700;
/*
* Commit Page
@@ -391,7 +393,7 @@ $common-green: $gl-text-green;
/*
* Editor
*/
-$editor-cancel-color: #b94a48;
+$editor-cancel-color: $red-600;
/*
* Events
@@ -415,10 +417,10 @@ $logs-p-color: #333;
* Forms
*/
$input-danger-bg: #f2dede;
-$input-danger-border: #d66;
+$input-danger-border: $red-400;
$input-group-addon-bg: #f7f8fa;
$gl-field-focus-shadow: rgba(0, 0, 0, 0.075);
-$gl-field-focus-shadow-error: rgba(210, 40, 82, 0.6);
+$gl-field-focus-shadow-error: rgba($red-500, 0.6);
/*
* Help
@@ -452,14 +454,14 @@ $label-border-radius: 100px;
/*
* Lint
*/
-$lint-incorrect-color: red;
-$lint-correct-color: #47a447;
+$lint-incorrect-color: $red-500;
+$lint-correct-color: $green-500;
/*
* Login
*/
$login-brand-holder-color: #888;
-$login-devise-error-color: #a00;
+$login-devise-error-color: $red-700;
/*
* Nav
@@ -473,33 +475,33 @@ $nav-toggle-gray: #666;
*/
$notify-details: #777;
$notify-footer: #777;
-$notify-new-file: #090;
-$notify-deleted-file: #b00;
+$notify-new-file: $green-600;
+$notify-deleted-file: $red-700;
/*
* Projects
*/
$project-option-descr-color: #54565b;
$project-breadcrumb-color: #999;
-$project-private-forks-notice-odd: #2aa056;
+$project-private-forks-notice-odd: $green-600;
$project-network-controls-color: #888;
/*
* Runners
*/
-$runner-state-shared-bg: #32b186;
-$runner-state-specific-bg: #3498db;
-$runner-status-online-color: $green-normal;
+$runner-state-shared-bg: $green-400;
+$runner-state-specific-bg: $blue-400;
+$runner-status-online-color: $green-600;
$runner-status-offline-color: $gray-darkest;
-$runner-status-paused-color: $red-normal;
+$runner-status-paused-color: $red-500;
/*
Stat Graph
*/
$stat-graph-common-bg: #f3f3f3;
-$stat-graph-area-fill: #1db34f;
+$stat-graph-area-fill: $green-500;
$stat-graph-axis-fill: #aaa;
-$stat-graph-orange-fill: #f17f49;
+$stat-graph-orange-fill: $orange-500;
$stat-graph-selection-fill: #333;
$stat-graph-selection-stroke: #333;
@@ -513,7 +515,7 @@ $select2-drop-shadow2: rgba(31, 37, 50, 0.317647);
/*
* Todo
*/
-$todo-alert-blue: #428bca;
+$todo-alert-blue: $blue-500;
$todo-body-pre-color: #777;
$todo-body-border: #ddd;
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index f9ee33019cd..575d32b1a23 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -487,9 +487,9 @@
right: -3px;
top: -3px;
width: 17px;
- background-color: $blue-light;
+ background-color: $blue-500;
color: $white-light;
- border: 1px solid $border-blue-light;
+ border: 1px solid $blue-600;
font-size: 9px;
line-height: 15px;
border-radius: 50%;
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index a24292a7c8c..969fc75c6eb 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -366,9 +366,3 @@
right: 0;
margin-top: -17px;
}
-
-@media (min-width: $screen-md-min) {
- .sub-nav.build {
- width: calc(100% + #{$gutter_width});
- }
-}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index da8410eca66..0dad91ba128 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -142,7 +142,9 @@
border: 1px solid $border-gray-dark;
border-radius: $border-radius-default;
margin-left: 5px;
- line-height: 1;
+ font-size: $gl-font-size;
+ line-height: $gl-font-size;
+ outline: none;
&:hover {
background-color: darken($gray-light, 10%);
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 84d21e48463..cf45f0af2aa 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -9,6 +9,13 @@
}
}
+.group-root-path {
+ max-width: 40vw;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ word-wrap: nowrap;
+}
+
.group-row {
.stats {
float: right;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index ddc0e78c7b6..c1a9bc4be28 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -520,12 +520,12 @@
&.over_estimate {
.meter-fill {
- background: $red-light;
+ background: $red-500;
}
.time-remaining,
.compare-value.spent {
- color: $red-light;
+ color: $red-500;
}
}
}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index b27741a928d..b2f45625a2a 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -69,21 +69,17 @@ ul.related-merge-requests > li {
height: 20px;
border-radius: 3px;
line-height: 18px;
- border: 1px solid;
&.merged {
- border-color: darken($blue-normal, 10%);
- background: $blue-normal;
+ background: $blue-500;
}
&.closed {
- border-color: darken($red-normal, 10%);
- background: $red-normal;
+ background: $red-500;
}
&.open {
- border: 1px solid darken($green-normal, 10%);
- background: $green-normal;
+ background: $green-500;
}
}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index 71ed5b1361a..8249e02b64a 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -85,11 +85,11 @@
}
.username .validation-success {
- color: $green-normal;
+ color: $green-600;
}
.username .validation-error {
- color: $red-normal;
+ color: $red-500;
}
}
}
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
index 5a9f199fb34..35cefd449f1 100644
--- a/app/assets/stylesheets/pages/merge_conflicts.scss
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -255,7 +255,7 @@ $colors: (
&.saved {
.editor {
- border-top: solid 2px $border-green-extra-light;
+ border-top: solid 2px $green-200;
}
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 7c3172421c1..6630904ec92 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -535,7 +535,7 @@
}
.fa-info-circle {
- color: $orange-normal;
+ color: $orange-500;
padding-right: 5px;
}
}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 27c47d36818..efbd9365fd9 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -63,7 +63,7 @@
}
.remaining-days {
- color: $orange-light;
+ color: $orange-600;
}
.milestone-stats-and-buttons {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index e238f0865f6..a2129722633 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -462,17 +462,18 @@ ul.notes {
background: $white-light;
padding: 1px 5px;
font-size: 12px;
- color: $gl-link-color;
+ color: $blue-500;
margin-left: -55px;
position: absolute;
z-index: 10;
width: 23px;
height: 23px;
- border: 1px solid $border-color;
+ border: 1px solid $blue-500;
transition: transform .1s ease-in-out;
&:hover {
- background: $gl-info;
+ background: $blue-500;
+ border-color: $blue-600;
color: $white-light;
transform: scale(1.15);
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 772f05feb12..a4fe652b52f 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -2,6 +2,7 @@
.realtime-loading {
font-size: 40px;
text-align: center;
+ margin: 0 auto;
}
.stage {
@@ -13,6 +14,10 @@
white-space: nowrap;
}
+ .empty-state {
+ margin: 5% auto 0;
+ }
+
.table-holder {
width: 100%;
@@ -668,51 +673,71 @@
// Dropdown button animation in mini pipeline graph
&.ci-status-icon-success {
- border-color: $gl-success;
- color: $gl-success;
+ border-color: $green-500;
+ color: $green-500;
&:hover,
&:focus,
&:active {
- background-color: rgba($gl-success, 0.1);
- border-color: $gl-success;
+ background-color: $green-50;
+ border-color: $green-600;
+ color: $green-600;
+
+ svg {
+ fill: $green-600;
+ }
}
}
&.ci-status-icon-failed {
- border-color: $gl-danger;
- color: $gl-danger;
+ border-color: $red-500;
+ color: $red-500;
&:hover,
&:focus,
&:active {
- background-color: rgba($gl-danger, 0.1);
- border-color: $gl-danger;
+ background-color: $red-50;
+ border-color: $red-600;
+ color: $red-600;
+
+ svg {
+ fill: $red-600;
+ }
}
}
&.ci-status-icon-pending,
&.ci-status-icon-success_with_warnings {
- border-color: $gl-warning;
- color: $gl-warning;
+ border-color: $orange-500;
+ color: $orange-500;
&:hover,
&:focus,
&:active {
- background-color: rgba($gl-warning, 0.1);
- border-color: $gl-warning;
+ background-color: $orange-50;
+ border-color: $orange-600;
+ color: $orange-600;
+
+ svg {
+ fill: $orange-600;
+ }
}
}
&.ci-status-icon-running {
- border-color: $blue-normal;
- color: $blue-normal;
+ border-color: $blue-400;
+ color: $blue-400;
&:hover,
&:focus,
&:active {
- background-color: rgba($blue-normal, 0.1);
- border-color: $blue-normal;
+ background-color: $blue-50;
+ border-color: $blue-600;
+ color: $blue-600;
+
+ svg {
+ fill: $blue-600;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 1a983d8c9ef..703c5fc8869 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -74,7 +74,6 @@
display: inline;
a {
- color: $blue-dark;
text-decoration: none;
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index efa47be9a73..949d52cffa2 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -582,54 +582,55 @@ pre.light-well {
/*
* Projects list rendered on dashboard and user page
*/
-
.projects-list {
@include basic-list;
+ display: flex;
+ flex-direction: column;
.project-row {
- border-color: $white-normal;
-
- .project-full-name {
- @include str-truncated;
+ display: flex;
+ align-items: center;
+ }
- @media (max-width: $screen-xs-max) {
- max-width: 50%;
- }
- }
+ h3 {
+ font-size: $gl-font-size;
+ }
- .controls {
- line-height: $list-text-height;
+ a {
+ color: $gl-text-color;
+ }
- .badge {
- @media (max-width: $screen-xs-max) {
- display: none;
- }
- }
+ .avatar-container,
+ .controls {
+ flex: 0 0 auto;
+ }
- a:hover {
- text-decoration: none;
- }
+ .avatar-container {
+ align-self: flex-start;
+ }
- > span {
- margin-left: 10px;
- }
+ .project-details {
+ min-width: 0;
- svg {
- position: relative;
- top: 2px;
- }
+ p,
+ .commit-row-message {
+ @include str-truncated(100%);
+ margin-bottom: 0;
}
+ }
- .description p {
- @media (max-width: $screen-xs-max) {
- max-width: 50%;
- }
- }
+ .controls {
+ margin-left: auto;
}
- .bottom {
- padding-top: $gl-padding;
- padding-bottom: 0;
+ .ci-status-link {
+ display: inline-block;
+ line-height: 17px;
+ vertical-align: middle;
+
+ &:hover {
+ text-decoration: none;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/sherlock.scss b/app/assets/stylesheets/pages/sherlock.scss
index bed6470dbd3..23a9c2ada80 100644
--- a/app/assets/stylesheets/pages/sherlock.scss
+++ b/app/assets/stylesheets/pages/sherlock.scss
@@ -28,6 +28,6 @@ table .sherlock-code {
}
.sherlock-line-samples-table .slow {
- color: $red-light;
+ color: $red-500;
font-weight: bold;
}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 6f31d4ed789..4a284247143 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -21,42 +21,41 @@
&.ci-failed,
&.ci-failed_with_warnings {
- color: $gl-danger;
- border-color: $gl-danger;
+ color: $red-500;
+ border-color: $red-500;
&:not(span):hover {
- background-color: rgba($gl-danger, .07);
+ background-color: $red-50;
+ color: $red-600;
+ border-color: $red-600;
+
+ svg {
+ fill: $red-600;
+ }
}
svg {
- fill: $gl-danger;
+ fill: $red-500;
}
}
&.ci-success,
&.ci-success_with_warnings {
- color: $gl-success;
- border-color: $gl-success;
+ color: $green-600;
+ border-color: $green-500;
&:not(span):hover {
- background-color: rgba($gl-success, .07);
- }
-
- svg {
- fill: $gl-success;
- }
- }
-
- &.ci-info {
- color: $gl-info;
- border-color: $gl-info;
+ background-color: $green-50;
+ color: $green-700;
+ border-color: $green-600;
- &:not(span):hover {
- background-color: rgba($gl-info, .07);
+ svg {
+ fill: $green-600;
+ }
}
svg {
- fill: $gl-info;
+ fill: $green-500;
}
}
@@ -75,28 +74,41 @@
}
&.ci-pending {
- color: $gl-warning;
- border-color: $gl-warning;
+ color: $orange-600;
+ border-color: $orange-500;
&:not(span):hover {
- background-color: rgba($gl-warning, .07);
+ background-color: $orange-50;
+ color: $orange-700;
+ border-color: $orange-600;
+
+ svg {
+ fill: $orange-600;
+ }
}
svg {
- fill: $gl-warning;
+ fill: $orange-500;
}
}
+ &.ci-info,
&.ci-running {
- color: $blue-normal;
- border-color: $blue-normal;
+ color: $blue-500;
+ border-color: $blue-500;
&:not(span):hover {
- background-color: rgba($blue-normal, .07);
+ background-color: $blue-50;
+ color: $blue-600;
+ border-color: $blue-600;
+
+ svg {
+ fill: $blue-600;
+ }
}
svg {
- fill: $blue-normal;
+ fill: $blue-500;
}
}
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 5f0aede4f5e..b071d7f18cd 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -47,6 +47,7 @@
.todo-avatar,
.todo-actions {
+ @include transition(opacity);
-webkit-flex: 0 0 auto;
flex: 0 0 auto;
}
@@ -67,21 +68,34 @@
flex: 0 1 100%;
min-width: 0;
}
-}
-.todos-list > .todo.todo-pending.done-reversible {
- background-color: $gray-light;
+ &.todo-pending.done-reversible {
+ background-color: $white-light;
- &:hover {
- border-color: $border-color;
- }
+ &:hover {
+ border-color: $white-dark;
+ background-color: $gray-light;
- .title {
- font-weight: normal;
+ .todo-avatar,
+ .todo-item {
+ opacity: .6;
+ }
+ }
+
+ .todo-avatar,
+ .todo-item {
+ opacity: .2;
+ }
+
+ .btn {
+ background-color: $gray-light;
+ }
}
}
.todo-item {
+ @include transition(opacity);
+
.todo-title {
display: flex;
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 24504685e48..563bcc65bd6 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -95,18 +95,14 @@ class Admin::UsersController < Admin::ApplicationController
def create
opts = {
- force_random_password: true,
- password_expires_at: nil
+ reset_password: true,
+ skip_confirmation: true
}
- @user = User.new(user_params.merge(opts))
- @user.created_by_id = current_user.id
- @user.generate_password
- @user.generate_reset_token
- @user.skip_confirmation!
+ @user = Users::CreateService.new(current_user, user_params.merge(opts)).execute
respond_to do |format|
- if @user.save
+ if @user.persisted?
format.html { redirect_to [:admin, @user], notice: 'User was successfully created.' }
format.json { render json: @user, status: :created, location: @user }
else
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index 886934a3f67..3f3c90a49ab 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -1,7 +1,7 @@
class Projects::BuildsController < Projects::ApplicationController
before_action :build, except: [:index, :cancel_all]
before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry, :play]
- before_action :authorize_update_build!, except: [:index, :show, :status, :raw]
+ before_action :authorize_update_build!, except: [:index, :show, :status, :raw, :trace]
layout 'project'
def index
@@ -74,7 +74,9 @@ class Projects::BuildsController < Projects::ApplicationController
end
def status
- render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha)
+ render json: BuildSerializer
+ .new(project: @project, user: @current_user)
+ .represent_status(@build)
end
def erase
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 0d6d9f492c1..d984e6d3918 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -260,4 +260,13 @@ class Projects::IssuesController < Projects::ApplicationController
:milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: []
)
end
+
+ def authenticate_user!
+ return if current_user
+
+ notice = "Please sign in to create the new issue."
+
+ store_location_for :user, request.fullpath
+ redirect_to new_user_session_path, notice: notice
+ end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 677a8a1a73a..1ee96799792 100644..100755
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -10,7 +10,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled
before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :merge_check,
- :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues
+ :ci_status, :pipeline_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues
]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines]
before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines]
@@ -402,7 +402,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
if params[:ref].present?
@ref = params[:ref]
- @commit = @repository.commit(@ref)
+ @commit = @repository.commit("refs/heads/#{@ref}")
end
render layout: false
@@ -413,7 +413,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
if params[:ref].present?
@ref = params[:ref]
- @commit = @target_project.commit(@ref)
+ @commit = @target_project.commit("refs/heads/#{@ref}")
end
render layout: false
@@ -473,6 +473,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
render json: response
end
+ def pipeline_status
+ render json: PipelineSerializer
+ .new(project: @project, user: @current_user)
+ .represent_status(@merge_request.head_pipeline)
+ end
+
def ci_environments_status
environments =
begin
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index be52b0fa7cf..5922e686cd0 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -13,11 +13,14 @@ class Projects::MilestonesController < Projects::ApplicationController
def index
@milestones =
case params[:state]
- when 'all' then @project.milestones.reorder(due_date: :desc, title: :asc)
- when 'closed' then @project.milestones.closed.reorder(due_date: :desc, title: :asc)
- else @project.milestones.active.reorder(due_date: :asc, title: :asc)
+ when 'all' then @project.milestones
+ when 'closed' then @project.milestones.closed
+ else @project.milestones.active
end
+ @sort = params[:sort] || 'due_date_asc'
+ @milestones = @milestones.sort(@sort)
+
@milestones = @milestones.includes(:project)
respond_to do |format|
format.html do
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 718d9e86bea..43a1abaa662 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -72,6 +72,12 @@ class Projects::PipelinesController < Projects::ApplicationController
end
end
+ def status
+ render json: PipelineSerializer
+ .new(project: @project, user: @current_user)
+ .represent_status(@pipeline)
+ end
+
def stage
@stage = pipeline.stage(params[:stage])
return not_found unless @stage
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index b44f38d4a0c..a49a1f50a81 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -1,5 +1,4 @@
class RegistrationsController < Devise::RegistrationsController
- before_action :signup_enabled?
include Recaptcha::Verify
def new
@@ -21,6 +20,8 @@ class RegistrationsController < Devise::RegistrationsController
flash.delete :recaptcha_error
render action: 'new'
end
+ rescue Gitlab::Access::AccessDeniedError
+ redirect_to(new_user_session_path)
end
def destroy
@@ -50,12 +51,6 @@ class RegistrationsController < Devise::RegistrationsController
private
- def signup_enabled?
- unless current_application_settings.signup_enabled?
- redirect_to(new_user_session_path)
- end
- end
-
def sign_up_params
params.require(:user).permit(:username, :email, :email_confirmation, :name, :password)
end
@@ -65,7 +60,7 @@ class RegistrationsController < Devise::RegistrationsController
end
def resource
- @resource ||= User.new(sign_up_params)
+ @resource ||= Users::CreateService.new(current_user, sign_up_params).build
end
def devise_mapping
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index fa0e2a5e3d8..e52083f86e4 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -20,8 +20,17 @@ class LabelsFinder < UnionFinder
if project?
if project
- label_ids << project.group.labels if project.group.present?
- label_ids << project.labels
+ if project.group.present?
+ labels_table = Label.arel_table
+
+ label_ids << Label.where(
+ labels_table[:type].eq('GroupLabel').and(labels_table[:group_id].eq(project.group.id)).or(
+ labels_table[:type].eq('ProjectLabel').and(labels_table[:project_id].eq(project.id))
+ )
+ )
+ else
+ label_ids << project.labels
+ end
end
else
label_ids << Label.where(group_id: projects.group_ids)
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index 5053b937c02..bd3f51fc658 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -89,10 +89,12 @@ module MilestonesHelper
content = time_ago.gsub(/\d+/) { |match| "<strong>#{match}</strong>" }
content.slice!("about ")
content << " remaining"
+ content.html_safe
elsif milestone.start_date && milestone.start_date.past?
days = milestone.elapsed_days
content = content_tag(:strong, days)
content << " #{'day'.pluralize(days)} elapsed"
+ content.html_safe
end
end
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index 2e3a15bc1b9..7f656b8caae 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -6,7 +6,13 @@ module NamespacesHelper
def namespaces_options(selected = :current_user, display_path: false, extra_group: nil)
groups = current_user.owned_groups + current_user.masters_groups
- groups << extra_group if extra_group && !Group.exists?(name: extra_group.name)
+ unless extra_group.nil? || extra_group.is_a?(Group)
+ extra_group = Group.find(extra_group) if Namespace.find(extra_group).kind == 'group'
+ end
+
+ if extra_group && extra_group.is_a?(Group) && (!Group.exists?(name: extra_group.name) || Ability.allowed?(current_user, :read_group, extra_group))
+ groups |= [extra_group]
+ end
users = [current_user.namespace]
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index a8f167cbff2..991fd949b94 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -31,7 +31,11 @@ module NavHelper
end
def layout_nav_class
- "page-with-layout-nav" if defined?(nav) && nav
+ class_name = ''
+ class_name << " page-with-layout-nav" if defined?(nav) && nav
+ class_name << " page-with-sub-nav" if content_for?(:sub_nav)
+
+ class_name
end
def nav_control_class
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 959ee310867..5c89cbea3fc 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -2,6 +2,7 @@ module SortingHelper
def sort_options_hash
{
sort_value_name => sort_title_name,
+ sort_value_name_desc => sort_title_name_desc,
sort_value_recently_updated => sort_title_recently_updated,
sort_value_oldest_updated => sort_title_oldest_updated,
sort_value_recently_created => sort_title_recently_created,
@@ -50,6 +51,17 @@ module SortingHelper
}
end
+ def milestone_sort_options_hash
+ {
+ sort_value_name => sort_title_name_asc,
+ sort_value_name_desc => sort_title_name_desc,
+ sort_value_due_date_soon => sort_title_due_date_soon,
+ sort_value_due_date_later => sort_title_due_date_later,
+ sort_value_start_date_soon => sort_title_start_date_soon,
+ sort_value_start_date_later => sort_title_start_date_later,
+ }
+ end
+
def sort_title_priority
'Priority'
end
@@ -90,6 +102,14 @@ module SortingHelper
'Due later'
end
+ def sort_title_start_date_soon
+ 'Start soon'
+ end
+
+ def sort_title_start_date_later
+ 'Start later'
+ end
+
def sort_title_name
'Name'
end
@@ -202,6 +222,14 @@ module SortingHelper
'due_date_desc'
end
+ def sort_value_start_date_soon
+ 'start_date_asc'
+ end
+
+ def sort_value_start_date_later
+ 'start_date_desc'
+ end
+
def sort_value_name
'name_asc'
end
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
new file mode 100644
index 00000000000..9c623c9ba7c
--- /dev/null
+++ b/app/helpers/users_helper.rb
@@ -0,0 +1,7 @@
+module UsersHelper
+ def user_link(user)
+ link_to(user.name, user_path(user),
+ title: user.email,
+ class: 'has-tooltip commit-committer-link')
+ end
+end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 4759829a15c..5ff83944d8c 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -154,8 +154,10 @@ class MergeRequest < ActiveRecord::Base
#
# Returns an ActiveRecord::Relation.
def self.in_projects(relation)
- source = where(source_project_id: relation).select(:id)
- target = where(target_project_id: relation).select(:id)
+ # unscoping unnecessary conditions that'll be applied
+ # when executing `where("merge_requests.id IN (#{union.to_sql})")`
+ source = unscoped.where(source_project_id: relation).select(:id)
+ target = unscoped.where(target_project_id: relation).select(:id)
union = Gitlab::SQL::Union.new([source, target])
where("merge_requests.id IN (#{union.to_sql})")
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index c0deb59ec4c..e85d5709624 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -107,6 +107,21 @@ class Milestone < ActiveRecord::Base
end
end
+ def self.sort(method)
+ case method.to_s
+ when 'due_date_asc'
+ reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC'))
+ when 'due_date_desc'
+ reorder(Gitlab::Database.nulls_last_order('due_date', 'DESC'))
+ when 'start_date_asc'
+ reorder(Gitlab::Database.nulls_last_order('start_date', 'ASC'))
+ when 'start_date_desc'
+ reorder(Gitlab::Database.nulls_last_order('start_date', 'DESC'))
+ else
+ order_by(method)
+ end
+ end
+
##
# Returns the String necessary to reference this Milestone in Markdown
#
diff --git a/app/models/project.rb b/app/models/project.rb
index 04641dd58a0..f1bba56d32c 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -314,20 +314,15 @@ class Project < ActiveRecord::Base
ntable = Namespace.arel_table
pattern = "%#{query}%"
- projects = select(:id).where(
+ # unscoping unnecessary conditions that'll be applied
+ # when executing `where("projects.id IN (#{union.to_sql})")`
+ projects = unscoped.select(:id).where(
ptable[:path].matches(pattern).
or(ptable[:name].matches(pattern)).
or(ptable[:description].matches(pattern))
)
- # We explicitly remove any eager loading clauses as they're:
- #
- # 1. Not needed by this query
- # 2. Combined with .joins(:namespace) lead to all columns from the
- # projects & namespaces tables being selected, leading to a SQL error
- # due to the columns of all UNION'd queries no longer being the same.
- namespaces = select(:id).
- except(:includes).
+ namespaces = unscoped.select(:id).
joins(:namespace).
where(ntable[:name].matches(pattern))
diff --git a/app/models/user.rb b/app/models/user.rb
index 5d19d873f43..612066654dc 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -115,7 +115,9 @@ class User < ActiveRecord::Base
validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email }
validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true
validates :bio, length: { maximum: 255 }, allow_blank: true
- validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 }
+ validates :projects_limit,
+ presence: true,
+ numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
validates :username,
namespace: true,
presence: true,
@@ -126,10 +128,9 @@ class User < ActiveRecord::Base
validate :unique_email, if: ->(user) { user.email_changed? }
validate :owns_notification_email, if: ->(user) { user.notification_email_changed? }
validate :owns_public_email, if: ->(user) { user.public_email_changed? }
+ validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
- before_validation :generate_password, on: :create
- before_validation :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
before_validation :sanitize_attrs
before_validation :set_notification_email, if: ->(user) { user.email_changed? }
before_validation :set_public_email, if: ->(user) { user.public_email_changed? }
@@ -139,8 +140,6 @@ class User < ActiveRecord::Base
before_save :ensure_external_user_rights
after_save :ensure_namespace_correct
after_initialize :set_projects_limit
- before_create :check_confirmation_email
- after_create :post_create_hook
after_destroy :post_destroy_hook
# User's Layout preference
@@ -384,10 +383,8 @@ class User < ActiveRecord::Base
"#{self.class.reference_prefix}#{username}"
end
- def generate_password
- if force_random_password
- self.password = self.password_confirmation = Devise.friendly_token.first(Devise.password_length.min)
- end
+ def skip_confirmation=(bool)
+ skip_confirmation! if bool
end
def generate_reset_token
@@ -399,10 +396,6 @@ class User < ActiveRecord::Base
@reset_token
end
- def check_confirmation_email
- skip_confirmation! unless current_application_settings.send_user_confirmation_email
- end
-
def recently_sent_password_reset?
reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
end
@@ -797,12 +790,6 @@ class User < ActiveRecord::Base
end
end
- def post_create_hook
- log_info("User \"#{name}\" (#{email}) was created")
- notification_service.new_user(self, @reset_token) if created_by_id
- system_hook_service.execute_hooks_for(self, :create)
- end
-
def post_destroy_hook
log_info("User \"#{name}\" (#{email}) was removed")
system_hook_service.execute_hooks_for(self, :destroy)
diff --git a/app/serializers/build_entity.rb b/app/serializers/build_entity.rb
index 5bcbe285052..fadd6c5c597 100644
--- a/app/serializers/build_entity.rb
+++ b/app/serializers/build_entity.rb
@@ -18,10 +18,17 @@ class BuildEntity < Grape::Entity
expose :created_at
expose :updated_at
+ expose :detailed_status, as: :status, with: StatusEntity
private
+ alias_method :build, :object
+
def path_to(route, build)
send("#{route}_path", build.project.namespace, build.project, build)
end
+
+ def detailed_status
+ build.detailed_status(request.user)
+ end
end
diff --git a/app/serializers/build_serializer.rb b/app/serializers/build_serializer.rb
new file mode 100644
index 00000000000..79b67001199
--- /dev/null
+++ b/app/serializers/build_serializer.rb
@@ -0,0 +1,8 @@
+class BuildSerializer < BaseSerializer
+ entity BuildEntity
+
+ def represent_status(resource)
+ data = represent(resource, { only: [:status] })
+ data.fetch(:status, {})
+ end
+end
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index 61f0f11d7d2..3f16dd66d54 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -12,12 +12,7 @@ class PipelineEntity < Grape::Entity
end
expose :details do
- expose :status do |pipeline, options|
- StatusEntity.represent(
- pipeline.detailed_status(request.user),
- options)
- end
-
+ expose :detailed_status, as: :status, with: StatusEntity
expose :duration
expose :finished_at
expose :stages, using: StageEntity
@@ -82,4 +77,8 @@ class PipelineEntity < Grape::Entity
pipeline.cancelable? &&
can?(request.user, :update_pipeline, pipeline)
end
+
+ def detailed_status
+ pipeline.detailed_status(request.user)
+ end
end
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
index ab2d3d5a3ec..7829df9fada 100644
--- a/app/serializers/pipeline_serializer.rb
+++ b/app/serializers/pipeline_serializer.rb
@@ -22,4 +22,11 @@ class PipelineSerializer < BaseSerializer
super(resource, opts)
end
end
+
+ def represent_status(resource)
+ return {} unless resource.present?
+
+ data = represent(resource, { only: [{ details: [:status] }] })
+ data.dig(:details, :status) || {}
+ end
end
diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb
index 47066bebfb1..dfd9d1584a1 100644
--- a/app/serializers/status_entity.rb
+++ b/app/serializers/status_entity.rb
@@ -1,7 +1,7 @@
class StatusEntity < Grape::Entity
include RequestAwareEntity
- expose :icon, :text, :label, :group
+ expose :icon, :favicon, :text, :label, :group
expose :has_details?, as: :has_details
expose :details_path
diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb
index 574561adc4c..f72ddbf690c 100644
--- a/app/services/ci/retry_pipeline_service.rb
+++ b/app/services/ci/retry_pipeline_service.rb
@@ -7,14 +7,14 @@ module Ci
raise Gitlab::Access::AccessDeniedError
end
- pipeline.builds.failed_or_canceled.find_each do |build|
+ pipeline.builds.latest.failed_or_canceled.find_each do |build|
next unless build.retryable?
Ci::RetryBuildService.new(project, current_user)
.reprocess(build)
end
- pipeline.builds.skipped.find_each do |skipped|
+ pipeline.builds.latest.skipped.find_each do |skipped|
retry_optimistic_lock(skipped) { |build| build.process }
end
diff --git a/app/services/users/create_service.rb b/app/services/users/create_service.rb
new file mode 100644
index 00000000000..f4f0b80f30a
--- /dev/null
+++ b/app/services/users/create_service.rb
@@ -0,0 +1,110 @@
+module Users
+ # Service for creating a new user.
+ class CreateService < BaseService
+ def initialize(current_user, params = {})
+ @current_user = current_user
+ @params = params.dup
+ end
+
+ def build
+ raise Gitlab::Access::AccessDeniedError unless can_create_user?
+
+ user = User.new(build_user_params)
+
+ if current_user&.is_admin?
+ if params[:reset_password]
+ @reset_token = user.generate_reset_token
+ params[:force_random_password] = true
+ end
+
+ if params[:force_random_password]
+ random_password = Devise.friendly_token.first(Devise.password_length.min)
+ user.password = user.password_confirmation = random_password
+ end
+ end
+
+ identity_attrs = params.slice(:extern_uid, :provider)
+
+ if identity_attrs.any?
+ user.identities.build(identity_attrs)
+ end
+
+ user
+ end
+
+ def execute
+ user = build
+
+ if user.save
+ log_info("User \"#{user.name}\" (#{user.email}) was created")
+ notification_service.new_user(user, @reset_token) if @reset_token
+ system_hook_service.execute_hooks_for(user, :create)
+ end
+
+ user
+ end
+
+ private
+
+ def can_create_user?
+ (current_user.nil? && current_application_settings.signup_enabled?) || current_user&.is_admin?
+ end
+
+ # Allowed params for creating a user (admins only)
+ def admin_create_params
+ [
+ :access_level,
+ :admin,
+ :avatar,
+ :bio,
+ :can_create_group,
+ :color_scheme_id,
+ :email,
+ :external,
+ :force_random_password,
+ :hide_no_password,
+ :hide_no_ssh_key,
+ :key_id,
+ :linkedin,
+ :name,
+ :password,
+ :password_expires_at,
+ :projects_limit,
+ :remember_me,
+ :skip_confirmation,
+ :skype,
+ :theme_id,
+ :twitter,
+ :username,
+ :website_url
+ ]
+ end
+
+ # Allowed params for user signup
+ def signup_params
+ [
+ :email,
+ :email_confirmation,
+ :name,
+ :password,
+ :username
+ ]
+ end
+
+ def build_user_params
+ if current_user&.is_admin?
+ user_params = params.slice(*admin_create_params)
+ user_params[:created_by_id] = current_user.id
+
+ if params[:reset_password]
+ user_params.merge!(force_random_password: true, password_expires_at: nil)
+ end
+ else
+ user_params = params.slice(*signup_params)
+ user_params[:skip_confirmation] = !current_application_settings.send_user_confirmation_email
+ end
+
+ user_params
+ end
+ end
+end
diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml
index 7855239dfe5..794aaec89bd 100644
--- a/app/views/admin/users/_access_levels.html.haml
+++ b/app/views/admin/users/_access_levels.html.haml
@@ -2,7 +2,7 @@
%legend Access
.form-group
= f.label :projects_limit, class: 'control-label'
- .col-sm-10= f.number_field :projects_limit, min: 0, class: 'form-control'
+ .col-sm-10= f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control'
.form-group
= f.label :can_create_group, class: 'control-label'
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index e31fa5fbe95..52d6ebd8a14 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -68,12 +68,11 @@
= link_to todos_filter_path(sort: sort_value_oldest_created) do
= sort_title_oldest_created
-
.js-todos-all
- if @todos.any?
.js-todos-list-container
.js-todos-options{ data: { per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages } }
- .panel.panel-default.panel-small.panel-without-border
+ .panel.panel-default.panel-without-border.panel-without-margin
%ul.content-list.todos-list
= render @todos
= paginate @todos, theme: "gitlab"
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index 56f463572bb..f630f1effdc 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -17,24 +17,3 @@
= link_to filter_projects_path(visibility_level: level) do
= visibility_level_icon(level)
= visibility_level_label(level)
-
-- if @tags.present?
- .dropdown
- %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" }
- = icon('tags')
- %span.light Tags:
- - if params[:tag].present?
- = params[:tag]
- - else
- Any
- = icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right
- %li
- = link_to filter_projects_path(tag: nil) do
- Any
-
- - @tags.each do |tag|
- %li{ class: active_when(tag.name == params[:tag]) || 'light' }
- = link_to filter_projects_path(tag: tag.name) do
- = icon('tag')
- = tag.name
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index a35a918d501..b7df11681d3 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -3,8 +3,9 @@
.layout-nav
.container-fluid
= render "layouts/nav/#{nav}"
- .content-wrapper{ class: "#{layout_nav_class}" }
+ - if content_for?(:sub_nav)
= yield :sub_nav
+ .content-wrapper{ class: layout_nav_class }
.alert-wrapper
= render "layouts/broadcast"
= render "layouts/flash"
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 7ddee0e5244..7bf4bc70f7c 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -15,6 +15,13 @@
%span.sr-only Toggle navigation
= icon('ellipsis-v')
+ .header-logo
+ = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do
+ = brand_header_logo
+
+ .title-container
+ %h1.title{ class: ('initializing' if @has_group_title) }= title
+
.navbar-collapse.collapse
%ul.nav.navbar-nav
%li.hidden-sm.hidden-xs
@@ -63,12 +70,6 @@
%div
= link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
- .header-logo
- = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do
- = brand_header_logo
-
- %h1.title{ class: ('initializing' if @has_group_title) }= title
-
= yield :header_content
= render 'shared/outdated_browser'
diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml
index 4924c73cf8e..e14885f264b 100644
--- a/app/views/projects/blob/_upload.html.haml
+++ b/app/views/projects/blob/_upload.html.haml
@@ -5,7 +5,7 @@
%a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title= title
.modal-body
- = form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form form-horizontal' do
+ = form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form form-horizontal', data: { method: method } do
.dropzone
.dropzone-previews.blob-upload-dropzone-previews
%p.dz-message.light
@@ -24,8 +24,5 @@
.inline.prepend-left-10
= commit_in_fork_help
-
-:javascript
- gl.utils.disableButtonIfEmptyField($('.js-upload-blob-form').find('.js-commit-message'), '.btn-upload-file');
- new BlobFileDropzone($('.js-upload-blob-form'), '#{method}');
- new NewCommitForm($('.js-upload-blob-form'))
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('blob')
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 3bcddcb37f1..afe0b5dba45 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -2,7 +2,7 @@
- page_title "Edit", @blob.path, @ref
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
- = page_specific_javascript_bundle_tag('blob_edit')
+ = page_specific_javascript_bundle_tag('blob')
= render "projects/commits/head"
%div{ class: container_class }
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index e0ce8cc9601..4c449e040ee 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -1,7 +1,7 @@
- page_title "New File", @path.presence, @ref
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
- = page_specific_javascript_bundle_tag('blob_edit')
+ = page_specific_javascript_bundle_tag('blob')
%h3.page-title
New File
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index 78720d88e4e..b597c7f7a12 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -1,6 +1,6 @@
- builds = @build.pipeline.builds.to_a
-%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "151", "spy" => "affix" } }
+%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "153", "spy" => "affix" } }
.block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default
Job
%strong ##{@build.id}
@@ -137,3 +137,6 @@
= build.id
- if build.retried?
%i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
+
+:javascript
+ new Sidebar();
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index 307010edb58..d5fe771613c 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -1,6 +1,6 @@
- @no_container = true
- page_title "#{@build.name} (##{@build.id})", "Jobs"
-= render "projects/pipelines/head", build_subnav: true
+= render "projects/pipelines/head"
%div{ class: container_class }
.build-page
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index da5a676274f..09e3a775d1c 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -1,6 +1,7 @@
- disable_initialization = local_assigns.fetch(:disable_initialization, false)
#commit-pipeline-table-view{ data: { disable_initialization: disable_initialization,
endpoint: endpoint,
+ "help-page-path" => help_page_path('ci/quick_start/README'),
} }
- content_for :page_specific_javascripts do
diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml
index ef0dd0eda3c..c8363087d6a 100644
--- a/app/views/projects/environments/terminal.html.haml
+++ b/app/views/projects/environments/terminal.html.haml
@@ -16,7 +16,7 @@
.col-sm-6
.nav-controls
- = link_to @environment.external_url, class: 'btn btn-default' do
+ = link_to @environment.external_url, class: 'btn btn-default', target: '_blank', rel: 'noopener noreferrer nofollow' do
= icon('external-link')
= render 'projects/deployments/actions', deployment: @environment.last_deployment
diff --git a/app/views/projects/merge_requests/merge.js.haml b/app/views/projects/merge_requests/merge.js.haml
index f0a23bec5e7..e632fc681cf 100644
--- a/app/views/projects/merge_requests/merge.js.haml
+++ b/app/views/projects/merge_requests/merge.js.haml
@@ -1,7 +1,8 @@
- case @status
- when :success
+ - remove_source_branch = params[:should_remove_source_branch] == '1' || @merge_request.remove_source_branch?
:plain
- merge_request_widget.mergeInProgress(#{params[:should_remove_source_branch] == '1'});
+ merge_request_widget.mergeInProgress(#{remove_source_branch});
- when :merge_when_pipeline_succeeds
:plain
$('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_pipeline_succeeds'))}");
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index 918f5d161bb..b6340a00b29 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -7,6 +7,7 @@
= render 'shared/milestones_filter', counts: milestone_counts(@project.milestones)
.nav-controls
+ = render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @project)
= link_to new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New Milestone' do
New Milestone
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index d129da943f8..34a1214a350 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -23,7 +23,7 @@
- if current_user.can_select_namespace?
.input-group-addon
= root_url
- = f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true), {}, {class: 'select2 js-select-namespace', tabindex: 1}
+ = f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), {}, { class: 'select2 js-select-namespace', tabindex: 1}
- else
.input-group-addon.static-namespace
diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml
index a5acb7ac4a5..b02fef638ff 100644
--- a/app/views/projects/pipelines/_head.html.haml
+++ b/app/views/projects/pipelines/_head.html.haml
@@ -1,7 +1,7 @@
= content_for :sub_nav do
.scrolling-tabs-container.sub-nav-scroll
= render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs{ class: ('build' if local_assigns.fetch(:build_subnav, false)) }
+ .nav-links.sub-nav.scrolling-tabs
%ul{ class: (container_class) }
- if project_nav_tab? :pipelines
= nav_link(path: 'pipelines#index', controller: :pipelines) do
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 0605af4fcd3..4be9a1371ec 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -1,10 +1,12 @@
.page-content-header
.header-main-content
= render 'ci/status/badge', status: @pipeline.detailed_status(current_user)
- %strong Pipeline ##{@commit.pipelines.last.id}
- triggered #{time_ago_with_tooltip(@commit.authored_date)} by
- = author_avatar(@commit, size: 24)
- = commit_author_link(@commit)
+ %strong Pipeline ##{@pipeline.id}
+ triggered #{time_ago_with_tooltip(@pipeline.created_at)}
+ - if @pipeline.user
+ by
+ = user_avatar(user: @pipeline.user, size: 24)
+ = user_link(@pipeline.user)
.header-action-buttons
- if can?(current_user, :update_pipeline, @pipeline.project)
- if @pipeline.retryable?
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 5d59ce06612..3d73284699f 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -2,53 +2,19 @@
- page_title "Pipelines"
= render "projects/pipelines/head"
-%div{ class: container_class }
- .top-area
- %ul.nav-links
- %li.js-pipelines-tab-all{ class: active_when(@scope.nil?) }>
- = link_to project_pipelines_path(@project) do
- All
- %span.badge.js-totalbuilds-count
- = number_with_delimiter(@pipelines_count)
-
- %li.js-pipelines-tab-pending{ class: active_when(@scope == 'pending') }>
- = link_to project_pipelines_path(@project, scope: :pending) do
- Pending
- %span.badge
- = number_with_delimiter(@pending_count)
-
- %li.js-pipelines-tab-running{ class: active_when(@scope == 'running') }>
- = link_to project_pipelines_path(@project, scope: :running) do
- Running
- %span.badge.js-running-count
- = number_with_delimiter(@running_count)
-
- %li.js-pipelines-tab-finished{ class: active_when(@scope == 'finished') }>
- = link_to project_pipelines_path(@project, scope: :finished) do
- Finished
- %span.badge
- = number_with_delimiter(@finished_count)
-
- %li.js-pipelines-tab-branches{ class: active_when(@scope == 'branches') }>
- = link_to project_pipelines_path(@project, scope: :branches) do
- Branches
-
- %li.js-pipelines-tab-tags{ class: active_when(@scope == 'tags') }>
- = link_to project_pipelines_path(@project, scope: :tags) do
- Tags
-
- .nav-controls
- - if can? current_user, :create_pipeline, @project
- = link_to new_namespace_project_pipeline_path(@project.namespace, @project), class: 'btn btn-create' do
- Run pipeline
-
- - unless @repository.gitlab_ci_yml
- = link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
-
- = link_to ci_lint_path, class: 'btn btn-default' do
- %span CI Lint
- .content-list.pipelines{ data: { url: namespace_project_pipelines_path(@project.namespace, @project, format: :json) } }
- .vue-pipelines-index
+#pipelines-list-vue{ data: { endpoint: namespace_project_pipelines_path(@project.namespace, @project, format: :json),
+ "css-class" => container_class,
+ "help-page-path" => help_page_path('ci/quick_start/README'),
+ "new-pipeline-path" => new_namespace_project_pipeline_path(@project.namespace, @project),
+ "can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
+ "all-path" => project_pipelines_path(@project),
+ "pending-path" => project_pipelines_path(@project, scope: :pending),
+ "running-path" => project_pipelines_path(@project, scope: :running),
+ "finished-path" => project_pipelines_path(@project, scope: :finished),
+ "branches-path" => project_pipelines_path(@project, scope: :branches),
+ "tags-path" => project_pipelines_path(@project, scope: :tags),
+ "has-ci" => @repository.gitlab_ci_yml,
+ "ci-lint-path" => ci_lint_path } }
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('vue_pipelines')
diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml
index 8c582f747b3..713b758727e 100644
--- a/app/views/projects/wikis/_sidebar.html.haml
+++ b/app/views/projects/wikis/_sidebar.html.haml
@@ -1,4 +1,4 @@
-%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } }
+%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar.js-right-sidebar{ data: { "offset-top" => "102", "spy" => "affix" } }
.block.wiki-sidebar-header.append-bottom-default
%a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-wiki-toggle{ href: "#" }
= icon('angle-double-right')
@@ -19,3 +19,6 @@
More Pages
= render 'projects/wikis/new'
+
+:javascript
+ new Sidebar();
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index c2d9ac87b20..7974eb67f0f 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -1,4 +1,6 @@
- parent = Group.find_by(id: params[:parent_id] || @group.parent_id)
+- group_path = root_url
+- group_path << parent.full_path + '/' if parent
- if @group.persisted?
.form-group
= f.label :name, class: 'control-label' do
@@ -11,7 +13,7 @@
Group path
.col-sm-10
.input-group.gl-field-error-anchor
- .input-group-addon
+ .group-root-path.input-group-addon.has-tooltip{ title: group_path, :'data-placement' => 'bottom' }
%span>= root_url
- if parent
%strong= parent.full_path + '/'
diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index 57a0eaa919e..db2ac1e1d12 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -4,10 +4,10 @@
Open
%span.badge= counts[:opened]
%li{ class: milestone_class_for_state(params[:state], 'closed') }>
- = link_to milestones_filter_path(state: 'closed') do
+ = link_to milestones_filter_path(state: 'closed', sort: 'due_date_desc') do
Closed
%span.badge= counts[:closed]
%li{ class: milestone_class_for_state(params[:state], 'all') }>
- = link_to milestones_filter_path(state: 'all') do
+ = link_to milestones_filter_path(state: 'all', sort: 'due_date_desc') do
All
%span.badge= counts[:all]
diff --git a/app/views/shared/_milestones_sort_dropdown.html.haml b/app/views/shared/_milestones_sort_dropdown.html.haml
new file mode 100644
index 00000000000..9b2f2fdcc93
--- /dev/null
+++ b/app/views/shared/_milestones_sort_dropdown.html.haml
@@ -0,0 +1,22 @@
+.dropdown.inline.prepend-left-10
+ %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown' } }
+ %span.light
+ - if @sort.present?
+ = milestone_sort_options_hash[@sort]
+ - else
+ = sort_title_due_date_soon
+ = icon('chevron-down')
+ %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
+ %li
+ = link_to page_filter_path(sort: sort_value_due_date_soon, label: true) do
+ = sort_title_due_date_soon
+ = link_to page_filter_path(sort: sort_value_due_date_later, label: true) do
+ = sort_title_due_date_later
+ = link_to page_filter_path(sort: sort_value_start_date_soon, label: true) do
+ = sort_title_start_date_soon
+ = link_to page_filter_path(sort: sort_value_start_date_later, label: true) do
+ = sort_title_start_date_later
+ = link_to page_filter_path(sort: sort_value_name, label: true) do
+ = sort_title_name_asc
+ = link_to page_filter_path(sort: sort_value_name_desc, label: true) do
+ = sort_title_name_desc
diff --git a/app/views/shared/empty_states/icons/_pipelines_empty.svg b/app/views/shared/empty_states/icons/_pipelines_empty.svg
new file mode 100644
index 00000000000..8119d5bebe0
--- /dev/null
+++ b/app/views/shared/empty_states/icons/_pipelines_empty.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 150"><g fill="none" fill-rule="evenodd" transform="translate(0-3)"><g transform="translate(0 105)"><g fill="#e5e5e5"><rect width="78" height="4" x="34" y="21" opacity=".5" rx="2"/><path d="m152 23c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2"/></g><g transform="translate(0 4)"><path fill="#98d7b2" fill-rule="nonzero" d="m19 38c-10.493 0-19-8.507-19-19 0-10.493 8.507-19 19-19 10.493 0 19 8.507 19 19 0 10.493-8.507 19-19 19m0-4c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15"/><path fill="#31af64" d="m17.07 21.02l-2.829-2.829c-.786-.786-2.047-.781-2.828 0-.786.786-.781 2.047 0 2.828l4.243 4.243c.392.392.902.587 1.412.588.512.002 1.021-.193 1.41-.582l7.79-7.79c.777-.777.775-2.042-.006-2.823-.786-.786-2.045-.784-2.823-.006l-6.37 6.37"/></g><g fill="#e52c5a" transform="translate(102)"><path fill-rule="nonzero" d="m24 47.5c-12.979 0-23.5-10.521-23.5-23.5 0-12.979 10.521-23.5 23.5-23.5 12.979 0 23.5 10.521 23.5 23.5 0 12.979-10.521 23.5-23.5 23.5m0-5c10.217 0 18.5-8.283 18.5-18.5 0-10.217-8.283-18.5-18.5-18.5-10.217 0-18.5 8.283-18.5 18.5 0 10.217 8.283 18.5 18.5 18.5"/><path d="m28.24 24l2.833-2.833c1.167-1.167 1.167-3.067-.004-4.239-1.169-1.169-3.069-1.173-4.239-.004l-2.833 2.833-2.833-2.833c-1.167-1.167-3.067-1.167-4.239.004-1.169 1.169-1.173 3.069-.004 4.239l2.833 2.833-2.833 2.833c-1.167 1.167-1.167 3.067.004 4.239 1.169 1.169 3.069 1.173 4.239.004l2.833-2.833 2.833 2.833c1.167 1.167 3.067 1.167 4.239-.004 1.169-1.169 1.173-3.069.004-4.239l-2.833-2.833"/></g><path fill="#e5e5e5" fill-rule="nonzero" d="m236 37c-7.732 0-14-6.268-14-14 0-7.732 6.268-14 14-14 7.732 0 14 6.268 14 14 0 7.732-6.268 14-14 14m0-4c5.523 0 10-4.477 10-10 0-5.523-4.477-10-10-10-5.523 0-10 4.477-10 10 0 5.523 4.477 10 10 10"/></g><g transform="translate(69 3)"><path fill="#e5e5e5" fill-rule="nonzero" d="m4 11.99v60.02c0 4.413 3.583 7.99 8 7.99h89.991c4.419 0 8-3.579 8-7.99v-60.02c0-4.413-3.583-7.99-8-7.99h-89.991c-4.419 0-8 3.579-8 7.99m-4 0c0-6.622 5.378-11.99 12-11.99h89.991c6.629 0 12 5.367 12 11.99v60.02c0 6.622-5.378 11.99-12 11.99h-89.991c-6.629 0-12-5.367-12-11.99v-60.02m52.874 80.3l-13.253-15.292h34.76l-13.253 15.292c-2.237 2.582-6.01 2.585-8.253 0m3.02-2.62c.644.743 1.564.743 2.207 0l7.516-8.673h-17.24l7.516 8.673"/><rect width="18" height="6" x="15" y="23" fill="#fc8a51" rx="3"/><rect width="18" height="6" x="39" y="39" fill="#e52c5a" rx="3"/><rect width="18" height="6" x="33" y="55" fill="#e5e5e5" rx="3"/><rect width="12" height="6" x="39" y="23" fill="#fde5d8" rx="3"/><rect width="12" height="6" x="57" y="55" fill="#e52c5a" rx="3"/><rect width="12" height="6" x="15" y="55" fill="#b5a7dd" rx="3"/><rect width="18" height="6" x="81" y="23" fill="#fc8a51" rx="3"/><rect width="18" height="6" x="15" y="39" fill="#fde5d8" rx="3"/><rect width="6" height="6" x="57" y="23" fill="#e52c5a" rx="3"/><g fill="#fde5d8"><rect width="6" height="6" x="69" y="23" rx="3"/><rect width="6" height="6" x="75" y="39" rx="3"/></g><rect width="6" height="6" x="63" y="39" fill="#e52c5a" rx="3"/></g><g transform="matrix(.70711-.70711.70711.70711 84.34 52.5)"><path fill="#6b4fbb" fill-rule="nonzero" d="m28.02 67.48c-15.927-2.825-28.02-16.738-28.02-33.476 0-18.778 15.222-34 34-34 18.778 0 34 15.222 34 34 0 16.738-12.1 30.652-28.02 33.476.015.173.023.347.023.524v21.999c0 3.314-2.693 6-6 6-3.314 0-6-2.682-6-6v-21.999c0-.177.008-.351.023-.524m5.977-7.476c14.359 0 26-11.641 26-26 0-14.359-11.641-26-26-26-14.359 0-26 11.641-26 26 0 14.359 11.641 26 26 26"/><path fill="#fff" fill-opacity=".3" stroke="#6b4fbb" stroke-width="8" d="m31 71c16.569 0 30-13.431 30-30 0-16.569-13.431-30-30-30" transform="matrix(.86603.5-.5.86603 26.663-17.507)"/></g></g></svg> \ No newline at end of file
diff --git a/app/views/shared/empty_states/icons/_pipelines_failed.svg b/app/views/shared/empty_states/icons/_pipelines_failed.svg
new file mode 100644
index 00000000000..7dbabf7e4ef
--- /dev/null
+++ b/app/views/shared/empty_states/icons/_pipelines_failed.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 446 249" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="0" d="m260.03 114h23.972v-.013c19.972-.53 36-16.887 36-36.987 0-20.435-16.565-37-37-37-.993 0-1.977.039-2.95.116-4.95-14.605-18.773-25.12-35.05-25.12-5.464 0-10.652 1.185-15.32 3.311-6.649-9.841-17.909-16.311-30.68-16.311-20.435 0-37 16.565-37 37 0 .701.019 1.397.058 2.088-16.11 3.999-28.06 18.561-28.06 35.912 0 20.435 16.565 37 37 37 .324 0 .646-.004.968-.012"/><ellipse id="2" cx="41" cy="41" rx="41" ry="41"/><mask id="1" width="186" height="112" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask><mask id="3" width="82" height="82" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask></defs><g fill="none" fill-rule="evenodd"><g transform="matrix(.86603.5-.5.86603 228.11 137.43)"><path stroke="#b5a7dd" stroke-width="4" d="m.445.161c15.89 10.636 34.998 16.839 55.55 16.839"/><g transform="translate(56 4)"><path fill="#fb722e" d="m16 8c0-1.105.902-2 2.01-2h7.983c1.109 0 2.01.888 2.01 2 0 1.105-.902 2-2.01 2h-7.983c-1.109 0-2.01-.888-2.01-2m0 10c0-1.105.902-2 2.01-2h7.983c1.109 0 2.01.888 2.01 2 0 1.105-.902 2-2.01 2h-7.983c-1.109 0-2.01-.888-2.01-2"/><path fill="#fde5d8" fill-rule="nonzero" d="m4 22h6c3.315 0 6-2.685 6-5.997v-6.01c0-3.315-2.684-5.997-6-5.997h-6v18m-4-18.992c0-1.661 1.343-3.01 2.994-3.01h7.01c5.523 0 10 4.47 10 9.997v6.01c0 5.521-4.476 9.997-10 9.997h-7.01c-1.654 0-2.994-1.343-2.994-3.01v-19.984"/></g></g><g fill-rule="nonzero" transform="translate(257)"><path fill="#e5e5e5" d="m3.597 18.747c5.611-9.09 15.519-14.747 26.403-14.747 17.12 0 31 13.879 31 31 0 7.02-2.34 13.685-6.58 19.1l3.149 2.466c4.786-6.111 7.431-13.639 7.431-21.565 0-19.33-15.67-35-35-35-12.286 0-23.476 6.384-29.808 16.647l3.404 2.1"/><g transform="matrix(.96593.25882-.25882.96593 15.98 9.578)"><path fill="#b5a7dd" d="m12.426 11.592l-2.142 1.768-3.664-2.116c-.186-.107-.43-.042-.543.154l-1.229 2.129c-.116.2-.052.438.138.547l3.658 2.112-.455 2.735c-.109.657-.165 1.327-.165 2.01 0 .678.055 1.348.165 2.01l.455 2.735-3.658 2.112c-.186.107-.251.351-.138.547l1.229 2.129c.116.2.353.264.543.154l3.664-2.116 2.142 1.768c1.036.855 2.205 1.533 3.462 2l2.6.972v4.225c0 .215.179.393.405.393h2.458c.231 0 .405-.174.405-.393v-4.225l2.6-.972c1.257-.47 2.426-1.147 3.462-2l2.142-1.768 3.664 2.116c.186.107.43.042.543-.154l1.229-2.129c.116-.2.052-.438-.138-.547l-3.658-2.112.455-2.735c.109-.657.165-1.327.165-2.01 0-.678-.055-1.348-.165-2.01l-.455-2.735 3.658-2.112c.186-.107.251-.351.138-.547l-1.229-2.129c-.116-.2-.353-.264-.543-.154l-3.664 2.116-2.142-1.768c-1.036-.855-2.205-1.533-3.462-2l-2.6-.972v-4.225c0-.215-.179-.393-.405-.393h-2.458c-.231 0-.405.174-.405.393v4.225l-2.6.972c-1.257.47-2.426 1.147-3.462 2m2.062-5.749v-1.45c0-2.426 1.963-4.393 4.405-4.393h2.458c2.433 0 4.405 1.967 4.405 4.393v1.45c1.689.631 3.243 1.538 4.608 2.665l1.259-.727c2.101-1.213 4.786-.497 6.01 1.618l1.229 2.129c1.216 2.107.499 4.798-1.602 6.01l-1.257.726c.144.866.219 1.755.219 2.662 0 .907-.075 1.796-.219 2.662l1.257.726c2.101 1.213 2.823 3.896 1.602 6.01l-1.229 2.129c-1.216 2.107-3.906 2.832-6.01 1.618l-1.259-.727c-1.365 1.127-2.92 2.034-4.608 2.665v1.45c0 2.426-1.963 4.393-4.405 4.393h-2.458c-2.433 0-4.405-1.967-4.405-4.393v-1.45c-1.689-.631-3.243-1.538-4.608-2.665l-1.259.727c-2.101 1.213-4.786.497-6.01-1.618l-1.229-2.129c-1.216-2.107-.499-4.798 1.602-6.01l1.257-.726c-.144-.866-.219-1.755-.219-2.662 0-.907.075-1.796.219-2.662l-1.257-.726c-2.101-1.213-2.823-3.896-1.602-6.01l1.229-2.129c1.216-2.107 3.906-2.832 6.01-1.618l1.259.727c1.365-1.127 2.92-2.034 4.608-2.665"/><path fill="#6b4fbb" d="m20.12 23.366c1.347 0 2.439-1.092 2.439-2.439 0-1.347-1.092-2.439-2.439-2.439-1.347 0-2.439 1.092-2.439 2.439 0 1.347 1.092 2.439 2.439 2.439m0 4c-3.556 0-6.439-2.883-6.439-6.439 0-3.556 2.883-6.439 6.439-6.439 3.556 0 6.439 2.883 6.439 6.439 0 3.556-2.883 6.439-6.439 6.439"/></g></g><use fill="#fff" stroke="#e5e5e5" stroke-width="8" mask="url(#1)" stroke-linejoin="round" xlink:href="#0"/><g transform="translate(175 58)"><use fill="#fff" stroke="#e5e5e5" stroke-width="8" mask="url(#3)" xlink:href="#2"/><g fill-rule="nonzero"><path fill="#e5e5e5" d="m41 78c20.435 0 37-16.565 37-37 0-20.435-16.565-37-37-37-20.435 0-37 16.565-37 37 0 20.435 16.565 37 37 37m0 4c-22.644 0-41-18.356-41-41 0-22.644 18.356-41 41-41 22.644 0 41 18.356 41 41 0 22.644-18.356 41-41 41"/><g transform="matrix(.96593.25882-.25882.96593 23.581 9.415)"><path fill="#b5a7dd" d="m14.821 13.655l-2.142 1.768-3.933-2.271c-.72-.416-1.634-.171-2.046.543l-1.507 2.61c-.409.708-.161 1.631.553 2.043l3.926 2.267-.455 2.735c-.145.869-.218 1.754-.218 2.65 0 .896.073 1.782.218 2.65l.455 2.735-3.926 2.267c-.72.416-.965 1.329-.553 2.043l1.507 2.61c.409.708 1.332.955 2.046.543l3.933-2.271 2.142 1.768c1.369 1.131 2.916 2.027 4.579 2.648l2.6.972v4.534c0 .831.669 1.5 1.493 1.5h3.01c.817 0 1.493-.676 1.493-1.5v-4.534l2.6-.972c1.663-.621 3.21-1.518 4.579-2.648l2.142-1.768 3.933 2.271c.72.416 1.634.171 2.046-.543l1.507-2.61c.409-.708.161-1.631-.553-2.043l-3.926-2.267.455-2.735c.145-.869.218-1.754.218-2.65 0-.896-.073-1.782-.218-2.65l-.455-2.735 3.926-2.267c.72-.416.965-1.329.553-2.043l-1.507-2.61c-.409-.708-1.332-.955-2.046-.543l-3.933 2.271-2.142-1.768c-1.369-1.131-2.916-2.027-4.579-2.648l-2.6-.972v-4.534c0-.831-.669-1.5-1.493-1.5h-3.01c-.817 0-1.493.676-1.493 1.5v4.534l-2.6.972c-1.663.621-3.21 1.518-4.579 2.648m3.179-6.395v-1.759c0-3.038 2.471-5.5 5.493-5.5h3.01c3.034 0 5.493 2.46 5.493 5.5v1.759c2.098.784 4.03 1.91 5.725 3.311l1.528-.882c2.631-1.519 5.999-.61 7.51 2.01l1.507 2.61c1.517 2.627.616 5.987-2.02 7.507l-1.525.881c.179 1.076.272 2.18.272 3.307 0 1.127-.093 2.231-.272 3.307l1.525.881c2.631 1.519 3.528 4.89 2.02 7.507l-1.507 2.61c-1.517 2.627-4.877 3.527-7.51 2.01l-1.528-.882c-1.696 1.401-3.627 2.527-5.725 3.311v1.759c0 3.038-2.471 5.5-5.493 5.5h-3.01c-3.034 0-5.493-2.46-5.493-5.5v-1.759c-2.098-.784-4.03-1.91-5.725-3.311l-1.528.882c-2.631 1.519-5.999.61-7.51-2.01l-1.507-2.61c-1.517-2.627-.616-5.987 2.02-7.507l1.525-.881c-.179-1.076-.272-2.18-.272-3.307 0-1.127.093-2.231.272-3.307l-1.525-.881c-2.631-1.519-3.528-4.89-2.02-7.507l1.507-2.61c1.517-2.627 4.877-3.527 7.51-2.01l1.528.882c1.696-1.401 3.627-2.527 5.725-3.311"/><path fill="#6b4fbb" d="m25 30c2.209 0 4-1.791 4-4 0-2.209-1.791-4-4-4-2.209 0-4 1.791-4 4 0 2.209 1.791 4 4 4m0 4c-4.418 0-8-3.582-8-8 0-4.418 3.582-8 8-8 4.418 0 8 3.582 8 8 0 4.418-3.582 8-8 8"/></g></g></g><g transform="translate(140 161)"><path fill="#e5e5e5" fill-rule="nonzero" d="m4 8.541v30.01c0 2.202 1.793 3.995 4 3.995h20c2.209 0 4-1.789 4-3.995v-30.01c0-2.202-1.793-3.995-4-3.995h-20c-2.209 0-4 1.789-4 3.995m-4 0c0-4.416 3.583-7.995 8-7.995h20c4.416 0 8 3.584 8 7.995v30.01c0 4.416-3.583 7.995-8 7.995h-20c-4.416 0-8-3.584-8-7.995v-30.01"/><g fill="#fb722e"><rect width="4" height="11" x="10" y="18.545" rx="2"/><rect width="4" height="11" x="21" y="18.545" rx="2"/></g></g><path fill="#e5e5e5" fill-rule="nonzero" d="m445.16 245.34c-16.874-11.778-110.62-20.336-222.14-20.336-111.61 0-205.4 8.571-222.18 20.364-.904.635-1.121 1.883-.486 2.786.635.904 1.883 1.121 2.786.486 15.756-11.07 109.46-19.636 219.88-19.636 110.34 0 203.99 8.55 219.85 19.617.906.632 2.153.41 2.785-.495.632-.906.41-2.153-.495-2.785"/></g></svg> \ No newline at end of file
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index a95020a9be8..09f946f1d88 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -17,7 +17,7 @@
.stats
%span
= icon('bookmark')
- = number_with_delimiter(group.projects.count)
+ = number_with_delimiter(group.projects.non_archived.count)
%span
= icon('users')
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 25a4aec0a38..884bd3ca9ca 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -3,7 +3,7 @@
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('issuable')
-%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
+%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => "102", "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.block.issuable-sidebar-header
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index c57282c5742..c0699b13719 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -10,7 +10,7 @@
.js-projects-list-holder
- if projects.any?
- %ul.projects-list.content-list
+ %ul.projects-list
- projects.each_with_index do |project, i|
- css_class = (i >= projects_limit) ? 'hide' : nil
= render "shared/projects/project", project: project, skip_namespace: skip_namespace,
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index df21857e1ad..059aeebaf34 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -10,44 +10,44 @@
%li.project-row{ class: css_class }
= cache(cache_key) do
+ - if avatar
+ .avatar-container.s40
+ - if use_creator_avatar
+ = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
+ - else
+ = project_icon(project, alt: '', class: 'avatar project-avatar s40')
+ .project-details
+ %h3.prepend-top-0.append-bottom-0
+ = link_to project_path(project), class: dom_class(project) do
+ %span.project-full-name
+ %span.namespace-name
+ - if project.namespace && !skip_namespace
+ = project.namespace.human_name
+ \/
+ %span.project-name.filter-title
+ = project.name
+
+ - if show_last_commit_as_description
+ .description.prepend-top-5
+ = link_to_gfm project.commit.title, namespace_project_commit_path(project.namespace, project, project.commit),
+ class: "commit-row-message"
+ - elsif project.description.present?
+ .description.prepend-top-5
+ = markdown_field(project, :description)
+
.controls
- if project.archived
- %span.label.label-warning archived
+ %span.prepend-left-10.label.label-warning archived
- if project.pipeline_status.has_status?
- %span
+ %span.prepend-left-10
= render_project_pipeline_status(project.pipeline_status)
- if forks
- %span
+ %span.prepend-left-10
= icon('code-fork')
= number_with_delimiter(project.forks_count)
- if stars
- %span
+ %span.prepend-left-10
= icon('star')
= number_with_delimiter(project.star_count)
- %span.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project) }
+ %span.prepend-left-10.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project) }
= visibility_level_icon(project.visibility_level, fw: true)
-
- .title
- = link_to project_path(project), class: dom_class(project) do
- - if avatar
- .dash-project-avatar
- .avatar-container.s40
- - if use_creator_avatar
- = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
- - else
- = project_icon(project, alt: '', class: 'avatar project-avatar s40')
- %span.project-full-name
- %span.namespace-name
- - if project.namespace && !skip_namespace
- = project.namespace.human_name
- \/
- %span.project-name.filter-title
- = project.name
-
- - if show_last_commit_as_description
- .description
- = link_to_gfm project.commit.title, namespace_project_commit_path(project.namespace, project, project.commit),
- class: "commit-row-message"
- - elsif project.description.present?
- .description
- = markdown_field(project, :description)
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index dc9a3b0d0df..601187455b3 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -13,7 +13,7 @@
.cover-block.user-cover-block
.cover-controls
- if @user == current_user
- = link_to profile_path, class: 'btn btn-gray' do
+ = link_to profile_path, class: 'btn btn-gray has-tooltip', title: 'Edit profile', 'aria-label': 'Edit profile' do
= icon('pencil')
- elsif current_user
- if @user.abuse_report
@@ -24,7 +24,7 @@
= link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray',
title: 'Report abuse', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('exclamation-circle')
- = link_to user_path(@user, rss_url_options), class: 'btn btn-gray' do
+ = link_to user_path(@user, rss_url_options), class: 'btn btn-gray has-tooltip', title: 'Subscribe', 'aria-label': 'Subscribe' do
= icon('rss')
- if current_user && current_user.admin?
= link_to [:admin, @user], class: 'btn btn-gray', title: 'View user in admin area',
@@ -44,7 +44,7 @@
%span.middle-dot-divider
@#{@user.username}
%span.middle-dot-divider
- Member since #{@user.created_at.to_s(:medium)}
+ Member since #{@user.created_at.to_date.to_s(:long)}
.cover-desc
- unless @user.public_email.blank?
@@ -97,7 +97,8 @@
Snippets
%div{ class: container_class }
- .user-callout{ 'callout-svg' => custom_icon('icon_customization') }
+ - if @user == current_user
+ .user-callout{ 'callout-svg' => custom_icon('icon_customization') }
.tab-content
#activity.tab-pane
.row-content-block.calender-block.white.second-block.hidden-xs
diff --git a/changelogs/unreleased/12818-expose-simple-cicd-status-endpoints-with-status-serializer-gitlab-ci-status-for-pipeline-job-and-merge-request.yml b/changelogs/unreleased/12818-expose-simple-cicd-status-endpoints-with-status-serializer-gitlab-ci-status-for-pipeline-job-and-merge-request.yml
new file mode 100644
index 00000000000..953009213df
--- /dev/null
+++ b/changelogs/unreleased/12818-expose-simple-cicd-status-endpoints-with-status-serializer-gitlab-ci-status-for-pipeline-job-and-merge-request.yml
@@ -0,0 +1,5 @@
+---
+title: Expose CI/CD status API endpoints with Gitlab::Ci::Status facility on pipeline,
+ job and merge request for favicon
+merge_request: 9561
+author: dosuken123
diff --git a/changelogs/unreleased/22850-404-when-requesting-build-trace.yml b/changelogs/unreleased/22850-404-when-requesting-build-trace.yml
new file mode 100644
index 00000000000..6b442130d9b
--- /dev/null
+++ b/changelogs/unreleased/22850-404-when-requesting-build-trace.yml
@@ -0,0 +1,4 @@
+---
+title: Resolve "404 when requesting build trace"
+merge_request: 9759
+author: dosuken123
diff --git a/changelogs/unreleased/23862-fix-group-project-count.yml b/changelogs/unreleased/23862-fix-group-project-count.yml
new file mode 100644
index 00000000000..7b2e9f9bfa6
--- /dev/null
+++ b/changelogs/unreleased/23862-fix-group-project-count.yml
@@ -0,0 +1,4 @@
+---
+title: Adding non_archived scope for counting projects
+merge_request: 8305
+author: Naveen Kumar
diff --git a/changelogs/unreleased/25188-polyfill-es-symbol.yml b/changelogs/unreleased/25188-polyfill-es-symbol.yml
new file mode 100644
index 00000000000..d0cf36b9ec6
--- /dev/null
+++ b/changelogs/unreleased/25188-polyfill-es-symbol.yml
@@ -0,0 +1,4 @@
+---
+title: Add ECMAScript polyfills for Symbol and Array.find
+merge_request: 10120
+author:
diff --git a/changelogs/unreleased/27574-pipelines-empty-state.yml b/changelogs/unreleased/27574-pipelines-empty-state.yml
new file mode 100644
index 00000000000..c26ea97205f
--- /dev/null
+++ b/changelogs/unreleased/27574-pipelines-empty-state.yml
@@ -0,0 +1,4 @@
+---
+title: Adds empty and error state to pipelines
+merge_request:
+author:
diff --git a/changelogs/unreleased/27878-new-service-for-creating-user.yml b/changelogs/unreleased/27878-new-service-for-creating-user.yml
new file mode 100644
index 00000000000..c07f0cef8db
--- /dev/null
+++ b/changelogs/unreleased/27878-new-service-for-creating-user.yml
@@ -0,0 +1,4 @@
+---
+title: Implement user create service
+merge_request: 9220
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/27910-admin-can-create-project-in-all-groups.yml b/changelogs/unreleased/27910-admin-can-create-project-in-all-groups.yml
new file mode 100644
index 00000000000..40fd8dacc82
--- /dev/null
+++ b/changelogs/unreleased/27910-admin-can-create-project-in-all-groups.yml
@@ -0,0 +1,4 @@
+---
+title: Allow admin to view all namespaces
+merge_request:
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/28614-harmonious-color-palette.yml b/changelogs/unreleased/28614-harmonious-color-palette.yml
new file mode 100644
index 00000000000..b436e7129a4
--- /dev/null
+++ b/changelogs/unreleased/28614-harmonious-color-palette.yml
@@ -0,0 +1,4 @@
+---
+title: Update color palette to a more harmonious and consistent one.
+merge_request: 10154
+author:
diff --git a/changelogs/unreleased/28634-todos-margin.yml b/changelogs/unreleased/28634-todos-margin.yml
new file mode 100644
index 00000000000..f4221ce4350
--- /dev/null
+++ b/changelogs/unreleased/28634-todos-margin.yml
@@ -0,0 +1,4 @@
+---
+title: Remove extra margin at bottom of todos page
+merge_request:
+author:
diff --git a/changelogs/unreleased/29116-maxint-error.yml b/changelogs/unreleased/29116-maxint-error.yml
new file mode 100644
index 00000000000..06e976617d5
--- /dev/null
+++ b/changelogs/unreleased/29116-maxint-error.yml
@@ -0,0 +1,4 @@
+---
+title: Fix projects_limit RangeError on user create
+merge_request:
+author: Alexander Randa
diff --git a/changelogs/unreleased/29897-remove-force-scroll-for-mr-changes-diff.yml b/changelogs/unreleased/29897-remove-force-scroll-for-mr-changes-diff.yml
new file mode 100644
index 00000000000..d1da96096f8
--- /dev/null
+++ b/changelogs/unreleased/29897-remove-force-scroll-for-mr-changes-diff.yml
@@ -0,0 +1,4 @@
+---
+title: Remove forced scroll into view when switching to Changes MR tab
+merge_request:
+author:
diff --git a/changelogs/unreleased/29930-fix-profile-cover-button-a11y.yml b/changelogs/unreleased/29930-fix-profile-cover-button-a11y.yml
new file mode 100644
index 00000000000..754d471c7d7
--- /dev/null
+++ b/changelogs/unreleased/29930-fix-profile-cover-button-a11y.yml
@@ -0,0 +1,4 @@
+---
+title: Add tooltip and accessibility for profile cover buttons
+merge_request: 10182
+author:
diff --git a/changelogs/unreleased/30035-milestone-with-due-date-shows-escaped-html.yml b/changelogs/unreleased/30035-milestone-with-due-date-shows-escaped-html.yml
new file mode 100644
index 00000000000..651c299ac66
--- /dev/null
+++ b/changelogs/unreleased/30035-milestone-with-due-date-shows-escaped-html.yml
@@ -0,0 +1,4 @@
+---
+title: Fix escaped html appearing in milestone page
+merge_request: 10224
+author:
diff --git a/changelogs/unreleased/4195-add-sorting-to-project-milestones.yml b/changelogs/unreleased/4195-add-sorting-to-project-milestones.yml
new file mode 100644
index 00000000000..d4104dfa772
--- /dev/null
+++ b/changelogs/unreleased/4195-add-sorting-to-project-milestones.yml
@@ -0,0 +1,4 @@
+---
+title: Add dropdown sort to project milestones
+merge_request:
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/add-issue-modal-loading-indicator.yml b/changelogs/unreleased/add-issue-modal-loading-indicator.yml
new file mode 100644
index 00000000000..5398399c018
--- /dev/null
+++ b/changelogs/unreleased/add-issue-modal-loading-indicator.yml
@@ -0,0 +1,4 @@
+---
+title: Shows loading icon in issue boards modal when changing filters
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-ci-api-regression-for-after-script.yml b/changelogs/unreleased/fix-ci-api-regression-for-after-script.yml
new file mode 100644
index 00000000000..cdd7d1e6945
--- /dev/null
+++ b/changelogs/unreleased/fix-ci-api-regression-for-after-script.yml
@@ -0,0 +1,4 @@
+---
+title: Fix after_script processing for Runners APIv4
+merge_request: 10185
+author:
diff --git a/changelogs/unreleased/fix-gb-pipeline-retry-only-latest-jobs.yml b/changelogs/unreleased/fix-gb-pipeline-retry-only-latest-jobs.yml
new file mode 100644
index 00000000000..c14679be70f
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-pipeline-retry-only-latest-jobs.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bug that caused jobs that already had been retried to be retried again
+ when retrying a pipeline
+merge_request: 10249
+author:
diff --git a/changelogs/unreleased/fix-issue-23237.yml b/changelogs/unreleased/fix-issue-23237.yml
new file mode 100644
index 00000000000..ed0ffc0684d
--- /dev/null
+++ b/changelogs/unreleased/fix-issue-23237.yml
@@ -0,0 +1,4 @@
+---
+title: "Fixes an issue in the new merge request form, where a tag would be selected instead of a branch when they have the same names"
+merge_request: 9535
+author: Weiqing Chu
diff --git a/changelogs/unreleased/labels-finder-optimize-project.yml b/changelogs/unreleased/labels-finder-optimize-project.yml
new file mode 100644
index 00000000000..0d2e82e1efe
--- /dev/null
+++ b/changelogs/unreleased/labels-finder-optimize-project.yml
@@ -0,0 +1,4 @@
+---
+title: Optimize labels finder query when searching for a project with a group
+merge_request:
+author: mhasbini
diff --git a/changelogs/unreleased/projects-list-line-breaks.yml b/changelogs/unreleased/projects-list-line-breaks.yml
new file mode 100644
index 00000000000..179d7081293
--- /dev/null
+++ b/changelogs/unreleased/projects-list-line-breaks.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed projects list lines breaking
+merge_request:
+author:
diff --git a/changelogs/unreleased/sh-remove-tags-from-explore.yml b/changelogs/unreleased/sh-remove-tags-from-explore.yml
new file mode 100644
index 00000000000..b76ec89a006
--- /dev/null
+++ b/changelogs/unreleased/sh-remove-tags-from-explore.yml
@@ -0,0 +1,4 @@
+---
+title: Remove Tags filter from Projects Explore dropdown
+merge_request:
+author:
diff --git a/changelogs/unreleased/slow-search-changelog.yml b/changelogs/unreleased/slow-search-changelog.yml
new file mode 100644
index 00000000000..d50cf1f94cd
--- /dev/null
+++ b/changelogs/unreleased/slow-search-changelog.yml
@@ -0,0 +1,4 @@
+---
+title: Simplify search queries for projects and merge requests
+merge_request: 10053
+author: mhasbini
diff --git a/changelogs/unreleased/tc-pipeline-show-trigger-date.yml b/changelogs/unreleased/tc-pipeline-show-trigger-date.yml
new file mode 100644
index 00000000000..4de784d98f3
--- /dev/null
+++ b/changelogs/unreleased/tc-pipeline-show-trigger-date.yml
@@ -0,0 +1,4 @@
+---
+title: Show correct user & creation time in heading of the pipeline page
+merge_request: 9936
+author:
diff --git a/changelogs/unreleased/user-callout-showing-on-all-profiles.yml b/changelogs/unreleased/user-callout-showing-on-all-profiles.yml
new file mode 100644
index 00000000000..b8eb5a149b7
--- /dev/null
+++ b/changelogs/unreleased/user-callout-showing-on-all-profiles.yml
@@ -0,0 +1,4 @@
+---
+title: User callout only shows on current users profile
+merge_request:
+author:
diff --git a/changelogs/unreleased/user-profile-join-date.yml b/changelogs/unreleased/user-profile-join-date.yml
new file mode 100644
index 00000000000..f9d78b0dc3e
--- /dev/null
+++ b/changelogs/unreleased/user-profile-join-date.yml
@@ -0,0 +1,4 @@
+---
+title: Removed the hours & minutes from the users start date on their profile
+merge_request:
+author:
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 44b8ae7aedd..823e0614aeb 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -102,6 +102,7 @@ constraints(ProjectUrlConstrainer.new) do
get :merge_widget_refresh
post :cancel_merge_when_pipeline_succeeds
get :ci_status
+ get :pipeline_status
get :ci_environments_status
post :toggle_subscription
post :remove_wip
@@ -152,6 +153,7 @@ constraints(ProjectUrlConstrainer.new) do
post :cancel
post :retry
get :builds
+ get :status
end
end
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 3cf94b9b435..0859c8416c8 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -21,7 +21,7 @@ var config = {
common_vue: ['vue', './vue_shared/common_vue.js'],
common_d3: ['d3'],
main: './main.js',
- blob_edit: './blob_edit/blob_edit_bundle.js',
+ blob: './blob_edit/blob_bundle.js',
boards: './boards/boards_bundle.js',
simulate_drag: './test_utils/simulate_drag.js',
cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js',
diff --git a/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb b/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb
index 1e2abea5254..69dd15b8b4e 100644
--- a/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb
+++ b/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb
@@ -17,7 +17,7 @@ class RemoveUnusedCiTablesAndColumns < ActiveRecord::Migration
end
remove_column :ci_pipelines, :push_data, :text
- remove_column :ci_builds, :job_id, :integer
+ remove_column :ci_builds, :job_id, :integer if column_exists?(:ci_builds, :job_id)
remove_column :ci_builds, :deploy, :boolean
end
diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md
index 0a08591c3ce..cf3aca106e9 100644
--- a/doc/administration/high_availability/database.md
+++ b/doc/administration/high_availability/database.md
@@ -13,6 +13,8 @@ Database Service (RDS) that runs PostgreSQL.
If you use a cloud-managed service, or provide your own PostgreSQL:
+1. Setup PostgreSQL according to the
+ [database requirements document](doc/install/requirements.md#database).
1. Set up a `gitlab` username with a password of your choice. The `gitlab` user
needs privileges to create the `gitlabhq_production` database.
1. Configure the GitLab application servers with the appropriate details.
diff --git a/doc/api/README.md b/doc/api/README.md
index 58d090b8f5e..e627b6f2ee8 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -74,6 +74,12 @@ returned with status code `401`:
}
```
+### Session Cookie
+
+When signing in to GitLab as an ordinary user, a `_gitlab_session` cookie is
+set. The API will use this cookie for authentication if it is present, but using
+the API to generate a new session cookie is currently not supported.
+
### Private Tokens
You need to pass a `private_token` parameter via query string or header. If passed as a
@@ -113,65 +119,25 @@ moment – `read_user` and `api` – the groundwork has been laid to add more sc
At any time you can revoke any personal access token by just clicking **Revoke**.
-### Session Cookie
+### Impersonation tokens
-When signing in to GitLab as an ordinary user, a `_gitlab_session` cookie is
-set. The API will use this cookie for authentication if it is present, but using
-the API to generate a new session cookie is currently not supported.
+> [Introduced][ce-9099] in GitLab 9.0. Needs admin permissions.
-## Basic Usage
+Impersonation tokens are a type of [Personal Access Token](#personal-access-tokens)
+that can only be created by an admin for a specific user.
-API requests should be prefixed with `api` and the API version. The API version
-is defined in [`lib/api.rb`][lib-api-url].
+They are a better alternative to using the user's password/private token
+or using the [Sudo](#sudo) feature which also requires the admin's password
+or private token, since the password/token can change over time. Impersonation
+tokens are a great fit if you want to build applications or tools which
+authenticate with the API as a specific user.
-Example of a valid API request:
+For more information about the usage please refer to the
+[users API](users.md#retrieve-user-impersonation-tokens) docs.
-```shell
-GET https://gitlab.example.com/api/v4/projects?private_token=9koXpg98eAheJpvBs5tK
-```
-
-Example of a valid API request using cURL and authentication via header:
-
-```shell
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects"
-```
-
-The API uses JSON to serialize data. You don't need to specify `.json` at the
-end of an API URL.
-
-## Status codes
-
-The API is designed to return different status codes according to context and
-action. This way, if a request results in an error, the caller is able to get
-insight into what went wrong.
-
-The following table gives an overview of how the API functions generally behave.
-
-| Request type | Description |
-| ------------ | ----------- |
-| `GET` | Access one or more resources and return the result as JSON. |
-| `POST` | Return `201 Created` if the resource is successfully created and return the newly created resource as JSON. |
-| `GET` / `PUT` / `DELETE` | Return `200 OK` if the resource is accessed, modified or deleted successfully. The (modified) result is returned as JSON. |
-| `DELETE` | Designed to be idempotent, meaning a request to a resource still returns `200 OK` even it was deleted before or is not available. The reasoning behind this, is that the user is not really interested if the resource existed before or not. |
-
-The following table shows the possible return codes for API requests.
-
-| Return values | Description |
-| ------------- | ----------- |
-| `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON. |
-| `204 No Content` | The server has successfully fulfilled the request and that there is no additional content to send in the response payload body. |
-| `201 Created` | The `POST` request was successful and the resource is returned as JSON. |
-| `304 Not Modified` | Indicates that the resource has not been modified since the last request. |
-| `400 Bad Request` | A required attribute of the API request is missing, e.g., the title of an issue is not given. |
-| `401 Unauthorized` | The user is not authenticated, a valid [user token](#authentication) is necessary. |
-| `403 Forbidden` | The request is not allowed, e.g., the user is not allowed to delete a project. |
-| `404 Not Found` | A resource could not be accessed, e.g., an ID for a resource could not be found. |
-| `405 Method Not Allowed` | The request is not supported. |
-| `409 Conflict` | A conflicting resource already exists, e.g., creating a project with a name that already exists. |
-| `422 Unprocessable` | The entity could not be processed. |
-| `500 Server Error` | While handling the request something went wrong server-side. |
+### Sudo
-## Sudo
+> Needs admin permissions.
All API requests support performing an API call as if you were another user,
provided your private token is from an administrator account. You need to pass
@@ -202,7 +168,7 @@ returned with status code `404`:
Example of a valid API call and a request using cURL with sudo request,
providing a username:
-```shell
+```
GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=username
```
@@ -213,7 +179,7 @@ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: username" "h
Example of a valid API call and a request using cURL with sudo request,
providing an ID:
-```shell
+```
GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=23
```
@@ -221,13 +187,57 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=23
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: 23" "https://gitlab.example.com/api/v4/projects"
```
-## Impersonation Tokens
+## Basic Usage
+
+API requests should be prefixed with `api` and the API version. The API version
+is defined in [`lib/api.rb`][lib-api-url].
+
+Example of a valid API request:
+
+```
+GET /projects?private_token=9koXpg98eAheJpvBs5tK
+```
+
+Example of a valid API request using cURL and authentication via header:
+
+```shell
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects"
+```
+
+The API uses JSON to serialize data. You don't need to specify `.json` at the
+end of an API URL.
+
+## Status codes
+
+The API is designed to return different status codes according to context and
+action. This way, if a request results in an error, the caller is able to get
+insight into what went wrong.
+
+The following table gives an overview of how the API functions generally behave.
+
+| Request type | Description |
+| ------------ | ----------- |
+| `GET` | Access one or more resources and return the result as JSON. |
+| `POST` | Return `201 Created` if the resource is successfully created and return the newly created resource as JSON. |
+| `GET` / `PUT` / `DELETE` | Return `200 OK` if the resource is accessed, modified or deleted successfully. The (modified) result is returned as JSON. |
+| `DELETE` | Designed to be idempotent, meaning a request to a resource still returns `200 OK` even it was deleted before or is not available. The reasoning behind this, is that the user is not really interested if the resource existed before or not. |
-Impersonation Tokens are a type of Personal Access Token that can only be created by an admin for a specific user. These can be used by automated tools
-to authenticate with the API as a specific user, as a better alternative to using the user's password or private token directly, which may change over time,
-and to using the [Sudo](#sudo) feature, which requires the tool to know an admin's password or private token, which can change over time as well and are extremely powerful.
+The following table shows the possible return codes for API requests.
-For more information about the usage please refer to the [Users](users.md) page
+| Return values | Description |
+| ------------- | ----------- |
+| `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON. |
+| `204 No Content` | The server has successfully fulfilled the request and that there is no additional content to send in the response payload body. |
+| `201 Created` | The `POST` request was successful and the resource is returned as JSON. |
+| `304 Not Modified` | Indicates that the resource has not been modified since the last request. |
+| `400 Bad Request` | A required attribute of the API request is missing, e.g., the title of an issue is not given. |
+| `401 Unauthorized` | The user is not authenticated, a valid [user token](#authentication) is necessary. |
+| `403 Forbidden` | The request is not allowed, e.g., the user is not allowed to delete a project. |
+| `404 Not Found` | A resource could not be accessed, e.g., an ID for a resource could not be found. |
+| `405 Method Not Allowed` | The request is not supported. |
+| `409 Conflict` | A conflicting resource already exists, e.g., creating a project with a name that already exists. |
+| `422 Unprocessable` | The entity could not be processed. |
+| `500 Server Error` | While handling the request something went wrong server-side. |
## Pagination
@@ -307,14 +317,14 @@ For example, an issue might have `id: 46` and `iid: 5`.
That means that if you want to get an issue via the API you should use the `id`:
-```bash
+```
GET /projects/42/issues/:id
```
On the other hand, if you want to create a link to a web page you should use
the `iid`:
-```bash
+```
GET /projects/42/issues/:iid
```
@@ -398,3 +408,4 @@ programming languages. Visit the [GitLab website] for a complete list.
[lib-api-url]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/api/api.rb
[ce-3749]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3749
[ce-5951]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951
+[ce-9099]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9099
diff --git a/doc/api/users.md b/doc/api/users.md
index 14b5c6c713e..2ada4d09c84 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -828,10 +828,12 @@ Example response:
]
```
-## Retrieve user impersonation tokens
+## Get all impersonation tokens of a user
-It retrieves every impersonation token of the user. Note that only administrators can do this.
-This function takes pagination parameters `page` and `per_page` to restrict the list of impersonation tokens.
+> Requires admin permissions.
+
+It retrieves every impersonation token of the user. Use the pagination
+parameters `page` and `per_page` to restrict the list of impersonation tokens.
```
GET /users/:user_id/impersonation_tokens
@@ -842,27 +844,50 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `user_id` | integer | yes | The ID of the user |
-| `state` | string | no | filter tokens based on state (all, active, inactive) |
+| `state` | string | no | filter tokens based on state (`all`, `active`, `inactive`) |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/42/impersonation_tokens
+```
Example response:
+
```json
[
- {
- "id": 1,
- "name": "mytoken",
- "revoked": false,
- "expires_at": "2017-01-04",
- "scopes": ['api'],
- "active": true,
- "impersonation": true,
- "token": "9koXpg98eAheJpvBs5tK"
- }
+ {
+ "active" : true,
+ "token" : "EsMo-vhKfXGwX9RKrwiy",
+ "scopes" : [
+ "api"
+ ],
+ "revoked" : false,
+ "name" : "mytoken",
+ "id" : 2,
+ "created_at" : "2017-03-17T17:18:09.283Z",
+ "impersonation" : true,
+ "expires_at" : "2017-04-04"
+ },
+ {
+ "active" : false,
+ "scopes" : [
+ "read_user"
+ ],
+ "revoked" : true,
+ "token" : "ZcZRpLeEuQRprkRjYydY",
+ "name" : "mytoken2",
+ "created_at" : "2017-03-17T17:19:28.697Z",
+ "id" : 3,
+ "impersonation" : true,
+ "expires_at" : "2017-04-14"
+ }
]
```
-## Show a user's impersonation token
+## Get an impersonation token of a user
-It shows a user's impersonation token. Note that only administrators can do this.
+> Requires admin permissions.
+
+It shows a user's impersonation token.
```
GET /users/:user_id/impersonation_tokens/:impersonation_token_id
@@ -875,7 +900,31 @@ Parameters:
| `user_id` | integer | yes | The ID of the user |
| `impersonation_token_id` | integer | yes | The ID of the impersonation token |
-## Create a impersonation token
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/42/impersonation_tokens/2
+```
+
+Example response:
+
+```json
+{
+ "active" : true,
+ "token" : "EsMo-vhKfXGwX9RKrwiy",
+ "scopes" : [
+ "api"
+ ],
+ "revoked" : false,
+ "name" : "mytoken",
+ "id" : 2,
+ "created_at" : "2017-03-17T17:18:09.283Z",
+ "impersonation" : true,
+ "expires_at" : "2017-04-04"
+}
+```
+
+## Create an impersonation token
+
+> Requires admin permissions.
It creates a new impersonation token. Note that only administrators can do this.
You are only able to create impersonation tokens to impersonate the user and perform
@@ -891,32 +940,46 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `user_id` | integer | yes | The ID of the user |
-| `name` | string | yes | The name of the impersonation token |
-| `expires_at` | date | no | The expiration date of the impersonation token |
-| `scopes` | array | no | The array of scopes of the impersonation token (api, read_user) |
+| `name` | string | yes | The name of the impersonation token |
+| `expires_at` | date | no | The expiration date of the impersonation token in ISO format (`YYYY-MM-DD`)|
+| `scopes` | array | yes | The array of scopes of the impersonation token (`api`, `read_user`) |
+
+```
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "name=mytoken" --data "expires_at=2017-04-04" --data "scopes[]=api" https://gitlab.example.com/api/v4/users/42/impersonation_tokens
+```
Example response:
+
```json
{
- "id": 1,
- "name": "mytoken",
- "revoked": false,
- "expires_at": "2017-01-04",
- "scopes": ['api'],
- "active": true,
- "impersonation": true,
- "token": "9koXpg98eAheJpvBs5tK"
+ "id" : 2,
+ "revoked" : false,
+ "scopes" : [
+ "api"
+ ],
+ "token" : "EsMo-vhKfXGwX9RKrwiy",
+ "active" : true,
+ "impersonation" : true,
+ "name" : "mytoken",
+ "created_at" : "2017-03-17T17:18:09.283Z",
+ "expires_at" : "2017-04-04"
}
```
## Revoke an impersonation token
-It revokes an impersonation token. Note that only administrators can revoke impersonation tokens.
+> Requires admin permissions.
+
+It revokes an impersonation token.
```
DELETE /users/:user_id/impersonation_tokens/:impersonation_token_id
```
+```
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/42/impersonation_tokens/1
+```
+
Parameters:
| Attribute | Type | Required | Description |
diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md
index 7f4426ee85d..8e002fe0022 100644
--- a/doc/api/v3_to_v4.md
+++ b/doc/api/v3_to_v4.md
@@ -80,3 +80,4 @@ Below are the changes made between V3 and V4.
- `GET /projects/:id/repository/blobs/:sha` now returns JSON attributes for the blob identified by `:sha`, instead of finding the commit identified by `:sha` and returning the raw content of the blob in that commit identified by the required `?filepath=:filepath`
- Moved `GET /projects/:id/repository/commits/:sha/blob?file_path=:file_path` and `GET /projects/:id/repository/blobs/:sha?file_path=:file_path` to `GET /projects/:id/repository/files/:file_path/raw?ref=:sha`
- `GET /projects/:id/repository/tree` parameter `ref_name` has been renamed to `ref` for consistency
+- `confirm` parameter for `POST /users` has been deprecated in favor of `skip_confirmation` parameter
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index ccaee33dc92..e380282f910 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -4,6 +4,7 @@
- [Introduced][ci-229] in GitLab CE 7.14.
- GitLab 8.12 has a completely redesigned job permissions system. Read all
about the [new model and its implications](../../user/project/new_ci_build_permissions_model.md#job-triggers).
+- GitLab 9.0 introduced a trigger ownership to solve permission problems.
Triggers can be used to force a rebuild of a specific `ref` (branch or tag)
with an API call.
@@ -21,13 +22,30 @@ overview of the time the triggers were last used.
![Triggers page overview](img/triggers_page.png)
+## Take ownership
+
+Each created trigger when run will impersonate their associated user including
+their access to projects and their project permissions.
+
+You can take ownership of existing triggers by clicking *Take ownership*.
+From now on the trigger will be run as you.
+
+## Legacy triggers
+
+Old triggers, created before 9.0 will be marked as Legacy. Triggers with
+the legacy label do not have an associated user and only have access
+to the current project.
+
+Legacy trigger are considered deprecated and will be removed
+with one of the future versions of GitLab.
+
## Revoke a trigger
You can revoke a trigger any time by going at your project's
**Settings > Triggers** and hitting the **Revoke** button. The action is
irreversible.
-## Trigger a job
+## Trigger a pipeline
> **Note**:
Valid refs are only the branches and tags. If you pass a commit SHA as a ref,
@@ -63,7 +81,7 @@ below.
See the [Examples](#examples) section for more details on how to actually
trigger a rebuild.
-## Trigger a job from webhook
+## Trigger a pipeline from webhook
> Introduced in GitLab 8.14.
@@ -117,7 +135,7 @@ curl --request POST \
"https://gitlab.example.com/api/v4/projects/9/trigger/pipeline?token=TOKEN&ref=master"
```
-### Triggering a job within `.gitlab-ci.yml`
+### Triggering a pipeline within `.gitlab-ci.yml`
You can also benefit by using triggers in your `.gitlab-ci.yml`. Let's say that
you have two projects, A and B, and you want to trigger a rebuild on the `master`
diff --git a/doc/ci/triggers/img/triggers_page.png b/doc/ci/triggers/img/triggers_page.png
index 8ebf68d0384..eafd8519a23 100644
--- a/doc/ci/triggers/img/triggers_page.png
+++ b/doc/ci/triggers/img/triggers_page.png
Binary files differ
diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md
index b559d132590..55610a7b014 100644
--- a/doc/user/project/new_ci_build_permissions_model.md
+++ b/doc/user/project/new_ci_build_permissions_model.md
@@ -87,12 +87,12 @@ your Runners in the most possible secure way, by avoiding the following:
By using an insecure GitLab Runner configuration, you allow the rogue developers
to steal the tokens of other jobs.
-## job triggers
+## Pipeline triggers
-[job triggers][triggers] do not support the new permission model.
-They continue to use the old authentication mechanism where the CI job
-can access only its own sources. We plan to remove that limitation in one of
-the upcoming releases.
+Since 9.0 [pipelnie triggers][triggers] do support the new permission model.
+The new triggers do impersonate their associated user including their access
+to projects and their project permissions. To migrate trigger to use new permisison
+model use **Take ownership**.
## Before GitLab 8.12
diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb
index 1762d5bdf95..e55dc2913c3 100644
--- a/features/steps/project/issues/award_emoji.rb
+++ b/features/steps/project/issues/award_emoji.rb
@@ -45,7 +45,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
end
step 'I have new comment with emoji added' do
- expect(page).to have_selector ".emoji[title=':smile:']"
+ expect(page).to have_selector 'gl-emoji[data-name="smile"]'
end
step 'I have award added' do
diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb
index 11fa85ed2fe..071aa2e3eff 100644
--- a/features/steps/shared/diff_note.rb
+++ b/features/steps/shared/diff_note.rb
@@ -196,7 +196,7 @@ module SharedDiffNote
step 'The diff comment preview tab should display rendered Markdown' do
page.within(diff_file_selector) do
find('.js-md-preview-button').click
- expect(find('.js-md-preview')).to have_css('img.emoji', visible: true)
+ expect(find('.js-md-preview')).to have_css('gl-emoji', visible: true)
end
end
@@ -210,7 +210,7 @@ module SharedDiffNote
step 'I should see a diff comment with an emoji image' do
page.within("#{diff_file_selector} .note") do
- expect(page).to have_xpath("//img[@alt=':smile:']")
+ expect(page).to have_xpath("//gl-emoji[@data-name='smile']")
end
end
diff --git a/features/steps/shared/markdown.rb b/features/steps/shared/markdown.rb
index a036d9b884f..875d27d9383 100644
--- a/features/steps/shared/markdown.rb
+++ b/features/steps/shared/markdown.rb
@@ -40,7 +40,7 @@ module SharedMarkdown
step 'The Markdown preview tab should display rendered Markdown' do
page.within('.gfm-form') do
find('.js-md-preview-button').click
- expect(find('.js-md-preview')).to have_css('img.emoji', visible: true)
+ expect(find('.js-md-preview')).to have_css('gl-emoji', visible: true)
end
end
diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb
index 1870f6bc0c3..fd925e0d447 100644
--- a/features/steps/shared/note.rb
+++ b/features/steps/shared/note.rb
@@ -95,7 +95,7 @@ module SharedNote
step 'The comment preview tab should be display rendered Markdown' do
page.within(".js-main-target-form") do
find('.js-md-preview-button').click
- expect(find('.js-md-preview')).to have_css('img.emoji', visible: true)
+ expect(find('.js-md-preview')).to have_css('gl-emoji', visible: true)
end
end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 2d4d5a25221..a4201fe6fed 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -27,7 +27,7 @@ module API
optional :location, type: String, desc: 'The location of the user'
optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator'
optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups'
- optional :confirm, type: Boolean, desc: 'Flag indicating the account needs to be confirmed'
+ optional :skip_confirmation, type: Boolean, default: false, desc: 'Flag indicating the account is confirmed'
optional :external, type: Boolean, desc: 'Flag indicating the user is an external user'
all_or_none_of :extern_uid, :provider
end
@@ -97,29 +97,10 @@ module API
post do
authenticated_as_admin!
- # Filter out params which are used later
- user_params = declared_params(include_missing: false)
- identity_attrs = user_params.slice(:provider, :extern_uid)
- confirm = user_params.delete(:confirm)
- user = User.new(user_params.except(:extern_uid, :provider, :reset_password))
-
- if user_params.delete(:reset_password)
- user.attributes = {
- force_random_password: true,
- password_expires_at: nil,
- created_by_id: current_user.id
- }
- user.generate_password
- user.generate_reset_token
- end
-
- user.skip_confirmation! unless confirm
-
- if identity_attrs.any?
- user.identities.build(identity_attrs)
- end
+ params = declared_params(include_missing: false)
+ user = ::Users::CreateService.new(current_user, params).execute
- if user.save
+ if user.persisted?
present user, with: Entities::UserPublic
else
conflict!('Email has already been taken') if User.
diff --git a/lib/api/v3/users.rb b/lib/api/v3/users.rb
index 14f54731730..5e18cecc431 100644
--- a/lib/api/v3/users.rb
+++ b/lib/api/v3/users.rb
@@ -9,6 +9,59 @@ module API
end
resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
+ helpers do
+ params :optional_attributes do
+ optional :skype, type: String, desc: 'The Skype username'
+ optional :linkedin, type: String, desc: 'The LinkedIn username'
+ optional :twitter, type: String, desc: 'The Twitter username'
+ optional :website_url, type: String, desc: 'The website of the user'
+ optional :organization, type: String, desc: 'The organization of the user'
+ optional :projects_limit, type: Integer, desc: 'The number of projects a user can create'
+ optional :extern_uid, type: String, desc: 'The external authentication provider UID'
+ optional :provider, type: String, desc: 'The external provider'
+ optional :bio, type: String, desc: 'The biography of the user'
+ optional :location, type: String, desc: 'The location of the user'
+ optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator'
+ optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups'
+ optional :confirm, type: Boolean, default: true, desc: 'Flag indicating the account needs to be confirmed'
+ optional :external, type: Boolean, desc: 'Flag indicating the user is an external user'
+ all_or_none_of :extern_uid, :provider
+ end
+ end
+
+ desc 'Create a user. Available only for admins.' do
+ success ::API::Entities::UserPublic
+ end
+ params do
+ requires :email, type: String, desc: 'The email of the user'
+ optional :password, type: String, desc: 'The password of the new user'
+ optional :reset_password, type: Boolean, desc: 'Flag indicating the user will be sent a password reset token'
+ at_least_one_of :password, :reset_password
+ requires :name, type: String, desc: 'The name of the user'
+ requires :username, type: String, desc: 'The username of the user'
+ use :optional_attributes
+ end
+ post do
+ authenticated_as_admin!
+
+ params = declared_params(include_missing: false)
+ user = ::Users::CreateService.new(current_user, params.merge!(skip_confirmation: !params[:confirm])).execute
+
+ if user.persisted?
+ present user, with: ::API::Entities::UserPublic
+ else
+ conflict!('Email has already been taken') if User.
+ where(email: user.email).
+ count > 0
+
+ conflict!('Username has already been taken') if User.
+ where(username: user.username).
+ count > 0
+
+ render_validation_error!(user)
+ end
+ end
+
desc 'Get the SSH keys of a specified user. Available only for admins.' do
success ::API::Entities::SSHKey
end
diff --git a/lib/gitlab/ci/build/step.rb b/lib/gitlab/ci/build/step.rb
index 1877429ac46..ee034d9cc56 100644
--- a/lib/gitlab/ci/build/step.rb
+++ b/lib/gitlab/ci/build/step.rb
@@ -7,13 +7,12 @@ module Gitlab
WHEN_ALWAYS = 'always'.freeze
attr_reader :name
- attr_writer :script
- attr_accessor :timeout, :when, :allow_failure
+ attr_accessor :script, :timeout, :when, :allow_failure
class << self
def from_commands(job)
self.new(:script).tap do |step|
- step.script = job.commands
+ step.script = job.commands.split("\n")
step.timeout = job.timeout
step.when = WHEN_ON_SUCCESS
end
@@ -36,10 +35,6 @@ module Gitlab
@name = name
@allow_failure = false
end
-
- def script
- @script.split("\n")
- end
end
end
end
diff --git a/lib/gitlab/ci/status/canceled.rb b/lib/gitlab/ci/status/canceled.rb
index dd6d99e9075..97c121ce7b9 100644
--- a/lib/gitlab/ci/status/canceled.rb
+++ b/lib/gitlab/ci/status/canceled.rb
@@ -13,6 +13,10 @@ module Gitlab
def icon
'icon_status_canceled'
end
+
+ def favicon
+ 'favicon_status_canceled'
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb
index 3dd2b9e01f6..d4fd83b93f8 100644
--- a/lib/gitlab/ci/status/core.rb
+++ b/lib/gitlab/ci/status/core.rb
@@ -18,6 +18,10 @@ module Gitlab
raise NotImplementedError
end
+ def favicon
+ raise NotImplementedError
+ end
+
def label
raise NotImplementedError
end
diff --git a/lib/gitlab/ci/status/created.rb b/lib/gitlab/ci/status/created.rb
index 6596d7e01ca..0721bf6ec7c 100644
--- a/lib/gitlab/ci/status/created.rb
+++ b/lib/gitlab/ci/status/created.rb
@@ -13,6 +13,10 @@ module Gitlab
def icon
'icon_status_created'
end
+
+ def favicon
+ 'favicon_status_created'
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/failed.rb b/lib/gitlab/ci/status/failed.rb
index c5b5e3203ad..cb75e9383a8 100644
--- a/lib/gitlab/ci/status/failed.rb
+++ b/lib/gitlab/ci/status/failed.rb
@@ -13,6 +13,10 @@ module Gitlab
def icon
'icon_status_failed'
end
+
+ def favicon
+ 'favicon_status_failed'
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/manual.rb b/lib/gitlab/ci/status/manual.rb
index 5f28521901d..f8f6c2903ba 100644
--- a/lib/gitlab/ci/status/manual.rb
+++ b/lib/gitlab/ci/status/manual.rb
@@ -13,6 +13,10 @@ module Gitlab
def icon
'icon_status_manual'
end
+
+ def favicon
+ 'favicon_status_manual'
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/pending.rb b/lib/gitlab/ci/status/pending.rb
index d30f35a59a2..f40cc1314dc 100644
--- a/lib/gitlab/ci/status/pending.rb
+++ b/lib/gitlab/ci/status/pending.rb
@@ -13,6 +13,10 @@ module Gitlab
def icon
'icon_status_pending'
end
+
+ def favicon
+ 'favicon_status_pending'
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/running.rb b/lib/gitlab/ci/status/running.rb
index 2aba3c373c7..1237cd47dc8 100644
--- a/lib/gitlab/ci/status/running.rb
+++ b/lib/gitlab/ci/status/running.rb
@@ -13,6 +13,10 @@ module Gitlab
def icon
'icon_status_running'
end
+
+ def favicon
+ 'favicon_status_running'
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/skipped.rb b/lib/gitlab/ci/status/skipped.rb
index 16282aefd03..28005d91503 100644
--- a/lib/gitlab/ci/status/skipped.rb
+++ b/lib/gitlab/ci/status/skipped.rb
@@ -13,6 +13,10 @@ module Gitlab
def icon
'icon_status_skipped'
end
+
+ def favicon
+ 'favicon_status_skipped'
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/success.rb b/lib/gitlab/ci/status/success.rb
index c09c5f006e3..88f7758a270 100644
--- a/lib/gitlab/ci/status/success.rb
+++ b/lib/gitlab/ci/status/success.rb
@@ -13,6 +13,10 @@ module Gitlab
def icon
'icon_status_success'
end
+
+ def favicon
+ 'favicon_status_success'
+ end
end
end
end
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index e0fdf3f3d64..0829c1c318e 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -5,35 +5,44 @@ module Gitlab
CE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ce.git'.freeze
EE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze
CHECK_DIR = Rails.root.join('ee_compat_check')
- MAX_FETCH_DEPTH = 500
IGNORED_FILES_REGEX = /(VERSION|CHANGELOG\.md:\d+)/.freeze
-
- attr_reader :repo_dir, :patches_dir, :ce_repo, :ce_branch
+ PLEASE_READ_THIS_BANNER = %Q{
+ ============================================================
+ ===================== PLEASE READ THIS =====================
+ ============================================================
+ }.freeze
+ THANKS_FOR_READING_BANNER = %Q{
+ ============================================================
+ ==================== THANKS FOR READING ====================
+ ============================================================\n
+ }.freeze
+
+ attr_reader :ee_repo_dir, :patches_dir, :ce_repo, :ce_branch, :ee_branch_found
+ attr_reader :failed_files
def initialize(branch:, ce_repo: CE_REPO)
- @repo_dir = CHECK_DIR.join('repo')
+ @ee_repo_dir = CHECK_DIR.join('ee-repo')
@patches_dir = CHECK_DIR.join('patches')
@ce_branch = branch
@ce_repo = ce_repo
end
def check
- ensure_ee_repo
ensure_patches_dir
-
generate_patch(ce_branch, ce_patch_full_path)
- Dir.chdir(repo_dir) do
- step("In the #{repo_dir} directory")
+ ensure_ee_repo
+ Dir.chdir(ee_repo_dir) do
+ step("In the #{ee_repo_dir} directory")
status = catch(:halt_check) do
ce_branch_compat_check!
- delete_ee_branch_locally!
+ delete_ee_branches_locally!
ee_branch_presence_check!
ee_branch_compat_check!
end
- delete_ee_branch_locally!
+ delete_ee_branches_locally!
if status.nil?
true
@@ -46,11 +55,13 @@ module Gitlab
private
def ensure_ee_repo
- if Dir.exist?(repo_dir)
- step("#{repo_dir} already exists")
+ if Dir.exist?(ee_repo_dir)
+ step("#{ee_repo_dir} already exists")
else
- cmd = %W[git clone --branch master --single-branch --depth 200 #{EE_REPO} #{repo_dir}]
- step("Cloning #{EE_REPO} into #{repo_dir}", cmd)
+ step(
+ "Cloning #{EE_REPO} into #{ee_repo_dir}",
+ %W[git clone --branch master --single-branch --depth=200 #{EE_REPO} #{ee_repo_dir}]
+ )
end
end
@@ -61,23 +72,18 @@ module Gitlab
def generate_patch(branch, patch_path)
FileUtils.rm(patch_path, force: true)
- depth = 0
- loop do
- depth += 50
- cmd = %W[git fetch --depth #{depth} origin --prune +refs/heads/master:refs/remotes/origin/master]
- Gitlab::Popen.popen(cmd)
- _, status = Gitlab::Popen.popen(%w[git merge-base FETCH_HEAD HEAD])
+ find_merge_base_with_master(branch: branch)
- raise "#{branch} is too far behind master, please rebase it!" if depth >= MAX_FETCH_DEPTH
- break if status.zero?
- end
+ step(
+ "Generating the patch against origin/master in #{patch_path}",
+ %w[git format-patch origin/master --stdout]
+ ) do |output, status|
+ throw(:halt_check, :ko) unless status.zero?
- step("Generating the patch against master in #{patch_path}")
- output, status = Gitlab::Popen.popen(%w[git format-patch FETCH_HEAD --stdout])
- throw(:halt_check, :ko) unless status.zero?
+ File.write(patch_path, output)
- File.write(patch_path, output)
- throw(:halt_check, :ko) unless File.exist?(patch_path)
+ throw(:halt_check, :ko) unless File.exist?(patch_path)
+ end
end
def ce_branch_compat_check!
@@ -88,9 +94,17 @@ module Gitlab
end
def ee_branch_presence_check!
- status = step("Fetching origin/#{ee_branch}", %W[git fetch origin #{ee_branch}])
+ _, status = step("Fetching origin/#{ee_branch_prefix}", %W[git fetch origin #{ee_branch_prefix}])
- unless status.zero?
+ if status.zero?
+ @ee_branch_found = ee_branch_prefix
+ else
+ _, status = step("Fetching origin/#{ee_branch_suffix}", %W[git fetch origin #{ee_branch_suffix}])
+ end
+
+ if status.zero?
+ @ee_branch_found = ee_branch_suffix
+ else
puts
puts ce_branch_doesnt_apply_cleanly_and_no_ee_branch_msg
@@ -99,9 +113,9 @@ module Gitlab
end
def ee_branch_compat_check!
- step("Checking out origin/#{ee_branch}", %W[git checkout -b #{ee_branch} FETCH_HEAD])
+ step("Checking out origin/#{ee_branch_found}", %W[git checkout -b #{ee_branch_found} FETCH_HEAD])
- generate_patch(ee_branch, ee_patch_full_path)
+ generate_patch(ee_branch_found, ee_patch_full_path)
unless check_patch(ee_patch_full_path).zero?
puts
@@ -116,36 +130,72 @@ module Gitlab
def check_patch(patch_path)
step("Checking out master", %w[git checkout master])
- step("Reseting to latest master", %w[git reset --hard origin/master])
-
- step("Checking if #{patch_path} applies cleanly to EE/master")
- output, status = Gitlab::Popen.popen(%W[git apply --check --3way #{patch_path}])
-
- unless status.zero?
- failed_files = output.lines.reduce([]) do |memo, line|
- if line.start_with?('error: patch failed:')
- file = line.sub(/\Aerror: patch failed: /, '')
- memo << file unless file =~ IGNORED_FILES_REGEX
+ step("Resetting to latest master", %w[git reset --hard origin/master])
+ step(
+ "Checking if #{patch_path} applies cleanly to EE/master",
+ %W[git apply --check --3way #{patch_path}]
+ ) do |output, status|
+ unless status.zero?
+ @failed_files = output.lines.reduce([]) do |memo, line|
+ if line.start_with?('error: patch failed:')
+ file = line.sub(/\Aerror: patch failed: /, '')
+ memo << file unless file =~ IGNORED_FILES_REGEX
+ end
+ memo
end
- memo
+
+ status = 0 if failed_files.empty?
end
- if failed_files.empty?
- status = 0
- else
- puts "\nConflicting files:"
- failed_files.each do |file|
- puts " - #{file}"
- end
+ status
+ end
+ end
+
+ def delete_ee_branches_locally!
+ command(%w[git checkout master])
+ command(%W[git branch --delete --force #{ee_branch_prefix}])
+ command(%W[git branch --delete --force #{ee_branch_suffix}])
+ end
+
+ def merge_base_found?
+ step(
+ "Finding merge base with master",
+ %w[git merge-base origin/master HEAD]
+ ) do |output, status|
+ if status.zero?
+ puts "Merge base was found: #{output}"
+ true
end
end
+ end
+
+ def find_merge_base_with_master(branch:)
+ return if merge_base_found?
+
+ # Start with (Math.exp(3).to_i = 20) until (Math.exp(6).to_i = 403)
+ # In total we go (20 + 54 + 148 + 403 = 625) commits deeper
+ depth = 20
+ success =
+ (3..6).any? do |factor|
+ depth += Math.exp(factor).to_i
+ # Repository is initially cloned with a depth of 20 so we need to fetch
+ # deeper in the case the branch has more than 20 commits on top of master
+ fetch(branch: branch, depth: depth)
+ fetch(branch: 'master', depth: depth)
+
+ merge_base_found?
+ end
- status
+ raise "\n#{branch} is too far behind master, please rebase it!\n" unless success
end
- def delete_ee_branch_locally!
- command(%w[git checkout master])
- step("Deleting the local #{ee_branch} branch", %W[git branch -D #{ee_branch}])
+ def fetch(branch:, depth:)
+ step(
+ "Fetching deeper...",
+ %W[git fetch --depth=#{depth} --prune origin +refs/heads/#{branch}:refs/remotes/origin/#{branch}]
+ ) do |output, status|
+ raise "Fetch failed: #{output}" unless status.zero?
+ end
end
def ce_patch_name
@@ -156,8 +206,12 @@ module Gitlab
@ce_patch_full_path ||= patches_dir.join(ce_patch_name)
end
- def ee_branch
- @ee_branch ||= "#{ce_branch}-ee"
+ def ee_branch_suffix
+ @ee_branch_suffix ||= "#{ce_branch}-ee"
+ end
+
+ def ee_branch_prefix
+ @ee_branch_prefix ||= "ee-#{ce_branch}"
end
def ee_patch_name
@@ -178,98 +232,125 @@ module Gitlab
if cmd
start = Time.now
puts "\n$ #{cmd.join(' ')}"
- status = command(cmd)
- puts "\nFinished in #{Time.now - start} seconds"
- status
+
+ output, status = command(cmd)
+ puts "\n==> Finished in #{Time.now - start} seconds"
+
+ if block_given?
+ yield(output, status)
+ else
+ [output, status]
+ end
end
end
def command(cmd)
- output, status = Gitlab::Popen.popen(cmd)
- puts output
-
- status
+ Gitlab::Popen.popen(cmd)
end
def applies_cleanly_msg(branch)
- <<-MSG.strip_heredoc
- =================================================================
+ %Q{
+ #{PLEASE_READ_THIS_BANNER}
🎉 Congratulations!! 🎉
- The #{branch} branch applies cleanly to EE/master!
+ The `#{branch}` branch applies cleanly to EE/master!
- Much ❤️!!
- =================================================================\n
- MSG
+ Much ❤️! For more information, see
+ https://docs.gitlab.com/ce/development/limit_ee_conflicts.html#check-the-rake-ee_compat_check-in-your-merge-requests
+ #{THANKS_FOR_READING_BANNER}
+ }
end
def ce_branch_doesnt_apply_cleanly_and_no_ee_branch_msg
- <<-MSG.strip_heredoc
- =================================================================
+ %Q{
+ #{PLEASE_READ_THIS_BANNER}
💥 Oh no! 💥
- The #{ce_branch} branch does not apply cleanly to the current
- EE/master, and no #{ee_branch} branch was found in the EE repository.
+ The `#{ce_branch}` branch does not apply cleanly to the current
+ EE/master, and no `#{ee_branch_prefix}` or `#{ee_branch_suffix}` branch
+ was found in the EE repository.
- Please create a #{ee_branch} branch that includes changes from
- #{ce_branch} but also specific changes than can be applied cleanly
- to EE/master.
+ #{conflicting_files_msg}
+
+ We advise you to create a `#{ee_branch_prefix}` or `#{ee_branch_suffix}`
+ branch that includes changes from `#{ce_branch}` but also specific changes
+ than can be applied cleanly to EE/master. In some cases, the conflicts
+ are trivial and you can ignore the warning from this job. As always,
+ use your best judgment!
There are different ways to create such branch:
- 1. Create a new branch based on the CE branch and rebase it on top of EE/master
+ 1. Create a new branch from master and cherry-pick your CE commits
# In the EE repo
- $ git fetch #{ce_repo} #{ce_branch}
- $ git checkout -b #{ee_branch} FETCH_HEAD
-
- # You can squash the #{ce_branch} commits into a single "Port of #{ce_branch} to EE" commit
- # before rebasing to limit the conflicts-resolving steps during the rebase
$ git fetch origin
- $ git rebase origin/master
+ $ git checkout -b #{ee_branch_prefix} origin/master
+ $ git fetch #{ce_repo} #{ce_branch}
+ $ git cherry-pick SHA # Repeat for all the commits you want to pick
- At this point you will likely have conflicts.
- Solve them, and continue/finish the rebase.
+ You can squash the `#{ce_branch}` commits into a single "Port of #{ce_branch} to EE" commit.
- You can squash the #{ce_branch} commits into a single "Port of #{ce_branch} to EE".
+ 2. Apply your branch's patch to EE
- 2. Create a new branch from master and cherry-pick your CE commits
+ # In the CE repo
+ $ git fetch origin master
+ $ git format-patch origin/master --stdout > #{ce_branch}.patch
# In the EE repo
- $ git fetch origin
- $ git checkout -b #{ee_branch} origin/master
- $ git fetch #{ce_repo} #{ce_branch}
- $ git cherry-pick SHA # Repeat for all the commits you want to pick
+ $ git fetch origin master
+ $ git checkout -b #{ee_branch_prefix} origin/master
+ $ git apply --3way path/to/#{ce_branch}.patch
+
+ At this point you might have conflicts such as:
- You can squash the #{ce_branch} commits into a single "Port of #{ce_branch} to EE" commit.
+ error: patch failed: lib/gitlab/ee_compat_check.rb:5
+ Falling back to three-way merge...
+ Applied patch to 'lib/gitlab/ee_compat_check.rb' with conflicts.
+ U lib/gitlab/ee_compat_check.rb
- Don't forget to push your branch to #{EE_REPO}:
+ Resolve them, stage the changes and commit them.
+
+ ⚠️ Don't forget to push your branch to gitlab-ee:
# In the EE repo
- $ git push origin #{ee_branch}
+ $ git push origin #{ee_branch_prefix}
+
+ ⚠️ Also, don't forget to create a new merge request on gitlab-ce and
+ cross-link it with the CE merge request.
- You can then retry this failed build, and hopefully it should pass.
+ Once this is done, you can retry this failed build, and it should pass.
- Stay 💪 !
- =================================================================\n
- MSG
+ Stay 💪 ! For more information, see
+ https://docs.gitlab.com/ce/development/limit_ee_conflicts.html#check-the-rake-ee_compat_check-in-your-merge-requests
+ #{THANKS_FOR_READING_BANNER}
+ }
end
def ee_branch_doesnt_apply_cleanly_msg
- <<-MSG.strip_heredoc
- =================================================================
+ %Q{
+ #{PLEASE_READ_THIS_BANNER}
💥 Oh no! 💥
- The #{ce_branch} does not apply cleanly to the current
- EE/master, and even though a #{ee_branch} branch exists in the EE
- repository, it does not apply cleanly either to EE/master!
+ The `#{ce_branch}` does not apply cleanly to the current EE/master, and
+ even though a `#{ee_branch_found}` branch
+ exists in the EE repository, it does not apply cleanly either to
+ EE/master!
+
+ #{conflicting_files_msg}
- Please update the #{ee_branch}, push it again to #{EE_REPO}, and
+ Please update the `#{ee_branch_found}`, push it again to gitlab-ee, and
retry this build.
- Stay 💪 !
- =================================================================\n
- MSG
+ Stay 💪 ! For more information, see
+ https://docs.gitlab.com/ce/development/limit_ee_conflicts.html#check-the-rake-ee_compat_check-in-your-merge-requests
+ #{THANKS_FOR_READING_BANNER}
+ }
+ end
+
+ def conflicting_files_msg
+ failed_files.reduce("The conflicts detected were as follows:\n") do |memo, file|
+ memo << "\n - #{file}"
+ end
end
end
end
diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb
index fcf51b7fc5b..f98481c6d3a 100644
--- a/lib/gitlab/o_auth/user.rb
+++ b/lib/gitlab/o_auth/user.rb
@@ -147,10 +147,8 @@ module Gitlab
end
def build_new_user
- user = ::User.new(user_attributes)
- user.skip_confirmation!
- user.identities.new(extern_uid: auth_hash.uid, provider: auth_hash.provider)
- user
+ user_params = user_attributes.merge(extern_uid: auth_hash.uid, provider: auth_hash.provider, skip_confirmation: true)
+ Users::CreateService.new(nil, user_params).build
end
def user_attributes
diff --git a/package.json b/package.json
index 91d8752bebb..7b6c4556e2c 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,7 @@
"stats-webpack-plugin": "^0.4.3",
"timeago.js": "^2.0.5",
"underscore": "^1.8.3",
+ "visibilityjs": "^1.2.4",
"vue": "^2.2.4",
"vue-resource": "^0.9.3",
"webpack": "^2.2.1",
diff --git a/scripts/merge-reports b/scripts/merge-reports
index f7b574001ac..aad76bcc327 100755
--- a/scripts/merge-reports
+++ b/scripts/merge-reports
@@ -1,7 +1,6 @@
#!/usr/bin/env ruby
require 'json'
-require 'yaml'
main_report_file = ARGV.shift
unless main_report_file
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index 885b7eabba0..f170743aea3 100755
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -38,7 +38,7 @@ fi
# Only install knapsack after bundle install! Otherwise oddly some native
# gems could not be found under some circumstance. No idea why, hours wasted.
-retry gem install knapsack
+retry gem install knapsack fog-aws mime-types
if [ "$SETUP_DB" != "false" ]; then
bundle exec rake db:drop db:create db:schema:load db:migrate
diff --git a/scripts/sync-reports b/scripts/sync-reports
new file mode 100755
index 00000000000..5ed65e78005
--- /dev/null
+++ b/scripts/sync-reports
@@ -0,0 +1,95 @@
+#!/usr/bin/env ruby
+
+require 'rubygems'
+require 'fog/aws'
+
+class SyncReports
+ ACTIONS = %w[get put].freeze
+
+ attr_reader :options
+
+ def initialize(options)
+ @options = options
+
+ perform_sync!
+ end
+
+ private
+
+ def perform_sync!
+ case options[:action]
+ when 'get'
+ get_reports!
+ when 'put'
+ put_reports!
+ end
+ end
+
+ def get_reports!
+ options[:report_paths].each { |report_path| get_report!(report_path) }
+ end
+
+ def put_reports!
+ options[:report_paths].each { |report_path| put_report!(report_path) }
+ end
+
+ def get_report!(report_path)
+ file = bucket.files.get(report_path)
+
+ if file.respond_to?(:body)
+ File.write(report_path, file.body)
+ puts "#{report_path} was retrieved from S3."
+ else
+ puts "#{report_path} does not seem to exist on S3."
+ end
+ end
+
+ def put_report!(report_path)
+ bucket.files.create(
+ key: report_path,
+ body: File.open(report_path),
+ public: true
+ )
+ puts "#{report_path} was uploaded to S3."
+ end
+
+ def bucket
+ @bucket ||= storage.directories.get(options[:bucket])
+ end
+
+ def storage
+ @storage ||=
+ Fog::Storage.new(
+ provider: 'AWS',
+ aws_access_key_id: ENV['AWS_ACCESS_KEY_ID'],
+ aws_secret_access_key: ENV['AWS_SECRET_ACCESS_KEY']
+ )
+ end
+end
+
+def usage!(error: 'action')
+ print "\n[ERROR]: "
+ case error
+ when 'action'
+ puts "Please specify an action as first argument: #{SyncReports::ACTIONS.join(', ')}\n\n"
+ when 'bucket'
+ puts "Please specify a bucket as second argument!\n\n"
+ when 'files'
+ puts "Please specify one or more file paths as third argument!\n\n"
+ end
+ puts "Usage: #{__FILE__} [get|put] bucket report_path ...\n\n"
+ puts "Note: the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment "\
+ "variables need to be set\n\n"
+ exit 1
+end
+
+if $0 == __FILE__
+ action = ARGV.shift
+ usage!(error: 'action') unless SyncReports::ACTIONS.include?(action)
+
+ bucket = ARGV.shift
+ usage!(error: 'bucket') unless bucket
+ usage!(error: 'files') unless ARGV.any?
+
+ SyncReports.new(action: action, bucket: bucket, report_paths: ARGV)
+end
diff --git a/spec/controllers/projects/builds_controller_spec.rb b/spec/controllers/projects/builds_controller_spec.rb
new file mode 100644
index 00000000000..683667129e5
--- /dev/null
+++ b/spec/controllers/projects/builds_controller_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Projects::BuildsController do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :public) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET status.json' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let(:status) { build.detailed_status(double('user')) }
+
+ before do
+ get :status, namespace_id: project.namespace,
+ project_id: project,
+ id: build.id,
+ format: :json
+ end
+
+ it 'return a detailed build status in json' do
+ expect(response).to have_http_status(:ok)
+ expect(json_response['text']).to eq status.text
+ expect(json_response['label']).to eq status.label
+ expect(json_response['icon']).to eq status.icon
+ expect(json_response['favicon']).to eq status.favicon
+ end
+ end
+end
diff --git a/spec/controllers/projects/builds_controller_specs.rb b/spec/controllers/projects/builds_controller_specs.rb
new file mode 100644
index 00000000000..d501f7b3155
--- /dev/null
+++ b/spec/controllers/projects/builds_controller_specs.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Projects::BuildsController do
+ include ApiHelpers
+
+ let(:project) { create(:empty_project, :public) }
+
+ describe 'GET trace.json' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let(:user) { create(:user) }
+
+ context 'when user is logged in as developer' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ get_trace
+ end
+
+ it 'traces build log' do
+ expect(response).to have_http_status(:ok)
+ expect(json_response['id']).to eq build.id
+ expect(json_response['status']).to eq build.status
+ end
+ end
+
+ context 'when user is logged in as non member' do
+ before do
+ sign_in(user)
+ get_trace
+ end
+
+ it 'traces build log' do
+ expect(response).to have_http_status(:ok)
+ expect(json_response['id']).to eq build.id
+ expect(json_response['status']).to eq build.status
+ end
+ end
+
+ def get_trace
+ get :trace, namespace_id: project.namespace,
+ project_id: project,
+ id: build.id,
+ format: :json
+ end
+ end
+end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index c467ab9fb8a..734966d50b2 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -90,6 +90,7 @@ describe Projects::IssuesController do
it 'redirects to signin if not logged in' do
get :new, namespace_id: project.namespace, project_id: project
+ expect(flash[:notice]).to eq 'Please sign in to create the new issue.'
expect(response).to redirect_to(new_user_session_path)
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index c310d830e81..72f41f7209a 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -1178,4 +1178,42 @@ describe Projects::MergeRequestsController do
end
end
end
+
+ describe 'GET pipeline_status.json' do
+ context 'when head_pipeline exists' do
+ let!(:pipeline) do
+ create(:ci_pipeline, project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha)
+ end
+
+ let(:status) { pipeline.detailed_status(double('user')) }
+
+ before { get_pipeline_status }
+
+ it 'return a detailed head_pipeline status in json' do
+ expect(response).to have_http_status(:ok)
+ expect(json_response['text']).to eq status.text
+ expect(json_response['label']).to eq status.label
+ expect(json_response['icon']).to eq status.icon
+ expect(json_response['favicon']).to eq status.favicon
+ end
+ end
+
+ context 'when head_pipeline does not exist' do
+ before { get_pipeline_status }
+
+ it 'return empty' do
+ expect(response).to have_http_status(:ok)
+ expect(json_response).to be_empty
+ end
+ end
+
+ def get_pipeline_status
+ get :pipeline_status, namespace_id: project.namespace,
+ project_id: project,
+ id: merge_request.iid,
+ format: :json
+ end
+ end
end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 04bb5cbbd59..d8f9bfd0d37 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -69,4 +69,24 @@ describe Projects::PipelinesController do
format: :json
end
end
+
+ describe 'GET status.json' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:status) { pipeline.detailed_status(double('user')) }
+
+ before do
+ get :status, namespace_id: project.namespace,
+ project_id: project,
+ id: pipeline.id,
+ format: :json
+ end
+
+ it 'return a detailed pipeline status in json' do
+ expect(response).to have_http_status(:ok)
+ expect(json_response['text']).to eq status.text
+ expect(json_response['label']).to eq status.label
+ expect(json_response['icon']).to eq status.icon
+ expect(json_response['favicon']).to eq status.favicon
+ end
+ end
end
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index 8cc216445eb..902911071c4 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -30,6 +30,15 @@ describe RegistrationsController do
expect(subject.current_user).to be_nil
end
end
+
+ context 'when signup_enabled? is false' do
+ it 'redirects to sign_in' do
+ allow_any_instance_of(ApplicationSetting).to receive(:signup_enabled?).and_return(false)
+
+ expect { post(:create, user_params) }.not_to change(User, :count)
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
end
context 'when reCAPTCHA is enabled' do
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 6b0d084614b..f78086211f7 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -172,7 +172,7 @@ FactoryGirl.define do
{
image: 'ruby:2.1',
services: ['postgres'],
- after_script: "ls\ndate",
+ after_script: %w(ls date),
artifacts: {
name: 'artifacts_file',
untracked: false,
diff --git a/spec/features/admin/admin_broadcast_messages_spec.rb b/spec/features/admin/admin_broadcast_messages_spec.rb
index bc957ec72e1..d6c63f66a9b 100644
--- a/spec/features/admin/admin_broadcast_messages_spec.rb
+++ b/spec/features/admin/admin_broadcast_messages_spec.rb
@@ -45,7 +45,7 @@ feature 'Admin Broadcast Messages', feature: true do
page.within('.broadcast-message-preview') do
expect(page).to have_selector('strong', text: 'Markdown')
- expect(page).to have_selector('img.emoji')
+ expect(page).to have_selector('gl-emoji[data-name="tada"]')
end
end
end
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 0e305c52358..881f1fca4d1 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -11,12 +11,16 @@ describe 'Commits' do
stub_ci_pipeline_to_return_yaml_file
end
+ let(:creator) { create(:user) }
+
let!(:pipeline) do
create(:ci_pipeline,
project: project,
+ user: creator,
ref: project.default_branch,
sha: project.commit.sha,
- status: :success)
+ status: :success,
+ created_at: 5.months.ago)
end
context 'commit status is Generic Commit Status' do
@@ -80,7 +84,8 @@ describe 'Commits' do
it 'shows pipeline`s data' do
expect(page).to have_content pipeline.sha[0..7]
expect(page).to have_content pipeline.git_commit_message
- expect(page).to have_content pipeline.git_author_name
+ expect(page).to have_content pipeline.user.name
+ expect(page).to have_content pipeline.created_at.strftime('%b %d, %Y')
end
end
@@ -150,7 +155,7 @@ describe 'Commits' do
it do
expect(page).to have_content pipeline.sha[0..7]
expect(page).to have_content pipeline.git_commit_message
- expect(page).to have_content pipeline.git_author_name
+ expect(page).to have_content pipeline.user.name
expect(page).to have_link('Download artifacts')
expect(page).not_to have_link('Cancel running')
expect(page).not_to have_link('Retry')
@@ -169,7 +174,7 @@ describe 'Commits' do
it do
expect(page).to have_content pipeline.sha[0..7]
expect(page).to have_content pipeline.git_commit_message
- expect(page).to have_content pipeline.git_author_name
+ expect(page).to have_content pipeline.user.name
expect(page).not_to have_link('Download artifacts')
expect(page).not_to have_link('Cancel running')
expect(page).not_to have_link('Retry')
diff --git a/spec/features/explore/groups_list_spec.rb b/spec/features/explore/groups_list_spec.rb
index 773ae4b38bc..9daaaa8e555 100644
--- a/spec/features/explore/groups_list_spec.rb
+++ b/spec/features/explore/groups_list_spec.rb
@@ -7,6 +7,7 @@ describe 'Explore Groups page', js: true, feature: true do
let!(:group) { create(:group) }
let!(:public_group) { create(:group, :public) }
let!(:private_group) { create(:group, :private) }
+ let!(:empty_project) { create(:empty_project, group: public_group) }
before do
group.add_owner(user)
@@ -43,4 +44,23 @@ describe 'Explore Groups page', js: true, feature: true do
expect(page).not_to have_content(private_group.full_name)
expect(page.all('.js-groups-list-holder .content-list li').length).to eq 2
end
+
+ it 'shows non-archived projects count' do
+ # Initially project is not archived
+ expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("1")
+
+ # Archive project
+ empty_project.archive!
+ visit explore_groups_path
+
+ # Check project count
+ expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("0")
+
+ # Unarchive project
+ empty_project.unarchive!
+ visit explore_groups_path
+
+ # Check project count
+ expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("1")
+ end
end
diff --git a/spec/features/groups/group_name_toggle_spec.rb b/spec/features/groups/group_name_toggle_spec.rb
index 8528718a2f7..8a1d415c4f1 100644
--- a/spec/features/groups/group_name_toggle_spec.rb
+++ b/spec/features/groups/group_name_toggle_spec.rb
@@ -6,39 +6,46 @@ feature 'Group name toggle', feature: true, js: true do
let(:nested_group_2) { create(:group, parent: nested_group_1) }
let(:nested_group_3) { create(:group, parent: nested_group_2) }
+ SMALL_SCREEN = 300
+
before do
login_as :user
end
- it 'is not present for less than 3 groups' do
- visit group_path(group)
- expect(page).not_to have_css('.group-name-toggle')
+ it 'is not present if enough horizontal space' do
+ visit group_path(nested_group_3)
- visit group_path(nested_group_1)
+ container_width = page.evaluate_script("$('.title-container')[0].offsetWidth")
+ title_width = page.evaluate_script("$('.title')[0].offsetWidth")
+
+ expect(container_width).to be > title_width
expect(page).not_to have_css('.group-name-toggle')
end
- it 'is present for nested group of 3 or more in the namespace' do
- visit group_path(nested_group_2)
- expect(page).to have_css('.group-name-toggle')
-
+ it 'is present if the title is longer than the container' do
visit group_path(nested_group_3)
- expect(page).to have_css('.group-name-toggle')
+ title_width = page.evaluate_script("$('.title')[0].offsetWidth")
+
+ page_height = page.current_window.size[1]
+ page.current_window.resize_to(SMALL_SCREEN, page_height)
+
+ find('.group-name-toggle')
+ container_width = page.evaluate_script("$('.title-container')[0].offsetWidth")
+
+ expect(title_width).to be > container_width
end
- context 'for group with at least 3 groups' do
- before do
- visit group_path(nested_group_2)
- end
+ it 'should show the full group namespace when toggled' do
+ page_height = page.current_window.size[1]
+ page.current_window.resize_to(SMALL_SCREEN, page_height)
+ visit group_path(nested_group_3)
- it 'should show the full group namespace when toggled' do
- expect(page).not_to have_content(group.name)
- expect(page).to have_css('.group-path.hidable', visible: false)
+ expect(page).not_to have_content(group.name)
+ expect(page).to have_css('.group-path.hidable', visible: false)
- click_button '...'
+ click_button '...'
- expect(page).to have_content(group.name)
- expect(page).to have_css('.group-path.hidable', visible: true)
- end
+ expect(page).to have_content(group.name)
+ expect(page).to have_css('.group-path.hidable', visible: true)
end
end
diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb
index 8cc0996acab..f1ad4a55246 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -12,6 +12,33 @@ feature 'Create New Merge Request', feature: true, js: true do
login_as user
end
+ it 'selects the source branch sha when a tag with the same name exists' do
+ visit namespace_project_merge_requests_path(project.namespace, project)
+
+ click_link 'New Merge Request'
+ expect(page).to have_content('Source branch')
+ expect(page).to have_content('Target branch')
+
+ first('.js-source-branch').click
+ first('.dropdown-source-branch .dropdown-content a', text: 'v1.1.0').click
+
+ expect(page).to have_content "b83d6e3"
+ end
+
+ it 'selects the target branch sha when a tag with the same name exists' do
+ visit namespace_project_merge_requests_path(project.namespace, project)
+
+ click_link 'New Merge Request'
+
+ expect(page).to have_content('Source branch')
+ expect(page).to have_content('Target branch')
+
+ first('.js-target-branch').click
+ first('.dropdown-target-branch .dropdown-content a', text: 'v1.1.0').click
+
+ expect(page).to have_content "b83d6e3"
+ end
+
it 'generates a diff for an orphaned branch' do
visit namespace_project_merge_requests_path(project.namespace, project)
diff --git a/spec/features/merge_requests/toggler_behavior_spec.rb b/spec/features/merge_requests/toggler_behavior_spec.rb
index a2cf9b18bf2..3acd3f6a8b3 100644
--- a/spec/features/merge_requests/toggler_behavior_spec.rb
+++ b/spec/features/merge_requests/toggler_behavior_spec.rb
@@ -18,7 +18,7 @@ feature 'toggler_behavior', js: true, feature: true do
it 'should be scrolled down to fragment' do
page_height = page.current_window.size[1]
page_scroll_y = page.evaluate_script("window.scrollY")
- fragment_position_top = page.evaluate_script("$('#{fragment_id}').offset().top")
+ fragment_position_top = page.evaluate_script("Math.round($('#{fragment_id}').offset().top)")
expect(find('.js-toggle-content').visible?).to eq true
expect(find(fragment_id).visible?).to eq true
expect(fragment_position_top).to be >= page_scroll_y
diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb
index 030043d14aa..b2a3b111c9e 100644
--- a/spec/features/projects/compare_spec.rb
+++ b/spec/features/projects/compare_spec.rb
@@ -53,6 +53,7 @@ describe "Compare", js: true do
dropdown = find(".js-compare-#{dropdown_type}-dropdown")
dropdown.find(".compare-dropdown-toggle").click
dropdown.fill_in("Filter by Git revision", with: selection)
- find_link(selection, visible: true).click
+ wait_for_ajax
+ dropdown.find_all("a[data-ref=\"#{selection}\"]", visible: true).last.click
end
end
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index b64c15e0adc..de25d45f447 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -61,7 +61,7 @@ feature 'Projects > Members > User requests access', feature: true do
click_link('Settings')
end
- page.within('.page-with-layout-nav .sub-nav') do
+ page.within('.sub-nav') do
click_link('Members')
end
end
diff --git a/spec/features/projects/milestones/milestones_sorting_spec.rb b/spec/features/projects/milestones/milestones_sorting_spec.rb
new file mode 100644
index 00000000000..da3eaed707a
--- /dev/null
+++ b/spec/features/projects/milestones/milestones_sorting_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+feature 'Milestones sorting', :feature, :js do
+ include SortingHelper
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, name: 'test', namespace: user.namespace) }
+
+ before do
+ # Milestones
+ create(:milestone,
+ due_date: 10.days.from_now,
+ created_at: 2.hours.ago,
+ title: "aaa", project: project)
+ create(:milestone,
+ due_date: 11.days.from_now,
+ created_at: 1.hour.ago,
+ title: "bbb", project: project)
+ login_as(user)
+ end
+
+ scenario 'visit project milestones and sort by due_date_asc' do
+ visit namespace_project_milestones_path(project.namespace, project)
+
+ expect(page).to have_button('Due soon')
+
+ # assert default sorting
+ within '.milestones' do
+ expect(page.all('ul.content-list > li').first.text).to include('aaa')
+ expect(page.all('ul.content-list > li').last.text).to include('bbb')
+ end
+
+ click_button 'Due soon'
+
+ sort_options = find('ul.dropdown-menu-sort li').all('a').collect(&:text)
+
+ expect(sort_options[0]).to eq('Due soon')
+ expect(sort_options[1]).to eq('Due later')
+ expect(sort_options[2]).to eq('Start soon')
+ expect(sort_options[3]).to eq('Start later')
+ expect(sort_options[4]).to eq('Name, ascending')
+ expect(sort_options[5]).to eq('Name, descending')
+
+ click_link 'Due later'
+
+ expect(page).to have_button('Due later')
+
+ within '.milestones' do
+ expect(page.all('ul.content-list > li').first.text).to include('bbb')
+ expect(page.all('ul.content-list > li').last.text).to include('aaa')
+ end
+ end
+end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 9f06e52ab55..5a53e48f5f8 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -45,7 +45,7 @@ describe 'Pipeline', :feature, :js do
include_context 'pipeline builds'
let(:project) { create(:project) }
- let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id, user: user) }
before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) }
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 162056671e0..2272b19bc8f 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -442,7 +442,7 @@ describe 'Pipelines', :feature, :js do
context 'when project is public' do
let(:project) { create(:project, :public) }
- it { expect(page).to have_content 'No pipelines to show' }
+ it { expect(page).to have_content 'Build with confidence' }
it { expect(page).to have_http_status(:success) }
end
diff --git a/spec/features/user_callout_spec.rb b/spec/features/user_callout_spec.rb
index 336c4092c98..659cd7c7af7 100644
--- a/spec/features/user_callout_spec.rb
+++ b/spec/features/user_callout_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe 'User Callouts', js: true do
let(:user) { create(:user) }
+ let(:another_user) { create(:user) }
let(:project) { create(:empty_project, path: 'gitlab', name: 'sample') }
before do
@@ -32,6 +33,11 @@ describe 'User Callouts', js: true do
within('.user-callout') do
find('.close-user-callout').click
end
- expect(page).not_to have_selector('#user-callout')
+ expect(page).not_to have_selector('.user-callout')
+ end
+
+ it 'does not show callout on another users profile' do
+ visit user_path(another_user)
+ expect(page).not_to have_selector('.user-callout')
end
end
diff --git a/spec/helpers/avatars_helper_spec.rb b/spec/helpers/avatars_helper_spec.rb
new file mode 100644
index 00000000000..581726c1d0e
--- /dev/null
+++ b/spec/helpers/avatars_helper_spec.rb
@@ -0,0 +1,21 @@
+require 'rails_helper'
+
+describe AvatarsHelper do
+ let(:user) { create(:user) }
+
+ describe '#user_avatar' do
+ subject { helper.user_avatar(user: user) }
+
+ it "links to the user's profile" do
+ is_expected.to include("href=\"#{user_path(user)}\"")
+ end
+
+ it "has the user's name as title" do
+ is_expected.to include("title=\"#{user.name}\"")
+ end
+
+ it "contains the user's avatar image" do
+ is_expected.to include(CGI.escapeHTML(user.avatar_url(16)))
+ end
+ end
+end
diff --git a/spec/helpers/namespaces_helper_spec.rb b/spec/helpers/namespaces_helper_spec.rb
new file mode 100644
index 00000000000..e5143a0263d
--- /dev/null
+++ b/spec/helpers/namespaces_helper_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe NamespacesHelper, type: :helper do
+ let!(:admin) { create(:admin) }
+ let!(:admin_group) { create(:group, :private) }
+ let!(:user) { create(:user) }
+ let!(:user_group) { create(:group, :private) }
+
+ before do
+ admin_group.add_owner(admin)
+ user_group.add_owner(user)
+ end
+
+ describe '#namespaces_options' do
+ it 'returns groups without being a member for admin' do
+ allow(helper).to receive(:current_user).and_return(admin)
+
+ options = helper.namespaces_options(user_group.id, display_path: true, extra_group: user_group.id)
+
+ expect(options).to include(admin_group.name)
+ expect(options).to include(user_group.name)
+ end
+
+ it 'returns only allowed namespaces for user' do
+ allow(helper).to receive(:current_user).and_return(user)
+
+ options = helper.namespaces_options
+
+ expect(options).not_to include(admin_group.name)
+ expect(options).to include(user_group.name)
+ end
+ end
+end
diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb
new file mode 100644
index 00000000000..03f78de8e91
--- /dev/null
+++ b/spec/helpers/users_helper_spec.rb
@@ -0,0 +1,17 @@
+require 'rails_helper'
+
+describe UsersHelper do
+ let(:user) { create(:user) }
+
+ describe '#user_link' do
+ subject { helper.user_link(user) }
+
+ it "links to the user's profile" do
+ is_expected.to include("href=\"#{user_path(user)}\"")
+ end
+
+ it "has the user's email as title" do
+ is_expected.to include("title=\"#{user.email}\"")
+ end
+ end
+end
diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js
index e21f4ca2bc0..8153e46c438 100644
--- a/spec/javascripts/boards/boards_store_spec.js
+++ b/spec/javascripts/boards/boards_store_spec.js
@@ -50,9 +50,9 @@ describe('Store', () => {
it('finds list by ID', () => {
gl.issueBoards.BoardsStore.addList(listObj);
- const list = gl.issueBoards.BoardsStore.findList('id', 1);
+ const list = gl.issueBoards.BoardsStore.findList('id', listObj.id);
- expect(list.id).toBe(1);
+ expect(list.id).toBe(listObj.id);
});
it('finds list by type', () => {
@@ -64,7 +64,7 @@ describe('Store', () => {
it('gets issue when new list added', (done) => {
gl.issueBoards.BoardsStore.addList(listObj);
- const list = gl.issueBoards.BoardsStore.findList('id', 1);
+ const list = gl.issueBoards.BoardsStore.findList('id', listObj.id);
expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
@@ -89,9 +89,9 @@ describe('Store', () => {
expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
setTimeout(() => {
- const list = gl.issueBoards.BoardsStore.findList('id', 1);
+ const list = gl.issueBoards.BoardsStore.findList('id', listObj.id);
expect(list).toBeDefined();
- expect(list.id).toBe(1);
+ expect(list.id).toBe(listObj.id);
expect(list.position).toBe(0);
done();
}, 0);
@@ -126,7 +126,7 @@ describe('Store', () => {
expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
- gl.issueBoards.BoardsStore.removeList(1, 'label');
+ gl.issueBoards.BoardsStore.removeList(listObj.id, 'label');
expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
});
@@ -137,7 +137,7 @@ describe('Store', () => {
expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
- gl.issueBoards.BoardsStore.moveList(listOne, ['2', '1']);
+ gl.issueBoards.BoardsStore.moveList(listOne, [listObjDuplicate.id, listObj.id]);
expect(listOne.position).toBe(1);
});
diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js
index 66fc01fa1e5..a9d4c6ef76f 100644
--- a/spec/javascripts/boards/list_spec.js
+++ b/spec/javascripts/boards/list_spec.js
@@ -43,7 +43,7 @@ describe('List model', () => {
list = new List({
title: 'test',
label: {
- id: 1,
+ id: _.random(10000),
title: 'test',
color: 'red'
}
@@ -51,7 +51,7 @@ describe('List model', () => {
list.save();
setTimeout(() => {
- expect(list.id).toBe(1);
+ expect(list.id).toBe(listObj.id);
expect(list.type).toBe('label');
expect(list.position).toBe(0);
done();
@@ -60,7 +60,7 @@ describe('List model', () => {
it('destroys the list', (done) => {
gl.issueBoards.BoardsStore.addList(listObj);
- list = gl.issueBoards.BoardsStore.findList('id', 1);
+ list = gl.issueBoards.BoardsStore.findList('id', listObj.id);
expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
list.destroy();
@@ -92,7 +92,7 @@ describe('List model', () => {
const listDup = new List(listObjDuplicate);
const issue = new ListIssue({
title: 'Testing',
- iid: 1,
+ iid: _.random(10000),
confidential: false,
labels: [list.label, listDup.label]
});
@@ -102,7 +102,7 @@ describe('List model', () => {
spyOn(gl.boardService, 'moveIssue').and.callThrough();
- listDup.updateIssueLabel(list, issue);
+ listDup.updateIssueLabel(issue, list);
expect(gl.boardService.moveIssue)
.toHaveBeenCalledWith(issue.id, list.id, listDup.id, undefined, undefined);
diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js
index 7a399b307ad..a4fa694eebe 100644
--- a/spec/javascripts/boards/mock_data.js
+++ b/spec/javascripts/boards/mock_data.js
@@ -1,12 +1,12 @@
/* eslint-disable comma-dangle, no-unused-vars, quote-props */
const listObj = {
- id: 1,
+ id: _.random(10000),
position: 0,
title: 'Test',
list_type: 'label',
label: {
- id: 1,
+ id: _.random(10000),
title: 'Testing',
color: 'red',
description: 'testing;'
@@ -14,12 +14,12 @@ const listObj = {
};
const listObjDuplicate = {
- id: 2,
+ id: listObj.id,
position: 1,
title: 'Test',
list_type: 'label',
label: {
- id: 2,
+ id: listObj.label.id,
title: 'Testing',
color: 'red',
description: 'testing;'
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index 75efcc06585..bc2e092db65 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -33,7 +33,8 @@ describe('Pipelines table in Commits and Merge requests', () => {
});
setTimeout(() => {
- expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show');
+ expect(component.$el.querySelector('.empty-state')).toBeDefined();
+ expect(component.$el.querySelector('.realtime-loading')).toBe(null);
done();
}, 1);
});
@@ -63,6 +64,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
setTimeout(() => {
expect(component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1);
+ expect(component.$el.querySelector('.realtime-loading')).toBe(null);
done();
}, 0);
});
@@ -92,7 +94,8 @@ describe('Pipelines table in Commits and Merge requests', () => {
});
setTimeout(() => {
- expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show');
+ expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined();
+ expect(component.$el.querySelector('.realtime-loading')).toBe(null);
done();
}, 0);
});
diff --git a/spec/javascripts/cycle_analytics/limit_warning_component_spec.js b/spec/javascripts/cycle_analytics/limit_warning_component_spec.js
new file mode 100644
index 00000000000..50000c5a5f5
--- /dev/null
+++ b/spec/javascripts/cycle_analytics/limit_warning_component_spec.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import limitWarningComp from '~/cycle_analytics/components/limit_warning_component';
+
+describe('Limit warning component', () => {
+ let component;
+ let LimitWarningComponent;
+
+ beforeEach(() => {
+ LimitWarningComponent = Vue.extend(limitWarningComp);
+ });
+
+ it('should not render if count is not exactly than 50', () => {
+ component = new LimitWarningComponent({
+ propsData: {
+ count: 5,
+ },
+ }).$mount();
+
+ expect(component.$el.textContent.trim()).toBe('');
+
+ component = new LimitWarningComponent({
+ propsData: {
+ count: 55,
+ },
+ }).$mount();
+
+ expect(component.$el.textContent.trim()).toBe('');
+ });
+
+ it('should render if count is exactly 50', () => {
+ component = new LimitWarningComponent({
+ propsData: {
+ count: 50,
+ },
+ }).$mount();
+
+ expect(component.$el.textContent.trim()).toBe('Showing 50 events');
+ });
+});
diff --git a/spec/javascripts/fixtures/pipelines.html.haml b/spec/javascripts/fixtures/pipelines.html.haml
new file mode 100644
index 00000000000..418a38a0e2e
--- /dev/null
+++ b/spec/javascripts/fixtures/pipelines.html.haml
@@ -0,0 +1,14 @@
+%div
+ #pipelines-list-vue{ data: { endpoint: 'foo',
+ "css-class" => 'foo',
+ "help-page-path" => 'foo',
+ "new-pipeline-path" => 'foo',
+ "can-create-pipeline" => 'true',
+ "all-path" => 'foo',
+ "pending-path" => 'foo',
+ "running-path" => 'foo',
+ "finished-path" => 'foo',
+ "branches-path" => 'foo',
+ "tags-path" => 'foo',
+ "has-ci" => 'foo',
+ "ci-lint-path" => 'foo' } }
diff --git a/spec/javascripts/fixtures/pipelines_table.html.haml b/spec/javascripts/fixtures/pipelines_table.html.haml
index fbe4a434f76..ad1682704bb 100644
--- a/spec/javascripts/fixtures/pipelines_table.html.haml
+++ b/spec/javascripts/fixtures/pipelines_table.html.haml
@@ -1,2 +1 @@
-#commit-pipeline-table-view{ data: { endpoint: "endpoint" } }
-.pipeline-svgs{ data: { "commit_icon_svg": "svg"} }
+#commit-pipeline-table-view{ data: { endpoint: "endpoint", "help-page-path": "foo" } }
diff --git a/spec/javascripts/lib/utils/poll_spec.js b/spec/javascripts/lib/utils/poll_spec.js
index 05bc6bfd74b..c794a632417 100644
--- a/spec/javascripts/lib/utils/poll_spec.js
+++ b/spec/javascripts/lib/utils/poll_spec.js
@@ -124,4 +124,40 @@ describe('Poll', () => {
Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
});
+
+ describe('stop', () => {
+ it('stops polling when method is called', (done) => {
+ const pollInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': 2 } }));
+ };
+
+ Vue.http.interceptors.push(pollInterceptor);
+
+ const service = new ServiceMock('endpoint');
+ spyOn(service, 'fetch').and.callThrough();
+
+ const Polling = new Poll({
+ resource: service,
+ method: 'fetch',
+ data: { page: 1 },
+ successCallback: () => {
+ Polling.stop();
+ },
+ errorCallback: callbacks.error,
+ });
+
+ spyOn(Polling, 'stop').and.callThrough();
+
+ Polling.makeRequest();
+
+ setTimeout(() => {
+ expect(service.fetch.calls.count()).toEqual(1);
+ expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
+ expect(Polling.stop).toHaveBeenCalled();
+ done();
+ }, 100);
+
+ Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
+ });
+ });
});
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index 15465588223..d658f680f97 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -35,7 +35,8 @@ testsContext.keys().forEach(function (path) {
if (process.env.BABEL_ENV === 'coverage') {
// exempt these files from the coverage report
const troubleMakers = [
- './blob_edit/blob_edit_bundle.js',
+ './blob_edit/blob_bundle.js',
+ './boards/boards_bundle.js',
'./cycle_analytics/components/stage_plan_component.js',
'./cycle_analytics/components/stage_staging_component.js',
'./cycle_analytics/components/stage_test_component.js',
diff --git a/spec/javascripts/vue_pipelines_index/empty_state_spec.js b/spec/javascripts/vue_pipelines_index/empty_state_spec.js
new file mode 100644
index 00000000000..733337168dc
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/empty_state_spec.js
@@ -0,0 +1,38 @@
+import Vue from 'vue';
+import emptyStateComp from '~/vue_pipelines_index/components/empty_state';
+
+describe('Pipelines Empty State', () => {
+ let component;
+ let EmptyStateComponent;
+
+ beforeEach(() => {
+ EmptyStateComponent = Vue.extend(emptyStateComp);
+
+ component = new EmptyStateComponent({
+ propsData: {
+ helpPagePath: 'foo',
+ },
+ }).$mount();
+ });
+
+ it('should render empty state SVG', () => {
+ expect(component.$el.querySelector('.svg-content svg')).toBeDefined();
+ });
+
+ it('should render emtpy state information', () => {
+ expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence');
+
+ expect(
+ component.$el.querySelector('p').textContent,
+ ).toContain('Continous Integration can help catch bugs by running your tests automatically');
+
+ expect(
+ component.$el.querySelector('p').textContent,
+ ).toContain('Continuous Deployment can help you deliver code to your product environment');
+ });
+
+ it('should render a link with provided help path', () => {
+ expect(component.$el.querySelector('.btn-info').getAttribute('href')).toEqual('foo');
+ expect(component.$el.querySelector('.btn-info').textContent).toContain('Get started with Pipelines');
+ });
+});
diff --git a/spec/javascripts/vue_pipelines_index/error_state_spec.js b/spec/javascripts/vue_pipelines_index/error_state_spec.js
new file mode 100644
index 00000000000..524e018b1fa
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/error_state_spec.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import errorStateComp from '~/vue_pipelines_index/components/error_state';
+
+describe('Pipelines Error State', () => {
+ let component;
+ let ErrorStateComponent;
+
+ beforeEach(() => {
+ ErrorStateComponent = Vue.extend(errorStateComp);
+
+ component = new ErrorStateComponent().$mount();
+ });
+
+ it('should render error state SVG', () => {
+ expect(component.$el.querySelector('.svg-content svg')).toBeDefined();
+ });
+
+ it('should render emtpy state information', () => {
+ expect(
+ component.$el.querySelector('h4').textContent,
+ ).toContain('The API failed to fetch the pipelines');
+ });
+});
diff --git a/spec/javascripts/vue_pipelines_index/mock_data.js b/spec/javascripts/vue_pipelines_index/mock_data.js
new file mode 100644
index 00000000000..2365a662b9f
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/mock_data.js
@@ -0,0 +1,107 @@
+export default {
+ pipelines: [{
+ id: 115,
+ user: {
+ name: 'Root',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3000/root',
+ },
+ path: '/root/review-app/pipelines/115',
+ details: {
+ status: {
+ icon: 'icon_status_failed',
+ text: 'failed',
+ label: 'failed',
+ group: 'failed',
+ has_details: true,
+ details_path: '/root/review-app/pipelines/115',
+ },
+ duration: null,
+ finished_at: '2017-03-17T19:00:15.996Z',
+ stages: [{
+ name: 'build',
+ title: 'build: failed',
+ status: {
+ icon: 'icon_status_failed',
+ text: 'failed',
+ label: 'failed',
+ group: 'failed',
+ has_details: true,
+ details_path: '/root/review-app/pipelines/115#build',
+ },
+ path: '/root/review-app/pipelines/115#build',
+ dropdown_path: '/root/review-app/pipelines/115/stage.json?stage=build',
+ },
+ {
+ name: 'review',
+ title: 'review: skipped',
+ status: {
+ icon: 'icon_status_skipped',
+ text: 'skipped',
+ label: 'skipped',
+ group: 'skipped',
+ has_details: true,
+ details_path: '/root/review-app/pipelines/115#review',
+ },
+ path: '/root/review-app/pipelines/115#review',
+ dropdown_path: '/root/review-app/pipelines/115/stage.json?stage=review',
+ }],
+ artifacts: [],
+ manual_actions: [{
+ name: 'stop_review',
+ path: '/root/review-app/builds/3766/play',
+ }],
+ },
+ flags: {
+ latest: true,
+ triggered: false,
+ stuck: false,
+ yaml_errors: false,
+ retryable: true,
+ cancelable: false,
+ },
+ ref: {
+ name: 'thisisabranch',
+ path: '/root/review-app/tree/thisisabranch',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: '9e87f87625b26c42c59a2ee0398f81d20cdfe600',
+ short_id: '9e87f876',
+ title: 'Update README.md',
+ created_at: '2017-03-15T22:58:28.000+00:00',
+ parent_ids: ['3744f9226e699faec2662a8b267e5d3fd0bfff0e'],
+ message: 'Update README.md',
+ author_name: 'Root',
+ author_email: 'admin@example.com',
+ authored_date: '2017-03-15T22:58:28.000+00:00',
+ committer_name: 'Root',
+ committer_email: 'admin@example.com',
+ committed_date: '2017-03-15T22:58:28.000+00:00',
+ author: {
+ name: 'Root',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3000/root',
+ },
+ author_gravatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ commit_url: 'http://localhost:3000/root/review-app/commit/9e87f87625b26c42c59a2ee0398f81d20cdfe600',
+ commit_path: '/root/review-app/commit/9e87f87625b26c42c59a2ee0398f81d20cdfe600',
+ },
+ retry_path: '/root/review-app/pipelines/115/retry',
+ created_at: '2017-03-15T22:58:33.436Z',
+ updated_at: '2017-03-17T19:00:15.997Z',
+ }],
+ count: {
+ all: 52,
+ running: 0,
+ pending: 0,
+ finished: 52,
+ },
+};
diff --git a/spec/javascripts/vue_pipelines_index/nav_controls_spec.js b/spec/javascripts/vue_pipelines_index/nav_controls_spec.js
new file mode 100644
index 00000000000..659c4854a56
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/nav_controls_spec.js
@@ -0,0 +1,93 @@
+import Vue from 'vue';
+import navControlsComp from '~/vue_pipelines_index/components/nav_controls';
+
+describe('Pipelines Nav Controls', () => {
+ let NavControlsComponent;
+
+ beforeEach(() => {
+ NavControlsComponent = Vue.extend(navControlsComp);
+ });
+
+ it('should render link to create a new pipeline', () => {
+ const mockData = {
+ newPipelinePath: 'foo',
+ hasCiEnabled: true,
+ helpPagePath: 'foo',
+ ciLintPath: 'foo',
+ canCreatePipeline: true,
+ };
+
+ const component = new NavControlsComponent({
+ propsData: mockData,
+ }).$mount();
+
+ expect(component.$el.querySelector('.btn-create').textContent).toContain('Run Pipeline');
+ expect(component.$el.querySelector('.btn-create').getAttribute('href')).toEqual(mockData.newPipelinePath);
+ });
+
+ it('should not render link to create pipeline if no permission is provided', () => {
+ const mockData = {
+ newPipelinePath: 'foo',
+ hasCiEnabled: true,
+ helpPagePath: 'foo',
+ ciLintPath: 'foo',
+ canCreatePipeline: false,
+ };
+
+ const component = new NavControlsComponent({
+ propsData: mockData,
+ }).$mount();
+
+ expect(component.$el.querySelector('.btn-create')).toEqual(null);
+ });
+
+ it('should render link for CI lint', () => {
+ const mockData = {
+ newPipelinePath: 'foo',
+ hasCiEnabled: true,
+ helpPagePath: 'foo',
+ ciLintPath: 'foo',
+ canCreatePipeline: true,
+ };
+
+ const component = new NavControlsComponent({
+ propsData: mockData,
+ }).$mount();
+
+ expect(component.$el.querySelector('.btn-default').textContent).toContain('CI Lint');
+ expect(component.$el.querySelector('.btn-default').getAttribute('href')).toEqual(mockData.ciLintPath);
+ });
+
+ it('should render link to help page when CI is not enabled', () => {
+ const mockData = {
+ newPipelinePath: 'foo',
+ hasCiEnabled: false,
+ helpPagePath: 'foo',
+ ciLintPath: 'foo',
+ canCreatePipeline: true,
+ };
+
+ const component = new NavControlsComponent({
+ propsData: mockData,
+ }).$mount();
+
+ expect(component.$el.querySelector('.btn-info').textContent).toContain('Get started with Pipelines');
+ expect(component.$el.querySelector('.btn-info').getAttribute('href')).toEqual(mockData.helpPagePath);
+ });
+
+ it('should not render link to help page when CI is enabled', () => {
+ const mockData = {
+ newPipelinePath: 'foo',
+ hasCiEnabled: true,
+ helpPagePath: 'foo',
+ ciLintPath: 'foo',
+ canCreatePipeline: true,
+ };
+
+ const component = new NavControlsComponent({
+ propsData: mockData,
+ }).$mount();
+
+ expect(component.$el.querySelector('.btn-info')).toEqual(null);
+ });
+});
diff --git a/spec/javascripts/vue_pipelines_index/pipelines_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_spec.js
new file mode 100644
index 00000000000..725f6cb2d7a
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/pipelines_spec.js
@@ -0,0 +1,114 @@
+import Vue from 'vue';
+import pipelinesComp from '~/vue_pipelines_index/pipelines';
+import Store from '~/vue_pipelines_index/stores/pipelines_store';
+import pipelinesData from './mock_data';
+
+describe('Pipelines', () => {
+ preloadFixtures('static/pipelines.html.raw');
+
+ let PipelinesComponent;
+
+ beforeEach(() => {
+ loadFixtures('static/pipelines.html.raw');
+
+ PipelinesComponent = Vue.extend(pipelinesComp);
+ });
+
+ describe('successfull request', () => {
+ describe('with pipelines', () => {
+ const pipelinesInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify(pipelinesData), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(pipelinesInterceptor);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, pipelinesInterceptor,
+ );
+ });
+
+ it('should render table', (done) => {
+ const component = new PipelinesComponent({
+ propsData: {
+ store: new Store(),
+ },
+ }).$mount();
+
+ setTimeout(() => {
+ expect(component.$el.querySelector('.table-holder')).toBeDefined();
+ expect(component.$el.querySelector('.realtime-loading')).toBe(null);
+ done();
+ });
+ });
+ });
+
+ describe('without pipelines', () => {
+ const emptyInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(emptyInterceptor);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, emptyInterceptor,
+ );
+ });
+
+ it('should render empty state', (done) => {
+ const component = new PipelinesComponent({
+ propsData: {
+ store: new Store(),
+ },
+ }).$mount();
+
+ setTimeout(() => {
+ expect(component.$el.querySelector('.empty-state')).toBeDefined();
+ expect(component.$el.querySelector('.realtime-loading')).toBe(null);
+ done();
+ });
+ });
+ });
+ });
+
+ describe('unsuccessfull request', () => {
+ const errorInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), {
+ status: 500,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(errorInterceptor);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, errorInterceptor,
+ );
+ });
+
+ it('should render error state', (done) => {
+ const component = new PipelinesComponent({
+ propsData: {
+ store: new Store(),
+ },
+ }).$mount();
+
+ setTimeout(() => {
+ expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined();
+ expect(component.$el.querySelector('.realtime-loading')).toBe(null);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/lib/gitlab/ci/build/step_spec.rb b/spec/lib/gitlab/ci/build/step_spec.rb
index 2a314a744ca..49457b129e3 100644
--- a/spec/lib/gitlab/ci/build/step_spec.rb
+++ b/spec/lib/gitlab/ci/build/step_spec.rb
@@ -25,7 +25,7 @@ describe Gitlab::Ci::Build::Step do
end
context 'when after_script is not empty' do
- let(:job) { create(:ci_build, options: { after_script: "ls -la\ndate" }) }
+ let(:job) { create(:ci_build, options: { after_script: ['ls -la', 'date'] }) }
it 'fabricates an object' do
expect(subject.name).to eq(:after_script)
diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb
index 8b3bd08cf13..e648a3ac3a2 100644
--- a/spec/lib/gitlab/ci/status/build/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb
@@ -27,6 +27,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'passed'
expect(status.icon).to eq 'icon_status_success'
+ expect(status.favicon).to eq 'favicon_status_success'
expect(status.label).to eq 'passed'
expect(status).to have_details
expect(status).to have_action
@@ -53,6 +54,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'failed'
expect(status.icon).to eq 'icon_status_failed'
+ expect(status.favicon).to eq 'favicon_status_failed'
expect(status.label).to eq 'failed'
expect(status).to have_details
expect(status).to have_action
@@ -79,6 +81,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'failed'
expect(status.icon).to eq 'icon_status_warning'
+ expect(status.favicon).to eq 'favicon_status_failed'
expect(status.label).to eq 'failed (allowed to fail)'
expect(status).to have_details
expect(status).to have_action
@@ -107,6 +110,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'canceled'
expect(status.icon).to eq 'icon_status_canceled'
+ expect(status.favicon).to eq 'favicon_status_canceled'
expect(status.label).to eq 'canceled'
expect(status).to have_details
expect(status).to have_action
@@ -132,6 +136,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'running'
expect(status.icon).to eq 'icon_status_running'
+ expect(status.favicon).to eq 'favicon_status_running'
expect(status.label).to eq 'running'
expect(status).to have_details
expect(status).to have_action
@@ -157,6 +162,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'pending'
expect(status.icon).to eq 'icon_status_pending'
+ expect(status.favicon).to eq 'favicon_status_pending'
expect(status.label).to eq 'pending'
expect(status).to have_details
expect(status).to have_action
@@ -181,6 +187,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'skipped'
expect(status.icon).to eq 'icon_status_skipped'
+ expect(status.favicon).to eq 'favicon_status_skipped'
expect(status.label).to eq 'skipped'
expect(status).to have_details
expect(status).not_to have_action
@@ -208,6 +215,7 @@ describe Gitlab::Ci::Status::Build::Factory do
expect(status.text).to eq 'manual'
expect(status.group).to eq 'manual'
expect(status.icon).to eq 'icon_status_manual'
+ expect(status.favicon).to eq 'favicon_status_manual'
expect(status.label).to eq 'manual play action'
expect(status).to have_details
expect(status).to have_action
@@ -235,6 +243,7 @@ describe Gitlab::Ci::Status::Build::Factory do
expect(status.text).to eq 'manual'
expect(status.group).to eq 'manual'
expect(status.icon).to eq 'icon_status_manual'
+ expect(status.favicon).to eq 'favicon_status_manual'
expect(status.label).to eq 'manual stop action'
expect(status).to have_details
expect(status).to have_action
diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb
index 768f8926f1d..530639a5897 100644
--- a/spec/lib/gitlab/ci/status/canceled_spec.rb
+++ b/spec/lib/gitlab/ci/status/canceled_spec.rb
@@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Canceled do
it { expect(subject.icon).to eq 'icon_status_canceled' }
end
+ describe '#favicon' do
+ it { expect(subject.favicon).to eq 'favicon_status_canceled' }
+ end
+
describe '#group' do
it { expect(subject.group).to eq 'canceled' }
end
diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb
index e96c13aede3..aef982e17f1 100644
--- a/spec/lib/gitlab/ci/status/created_spec.rb
+++ b/spec/lib/gitlab/ci/status/created_spec.rb
@@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Created do
it { expect(subject.icon).to eq 'icon_status_created' }
end
+ describe '#favicon' do
+ it { expect(subject.favicon).to eq 'favicon_status_created' }
+ end
+
describe '#group' do
it { expect(subject.group).to eq 'created' }
end
diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb
index e5da0a91159..9a25743885c 100644
--- a/spec/lib/gitlab/ci/status/failed_spec.rb
+++ b/spec/lib/gitlab/ci/status/failed_spec.rb
@@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Failed do
it { expect(subject.icon).to eq 'icon_status_failed' }
end
+ describe '#favicon' do
+ it { expect(subject.favicon).to eq 'favicon_status_failed' }
+ end
+
describe '#group' do
it { expect(subject.group).to eq 'failed' }
end
diff --git a/spec/lib/gitlab/ci/status/manual_spec.rb b/spec/lib/gitlab/ci/status/manual_spec.rb
index 3fd3727b92d..6fdc3801d71 100644
--- a/spec/lib/gitlab/ci/status/manual_spec.rb
+++ b/spec/lib/gitlab/ci/status/manual_spec.rb
@@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Manual do
it { expect(subject.icon).to eq 'icon_status_manual' }
end
+ describe '#favicon' do
+ it { expect(subject.favicon).to eq 'favicon_status_manual' }
+ end
+
describe '#group' do
it { expect(subject.group).to eq 'manual' }
end
diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb
index 8d09cf2a05a..ffc53f0506b 100644
--- a/spec/lib/gitlab/ci/status/pending_spec.rb
+++ b/spec/lib/gitlab/ci/status/pending_spec.rb
@@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Pending do
it { expect(subject.icon).to eq 'icon_status_pending' }
end
+ describe '#favicon' do
+ it { expect(subject.favicon).to eq 'favicon_status_pending' }
+ end
+
describe '#group' do
it { expect(subject.group).to eq 'pending' }
end
diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb
index 10d3bf749c1..0babf1fb54e 100644
--- a/spec/lib/gitlab/ci/status/running_spec.rb
+++ b/spec/lib/gitlab/ci/status/running_spec.rb
@@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Running do
it { expect(subject.icon).to eq 'icon_status_running' }
end
+ describe '#favicon' do
+ it { expect(subject.favicon).to eq 'favicon_status_running' }
+ end
+
describe '#group' do
it { expect(subject.group).to eq 'running' }
end
diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb
index 10db93d3802..670747c9f0b 100644
--- a/spec/lib/gitlab/ci/status/skipped_spec.rb
+++ b/spec/lib/gitlab/ci/status/skipped_spec.rb
@@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Skipped do
it { expect(subject.icon).to eq 'icon_status_skipped' }
end
+ describe '#favicon' do
+ it { expect(subject.favicon).to eq 'favicon_status_skipped' }
+ end
+
describe '#group' do
it { expect(subject.group).to eq 'skipped' }
end
diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb
index 230f24b94a4..ff65b074808 100644
--- a/spec/lib/gitlab/ci/status/success_spec.rb
+++ b/spec/lib/gitlab/ci/status/success_spec.rb
@@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Success do
it { expect(subject.icon).to eq 'icon_status_success' }
end
+ describe '#favicon' do
+ it { expect(subject.favicon).to eq 'favicon_status_success' }
+ end
+
describe '#group' do
it { expect(subject.group).to eq 'success' }
end
diff --git a/spec/lib/gitlab/git/blob_snippet_spec.rb b/spec/lib/gitlab/git/blob_snippet_spec.rb
index 17d6be470ac..d6d365f6492 100644
--- a/spec/lib/gitlab/git/blob_snippet_spec.rb
+++ b/spec/lib/gitlab/git/blob_snippet_spec.rb
@@ -3,7 +3,7 @@
require "spec_helper"
describe Gitlab::Git::BlobSnippet, seed_helper: true do
- describe :data do
+ describe '#data' do
context 'empty lines' do
let(:snippet) { Gitlab::Git::BlobSnippet.new('master', nil, nil, nil) }
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index 8049e2c120d..b883526151e 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -5,7 +5,7 @@ require "spec_helper"
describe Gitlab::Git::Blob, seed_helper: true do
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
- describe :initialize do
+ describe 'initialize' do
let(:blob) { Gitlab::Git::Blob.new(name: 'test') }
it 'handles nil data' do
@@ -15,7 +15,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
end
- describe :find do
+ describe '.find' do
context 'file in subdir' do
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") }
@@ -101,7 +101,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
end
- describe :raw do
+ describe '.raw' do
let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) }
it { expect(raw_blob.id).to eq(SeedRepo::RubyBlob::ID) }
it { expect(raw_blob.data[0..10]).to eq("require \'fi") }
@@ -222,7 +222,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
end
- describe :lfs_pointers do
+ describe 'lfs_pointers' do
context 'file a valid lfs pointer' do
let(:blob) do
Gitlab::Git::Blob.find(
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index e1be6784c20..5cf4631fbfc 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -65,7 +65,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
context 'Class methods' do
- describe :find do
+ describe '.find' do
it "should return first head commit if without params" do
expect(Gitlab::Git::Commit.last(repository).id).to eq(
repository.raw.head.target.oid
@@ -103,7 +103,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
- describe :last_for_path do
+ describe '.last_for_path' do
context 'no path' do
subject { Gitlab::Git::Commit.last_for_path(repository, 'master') }
@@ -132,7 +132,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
- describe "where" do
+ describe '.where' do
context 'path is empty string' do
subject do
commits = Gitlab::Git::Commit.where(
@@ -230,7 +230,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
- describe :between do
+ describe '.between' do
subject do
commits = Gitlab::Git::Commit.between(repository, SeedRepo::Commit::PARENT_ID, SeedRepo::Commit::ID)
commits.map { |c| c.id }
@@ -243,7 +243,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
it { is_expected.not_to include(SeedRepo::FirstCommit::ID) }
end
- describe :find_all do
+ describe '.find_all' do
context 'max_count' do
subject do
commits = Gitlab::Git::Commit.find_all(
@@ -304,7 +304,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
- describe :init_from_rugged do
+ describe '#init_from_rugged' do
let(:gitlab_commit) { Gitlab::Git::Commit.new(rugged_commit) }
subject { gitlab_commit }
@@ -314,7 +314,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
- describe :init_from_hash do
+ describe '#init_from_hash' do
let(:commit) { Gitlab::Git::Commit.new(sample_commit_hash) }
subject { commit }
@@ -329,7 +329,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
- describe :stats do
+ describe '#stats' do
subject { commit.stats }
describe '#additions' do
@@ -343,25 +343,25 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
- describe :to_diff do
+ describe '#to_diff' do
subject { commit.to_diff }
it { is_expected.not_to include "From #{SeedRepo::Commit::ID}" }
it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'}
end
- describe :has_zero_stats? do
+ describe '#has_zero_stats?' do
it { expect(commit.has_zero_stats?).to eq(false) }
end
- describe :to_patch do
+ describe '#to_patch' do
subject { commit.to_patch }
it { is_expected.to include "From #{SeedRepo::Commit::ID}" }
it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'}
end
- describe :to_hash do
+ describe '#to_hash' do
let(:hash) { commit.to_hash }
subject { hash }
@@ -373,7 +373,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
- describe :diffs do
+ describe '#diffs' do
subject { commit.diffs }
it { is_expected.to be_kind_of Gitlab::Git::DiffCollection }
@@ -381,7 +381,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
it { expect(subject.first).to be_kind_of Gitlab::Git::Diff }
end
- describe :ref_names do
+ describe '#ref_names' do
let(:commit) { Gitlab::Git::Commit.find(repository, 'master') }
subject { commit.ref_names(repository) }
diff --git a/spec/lib/gitlab/git/compare_spec.rb b/spec/lib/gitlab/git/compare_spec.rb
index f66b68e4218..e28debe1494 100644
--- a/spec/lib/gitlab/git/compare_spec.rb
+++ b/spec/lib/gitlab/git/compare_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::Git::Compare, seed_helper: true do
let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, false) }
let(:compare_straight) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, true) }
- describe :commits do
+ describe '#commits' do
subject do
compare.commits.map(&:id)
end
@@ -42,7 +42,7 @@ describe Gitlab::Git::Compare, seed_helper: true do
end
end
- describe :diffs do
+ describe '#diffs' do
subject do
compare.diffs.map(&:new_path)
end
@@ -67,7 +67,7 @@ describe Gitlab::Git::Compare, seed_helper: true do
end
end
- describe :same do
+ describe '#same' do
subject do
compare.same
end
@@ -81,7 +81,7 @@ describe Gitlab::Git::Compare, seed_helper: true do
end
end
- describe :commits_straight do
+ describe '#commits', 'straight compare' do
subject do
compare_straight.commits.map(&:id)
end
@@ -94,7 +94,7 @@ describe Gitlab::Git::Compare, seed_helper: true do
it { is_expected.not_to include(SeedRepo::BigCommit::PARENT_ID) }
end
- describe :diffs_straight do
+ describe '#diffs', 'straight compare' do
subject do
compare_straight.diffs.map(&:new_path)
end
diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb
index 47bdd7310d5..122c93dcd69 100644
--- a/spec/lib/gitlab/git/diff_collection_spec.rb
+++ b/spec/lib/gitlab/git/diff_collection_spec.rb
@@ -24,7 +24,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
it { is_expected.to be_kind_of ::Array }
end
- describe :decorate! do
+ describe '#decorate!' do
let(:file_count) { 3 }
it 'modifies the array in place' do
@@ -302,7 +302,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
end
end
- describe :each do
+ describe '#each' do
context 'when diff are too large' do
let(:collection) do
Gitlab::Git::DiffCollection.new([{ diff: 'a' * 204800 }])
diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb
index 688e2a75373..83d2ff8f9b3 100644
--- a/spec/lib/gitlab/git/tree_spec.rb
+++ b/spec/lib/gitlab/git/tree_spec.rb
@@ -11,7 +11,7 @@ describe Gitlab::Git::Tree, seed_helper: true do
it { expect(tree.select(&:file?).size).to eq(10) }
it { expect(tree.select(&:submodule?).size).to eq(2) }
- describe :dir do
+ describe '#dir?' do
let(:dir) { tree.select(&:dir?).first }
it { expect(dir).to be_kind_of Gitlab::Git::Tree }
@@ -41,7 +41,7 @@ describe Gitlab::Git::Tree, seed_helper: true do
end
end
- describe :file do
+ describe '#file?' do
let(:file) { tree.select(&:file?).first }
it { expect(file).to be_kind_of Gitlab::Git::Tree }
@@ -50,21 +50,21 @@ describe Gitlab::Git::Tree, seed_helper: true do
it { expect(file.name).to eq('.gitignore') }
end
- describe :readme do
+ describe '#readme?' do
let(:file) { tree.select(&:readme?).first }
it { expect(file).to be_kind_of Gitlab::Git::Tree }
it { expect(file.name).to eq('README.md') }
end
- describe :contributing do
+ describe '#contributing?' do
let(:file) { tree.select(&:contributing?).first }
it { expect(file).to be_kind_of Gitlab::Git::Tree }
it { expect(file.name).to eq('CONTRIBUTING.md') }
end
- describe :submodule do
+ describe '#submodule?' do
let(:submodule) { tree.select(&:submodule?).first }
it { expect(submodule).to be_kind_of Gitlab::Git::Tree }
diff --git a/spec/lib/gitlab/git/util_spec.rb b/spec/lib/gitlab/git/util_spec.rb
index 8d43b570e98..bcca4d4c746 100644
--- a/spec/lib/gitlab/git/util_spec.rb
+++ b/spec/lib/gitlab/git/util_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Git::Util do
- describe :count_lines do
+ describe '#count_lines' do
[
["", 0],
["foo", 1],
diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb
index 2f3bd4393b7..346cf0d117c 100644
--- a/spec/lib/gitlab/ldap/user_spec.rb
+++ b/spec/lib/gitlab/ldap/user_spec.rb
@@ -57,7 +57,7 @@ describe Gitlab::LDAP::User, lib: true do
end
end
- describe :find_or_create do
+ describe 'find or create' do
it "finds the user if already existing" do
create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain')
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index e8caad00c44..8acec805584 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -6,6 +6,9 @@ describe SystemHook, models: true do
let(:user) { create(:user) }
let(:project) { create(:empty_project, namespace: user.namespace) }
let(:group) { create(:group) }
+ let(:params) do
+ { name: 'John Doe', username: 'jduser', email: 'jg@example.com', password: 'mydummypass' }
+ end
before do
WebMock.stub_request(:post, system_hook.url)
@@ -29,7 +32,7 @@ describe SystemHook, models: true do
end
it "user_create hook" do
- create(:user)
+ Users::CreateService.new(nil, params).execute
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_create/,
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 3cee2b7714f..f3f48f951a8 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -109,7 +109,7 @@ describe Milestone, models: true do
it { expect(milestone.percent_complete(user)).to eq(75) }
end
- describe :items_count do
+ describe '#is_empty?' do
before do
milestone.issues << create(:issue, project: project)
milestone.issues << create(:closed_issue, project: project)
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 67d48557184..09aa6e9337f 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -163,7 +163,7 @@ describe Namespace, models: true do
end
end
- describe :rm_dir do
+ describe '#rm_dir', 'callback' do
let!(:project) { create(:empty_project, namespace: namespace) }
let!(:path) { File.join(Gitlab.config.repositories.storages.default['path'], namespace.full_path) }
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index e6a4583a8fb..c6c45d78990 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -5,7 +5,7 @@ describe PagesDomain, models: true do
it { is_expected.to belong_to(:project) }
end
- describe :validate_domain do
+ describe 'validate domain' do
subject { build(:pages_domain, domain: domain) }
context 'is unique' do
@@ -75,7 +75,7 @@ describe PagesDomain, models: true do
end
end
- describe :url do
+ describe '#url' do
subject { domain.url }
context 'without the certificate' do
@@ -91,7 +91,7 @@ describe PagesDomain, models: true do
end
end
- describe :has_matching_key? do
+ describe '#has_matching_key?' do
subject { domain.has_matching_key? }
context 'for matching key' do
@@ -107,7 +107,7 @@ describe PagesDomain, models: true do
end
end
- describe :has_intermediates? do
+ describe '#has_intermediates?' do
subject { domain.has_intermediates? }
context 'for self signed' do
@@ -133,7 +133,7 @@ describe PagesDomain, models: true do
end
end
- describe :expired? do
+ describe '#expired?' do
subject { domain.expired? }
context 'for valid' do
@@ -149,7 +149,7 @@ describe PagesDomain, models: true do
end
end
- describe :subject do
+ describe '#subject' do
let(:domain) { build(:pages_domain, :with_certificate) }
subject { domain.subject }
@@ -157,7 +157,7 @@ describe PagesDomain, models: true do
it { is_expected.to eq('/CN=test-certificate') }
end
- describe :certificate_text do
+ describe '#certificate_text' do
let(:domain) { build(:pages_domain, :with_certificate) }
subject { domain.certificate_text }
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 274e4f00a0a..585b87b828d 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1083,7 +1083,7 @@ describe Repository, models: true do
end
end
- describe :skip_merged_commit do
+ describe 'skip_merges option' do
subject { repository.commits(Gitlab::Git::BRANCH_REF_PREFIX + "'test'", limit: 100, skip_merges: true).map{ |k| k.id } }
it { is_expected.not_to include('e56497bb5f03a90a51293fc6d516788730953899') }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 90378179e32..a9e37be1157 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -81,6 +81,7 @@ describe User, models: true do
it { is_expected.to validate_numericality_of(:projects_limit) }
it { is_expected.to allow_value(0).for(:projects_limit) }
it { is_expected.not_to allow_value(-1).for(:projects_limit) }
+ it { is_expected.not_to allow_value(Gitlab::Database::MAX_INT_VALUE + 1).for(:projects_limit) }
it { is_expected.to validate_length_of(:bio).is_at_most(255) }
@@ -360,22 +361,10 @@ describe User, models: true do
end
describe '#generate_password' do
- it "executes callback when force_random_password specified" do
- user = build(:user, force_random_password: true)
- expect(user).to receive(:generate_password)
- user.save
- end
-
it "does not generate password by default" do
user = create(:user, password: 'abcdefghe')
expect(user.password).to eq('abcdefghe')
end
-
- it "generates password when forcing random password" do
- allow(Devise).to receive(:friendly_token).and_return('123456789')
- user = create(:user, password: 'abcdefg', force_random_password: true)
- expect(user.password).to eq('12345678')
- end
end
describe 'authentication token' do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index c481b7e72b1..a3de4702ad0 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -902,7 +902,7 @@ describe API::Projects, :api do
end
end
- describe :fork_admin do
+ describe 'fork management' do
let(:project_fork_target) { create(:empty_project) }
let(:project_fork_source) { create(:empty_project, :public) }
diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb
index d8bb562587d..b1aa793ec00 100644
--- a/spec/requests/api/v3/projects_spec.rb
+++ b/spec/requests/api/v3/projects_spec.rb
@@ -949,7 +949,7 @@ describe API::V3::Projects, api: true do
end
end
- describe :fork_admin do
+ describe 'fork management' do
let(:project_fork_target) { create(:empty_project) }
let(:project_fork_source) { create(:empty_project, :public) }
diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb
index 17bbb0b53c1..b38cbe74b85 100644
--- a/spec/requests/api/v3/users_spec.rb
+++ b/spec/requests/api/v3/users_spec.rb
@@ -263,4 +263,18 @@ describe API::V3::Users, api: true do
expect(json_response['message']).to eq('404 User Not Found')
end
end
+
+ describe 'POST /users' do
+ it 'creates confirmed user when confirm parameter is false' do
+ optional_attributes = { confirm: false }
+ attributes = attributes_for(:user).merge(optional_attributes)
+
+ post v3_api('/users', admin), attributes
+
+ user_id = json_response['id']
+ new_user = User.find(user_id)
+
+ expect(new_user).to be_confirmed
+ end
+ end
end
diff --git a/spec/serializers/build_entity_spec.rb b/spec/serializers/build_entity_spec.rb
index 60c9642ee2c..7dcdf54fd93 100644
--- a/spec/serializers/build_entity_spec.rb
+++ b/spec/serializers/build_entity_spec.rb
@@ -1,10 +1,16 @@
require 'spec_helper'
describe BuildEntity do
+ let(:user) { create(:user) }
let(:build) { create(:ci_build) }
+ let(:request) { double('request') }
+
+ before do
+ allow(request).to receive(:user).and_return(user)
+ end
let(:entity) do
- described_class.new(build, request: double)
+ described_class.new(build, request: request)
end
subject { entity.as_json }
@@ -22,6 +28,11 @@ describe BuildEntity do
expect(subject).to include(:created_at, :updated_at)
end
+ it 'contains details' do
+ expect(subject).to include :status
+ expect(subject[:status]).to include :icon, :favicon, :text, :label
+ end
+
context 'when build is a regular job' do
it 'does not contain path to play action' do
expect(subject).not_to include(:play_path)
diff --git a/spec/serializers/build_serializer_spec.rb b/spec/serializers/build_serializer_spec.rb
new file mode 100644
index 00000000000..3cc791bca50
--- /dev/null
+++ b/spec/serializers/build_serializer_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe BuildSerializer do
+ let(:user) { create(:user) }
+
+ let(:serializer) do
+ described_class.new(user: user)
+ end
+
+ subject { serializer.represent(resource) }
+
+ describe '#represent' do
+ context 'when a single object is being serialized' do
+ let(:resource) { create(:ci_build) }
+
+ it 'serializers the pipeline object' do
+ expect(subject[:id]).to eq resource.id
+ end
+ end
+
+ context 'when multiple objects are being serialized' do
+ let(:resource) { create_list(:ci_build, 2) }
+
+ it 'serializers the array of pipelines' do
+ expect(subject).not_to be_empty
+ end
+ end
+ end
+
+ describe '#represent_status' do
+ context 'when represents only status' do
+ let(:resource) { create(:ci_build) }
+ let(:status) { resource.detailed_status(double('user')) }
+
+ subject { serializer.represent_status(resource) }
+
+ it 'serializes only status' do
+ expect(subject[:text]).to eq(status.text)
+ expect(subject[:label]).to eq(status.label)
+ expect(subject[:icon]).to eq(status.icon)
+ expect(subject[:favicon]).to eq(status.favicon)
+ end
+ end
+ end
+end
diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb
index ea87771e2a2..95eca5463eb 100644
--- a/spec/serializers/deployment_entity_spec.rb
+++ b/spec/serializers/deployment_entity_spec.rb
@@ -1,8 +1,15 @@
require 'spec_helper'
describe DeploymentEntity do
+ let(:user) { create(:user) }
+ let(:request) { double('request') }
+
+ before do
+ allow(request).to receive(:user).and_return(user)
+ end
+
let(:entity) do
- described_class.new(deployment, request: double)
+ described_class.new(deployment, request: request)
end
let(:deployment) { create(:deployment) }
diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb
index ccb72973f9c..93d5a21419d 100644
--- a/spec/serializers/pipeline_entity_spec.rb
+++ b/spec/serializers/pipeline_entity_spec.rb
@@ -30,7 +30,7 @@ describe PipelineEntity do
.to include :duration, :finished_at
expect(subject[:details])
.to include :stages, :artifacts, :manual_actions
- expect(subject[:details][:status]).to include :icon, :text, :label
+ expect(subject[:details][:status]).to include :icon, :favicon, :text, :label
end
it 'contains flags' do
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index 2aaef03cb93..8642b803844 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -94,4 +94,20 @@ describe PipelineSerializer do
end
end
end
+
+ describe '#represent_status' do
+ context 'when represents only status' do
+ let(:resource) { create(:ci_pipeline) }
+ let(:status) { resource.detailed_status(double('user')) }
+
+ subject { serializer.represent_status(resource) }
+
+ it 'serializes only status' do
+ expect(subject[:text]).to eq(status.text)
+ expect(subject[:label]).to eq(status.label)
+ expect(subject[:icon]).to eq(status.icon)
+ expect(subject[:favicon]).to eq(status.favicon)
+ end
+ end
+ end
end
diff --git a/spec/serializers/status_entity_spec.rb b/spec/serializers/status_entity_spec.rb
index 89428b4216e..c94902dbab8 100644
--- a/spec/serializers/status_entity_spec.rb
+++ b/spec/serializers/status_entity_spec.rb
@@ -16,7 +16,7 @@ describe StatusEntity do
subject { entity.as_json }
it 'contains status details' do
- expect(subject).to include :text, :icon, :label, :group
+ expect(subject).to include :text, :icon, :favicon, :label, :group
expect(subject).to include :has_details, :details_path
end
end
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
index 5445b65f4e8..f1b2d3a4798 100644
--- a/spec/services/ci/retry_pipeline_service_spec.rb
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -9,6 +9,19 @@ describe Ci::RetryPipelineService, '#execute', :services do
context 'when user has ability to modify pipeline' do
let(:user) { create(:admin) }
+ context 'when there are already retried jobs present' do
+ before do
+ create_build('rspec', :canceled, 0)
+ create_build('rspec', :failed, 0)
+ end
+
+ it 'does not retry jobs that has already been retried' do
+ expect(statuses.first).to be_retried
+ expect { service.execute(pipeline) }
+ .to change { CommitStatus.count }.by(1)
+ end
+ end
+
context 'when there are failed builds in the last stage' do
before do
create_build('rspec 1', :success, 0)
diff --git a/spec/services/milestones/close_service_spec.rb b/spec/services/milestones/close_service_spec.rb
index 92b84308f73..fe6a19e97ea 100644
--- a/spec/services/milestones/close_service_spec.rb
+++ b/spec/services/milestones/close_service_spec.rb
@@ -17,7 +17,7 @@ describe Milestones::CloseService, services: true do
it { expect(milestone).to be_valid }
it { expect(milestone).to be_closed }
- describe :event do
+ describe 'event' do
let(:event) { Event.recent.first }
it { expect(event.milestone).to be_truthy }
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index 8e614211116..e3be1989c93 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Projects::ForkService, services: true do
- describe :fork_by_user do
+ describe 'fork by user' do
before do
@from_namespace = create(:namespace)
@from_user = create(:user, namespace: @from_namespace )
@@ -100,7 +100,7 @@ describe Projects::ForkService, services: true do
end
end
- describe :fork_to_namespace do
+ describe 'fork to namespace' do
before do
@group_owner = create(:user)
@developer = create(:user)
diff --git a/spec/services/users/create_service_spec.rb b/spec/services/users/create_service_spec.rb
new file mode 100644
index 00000000000..5f79203701a
--- /dev/null
+++ b/spec/services/users/create_service_spec.rb
@@ -0,0 +1,182 @@
+require 'spec_helper'
+
+describe Users::CreateService, services: true do
+ describe '#build' do
+ let(:params) do
+ { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass' }
+ end
+
+ context 'with an admin user' do
+ let(:admin_user) { create(:admin) }
+ let(:service) { described_class.new(admin_user, params) }
+
+ it 'returns a valid user' do
+ expect(service.build).to be_valid
+ end
+ end
+
+ context 'with non admin user' do
+ let(:user) { create(:user) }
+ let(:service) { described_class.new(user, params) }
+
+ it 'raises AccessDeniedError exception' do
+ expect { service.build }.to raise_error Gitlab::Access::AccessDeniedError
+ end
+ end
+
+ context 'with nil user' do
+ let(:service) { described_class.new(nil, params) }
+
+ it 'returns a valid user' do
+ expect(service.build).to be_valid
+ end
+ end
+ end
+
+ describe '#execute' do
+ let(:admin_user) { create(:admin) }
+
+ context 'with an admin user' do
+ let(:service) { described_class.new(admin_user, params) }
+
+ context 'when required parameters are provided' do
+ let(:params) do
+ { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass' }
+ end
+
+ it 'returns a persisted user' do
+ expect(service.execute).to be_persisted
+ end
+
+ it 'persists the given attributes' do
+ user = service.execute
+ user.reload
+
+ expect(user).to have_attributes(
+ name: params[:name],
+ username: params[:username],
+ email: params[:email],
+ password: params[:password],
+ created_by_id: admin_user.id
+ )
+ end
+
+ it 'user is not confirmed if skip_confirmation param is not present' do
+ expect(service.execute).not_to be_confirmed
+ end
+
+ it 'logs the user creation' do
+ expect(service).to receive(:log_info).with("User \"John Doe\" (jd@example.com) was created")
+
+ service.execute
+ end
+
+ it 'executes system hooks ' do
+ system_hook_service = spy(:system_hook_service)
+
+ expect(service).to receive(:system_hook_service).and_return(system_hook_service)
+
+ user = service.execute
+
+ expect(system_hook_service).to have_received(:execute_hooks_for).with(user, :create)
+ end
+
+ it 'does not send a notification email' do
+ notification_service = spy(:notification_service)
+
+ expect(service).not_to receive(:notification_service)
+
+ service.execute
+
+ expect(notification_service).not_to have_received(:new_user)
+ end
+ end
+
+ context 'when force_random_password parameter is true' do
+ let(:params) do
+ { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', force_random_password: true }
+ end
+
+ it 'generates random password' do
+ user = service.execute
+
+ expect(user.password).not_to eq 'mydummypass'
+ expect(user.password).to be_present
+ end
+ end
+
+ context 'when skip_confirmation parameter is true' do
+ let(:params) do
+ { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', skip_confirmation: true }
+ end
+
+ it 'confirms the user' do
+ expect(service.execute).to be_confirmed
+ end
+ end
+
+ context 'when reset_password parameter is true' do
+ let(:params) do
+ { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', reset_password: true }
+ end
+
+ it 'resets password even if a password parameter is given' do
+ expect(service.execute).to be_recently_sent_password_reset
+ end
+
+ it 'sends a notification email' do
+ notification_service = spy(:notification_service)
+
+ expect(service).to receive(:notification_service).and_return(notification_service)
+
+ user = service.execute
+
+ expect(notification_service).to have_received(:new_user).with(user, an_instance_of(String))
+ end
+ end
+ end
+
+ context 'with nil user' do
+ let(:params) do
+ { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', skip_confirmation: true }
+ end
+ let(:service) { described_class.new(nil, params) }
+
+ context 'when "send_user_confirmation_email" application setting is true' do
+ before do
+ current_application_settings = double(:current_application_settings, send_user_confirmation_email: true, signup_enabled?: true)
+ allow(service).to receive(:current_application_settings).and_return(current_application_settings)
+ end
+
+ it 'does not confirm the user' do
+ expect(service.execute).not_to be_confirmed
+ end
+ end
+
+ context 'when "send_user_confirmation_email" application setting is false' do
+ before do
+ current_application_settings = double(:current_application_settings, send_user_confirmation_email: false, signup_enabled?: true)
+ allow(service).to receive(:current_application_settings).and_return(current_application_settings)
+ end
+
+ it 'confirms the user' do
+ expect(service.execute).to be_confirmed
+ end
+
+ it 'persists the given attributes' do
+ user = service.execute
+ user.reload
+
+ expect(user).to have_attributes(
+ name: params[:name],
+ username: params[:username],
+ email: params[:email],
+ password: params[:password],
+ created_by_id: nil,
+ admin: false
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index f1d226b6ae3..648b0380f18 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -37,9 +37,10 @@ module TestEnv
'conflict-too-large' => '39fa04f',
'deleted-image-test' => '6c17798',
'wip' => 'b9238ee',
- 'csv' => '3dd0896'
+ 'csv' => '3dd0896',
+ 'v1.1.0' => 'b83d6e3'
}.freeze
-
+
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
# need to keep all the branches in sync.
# We currently only need a subset of the branches
diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb
index c101f6f164d..e4aeaeca508 100644
--- a/spec/views/projects/pipelines/show.html.haml_spec.rb
+++ b/spec/views/projects/pipelines/show.html.haml_spec.rb
@@ -3,8 +3,9 @@ require 'spec_helper'
describe 'projects/pipelines/show' do
include Devise::Test::ControllerHelpers
+ let(:user) { create(:user) }
let(:project) { create(:project) }
- let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, user: user) }
before do
controller.prepend_view_path('app/views/projects')
@@ -21,6 +22,7 @@ describe 'projects/pipelines/show' do
assign(:project, project)
assign(:pipeline, pipeline)
+ assign(:commit, project.commit)
allow(view).to receive(:can?).and_return(true)
end
@@ -31,6 +33,12 @@ describe 'projects/pipelines/show' do
expect(rendered).to have_css('.js-pipeline-graph')
expect(rendered).to have_css('.js-grouped-pipeline-dropdown')
+ # header
+ expect(rendered).to have_text("##{pipeline.id}")
+ expect(rendered).to have_css('time', text: pipeline.created_at.strftime("%b %d, %Y"))
+ expect(rendered).to have_selector(%Q(img[alt$="#{pipeline.user.name}'s avatar"]))
+ expect(rendered).to have_link(pipeline.user.name, href: user_path(pipeline.user))
+
# stages
expect(rendered).to have_text('Build')
expect(rendered).to have_text('Test')
diff --git a/yarn.lock b/yarn.lock
index fb3bfce4d11..f254668646c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4450,6 +4450,10 @@ verror@1.3.6:
dependencies:
extsprintf "1.0.2"
+visibilityjs@^1.2.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/visibilityjs/-/visibilityjs-1.2.4.tgz#bff8663da62c8c10ad4ee5ae6a1ae6fac4259d63"
+
vm-browserify@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73"