summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop.yml2
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile18
-rw-r--r--Gemfile.lock62
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js2
-rw-r--r--app/assets/javascripts/blob/create_branch_dropdown.js88
-rw-r--r--app/assets/javascripts/blob/target_branch_dropdown.js152
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js3
-rw-r--r--app/assets/javascripts/build.js85
-rw-r--r--app/assets/javascripts/commons/polyfills.js1
-rw-r--r--app/assets/javascripts/dispatcher.js20
-rw-r--r--app/assets/javascripts/filterable_list.js82
-rw-r--r--app/assets/javascripts/gl_dropdown.js18
-rw-r--r--app/assets/javascripts/groups/components/group_folder.vue27
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue220
-rw-r--r--app/assets/javascripts/groups/components/groups.vue39
-rw-r--r--app/assets/javascripts/groups/event_hub.js3
-rw-r--r--app/assets/javascripts/groups/groups_filterable_list.js87
-rw-r--r--app/assets/javascripts/groups/index.js190
-rw-r--r--app/assets/javascripts/groups/services/groups_service.js38
-rw-r--r--app/assets/javascripts/groups/stores/groups_store.js151
-rw-r--r--app/assets/javascripts/jobs/components/header.vue83
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_detail_row.vue31
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_details_block.vue150
-rw-r--r--app/assets/javascripts/jobs/job_details_bundle.js68
-rw-r--r--app/assets/javascripts/jobs/job_details_mediator.js67
-rw-r--r--app/assets/javascripts/jobs/services/job_service.js14
-rw-r--r--app/assets/javascripts/jobs/stores/job_store.js11
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js4
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js21
-rw-r--r--app/assets/javascripts/locale/bg/app.js1
-rw-r--r--app/assets/javascripts/locale/de/app.js2
-rw-r--r--app/assets/javascripts/locale/en/app.js2
-rw-r--r--app/assets/javascripts/locale/pt_BR/app.js1
-rw-r--r--app/assets/javascripts/locale/zh_CN/app.js2
-rw-r--r--app/assets/javascripts/locale/zh_HK/app.js2
-rw-r--r--app/assets/javascripts/locale/zh_TW/app.js2
-rw-r--r--app/assets/javascripts/main.js4
-rw-r--r--app/assets/javascripts/new_commit_form.js11
-rw-r--r--app/assets/javascripts/notes.js4
-rw-r--r--app/assets/javascripts/peek.js16
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/nav_controls.js52
-rw-r--r--app/assets/javascripts/pipelines/components/nav_controls.vue54
-rw-r--r--app/assets/javascripts/pipelines/components/navigation_tabs.js72
-rw-r--r--app/assets/javascripts/pipelines/components/navigation_tabs.vue76
-rw-r--r--app/assets/javascripts/pipelines/pipelines.js6
-rw-r--r--app/assets/javascripts/shortcuts.js14
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue32
-rw-r--r--app/assets/stylesheets/framework/awards.scss8
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss5
-rw-r--r--app/assets/stylesheets/framework/lists.scss100
-rw-r--r--app/assets/stylesheets/pages/builds.scss131
-rw-r--r--app/assets/stylesheets/pages/issuable.scss3
-rw-r--r--app/assets/stylesheets/pages/issues.scss19
-rw-r--r--app/assets/stylesheets/pages/login.scss2
-rw-r--r--app/assets/stylesheets/pages/members.scss35
-rw-r--r--app/assets/stylesheets/pages/notes.scss1
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss20
-rw-r--r--app/assets/stylesheets/pages/projects.scss3
-rw-r--r--app/assets/stylesheets/pages/status.scss10
-rw-r--r--app/controllers/application_controller.rb18
-rw-r--r--app/controllers/autocomplete_controller.rb2
-rw-r--r--app/controllers/concerns/creates_commit.rb5
-rw-r--r--app/controllers/concerns/issues_action.rb2
-rw-r--r--app/controllers/concerns/membership_actions.rb9
-rw-r--r--app/controllers/concerns/milestone_actions.rb4
-rw-r--r--app/controllers/concerns/spammable_actions.rb10
-rw-r--r--app/controllers/dashboard/groups_controller.rb28
-rw-r--r--app/controllers/dashboard/milestones_controller.rb4
-rw-r--r--app/controllers/dashboard/projects_controller.rb2
-rw-r--r--app/controllers/dashboard/todos_controller.rb5
-rw-r--r--app/controllers/groups_controller.rb2
-rw-r--r--app/controllers/jwt_controller.rb2
-rw-r--r--app/controllers/projects/blob_controller.rb16
-rw-r--r--app/controllers/projects/branches_controller.rb7
-rw-r--r--app/controllers/projects/commits_controller.rb2
-rw-r--r--app/controllers/projects/git_http_client_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb2
-rw-r--r--app/controllers/projects/labels_controller.rb6
-rw-r--r--app/controllers/projects/pipeline_schedules_controller.rb4
-rw-r--r--app/controllers/projects/tree_controller.rb1
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/controllers/users_controller.rb2
-rw-r--r--app/helpers/form_helper.rb2
-rw-r--r--app/helpers/groups_helper.rb2
-rw-r--r--app/helpers/milestones_helper.rb6
-rw-r--r--app/helpers/nav_helper.rb3
-rw-r--r--app/helpers/todos_helper.rb2
-rw-r--r--app/helpers/u2f_helper.rb2
-rw-r--r--app/models/blob.rb17
-rw-r--r--app/models/blob_viewer/base.rb16
-rw-r--r--app/models/blob_viewer/empty.rb1
-rw-r--r--app/models/blob_viewer/server_side.rb4
-rw-r--r--app/models/commit.rb27
-rw-r--r--app/models/commit_status.rb7
-rw-r--r--app/models/project.rb37
-rw-r--r--app/models/repository.rb2
-rw-r--r--app/policies/project_snippet_policy.rb5
-rw-r--r--app/presenters/merge_request_presenter.rb16
-rw-r--r--app/serializers/build_details_entity.rb6
-rw-r--r--app/serializers/build_entity.rb14
-rw-r--r--app/serializers/group_entity.rb46
-rw-r--r--app/serializers/group_serializer.rb19
-rw-r--r--app/services/ci/create_pipeline_service.rb6
-rw-r--r--app/services/issuable_base_service.rb12
-rw-r--r--app/uploaders/artifact_uploader.rb4
-rw-r--r--app/uploaders/file_uploader.rb7
-rw-r--r--app/uploaders/gitlab_uploader.rb43
-rw-r--r--app/uploaders/lfs_object_uploader.rb12
-rw-r--r--app/views/dashboard/groups/_groups.html.haml13
-rw-r--r--app/views/dashboard/groups/index.html.haml5
-rw-r--r--app/views/dashboard/issues.atom.builder15
-rw-r--r--app/views/dashboard/projects/index.atom.builder15
-rw-r--r--app/views/groups/issues.atom.builder15
-rw-r--r--app/views/groups/show.atom.builder15
-rw-r--r--app/views/help/_shortcuts.html.haml4
-rw-r--r--app/views/help/index.html.haml2
-rw-r--r--app/views/layouts/_head.html.haml2
-rw-r--r--app/views/layouts/application.html.haml1
-rw-r--r--app/views/layouts/xml.atom.builder4
-rw-r--r--app/views/peek/views/_mysql2.html.haml4
-rw-r--r--app/views/peek/views/_pg.html.haml4
-rw-r--r--app/views/peek/views/_sql.html.haml13
-rw-r--r--app/views/projects/blob/_remove.html.haml4
-rw-r--r--app/views/projects/blob/_viewer.html.haml14
-rw-r--r--app/views/projects/commit/_change.html.haml14
-rw-r--r--app/views/projects/commits/show.atom.builder15
-rw-r--r--app/views/projects/diffs/_content.html.haml2
-rw-r--r--app/views/projects/diffs/viewers/_text.html.haml2
-rw-r--r--app/views/projects/group_links/_index.html.haml53
-rw-r--r--app/views/projects/issues/index.atom.builder15
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml80
-rw-r--r--app/views/projects/jobs/show.html.haml77
-rw-r--r--app/views/projects/pipeline_schedules/_form.html.haml20
-rw-r--r--app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml12
-rw-r--r--app/views/projects/pipeline_schedules/_table.html.haml10
-rw-r--r--app/views/projects/pipeline_schedules/_tabs.html.haml6
-rw-r--r--app/views/projects/pipeline_schedules/edit.html.haml4
-rw-r--r--app/views/projects/pipeline_schedules/index.html.haml4
-rw-r--r--app/views/projects/pipeline_schedules/new.html.haml4
-rw-r--r--app/views/projects/pipelines/_head.html.haml2
-rw-r--r--app/views/projects/project_members/_index.html.haml20
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml37
-rw-r--r--app/views/projects/project_members/_new_shared_group.html.haml20
-rw-r--r--app/views/projects/settings/members/show.html.haml3
-rw-r--r--app/views/projects/show.atom.builder15
-rw-r--r--app/views/shared/_branch_switcher.html.haml8
-rw-r--r--app/views/shared/_label.html.haml2
-rw-r--r--app/views/shared/_new_commit_form.html.haml2
-rw-r--r--app/views/shared/groups/_dropdown.html.haml12
-rw-r--r--app/views/shared/projects/blob/_branch_page_create.html.haml8
-rw-r--r--app/views/shared/projects/blob/_branch_page_default.html.haml10
-rw-r--r--app/views/users/show.atom.builder15
-rw-r--r--app/workers/background_migration_worker.rb23
-rw-r--r--app/workers/post_receive.rb38
-rw-r--r--changelogs/unreleased/12614-fix-long-message.yml4
-rw-r--r--changelogs/unreleased/25426-group-dashboard-ui.yml4
-rw-r--r--changelogs/unreleased/28607-forking-and-configuring-project-via-api-works-very-unreliable.yml4
-rw-r--r--changelogs/unreleased/29010-perf-bar.yml4
-rw-r--r--changelogs/unreleased/31397-job-detail-real-time.yml4
-rw-r--r--changelogs/unreleased/32517-disable-hover-state.yml5
-rw-r--r--changelogs/unreleased/32720-emoji-spacing.yml4
-rw-r--r--changelogs/unreleased/32834-task-note-only.yml4
-rw-r--r--changelogs/unreleased/33003-avatar-in-project-api.yml4
-rw-r--r--changelogs/unreleased/33132-change-icon-color.yml4
-rw-r--r--changelogs/unreleased/33208-singup-active-state-underline.yml4
-rw-r--r--changelogs/unreleased/33334-portuguese_brazil_translation_of_cycle_analytics_page.yml4
-rw-r--r--changelogs/unreleased/33381-display-issue-state-in-mr-widget-issue-links.yml4
-rw-r--r--changelogs/unreleased/33383-bulgarian_translation_of_cycle_analytics_page.yml4
-rw-r--r--changelogs/unreleased/allow-reporters-to-promote-group-labels.yml4
-rw-r--r--changelogs/unreleased/artifacts-keyboard-shortcuts.yml4
-rw-r--r--changelogs/unreleased/ce-31853-projects-shared-groups.yml4
-rw-r--r--changelogs/unreleased/dashboard-milestone-tabs-loading-async.yml4
-rw-r--r--changelogs/unreleased/dm-blob-binaryness-change.yml5
-rw-r--r--changelogs/unreleased/dm-mail-room-check-without-omnibus.yml4
-rw-r--r--changelogs/unreleased/dm-revert-mr-8427.yml4
-rw-r--r--changelogs/unreleased/fix-u2f-for-opera.yml4
-rw-r--r--changelogs/unreleased/fix_docs_commits_multiple_files.yml5
-rw-r--r--changelogs/unreleased/issuable-sidebar-edit-button-field-focus.yml4
-rw-r--r--changelogs/unreleased/pat-msg-on-auth-failure.yml4
-rw-r--r--changelogs/unreleased/sh-bump-oauth2-gem.yml4
-rw-r--r--changelogs/unreleased/sh-fix-refactor-uploader-work-dir.yml4
-rw-r--r--changelogs/unreleased/sh-recaptcha-fix-try2.yml4
-rw-r--r--changelogs/unreleased/tc-link-to-commit-on-help-page.yml4
-rw-r--r--changelogs/unreleased/zj-i18n-pipeline-schedules.yml4
-rw-r--r--changelogs/unreleased/zj-prom-pipeline-count.yml4
-rw-r--r--config/application.rb1
-rw-r--r--config/boot.rb12
-rw-r--r--config/initializers/peek.rb32
-rw-r--r--config/locales/de.yml37
-rw-r--r--config/locales/es.yml2
-rw-r--r--config/routes.rb1
-rw-r--r--config/routes/dashboard.rb8
-rw-r--r--config/routes/uploads.rb4
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--config/webpack.config.js16
-rw-r--r--db/migrate/20170316163800_rename_system_namespaces.rb231
-rw-r--r--db/migrate/20170316163845_move_uploads_to_system_dir.rb59
-rw-r--r--db/post_migrate/20170317162059_update_upload_paths_to_system.rb55
-rw-r--r--db/post_migrate/20170406111121_clean_upload_symlinks.rb52
-rw-r--r--db/post_migrate/20170606202615_move_appearance_to_system_dir.rb57
-rw-r--r--db/schema.rb3
-rw-r--r--doc/api/README.md13
-rw-r--r--doc/api/commits.md2
-rw-r--r--doc/api/projects.md13
-rw-r--r--doc/ci/runners/README.md214
-rw-r--r--doc/ci/runners/img/shared_runners_admin.pngbin0 -> 29192 bytes
-rw-r--r--doc/ci/runners/project_specific.pngbin30196 -> 0 bytes
-rw-r--r--doc/ci/runners/shared_runner.pngbin17797 -> 0 bytes
-rw-r--r--doc/ci/triggers/README.md160
-rw-r--r--doc/ci/triggers/img/triggers_page.pngbin110560 -> 20857 bytes
-rw-r--r--doc/ci/variables/README.md1
-rw-r--r--doc/development/README.md1
-rw-r--r--doc/development/background_migrations.md205
-rw-r--r--doc/development/i18n_guide.md8
-rw-r--r--doc/install/requirements.md3
-rw-r--r--doc/integration/google.md15
-rw-r--r--doc/university/glossary/README.md3
-rw-r--r--doc/update/9.1-to-9.2.md4
-rw-r--r--doc/user/project/container_registry.md8
-rw-r--r--doc/user/project/img/protected_branches_delete.pngbin0 -> 21510 bytes
-rw-r--r--doc/user/project/pipelines/schedules.md20
-rw-r--r--doc/user/project/protected_branches.md27
-rw-r--r--doc/workflow/groups.md5
-rw-r--r--doc/workflow/shortcuts.md3
-rw-r--r--features/project/builds/permissions.feature1
-rw-r--r--features/project/builds/summary.feature3
-rw-r--r--features/steps/groups.rb2
-rw-r--r--features/steps/profile/profile.rb2
-rw-r--r--features/steps/project/builds/summary.rb2
-rw-r--r--features/steps/project/project.rb2
-rw-r--r--features/steps/project/project_group_links.rb7
-rw-r--r--features/steps/project/source/browse_files.rb5
-rw-r--r--features/steps/project/source/markdown_render.rb2
-rw-r--r--features/support/capybara.rb4
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/files.rb2
-rw-r--r--lib/api/projects.rb1
-rw-r--r--lib/api/v3/files.rb2
-rw-r--r--lib/banzai/reference_parser/base_parser.rb4
-rw-r--r--lib/banzai/reference_parser/commit_parser.rb2
-rw-r--r--lib/banzai/reference_parser/commit_range_parser.rb2
-rw-r--r--lib/banzai/reference_parser/external_issue_parser.rb2
-rw-r--r--lib/banzai/reference_parser/label_parser.rb2
-rw-r--r--lib/banzai/reference_parser/merge_request_parser.rb4
-rw-r--r--lib/banzai/reference_parser/milestone_parser.rb2
-rw-r--r--lib/banzai/reference_parser/snippet_parser.rb4
-rw-r--r--lib/banzai/reference_parser/user_parser.rb2
-rw-r--r--lib/gitlab.rb4
-rw-r--r--lib/gitlab/auth.rb6
-rw-r--r--lib/gitlab/background_migration.rb31
-rw-r--r--lib/gitlab/background_migration/.gitkeep0
-rw-r--r--lib/gitlab/blame.rb2
-rw-r--r--lib/gitlab/diff/file.rb50
-rw-r--r--lib/gitlab/diff/file_collection/merge_request_diff.rb5
-rw-r--r--lib/gitlab/diff/highlight.rb17
-rw-r--r--lib/gitlab/ee_compat_check.rb10
-rw-r--r--lib/gitlab/git/blob.rb1
-rw-r--r--lib/gitlab/git/diff.rb20
-rw-r--r--lib/gitlab/git/repository.rb5
-rw-r--r--lib/gitlab/highlight.rb8
-rw-r--r--lib/gitlab/i18n.rb4
-rw-r--r--lib/gitlab/path_regex.rb1
-rw-r--r--lib/gitlab/performance_bar.rb7
-rw-r--r--lib/gitlab/performance_bar/peek_performance_bar_with_rack_body.rb22
-rw-r--r--lib/gitlab/performance_bar/peek_query_tracker.rb39
-rw-r--r--lib/gitlab/uploads_transfer.rb2
-rw-r--r--lib/peek/rblineprof/custom_controller_helpers.rb96
-rw-r--r--lib/tasks/gitlab/check.rake9
-rw-r--r--locale/bg/gitlab.po260
-rw-r--r--locale/bg/gitlab.po.time_stamp0
-rw-r--r--locale/de/gitlab.po596
-rw-r--r--locale/en/gitlab.po594
-rw-r--r--locale/gitlab.pot592
-rw-r--r--locale/pt_BR/gitlab.po260
-rw-r--r--locale/pt_BR/gitlab.po.time_stamp0
-rw-r--r--locale/zh_CN/gitlab.po144
-rw-r--r--locale/zh_HK/gitlab.po22
-rw-r--r--locale/zh_HK/gitlab.po.time_stamp1
-rw-r--r--locale/zh_TW/gitlab.po148
-rwxr-xr-xscripts/static-analysis2
-rwxr-xr-xscripts/trigger-build9
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb30
-rw-r--r--spec/controllers/dashboard/milestones_controller_spec.rb38
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb7
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb14
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb1
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb11
-rw-r--r--spec/controllers/projects/labels_controller_spec.rb6
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb4
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb9
-rw-r--r--spec/factories/uploads.rb8
-rw-r--r--spec/features/admin/admin_appearance_spec.rb4
-rw-r--r--spec/features/dashboard/groups_list_spec.rb122
-rw-r--r--spec/features/dashboard/milestone_tabs_spec.rb40
-rw-r--r--spec/features/help_pages_spec.rb16
-rw-r--r--spec/features/projects/artifacts/file_spec.rb1
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb37
-rw-r--r--spec/features/projects/blobs/edit_spec.rb6
-rw-r--r--spec/features/projects/blobs/user_create_spec.rb94
-rw-r--r--spec/features/projects/group_links_spec.rb9
-rw-r--r--spec/features/projects/jobs_spec.rb115
-rw-r--r--spec/features/projects/user_create_dir_spec.rb16
-rw-r--r--spec/features/projects/wiki/markdown_preview_spec.rb3
-rw-r--r--spec/features/todos/todos_spec.rb23
-rw-r--r--spec/features/uploads/user_uploads_avatar_to_group_spec.rb2
-rw-r--r--spec/features/uploads/user_uploads_avatar_to_profile_spec.rb2
-rw-r--r--spec/features/user_can_display_performance_bar_spec.rb81
-rw-r--r--spec/helpers/application_helper_spec.rb13
-rw-r--r--spec/helpers/emails_helper_spec.rb2
-rw-r--r--spec/helpers/groups_helper_spec.rb2
-rw-r--r--spec/helpers/page_layout_helper_spec.rb2
-rw-r--r--spec/helpers/todos_helper_spec.rb13
-rw-r--r--spec/helpers/u2f_helper_spec.rb49
-rw-r--r--spec/javascripts/blob/create_branch_dropdown_spec.js106
-rw-r--r--spec/javascripts/blob/target_branch_dropdown_spec.js118
-rw-r--r--spec/javascripts/build_spec.js17
-rw-r--r--spec/javascripts/datetime_utility_spec.js11
-rw-r--r--spec/javascripts/fixtures/project_branches.json5
-rw-r--r--spec/javascripts/fixtures/target_branch_dropdown.html.haml28
-rw-r--r--spec/javascripts/gl_dropdown_spec.js4
-rw-r--r--spec/javascripts/groups/group_item_spec.js102
-rw-r--r--spec/javascripts/groups/groups_spec.js64
-rw-r--r--spec/javascripts/groups/mock_data.js110
-rw-r--r--spec/javascripts/jobs/header_spec.js63
-rw-r--r--spec/javascripts/jobs/job_details_mediator_spec.js43
-rw-r--r--spec/javascripts/jobs/job_store_spec.js26
-rw-r--r--spec/javascripts/jobs/mock_data.js123
-rw-r--r--spec/javascripts/jobs/sidebar_detail_row_spec.js40
-rw-r--r--spec/javascripts/jobs/sidebar_details_block_spec.js111
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js8
-rw-r--r--spec/javascripts/notes_spec.js39
-rw-r--r--spec/javascripts/pipelines/nav_controls_spec.js2
-rw-r--r--spec/javascripts/test_bundle.js1
-rw-r--r--spec/javascripts/vue_shared/components/commit_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/header_ci_component_spec.js5
-rw-r--r--spec/lib/banzai/filter/abstract_reference_filter_spec.rb11
-rw-r--r--spec/lib/banzai/issuable_extractor_spec.rb11
-rw-r--r--spec/lib/banzai/reference_parser/base_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/snippet_parser_spec.rb189
-rw-r--r--spec/lib/banzai/reference_parser/user_parser_spec.rb11
-rw-r--r--spec/lib/gitlab/auth_spec.rb6
-rw-r--r--spec/lib/gitlab/background_migration_spec.rb48
-rw-r--r--spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb12
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb28
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb41
-rw-r--r--spec/lib/gitlab/highlight_spec.rb37
-rw-r--r--spec/lib/gitlab/uploads_transfer_spec.rb11
-rw-r--r--spec/migrations/clean_upload_symlinks_spec.rb46
-rw-r--r--spec/migrations/move_uploads_to_system_dir_spec.rb68
-rw-r--r--spec/migrations/rename_system_namespaces_spec.rb254
-rw-r--r--spec/migrations/update_upload_paths_to_system_spec.rb53
-rw-r--r--spec/models/blob_spec.rb8
-rw-r--r--spec/models/blob_viewer/base_spec.rb4
-rw-r--r--spec/models/commit_spec.rb28
-rw-r--r--spec/models/concerns/routable_spec.rb11
-rw-r--r--spec/models/group_spec.rb2
-rw-r--r--spec/models/namespace_spec.rb10
-rw-r--r--spec/models/project_spec.rb2
-rw-r--r--spec/models/project_team_spec.rb11
-rw-r--r--spec/models/user_spec.rb2
-rw-r--r--spec/policies/project_snippet_policy_spec.rb4
-rw-r--r--spec/presenters/merge_request_presenter_spec.rb10
-rw-r--r--spec/requests/api/projects_spec.rb9
-rw-r--r--spec/requests/api/runner_spec.rb25
-rw-r--r--spec/requests/git_http_spec.rb45
-rw-r--r--spec/requests/jwt_controller_spec.rb21
-rw-r--r--spec/requests/openid_connect_spec.rb2
-rw-r--r--spec/serializers/build_entity_spec.rb32
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb11
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb8
-rw-r--r--spec/services/issues/update_service_spec.rb27
-rw-r--r--spec/services/merge_requests/merge_request_diff_cache_service_spec.rb4
-rw-r--r--spec/services/merge_requests/update_service_spec.rb13
-rw-r--r--spec/services/projects/participants_service_spec.rb4
-rw-r--r--spec/services/system_note_service_spec.rb2
-rw-r--r--spec/spec_helper.rb14
-rw-r--r--spec/support/capybara.rb4
-rw-r--r--spec/support/milestone_tabs_examples.rb14
-rw-r--r--spec/support/target_branch_helpers.rb16
-rw-r--r--spec/unicorn/unicorn_spec.rb4
-rw-r--r--spec/uploaders/artifact_uploader_spec.rb31
-rw-r--r--spec/uploaders/attachment_uploader_spec.rb11
-rw-r--r--spec/uploaders/avatar_uploader_spec.rb11
-rw-r--r--spec/uploaders/file_uploader_spec.rb10
-rw-r--r--spec/uploaders/gitlab_uploader_spec.rb15
-rw-r--r--spec/uploaders/lfs_object_uploader_spec.rb39
-rw-r--r--spec/views/help/index.html.haml_spec.rb2
-rw-r--r--spec/views/projects/jobs/show.html.haml_spec.rb79
-rw-r--r--spec/workers/background_migration_worker_spec.rb13
-rw-r--r--spec/workers/post_receive_spec.rb29
-rw-r--r--vendor/assets/javascripts/peek.js78
-rw-r--r--vendor/assets/javascripts/peek.performance_bar.js182
-rw-r--r--vendor/assets/stylesheets/peek.scss94
396 files changed, 7601 insertions, 3959 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 8f611a96702..66a40f2cf57 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -397,7 +397,7 @@ Style/ParenthesesAroundCondition:
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: short, verbose
Style/PreferredHashMethods:
- Enabled: true
+ Enabled: false
# Checks for an obsolete RuntimeException argument in raise/fail.
Style/RedundantException:
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index d9df1bbc0c7..bc859cbd6d9 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.11.0
+0.11.2
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 227cea21564..3e3c2f1e5ed 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-2.0.0
+2.1.1
diff --git a/Gemfile b/Gemfile
index e197f53d9b5..2c200f2fa7a 100644
--- a/Gemfile
+++ b/Gemfile
@@ -2,6 +2,7 @@ source 'https://rubygems.org'
gem 'rails', '4.2.8'
gem 'rails-deprecated_sanitizer', '~> 1.0.3'
+gem 'bootsnap', '~> 1.0.0'
# Responders respond_to and respond_with
gem 'responders', '~> 2.0'
@@ -17,7 +18,7 @@ gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.25.1.1'
-gem 'faraday', '~> 0.11.0'
+gem 'faraday', '~> 0.12'
# Authentication libraries
gem 'devise', '~> 4.2'
@@ -258,11 +259,22 @@ gem 'sentry-raven', '~> 2.4.0'
gem 'premailer-rails', '~> 1.9.0'
# I18n
-gem 'ruby_parser', '~> 3.8.4', require: false
+gem 'ruby_parser', '~> 3.8', require: false
gem 'gettext_i18n_rails', '~> 1.8.0'
gem 'gettext_i18n_rails_js', '~> 1.2.0'
gem 'gettext', '~> 3.2.2', require: false, group: :development
+# Perf bar
+gem 'peek', '~> 1.0.1'
+gem 'peek-gc', '~> 0.0.2'
+gem 'peek-host', '~> 1.0.0'
+gem 'peek-mysql2', '~> 1.1.0', group: :mysql
+gem 'peek-performance_bar', '~> 1.2.1'
+gem 'peek-pg', '~> 1.3.0', group: :postgres
+gem 'peek-rblineprof', '~> 0.2.0'
+gem 'peek-redis', '~> 1.2.0'
+gem 'peek-sidekiq', '~> 1.0.3'
+
# Metrics
group :metrics do
gem 'allocations', '~> 1.0', require: false, platform: :mri
@@ -358,7 +370,7 @@ gem 'html2text'
gem 'ruby-prof', '~> 0.16.2'
# OAuth
-gem 'oauth2', '~> 1.3.0'
+gem 'oauth2', '~> 1.4'
# Soft deletion
gem 'paranoia', '~> 2.2'
diff --git a/Gemfile.lock b/Gemfile.lock
index b5f9c3beca7..6755c75e331 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -56,6 +56,7 @@ GEM
asciidoctor-plantuml (0.0.7)
asciidoctor (~> 1.5)
ast (2.3.0)
+ atomic (1.1.99)
attr_encrypted (3.0.3)
encryptor (~> 3.0.0)
attr_required (1.0.0)
@@ -82,6 +83,8 @@ GEM
bindata (2.3.5)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
+ bootsnap (1.0.0)
+ msgpack (~> 1.0)
bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
@@ -129,6 +132,8 @@ GEM
coffee-script-source (1.10.0)
colorize (0.7.7)
concurrent-ruby (1.0.5)
+ concurrent-ruby-ext (1.0.5)
+ concurrent-ruby (= 1.0.5)
connection_pool (2.2.1)
crack (0.4.3)
safe_yaml (~> 1.0.0)
@@ -191,7 +196,7 @@ GEM
factory_girl_rails (4.7.0)
factory_girl (~> 4.7.0)
railties (>= 3.0.0)
- faraday (0.11.0)
+ faraday (0.12.1)
multipart-post (>= 1.2, < 3)
faraday_middleware (0.11.0.1)
faraday (>= 0.7.4, < 1.0)
@@ -459,6 +464,7 @@ GEM
minitest (5.7.0)
mmap2 (2.2.6)
mousetrap-rails (1.4.6)
+ msgpack (1.1.0)
multi_json (1.12.1)
multi_xml (0.6.0)
multipart-post (2.0.0)
@@ -474,8 +480,8 @@ GEM
mini_portile2 (~> 2.1.0)
numerizer (0.1.1)
oauth (0.5.1)
- oauth2 (1.3.1)
- faraday (>= 0.8, < 0.12)
+ oauth2 (1.4.0)
+ faraday (>= 0.8, < 0.13)
jwt (~> 1.0)
multi_json (~> 1.3)
multi_xml (~> 0.5)
@@ -545,6 +551,36 @@ GEM
parser (2.4.0.0)
ast (~> 2.2)
path_expander (1.0.1)
+ peek (1.0.1)
+ concurrent-ruby (>= 0.9.0)
+ concurrent-ruby-ext (>= 0.9.0)
+ railties (>= 4.0.0)
+ peek-gc (0.0.2)
+ peek
+ peek-host (1.0.0)
+ peek
+ peek-mysql2 (1.1.0)
+ atomic (>= 1.0.0)
+ mysql2
+ peek
+ peek-performance_bar (1.2.1)
+ peek (>= 0.1.0)
+ peek-pg (1.3.0)
+ concurrent-ruby
+ concurrent-ruby-ext
+ peek
+ pg
+ peek-rblineprof (0.2.0)
+ peek
+ rblineprof
+ peek-redis (1.2.0)
+ atomic (>= 1.0.0)
+ peek
+ redis
+ peek-sidekiq (1.0.3)
+ atomic (>= 1.0.0)
+ peek
+ sidekiq
pg (0.18.4)
po_to_json (1.0.1)
json (>= 1.6.0)
@@ -703,7 +739,7 @@ GEM
ruby-progressbar (1.8.1)
ruby-saml (1.4.1)
nokogiri (>= 1.5.10)
- ruby_parser (3.8.4)
+ ruby_parser (3.9.0)
sexp_processor (~> 4.1)
rubyntlm (0.5.2)
rubypants (0.2.0)
@@ -736,7 +772,7 @@ GEM
sentry-raven (2.4.0)
faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9)
- sexp_processor (4.8.0)
+ sexp_processor (4.9.0)
sham_rack (1.3.6)
rack
shoulda-matchers (2.8.0)
@@ -888,6 +924,7 @@ DEPENDENCIES
benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0)
binding_of_caller (~> 0.7.2)
+ bootsnap (~> 1.0.0)
bootstrap-sass (~> 3.3.0)
brakeman (~> 3.6.0)
browser (~> 2.2)
@@ -916,7 +953,7 @@ DEPENDENCIES
email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0)
factory_girl_rails (~> 4.7.0)
- faraday (~> 0.11.0)
+ faraday (~> 0.12)
ffaker (~> 2.4)
flay (~> 2.8.0)
flipper (~> 0.10.2)
@@ -975,7 +1012,7 @@ DEPENDENCIES
mysql2 (~> 0.3.16)
net-ssh (~> 3.0.1)
nokogiri (~> 1.6.7, >= 1.6.7.2)
- oauth2 (~> 1.3.0)
+ oauth2 (~> 1.4)
octokit (~> 4.6.2)
oj (~> 2.17.4)
omniauth (~> 1.4.2)
@@ -995,6 +1032,15 @@ DEPENDENCIES
omniauth_crowd (~> 2.2.0)
org-ruby (~> 0.9.12)
paranoia (~> 2.2)
+ peek (~> 1.0.1)
+ peek-gc (~> 0.0.2)
+ peek-host (~> 1.0.0)
+ peek-mysql2 (~> 1.1.0)
+ peek-performance_bar (~> 1.2.1)
+ peek-pg (~> 1.3.0)
+ peek-rblineprof (~> 0.2.0)
+ peek-redis (~> 1.2.0)
+ peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
poltergeist (~> 1.9.0)
premailer-rails (~> 1.9.0)
@@ -1027,7 +1073,7 @@ DEPENDENCIES
rubocop-rspec (~> 1.15.0)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2)
- ruby_parser (~> 3.8.4)
+ ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4)
rugged (~> 0.25.1.1)
sanitize (~> 2.0)
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index 4568b86f298..dc636050221 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -35,7 +35,7 @@ export default class BlobFileDropzone {
this.removeFile(file);
});
this.on('sending', function (file, xhr, formData) {
- formData.append('branch_name', form.find('input[name="branch_name"]').val());
+ formData.append('branch_name', form.find('.js-branch-name').val());
formData.append('create_merge_request', form.find('.js-create-merge-request').val());
formData.append('commit_message', form.find('.js-commit-message').val());
});
diff --git a/app/assets/javascripts/blob/create_branch_dropdown.js b/app/assets/javascripts/blob/create_branch_dropdown.js
deleted file mode 100644
index 95517f51b1c..00000000000
--- a/app/assets/javascripts/blob/create_branch_dropdown.js
+++ /dev/null
@@ -1,88 +0,0 @@
-class CreateBranchDropdown {
- constructor(el, targetBranchDropdown) {
- this.targetBranchDropdown = targetBranchDropdown;
- this.el = el;
- this.dropdownBack = this.el.closest('.dropdown').querySelector('.dropdown-menu-back');
- this.cancelButton = this.el.querySelector('.js-cancel-branch-btn');
- this.newBranchField = this.el.querySelector('#new_branch_name');
- this.newBranchCreateButton = this.el.querySelector('.js-new-branch-btn');
-
- this.newBranchCreateButton.setAttribute('disabled', '');
-
- this.addBindings();
- this.cleanupWrapper = this.cleanup.bind(this);
- document.addEventListener('beforeunload', this.cleanupWrapper);
- }
-
- cleanup() {
- this.cleanBindings();
- document.removeEventListener('beforeunload', this.cleanupWrapper);
- }
-
- cleanBindings() {
- this.newBranchField.removeEventListener('keyup', this.enableBranchCreateButtonWrapper);
- this.newBranchField.removeEventListener('change', this.enableBranchCreateButtonWrapper);
- this.newBranchField.removeEventListener('keydown', this.handleNewBranchKeydownWrapper);
- this.dropdownBack.removeEventListener('click', this.resetFormWrapper);
- this.cancelButton.removeEventListener('click', this.handleCancelClickWrapper);
- this.newBranchCreateButton.removeEventListener('click', this.createBranchWrapper);
- }
-
- addBindings() {
- this.enableBranchCreateButtonWrapper = this.enableBranchCreateButton.bind(this);
- this.handleNewBranchKeydownWrapper = this.handleNewBranchKeydown.bind(this);
- this.resetFormWrapper = this.resetForm.bind(this);
- this.handleCancelClickWrapper = this.handleCancelClick.bind(this);
- this.createBranchWrapper = this.createBranch.bind(this);
-
- this.newBranchField.addEventListener('keyup', this.enableBranchCreateButtonWrapper);
- this.newBranchField.addEventListener('change', this.enableBranchCreateButtonWrapper);
- this.newBranchField.addEventListener('keydown', this.handleNewBranchKeydownWrapper);
- this.dropdownBack.addEventListener('click', this.resetFormWrapper);
- this.cancelButton.addEventListener('click', this.handleCancelClickWrapper);
- this.newBranchCreateButton.addEventListener('click', this.createBranchWrapper);
- }
-
- handleCancelClick(e) {
- e.preventDefault();
- e.stopPropagation();
-
- this.resetForm();
- this.dropdownBack.click();
- }
-
- handleNewBranchKeydown(e) {
- const keyCode = e.which;
- const ENTER_KEYCODE = 13;
- if (keyCode === ENTER_KEYCODE) {
- this.createBranch(e);
- }
- }
-
- enableBranchCreateButton() {
- if (this.newBranchField.value !== '') {
- this.newBranchCreateButton.removeAttribute('disabled');
- } else {
- this.newBranchCreateButton.setAttribute('disabled', '');
- }
- }
-
- resetForm() {
- this.newBranchField.value = '';
- this.enableBranchCreateButtonWrapper();
- }
-
- createBranch(e) {
- e.preventDefault();
-
- if (this.newBranchCreateButton.getAttribute('disabled') === '') {
- return;
- }
- const newBranchName = this.newBranchField.value;
- this.targetBranchDropdown.setNewBranch(newBranchName);
- this.resetForm();
- }
-}
-
-window.gl = window.gl || {};
-gl.CreateBranchDropdown = CreateBranchDropdown;
diff --git a/app/assets/javascripts/blob/target_branch_dropdown.js b/app/assets/javascripts/blob/target_branch_dropdown.js
deleted file mode 100644
index d52d69b1274..00000000000
--- a/app/assets/javascripts/blob/target_branch_dropdown.js
+++ /dev/null
@@ -1,152 +0,0 @@
-/* eslint-disable class-methods-use-this */
-const SELECT_ITEM_MSG = 'Select';
-
-class TargetBranchDropDown {
- constructor(dropdown) {
- this.dropdown = dropdown;
- this.$dropdown = $(dropdown);
- this.fieldName = this.dropdown.getAttribute('data-field-name');
- this.form = this.dropdown.closest('form');
- this.createDropdown();
- }
-
- static bootstrap() {
- const dropdowns = document.querySelectorAll('.js-project-branches-dropdown');
- [].forEach.call(dropdowns, dropdown => new TargetBranchDropDown(dropdown));
- }
-
- createDropdown() {
- const self = this;
- this.$dropdown.glDropdown({
- selectable: true,
- filterable: true,
- search: {
- fields: ['title'],
- },
- data: (term, callback) => $.ajax({
- url: self.dropdown.getAttribute('data-refs-url'),
- data: {
- ref: self.dropdown.getAttribute('data-ref'),
- show_all: true,
- },
- dataType: 'json',
- }).done(refs => callback(self.dropdownData(refs))),
- toggleLabel(item, el) {
- if (el.is('.is-active')) {
- return item.text;
- }
- return SELECT_ITEM_MSG;
- },
- clicked(options) {
- options.e.preventDefault();
- self.onClick.call(self);
- },
- fieldName: self.fieldName,
- });
- return new gl.CreateBranchDropdown(this.form.querySelector('.dropdown-new-branch'), this);
- }
-
- onClick() {
- this.enableSubmit();
- this.$dropdown.trigger('change.branch');
- }
-
- enableSubmit() {
- const submitBtn = this.form.querySelector('[type="submit"]');
- if (this.branchInput && this.branchInput.value) {
- submitBtn.removeAttribute('disabled');
- } else {
- submitBtn.setAttribute('disabled', '');
- }
- }
-
- dropdownData(refs) {
- const branchList = this.dropdownItems(refs);
- this.cachedRefs = refs;
- this.addDefaultBranch(branchList);
- this.addNewBranch(branchList);
- return { Branches: branchList };
- }
-
- dropdownItems(refs) {
- return refs.map(this.dropdownItem);
- }
-
- dropdownItem(ref) {
- return { id: ref, text: ref, title: ref };
- }
-
- addDefaultBranch(branchList) {
- // when no branch is selected do nothing
- if (!this.branchInput) {
- return;
- }
-
- const branchInputVal = this.branchInput.value;
- const currentBranchIndex = this.searchBranch(branchList, branchInputVal);
-
- if (currentBranchIndex === -1) {
- this.unshiftBranch(branchList, this.dropdownItem(branchInputVal));
- }
- }
-
- addNewBranch(branchList) {
- if (this.newBranch) {
- this.unshiftBranch(branchList, this.newBranch);
- }
- }
-
- searchBranch(branchList, branchName) {
- return _.findIndex(branchList, el => branchName === el.id);
- }
-
- unshiftBranch(branchList, branch) {
- const branchIndex = this.searchBranch(branchList, branch.id);
-
- if (branchIndex === -1) {
- branchList.unshift(branch);
- }
- }
-
- setNewBranch(newBranchName) {
- this.newBranch = this.dropdownItem(newBranchName);
- this.refreshData();
- this.selectBranch(this.searchBranch(this.glDropdown.fullData.Branches, newBranchName));
- }
-
- refreshData() {
- this.glDropdown.fullData = this.dropdownData(this.cachedRefs);
- this.clearFilter();
- }
-
- clearFilter() {
- // apply an empty filter in order to refresh the data
- this.glDropdown.filter.filter('');
- this.dropdown.closest('.dropdown').querySelector('.dropdown-page-one .dropdown-input-field').value = '';
- }
-
- selectBranch(index) {
- const branch = this.dropdown.closest('.dropdown').querySelectorAll('li a')[index];
-
- if (!branch.classList.contains('is-active')) {
- branch.click();
- } else {
- this.closeDropdown();
- }
- }
-
- closeDropdown() {
- this.dropdown.closest('.dropdown').querySelector('.dropdown-menu-close').click();
- }
-
- get branchInput() {
- return this.form.querySelector(`input[name="${this.fieldName}"]`);
- }
-
- get glDropdown() {
- return this.$dropdown.data('glDropdown');
- }
-}
-
-window.gl = window.gl || {};
-gl.TargetBranchDropDown = TargetBranchDropDown;
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 386102032cb..c7afd4ead6b 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -32,9 +32,6 @@ gl.issueBoards.BoardSidebar = Vue.extend({
showSidebar () {
return Object.keys(this.issue).length;
},
- assigneeId() {
- return this.issue.assignee ? this.issue.assignee.id : 0;
- },
milestoneTitle() {
return this.issue.milestone ? this.issue.milestone.title : 'No Milestone';
}
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 072a899e9f2..c28f6e151a0 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -20,6 +20,7 @@ window.Build = (function () {
this.$document = $(document);
this.logBytes = 0;
this.scrollOffsetPadding = 30;
+ this.hasBeenScrolled = false;
this.updateDropdown = this.updateDropdown.bind(this);
this.getBuildTrace = this.getBuildTrace.bind(this);
@@ -62,6 +63,15 @@ window.Build = (function () {
.off('click')
.on('click', this.scrollToBottom.bind(this));
+ const scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
+
+ this.$scrollContainer
+ .off('scroll')
+ .on('scroll', () => {
+ this.hasBeenScrolled = true;
+ scrollThrottled();
+ });
+
$(window)
.off('resize.build')
.on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100));
@@ -70,25 +80,16 @@ window.Build = (function () {
// eslint-disable-next-line
this.getBuildTrace()
- .then(() => this.makeTraceScrollable())
- .then(() => this.scrollToBottom());
+ .then(() => this.toggleScroll())
+ .then(() => {
+ if (!this.hasBeenScrolled) {
+ this.scrollToBottom();
+ }
+ });
this.verifyTopPosition();
}
- Build.prototype.makeTraceScrollable = function () {
- this.$scrollContainer.niceScroll({
- cursorcolor: '#fff',
- cursoropacitymin: 1,
- cursorwidth: '3px',
- railpadding: { top: 5, bottom: 5, right: 5 },
- });
-
- this.$scrollContainer.on('scroll', _.throttle(this.toggleScroll.bind(this), 100));
-
- this.toggleScroll();
- };
-
Build.prototype.canScroll = function () {
return (this.$scrollContainer.prop('scrollHeight') - this.scrollOffsetPadding) > this.$scrollContainer.height();
};
@@ -104,12 +105,11 @@ window.Build = (function () {
*
*/
Build.prototype.toggleScroll = function () {
- const bottomScroll = this.$scrollContainer.scrollTop() +
- this.scrollOffsetPadding +
- this.$scrollContainer.height();
+ const currentPosition = this.$scrollContainer.scrollTop();
+ const bottomScroll = currentPosition + this.$scrollContainer.innerHeight();
if (this.canScroll()) {
- if (this.$scrollContainer.scrollTop() === 0) {
+ if (currentPosition === 0) {
this.toggleDisableButton(this.$scrollTopBtn, true);
this.toggleDisableButton(this.$scrollBottomBtn, false);
} else if (bottomScroll === this.$scrollContainer.prop('scrollHeight')) {
@@ -123,12 +123,14 @@ window.Build = (function () {
};
Build.prototype.scrollToTop = function () {
- this.$scrollContainer.getNiceScroll(0).doScrollTop(0);
+ this.hasBeenScrolled = true;
+ this.$scrollContainer.scrollTop(0);
this.toggleScroll();
};
Build.prototype.scrollToBottom = function () {
- this.$scrollContainer.getNiceScroll(0).doScrollTo(this.$scrollContainer.prop('scrollHeight'));
+ this.hasBeenScrolled = true;
+ this.$scrollContainer.scrollTop(this.$scrollContainer.prop('scrollHeight'));
this.toggleScroll();
};
@@ -147,27 +149,34 @@ window.Build = (function () {
Build.prototype.verifyTopPosition = function () {
const $buildPage = $('.build-page');
+ const $flashError = $('.alert-wrapper');
const $header = $('.build-header', $buildPage);
const $runnersStuck = $('.js-build-stuck', $buildPage);
const $startsEnvironment = $('.js-environment-container', $buildPage);
const $erased = $('.js-build-erased', $buildPage);
+ const prependTopDefault = 20;
+ // header + navigation + margin
let topPostion = 168;
- if ($header) {
+ if ($header.length) {
topPostion += $header.outerHeight();
}
- if ($runnersStuck) {
+ if ($runnersStuck.length) {
topPostion += $runnersStuck.outerHeight();
}
- if ($startsEnvironment) {
- topPostion += $startsEnvironment.outerHeight();
+ if ($startsEnvironment.length) {
+ topPostion += $startsEnvironment.outerHeight() + prependTopDefault;
+ }
+
+ if ($erased.length) {
+ topPostion += $erased.outerHeight() + prependTopDefault;
}
- if ($erased) {
- topPostion += $erased.outerHeight() + 10;
+ if ($flashError.length) {
+ topPostion += $flashError.outerHeight();
}
this.$buildTrace.css({
@@ -216,7 +225,11 @@ window.Build = (function () {
Build.timeout = setTimeout(() => {
//eslint-disable-next-line
this.getBuildTrace()
- .then(() => this.scrollToBottom());
+ .then(() => {
+ if (!this.hasBeenScrolled) {
+ this.scrollToBottom();
+ }
+ });
}, 4000);
} else {
this.$buildRefreshAnimation.remove();
@@ -238,7 +251,8 @@ window.Build = (function () {
};
Build.prototype.toggleSidebar = function (shouldHide) {
- const shouldShow = !shouldHide;
+ const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
+ const $toggleButton = $('.js-sidebar-build-toggle-header');
this.$buildTrace
.toggleClass('sidebar-expanded', shouldShow)
@@ -246,6 +260,16 @@ window.Build = (function () {
this.$sidebar
.toggleClass('right-sidebar-expanded', shouldShow)
.toggleClass('right-sidebar-collapsed', shouldHide);
+
+ $('.js-build-page')
+ .toggleClass('sidebar-expanded', shouldShow)
+ .toggleClass('sidebar-collapsed', shouldHide);
+
+ if (this.$sidebar.hasClass('right-sidebar-expanded')) {
+ $toggleButton.addClass('hidden');
+ } else {
+ $toggleButton.removeClass('hidden');
+ }
};
Build.prototype.sidebarOnResize = function () {
@@ -253,13 +277,14 @@ window.Build = (function () {
this.verifyTopPosition();
- if (this.$scrollContainer.getNiceScroll(0)) {
+ if (this.canScroll()) {
this.toggleScroll();
}
};
Build.prototype.sidebarOnClick = function () {
if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
+ this.verifyTopPosition();
};
Build.prototype.updateArtifactRemoveDate = function () {
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index cb054a2a197..bc3e741f524 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -1,5 +1,6 @@
// ECMAScript polyfills
import 'core-js/fn/array/find';
+import 'core-js/fn/array/find-index';
import 'core-js/fn/array/from';
import 'core-js/fn/array/includes';
import 'core-js/fn/object/assign';
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 0c420c12345..5f87a05067b 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -2,7 +2,6 @@
/* global UsernameValidator */
/* global ActiveTabMemoizer */
/* global ShortcutsNavigation */
-/* global Build */
/* global IssuableIndex */
/* global ShortcutsIssuable */
/* global ZenMode */
@@ -119,9 +118,6 @@ import initSettingsPanels from './settings_panels';
shortcut_handler = new ShortcutsNavigation();
new UsersSelect();
break;
- case 'projects:jobs:show':
- new Build();
- break;
case 'projects:merge_requests:index':
case 'projects:issues:index':
if (gl.FilteredSearchManager && document.querySelector('.filtered-search')) {
@@ -160,9 +156,6 @@ import initSettingsPanels from './settings_panels';
case 'admin:projects:index':
new ProjectsList();
break;
- case 'dashboard:groups:index':
- new GroupsList();
- break;
case 'explore:groups:index':
new GroupsList();
@@ -332,25 +325,14 @@ import initSettingsPanels from './settings_panels';
shortcut_handler = new ShortcutsNavigation();
new TreeView();
new BlobViewer();
- gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:find_file:show':
shortcut_handler = true;
break;
- case 'projects:blob:new':
- gl.TargetBranchDropDown.bootstrap();
- break;
- case 'projects:blob:create':
- gl.TargetBranchDropDown.bootstrap();
- break;
case 'projects:blob:show':
new BlobViewer();
- gl.TargetBranchDropDown.bootstrap();
initBlob();
break;
- case 'projects:blob:edit':
- gl.TargetBranchDropDown.bootstrap();
- break;
case 'projects:blame:show':
initBlob();
break;
@@ -374,9 +356,11 @@ import initSettingsPanels from './settings_panels';
new ProjectFork();
break;
case 'projects:artifacts:browse':
+ new ShortcutsNavigation();
new BuildArtifacts();
break;
case 'projects:artifacts:file':
+ new ShortcutsNavigation();
new BlobViewer();
break;
case 'help:index':
diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js
index aaaeb9bddb1..139206cc185 100644
--- a/app/assets/javascripts/filterable_list.js
+++ b/app/assets/javascripts/filterable_list.js
@@ -8,39 +8,87 @@ export default class FilterableList {
this.filterForm = form;
this.listFilterElement = filter;
this.listHolderElement = holder;
+ this.isBusy = false;
+ }
+
+ getFilterEndpoint() {
+ return `${this.filterForm.getAttribute('action')}?${$(this.filterForm).serialize()}`;
+ }
+
+ getPagePath() {
+ return this.getFilterEndpoint();
}
initSearch() {
- this.debounceFilter = _.debounce(this.filterResults.bind(this), 500);
+ // Wrap to prevent passing event arguments to .filterResults;
+ this.debounceFilter = _.debounce(this.onFilterInput.bind(this), 500);
- this.listFilterElement.removeEventListener('input', this.debounceFilter);
+ this.unbindEvents();
+ this.bindEvents();
+ }
+
+ onFilterInput() {
+ const $form = $(this.filterForm);
+ const queryData = {};
+ const filterGroupsParam = $form.find('[name="filter_groups"]').val();
+
+ if (filterGroupsParam) {
+ queryData.filter_groups = filterGroupsParam;
+ }
+
+ this.filterResults(queryData);
+
+ if (this.setDefaultFilterOption) {
+ this.setDefaultFilterOption();
+ }
+ }
+
+ bindEvents() {
this.listFilterElement.addEventListener('input', this.debounceFilter);
}
- filterResults() {
- const form = this.filterForm;
- const filterUrl = `${form.getAttribute('action')}?${$(form).serialize()}`;
+ unbindEvents() {
+ this.listFilterElement.removeEventListener('input', this.debounceFilter);
+ }
+
+ filterResults(queryData) {
+ if (this.isBusy) {
+ return false;
+ }
$(this.listHolderElement).fadeTo(250, 0.5);
return $.ajax({
- url: form.getAttribute('action'),
- data: $(form).serialize(),
+ url: this.getFilterEndpoint(),
+ data: queryData,
type: 'GET',
dataType: 'json',
context: this,
- complete() {
- $(this.listHolderElement).fadeTo(250, 1);
+ complete: this.onFilterComplete,
+ beforeSend: () => {
+ this.isBusy = true;
},
- success(data) {
- this.listHolderElement.innerHTML = data.html;
-
- // Change url so if user reload a page - search results are saved
- return window.history.replaceState({
- page: filterUrl,
-
- }, document.title, filterUrl);
+ success: (response, textStatus, xhr) => {
+ this.onFilterSuccess(response, xhr, queryData);
},
});
}
+
+ onFilterSuccess(response, xhr, queryData) {
+ if (response.html) {
+ this.listHolderElement.innerHTML = response.html;
+ }
+
+ // Change url so if user reload a page - search results are saved
+ const currentPath = this.getPagePath(queryData);
+
+ return window.history.replaceState({
+ page: currentPath,
+ }, document.title, currentPath);
+ }
+
+ onFilterComplete() {
+ this.isBusy = false;
+ $(this.listHolderElement).fadeTo(250, 1);
+ }
}
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index d34561e5512..3babe273100 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -248,7 +248,7 @@ GitLabDropdown = (function() {
return function(data) {
_this.fullData = data;
_this.parseData(_this.fullData);
- _this.focusTextInput();
+ _this.focusTextInput(true);
if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') {
return _this.filter.input.trigger('input');
}
@@ -728,8 +728,20 @@ GitLabDropdown = (function() {
return [selectedObject, isMarking];
};
- GitLabDropdown.prototype.focusTextInput = function() {
- if (this.options.filterable) { this.filterInput.focus(); }
+ GitLabDropdown.prototype.focusTextInput = function(triggerFocus = false) {
+ if (this.options.filterable) {
+ $(':focus').blur();
+
+ this.dropdown.one('transitionend', () => {
+ this.filterInput.focus();
+ });
+
+ if (triggerFocus) {
+ // This triggers after a ajax request
+ // in case of slow requests, the dropdown transition could already be finished
+ this.dropdown.trigger('transitionend');
+ }
+ }
};
GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
diff --git a/app/assets/javascripts/groups/components/group_folder.vue b/app/assets/javascripts/groups/components/group_folder.vue
new file mode 100644
index 00000000000..7cc6c4b0359
--- /dev/null
+++ b/app/assets/javascripts/groups/components/group_folder.vue
@@ -0,0 +1,27 @@
+<script>
+export default {
+ props: {
+ groups: {
+ type: Object,
+ required: true,
+ },
+ baseGroup: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+};
+</script>
+
+<template>
+ <ul class="content-list group-list-tree">
+ <group-item
+ v-for="(group, index) in groups"
+ :key="index"
+ :group="group"
+ :base-group="baseGroup"
+ :collection="groups"
+ />
+ </ul>
+</template>
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
new file mode 100644
index 00000000000..32815b9f73e
--- /dev/null
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -0,0 +1,220 @@
+<script>
+import eventHub from '../event_hub';
+
+export default {
+ props: {
+ group: {
+ type: Object,
+ required: true,
+ },
+ baseGroup: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ collection: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ methods: {
+ onClickRowGroup(e) {
+ e.stopPropagation();
+
+ // Skip for buttons
+ if (!(e.target.tagName === 'A') && !(e.target.tagName === 'I' && e.target.parentElement.tagName === 'A')) {
+ if (this.group.hasSubgroups) {
+ eventHub.$emit('toggleSubGroups', this.group);
+ } else {
+ window.location.href = this.group.webUrl;
+ }
+ }
+ },
+ onLeaveGroup(e) {
+ e.preventDefault();
+
+ // eslint-disable-next-line no-alert
+ if (confirm(`Are you sure you want to leave the "${this.group.fullName}" group?`)) {
+ this.leaveGroup();
+ }
+ },
+ leaveGroup() {
+ eventHub.$emit('leaveGroup', this.group, this.collection);
+ },
+ },
+ computed: {
+ groupDomId() {
+ return `group-${this.group.id}`;
+ },
+ rowClass() {
+ return {
+ 'group-row': true,
+ 'is-open': this.group.isOpen,
+ 'has-subgroups': this.group.hasSubgroups,
+ 'no-description': !this.group.description,
+ };
+ },
+ visibilityIcon() {
+ return {
+ fa: true,
+ 'fa-globe': this.group.visibility === 'public',
+ 'fa-shield': this.group.visibility === 'internal',
+ 'fa-lock': this.group.visibility === 'private',
+ };
+ },
+ fullPath() {
+ let fullPath = '';
+
+ if (this.group.isOrphan) {
+ // check if current group is baseGroup
+ if (Object.keys(this.baseGroup).length > 0 && this.baseGroup !== this.group) {
+ // Remove baseGroup prefix from our current group.fullName. e.g:
+ // baseGroup.fullName: `level1`
+ // group.fullName: `level1 / level2 / level3`
+ // Result: `level2 / level3`
+ const gfn = this.group.fullName;
+ const bfn = this.baseGroup.fullName;
+ const length = bfn.length;
+ const start = gfn.indexOf(bfn);
+ const extraPrefixChars = 3;
+
+ fullPath = gfn.substr(start + length + extraPrefixChars);
+ } else {
+ fullPath = this.group.fullName;
+ }
+ } else {
+ fullPath = this.group.name;
+ }
+
+ return fullPath;
+ },
+ hasGroups() {
+ return Object.keys(this.group.subGroups).length > 0;
+ },
+ },
+};
+</script>
+
+<template>
+ <li
+ @click.stop="onClickRowGroup"
+ :id="groupDomId"
+ :class="rowClass"
+ >
+ <div
+ class="group-row-contents">
+ <div
+ class="controls">
+ <a
+ v-if="group.canEdit"
+ class="edit-group btn"
+ :href="group.editPath">
+ <i
+ class="fa fa-cogs"
+ aria-hidden="true"
+ >
+ </i>
+ </a>
+ <a
+ @click="onLeaveGroup"
+ :href="group.leavePath"
+ class="leave-group btn"
+ title="Leave this group">
+ <i
+ class="fa fa-sign-out"
+ aria-hidden="true"
+ >
+ </i>
+ </a>
+ </div>
+ <div
+ class="stats">
+ <span
+ class="number-projects">
+ <i
+ class="fa fa-bookmark"
+ aria-hidden="true"
+ >
+ </i>
+ {{group.numberProjects}}
+ </span>
+ <span
+ class="number-users">
+ <i
+ class="fa fa-users"
+ aria-hidden="true"
+ >
+ </i>
+ {{group.numberUsers}}
+ </span>
+ <span
+ class="group-visibility">
+ <i
+ :class="visibilityIcon"
+ aria-hidden="true"
+ >
+ </i>
+ </span>
+ </div>
+ <div
+ class="folder-toggle-wrap">
+ <span
+ class="folder-caret"
+ v-if="group.hasSubgroups">
+ <i
+ v-if="group.isOpen"
+ class="fa fa-caret-down"
+ aria-hidden="true"
+ >
+ </i>
+ <i
+ v-if="!group.isOpen"
+ class="fa fa-caret-right"
+ aria-hidden="true"
+ >
+ </i>
+ </span>
+ <span class="folder-icon">
+ <i
+ v-if="group.isOpen"
+ class="fa fa-folder-open"
+ aria-hidden="true"
+ >
+ </i>
+ <i
+ v-if="!group.isOpen"
+ class="fa fa-folder"
+ aria-hidden="true">
+ </i>
+ </span>
+ </div>
+ <div
+ class="avatar-container s40 hidden-xs">
+ <a
+ :href="group.webUrl">
+ <img
+ class="avatar s40"
+ :src="group.avatarUrl"
+ />
+ </a>
+ </div>
+ <div
+ class="title">
+ <a
+ :href="group.webUrl">{{fullPath}}</a>
+ <template v-if="group.permissions.humanGroupAccess">
+ as
+ <span class="access-type">{{group.permissions.humanGroupAccess}}</span>
+ </template>
+ </div>
+ <div
+ class="description">{{group.description}}</div>
+ </div>
+ <group-folder
+ v-if="group.isOpen && hasGroups"
+ :groups="group.subGroups"
+ :baseGroup="group"
+ />
+ </li>
+</template>
diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue
new file mode 100644
index 00000000000..36a04d4202f
--- /dev/null
+++ b/app/assets/javascripts/groups/components/groups.vue
@@ -0,0 +1,39 @@
+<script>
+import tablePagination from '~/vue_shared/components/table_pagination.vue';
+import eventHub from '../event_hub';
+
+export default {
+ props: {
+ groups: {
+ type: Object,
+ required: true,
+ },
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
+ },
+ components: {
+ tablePagination,
+ },
+ methods: {
+ change(page) {
+ const filterGroupsParam = gl.utils.getParameterByName('filter_groups');
+ const sortParam = gl.utils.getParameterByName('sort');
+ eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="groups-list-tree-container">
+ <group-folder
+ :groups="groups"
+ />
+ <table-pagination
+ :change="change"
+ :pageInfo="pageInfo"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/groups/event_hub.js b/app/assets/javascripts/groups/event_hub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/groups/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js
new file mode 100644
index 00000000000..439a931ddad
--- /dev/null
+++ b/app/assets/javascripts/groups/groups_filterable_list.js
@@ -0,0 +1,87 @@
+import FilterableList from '~/filterable_list';
+import eventHub from './event_hub';
+
+export default class GroupFilterableList extends FilterableList {
+ constructor({ form, filter, holder, filterEndpoint, pagePath }) {
+ super(form, filter, holder);
+ this.form = form;
+ this.filterEndpoint = filterEndpoint;
+ this.pagePath = pagePath;
+ this.$dropdown = $('.js-group-filter-dropdown-wrap');
+ }
+
+ getFilterEndpoint() {
+ return this.filterEndpoint;
+ }
+
+ getPagePath(queryData) {
+ const params = queryData ? $.param(queryData) : '';
+ const queryString = params ? `?${params}` : '';
+ return `${this.pagePath}${queryString}`;
+ }
+
+ bindEvents() {
+ super.bindEvents();
+
+ this.onFormSubmitWrapper = this.onFormSubmit.bind(this);
+ this.onFilterOptionClikWrapper = this.onOptionClick.bind(this);
+
+ this.filterForm.addEventListener('submit', this.onFormSubmitWrapper);
+ this.$dropdown.on('click', 'a', this.onFilterOptionClikWrapper);
+ }
+
+ onFormSubmit(e) {
+ e.preventDefault();
+
+ const $form = $(this.form);
+ const filterGroupsParam = $form.find('[name="filter_groups"]').val();
+ const queryData = {};
+
+ if (filterGroupsParam) {
+ queryData.filter_groups = filterGroupsParam;
+ }
+
+ this.filterResults(queryData);
+ this.setDefaultFilterOption();
+ }
+
+ setDefaultFilterOption() {
+ const defaultOption = $.trim(this.$dropdown.find('.dropdown-menu a:first-child').text());
+ this.$dropdown.find('.dropdown-label').text(defaultOption);
+ }
+
+ onOptionClick(e) {
+ e.preventDefault();
+
+ const queryData = {};
+ const sortParam = gl.utils.getParameterByName('sort', e.currentTarget.href);
+
+ if (sortParam) {
+ queryData.sort = sortParam;
+ }
+
+ this.filterResults(queryData);
+
+ // Active selected option
+ this.$dropdown.find('.dropdown-label').text($.trim(e.currentTarget.text));
+
+ // Clear current value on search form
+ this.form.querySelector('[name="filter_groups"]').value = '';
+ }
+
+ onFilterSuccess(data, xhr, queryData) {
+ super.onFilterSuccess(data, xhr, queryData);
+
+ const paginationData = {
+ 'X-Per-Page': xhr.getResponseHeader('X-Per-Page'),
+ 'X-Page': xhr.getResponseHeader('X-Page'),
+ 'X-Total': xhr.getResponseHeader('X-Total'),
+ 'X-Total-Pages': xhr.getResponseHeader('X-Total-Pages'),
+ 'X-Next-Page': xhr.getResponseHeader('X-Next-Page'),
+ 'X-Prev-Page': xhr.getResponseHeader('X-Prev-Page'),
+ };
+
+ eventHub.$emit('updateGroups', data);
+ eventHub.$emit('updatePagination', paginationData);
+ }
+}
diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js
new file mode 100644
index 00000000000..ff601db2aa6
--- /dev/null
+++ b/app/assets/javascripts/groups/index.js
@@ -0,0 +1,190 @@
+/* global Flash */
+
+import Vue from 'vue';
+import GroupFilterableList from './groups_filterable_list';
+import GroupsComponent from './components/groups.vue';
+import GroupFolder from './components/group_folder.vue';
+import GroupItem from './components/group_item.vue';
+import GroupsStore from './stores/groups_store';
+import GroupsService from './services/groups_service';
+import eventHub from './event_hub';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const el = document.getElementById('dashboard-group-app');
+
+ // Don't do anything if element doesn't exist (No groups)
+ // This is for when the user enters directly to the page via URL
+ if (!el) {
+ return;
+ }
+
+ Vue.component('groups-component', GroupsComponent);
+ Vue.component('group-folder', GroupFolder);
+ Vue.component('group-item', GroupItem);
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ data() {
+ this.store = new GroupsStore();
+ this.service = new GroupsService(el.dataset.endpoint);
+
+ return {
+ store: this.store,
+ isLoading: true,
+ state: this.store.state,
+ loading: true,
+ };
+ },
+ computed: {
+ isEmpty() {
+ return Object.keys(this.state.groups).length === 0;
+ },
+ },
+ methods: {
+ fetchGroups(parentGroup) {
+ let parentId = null;
+ let getGroups = null;
+ let page = null;
+ let sort = null;
+ let pageParam = null;
+ let sortParam = null;
+ let filterGroups = null;
+ let filterGroupsParam = null;
+
+ if (parentGroup) {
+ parentId = parentGroup.id;
+ } else {
+ this.isLoading = true;
+ }
+
+ pageParam = gl.utils.getParameterByName('page');
+ if (pageParam) {
+ page = pageParam;
+ }
+
+ filterGroupsParam = gl.utils.getParameterByName('filter_groups');
+ if (filterGroupsParam) {
+ filterGroups = filterGroupsParam;
+ }
+
+ sortParam = gl.utils.getParameterByName('sort');
+ if (sortParam) {
+ sort = sortParam;
+ }
+
+ getGroups = this.service.getGroups(parentId, page, filterGroups, sort);
+ getGroups
+ .then(response => response.json())
+ .then((response) => {
+ this.isLoading = false;
+
+ this.updateGroups(response, parentGroup);
+ })
+ .catch(this.handleErrorResponse);
+
+ return getGroups;
+ },
+ fetchPage(page, filterGroups, sort) {
+ this.isLoading = true;
+
+ return this.service
+ .getGroups(null, page, filterGroups, sort)
+ .then((response) => {
+ this.isLoading = false;
+ $.scrollTo(0);
+
+ const currentPath = gl.utils.mergeUrlParams({ page }, window.location.href);
+ window.history.replaceState({
+ page: currentPath,
+ }, document.title, currentPath);
+
+ this.updateGroups(response.json());
+ this.updatePagination(response.headers);
+ })
+ .catch(this.handleErrorResponse);
+ },
+ toggleSubGroups(parentGroup = null) {
+ if (!parentGroup.isOpen) {
+ this.store.resetGroups(parentGroup);
+ this.fetchGroups(parentGroup);
+ }
+
+ this.store.toggleSubGroups(parentGroup);
+ },
+ leaveGroup(group, collection) {
+ this.service.leaveGroup(group.leavePath)
+ .then((response) => {
+ $.scrollTo(0);
+
+ this.store.removeGroup(group, collection);
+
+ // eslint-disable-next-line no-new
+ new Flash(response.json().notice, 'notice');
+ })
+ .catch((response) => {
+ let message = 'An error occurred. Please try again.';
+
+ if (response.status === 403) {
+ message = 'Failed to leave the group. Please make sure you are not the only owner';
+ }
+
+ // eslint-disable-next-line no-new
+ new Flash(message);
+ });
+ },
+ updateGroups(groups, parentGroup) {
+ this.store.setGroups(groups, parentGroup);
+ },
+ updatePagination(headers) {
+ this.store.storePagination(headers);
+ },
+ handleErrorResponse() {
+ this.isLoading = false;
+ $.scrollTo(0);
+
+ // eslint-disable-next-line no-new
+ new Flash('An error occurred. Please try again.');
+ },
+ },
+ created() {
+ eventHub.$on('fetchPage', this.fetchPage);
+ eventHub.$on('toggleSubGroups', this.toggleSubGroups);
+ eventHub.$on('leaveGroup', this.leaveGroup);
+ eventHub.$on('updateGroups', this.updateGroups);
+ eventHub.$on('updatePagination', this.updatePagination);
+ },
+ beforeMount() {
+ let groupFilterList = null;
+ const form = document.querySelector('form#group-filter-form');
+ const filter = document.querySelector('.js-groups-list-filter');
+ const holder = document.querySelector('.js-groups-list-holder');
+
+ const opts = {
+ form,
+ filter,
+ holder,
+ filterEndpoint: el.dataset.endpoint,
+ pagePath: el.dataset.path,
+ };
+
+ groupFilterList = new GroupFilterableList(opts);
+ groupFilterList.initSearch();
+ },
+ mounted() {
+ this.fetchGroups()
+ .then((response) => {
+ this.updatePagination(response.headers);
+ this.isLoading = false;
+ })
+ .catch(this.handleErrorResponse);
+ },
+ beforeDestroy() {
+ eventHub.$off('fetchPage', this.fetchPage);
+ eventHub.$off('toggleSubGroups', this.toggleSubGroups);
+ eventHub.$off('leaveGroup', this.leaveGroup);
+ eventHub.$off('updateGroups', this.updateGroups);
+ eventHub.$off('updatePagination', this.updatePagination);
+ },
+ });
+});
diff --git a/app/assets/javascripts/groups/services/groups_service.js b/app/assets/javascripts/groups/services/groups_service.js
new file mode 100644
index 00000000000..97e02fcb76d
--- /dev/null
+++ b/app/assets/javascripts/groups/services/groups_service.js
@@ -0,0 +1,38 @@
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
+
+export default class GroupsService {
+ constructor(endpoint) {
+ this.groups = Vue.resource(endpoint);
+ }
+
+ getGroups(parentId, page, filterGroups, sort) {
+ const data = {};
+
+ if (parentId) {
+ data.parent_id = parentId;
+ } else {
+ // Do not send the following param for sub groups
+ if (page) {
+ data.page = page;
+ }
+
+ if (filterGroups) {
+ data.filter_groups = filterGroups;
+ }
+
+ if (sort) {
+ data.sort = sort;
+ }
+ }
+
+ return this.groups.get(data);
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ leaveGroup(endpoint) {
+ return Vue.http.delete(endpoint);
+ }
+}
diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js
new file mode 100644
index 00000000000..67ee7d140ce
--- /dev/null
+++ b/app/assets/javascripts/groups/stores/groups_store.js
@@ -0,0 +1,151 @@
+import Vue from 'vue';
+
+export default class GroupsStore {
+ constructor() {
+ this.state = {};
+ this.state.groups = {};
+ this.state.pageInfo = {};
+ }
+
+ setGroups(rawGroups, parent) {
+ const parentGroup = parent;
+ const tree = this.buildTree(rawGroups, parentGroup);
+
+ if (parentGroup) {
+ parentGroup.subGroups = tree;
+ } else {
+ this.state.groups = tree;
+ }
+
+ return tree;
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ resetGroups(parent) {
+ const parentGroup = parent;
+ parentGroup.subGroups = {};
+ }
+
+ storePagination(pagination = {}) {
+ let paginationInfo;
+
+ if (Object.keys(pagination).length) {
+ const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
+ paginationInfo = gl.utils.parseIntPagination(normalizedHeaders);
+ } else {
+ paginationInfo = pagination;
+ }
+
+ this.state.pageInfo = paginationInfo;
+ }
+
+ buildTree(rawGroups, parentGroup) {
+ const groups = this.decorateGroups(rawGroups);
+ const tree = {};
+ const mappedGroups = {};
+ const orphans = [];
+
+ // Map groups to an object
+ groups.map((group) => {
+ mappedGroups[group.id] = group;
+ mappedGroups[group.id].subGroups = {};
+ return group;
+ });
+
+ Object.keys(mappedGroups).map((key) => {
+ const currentGroup = mappedGroups[key];
+ if (currentGroup.parentId) {
+ // If the group is not at the root level, add it to its parent array of subGroups.
+ const findParentGroup = mappedGroups[currentGroup.parentId];
+ if (findParentGroup) {
+ mappedGroups[currentGroup.parentId].subGroups[currentGroup.id] = currentGroup;
+ mappedGroups[currentGroup.parentId].isOpen = true; // Expand group if it has subgroups
+ } else if (parentGroup && parentGroup.id === currentGroup.parentId) {
+ tree[currentGroup.id] = currentGroup;
+ } else {
+ // Means the groups hast no direct parent.
+ // Save for later processing, we will add them to its corresponding base group
+ orphans.push(currentGroup);
+ }
+ } else {
+ // If the group is at the root level, add it to first level elements array.
+ tree[currentGroup.id] = currentGroup;
+ }
+
+ return key;
+ });
+
+ // Hopefully this array will be empty for most cases
+ if (orphans.length) {
+ orphans.map((orphan) => {
+ let found = false;
+ const currentOrphan = orphan;
+
+ Object.keys(tree).map((key) => {
+ const group = tree[key];
+ if (currentOrphan.fullPath.lastIndexOf(group.fullPath) === 0) {
+ group.subGroups[currentOrphan.id] = currentOrphan;
+ group.isOpen = true;
+ currentOrphan.isOrphan = true;
+ found = true;
+ }
+
+ return key;
+ });
+
+ if (!found) {
+ currentOrphan.isOrphan = true;
+ tree[currentOrphan.id] = currentOrphan;
+ }
+
+ return orphan;
+ });
+ }
+
+ return tree;
+ }
+
+ decorateGroups(rawGroups) {
+ this.groups = rawGroups.map(this.decorateGroup);
+ return this.groups;
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ decorateGroup(rawGroup) {
+ return {
+ id: rawGroup.id,
+ fullName: rawGroup.full_name,
+ fullPath: rawGroup.full_path,
+ avatarUrl: rawGroup.avatar_url,
+ name: rawGroup.name,
+ hasSubgroups: rawGroup.has_subgroups,
+ canEdit: rawGroup.can_edit,
+ description: rawGroup.description,
+ webUrl: rawGroup.web_url,
+ parentId: rawGroup.parent_id,
+ visibility: rawGroup.visibility,
+ leavePath: rawGroup.leave_path,
+ editPath: rawGroup.edit_path,
+ isOpen: false,
+ isOrphan: false,
+ numberProjects: rawGroup.number_projects_with_delimiter,
+ numberUsers: rawGroup.number_users_with_delimiter,
+ permissions: {
+ humanGroupAccess: rawGroup.permissions.human_group_access,
+ },
+ subGroups: {},
+ };
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ removeGroup(group, collection) {
+ Vue.delete(collection, group.id);
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ toggleSubGroups(toggleGroup) {
+ const group = toggleGroup;
+ group.isOpen = !group.isOpen;
+ return group;
+ }
+}
diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue
new file mode 100644
index 00000000000..5b9cf577189
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/header.vue
@@ -0,0 +1,83 @@
+<script>
+ import ciHeader from '../../vue_shared/components/header_ci_component.vue';
+ import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+
+ export default {
+ name: 'jobHeaderSection',
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ isLoading: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ components: {
+ ciHeader,
+ loadingIcon,
+ },
+ data() {
+ return {
+ actions: this.getActions(),
+ };
+ },
+ computed: {
+ status() {
+ return this.job && this.job.status;
+ },
+ shouldRenderContent() {
+ return !this.isLoading && Object.keys(this.job).length;
+ },
+ },
+ methods: {
+ getActions() {
+ const actions = [];
+
+ if (this.job.new_issue_path) {
+ actions.push({
+ label: 'New issue',
+ path: this.job.new_issue_path,
+ cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block',
+ type: 'ujs-link',
+ });
+ }
+
+ if (this.job.retry_path) {
+ actions.push({
+ label: 'Retry',
+ path: this.job.retry_path,
+ cssClass: 'js-retry-button btn btn-inverted-secondary visible-md-block visible-lg-block',
+ type: 'ujs-link',
+ });
+ }
+
+ return actions;
+ },
+ },
+ watch: {
+ job() {
+ this.actions = this.getActions();
+ },
+ },
+ };
+</script>
+<template>
+ <div class="js-build-header build-header top-area">
+ <ci-header
+ v-if="shouldRenderContent"
+ :status="status"
+ item-name="Job"
+ :item-id="job.id"
+ :time="job.created_at"
+ :user="job.user"
+ :actions="actions"
+ :hasSidebarButton="true"
+ />
+ <loading-icon
+ v-if="isLoading"
+ size="2"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
new file mode 100644
index 00000000000..ab2bcd728a8
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
@@ -0,0 +1,31 @@
+<script>
+ export default {
+ name: 'SidebarDetailRow',
+ props: {
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ value: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ hasTitle() {
+ return this.title.length > 0;
+ },
+ },
+ };
+</script>
+<template>
+ <p class="build-detail-row">
+ <span
+ v-if="hasTitle"
+ class="build-light-text">
+ {{title}}:
+ </span>
+ {{value}}
+ </p>
+</template>
diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
new file mode 100644
index 00000000000..4223a8fea49
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
@@ -0,0 +1,150 @@
+<script>
+ import detailRow from './sidebar_detail_row.vue';
+ import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+ import timeagoMixin from '../../vue_shared/mixins/timeago';
+ import { timeIntervalInWords } from '../../lib/utils/datetime_utility';
+
+ export default {
+ name: 'SidebarDetailsBlock',
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ isLoading: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ mixins: [
+ timeagoMixin,
+ ],
+ components: {
+ detailRow,
+ loadingIcon,
+ },
+ computed: {
+ shouldRenderContent() {
+ return !this.isLoading && Object.keys(this.job).length > 0;
+ },
+ coverage() {
+ return `${this.job.coverage}%`;
+ },
+ duration() {
+ return timeIntervalInWords(this.job.duration);
+ },
+ queued() {
+ return timeIntervalInWords(this.job.queued);
+ },
+ runnerId() {
+ return `#${this.job.runner.id}`;
+ },
+ },
+ };
+</script>
+<template>
+ <div>
+ <template v-if="shouldRenderContent">
+ <div
+ class="block retry-link"
+ v-if="job.retry_path || job.new_issue_path">
+ <a
+ v-if="job.new_issue_path"
+ class="js-new-issue btn btn-new btn-inverted"
+ :href="job.new_issue_path">
+ New issue
+ </a>
+ <a
+ v-if="job.retry_path"
+ class="js-retry-job btn btn-inverted-secondary"
+ :href="job.retry_path"
+ data-method="post"
+ rel="nofollow">
+ Retry
+ </a>
+ </div>
+ <div class="block">
+ <p
+ class="build-detail-row js-job-mr"
+ v-if="job.merge_request">
+ <span
+ class="build-light-text">
+ Merge Request:
+ </span>
+ <a :href="job.merge_request.path">
+ !{{job.merge_request.iid}}
+ </a>
+ </p>
+
+ <detail-row
+ class="js-job-duration"
+ v-if="job.duration"
+ title="Duration"
+ :value="duration"
+ />
+ <detail-row
+ class="js-job-finished"
+ v-if="job.finished_at"
+ title="Finished"
+ :value="timeFormated(job.finished_at)"
+ />
+ <detail-row
+ class="js-job-erased"
+ v-if="job.erased_at"
+ title="Erased"
+ :value="timeFormated(job.erased_at)"
+ />
+ <detail-row
+ class="js-job-queued"
+ v-if="job.queued"
+ title="Queued"
+ :value="queued"
+ />
+ <detail-row
+ class="js-job-runner"
+ v-if="job.runner"
+ title="Runner"
+ :value="runnerId"
+ />
+ <detail-row
+ class="js-job-coverage"
+ v-if="job.coverage"
+ title="Coverage"
+ :value="coverage"
+ />
+ <p
+ class="build-detail-row js-job-tags"
+ v-if="job.tags.length">
+ <span
+ class="build-light-text">
+ Tags:
+ </span>
+ <span
+ v-for="tag in job.tags"
+ key="tag"
+ class="label label-primary">
+ {{tag}}
+ </span>
+ </p>
+
+ <div
+ v-if="job.cancel_path"
+ class="btn-group prepend-top-5"
+ role="group">
+ <a
+ class="js-cancel-job btn btn-sm btn-default"
+ :href="job.cancel_path"
+ data-method="post"
+ rel="nofollow">
+ Cancel
+ </a>
+ </div>
+ </div>
+ </template>
+ <loading-icon
+ class="prepend-top-10"
+ v-if="isLoading"
+ size="2"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js
new file mode 100644
index 00000000000..939d17129de
--- /dev/null
+++ b/app/assets/javascripts/jobs/job_details_bundle.js
@@ -0,0 +1,68 @@
+/* global Flash */
+
+import Vue from 'vue';
+import JobMediator from './job_details_mediator';
+import jobHeader from './components/header.vue';
+import detailsBlock from './components/sidebar_details_block.vue';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const dataset = document.getElementById('js-job-details-vue').dataset;
+ const mediator = new JobMediator({ endpoint: dataset.endpoint });
+
+ mediator.fetchJob();
+
+ // Header
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: '#js-build-header-vue',
+ data() {
+ return {
+ mediator,
+ };
+ },
+ components: {
+ jobHeader,
+ },
+ mounted() {
+ this.mediator.initBuildClass();
+ },
+ updated() {
+ // Wait for flash message to be appended
+ Vue.nextTick(() => {
+ if (this.mediator.build) {
+ this.mediator.build.verifyTopPosition();
+ }
+ });
+ },
+ render(createElement) {
+ return createElement('job-header', {
+ props: {
+ isLoading: this.mediator.state.isLoading,
+ job: this.mediator.store.state.job,
+ },
+ });
+ },
+ });
+
+ // Sidebar information block
+ // eslint-disable-next-line
+ new Vue({
+ el: '#js-details-block-vue',
+ data() {
+ return {
+ mediator,
+ };
+ },
+ components: {
+ detailsBlock,
+ },
+ render(createElement) {
+ return createElement('details-block', {
+ props: {
+ isLoading: this.mediator.state.isLoading,
+ job: this.mediator.store.state.job,
+ },
+ });
+ },
+ });
+});
diff --git a/app/assets/javascripts/jobs/job_details_mediator.js b/app/assets/javascripts/jobs/job_details_mediator.js
new file mode 100644
index 00000000000..063c52fac74
--- /dev/null
+++ b/app/assets/javascripts/jobs/job_details_mediator.js
@@ -0,0 +1,67 @@
+/* global Flash */
+/* global Build */
+
+import Visibility from 'visibilityjs';
+import Poll from '../lib/utils/poll';
+import JobStore from './stores/job_store';
+import JobService from './services/job_service';
+import '../build';
+
+export default class JobMediator {
+ constructor(options = {}) {
+ this.options = options;
+
+ this.store = new JobStore();
+ this.service = new JobService(options.endpoint);
+
+ this.state = {
+ isLoading: false,
+ };
+ }
+
+ initBuildClass() {
+ this.build = new Build();
+ }
+
+ fetchJob() {
+ this.poll = new Poll({
+ resource: this.service,
+ method: 'getJob',
+ successCallback: this.successCallback.bind(this),
+ errorCallback: this.errorCallback.bind(this),
+ });
+
+ if (!Visibility.hidden()) {
+ this.state.isLoading = true;
+ this.poll.makeRequest();
+ } else {
+ this.getJob();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ this.poll.restart();
+ } else {
+ this.poll.stop();
+ }
+ });
+ }
+
+ getJob() {
+ return this.service.getJob()
+ .then(response => this.successCallback(response))
+ .catch(() => this.errorCallback());
+ }
+
+ successCallback(response) {
+ const data = response.json();
+ this.state.isLoading = false;
+ this.store.storeJob(data);
+ }
+
+ errorCallback() {
+ this.state.isLoading = false;
+
+ return new Flash('An error occurred while fetching the job.');
+ }
+}
diff --git a/app/assets/javascripts/jobs/services/job_service.js b/app/assets/javascripts/jobs/services/job_service.js
new file mode 100644
index 00000000000..eaf1c6e500a
--- /dev/null
+++ b/app/assets/javascripts/jobs/services/job_service.js
@@ -0,0 +1,14 @@
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
+
+export default class JobService {
+ constructor(endpoint) {
+ this.job = Vue.resource(endpoint);
+ }
+
+ getJob() {
+ return this.job.get();
+ }
+}
diff --git a/app/assets/javascripts/jobs/stores/job_store.js b/app/assets/javascripts/jobs/stores/job_store.js
new file mode 100644
index 00000000000..766194b8387
--- /dev/null
+++ b/app/assets/javascripts/jobs/stores/job_store.js
@@ -0,0 +1,11 @@
+export default class JobStore {
+ constructor() {
+ this.state = {
+ job: {},
+ };
+ }
+
+ storeJob(job = {}) {
+ this.state.job = job;
+ }
+}
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index a537267643e..2aca86189fd 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -167,8 +167,8 @@
if the name does not exist this function will return `null`
otherwise it will return the value of the param key provided
*/
- w.gl.utils.getParameterByName = (name) => {
- const url = window.location.href;
+ w.gl.utils.getParameterByName = (name, parseUrl) => {
+ const url = parseUrl || window.location.href;
name = name.replace(/[[\]]/g, '\\$&');
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
const results = regex.exec(url);
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 40eadd9396c..54c0da3fc9c 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -146,3 +146,24 @@ window.dateFormat = dateFormat;
};
})(window);
}).call(window);
+
+/**
+ * Port of ruby helper time_interval_in_words.
+ *
+ * @param {Number} seconds
+ * @return {String}
+ */
+// eslint-disable-next-line import/prefer-default-export
+export function timeIntervalInWords(intervalInSeconds) {
+ const secondsInteger = parseInt(intervalInSeconds, 10);
+ const minutes = Math.floor(secondsInteger / 60);
+ const seconds = secondsInteger - (minutes * 60);
+ let text = '';
+
+ if (minutes >= 1) {
+ text = `${minutes} ${gl.text.pluralize('minute', minutes)} ${seconds} ${gl.text.pluralize('second', seconds)}`;
+ } else {
+ text = `${seconds} ${gl.text.pluralize('second', seconds)}`;
+ }
+ return text;
+}
diff --git a/app/assets/javascripts/locale/bg/app.js b/app/assets/javascripts/locale/bg/app.js
new file mode 100644
index 00000000000..ba56c0bea25
--- /dev/null
+++ b/app/assets/javascripts/locale/bg/app.js
@@ -0,0 +1 @@
+var locales = locales || {}; locales['bg'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-05-04 19:24-0500","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","PO-Revision-Date":"2017-06-05 09:40-0400","Last-Translator":"Lyubomir Vasilev <lyubomirv@abv.bg>","Language-Team":"Bulgarian","Language":"bg","X-Generator":"Zanata 3.9.6","Plural-Forms":"nplurals=2; plural=(n != 1)","lang":"bg","domain":"app","plural_forms":"nplurals=2; plural=(n != 1)"},"ByAuthor|by":["от"],"Commit":["Подаване","Подавания"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["Анализът на циклите дава общ поглед върху това колко време е нужно на една идея да се превърне в завършена функционалност в проекта."],"CycleAnalyticsStage|Code":["Програмиране"],"CycleAnalyticsStage|Issue":["Проблем"],"CycleAnalyticsStage|Plan":["Планиране"],"CycleAnalyticsStage|Production":["Издаване"],"CycleAnalyticsStage|Review":["Преглед и одобрение"],"CycleAnalyticsStage|Staging":["Подготовка за издаване"],"CycleAnalyticsStage|Test":["Тестване"],"Deploy":["Внедряване","Внедрявания"],"FirstPushedBy|First":["Първо"],"FirstPushedBy|pushed by":["изпращане на промени от"],"From issue creation until deploy to production":["От създаването на проблема до внедряването в крайната версия"],"From merge request merge until deploy to production":["От прилагането на заявката за сливане до внедряването в крайната версия"],"Introducing Cycle Analytics":["Представяме Ви анализът на циклите"],"Last %d day":["Последния %d ден","Последните %d дни"],"Limited to showing %d event at most":["Ограничено до показване на последното %d събитие","Ограничено до показване на последните %d събития"],"Median":["Медиана"],"New Issue":["Нов проблем","Нови проблема"],"Not available":["Не е налично"],"Not enough data":["Няма достатъчно данни"],"OpenedNDaysAgo|Opened":["Отворен"],"Pipeline Health":["Състояние"],"ProjectLifecycle|Stage":["Етап"],"Read more":["Прочетете повече"],"Related Commits":["Свързани подавания"],"Related Deployed Jobs":["Свързани задачи за внедряване"],"Related Issues":["Свързани проблеми"],"Related Jobs":["Свързани задачи"],"Related Merge Requests":["Свързани заявки за сливане"],"Related Merged Requests":["Свързани приложени заявки за сливане"],"Showing %d event":["Показване на %d събитие","Показване на %d събития"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["Етапът на програмиране показва времето от първото подаване до създаването на заявката за сливане. Данните ще бъдат добавени тук автоматично след като бъде създадена първата заявка за сливане."],"The collection of events added to the data gathered for that stage.":["Съвкупността от събития добавени към данните събрани за този етап."],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["Етапът на проблемите показва колко е времето от създаването на проблем до определянето на целеви етап на проекта за него, или до добавянето му в списък на дъската за проблеми. Започнете да добавяте проблеми, за да видите данните за този етап."],"The phase of the development lifecycle.":["Етапът от цикъла на разработка"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["Етапът на планиране показва колко е времето от преходната стъпка до изпращането на първото подаване. Това време ще бъде добавено автоматично след като изпратите първото си подаване."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["Етапът на издаване показва общото време, което е нужно от създаването на проблем до внедряването на кода в крайната версия."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["Етапът на преглед и одобрение показва времето от създаването на заявката за сливане до прилагането ѝ. Данните ще бъдат добавени автоматично след като приложите първата си заявка за сливане."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["Етапът на подготовка за издаване показва времето между прилагането на заявката за сливане и внедряването на кода в средата на работещата крайна версия. Данните ще бъдат добавени автоматично след като направите първото си внедряване в крайната версия."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["Етапът на тестване показва времето, което е нужно на „Gitlab CI“ да изпълни всички задачи за свързаната заявка за сливане. Данните ще бъдат добавени автоматично след като приключи изпълнените на първата Ви такава задача."],"The time taken by each data entry gathered by that stage.":["Времето, което отнема всеки запис от данни за съответния етап."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["Стойността, която се намира в средата на последователността от наблюдавани данни. Например: медианата на 3, 5 и 9 е 5, а медианата на 3, 5, 7 и 8 е (5+7)/2 = 6."],"Time before an issue gets scheduled":["Време преди един проблем да бъде планиран за работа"],"Time before an issue starts implementation":["Време преди работата по проблем да започне"],"Time between merge request creation and merge/close":["Време между създаване на заявка за сливане и прилагането/отхвърлянето ѝ"],"Time until first merge request":["Време преди първата заявка за сливане"],"Time|hr":["час","часа"],"Time|min":["мин","мин"],"Time|s":["сек"],"Total Time":["Общо време"],"Total test time for all commits/merges":["Общо време за тестване на всички подавания/сливания"],"Want to see the data? Please ask an administrator for access.":["Искате ли да видите данните? Помолете администратор за достъп."],"We don't have enough data to show this stage.":["Няма достатъчно данни за този етап."],"You need permission.":["Нуждаете се от разрешение."],"day":["ден","дни"]}}}; \ No newline at end of file
diff --git a/app/assets/javascripts/locale/de/app.js b/app/assets/javascripts/locale/de/app.js
index 960b8e5ecb3..e7d2b174405 100644
--- a/app/assets/javascripts/locale/de/app.js
+++ b/app/assets/javascripts/locale/de/app.js
@@ -1 +1 @@
-var locales = locales || {}; locales['de'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-06-07 12:17+0200","Language-Team":"German","Language":"de","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","Last-Translator":"Bob Van Landuyt <bob@gitlab.com>","X-Generator":"Poedit 2.0.2","lang":"de","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"About auto deploy":[""],"Activity":[""],"Add Changelog":[""],"Add Contribution guide":[""],"Add License":[""],"Add an SSH key to your profile to pull or push via SSH.":[""],"Add new directory":[""],"Archived project! Repository is read-only":[""],"Branch":["",""],"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}":[""],"Branches":[""],"ByAuthor|by":["Von"],"CI configuration":[""],"Changelog":[""],"Charts":[""],"CiStatusLabel|canceled":[""],"CiStatusLabel|created":[""],"CiStatusLabel|failed":[""],"CiStatusLabel|manual action":[""],"CiStatusLabel|passed":[""],"CiStatusLabel|passed with warnings":[""],"CiStatusLabel|pending":[""],"CiStatusLabel|skipped":[""],"CiStatusLabel|waiting for manual action":[""],"CiStatusText|blocked":[""],"CiStatusText|canceled":[""],"CiStatusText|created":[""],"CiStatusText|failed":[""],"CiStatusText|manual":[""],"CiStatusText|passed":[""],"CiStatusText|pending":[""],"CiStatusText|skipped":[""],"CiStatus|running":[""],"Commit":["Commit","Commits"],"CommitMessage|Add %{file_name}":[""],"Commits":["Commits"],"Commits|History":["Commits"],"Compare":[""],"Contribution guide":[""],"Contributors":[""],"Copy URL to clipboard":[""],"Copy commit SHA to clipboard":[""],"Create New Directory":[""],"Create directory":[""],"Create empty bare repository":[""],"Create merge request":[""],"CreateNewFork|Fork":[""],"Custom notification events":[""],"Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}.":[""],"Cycle Analytics":[""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["Cycle Analytics liefern einen Überblick darüber, wie viel Zeit in Ihrem Projekt von einer Idee bis zum Produktivdeployment vergeht."],"CycleAnalyticsStage|Code":["Code"],"CycleAnalyticsStage|Issue":["Issue"],"CycleAnalyticsStage|Plan":["Planung"],"CycleAnalyticsStage|Production":["Produktiv"],"CycleAnalyticsStage|Review":["Review"],"CycleAnalyticsStage|Staging":["Staging"],"CycleAnalyticsStage|Test":["Test"],"Deploy":["Deployment","Deployments"],"Directory name":[""],"Don't show again":[""],"Download tar":[""],"Download tar.bz2":[""],"Download tar.gz":[""],"Download zip":[""],"DownloadArtifacts|Download":[""],"DownloadSource|Download":[""],"Files":[""],"Find by path":[""],"Find file":[""],"FirstPushedBy|First":["Erster"],"FirstPushedBy|pushed by":["gepusht von"],"ForkedFromProjectPath|Forked from":[""],"Forks":[""],"From issue creation until deploy to production":["Vom Anlegen des Issues bis zum Produktivdeployment"],"From merge request merge until deploy to production":["Vom Merge Request bis zum Produktivdeployment"],"Go to your fork":[""],"GoToYourFork|Fork":[""],"Home":[""],"Housekeeping successfully started":[""],"Import repository":[""],"Introducing Cycle Analytics":["Was sind Cycle Analytics?"],"LFSStatus|Disabled":[""],"LFSStatus|Enabled":[""],"Last %d day":["Letzter %d Tag","Letzten %d Tage"],"Last Update":[""],"Last commit":[""],"Leave group":[""],"Leave project":[""],"Limited to showing %d event at most":["Eingeschränkt auf maximal %d Ereignis","Eingeschränkt auf maximal %d Ereignisse"],"Median":["Median"],"MissingSSHKeyWarningLink|add an SSH key":[""],"New Issue":["Neues Issue","Neue Issues"],"New branch":[""],"New directory":[""],"New file":[""],"New issue":["Neues Issue"],"New merge request":[""],"New snippet":[""],"New tag":[""],"No repository":[""],"Not available":["Nicht verfügbar"],"Not enough data":["Nicht genügend Daten"],"Notification events":[""],"NotificationEvent|Close issue":[""],"NotificationEvent|Close merge request":[""],"NotificationEvent|Failed pipeline":[""],"NotificationEvent|Merge merge request":[""],"NotificationEvent|New issue":[""],"NotificationEvent|New merge request":[""],"NotificationEvent|New note":[""],"NotificationEvent|Reassign issue":[""],"NotificationEvent|Reassign merge request":[""],"NotificationEvent|Reopen issue":[""],"NotificationEvent|Successful pipeline":[""],"NotificationLevel|Custom":[""],"NotificationLevel|Disabled":[""],"NotificationLevel|Global":[""],"NotificationLevel|On mention":[""],"NotificationLevel|Participate":[""],"NotificationLevel|Watch":[""],"OpenedNDaysAgo|Opened":["Erstellt"],"Pipeline Health":["Pipeline Kennzahlen"],"Project '%{project_name}' queued for deletion.":[""],"Project '%{project_name}' was successfully created.":[""],"Project '%{project_name}' was successfully updated.":[""],"Project '%{project_name}' will be deleted.":[""],"Project access must be granted explicitly to each user.":[""],"Project export could not be deleted.":[""],"Project export has been deleted.":[""],"Project export link has expired. Please generate a new export from your project settings.":[""],"Project export started. A download link will be sent by email.":[""],"Project home":[""],"ProjectFeature|Disabled":[""],"ProjectFeature|Everyone with access":[""],"ProjectFeature|Only team members":[""],"ProjectFileTree|Name":[""],"ProjectLastActivity|Never":[""],"ProjectLifecycle|Stage":["Phase"],"ProjectNetworkGraph|Graph":[""],"Read more":["Mehr"],"Readme":[""],"RefSwitcher|Branches":[""],"RefSwitcher|Tags":[""],"Related Commits":["Zugehörige Commits"],"Related Deployed Jobs":["Zugehörige Deploymentjobs"],"Related Issues":["Zugehörige Issues"],"Related Jobs":["Zugehörige Jobs"],"Related Merge Requests":["Zugehörige Merge Requests"],"Related Merged Requests":["Zugehörige abgeschlossene Merge Requests"],"Remind later":[""],"Remove project":[""],"Request Access":[""],"Search branches and tags":[""],"Select Archive Format":[""],"Set a password on your account to pull or push via %{protocol}":[""],"Set up CI":[""],"Set up Koding":[""],"Set up auto deploy":[""],"SetPasswordToCloneLink|set a password":[""],"Showing %d event":["Zeige %d Ereignis","Zeige %d Ereignisse"],"Source code":[""],"StarProject|Star":[""],"Switch branch/tag":[""],"Tag":["",""],"Tags":[""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["Die Code-Phase stellt die Zeit vom ersten Commit bis zum Erstellen eines Merge Requests dar. Sobald Sie Ihren ersten Merge Request anlegen, werden dessen Daten automatisch ergänzt."],"The collection of events added to the data gathered for that stage.":["Ereignisse, die für diese Phase ausgewertet wurden."],"The fork relationship has been removed.":[""],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["Die Issue-Phase stellt die Zeit vom Anlegen eines Issues bis zum Zuweisen eines Meilensteins oder Hinzufügen zum Issue Board dar. Erstellen Sie einen Issue, damit dessen Daten hier erscheinen."],"The phase of the development lifecycle.":["Die Phase im Entwicklungsprozess."],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["Die Planungsphase stellt die Zeit von der vorherigen Phase bis zum Pushen des ersten Commits dar. Sobald Sie den ersten Commit pushen, werden dessen Daten hier erscheinen."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["Die Produktiv-Phase stellt die Gesamtzeit vom Anlegen eines Issues bis zum Deployment auf dem Produktivsystem dar. Sobald Sie den vollständigen Entwicklungszyklus von einer Idee bis zum Produktivdeployment durchlaufen haben, erscheinen die zugehörigen Daten hier."],"The project can be accessed by any logged in user.":[""],"The project can be accessed without any authentication.":[""],"The repository for this project does not exist.":[""],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["Die Review-Phase stellt die Zeit vom Anlegen eines Merge Requests bis zum Mergen dar. Sobald Sie Ihren ersten Merge Request abschließen, werden dessen Daten hier automatisch angezeigt."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["Die Staging-Phase stellt die Zeit zwischen Mergen eines Merge Requests und dem Produktivdeployment dar. Sobald Sie das erste Produktivdeployment durchgeführt haben, werden dessen Daten hier automatisch angezeigt."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["Die Test-Phase stellt die Zeit dar, die GitLab CI benötigt um die Pipelines von Merge Requests abzuarbeiten. Sobald die erste Pipeline abgeschlossen ist, werden deren Daten hier automatisch angezeigt."],"The time taken by each data entry gathered by that stage.":["Zeit die für das jeweilige Ereignis in der Phase ermittelt wurde."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["Der mittlere aller erfassten Werte. Zum Beispiel ist für 3, 5, 9 der Median 5. Bei 3, 5, 7, 8 ist der Median (5+7)/2 = 6."],"This means you can not push code until you create an empty repository or import existing one.":[""],"Time before an issue gets scheduled":["Zeit bis ein Issue geplant wird"],"Time before an issue starts implementation":["Zeit bis die Implementierung für ein Issue beginnt"],"Time between merge request creation and merge/close":["Zeit zwischen Anlegen und Mergen/Schließen eines Merge Requests"],"Time until first merge request":["Zeit bis zum ersten Merge Request"],"Timeago|%s days ago":[""],"Timeago|%s days remaining":[""],"Timeago|%s hours remaining":[""],"Timeago|%s minutes ago":[""],"Timeago|%s minutes remaining":[""],"Timeago|%s months ago":[""],"Timeago|%s months remaining":[""],"Timeago|%s seconds remaining":[""],"Timeago|%s weeks ago":[""],"Timeago|%s weeks remaining":[""],"Timeago|%s years ago":[""],"Timeago|%s years remaining":[""],"Timeago|1 day remaining":[""],"Timeago|1 hour remaining":[""],"Timeago|1 minute remaining":[""],"Timeago|1 month remaining":[""],"Timeago|1 week remaining":[""],"Timeago|1 year remaining":[""],"Timeago|Past due":[""],"Timeago|a day ago":[""],"Timeago|a month ago":[""],"Timeago|a week ago":[""],"Timeago|a while":[""],"Timeago|a year ago":[""],"Timeago|about %s hours ago":[""],"Timeago|about a minute ago":[""],"Timeago|about an hour ago":[""],"Timeago|in %s days":[""],"Timeago|in %s hours":[""],"Timeago|in %s minutes":[""],"Timeago|in %s months":[""],"Timeago|in %s seconds":[""],"Timeago|in %s weeks":[""],"Timeago|in %s years":[""],"Timeago|in 1 day":[""],"Timeago|in 1 hour":[""],"Timeago|in 1 minute":[""],"Timeago|in 1 month":[""],"Timeago|in 1 week":[""],"Timeago|in 1 year":[""],"Timeago|less than a minute ago":[""],"Time|hr":["h","h"],"Time|min":["min","min"],"Time|s":["s"],"Total Time":["Gesamtzeit"],"Total test time for all commits/merges":["Gesamte Testlaufzeit für alle Commits/Merges"],"Unstar":[""],"Upload New File":[""],"Upload file":[""],"Use your global notification setting":[""],"VisibilityLevel|Internal":[""],"VisibilityLevel|Private":[""],"VisibilityLevel|Public":[""],"Want to see the data? Please ask an administrator for access.":["Um diese Daten einsehen zu können, wenden Sie sich bitte an Ihren Administrator."],"We don't have enough data to show this stage.":["Es liegen nicht genügend Daten vor, um diese Phase anzuzeigen."],"Withdraw Access Request":[""],"You are going to remove %{project_name_with_namespace}.\\nRemoved project CANNOT be restored!\\nAre you ABSOLUTELY sure?":[""],"You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?":[""],"You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?":[""],"You can only add files when you are on a branch":[""],"You must sign in to star a project":[""],"You need permission.":["Sie benötigen Zugriffsrechte."],"You will not get any notifications via email":[""],"You will only receive notifications for the events you choose":[""],"You will only receive notifications for threads you have participated in":[""],"You will receive notifications for any activity":[""],"You will receive notifications only for comments in which you were @mentioned":[""],"You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account":[""],"You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile":[""],"Your name":[""],"committed":[""],"day":["Tag","Tage"],"notification emails":[""]}}}; \ No newline at end of file
+var locales = locales || {}; locales['de'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-05-09 13:44+0200","Language-Team":"German","Language":"de","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","Last-Translator":"","X-Generator":"Poedit 2.0.1","lang":"de","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"Are you sure you want to delete this pipeline schedule?":[""],"ByAuthor|by":["Von"],"Cancel":[""],"Commit":["Commit","Commits"],"Cron Timezone":[""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["Cycle Analytics liefern einen Überblick darüber, wie viel Zeit in Ihrem Projekt von einer Idee bis zum Produktivdeployment vergeht."],"CycleAnalyticsStage|Code":["Code"],"CycleAnalyticsStage|Issue":["Issue"],"CycleAnalyticsStage|Plan":["Planung"],"CycleAnalyticsStage|Production":["Produktiv"],"CycleAnalyticsStage|Review":["Review"],"CycleAnalyticsStage|Staging":["Staging"],"CycleAnalyticsStage|Test":["Test"],"Delete":[""],"Deploy":["Deployment","Deployments"],"Description":[""],"Edit":[""],"Edit Pipeline Schedule %{id}":[""],"Failed to change the owner":[""],"Failed to remove the pipeline schedule":[""],"Filter":[""],"FirstPushedBy|First":["Erster"],"FirstPushedBy|pushed by":["gepusht von"],"From issue creation until deploy to production":["Vom Anlegen des Issues bis zum Produktivdeployment"],"From merge request merge until deploy to production":["Vom Merge Request bis zum Produktivdeployment"],"Interval Pattern":[""],"Introducing Cycle Analytics":["Was sind Cycle Analytics?"],"Last %d day":["Letzter %d Tag","Letzten %d Tage"],"Last Pipeline":[""],"Limited to showing %d event at most":["Eingeschränkt auf maximal %d Ereignis","Eingeschränkt auf maximal %d Ereignisse"],"Median":["Median"],"New Issue":["Neues Issue","Neue Issues"],"New Pipeline Schedule":[""],"No schedules":[""],"Not available":["Nicht verfügbar"],"Not enough data":["Nicht genügend Daten"],"OpenedNDaysAgo|Opened":["Erstellt"],"Owner":[""],"Pipeline Health":["Pipeline Kennzahlen"],"Pipeline Schedule":[""],"Pipeline Schedules":[""],"PipelineSchedules|Activated":[""],"PipelineSchedules|Active":[""],"PipelineSchedules|All":[""],"PipelineSchedules|Inactive":[""],"PipelineSchedules|Next Run":[""],"PipelineSchedules|None":[""],"PipelineSchedules|Provide a short description for this pipeline":[""],"PipelineSchedules|Take ownership":[""],"PipelineSchedules|Target":[""],"ProjectLifecycle|Stage":["Phase"],"Read more":["Mehr"],"Related Commits":["Zugehörige Commits"],"Related Deployed Jobs":["Zugehörige Deploymentjobs"],"Related Issues":["Zugehörige Issues"],"Related Jobs":["Zugehörige Jobs"],"Related Merge Requests":["Zugehörige Merge Requests"],"Related Merged Requests":["Zugehörige abgeschlossene Merge Requests"],"Save pipeline schedule":[""],"Schedule a new pipeline":[""],"Select a timezone":[""],"Select target branch":[""],"Showing %d event":["Zeige %d Ereignis","Zeige %d Ereignisse"],"Target Branch":[""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["Die Code-Phase stellt die Zeit vom ersten Commit bis zum Erstellen eines Merge Requests dar. Sobald Sie Ihren ersten Merge Request anlegen, werden dessen Daten automatisch ergänzt."],"The collection of events added to the data gathered for that stage.":["Ereignisse, die für diese Phase ausgewertet wurden."],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["Die Issue-Phase stellt die Zeit vom Anlegen eines Issues bis zum Zuweisen eines Meilensteins oder Hinzufügen zum Issue Board dar. Erstellen Sie einen Issue, damit dessen Daten hier erscheinen."],"The phase of the development lifecycle.":["Die Phase im Entwicklungsprozess."],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["Die Planungsphase stellt die Zeit von der vorherigen Phase bis zum Pushen des ersten Commits dar. Sobald Sie den ersten Commit pushen, werden dessen Daten hier erscheinen."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["Die Produktiv-Phase stellt die Gesamtzeit vom Anlegen eines Issues bis zum Deployment auf dem Produktivsystem dar. Sobald Sie den vollständigen Entwicklungszyklus von einer Idee bis zum Produktivdeployment durchlaufen haben, erscheinen die zugehörigen Daten hier."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["Die Review-Phase stellt die Zeit vom Anlegen eines Merge Requests bis zum Mergen dar. Sobald Sie Ihren ersten Merge Request abschließen, werden dessen Daten hier automatisch angezeigt."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["Die Staging-Phase stellt die Zeit zwischen Mergen eines Merge Requests und dem Produktivdeployment dar. Sobald Sie das erste Produktivdeployment durchgeführt haben, werden dessen Daten hier automatisch angezeigt."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["Die Test-Phase stellt die Zeit dar, die GitLab CI benötigt um die Pipelines von Merge Requests abzuarbeiten. Sobald die erste Pipeline abgeschlossen ist, werden deren Daten hier automatisch angezeigt."],"The time taken by each data entry gathered by that stage.":["Zeit die für das jeweilige Ereignis in der Phase ermittelt wurde."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["Der mittlere aller erfassten Werte. Zum Beispiel ist für 3, 5, 9 der Median 5. Bei 3, 5, 7, 8 ist der Median (5+7)/2 = 6."],"Time before an issue gets scheduled":["Zeit bis ein Issue geplant wird"],"Time before an issue starts implementation":["Zeit bis die Implementierung für ein Issue beginnt"],"Time between merge request creation and merge/close":["Zeit zwischen Anlegen und Mergen/Schließen eines Merge Requests"],"Time until first merge request":["Zeit bis zum ersten Merge Request"],"Time|hr":["h","h"],"Time|min":["min","min"],"Time|s":["s"],"Total Time":["Gesamtzeit"],"Total test time for all commits/merges":["Gesamte Testlaufzeit für alle Commits/Merges"],"Want to see the data? Please ask an administrator for access.":["Um diese Daten einsehen zu können, wenden Sie sich bitte an Ihren Administrator."],"We don't have enough data to show this stage.":["Es liegen nicht genügend Daten vor, um diese Phase anzuzeigen."],"You need permission.":["Sie benötigen Zugriffsrechte."],"day":["Tag","Tage"]}}}; \ No newline at end of file
diff --git a/app/assets/javascripts/locale/en/app.js b/app/assets/javascripts/locale/en/app.js
index 80203308b00..0bb76c80b7a 100644
--- a/app/assets/javascripts/locale/en/app.js
+++ b/app/assets/javascripts/locale/en/app.js
@@ -1 +1 @@
-var locales = locales || {}; locales['en'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-06-07 12:14+0200","Language-Team":"English","Language":"en","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","Last-Translator":"Bob Van Landuyt <bob@gitlab.com>","X-Generator":"Poedit 2.0.2","lang":"en","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"About auto deploy":[""],"Activity":[""],"Add Changelog":[""],"Add Contribution guide":[""],"Add License":[""],"Add an SSH key to your profile to pull or push via SSH.":[""],"Add new directory":[""],"Archived project! Repository is read-only":[""],"Branch":["",""],"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}":[""],"Branches":[""],"ByAuthor|by":[""],"CI configuration":[""],"Changelog":[""],"Charts":[""],"CiStatusLabel|canceled":[""],"CiStatusLabel|created":[""],"CiStatusLabel|failed":[""],"CiStatusLabel|manual action":[""],"CiStatusLabel|passed":[""],"CiStatusLabel|passed with warnings":[""],"CiStatusLabel|pending":[""],"CiStatusLabel|skipped":[""],"CiStatusLabel|waiting for manual action":[""],"CiStatusText|blocked":[""],"CiStatusText|canceled":[""],"CiStatusText|created":[""],"CiStatusText|failed":[""],"CiStatusText|manual":[""],"CiStatusText|passed":[""],"CiStatusText|pending":[""],"CiStatusText|skipped":[""],"CiStatus|running":[""],"Commit":["",""],"CommitMessage|Add %{file_name}":[""],"Commits":[""],"Commits|History":[""],"Compare":[""],"Contribution guide":[""],"Contributors":[""],"Copy URL to clipboard":[""],"Copy commit SHA to clipboard":[""],"Create New Directory":[""],"Create directory":[""],"Create empty bare repository":[""],"Create merge request":[""],"CreateNewFork|Fork":[""],"Custom notification events":[""],"Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}.":[""],"Cycle Analytics":[""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":[""],"CycleAnalyticsStage|Code":[""],"CycleAnalyticsStage|Issue":[""],"CycleAnalyticsStage|Plan":[""],"CycleAnalyticsStage|Production":[""],"CycleAnalyticsStage|Review":[""],"CycleAnalyticsStage|Staging":[""],"CycleAnalyticsStage|Test":[""],"Deploy":["",""],"Directory name":[""],"Don't show again":[""],"Download tar":[""],"Download tar.bz2":[""],"Download tar.gz":[""],"Download zip":[""],"DownloadArtifacts|Download":[""],"DownloadSource|Download":[""],"Files":[""],"Find by path":[""],"Find file":[""],"FirstPushedBy|First":[""],"FirstPushedBy|pushed by":[""],"ForkedFromProjectPath|Forked from":[""],"Forks":[""],"From issue creation until deploy to production":[""],"From merge request merge until deploy to production":[""],"Go to your fork":[""],"GoToYourFork|Fork":[""],"Home":[""],"Housekeeping successfully started":[""],"Import repository":[""],"Introducing Cycle Analytics":[""],"LFSStatus|Disabled":[""],"LFSStatus|Enabled":[""],"Last %d day":["",""],"Last Update":[""],"Last commit":[""],"Leave group":[""],"Leave project":[""],"Limited to showing %d event at most":["",""],"Median":[""],"MissingSSHKeyWarningLink|add an SSH key":[""],"New Issue":["",""],"New branch":[""],"New directory":[""],"New file":[""],"New issue":[""],"New merge request":[""],"New snippet":[""],"New tag":[""],"No repository":[""],"Not available":[""],"Not enough data":[""],"Notification events":[""],"NotificationEvent|Close issue":[""],"NotificationEvent|Close merge request":[""],"NotificationEvent|Failed pipeline":[""],"NotificationEvent|Merge merge request":[""],"NotificationEvent|New issue":[""],"NotificationEvent|New merge request":[""],"NotificationEvent|New note":[""],"NotificationEvent|Reassign issue":[""],"NotificationEvent|Reassign merge request":[""],"NotificationEvent|Reopen issue":[""],"NotificationEvent|Successful pipeline":[""],"NotificationLevel|Custom":[""],"NotificationLevel|Disabled":[""],"NotificationLevel|Global":[""],"NotificationLevel|On mention":[""],"NotificationLevel|Participate":[""],"NotificationLevel|Watch":[""],"OpenedNDaysAgo|Opened":[""],"Pipeline Health":[""],"Project '%{project_name}' queued for deletion.":[""],"Project '%{project_name}' was successfully created.":[""],"Project '%{project_name}' was successfully updated.":[""],"Project '%{project_name}' will be deleted.":[""],"Project access must be granted explicitly to each user.":[""],"Project export could not be deleted.":[""],"Project export has been deleted.":[""],"Project export link has expired. Please generate a new export from your project settings.":[""],"Project export started. A download link will be sent by email.":[""],"Project home":[""],"ProjectFeature|Disabled":[""],"ProjectFeature|Everyone with access":[""],"ProjectFeature|Only team members":[""],"ProjectFileTree|Name":[""],"ProjectLastActivity|Never":[""],"ProjectLifecycle|Stage":[""],"ProjectNetworkGraph|Graph":[""],"Read more":[""],"Readme":[""],"RefSwitcher|Branches":[""],"RefSwitcher|Tags":[""],"Related Commits":[""],"Related Deployed Jobs":[""],"Related Issues":[""],"Related Jobs":[""],"Related Merge Requests":[""],"Related Merged Requests":[""],"Remind later":[""],"Remove project":[""],"Request Access":[""],"Search branches and tags":[""],"Select Archive Format":[""],"Set a password on your account to pull or push via %{protocol}":[""],"Set up CI":[""],"Set up Koding":[""],"Set up auto deploy":[""],"SetPasswordToCloneLink|set a password":[""],"Showing %d event":["",""],"Source code":[""],"StarProject|Star":[""],"Switch branch/tag":[""],"Tag":["",""],"Tags":[""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":[""],"The collection of events added to the data gathered for that stage.":[""],"The fork relationship has been removed.":[""],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":[""],"The phase of the development lifecycle.":[""],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":[""],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":[""],"The project can be accessed by any logged in user.":[""],"The project can be accessed without any authentication.":[""],"The repository for this project does not exist.":[""],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":[""],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":[""],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":[""],"The time taken by each data entry gathered by that stage.":[""],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":[""],"This means you can not push code until you create an empty repository or import existing one.":[""],"Time before an issue gets scheduled":[""],"Time before an issue starts implementation":[""],"Time between merge request creation and merge/close":[""],"Time until first merge request":[""],"Timeago|%s days ago":[""],"Timeago|%s days remaining":[""],"Timeago|%s hours remaining":[""],"Timeago|%s minutes ago":[""],"Timeago|%s minutes remaining":[""],"Timeago|%s months ago":[""],"Timeago|%s months remaining":[""],"Timeago|%s seconds remaining":[""],"Timeago|%s weeks ago":[""],"Timeago|%s weeks remaining":[""],"Timeago|%s years ago":[""],"Timeago|%s years remaining":[""],"Timeago|1 day remaining":[""],"Timeago|1 hour remaining":[""],"Timeago|1 minute remaining":[""],"Timeago|1 month remaining":[""],"Timeago|1 week remaining":[""],"Timeago|1 year remaining":[""],"Timeago|Past due":[""],"Timeago|a day ago":[""],"Timeago|a month ago":[""],"Timeago|a week ago":[""],"Timeago|a while":[""],"Timeago|a year ago":[""],"Timeago|about %s hours ago":[""],"Timeago|about a minute ago":[""],"Timeago|about an hour ago":[""],"Timeago|in %s days":[""],"Timeago|in %s hours":[""],"Timeago|in %s minutes":[""],"Timeago|in %s months":[""],"Timeago|in %s seconds":[""],"Timeago|in %s weeks":[""],"Timeago|in %s years":[""],"Timeago|in 1 day":[""],"Timeago|in 1 hour":[""],"Timeago|in 1 minute":[""],"Timeago|in 1 month":[""],"Timeago|in 1 week":[""],"Timeago|in 1 year":[""],"Timeago|less than a minute ago":[""],"Time|hr":["",""],"Time|min":["",""],"Time|s":[""],"Total Time":[""],"Total test time for all commits/merges":[""],"Unstar":[""],"Upload New File":[""],"Upload file":[""],"Use your global notification setting":[""],"VisibilityLevel|Internal":[""],"VisibilityLevel|Private":[""],"VisibilityLevel|Public":[""],"Want to see the data? Please ask an administrator for access.":[""],"We don't have enough data to show this stage.":[""],"Withdraw Access Request":[""],"You are going to remove %{project_name_with_namespace}.\\nRemoved project CANNOT be restored!\\nAre you ABSOLUTELY sure?":[""],"You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?":[""],"You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?":[""],"You can only add files when you are on a branch":[""],"You must sign in to star a project":[""],"You need permission.":[""],"You will not get any notifications via email":[""],"You will only receive notifications for the events you choose":[""],"You will only receive notifications for threads you have participated in":[""],"You will receive notifications for any activity":[""],"You will receive notifications only for comments in which you were @mentioned":[""],"You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account":[""],"You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile":[""],"Your name":[""],"committed":[""],"day":["",""],"notification emails":[""]}}}; \ No newline at end of file
+var locales = locales || {}; locales['en'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-04-12 22:36-0500","Last-Translator":"FULL NAME <EMAIL@ADDRESS>","Language-Team":"English","Language":"en","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","lang":"en","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"Are you sure you want to delete this pipeline schedule?":[""],"ByAuthor|by":[""],"Cancel":[""],"Commit":["",""],"Cron Timezone":[""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":[""],"CycleAnalyticsStage|Code":[""],"CycleAnalyticsStage|Issue":[""],"CycleAnalyticsStage|Plan":[""],"CycleAnalyticsStage|Production":[""],"CycleAnalyticsStage|Review":[""],"CycleAnalyticsStage|Staging":[""],"CycleAnalyticsStage|Test":[""],"Delete":[""],"Deploy":["",""],"Description":[""],"Edit":[""],"Edit Pipeline Schedule %{id}":[""],"Failed to change the owner":[""],"Failed to remove the pipeline schedule":[""],"Filter":[""],"FirstPushedBy|First":[""],"FirstPushedBy|pushed by":[""],"From issue creation until deploy to production":[""],"From merge request merge until deploy to production":[""],"Interval Pattern":[""],"Introducing Cycle Analytics":[""],"Last %d day":["",""],"Last Pipeline":[""],"Limited to showing %d event at most":["",""],"Median":[""],"New Issue":["",""],"New Pipeline Schedule":[""],"No schedules":[""],"Not available":[""],"Not enough data":[""],"OpenedNDaysAgo|Opened":[""],"Owner":[""],"Pipeline Health":[""],"Pipeline Schedule":[""],"Pipeline Schedules":[""],"PipelineSchedules|Activated":[""],"PipelineSchedules|Active":[""],"PipelineSchedules|All":[""],"PipelineSchedules|Inactive":[""],"PipelineSchedules|Next Run":[""],"PipelineSchedules|None":[""],"PipelineSchedules|Provide a short description for this pipeline":[""],"PipelineSchedules|Take ownership":[""],"PipelineSchedules|Target":[""],"ProjectLifecycle|Stage":[""],"Read more":[""],"Related Commits":[""],"Related Deployed Jobs":[""],"Related Issues":[""],"Related Jobs":[""],"Related Merge Requests":[""],"Related Merged Requests":[""],"Save pipeline schedule":[""],"Schedule a new pipeline":[""],"Select a timezone":[""],"Select target branch":[""],"Showing %d event":["",""],"Target Branch":[""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":[""],"The collection of events added to the data gathered for that stage.":[""],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":[""],"The phase of the development lifecycle.":[""],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":[""],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":[""],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":[""],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":[""],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":[""],"The time taken by each data entry gathered by that stage.":[""],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":[""],"Time before an issue gets scheduled":[""],"Time before an issue starts implementation":[""],"Time between merge request creation and merge/close":[""],"Time until first merge request":[""],"Time|hr":["",""],"Time|min":["",""],"Time|s":[""],"Total Time":[""],"Total test time for all commits/merges":[""],"Want to see the data? Please ask an administrator for access.":[""],"We don't have enough data to show this stage.":[""],"You need permission.":[""],"day":["",""]}}}; \ No newline at end of file
diff --git a/app/assets/javascripts/locale/pt_BR/app.js b/app/assets/javascripts/locale/pt_BR/app.js
new file mode 100644
index 00000000000..f2eed3da064
--- /dev/null
+++ b/app/assets/javascripts/locale/pt_BR/app.js
@@ -0,0 +1 @@
+var locales = locales || {}; locales['pt_BR'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-05-04 19:24-0500","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","PO-Revision-Date":"2017-06-05 03:29-0400","Last-Translator":"Alexandre Alencar <alexandre.alencar@gmail.com>","Language-Team":"Portuguese (Brazil)","Language":"pt-BR","X-Generator":"Zanata 3.9.6","Plural-Forms":"nplurals=2; plural=(n != 1)","lang":"pt_BR","domain":"app","plural_forms":"nplurals=2; plural=(n != 1)"},"ByAuthor|by":["por"],"Commit":["Commit","Commits"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["A Análise de Ciclo fornece uma visão geral de quanto tempo uma ideia demora para ir para produção em seu projeto."],"CycleAnalyticsStage|Code":["Código"],"CycleAnalyticsStage|Issue":["Tarefa"],"CycleAnalyticsStage|Plan":["Plano"],"CycleAnalyticsStage|Production":["Produção"],"CycleAnalyticsStage|Review":["Revisão"],"CycleAnalyticsStage|Staging":["Homologação"],"CycleAnalyticsStage|Test":["Teste"],"Deploy":["Implantação","Implantações"],"FirstPushedBy|First":["Primeiro"],"FirstPushedBy|pushed by":["publicado por"],"From issue creation until deploy to production":["Da criação de tarefas até a implantação para a produção"],"From merge request merge until deploy to production":["Da incorporação do merge request até a implantação em produção"],"Introducing Cycle Analytics":["Apresentando a Análise de Ciclo"],"Last %d day":["Último %d dia","Últimos %d dias"],"Limited to showing %d event at most":["Limitado a mostrar %d evento no máximo","Limitado a mostrar %d eventos no máximo"],"Median":["Mediana"],"New Issue":["Nova Tarefa","Novas Tarefas"],"Not available":["Não disponível"],"Not enough data":["Dados insuficientes"],"OpenedNDaysAgo|Opened":["Aberto"],"Pipeline Health":["Saúde da Pipeline"],"ProjectLifecycle|Stage":["Etapa"],"Read more":["Ler mais"],"Related Commits":["Commits Relacionados"],"Related Deployed Jobs":["Jobs Relacionados Incorporados"],"Related Issues":["Tarefas Relacionadas"],"Related Jobs":["Jobs Relacionados"],"Related Merge Requests":["Merge Requests Relacionados"],"Related Merged Requests":["Merge Requests Relacionados"],"Showing %d event":["Mostrando %d evento","Mostrando %d eventos"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["O estágio de codificação mostra o tempo desde o primeiro commit até a criação do merge request. \\nOs dados serão automaticamente adicionados aqui uma vez que você tenha criado seu primeiro merge request."],"The collection of events added to the data gathered for that stage.":["A coleção de eventos adicionados aos dados coletados para esse estágio."],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["O estágio em questão mostra o tempo que leva desde a criação de uma tarefa até a sua assinatura para um milestone, ou a sua adição para a lista no seu Painel de Tarefas. Comece a criar tarefas para ver dados para esta etapa."],"The phase of the development lifecycle.":["A fase do ciclo de vida do desenvolvimento."],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["A fase de planejamento mostra o tempo do passo anterior até empurrar o seu primeiro commit. Este tempo será adicionado automaticamente assim que você realizar seu primeiro commit."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["O estágio de produção mostra o tempo total que leva entre criar uma tarefa e implantar o código na produção. Os dados serão adicionados automaticamente até que você complete todo o ciclo de produção."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["A etapa de revisão mostra o tempo de criação de um merge request até que o merge seja feito. Os dados serão automaticamente adicionados depois que você fizer seu primeiro merge request."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["O estágio de estágio mostra o tempo entre a fusão do MR e o código de implantação para o ambiente de produção. Os dados serão automaticamente adicionados depois de implantar na produção pela primeira vez."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["A fase de teste mostra o tempo que o GitLab CI leva para executar cada pipeline para o merge request relacionado. Os dados serão automaticamente adicionados após a conclusão do primeiro pipeline."],"The time taken by each data entry gathered by that stage.":["O tempo necessário para cada entrada de dados reunida por essa etapa."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["O valor situado no ponto médio de uma série de valores observados. Ex., entre 3, 5, 9, a mediana é 5. Entre 3, 5, 7, 8, a mediana é (5 + 7) / 2 = 6."],"Time before an issue gets scheduled":["Tempo até que uma tarefa seja planejada"],"Time before an issue starts implementation":["Tempo até que uma tarefa comece a ser implementada"],"Time between merge request creation and merge/close":["Tempo entre a criação do merge request e o merge/fechamento"],"Time until first merge request":["Tempo até o primeiro merge request"],"Time|hr":["h","hs"],"Time|min":["min","mins"],"Time|s":["s"],"Total Time":["Tempo Total"],"Total test time for all commits/merges":["Tempo de teste total para todos os commits/merges"],"Want to see the data? Please ask an administrator for access.":["Precisa visualizar os dados? Solicite acesso ao administrador."],"We don't have enough data to show this stage.":["Não temos dados suficientes para mostrar esta fase."],"You need permission.":["Você precisa de permissão."],"day":["dia","dias"]}}}; \ No newline at end of file
diff --git a/app/assets/javascripts/locale/zh_CN/app.js b/app/assets/javascripts/locale/zh_CN/app.js
index 9525bc88190..d1335cfbc0f 100644
--- a/app/assets/javascripts/locale/zh_CN/app.js
+++ b/app/assets/javascripts/locale/zh_CN/app.js
@@ -1 +1 @@
-var locales = locales || {}; locales['zh_CN'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-05-04 19:24-0500","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (China) (https://www.transifex.com/gitlab-zh/teams/75177/zh_CN/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_CN","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_CN","domain":"app","plural_forms":"nplurals=1; plural=0;"},"ByAuthor|by":["作者:"],"Commit":["提交"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["周期分析概述了项目从想法到产品实现的各阶段所需的时间。"],"CycleAnalyticsStage|Code":["编码"],"CycleAnalyticsStage|Issue":["议题"],"CycleAnalyticsStage|Plan":["计划"],"CycleAnalyticsStage|Production":["生产"],"CycleAnalyticsStage|Review":["评审"],"CycleAnalyticsStage|Staging":["预发布"],"CycleAnalyticsStage|Test":["测试"],"Deploy":["部署"],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["从创建议题到部署至生产环境"],"From merge request merge until deploy to production":["从合并请求被合并后到部署至生产环境"],"Introducing Cycle Analytics":["周期分析简介"],"Last %d day":["最后 %d 天"],"Limited to showing %d event at most":["最多显示 %d 个事件"],"Median":["中位数"],"New Issue":["新议题"],"Not available":["数据不足"],"Not enough data":["数据不足"],"OpenedNDaysAgo|Opened":["开始于"],"Pipeline Health":["流水线健康指标"],"ProjectLifecycle|Stage":["项目生命周期"],"Read more":["了解更多"],"Related Commits":["相关的提交"],"Related Deployed Jobs":["相关的部署作业"],"Related Issues":["相关的议题"],"Related Jobs":["相关的作业"],"Related Merge Requests":["相关的合并请求"],"Related Merged Requests":["相关已合并的合并请求"],"Showing %d event":["显示 %d 个事件"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["编码阶段概述了从第一次提交到创建合并请求的时间。创建第一个合并请求后,数据将自动添加到此处。"],"The collection of events added to the data gathered for that stage.":["与该阶段相关的事件。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["议题阶段概述了从创建议题到将议题设置里程碑或将议题添加到议题看板的时间。开始创建议题以查看此阶段的数据。"],"The phase of the development lifecycle.":["项目生命周期中的各个阶段。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["计划阶段概述了从议题添加到日程后到推送首次提交的时间。当首次推送提交后,数据将自动添加到此处。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["生产阶段概述了从创建一个议题到将代码部署到生产环境的总时间。当完成想法到部署生产的循环,数据将自动添加到此处。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["评审阶段概述了从创建合并请求到被合并的时间。当创建第一个合并请求后,数据将自动添加到此处。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["预发布阶段概述了从合并请求被合并到部署至生产环境的总时间。首次部署到生产环境后,数据将自动添加到此处。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["测试阶段概述了GitLab CI为相关合并请求运行每个流水线所需的时间。当第一个流水线运行完成后,数据将自动添加到此处。"],"The time taken by each data entry gathered by that stage.":["该阶段每条数据所花的时间"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位数是一个数列中最中间的值。例如在 3、5、9 之间,中位数是 5。在 3、5、7、8 之间,中位数是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["议题被列入日程表的时间"],"Time before an issue starts implementation":["开始进行编码前的时间"],"Time between merge request creation and merge/close":["从创建合并请求到被合并或关闭的时间"],"Time until first merge request":["创建第一个合并请求之前的时间"],"Time|hr":["小时"],"Time|min":["分钟"],"Time|s":["秒"],"Total Time":["总时间"],"Total test time for all commits/merges":["所有提交和合并的总测试时间"],"Want to see the data? Please ask an administrator for access.":["权限不足。如需查看相关数据,请向管理员申请权限。"],"We don't have enough data to show this stage.":["该阶段的数据不足,无法显示。"],"You need permission.":["您需要相关的权限。"],"day":["天"]}}}; \ No newline at end of file
+var locales = locales || {}; locales['zh_CN'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (China) (https://www.transifex.com/gitlab-zh/teams/75177/zh_CN/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_CN","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_CN","domain":"app","plural_forms":"nplurals=1; plural=0;"},"Are you sure you want to delete this pipeline schedule?":[""],"ByAuthor|by":["作者:"],"Cancel":[""],"Commit":["提交"],"Cron Timezone":[""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["周期分析概述了项目从想法到产品实现的各阶段所需的时间。"],"CycleAnalyticsStage|Code":["编码"],"CycleAnalyticsStage|Issue":["议题"],"CycleAnalyticsStage|Plan":["计划"],"CycleAnalyticsStage|Production":["生产"],"CycleAnalyticsStage|Review":["评审"],"CycleAnalyticsStage|Staging":["预发布"],"CycleAnalyticsStage|Test":["测试"],"Delete":[""],"Deploy":["部署"],"Description":[""],"Edit":[""],"Edit Pipeline Schedule %{id}":[""],"Failed to change the owner":[""],"Failed to remove the pipeline schedule":[""],"Filter":[""],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["从创建议题到部署至生产环境"],"From merge request merge until deploy to production":["从合并请求被合并后到部署至生产环境"],"Interval Pattern":[""],"Introducing Cycle Analytics":["周期分析简介"],"Last %d day":["最后 %d 天"],"Last Pipeline":[""],"Limited to showing %d event at most":["最多显示 %d 个事件"],"Median":["中位数"],"New Issue":["新议题"],"New Pipeline Schedule":[""],"No schedules":[""],"Not available":["数据不足"],"Not enough data":["数据不足"],"OpenedNDaysAgo|Opened":["开始于"],"Owner":[""],"Pipeline Health":["流水线健康指标"],"Pipeline Schedule":[""],"Pipeline Schedules":[""],"PipelineSchedules|Activated":[""],"PipelineSchedules|Active":[""],"PipelineSchedules|All":[""],"PipelineSchedules|Inactive":[""],"PipelineSchedules|Next Run":[""],"PipelineSchedules|None":[""],"PipelineSchedules|Provide a short description for this pipeline":[""],"PipelineSchedules|Take ownership":[""],"PipelineSchedules|Target":[""],"ProjectLifecycle|Stage":["项目生命周期"],"Read more":["了解更多"],"Related Commits":["相关的提交"],"Related Deployed Jobs":["相关的部署作业"],"Related Issues":["相关的议题"],"Related Jobs":["相关的作业"],"Related Merge Requests":["相关的合并请求"],"Related Merged Requests":["相关已合并的合并请求"],"Save pipeline schedule":[""],"Schedule a new pipeline":[""],"Select a timezone":[""],"Select target branch":[""],"Showing %d event":["显示 %d 个事件"],"Target Branch":[""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["编码阶段概述了从第一次提交到创建合并请求的时间。创建第一个合并请求后,数据将自动添加到此处。"],"The collection of events added to the data gathered for that stage.":["与该阶段相关的事件。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["议题阶段概述了从创建议题到将议题设置里程碑或将议题添加到议题看板的时间。开始创建议题以查看此阶段的数据。"],"The phase of the development lifecycle.":["项目生命周期中的各个阶段。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["计划阶段概述了从议题添加到日程后到推送首次提交的时间。当首次推送提交后,数据将自动添加到此处。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["生产阶段概述了从创建一个议题到将代码部署到生产环境的总时间。当完成想法到部署生产的循环,数据将自动添加到此处。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["评审阶段概述了从创建合并请求到被合并的时间。当创建第一个合并请求后,数据将自动添加到此处。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["预发布阶段概述了从合并请求被合并到部署至生产环境的总时间。首次部署到生产环境后,数据将自动添加到此处。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["测试阶段概述了GitLab CI为相关合并请求运行每个流水线所需的时间。当第一个流水线运行完成后,数据将自动添加到此处。"],"The time taken by each data entry gathered by that stage.":["该阶段每条数据所花的时间"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位数是一个数列中最中间的值。例如在 3、5、9 之间,中位数是 5。在 3、5、7、8 之间,中位数是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["议题被列入日程表的时间"],"Time before an issue starts implementation":["开始进行编码前的时间"],"Time between merge request creation and merge/close":["从创建合并请求到被合并或关闭的时间"],"Time until first merge request":["创建第一个合并请求之前的时间"],"Time|hr":["小时"],"Time|min":["分钟"],"Time|s":["秒"],"Total Time":["总时间"],"Total test time for all commits/merges":["所有提交和合并的总测试时间"],"Want to see the data? Please ask an administrator for access.":["权限不足。如需查看相关数据,请向管理员申请权限。"],"We don't have enough data to show this stage.":["该阶段的数据不足,无法显示。"],"You need permission.":["您需要相关的权限。"],"day":["天"]}}}; \ No newline at end of file
diff --git a/app/assets/javascripts/locale/zh_HK/app.js b/app/assets/javascripts/locale/zh_HK/app.js
index bfc6f7e5db4..67054b3ef4a 100644
--- a/app/assets/javascripts/locale/zh_HK/app.js
+++ b/app/assets/javascripts/locale/zh_HK/app.js
@@ -1 +1 @@
-var locales = locales || {}; locales['zh_HK'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-06-12 19:29-0500","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","PO-Revision-Date":"2017-06-12 11:08-0400","Last-Translator":"Huang Tao <htve@outlook.com>","Language-Team":"Chinese (Hong Kong) (https://translate.zanata.org/project/view/GitLab)","Language":"zh-HK","Plural-Forms":"nplurals=1; plural=0;","X-Generator":"Zanata 3.9.6","lang":"zh_HK","domain":"app","plural_forms":"nplurals=1; plural=0;"},"%{commit_author_link} committed %{commit_timeago}":["由 %{commit_author_link} 提交於 %{commit_timeago}"],"About auto deploy":["關於自動部署"],"Active":["激活"],"Activity":["活動"],"Add Changelog":["添加變更日誌"],"Add Contribution guide":["添加貢獻指南"],"Add License":["添加許可證"],"Add an SSH key to your profile to pull or push via SSH.":["新增壹個用於推送或拉取的 SSH 秘鑰到您的個人賬戶中。"],"Add new directory":["添加新目錄"],"Archived project! Repository is read-only":["歸檔項目!存儲庫為只讀"],"Are you sure you want to delete this pipeline schedule?":["您確定要刪除此流水線計劃嗎?"],"Attach a file by drag &amp; drop or %{upload_link}":["拖放文件到此處或者 %{upload_link}"],"Branch":["分支"],"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}":["分支 <strong>%{branch_name}</strong> 已創建。如要設置自動部署, 請選擇合適的 GitLab CI Yaml 模板併提交更改。%{link_to_autodeploy_doc}"],"Branches":["分支"],"Browse files":["瀏覽文件"],"ByAuthor|by":["作者:"],"CI configuration":["CI 配置"],"Cancel":["取消"],"ChangeTypeActionLabel|Pick into branch":["挑選到分支"],"ChangeTypeActionLabel|Revert in branch":["還原分支"],"ChangeTypeAction|Cherry-pick":["優選"],"ChangeType|commit":["提交"],"ChangeType|merge request":["合併請求"],"Changelog":["變更日誌"],"Charts":["統計圖"],"Cherry-pick this commit":["優選此提交"],"Cherry-pick this merge-request":["優選此合併請求"],"CiStatusLabel|canceled":["已取消"],"CiStatusLabel|created":["已創建"],"CiStatusLabel|failed":["已失敗"],"CiStatusLabel|manual action":["手動操作"],"CiStatusLabel|passed":["已通過"],"CiStatusLabel|passed with warnings":["已通過但有警告"],"CiStatusLabel|pending":["等待中"],"CiStatusLabel|skipped":["已跳過"],"CiStatusLabel|waiting for manual action":["等待手動操作"],"CiStatusText|blocked":["已阻塞"],"CiStatusText|canceled":["已取消"],"CiStatusText|created":["已創建"],"CiStatusText|failed":["已失敗"],"CiStatusText|manual":["待手動"],"CiStatusText|passed":["已通過"],"CiStatusText|pending":["等待中"],"CiStatusText|skipped":["已跳過"],"CiStatus|running":["運行中"],"Commit":["提交"],"Commit message":["提交信息"],"CommitMessage|Add %{file_name}":["添加 %{file_name}"],"Commits":["提交"],"Commits|History":["歷史"],"Committed by":["作者:"],"Compare":["比較"],"Contribution guide":["貢獻指南"],"Contributors":["貢獻者"],"Copy URL to clipboard":["複製URL到剪貼板"],"Copy commit SHA to clipboard":["複製提交 SHA 到剪貼板"],"Create New Directory":["創建新目錄"],"Create directory":["創建目錄"],"Create empty bare repository":["創建空的存儲庫"],"Create merge request":["創建合併請求"],"Create new...":["創建..."],"CreateNewFork|Fork":["派生"],"CreateTag|Tag":["標簽"],"Cron Timezone":["Cron 時區"],"Cron syntax":["Cron 語法"],"Custom":["自定義"],"Custom notification events":["自定義通知事件"],"Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}.":["自定義通知級別與參與級別相同。使用自定義通知級別,您只能收到所選定事件通知。想瞭解更多信息,請查看 %{notification_link}."],"Cycle Analytics":["週期分析"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["週期分析概述了項目從想法到產品實現的各階段所需的時間。"],"CycleAnalyticsStage|Code":["編碼"],"CycleAnalyticsStage|Issue":["議題"],"CycleAnalyticsStage|Plan":["計劃"],"CycleAnalyticsStage|Production":["生產"],"CycleAnalyticsStage|Review":["評審"],"CycleAnalyticsStage|Staging":["預發布"],"CycleAnalyticsStage|Test":["測試"],"Define a custom pattern with cron syntax":["使用 Cron 語法定義自定義模式"],"Delete":["刪除"],"Deploy":["部署"],"Description":["描述"],"Directory name":["目錄名稱"],"Don't show again":["不再顯示"],"Download":["下載"],"Download tar":["下載 tar"],"Download tar.bz2":["下載 tar.bz2"],"Download tar.gz":["下載 tar.gz"],"Download zip":["下載 zip"],"DownloadArtifacts|Download":["下載"],"DownloadCommit|Email Patches":["電子郵件補丁"],"DownloadCommit|Plain Diff":["Diff 文件"],"DownloadSource|Download":["下載"],"Edit":["編輯"],"Edit Pipeline Schedule %{id}":["編輯 %{id} 流水線計劃"],"Every day (at 4:00am)":["每天(淩晨4點)"],"Every month (on the 1st at 4:00am)":["每月1日(淩晨4點)"],"Every week (Sundays at 4:00am)":["每周日(淩晨4點)"],"Failed to change the owner":["無法變更所有者"],"Failed to remove the pipeline schedule":["無法刪除流水線計劃"],"Files":["文件"],"Find by path":["按路徑查找"],"Find file":["查找文件"],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"Fork":["派生"],"ForkedFromProjectPath|Forked from":["派生自"],"From issue creation until deploy to production":["從創建議題到部署到生產環境"],"From merge request merge until deploy to production":["從合併請求的合併到部署至生產環境"],"Go to your fork":["跳轉到您的派生項目"],"GoToYourFork|Fork":["跳轉到您的派生項目"],"Home":["首頁"],"Housekeeping successfully started":["已開始維護"],"Import repository":["導入存儲庫"],"Interval Pattern":["間隔模式"],"Introducing Cycle Analytics":["週期分析簡介"],"LFSStatus|Disabled":["禁用"],"LFSStatus|Enabled":["啟用"],"Last %d day":["最後 %d 天"],"Last Pipeline":["最新流水線"],"Last Update":["最後更新"],"Last commit":["最後提交"],"Learn more in the":["了解更多"],"Leave group":["離開群組"],"Leave project":["離開項目"],"Limited to showing %d event at most":["最多顯示 %d 個事件"],"Median":["中位數"],"MissingSSHKeyWarningLink|add an SSH key":["添加壹個 SSH 公鑰"],"New Issue":["新議題"],"New Pipeline Schedule":["創建流水線計劃"],"New branch":["新分支"],"New directory":["新增目錄"],"New file":["新增文件"],"New issue":["新議題"],"New merge request":["新合併請求"],"New schedule":["新计划"],"New snippet":["新代碼片段"],"New tag":["新標籤"],"No repository":["沒有存儲庫"],"No schedules":["沒有計劃"],"Not available":["不可用"],"Not enough data":["數據不足"],"Notification events":["通知事件"],"NotificationEvent|Close issue":["關閉議題"],"NotificationEvent|Close merge request":["關閉合併請求"],"NotificationEvent|Failed pipeline":["流水線失敗"],"NotificationEvent|Merge merge request":["合併請求被合併"],"NotificationEvent|New issue":["新議題"],"NotificationEvent|New merge request":["新合併請求"],"NotificationEvent|New note":["新評論"],"NotificationEvent|Reassign issue":["重新指派議題"],"NotificationEvent|Reassign merge request":["重新指派合併請求"],"NotificationEvent|Reopen issue":["重新打開議題"],"NotificationEvent|Successful pipeline":["流水線成功完成"],"NotificationLevel|Custom":["自定義"],"NotificationLevel|Disabled":["禁用"],"NotificationLevel|Global":["全局"],"NotificationLevel|On mention":["提及"],"NotificationLevel|Participate":["參與"],"NotificationLevel|Watch":["關注"],"OfSearchInADropdown|Filter":["篩選"],"OpenedNDaysAgo|Opened":["開始於"],"Options":["選項"],"Owner":["所有者"],"Pipeline":["流水線"],"Pipeline Health":["流水線健康指標"],"Pipeline Schedule":["流水線計劃"],"Pipeline Schedules":["流水線計劃"],"PipelineSchedules|Activated":["已激活"],"PipelineSchedules|Active":["激活"],"PipelineSchedules|All":["所有"],"PipelineSchedules|Inactive":["等待"],"PipelineSchedules|Next Run":["下壹個運行"],"PipelineSchedules|None":["無"],"PipelineSchedules|Provide a short description for this pipeline":["為此流水線提供簡短描述"],"PipelineSchedules|Take ownership":["取得所有權"],"PipelineSchedules|Target":["目標"],"Project '%{project_name}' queued for deletion.":["項目 '%{project_name}' 已進入刪除隊列。"],"Project '%{project_name}' was successfully created.":["項目 '%{project_name}' 已創建完成。"],"Project '%{project_name}' was successfully updated.":["項目 '%{project_name}' 已更新完成。"],"Project '%{project_name}' will be deleted.":["項目 '%{project_name}' 已刪除。"],"Project access must be granted explicitly to each user.":["項目訪問權限必須明確授權給每個用戶。"],"Project export could not be deleted.":["無法刪除項目導出。"],"Project export has been deleted.":["項目導出已刪除。"],"Project export link has expired. Please generate a new export from your project settings.":["項目導出鏈接已過期。請從項目設置中重新生成項目導出。"],"Project export started. A download link will be sent by email.":["項目導出已開始。下載鏈接將通過電子郵件發送。"],"Project home":["項目首頁"],"ProjectFeature|Disabled":["禁用"],"ProjectFeature|Everyone with access":["任何人都可訪問"],"ProjectFeature|Only team members":["只有團隊成員"],"ProjectFileTree|Name":["名稱"],"ProjectLastActivity|Never":["從未"],"ProjectLifecycle|Stage":["階段"],"ProjectNetworkGraph|Graph":["分支圖"],"Read more":["了解更多"],"Readme":["自述文件"],"RefSwitcher|Branches":["分支"],"RefSwitcher|Tags":["標籤"],"Related Commits":["相關的提交"],"Related Deployed Jobs":["相關的部署作業"],"Related Issues":["相關的議題"],"Related Jobs":["相關的作業"],"Related Merge Requests":["相關的合併請求"],"Related Merged Requests":["相關已合併的合併請求"],"Remind later":["稍後提醒"],"Remove project":["刪除項目"],"Request Access":["申請訪問"],"Revert this commit":["還原此提交"],"Revert this merge-request":["還原此合併請求"],"Save pipeline schedule":["保存流水線計劃"],"Schedule a new pipeline":["新增流水線計劃"],"Scheduling Pipelines":["流水線計劃"],"Search branches and tags":["搜索分支和標籤"],"Select Archive Format":["選擇下載格式"],"Select a timezone":["選擇時區"],"Select target branch":["選擇目標分支"],"Set a password on your account to pull or push via %{protocol}":["為您的賬戶添加壹個用於推送或拉取的 %{protocol} 密碼。"],"Set up CI":["設置 CI"],"Set up Koding":["設置 Koding"],"Set up auto deploy":["設置自動部署"],"SetPasswordToCloneLink|set a password":["設置密碼"],"Showing %d event":["顯示 %d 個事件"],"Source code":["源代碼"],"StarProject|Star":["星標"],"Start a <strong>new merge request</strong> with these changes":["包含這些更改到 <strong>新合併請求</strong>"],"Switch branch/tag":["切換分支/標籤"],"Tag":["標籤"],"Tags":["標籤"],"Target Branch":["目標分支"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["編碼階段概述了從第壹次提交到創建合併請求的時間。創建第壹個合併請求後,數據將自動添加到此處。"],"The collection of events added to the data gathered for that stage.":["與該階段相關的事件。"],"The fork relationship has been removed.":["派生關係已被刪除。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["議題階段概述了從創建議題到將議題設置裏程碑或將議題添加到議題看板的時間。創建壹個議題後,數據將自動添加到此處。"],"The phase of the development lifecycle.":["項目生命週期中的各個階段。"],"The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.":["流水線計劃會針對特定的分支或標簽在以後重復運行流水線。這些預定的流水線將根據其相關用戶繼承有限的項目訪問。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["計劃階段概述了從議題添加到日程後到推送首次提交的時間。當首次推送提交後,數據將自動添加到此處。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["生產階段概述了從創建議題到將代碼部署到生產環境的時間。當完成完整的想法到部署生產,數據將自動添加到此處。"],"The project can be accessed by any logged in user.":["該項目允許已登錄的用戶訪問。"],"The project can be accessed without any authentication.":["該項目允許任何人訪問。"],"The repository for this project does not exist.":["此項目的存儲庫不存在。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["評審階段概述了從創建合併請求到合併的時間。當創建第壹個合併請求後,數據將自動添加到此處。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["預發布階段概述了合併請求的合併到部署代碼到生產環境的總時間。當首次部署到生產環境後,數據將自動添加到此處。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["測試階段概述了 GitLab CI 為相關合併請求運行每個流水線所需的時間。當第壹個流水線運行完成後,數據將自動添加到此處。"],"The time taken by each data entry gathered by that stage.":["該階段每條數據所花的時間"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位數是壹個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。"],"This means you can not push code until you create an empty repository or import existing one.":["在創建壹個空的存儲庫或導入現有存儲庫之前,您將無法推送代碼。"],"Time before an issue gets scheduled":["議題被列入日程表的時間"],"Time before an issue starts implementation":["開始進行編碼前的時間"],"Time between merge request creation and merge/close":["從創建合併請求到被合併或關閉的時間"],"Time until first merge request":["創建第壹個合併請求之前的時間"],"Timeago|%s days ago":["%s 天前"],"Timeago|%s days remaining":["剩餘 %s 天"],"Timeago|%s hours remaining":["剩餘 %s 小時"],"Timeago|%s minutes ago":["%s 分鐘前"],"Timeago|%s minutes remaining":["剩餘 %s 分鐘"],"Timeago|%s months ago":["%s 個月前"],"Timeago|%s months remaining":["剩餘 %s 月"],"Timeago|%s seconds remaining":["剩餘 %s 秒"],"Timeago|%s weeks ago":["%s 星期前"],"Timeago|%s weeks remaining":["剩餘 %s 星期"],"Timeago|%s years ago":["%s 年前"],"Timeago|%s years remaining":["剩餘 %s 年"],"Timeago|1 day remaining":["剩餘 1 天"],"Timeago|1 hour remaining":["剩餘 1 小時"],"Timeago|1 minute remaining":["剩餘 1 分鐘"],"Timeago|1 month remaining":["剩餘 1 個月"],"Timeago|1 week remaining":["剩餘 1 星期"],"Timeago|1 year remaining":["剩餘 1 年"],"Timeago|Past due":["逾期"],"Timeago|a day ago":["1 天前"],"Timeago|a month ago":["1 個月前"],"Timeago|a week ago":["1 星期前"],"Timeago|a while":["剛剛"],"Timeago|a year ago":["1 年前"],"Timeago|about %s hours ago":["大約 %s 小時前"],"Timeago|about a minute ago":["大約 1 分鐘前"],"Timeago|about an hour ago":["大約 1 小時前"],"Timeago|in %s days":["在 %s 天"],"Timeago|in %s hours":["在 %s 小時"],"Timeago|in %s minutes":["在 %s 分鐘"],"Timeago|in %s months":["在 %s 個月"],"Timeago|in %s seconds":["在 %s 秒"],"Timeago|in %s weeks":["在 %s 星期"],"Timeago|in %s years":["在 %s 年"],"Timeago|in 1 day":["在 1 天"],"Timeago|in 1 hour":["在 1 小時"],"Timeago|in 1 minute":["在 1 分鐘"],"Timeago|in 1 month":["在 1 月"],"Timeago|in 1 week":["在 1 星期"],"Timeago|in 1 year":["在 1 年"],"Timeago|less than a minute ago":["不到 1 分鐘前"],"Time|hr":["小時"],"Time|min":["分鐘"],"Time|s":["秒"],"Total Time":["總時間"],"Total test time for all commits/merges":["所有提交和合併的總測試時間"],"Unstar":["取消星標"],"Upload New File":["上傳新文件"],"Upload file":["上傳文件"],"Use your global notification setting":["使用全局通知設置"],"VisibilityLevel|Internal":["內部"],"VisibilityLevel|Private":["私有"],"VisibilityLevel|Public":["公開"],"Want to see the data? Please ask an administrator for access.":["權限不足。如需查看相關數據,請向管理員申請權限。"],"We don't have enough data to show this stage.":["該階段的數據不足,無法顯示。"],"Withdraw Access Request":["取消訪問請求"],"You are going to remove %{project_name_with_namespace}.\\nRemoved project CANNOT be restored!\\nAre you ABSOLUTELY sure?":["您將要刪除 %{project_name_with_namespace}。\\n已刪除的項目無法恢復!\\n您確定繼續嗎?"],"You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?":["您將刪除與源項目 %{forked_from_project} 的派生關系。您確定繼續嗎?"],"You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?":["將 %{project_name_with_namespace} 轉義給另壹個所有者。您確定繼續嗎?"],"You can only add files when you are on a branch":["您只能在分支上添加文件"],"You must sign in to star a project":["您必須登錄才能對項目加星標"],"You need permission.":["您需要相關的權限。"],"You will not get any notifications via email":["您將不會收到任何通知郵件"],"You will only receive notifications for the events you choose":["您只會收到您選擇的事件通知"],"You will only receive notifications for threads you have participated in":["您只會收到您參與的主題的通知"],"You will receive notifications for any activity":["您將不會收到任何通知"],"You will receive notifications only for comments in which you were @mentioned":["您只會收到評論中提及(@)您的通知"],"You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account":["在您的賬戶上 %{set_password_link} 之前, 您將無法通過 %{protocol} 拉取或推送代碼。"],"You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile":["在您的個人資料中添加 %{add_ssh_key_link} 之前, 您將無法通過 SSH 拉取或推送代碼。"],"Your name":["您的名字"],"day":["天"],"notification emails":["通知郵件"],"parent":["父級"],"pipeline schedules documentation":["流水線計劃文檔"],"with stage":["於階段"]}}}; \ No newline at end of file
+var locales = locales || {}; locales['zh_HK'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-06-12 19:29-0500","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","PO-Revision-Date":"2017-06-13 04:07-0400","Last-Translator":"Huang Tao <htve@outlook.com>","Language-Team":"Chinese (Hong Kong) (https://translate.zanata.org/project/view/GitLab)","Language":"zh-HK","Plural-Forms":"nplurals=1; plural=0;","X-Generator":"Zanata 3.9.6","lang":"zh_HK","domain":"app","plural_forms":"nplurals=1; plural=0;"},"%{commit_author_link} committed %{commit_timeago}":["由 %{commit_author_link} 提交於 %{commit_timeago}"],"About auto deploy":["關於自動部署"],"Active":["激活"],"Activity":["活動"],"Add Changelog":["添加變更日誌"],"Add Contribution guide":["添加貢獻指南"],"Add License":["添加許可證"],"Add an SSH key to your profile to pull or push via SSH.":["新增壹個用於推送或拉取的 SSH 秘鑰到賬號中。"],"Add new directory":["添加新目錄"],"Archived project! Repository is read-only":["歸檔項目!存儲庫為只讀"],"Are you sure you want to delete this pipeline schedule?":["您確定要刪除此流水線計劃嗎?"],"Attach a file by drag &amp; drop or %{upload_link}":["拖放文件到此處或者 %{upload_link}"],"Branch":["分支"],"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}":["分支 <strong>%{branch_name}</strong> 已創建。如要設置自動部署, 請選擇合適的 GitLab CI Yaml 模板併提交更改。%{link_to_autodeploy_doc}"],"Branches":["分支"],"Browse files":["瀏覽文件"],"ByAuthor|by":["作者:"],"CI configuration":["CI 配置"],"Cancel":["取消"],"ChangeTypeActionLabel|Pick into branch":["挑選到分支"],"ChangeTypeActionLabel|Revert in branch":["還原分支"],"ChangeTypeAction|Cherry-pick":["優選"],"ChangeType|commit":["提交"],"ChangeType|merge request":["合併請求"],"Changelog":["變更日誌"],"Charts":["統計圖"],"Cherry-pick this commit":["優選此提交"],"Cherry-pick this merge-request":["優選此合併請求"],"CiStatusLabel|canceled":["已取消"],"CiStatusLabel|created":["已創建"],"CiStatusLabel|failed":["已失敗"],"CiStatusLabel|manual action":["手動操作"],"CiStatusLabel|passed":["已通過"],"CiStatusLabel|passed with warnings":["已通過但有警告"],"CiStatusLabel|pending":["等待中"],"CiStatusLabel|skipped":["已跳過"],"CiStatusLabel|waiting for manual action":["等待手動操作"],"CiStatusText|blocked":["已阻塞"],"CiStatusText|canceled":["已取消"],"CiStatusText|created":["已創建"],"CiStatusText|failed":["已失敗"],"CiStatusText|manual":["待手動"],"CiStatusText|passed":["已通過"],"CiStatusText|pending":["等待中"],"CiStatusText|skipped":["已跳過"],"CiStatus|running":["運行中"],"Commit":["提交"],"Commit message":["提交信息"],"CommitMessage|Add %{file_name}":["添加 %{file_name}"],"Commits":["提交"],"Commits|History":["歷史"],"Committed by":["作者:"],"Compare":["比較"],"Contribution guide":["貢獻指南"],"Contributors":["貢獻者"],"Copy URL to clipboard":["複製URL到剪貼板"],"Copy commit SHA to clipboard":["複製提交 SHA 到剪貼板"],"Create New Directory":["創建新目錄"],"Create directory":["創建目錄"],"Create empty bare repository":["創建空的存儲庫"],"Create merge request":["創建合併請求"],"Create new...":["創建..."],"CreateNewFork|Fork":["派生"],"CreateTag|Tag":["標簽"],"Cron Timezone":["Cron 時區"],"Cron syntax":["Cron 語法"],"Custom":["自定義"],"Custom notification events":["自定義通知事件"],"Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}.":["自定義通知級別繼承自參與級別。使用自定義通知級別,您會收到參與級別及選定事件的通知。想了解更多信息,請查看 %{notification_link}."],"Cycle Analytics":["週期分析"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["週期分析概述了項目從想法到產品實現的各階段所需的時間。"],"CycleAnalyticsStage|Code":["編碼"],"CycleAnalyticsStage|Issue":["議題"],"CycleAnalyticsStage|Plan":["計劃"],"CycleAnalyticsStage|Production":["生產"],"CycleAnalyticsStage|Review":["評審"],"CycleAnalyticsStage|Staging":["預發布"],"CycleAnalyticsStage|Test":["測試"],"Define a custom pattern with cron syntax":["使用 Cron 語法定義自定義模式"],"Delete":["刪除"],"Deploy":["部署"],"Description":["描述"],"Directory name":["目錄名稱"],"Don't show again":["不再顯示"],"Download":["下載"],"Download tar":["下載 tar"],"Download tar.bz2":["下載 tar.bz2"],"Download tar.gz":["下載 tar.gz"],"Download zip":["下載 zip"],"DownloadArtifacts|Download":["下載"],"DownloadCommit|Email Patches":["電子郵件補丁"],"DownloadCommit|Plain Diff":["Diff 文件"],"DownloadSource|Download":["下載"],"Edit":["編輯"],"Edit Pipeline Schedule %{id}":["編輯 %{id} 流水線計劃"],"Every day (at 4:00am)":["每天(淩晨4點)"],"Every month (on the 1st at 4:00am)":["每月1日(淩晨4點)"],"Every week (Sundays at 4:00am)":["每周日(淩晨4點)"],"Failed to change the owner":["無法變更所有者"],"Failed to remove the pipeline schedule":["無法刪除流水線計劃"],"Files":["文件"],"Find by path":["按路徑查找"],"Find file":["查找文件"],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"Fork":["派生"],"ForkedFromProjectPath|Forked from":["派生自"],"From issue creation until deploy to production":["從創建議題到部署到生產環境"],"From merge request merge until deploy to production":["從合併請求的合併到部署至生產環境"],"Go to your fork":["跳轉到派生項目"],"GoToYourFork|Fork":["跳轉到派生項目"],"Home":["首頁"],"Housekeeping successfully started":["已開始維護"],"Import repository":["導入存儲庫"],"Interval Pattern":["間隔模式"],"Introducing Cycle Analytics":["週期分析簡介"],"LFSStatus|Disabled":["停用"],"LFSStatus|Enabled":["啟用"],"Last %d day":["最後 %d 天"],"Last Pipeline":["最新流水線"],"Last Update":["最後更新"],"Last commit":["最後提交"],"Learn more in the":["了解更多"],"Leave group":["離開群組"],"Leave project":["離開項目"],"Limited to showing %d event at most":["最多顯示 %d 個事件"],"Median":["中位數"],"MissingSSHKeyWarningLink|add an SSH key":["添加壹個 SSH 公鑰"],"New Issue":["新議題"],"New Pipeline Schedule":["創建流水線計劃"],"New branch":["新分支"],"New directory":["新增目錄"],"New file":["新增文件"],"New issue":["新議題"],"New merge request":["新合併請求"],"New schedule":["新计划"],"New snippet":["新代碼片段"],"New tag":["新標籤"],"No repository":["沒有存儲庫"],"No schedules":["沒有計劃"],"Not available":["不可用"],"Not enough data":["數據不足"],"Notification events":["通知事件"],"NotificationEvent|Close issue":["關閉議題"],"NotificationEvent|Close merge request":["關閉合併請求"],"NotificationEvent|Failed pipeline":["流水線失敗"],"NotificationEvent|Merge merge request":["合併請求被合併"],"NotificationEvent|New issue":["新議題"],"NotificationEvent|New merge request":["新合併請求"],"NotificationEvent|New note":["新評論"],"NotificationEvent|Reassign issue":["重新指派議題"],"NotificationEvent|Reassign merge request":["重新指派合併請求"],"NotificationEvent|Reopen issue":["重新打開議題"],"NotificationEvent|Successful pipeline":["流水線成功完成"],"NotificationLevel|Custom":["自定義"],"NotificationLevel|Disabled":["停用"],"NotificationLevel|Global":["全局"],"NotificationLevel|On mention":["提及"],"NotificationLevel|Participate":["參與"],"NotificationLevel|Watch":["關注"],"OfSearchInADropdown|Filter":["篩選"],"OpenedNDaysAgo|Opened":["開始於"],"Options":["選項"],"Owner":["所有者"],"Pipeline":["流水線"],"Pipeline Health":["流水線健康指標"],"Pipeline Schedule":["流水線計劃"],"Pipeline Schedules":["流水線計劃"],"PipelineSchedules|Activated":["已激活"],"PipelineSchedules|Active":["激活"],"PipelineSchedules|All":["所有"],"PipelineSchedules|Inactive":["等待"],"PipelineSchedules|Next Run":["下壹個運行"],"PipelineSchedules|None":["無"],"PipelineSchedules|Provide a short description for this pipeline":["為此流水線提供簡短描述"],"PipelineSchedules|Take ownership":["取得所有權"],"PipelineSchedules|Target":["目標"],"Project '%{project_name}' queued for deletion.":["項目 '%{project_name}' 已進入刪除隊列。"],"Project '%{project_name}' was successfully created.":["項目 '%{project_name}' 已創建完成。"],"Project '%{project_name}' was successfully updated.":["項目 '%{project_name}' 已更新完成。"],"Project '%{project_name}' will be deleted.":["項目 '%{project_name}' 已刪除。"],"Project access must be granted explicitly to each user.":["項目訪問權限必須明確授權給每個用戶。"],"Project export could not be deleted.":["無法刪除項目導出。"],"Project export has been deleted.":["項目導出已刪除。"],"Project export link has expired. Please generate a new export from your project settings.":["項目導出鏈接已過期。請從項目設置中重新生成項目導出。"],"Project export started. A download link will be sent by email.":["項目導出已開始。下載鏈接將通過電子郵件發送。"],"Project home":["項目首頁"],"ProjectFeature|Disabled":["停用"],"ProjectFeature|Everyone with access":["任何人都可訪問"],"ProjectFeature|Only team members":["只有團隊成員"],"ProjectFileTree|Name":["名稱"],"ProjectLastActivity|Never":["從未"],"ProjectLifecycle|Stage":["階段"],"ProjectNetworkGraph|Graph":["分支圖"],"Read more":["了解更多"],"Readme":["自述文件"],"RefSwitcher|Branches":["分支"],"RefSwitcher|Tags":["標籤"],"Related Commits":["相關的提交"],"Related Deployed Jobs":["相關的部署作業"],"Related Issues":["相關的議題"],"Related Jobs":["相關的作業"],"Related Merge Requests":["相關的合併請求"],"Related Merged Requests":["相關已合併的合併請求"],"Remind later":["稍後提醒"],"Remove project":["刪除項目"],"Request Access":["申請訪問"],"Revert this commit":["還原此提交"],"Revert this merge-request":["還原此合併請求"],"Save pipeline schedule":["保存流水線計劃"],"Schedule a new pipeline":["新增流水線計劃"],"Scheduling Pipelines":["流水線計劃"],"Search branches and tags":["搜索分支和標籤"],"Select Archive Format":["選擇下載格式"],"Select a timezone":["選擇時區"],"Select target branch":["選擇目標分支"],"Set a password on your account to pull or push via %{protocol}":["為賬號添加壹個用於推送或拉取的 %{protocol} 密碼。"],"Set up CI":["設置 CI"],"Set up Koding":["設置 Koding"],"Set up auto deploy":["設置自動部署"],"SetPasswordToCloneLink|set a password":["設置密碼"],"Showing %d event":["顯示 %d 個事件"],"Source code":["源代碼"],"StarProject|Star":["星標"],"Start a <strong>new merge request</strong> with these changes":["包含這些更改到 <strong>新合併請求</strong>"],"Switch branch/tag":["切換分支/標籤"],"Tag":["標籤"],"Tags":["標籤"],"Target Branch":["目標分支"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["編碼階段概述了從第壹次提交到創建合併請求的時間。創建第壹個合併請求後,數據將自動添加到此處。"],"The collection of events added to the data gathered for that stage.":["與該階段相關的事件。"],"The fork relationship has been removed.":["派生關係已被刪除。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["議題階段概述了從創建議題到將議題設置裏程碑或將議題添加到議題看板的時間。創建壹個議題後,數據將自動添加到此處。"],"The phase of the development lifecycle.":["項目生命週期中的各個階段。"],"The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.":["流水線計劃會針對特定的分支或標簽在以後重復運行流水線。這些預定的流水線將根據其相關用戶繼承有限的項目訪問。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["計劃階段概述了從議題添加到日程後到推送首次提交的時間。當首次推送提交後,數據將自動添加到此處。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["生產階段概述了從創建議題到將代碼部署到生產環境的時間。當完成完整的想法到部署生產,數據將自動添加到此處。"],"The project can be accessed by any logged in user.":["該項目允許已登錄的用戶訪問。"],"The project can be accessed without any authentication.":["該項目允許任何人訪問。"],"The repository for this project does not exist.":["此項目的存儲庫不存在。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["評審階段概述了從創建合併請求到合併的時間。當創建第壹個合併請求後,數據將自動添加到此處。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["預發布階段概述了合併請求的合併到部署代碼到生產環境的總時間。當首次部署到生產環境後,數據將自動添加到此處。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["測試階段概述了 GitLab CI 為相關合併請求運行每個流水線所需的時間。當第壹個流水線運行完成後,數據將自動添加到此處。"],"The time taken by each data entry gathered by that stage.":["該階段每條數據所花的時間"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位數是壹個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。"],"This means you can not push code until you create an empty repository or import existing one.":["在創建壹個空的存儲庫或導入現有存儲庫之前,您將無法推送代碼。"],"Time before an issue gets scheduled":["議題被列入日程表的時間"],"Time before an issue starts implementation":["開始進行編碼前的時間"],"Time between merge request creation and merge/close":["從創建合併請求到被合併或關閉的時間"],"Time until first merge request":["創建第壹個合併請求之前的時間"],"Timeago|%s days ago":["%s 天前"],"Timeago|%s days remaining":["剩餘 %s 天"],"Timeago|%s hours remaining":["剩餘 %s 小時"],"Timeago|%s minutes ago":["%s 分鐘前"],"Timeago|%s minutes remaining":["剩餘 %s 分鐘"],"Timeago|%s months ago":["%s 個月前"],"Timeago|%s months remaining":["剩餘 %s 月"],"Timeago|%s seconds remaining":["剩餘 %s 秒"],"Timeago|%s weeks ago":["%s 星期前"],"Timeago|%s weeks remaining":["剩餘 %s 星期"],"Timeago|%s years ago":["%s 年前"],"Timeago|%s years remaining":["剩餘 %s 年"],"Timeago|1 day remaining":["剩餘 1 天"],"Timeago|1 hour remaining":["剩餘 1 小時"],"Timeago|1 minute remaining":["剩餘 1 分鐘"],"Timeago|1 month remaining":["剩餘 1 個月"],"Timeago|1 week remaining":["剩餘 1 星期"],"Timeago|1 year remaining":["剩餘 1 年"],"Timeago|Past due":["逾期"],"Timeago|a day ago":["1 天前"],"Timeago|a month ago":["1 個月前"],"Timeago|a week ago":["1 星期前"],"Timeago|a while":["剛剛"],"Timeago|a year ago":["1 年前"],"Timeago|about %s hours ago":["大約 %s 小時前"],"Timeago|about a minute ago":["大約 1 分鐘前"],"Timeago|about an hour ago":["大約 1 小時前"],"Timeago|in %s days":["在 %s 天"],"Timeago|in %s hours":["在 %s 小時"],"Timeago|in %s minutes":["在 %s 分鐘"],"Timeago|in %s months":["在 %s 個月"],"Timeago|in %s seconds":["在 %s 秒"],"Timeago|in %s weeks":["在 %s 星期"],"Timeago|in %s years":["在 %s 年"],"Timeago|in 1 day":["在 1 天"],"Timeago|in 1 hour":["在 1 小時"],"Timeago|in 1 minute":["在 1 分鐘"],"Timeago|in 1 month":["在 1 月"],"Timeago|in 1 week":["在 1 星期"],"Timeago|in 1 year":["在 1 年"],"Timeago|less than a minute ago":["不到 1 分鐘前"],"Time|hr":["小時"],"Time|min":["分鐘"],"Time|s":["秒"],"Total Time":["總時間"],"Total test time for all commits/merges":["所有提交和合併的總測試時間"],"Unstar":["取消星標"],"Upload New File":["上傳新文件"],"Upload file":["上傳文件"],"Use your global notification setting":["使用全局通知設置"],"VisibilityLevel|Internal":["內部"],"VisibilityLevel|Private":["私有"],"VisibilityLevel|Public":["公開"],"Want to see the data? Please ask an administrator for access.":["權限不足。如需查看相關數據,請向管理員申請權限。"],"We don't have enough data to show this stage.":["該階段的數據不足,無法顯示。"],"Withdraw Access Request":["取消訪問請求"],"You are going to remove %{project_name_with_namespace}.\\nRemoved project CANNOT be restored!\\nAre you ABSOLUTELY sure?":["您將要刪除 %{project_name_with_namespace}。\\n已刪除的項目無法恢復!\\n您確定繼續嗎?"],"You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?":["您將刪除與源項目 %{forked_from_project} 的派生關系。您確定繼續嗎?"],"You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?":["將 %{project_name_with_namespace} 轉義給另壹個所有者。您確定繼續嗎?"],"You can only add files when you are on a branch":["您只能在分支上添加文件"],"You must sign in to star a project":["您必須登錄才能對項目加星標"],"You need permission.":["您需要相關的權限。"],"You will not get any notifications via email":["您將不會收到任何通知郵件"],"You will only receive notifications for the events you choose":["您只會收到您選擇的事件通知"],"You will only receive notifications for threads you have participated in":["您只會收到您參與的主題的通知"],"You will receive notifications for any activity":["您將不會收到任何通知"],"You will receive notifications only for comments in which you were @mentioned":["您只會收到評論中提及(@)您的通知"],"You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account":["在賬號上 %{set_password_link} 之前將無法通過 %{protocol} 拉取或推送代碼。"],"You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile":["在賬號中添加 %{add_ssh_key_link} 之前您將無法通過 SSH 拉取或推送代碼。"],"Your name":["您的名字"],"day":["天"],"notification emails":["通知郵件"],"parent":["父級"],"pipeline schedules documentation":["流水線計劃文檔"],"with stage":["於階段"]}}}; \ No newline at end of file
diff --git a/app/assets/javascripts/locale/zh_TW/app.js b/app/assets/javascripts/locale/zh_TW/app.js
index 79904d17bf6..f0fe1e31f18 100644
--- a/app/assets/javascripts/locale/zh_TW/app.js
+++ b/app/assets/javascripts/locale/zh_TW/app.js
@@ -1 +1 @@
-var locales = locales || {}; locales['zh_TW'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-05-04 19:24-0500","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (Taiwan) (https://www.transifex.com/gitlab-zh/teams/75177/zh_TW/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_TW","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_TW","domain":"app","plural_forms":"nplurals=1; plural=0;"},"ByAuthor|by":["作者:"],"Commit":["送交"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["週期分析概述了你的專案從想法到產品實現,各階段所需的時間。"],"CycleAnalyticsStage|Code":["程式開發"],"CycleAnalyticsStage|Issue":["議題"],"CycleAnalyticsStage|Plan":["計劃"],"CycleAnalyticsStage|Production":["上線"],"CycleAnalyticsStage|Review":["複閱"],"CycleAnalyticsStage|Staging":["預備"],"CycleAnalyticsStage|Test":["測試"],"Deploy":["部署"],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["從議題建立至線上部署"],"From merge request merge until deploy to production":["從請求被合併後至線上部署"],"Introducing Cycle Analytics":["週期分析簡介"],"Last %d day":["最後 %d 天"],"Limited to showing %d event at most":["最多顯示 %d 個事件"],"Median":["中位數"],"New Issue":["新議題"],"Not available":["無法使用"],"Not enough data":["資料不足"],"OpenedNDaysAgo|Opened":["開始於"],"Pipeline Health":["流水線健康指標"],"ProjectLifecycle|Stage":["專案生命週期"],"Read more":["了解更多"],"Related Commits":["相關的送交"],"Related Deployed Jobs":["相關的部署作業"],"Related Issues":["相關的議題"],"Related Jobs":["相關的作業"],"Related Merge Requests":["相關的合併請求"],"Related Merged Requests":["相關已合併的請求"],"Showing %d event":["顯示 %d 個事件"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["程式開發階段顯示從第一次送交到建立合併請求的時間。建立第一個合併請求後,資料將自動填入。"],"The collection of events added to the data gathered for that stage.":["與該階段相關的事件。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["議題階段顯示從議題建立到設置里程碑、或將該議題加至議題看板的時間。建立第一個議題後,資料將自動填入。"],"The phase of the development lifecycle.":["專案開發生命週期的各個階段。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["計劃階段顯示從議題添加到日程後至推送第一個送交的時間。當第一次推送送交後,資料將自動填入。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["上線階段顯示從建立一個議題到部署程式至線上的總時間。當完成從想法到產品實現的循環後,資料將自動填入。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["複閱階段顯示從合併請求建立後至被合併的時間。當建立第一個合併請求後,資料將自動填入。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["預備階段顯示從合併請求被合併後至部署上線的時間。當第一次部署上線後,資料將自動填入。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["測試階段顯示相關合併請求的流水線所花的時間。當第一個流水線運作完畢後,資料將自動填入。"],"The time taken by each data entry gathered by that stage.":["每筆該階段相關資料所花的時間。"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位數是一個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["議題被列入日程表的時間"],"Time before an issue starts implementation":["議題等待開始實作的時間"],"Time between merge request creation and merge/close":["合併請求被合併或是關閉的時間"],"Time until first merge request":["第一個合併請求被建立前的時間"],"Time|hr":["小時"],"Time|min":["分鐘"],"Time|s":["秒"],"Total Time":["總時間"],"Total test time for all commits/merges":["所有送交和合併的總測試時間"],"Want to see the data? Please ask an administrator for access.":["權限不足。如需查看相關資料,請向管理員申請權限。"],"We don't have enough data to show this stage.":["因該階段的資料不足而無法顯示相關資訊"],"You need permission.":["您需要相關的權限。"],"day":["天"]}}}; \ No newline at end of file
+var locales = locales || {}; locales['zh_TW'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (Taiwan) (https://www.transifex.com/gitlab-zh/teams/75177/zh_TW/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_TW","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_TW","domain":"app","plural_forms":"nplurals=1; plural=0;"},"Are you sure you want to delete this pipeline schedule?":[""],"ByAuthor|by":["作者:"],"Cancel":[""],"Commit":["送交"],"Cron Timezone":[""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["週期分析概述了你的專案從想法到產品實現,各階段所需的時間。"],"CycleAnalyticsStage|Code":["程式開發"],"CycleAnalyticsStage|Issue":["議題"],"CycleAnalyticsStage|Plan":["計劃"],"CycleAnalyticsStage|Production":["上線"],"CycleAnalyticsStage|Review":["複閱"],"CycleAnalyticsStage|Staging":["預備"],"CycleAnalyticsStage|Test":["測試"],"Delete":[""],"Deploy":["部署"],"Description":[""],"Edit":[""],"Edit Pipeline Schedule %{id}":[""],"Failed to change the owner":[""],"Failed to remove the pipeline schedule":[""],"Filter":[""],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["從議題建立至線上部署"],"From merge request merge until deploy to production":["從請求被合併後至線上部署"],"Interval Pattern":[""],"Introducing Cycle Analytics":["週期分析簡介"],"Last %d day":["最後 %d 天"],"Last Pipeline":[""],"Limited to showing %d event at most":["最多顯示 %d 個事件"],"Median":["中位數"],"New Issue":["新議題"],"New Pipeline Schedule":[""],"No schedules":[""],"Not available":["無法使用"],"Not enough data":["資料不足"],"OpenedNDaysAgo|Opened":["開始於"],"Owner":[""],"Pipeline Health":["流水線健康指標"],"Pipeline Schedule":[""],"Pipeline Schedules":[""],"PipelineSchedules|Activated":[""],"PipelineSchedules|Active":[""],"PipelineSchedules|All":[""],"PipelineSchedules|Inactive":[""],"PipelineSchedules|Next Run":[""],"PipelineSchedules|None":[""],"PipelineSchedules|Provide a short description for this pipeline":[""],"PipelineSchedules|Take ownership":[""],"PipelineSchedules|Target":[""],"ProjectLifecycle|Stage":["專案生命週期"],"Read more":["了解更多"],"Related Commits":["相關的送交"],"Related Deployed Jobs":["相關的部署作業"],"Related Issues":["相關的議題"],"Related Jobs":["相關的作業"],"Related Merge Requests":["相關的合併請求"],"Related Merged Requests":["相關已合併的請求"],"Save pipeline schedule":[""],"Schedule a new pipeline":[""],"Select a timezone":[""],"Select target branch":[""],"Showing %d event":["顯示 %d 個事件"],"Target Branch":[""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["程式開發階段顯示從第一次送交到建立合併請求的時間。建立第一個合併請求後,資料將自動填入。"],"The collection of events added to the data gathered for that stage.":["與該階段相關的事件。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["議題階段顯示從議題建立到設置里程碑、或將該議題加至議題看板的時間。建立第一個議題後,資料將自動填入。"],"The phase of the development lifecycle.":["專案開發生命週期的各個階段。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["計劃階段顯示從議題添加到日程後至推送第一個送交的時間。當第一次推送送交後,資料將自動填入。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["上線階段顯示從建立一個議題到部署程式至線上的總時間。當完成從想法到產品實現的循環後,資料將自動填入。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["複閱階段顯示從合併請求建立後至被合併的時間。當建立第一個合併請求後,資料將自動填入。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["預備階段顯示從合併請求被合併後至部署上線的時間。當第一次部署上線後,資料將自動填入。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["測試階段顯示相關合併請求的流水線所花的時間。當第一個流水線運作完畢後,資料將自動填入。"],"The time taken by each data entry gathered by that stage.":["每筆該階段相關資料所花的時間。"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位數是一個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["議題被列入日程表的時間"],"Time before an issue starts implementation":["議題等待開始實作的時間"],"Time between merge request creation and merge/close":["合併請求被合併或是關閉的時間"],"Time until first merge request":["第一個合併請求被建立前的時間"],"Time|hr":["小時"],"Time|min":["分鐘"],"Time|s":["秒"],"Total Time":["總時間"],"Total test time for all commits/merges":["所有送交和合併的總測試時間"],"Want to see the data? Please ask an administrator for access.":["權限不足。如需查看相關資料,請向管理員申請權限。"],"We don't have enough data to show this stage.":["因該階段的資料不足而無法顯示相關資訊"],"You need permission.":["您需要相關的權限。"],"day":["天"]}}}; \ No newline at end of file
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index fe367d0c42a..ed7629948ca 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -39,10 +39,6 @@ import './shortcuts_network';
// behaviors
import './behaviors/';
-// blob
-import './blob/create_branch_dropdown';
-import './blob/target_branch_dropdown';
-
// templates
import './templates/issuable_template_selector';
import './templates/issuable_template_selectors';
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
index 658879607e2..04073ef7270 100644
--- a/app/assets/javascripts/new_commit_form.js
+++ b/app/assets/javascripts/new_commit_form.js
@@ -1,23 +1,20 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-return-assign, max-len */
(function() {
this.NewCommitForm = (function() {
- function NewCommitForm(form, targetBranchName = 'target_branch') {
+ function NewCommitForm(form) {
this.form = form;
- this.targetBranchName = targetBranchName;
this.renderDestination = this.renderDestination.bind(this);
- this.targetBranchDropdown = form.find('button.js-target-branch');
+ this.branchName = form.find('.js-branch-name');
this.originalBranch = form.find('.js-original-branch');
this.createMergeRequest = form.find('.js-create-merge-request');
this.createMergeRequestContainer = form.find('.js-create-merge-request-container');
- this.targetBranchDropdown.on('change.branch', this.renderDestination);
+ this.branchName.keyup(this.renderDestination);
this.renderDestination();
}
NewCommitForm.prototype.renderDestination = function() {
var different;
- var targetBranch = this.form.find(`input[name="${this.targetBranchName}"]`);
-
- different = targetBranch.val() !== this.originalBranch.val();
+ different = this.branchName.val() !== this.originalBranch.val();
if (different) {
this.createMergeRequestContainer.show();
if (!this.wasDifferent) {
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 929965de5c1..b0143b12cfe 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1478,7 +1478,7 @@ const normalizeNewlines = function(str) {
const cachedNoteBodyText = $noteBodyText.html();
// Show updated comment content temporarily
- $noteBodyText.html(formContent);
+ $noteBodyText.html(_.escape(formContent));
$editingNote.removeClass('is-editing fade-in-full').addClass('being-posted fade-in-half');
$editingNote.find('.note-headline-meta a').html('<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>');
@@ -1491,7 +1491,7 @@ const normalizeNewlines = function(str) {
})
.fail(() => {
// Submission failed, revert back to original note
- $noteBodyText.html(cachedNoteBodyText);
+ $noteBodyText.html(_.escape(cachedNoteBodyText));
$editingNote.removeClass('being-posted fade-in');
$editingNote.find('.fa.fa-spinner').remove();
diff --git a/app/assets/javascripts/peek.js b/app/assets/javascripts/peek.js
new file mode 100644
index 00000000000..de1a99fa3bd
--- /dev/null
+++ b/app/assets/javascripts/peek.js
@@ -0,0 +1,16 @@
+import 'vendor/peek';
+import 'vendor/peek.performance_bar';
+
+$(document).on('click', '#peek-show-queries', (e) => {
+ e.preventDefault();
+ $('.peek-rblineprof-modal').hide();
+ const $modal = $('#modal-peek-pg-queries');
+ if ($modal.length) {
+ $modal.modal('toggle');
+ }
+});
+
+$(document).on('click', '.js-lineprof-file', (e) => {
+ e.preventDefault();
+ $(e.target).parents('.peek-rblineprof-file').find('.data').toggle();
+});
diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue
index 4f6c5c177cf..2a1ecac3707 100644
--- a/app/assets/javascripts/pipelines/components/header_component.vue
+++ b/app/assets/javascripts/pipelines/components/header_component.vue
@@ -91,7 +91,7 @@ export default {
@actionClicked="postAction"
/>
<loading-icon
- v-else
+ v-if="isLoading"
size="2"/>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/nav_controls.js b/app/assets/javascripts/pipelines/components/nav_controls.js
deleted file mode 100644
index 6aa10531034..00000000000
--- a/app/assets/javascripts/pipelines/components/nav_controls.js
+++ /dev/null
@@ -1,52 +0,0 @@
-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/pipelines/components/nav_controls.vue b/app/assets/javascripts/pipelines/components/nav_controls.vue
new file mode 100644
index 00000000000..632fc167f2b
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/nav_controls.vue
@@ -0,0 +1,54 @@
+<script>
+export default {
+ name: 'PipelineNavControls',
+ 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,
+ },
+ },
+};
+</script>
+<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>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/navigation_tabs.js b/app/assets/javascripts/pipelines/components/navigation_tabs.js
deleted file mode 100644
index 1626ae17a30..00000000000
--- a/app/assets/javascripts/pipelines/components/navigation_tabs.js
+++ /dev/null
@@ -1,72 +0,0 @@
-export default {
- props: {
- scope: {
- type: String,
- required: true,
- },
-
- count: {
- type: Object,
- required: true,
- },
-
- paths: {
- type: Object,
- required: true,
- },
- },
-
- mounted() {
- $(document).trigger('init.scrolling-tabs');
- },
-
- template: `
- <ul class="nav-links scrolling-tabs">
- <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/pipelines/components/navigation_tabs.vue b/app/assets/javascripts/pipelines/components/navigation_tabs.vue
new file mode 100644
index 00000000000..d2f6d47f043
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/navigation_tabs.vue
@@ -0,0 +1,76 @@
+<script>
+export default {
+ name: 'PipelineNavigationTabs',
+ props: {
+ scope: {
+ type: String,
+ required: true,
+ },
+ count: {
+ type: Object,
+ required: true,
+ },
+ paths: {
+ type: Object,
+ required: true,
+ },
+ },
+ mounted() {
+ $(document).trigger('init.scrolling-tabs');
+ },
+};
+</script>
+<template>
+ <ul class="nav-links scrolling-tabs">
+ <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>
+</template>
diff --git a/app/assets/javascripts/pipelines/pipelines.js b/app/assets/javascripts/pipelines/pipelines.js
index 9f247af1dec..b530461837c 100644
--- a/app/assets/javascripts/pipelines/pipelines.js
+++ b/app/assets/javascripts/pipelines/pipelines.js
@@ -5,8 +5,8 @@ import pipelinesTableComponent from '../vue_shared/components/pipelines_table';
import tablePagination from '../vue_shared/components/table_pagination.vue';
import emptyState from './components/empty_state.vue';
import errorState from './components/error_state.vue';
-import navigationTabs from './components/navigation_tabs';
-import navigationControls from './components/nav_controls';
+import navigationTabs from './components/navigation_tabs.vue';
+import navigationControls from './components/nav_controls.vue';
import loadingIcon from '../vue_shared/components/loading_icon.vue';
import Poll from '../lib/utils/poll';
@@ -284,9 +284,7 @@ export default {
<table-pagination
v-if="shouldRenderPagination"
- :pagenum="pagenum"
:change="change"
- :count="state.count.all"
:pageInfo="state.pageInfo"
/>
</div>
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index 8ac71797c14..a4a7f3fa944 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -1,6 +1,8 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */
/* global Mousetrap */
/* global findFileURL */
+import Cookies from 'js-cookie';
+
import findAndFollowLink from './shortcuts_dashboard_navigation';
(function() {
@@ -14,6 +16,7 @@ import findAndFollowLink from './shortcuts_dashboard_navigation';
Mousetrap.bind('?', this.onToggleHelp);
Mousetrap.bind('s', Shortcuts.focusSearch);
Mousetrap.bind('f', (e => this.focusFilter(e)));
+ Mousetrap.bind('p b', this.onTogglePerfBar);
const $globalDropdownMenu = $('.global-dropdown-menu');
const $globalDropdownToggle = $('.global-dropdown-toggle');
@@ -53,6 +56,17 @@ import findAndFollowLink from './shortcuts_dashboard_navigation';
return Shortcuts.toggleHelp(this.enabledHelp);
};
+ Shortcuts.prototype.onTogglePerfBar = function(e) {
+ e.preventDefault();
+ const performanceBarCookieName = 'perf_bar_enabled';
+ if (Cookies.get(performanceBarCookieName) === 'true') {
+ Cookies.remove(performanceBarCookieName, { path: '/' });
+ } else {
+ Cookies.set(performanceBarCookieName, true, { path: '/' });
+ }
+ gl.utils.refreshCurrentPage();
+ };
+
Shortcuts.prototype.toggleMarkdownPreview = function(e) {
// Check if short-cut was triggered while in Write Mode
const $target = $(e.target);
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index fe6d6a792e7..1d4d90f75b6 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -40,6 +40,11 @@ export default {
required: false,
default: () => [],
},
+ hasSidebarButton: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
mixins: [
@@ -66,8 +71,9 @@ export default {
},
};
</script>
+
<template>
- <header class="page-content-header">
+ <header class="page-content-header ci-header-container">
<section class="header-main-content">
<ci-icon-badge :status="status" />
@@ -102,7 +108,7 @@ export default {
</section>
<section
- class="header-action-button nav-controls"
+ class="header-action-buttons"
v-if="actions.length">
<template
v-for="action in actions">
@@ -113,6 +119,15 @@ export default {
{{action.label}}
</a>
+ <a
+ v-if="action.type === 'ujs-link'"
+ :href="action.path"
+ data-method="post"
+ rel="nofollow"
+ :class="action.cssClass">
+ {{action.label}}
+ </a>
+
<button
v-else="action.type === 'button'"
@click="onClickAction(action)"
@@ -120,7 +135,6 @@ export default {
:class="action.cssClass"
type="button">
{{action.label}}
-
<i
v-show="action.isLoading"
class="fa fa-spin fa-spinner"
@@ -128,6 +142,18 @@ export default {
</i>
</button>
</template>
+ <button
+ v-if="hasSidebarButton"
+ type="button"
+ class="btn btn-default visible-xs-block visible-sm-block sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header"
+ aria-label="Toggle Sidebar"
+ id="toggleSidebar">
+ <i
+ class="fa fa-angle-double-left"
+ aria-hidden="true"
+ aria-labelledby="toggleSidebar">
+ </i>
+ </button>
</section>
</header>
</template>
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index 75907c35b7e..19166757e64 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -1,4 +1,7 @@
.awards {
+ display: flex;
+ flex-wrap: wrap;
+
.emoji-icon {
width: 20px;
height: 20px;
@@ -100,7 +103,6 @@
.award-menu-holder {
display: inline-block;
- position: absolute;
.tooltip {
white-space: nowrap;
@@ -108,9 +110,11 @@
}
.award-control {
- margin: 0 5px 6px 0;
+ margin: 4px 8px 4px 0;
outline: 0;
position: relative;
+ display: block;
+ float: left;
&.disabled {
cursor: default;
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 17f1dc2f479..cba890ce831 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -48,6 +48,10 @@
@include chevron-active;
border-color: $gray-darkest;
}
+
+ [data-toggle="dropdown"] {
+ outline: 0;
+ }
}
.dropdown-toggle {
@@ -109,6 +113,7 @@
&:focus:active {
@include chevron-active;
border-color: $dropdown-toggle-active-border-color;
+ outline: 0;
}
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 49163653548..38727e15c6f 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -264,3 +264,103 @@ ul.controls {
ul.indent-list {
padding: 10px 0 0 30px;
}
+
+
+// Specific styles for tree list
+.group-list-tree {
+ .folder-toggle-wrap {
+ float: left;
+ line-height: $list-text-height;
+ font-size: 0;
+
+ span {
+ font-size: $gl-font-size;
+ }
+ }
+
+ .folder-caret,
+ .folder-icon {
+ display: inline-block;
+ }
+
+ .folder-caret {
+ width: 15px;
+ }
+
+ .folder-icon {
+ width: 20px;
+ }
+
+ > .group-row:not(.has-subgroups) {
+ .folder-caret .fa {
+ opacity: 0;
+ }
+ }
+
+ .content-list li:last-child {
+ padding-bottom: 0;
+ }
+
+ .group-list-tree {
+ margin-bottom: 0;
+ margin-left: 30px;
+ position: relative;
+
+ &::before {
+ content: '';
+ display: block;
+ width: 0;
+ position: absolute;
+ top: 5px;
+ bottom: 0;
+ left: -16px;
+ border-left: 2px solid $border-white-normal;
+ }
+
+ .group-row {
+ position: relative;
+
+ &::before {
+ content: "";
+ display: block;
+ width: 10px;
+ height: 0;
+ border-top: 2px solid $border-white-normal;
+ position: absolute;
+ top: 30px;
+ left: -16px;
+ }
+
+ &:last-child::before {
+ background: $white-light;
+ height: auto;
+ top: 30px;
+ bottom: 0;
+ }
+ }
+ }
+
+ .group-row {
+ padding: 0;
+ border: none;
+ }
+
+ .group-row-contents {
+ padding: 10px 10px 8px;
+ border-top: solid 1px transparent;
+ border-bottom: solid 1px $white-normal;
+
+ &:hover {
+ border-color: $row-hover-border;
+ background-color: $row-hover;
+ cursor: pointer;
+ }
+ }
+}
+
+.js-groups-list-holder {
+ .groups-list-loading {
+ font-size: 34px;
+ text-align: center;
+ }
+}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index e35558ad8e8..203fd6d07e4 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -71,7 +71,9 @@
height: 35px;
display: flex;
justify-content: flex-end;
- border-bottom: 1px outset $white-light;
+ background: $gray-light;
+ border: 1px solid $gray-normal;
+ color: $gl-text-color;
.truncated-info {
margin: 0 auto;
@@ -82,7 +84,7 @@
}
.raw-link {
- color: inherit;
+ color: $gl-text-color;
margin-left: 5px;
text-decoration: underline;
}
@@ -93,17 +95,25 @@
display: flex;
align-self: center;
font-size: 15px;
+ margin-bottom: 4px;
svg {
height: 15px;
display: block;
- fill: $white-light;
+ fill: $gl-text-color;
}
- a,
+ .controllers-buttons,
.btn-scroll {
- margin: 0 8px;
- color: $white-light;
+ color: $gl-text-color;
+ height: 15px;
+ vertical-align: middle;
+ padding: 0;
+ width: 12px;
+ }
+
+ .controllers-buttons {
+ margin: 1px 10px;
}
.btn-scroll.animate {
@@ -137,21 +147,22 @@
top: 35px;
left: 10px;
bottom: 0;
- overflow-y: hidden;
- padding-bottom: 20px;
- padding-right: 20px;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ padding: 10px 20px 20px 5px;
}
.environment-information {
- background-color: $gray-light;
border: 1px solid $border-color;
- padding: 12px $gl-padding;
+ padding: 8px $gl-padding 12px;
border-radius: $border-radius-default;
svg {
position: relative;
- top: 1px;
+ top: 5px;
margin-right: 5px;
+ width: 22px;
+ height: 22px;
}
}
@@ -165,54 +176,31 @@
}
}
-.status-message {
- display: inline-block;
- color: $white-light;
-
- .status-icon {
- display: inline-block;
- width: 16px;
- height: 33px;
+.build-header {
+ .ci-header-container,
+ .header-action-buttons {
+ display: flex;
}
- .status-text {
- float: left;
- opacity: 0;
- margin-right: 10px;
- font-weight: normal;
- line-height: 1.8;
- transition: opacity 1s ease-out;
-
- &.animate {
- animation: fade-out-status 2s ease;
- }
+ .ci-header-container {
+ min-height: 54px;
}
- &:hover .status-text {
- opacity: 1;
+ .page-content-header {
+ padding: 10px 0 9px;
}
-}
-
-.build-header {
- position: relative;
- padding: 0;
- display: flex;
- min-height: 58px;
- align-items: center;
-
- @media (max-width: $screen-sm-max) {
- padding-right: 40px;
- margin-top: 6px;
- .btn-inverted {
- display: none;
+ .header-action-buttons {
+ @media (max-width: $screen-xs-max) {
+ .sidebar-toggle-btn {
+ margin-top: 0;
+ margin-left: 10px;
+ max-height: 34px;
+ }
}
}
.header-content {
- flex: 1;
- line-height: 1.8;
-
a {
color: $gl-text-color;
@@ -235,7 +223,7 @@
}
.right-sidebar.build-sidebar {
- padding: $gl-padding 0;
+ padding: 0;
&.right-sidebar-collapsed {
display: none;
@@ -248,6 +236,10 @@
.block {
width: 100%;
+ &:last-child {
+ border-bottom: 1px solid $border-gray-normal;
+ }
+
&.coverage {
padding: 0 16px 11px;
}
@@ -257,34 +249,39 @@
}
}
- .js-build-variable {
+ .trigger-build-variable {
color: $code-color;
}
- .js-build-value {
+ .trigger-build-value {
padding: 2px 4px;
color: $black;
background-color: $white-light;
}
- .build-sidebar-header {
- padding: 0 $gl-padding $gl-padding;
-
- .gutter-toggle {
- margin-top: 0;
- }
+ .label {
+ margin-left: 2px;
}
.retry-link {
- color: $gl-link-color;
display: none;
- &:hover {
- text-decoration: underline;
+ .btn-inverted-secondary {
+ color: $blue-500;
+
+ &:hover {
+ color: $white-light;
+ }
}
@media (max-width: $screen-sm-max) {
display: block;
+
+ .btn {
+ i {
+ margin-left: 5px;
+ }
+ }
}
}
@@ -308,6 +305,12 @@
left: $gl-padding;
width: auto;
}
+
+ svg {
+ position: relative;
+ top: 2px;
+ margin-right: 3px;
+ }
}
.builds-container {
@@ -369,6 +372,10 @@
}
}
}
+
+ .link-commit {
+ color: $blue-600;
+ }
}
.build-sidebar {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index a321941e0c9..b3f310ff67d 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -58,7 +58,7 @@
}
.emoji-block {
- padding: 10px 0 4px;
+ padding: 10px 0;
}
}
@@ -756,7 +756,6 @@
position: -webkit-sticky;
top: 60px;
z-index: 200;
- @include transition(all);
}
}
}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 702e7662527..f923a1104a9 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -249,14 +249,19 @@ ul.related-merge-requests > li {
}
@media (min-width: $screen-sm-min) {
- .new-branch-col {
- padding-top: 0;
- text-align: right;
- }
+ .emoji-block .row {
+ display: flex;
- .create-mr-dropdown-wrap {
- .btn-group:not(.hide) {
- display: inline-block;
+ .new-branch-col {
+ padding-top: 0;
+ text-align: right;
+ align-self: center;
+ }
+
+ .create-mr-dropdown-wrap {
+ .btn-group:not(.hide) {
+ display: inline-block;
+ }
}
}
}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index 8249e02b64a..3cbe8dededb 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -128,6 +128,7 @@
a {
width: 100%;
font-size: 18px;
+ margin-right: 0;
&:hover {
border: 1px solid transparent;
@@ -140,6 +141,7 @@
a {
border: none;
border-bottom: 2px solid $link-underline-blue;
+ margin-right: 0;
color: $black;
&:hover {
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index 971d54e7472..4be0e133b69 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -3,6 +3,41 @@
border-bottom: 1px solid $border-color;
}
+.project-member-tabs {
+ background: $gray-light;
+ border: 1px solid $border-color;
+
+ li {
+ width: 50%;
+
+ &.active {
+ background: $white-light;
+ }
+
+ &:first-child {
+ border-right: 1px solid $border-color;
+ }
+
+ a {
+ width: 100%;
+ text-align: center;
+ }
+ }
+}
+
+.users-project-form {
+ .btn-create {
+ margin-right: 10px;
+ }
+}
+
+.project-member-tab-content {
+ padding: $gl-padding;
+ border: 1px solid $border-color;
+ border-top: 0;
+ margin-bottom: $gl-padding;
+}
+
.member {
.list-item-name {
@media (min-width: $screen-sm-min) {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index e622e5c3f4b..a0442463390 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -462,7 +462,6 @@ ul.notes {
.more-actions-toggle {
padding: 0;
- outline: none;
&:hover .icon,
&:focus .icon {
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 58b458cd837..cd9382e8de5 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -545,12 +545,13 @@
border: 1px solid $border-color;
border-radius: 30px;
background-color: $white-light;
+ }
- &:hover {
- background-color: $stage-hover-bg;
- border: 1px solid $stage-hover-border;
- color: $gl-text-color;
- }
+ a.build-content:hover,
+ button.build-content:hover {
+ background-color: $stage-hover-bg;
+ border: 1px solid $stage-hover-border;
+ color: $gl-text-color;
}
@@ -985,10 +986,17 @@
}
}
-.pipeline-header-container {
+.ci-header-container {
min-height: 55px;
.text-center {
padding-top: 12px;
}
+
+ .header-action-buttons {
+ .btn,
+ a {
+ margin-left: 10px;
+ }
+ }
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index a2f781a6a6e..062665bc634 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -769,8 +769,7 @@ pre.light-well {
}
.project-refs-form .dropdown-menu,
-.dropdown-menu-projects,
-.dropdown-menu-branches {
+.dropdown-menu-projects {
width: 300px;
@media (min-width: $screen-sm-min) {
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 4a284247143..4ed8617b6a3 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -19,8 +19,7 @@
overflow: visible;
}
- &.ci-failed,
- &.ci-failed_with_warnings {
+ &.ci-failed {
color: $red-500;
border-color: $red-500;
@@ -39,8 +38,7 @@
}
}
- &.ci-success,
- &.ci-success_with_warnings {
+ &.ci-success {
color: $green-600;
border-color: $green-500;
@@ -73,7 +71,9 @@
}
}
- &.ci-pending {
+ &.ci-pending,
+ &.ci-success_with_warnings,
+ &.ci-failed_with_warnings {
color: $orange-600;
border-color: $orange-500;
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 47ce21d238b..91694ebcd1d 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -9,6 +9,7 @@ class ApplicationController < ActionController::Base
include SentryHelper
include WorkhorseHelper
include EnforcesTwoFactorAuthentication
+ include Peek::Rblineprof::CustomControllerHelpers
before_action :authenticate_user_from_private_token!
before_action :authenticate_user_from_rss_token!
@@ -18,7 +19,7 @@ class ApplicationController < ActionController::Base
before_action :ldap_security_check
before_action :sentry_context
before_action :default_headers
- before_action :add_gon_variables
+ before_action :add_gon_variables, unless: -> { request.path.start_with?('/-/peek') }
before_action :configure_permitted_parameters, if: :devise_controller?
before_action :require_email, unless: :devise_controller?
@@ -63,6 +64,21 @@ class ApplicationController < ActionController::Base
end
end
+ def peek_enabled?
+ return false unless Gitlab::PerformanceBar.enabled?
+ return false unless current_user
+
+ if RequestStore.active?
+ if RequestStore.store.key?(:peek_enabled)
+ RequestStore.store[:peek_enabled]
+ else
+ RequestStore.store[:peek_enabled] = cookies[:perf_bar_enabled].present?
+ end
+ else
+ cookies[:perf_bar_enabled].present?
+ end
+ end
+
protected
# This filter handles both private tokens and personal access tokens
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 907717dcb96..fe331a883c1 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -21,7 +21,7 @@ class AutocompleteController < ApplicationController
@users = [current_user, *@users].uniq
end
- if params[:author_id].present?
+ if params[:author_id].present? && current_user
author = User.find_by_id(params[:author_id])
@users = [author, *@users].uniq if author
end
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index 183eb00ef67..36ad307a93b 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -1,11 +1,6 @@
module CreatesCommit
extend ActiveSupport::Concern
- def set_start_branch_to_branch_name
- branch_exists = @repository.find_branch(@branch_name)
- @start_branch = @branch_name if branch_exists
- end
-
def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil)
if can?(current_user, :push_code, @project)
@project_to_commit_into = @project
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index b17c138d5c7..404559c8707 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -14,7 +14,7 @@ module IssuesAction
respond_to do |format|
format.html
- format.atom { render layout: false }
+ format.atom { render layout: 'xml.atom' }
end
end
end
diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb
index cefb9b4e766..8d07780f6c2 100644
--- a/app/controllers/concerns/membership_actions.rb
+++ b/app/controllers/concerns/membership_actions.rb
@@ -52,9 +52,14 @@ module MembershipActions
"You left the \"#{membershipable.human_name}\" #{source_type}."
end
- redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize]
+ respond_to do |format|
+ format.html do
+ redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize]
+ redirect_to redirect_path, notice: notice
+ end
- redirect_to redirect_path, notice: notice
+ format.json { render json: { notice: notice } }
+ end
end
protected
diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb
index 3e2a0fe4f8b..b2536a1c949 100644
--- a/app/controllers/concerns/milestone_actions.rb
+++ b/app/controllers/concerns/milestone_actions.rb
@@ -46,8 +46,10 @@ module MilestoneActions
def milestone_redirect_path
if @project
namespace_project_milestone_path(@project.namespace, @project, @milestone)
- else
+ elsif @group
group_milestone_path(@group, @milestone.safe_title, title: @milestone.title)
+ else
+ dashboard_milestone_path(@milestone.safe_title, title: @milestone.title)
end
end
end
diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb
index d0a692070d9..b68d76aeff0 100644
--- a/app/controllers/concerns/spammable_actions.rb
+++ b/app/controllers/concerns/spammable_actions.rb
@@ -17,10 +17,18 @@ module SpammableActions
private
+ def ensure_spam_config_loaded!
+ return @spam_config_loaded if defined?(@spam_config_loaded)
+
+ @spam_config_loaded = Gitlab::Recaptcha.load_configurations!
+ end
+
def recaptcha_check_with_fallback(&fallback)
if spammable.valid?
redirect_to spammable
elsif render_recaptcha?
+ ensure_spam_config_loaded!
+
if params[:recaptcha_verification]
flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
end
@@ -35,7 +43,7 @@ module SpammableActions
default_params = { request: request }
recaptcha_check = params[:recaptcha_verification] &&
- Gitlab::Recaptcha.load_configurations! &&
+ ensure_spam_config_loaded! &&
verify_recaptcha
return default_params unless recaptcha_check
diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb
index d03265e9f20..742157d113d 100644
--- a/app/controllers/dashboard/groups_controller.rb
+++ b/app/controllers/dashboard/groups_controller.rb
@@ -1,16 +1,30 @@
class Dashboard::GroupsController < Dashboard::ApplicationController
def index
- @group_members = current_user.group_members.includes(source: :route).joins(:group)
- @group_members = @group_members.merge(Group.search(params[:filter_groups])) if params[:filter_groups].present?
- @group_members = @group_members.merge(Group.sort(@sort = params[:sort]))
- @group_members = @group_members.page(params[:page])
+ @groups =
+ if params[:parent_id] && Group.supports_nested_groups?
+ parent = Group.find_by(id: params[:parent_id])
+
+ if can?(current_user, :read_group, parent)
+ GroupsFinder.new(current_user, parent: parent).execute
+ else
+ Group.none
+ end
+ else
+ current_user.groups
+ end
+
+ @groups = @groups.search(params[:filter_groups]) if params[:filter_groups].present?
+ @groups = @groups.includes(:route)
+ @groups = @groups.sort(@sort = params[:sort])
+ @groups = @groups.page(params[:page])
respond_to do |format|
format.html
format.json do
- render json: {
- html: view_to_html_string("dashboard/groups/_groups", locals: { group_members: @group_members })
- }
+ render json: GroupSerializer
+ .new(current_user: @current_user)
+ .with_pagination(request, response)
+ .represent(@groups)
end
end
end
diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb
index df528d10f6e..751dbbd8e96 100644
--- a/app/controllers/dashboard/milestones_controller.rb
+++ b/app/controllers/dashboard/milestones_controller.rb
@@ -1,6 +1,8 @@
class Dashboard::MilestonesController < Dashboard::ApplicationController
+ include MilestoneActions
+
before_action :projects
- before_action :milestone, only: [:show]
+ before_action :milestone, only: [:show, :merge_requests, :participants, :labels]
def index
respond_to do |format|
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 3d49ea97591..641c502dbe4 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -11,7 +11,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
format.html
format.atom do
load_events
- render layout: false
+ render layout: 'xml.atom'
end
format.json do
render json: {
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 623392c1240..28c90548cc1 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -47,11 +47,6 @@ class Dashboard::TodosController < Dashboard::ApplicationController
render json: todos_counts
end
- # Used in TodosHelper also
- def self.todos_count_format(count)
- count >= 100 ? '99+' : count
- end
-
private
def find_todos
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index c08943d993a..27137ffde54 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -58,7 +58,7 @@ class GroupsController < Groups::ApplicationController
format.atom do
load_events
- render layout: false
+ render layout: 'xml.atom'
end
end
end
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index c585d26df77..11db164b3fa 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -39,7 +39,7 @@ class JwtController < ApplicationController
errors: [
{ code: 'UNAUTHORIZED',
message: "HTTP Basic: Access denied\n" \
- "You have 2FA enabled, please use a personal access token for Git over HTTP.\n" \
+ "You must use a personal access token with 'api' scope for Git over HTTP.\n" \
"You can generate one at #{profile_personal_access_tokens_url}" }
]
}, status: 401
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 7025c7a1de6..66e6a9a451c 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -26,8 +26,6 @@ class Projects::BlobController < Projects::ApplicationController
end
def create
- set_start_branch_to_branch_name
-
create_commit(Files::CreateService, success_notice: "The file has been successfully created.",
success_path: -> { namespace_project_blob_path(@project.namespace, @project, File.join(@branch_name, @file_path)) },
failure_view: :new,
@@ -55,7 +53,7 @@ class Projects::BlobController < Projects::ApplicationController
def edit
if can_collaborate_with_project?
- blob.load_all_data!(@repository)
+ blob.load_all_data!
else
redirect_to action: 'show'
end
@@ -74,7 +72,7 @@ class Projects::BlobController < Projects::ApplicationController
def preview
@content = params[:content]
- @blob.load_all_data!(@repository)
+ @blob.load_all_data!
diffy = Diffy::Diff.new(@blob.data, @content, diff: '-U 3', include_diff_info: true)
diff_lines = diffy.diff.scan(/.*\n/)[2..-1]
diff_lines = Gitlab::Diff::Parser.new.parse(diff_lines)
@@ -93,9 +91,11 @@ class Projects::BlobController < Projects::ApplicationController
def diff
apply_diff_view_cookie!
- @form = UnfoldForm.new(params)
- @lines = Gitlab::Highlight.highlight_lines(repository, @ref, @path)
- @lines = @lines[@form.since - 1..@form.to - 1]
+ @blob.load_all_data!
+ @lines = Gitlab::Highlight.highlight(@blob.path, @blob.data, repository: @repository).lines
+
+ @form = UnfoldForm.new(params)
+ @lines = @lines[@form.since - 1..@form.to - 1].map(&:html_safe)
if @form.bottom?
@match_line = ''
@@ -111,7 +111,7 @@ class Projects::BlobController < Projects::ApplicationController
private
def blob
- @blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path), @project)
+ @blob ||= @repository.blob_at(@commit.id, @path)
if @blob
@blob
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index d8ed470e461..70b06cfd9b4 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -10,10 +10,10 @@ class Projects::BranchesController < Projects::ApplicationController
def index
@sort = params[:sort].presence || sort_value_name
@branches = BranchesFinder.new(@repository, params).execute
+ @branches = Kaminari.paginate_array(@branches).page(params[:page])
respond_to do |format|
format.html do
- paginate_branches
@refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
@max_commits = @branches.reduce(0) do |memo, branch|
@@ -22,7 +22,6 @@ class Projects::BranchesController < Projects::ApplicationController
end
end
format.json do
- paginate_branches unless params[:show_all]
render json: @branches.map(&:name)
end
end
@@ -106,10 +105,6 @@ class Projects::BranchesController < Projects::ApplicationController
end
end
- def paginate_branches
- @branches = Kaminari.paginate_array(@branches).page(params[:page])
- end
-
def url_to_autodeploy_setup(project, branch_name)
namespace_project_new_blob_path(
project.namespace,
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index ad92f05a42d..f33797ca310 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -26,7 +26,7 @@ class Projects::CommitsController < Projects::ApplicationController
respond_to do |format|
format.html
- format.atom { render layout: false }
+ format.atom { render layout: 'xml.atom' }
format.json do
pager_json(
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index 7f3205a8001..928f17e6a8e 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -104,7 +104,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
def render_missing_personal_token
render plain: "HTTP Basic: Access denied\n" \
- "You have 2FA enabled, please use a personal access token for Git over HTTP.\n" \
+ "You must use a personal access token with 'api' scope for Git over HTTP.\n" \
"You can generate one at #{profile_personal_access_tokens_url}",
status: 401
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 8b1efd0c572..ebb163bf9dc 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -55,7 +55,7 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to do |format|
format.html
- format.atom { render layout: false }
+ format.atom { render layout: 'xml.atom' }
format.json do
render json: {
html: view_to_html_string("projects/issues/_issues"),
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index ac151839f61..1beac202efe 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -8,7 +8,7 @@ class Projects::LabelsController < Projects::ApplicationController
before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update,
:generate, :destroy, :remove_priority,
:set_priorities]
- before_action :authorize_admin_group!, only: [:promote]
+ before_action :authorize_admin_group_labels!, only: [:promote]
respond_to :js, :html
@@ -161,7 +161,7 @@ class Projects::LabelsController < Projects::ApplicationController
return render_404 unless can?(current_user, :admin_label, @project)
end
- def authorize_admin_group!
- return render_404 unless can?(current_user, :admin_group, @project.group)
+ def authorize_admin_group_labels!
+ return render_404 unless can?(current_user, :admin_label, @project.group)
end
end
diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb
index 2662a146968..ef4f083b98f 100644
--- a/app/controllers/projects/pipeline_schedules_controller.rb
+++ b/app/controllers/projects/pipeline_schedules_controller.rb
@@ -43,7 +43,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
if schedule.update(owner: current_user)
redirect_to pipeline_schedules_path(@project)
else
- redirect_to pipeline_schedules_path(@project), alert: "Failed to change the owner"
+ redirect_to pipeline_schedules_path(@project), alert: _("Failed to change the owner")
end
end
@@ -53,7 +53,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
else
redirect_to pipeline_schedules_path(@project),
status: 302,
- alert: "Failed to remove the pipeline schedule"
+ alert: _("Failed to remove the pipeline schedule")
end
end
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index f8eb8e00a5d..266a15c1cf9 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -36,7 +36,6 @@ class Projects::TreeController < Projects::ApplicationController
def create_dir
return render_404 unless @commit_params.values.all?
- set_start_branch_to_branch_name
create_commit(Files::CreateDirService, success_notice: "The directory has been successfully created.",
success_path: namespace_project_tree_path(@project.namespace, @project, File.join(@branch_name, @dir_name)),
failure_path: namespace_project_tree_path(@project.namespace, @project, @ref))
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 38ed7d776a7..5480814874b 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -108,7 +108,7 @@ class ProjectsController < Projects::ApplicationController
format.atom do
load_events
- render layout: false
+ render layout: 'xml.atom'
end
end
end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 19fc1e5de49..c211106fbaa 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -10,7 +10,7 @@ class UsersController < ApplicationController
format.atom do
load_events
- render layout: false
+ render layout: 'xml.atom'
end
format.json do
diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb
index 53962b84618..014fc46b130 100644
--- a/app/helpers/form_helper.rb
+++ b/app/helpers/form_helper.rb
@@ -29,7 +29,7 @@ module FormHelper
current_user: true,
project_id: issuable.project.try(:id),
field_name: "#{issuable.class.model_name.param_key}[assignee_ids][]",
- default_label: 'Assignee',
+ default_label: 'Unassigned',
'max-select': 1,
'dropdown-header': 'Assignee',
multi_select: true,
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index a6014088e92..c003b01e226 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -8,7 +8,7 @@ module GroupsHelper
group = Group.find_by_full_path(group)
end
- group.try(:avatar_url) || image_path('no_group_avatar.png')
+ group.try(:avatar_url) || ActionController::Base.helpers.image_path('no_group_avatar.png')
end
def group_title(group, name = nil, url = nil)
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index c515774140c..a230db22fa2 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -121,6 +121,8 @@ module MilestonesHelper
merge_requests_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json)
elsif @group
merge_requests_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
+ else
+ merge_requests_dashboard_milestone_path(milestone, title: milestone.title, format: :json)
end
end
@@ -129,6 +131,8 @@ module MilestonesHelper
participants_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json)
elsif @group
participants_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
+ else
+ participants_dashboard_milestone_path(milestone, title: milestone.title, format: :json)
end
end
@@ -137,6 +141,8 @@ module MilestonesHelper
labels_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json)
elsif @group
labels_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
+ else
+ labels_dashboard_milestone_path(milestone, title: milestone.title, format: :json)
end
end
end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 17bfd07e00f..833d3c36b28 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -13,7 +13,7 @@ module NavHelper
else
"page-gutter right-sidebar-expanded"
end
- elsif current_path?('builds#show')
+ elsif current_path?('jobs#show')
"page-gutter build-sidebar right-sidebar-expanded"
elsif current_path?('wikis#show') ||
current_path?('wikis#edit') ||
@@ -27,6 +27,7 @@ module NavHelper
def nav_header_class
class_name = ''
class_name << " with-horizontal-nav" if defined?(nav) && nav
+ class_name << " with-peek" if peek_enabled?
class_name
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 19286fadb19..3d1b3a4711a 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -4,7 +4,7 @@ module TodosHelper
end
def todos_count_format(count)
- count > 99 ? '99+' : count
+ count > 99 ? '99+' : count.to_s
end
def todos_done_count
diff --git a/app/helpers/u2f_helper.rb b/app/helpers/u2f_helper.rb
index 143b4ca6b51..81bfe5d4eeb 100644
--- a/app/helpers/u2f_helper.rb
+++ b/app/helpers/u2f_helper.rb
@@ -1,5 +1,5 @@
module U2fHelper
def inject_u2f_api?
- browser.chrome? && browser.version.to_i >= 41 && !browser.device.mobile?
+ ((browser.chrome? && browser.version.to_i >= 41) || (browser.opera? && browser.version.to_i >= 40)) && !browser.device.mobile?
end
end
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 6a42a12891c..954d4e4d779 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -94,6 +94,10 @@ class Blob < SimpleDelegator
end
end
+ def load_all_data!
+ super(project.repository) if project
+ end
+
def no_highlighting?
raw_size && raw_size > MAXIMUM_TEXT_HIGHLIGHT_SIZE
end
@@ -151,6 +155,10 @@ class Blob < SimpleDelegator
@extension ||= extname.downcase.delete('.')
end
+ def file_type
+ Gitlab::FileDetector.type_of(path)
+ end
+
def video?
UploaderHelper::VIDEO_EXT.include?(extension)
end
@@ -176,16 +184,19 @@ class Blob < SimpleDelegator
end
def rendered_as_text?(ignore_errors: true)
- simple_viewer.text? && (ignore_errors || simple_viewer.render_error.nil?)
+ simple_viewer.is_a?(BlobViewer::Text) && (ignore_errors || simple_viewer.render_error.nil?)
end
def show_viewer_switcher?
rendered_as_text? && rich_viewer
end
+ def expanded?
+ !!@expanded
+ end
+
def expand!
- simple_viewer&.expanded = true
- rich_viewer&.expanded = true
+ @expanded = true
end
private
diff --git a/app/models/blob_viewer/base.rb b/app/models/blob_viewer/base.rb
index e6119d25fab..35965d01692 100644
--- a/app/models/blob_viewer/base.rb
+++ b/app/models/blob_viewer/base.rb
@@ -6,15 +6,15 @@ module BlobViewer
self.loading_partial_name = 'loading'
- delegate :partial_path, :loading_partial_path, :rich?, :simple?, :text?, :binary?, to: :class
+ delegate :partial_path, :loading_partial_path, :rich?, :simple?, :load_async?, :text?, :binary?, to: :class
attr_reader :blob
- attr_accessor :expanded
delegate :project, to: :blob
def initialize(blob)
@blob = blob
+ @initially_binary = blob.binary?
end
def self.partial_path
@@ -52,19 +52,15 @@ module BlobViewer
def self.can_render?(blob, verify_binary: true)
return false if verify_binary && binary? != blob.binary?
return true if extensions&.include?(blob.extension)
- return true if file_types&.include?(Gitlab::FileDetector.type_of(blob.path))
+ return true if file_types&.include?(blob.file_type)
false
end
- def load_async?
- self.class.load_async? && render_error.nil?
- end
-
def collapsed?
return @collapsed if defined?(@collapsed)
- @collapsed = !expanded && collapse_limit && blob.raw_size > collapse_limit
+ @collapsed = !blob.expanded? && collapse_limit && blob.raw_size > collapse_limit
end
def too_large?
@@ -73,6 +69,10 @@ module BlobViewer
@too_large = size_limit && blob.raw_size > size_limit
end
+ def binary_detected_after_load?
+ !@initially_binary && blob.binary?
+ end
+
# This method is used on the server side to check whether we can attempt to
# render the blob at all. Human-readable error messages are found in the
# `BlobHelper#blob_render_error_reason` helper.
diff --git a/app/models/blob_viewer/empty.rb b/app/models/blob_viewer/empty.rb
index d9d128eb273..2380578ed72 100644
--- a/app/models/blob_viewer/empty.rb
+++ b/app/models/blob_viewer/empty.rb
@@ -4,6 +4,5 @@ module BlobViewer
include ServerSide
self.partial_name = 'empty'
- self.binary = true
end
end
diff --git a/app/models/blob_viewer/server_side.rb b/app/models/blob_viewer/server_side.rb
index 05a3dd7d913..e6bcacf7f70 100644
--- a/app/models/blob_viewer/server_side.rb
+++ b/app/models/blob_viewer/server_side.rb
@@ -9,9 +9,7 @@ module BlobViewer
end
def prepare!
- if blob.project
- blob.load_all_data!(blob.project.repository)
- end
+ blob.load_all_data!
end
def render_error
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 1a766c9f6d0..20206d57c4c 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -114,16 +114,16 @@ class Commit
#
# Usually, the commit title is the first line of the commit message.
# In case this first line is longer than 100 characters, it is cut off
- # after 80 characters and ellipses (`&hellp;`) are appended.
+ # after 80 characters + `...`
def title
- full_title.length > 100 ? full_title[0..79] << "…" : full_title
+ return full_title if full_title.length < 100
+
+ full_title.truncate(81, separator: ' ', omission: '…')
end
# Returns the full commits title
def full_title
- return @full_title if @full_title
-
- @full_title =
+ @full_title ||=
if safe_message.blank?
no_commit_message
else
@@ -131,19 +131,14 @@ class Commit
end
end
- # Returns the commits description
- #
- # cut off, ellipses (`&hellp;`) are prepended to the commit message.
+ # Returns full commit message if title is truncated (greater than 99 characters)
+ # otherwise returns commit message without first line
def description
- title_end = safe_message.index("\n")
- @description ||=
- if (!title_end && safe_message.length > 100) || (title_end && title_end > 100)
- "…" << safe_message[80..-1]
- else
- safe_message.split("\n", 2)[1].try(:chomp)
- end
- end
+ return safe_message if full_title.length >= 100
+ safe_message.split("\n", 2)[1].try(:chomp)
+ end
+
def description?
description.present?
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 55c16f7e1fd..cb425706a9e 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -15,7 +15,7 @@ class CommitStatus < ActiveRecord::Base
validates :pipeline, presence: true, unless: :importing?
- validates :name, presence: true
+ validates :name, presence: true, unless: :importing?
alias_attribute :author, :user
@@ -132,6 +132,11 @@ class CommitStatus < ActiveRecord::Base
false
end
+ # To be overriden when inherrited from
+ def cancelable?
+ false
+ end
+
def stuck?
false
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 0caf7387450..4c394646787 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -63,16 +63,6 @@ class Project < ActiveRecord::Base
# update visibility_level of forks
after_update :update_forks_visibility_level
- def update_forks_visibility_level
- return unless visibility_level < visibility_level_was
-
- forks.each do |forked_project|
- if forked_project.visibility_level > visibility_level
- forked_project.visibility_level = visibility_level
- forked_project.save!
- end
- end
- end
after_validation :check_pending_delete
@@ -165,7 +155,7 @@ class Project < ActiveRecord::Base
has_many :todos, dependent: :destroy
has_many :notification_settings, dependent: :destroy, as: :source
- has_one :import_data, dependent: :delete, class_name: "ProjectImportData"
+ has_one :import_data, dependent: :delete, class_name: 'ProjectImportData'
has_one :project_feature, dependent: :destroy
has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete
has_many :container_repositories, dependent: :destroy
@@ -488,7 +478,11 @@ class Project < ActiveRecord::Base
ProjectCacheWorker.perform_async(self.id)
end
- self.import_data&.destroy
+ remove_import_data
+ end
+
+ def remove_import_data
+ import_data&.destroy
end
def import_url=(value)
@@ -1060,6 +1054,17 @@ class Project < ActiveRecord::Base
!!repository.exists?
end
+ def update_forks_visibility_level
+ return unless visibility_level < visibility_level_was
+
+ forks.each do |forked_project|
+ if forked_project.visibility_level > visibility_level
+ forked_project.visibility_level = visibility_level
+ forked_project.save!
+ end
+ end
+ end
+
def create_wiki
ProjectWiki.new(self, self.owner).wiki
true
@@ -1068,6 +1073,10 @@ class Project < ActiveRecord::Base
false
end
+ def wiki
+ @wiki ||= ProjectWiki.new(self, self.owner)
+ end
+
def jira_tracker_active?
jira_tracker? && jira_service.active
end
@@ -1190,10 +1199,6 @@ class Project < ActiveRecord::Base
end
end
- def wiki
- @wiki ||= ProjectWiki.new(self, self.owner)
- end
-
def running_or_pending_build_count(force: false)
Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do
builds.running_or_pending.count(:all)
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 00a0b407512..7460515fea8 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1104,7 +1104,7 @@ class Repository
blob = blob_at(sha, path)
return unless blob
- blob.load_all_data!(self)
+ blob.load_all_data!
blob.data
end
diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb
index cf8ff92617f..bc5c4f32f79 100644
--- a/app/policies/project_snippet_policy.rb
+++ b/app/policies/project_snippet_policy.rb
@@ -1,5 +1,10 @@
class ProjectSnippetPolicy < BasePolicy
def rules
+ # We have to check both project feature visibility and a snippet visibility and take the stricter one
+ # This will be simplified - check https://gitlab.com/gitlab-org/gitlab-ce/issues/27573
+ return unless @subject.project.feature_available?(:snippets, @user)
+ return unless Ability.allowed?(@user, :read_project, @subject.project)
+
can! :read_project_snippet if @subject.public?
return unless @user
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index 0db9e31031c..8bf35953d29 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -110,12 +110,24 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
def closing_issues_links
- markdown issues_sentence(project, closing_issues), pipeline: :gfm, author: author, project: project
+ markdown(
+ issues_sentence(project, closing_issues),
+ pipeline: :gfm,
+ author: author,
+ project: project,
+ issuable_state_filter_enabled: true
+ )
end
def mentioned_issues_links
mentioned_issues = issues_mentioned_but_not_closing(current_user)
- markdown issues_sentence(project, mentioned_issues), pipeline: :gfm, author: author, project: project
+ markdown(
+ issues_sentence(project, mentioned_issues),
+ pipeline: :gfm,
+ author: author,
+ project: project,
+ issuable_state_filter_enabled: true
+ )
end
def assign_to_closing_issues_link
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 0063920e603..514c4c2e35f 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -34,10 +34,8 @@ class BuildDetailsEntity < BuildEntity
private
def build_failed_issue_options
- {
- title: "Build Failed ##{build.id}",
- description: namespace_project_job_url(project.namespace, project, build)
- }
+ { title: "Build Failed ##{build.id}",
+ description: namespace_project_job_path(project.namespace, project, build) }
end
def current_user
diff --git a/app/serializers/build_entity.rb b/app/serializers/build_entity.rb
index c01efa9dd5c..67001f4547d 100644
--- a/app/serializers/build_entity.rb
+++ b/app/serializers/build_entity.rb
@@ -8,10 +8,14 @@ class BuildEntity < Grape::Entity
path_to(:namespace_project_job, build)
end
- expose :retry_path, if: -> (*) { build&.retryable? } do |build|
+ expose :retry_path, if: -> (*) { retryable? } do |build|
path_to(:retry_namespace_project_job, build)
end
+ expose :cancel_path, if: -> (*) { cancelable? } do |build|
+ path_to(:cancel_namespace_project_job, build)
+ end
+
expose :play_path, if: -> (*) { playable? } do |build|
path_to(:play_namespace_project_job, build)
end
@@ -25,6 +29,14 @@ class BuildEntity < Grape::Entity
alias_method :build, :object
+ def cancelable?
+ build.cancelable? && can?(request.current_user, :update_build, build)
+ end
+
+ def retryable?
+ build.retryable? && can?(request.current_user, :update_build, build)
+ end
+
def playable?
build.playable? && can?(request.current_user, :update_build, build)
end
diff --git a/app/serializers/group_entity.rb b/app/serializers/group_entity.rb
new file mode 100644
index 00000000000..4f506f7e745
--- /dev/null
+++ b/app/serializers/group_entity.rb
@@ -0,0 +1,46 @@
+class GroupEntity < Grape::Entity
+ include ActionView::Helpers::NumberHelper
+ include RequestAwareEntity
+ include MembersHelper
+ include GroupsHelper
+
+ expose :id, :name, :path, :description, :visibility
+ expose :web_url
+ expose :full_name, :full_path
+ expose :parent_id
+ expose :created_at, :updated_at
+
+ expose :permissions do
+ expose :human_group_access do |group, options|
+ group.group_members.find_by(user_id: request.current_user)&.human_access
+ end
+ end
+
+ expose :edit_path do |group|
+ edit_group_path(group)
+ end
+
+ expose :leave_path do |group|
+ leave_group_group_members_path(group)
+ end
+
+ expose :can_edit do |group|
+ can?(request.current_user, :admin_group, group)
+ end
+
+ expose :has_subgroups do |group|
+ GroupsFinder.new(request.current_user, parent: group).execute.any?
+ end
+
+ expose :number_projects_with_delimiter do |group|
+ number_with_delimiter(GroupProjectsFinder.new(group: group, current_user: request.current_user).execute.count)
+ end
+
+ expose :number_users_with_delimiter do |group|
+ number_with_delimiter(group.users.count)
+ end
+
+ expose :avatar_url do |group|
+ group_icon(group)
+ end
+end
diff --git a/app/serializers/group_serializer.rb b/app/serializers/group_serializer.rb
new file mode 100644
index 00000000000..26e8566828b
--- /dev/null
+++ b/app/serializers/group_serializer.rb
@@ -0,0 +1,19 @@
+class GroupSerializer < BaseSerializer
+ entity GroupEntity
+
+ def with_pagination(request, response)
+ tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) }
+ end
+
+ def paginated?
+ @paginator.present?
+ end
+
+ def represent(resource, opts = {})
+ if paginated?
+ super(@paginator.paginate(resource), opts)
+ else
+ super(resource, opts)
+ end
+ end
+end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index bffec216819..769749c9925 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -57,6 +57,8 @@ module Ci
cancel_pending_pipelines if project.auto_cancel_pending_pipelines?
+ pipeline_created_counter.increment(source: source)
+
pipeline.tap(&:process!)
end
@@ -131,5 +133,9 @@ module Ci
pipeline.drop if save
pipeline
end
+
+ def pipeline_created_counter
+ @pipeline_created_counter ||= Gitlab::Metrics.counter(:pipelines_created_count, "Pipelines created count")
+ end
end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index e77a3e3eac1..a65d6e11c47 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -313,11 +313,13 @@ class IssuableBaseService < BaseService
end
if issuable.previous_changes.include?('description')
- create_description_change_note(issuable)
- end
-
- if issuable.previous_changes.include?('description') && issuable.tasks?
- create_task_status_note(issuable)
+ if issuable.tasks? && issuable.updated_tasks.any?
+ create_task_status_note(issuable)
+ else
+ # TODO: Show this note if non-task content was modified.
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/33577
+ create_description_change_note(issuable)
+ end
end
if issuable.previous_changes.include?('time_estimate')
diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb
index 3bc0408f557..14addb6cf14 100644
--- a/app/uploaders/artifact_uploader.rb
+++ b/app/uploaders/artifact_uploader.rb
@@ -23,6 +23,10 @@ class ArtifactUploader < GitlabUploader
File.join(self.class.local_artifacts_store, 'tmp/cache')
end
+ def work_dir
+ File.join(self.class.local_artifacts_store, 'tmp/work')
+ end
+
private
def default_local_path
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 7e94218c23d..652277e3b78 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -13,6 +13,13 @@ class FileUploader < GitlabUploader
)
end
+ # Not using `GitlabUploader.base_dir` because all project namespaces are in
+ # the `public/uploads` dir.
+ #
+ def self.base_dir
+ root_dir
+ end
+
# Returns the part of `store_dir` that can change based on the model's current
# path
#
diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb
index 02afddb8c6a..0da7a025591 100644
--- a/app/uploaders/gitlab_uploader.rb
+++ b/app/uploaders/gitlab_uploader.rb
@@ -3,16 +3,28 @@ class GitlabUploader < CarrierWave::Uploader::Base
File.join(CarrierWave.root, upload_record.path)
end
- def self.base_dir
+ def self.root_dir
'uploads'
end
- delegate :base_dir, to: :class
+ # When object storage is used, keep the `root_dir` as `base_dir`.
+ # The files aren't really in folders there, they just have a name.
+ # The files that contain user input in their name, also contain a hash, so
+ # the names are still unique
+ #
+ # This method is overridden in the `FileUploader`
+ def self.base_dir
+ return root_dir unless file_storage?
- def file_storage?
- storage.is_a?(CarrierWave::Storage::File)
+ File.join(root_dir, 'system')
end
+ def self.file_storage?
+ self.storage == CarrierWave::Storage::File
+ end
+
+ delegate :base_dir, :file_storage?, to: :class
+
def file_cache_storage?
cache_storage.is_a?(CarrierWave::Storage::File)
end
@@ -41,4 +53,27 @@ class GitlabUploader < CarrierWave::Uploader::Base
def exists?
file.try(:exists?)
end
+
+ # Override this if you don't want to save files by default to the Rails.root directory
+ def work_dir
+ # Default path set by CarrierWave:
+ # https://github.com/carrierwaveuploader/carrierwave/blob/v1.0.0/lib/carrierwave/uploader/cache.rb#L182
+ CarrierWave.tmp_path
+ end
+
+ def filename
+ super || file&.filename
+ end
+
+ private
+
+ # To prevent files from moving across filesystems, override the default
+ # implementation:
+ # http://github.com/carrierwaveuploader/carrierwave/blob/v1.0.0/lib/carrierwave/uploader/cache.rb#L181-L183
+ def workfile_path(for_file = original_filename)
+ # To be safe, keep this directory outside of the the cache directory
+ # because calling CarrierWave.clean_cache_files! will remove any files in
+ # the cache directory.
+ File.join(work_dir, @cache_id, version_name.to_s, for_file)
+ end
end
diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb
index 02589959c2f..d11ebf0f9ca 100644
--- a/app/uploaders/lfs_object_uploader.rb
+++ b/app/uploaders/lfs_object_uploader.rb
@@ -16,16 +16,4 @@ class LfsObjectUploader < GitlabUploader
def work_dir
File.join(Gitlab.config.lfs.storage_path, 'tmp', 'work')
end
-
- private
-
- # To prevent LFS files from moving across filesystems, override the default
- # implementation:
- # http://github.com/carrierwaveuploader/carrierwave/blob/v1.0.0/lib/carrierwave/uploader/cache.rb#L181-L183
- def workfile_path(for_file = original_filename)
- # To be safe, keep this directory outside of the the cache directory
- # because calling CarrierWave.clean_cache_files! will remove any files in
- # the cache directory.
- File.join(work_dir, @cache_id, version_name.to_s, for_file)
- end
end
diff --git a/app/views/dashboard/groups/_groups.html.haml b/app/views/dashboard/groups/_groups.html.haml
index 6c3bf1a2b3b..168e6272d8e 100644
--- a/app/views/dashboard/groups/_groups.html.haml
+++ b/app/views/dashboard/groups/_groups.html.haml
@@ -1,6 +1,9 @@
.js-groups-list-holder
- %ul.content-list
- - @group_members.each do |group_member|
- = render 'shared/groups/group', group: group_member.group, group_member: group_member
-
- = paginate @group_members, theme: 'gitlab'
+ #dashboard-group-app{ data: { endpoint: dashboard_groups_path(format: :json), path: dashboard_groups_path } }
+ .groups-list-loading
+ = icon('spinner spin', 'v-show' => 'isLoading')
+ %template{ 'v-if' => '!isLoading && isEmpty' }
+ %div{ 'v-cloak' => true }
+ = render 'empty_state'
+ %template{ 'v-else-if' => '!isLoading && !isEmpty' }
+ %groups-component{ ':groups' => 'state.groups', ':page-info' => 'state.pageInfo' }
diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml
index 73ab2c95ff9..f9b45a539a1 100644
--- a/app/views/dashboard/groups/index.html.haml
+++ b/app/views/dashboard/groups/index.html.haml
@@ -2,7 +2,10 @@
- header_title "Groups", dashboard_groups_path
= render 'dashboard/groups_head'
-- if @group_members.empty?
+= webpack_bundle_tag 'common_vue'
+= webpack_bundle_tag 'groups'
+
+- if @groups.empty?
= render 'empty_state'
- else
= render 'groups'
diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder
index 06fb531b546..70ec6bc6257 100644
--- a/app/views/dashboard/issues.atom.builder
+++ b/app/views/dashboard/issues.atom.builder
@@ -1,10 +1,7 @@
-xml.instruct!
-xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
- xml.title "#{current_user.name} issues"
- xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
- xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html"
- xml.id issues_dashboard_url
- xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
+xml.title "#{current_user.name} issues"
+xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
+xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html"
+xml.id issues_dashboard_url
+xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
- xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any?
-end
+xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any?
diff --git a/app/views/dashboard/projects/index.atom.builder b/app/views/dashboard/projects/index.atom.builder
index 13f7a8ddcec..747c53b440e 100644
--- a/app/views/dashboard/projects/index.atom.builder
+++ b/app/views/dashboard/projects/index.atom.builder
@@ -1,10 +1,7 @@
-xml.instruct!
-xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
- xml.title "Activity"
- xml.link href: dashboard_projects_url(rss_url_options), rel: "self", type: "application/atom+xml"
- xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html"
- xml.id dashboard_projects_url
- xml.updated @events[0].updated_at.xmlschema if @events[0]
+xml.title "Activity"
+xml.link href: dashboard_projects_url(rss_url_options), rel: "self", type: "application/atom+xml"
+xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html"
+xml.id dashboard_projects_url
+xml.updated @events[0].updated_at.xmlschema if @events[0]
- xml << render(partial: 'events/event', collection: @events) if @events.any?
-end
+xml << render(partial: 'events/event', collection: @events) if @events.any?
diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder
index 469768d83f2..a239ea8caf0 100644
--- a/app/views/groups/issues.atom.builder
+++ b/app/views/groups/issues.atom.builder
@@ -1,10 +1,7 @@
-xml.instruct!
-xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
- xml.title "#{@group.name} issues"
- xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
- xml.link href: issues_group_url, rel: "alternate", type: "text/html"
- xml.id issues_group_url
- xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
+xml.title "#{@group.name} issues"
+xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
+xml.link href: issues_group_url, rel: "alternate", type: "text/html"
+xml.id issues_group_url
+xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
- xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any?
-end
+xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any?
diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder
index 914091dfd15..0f67b15c301 100644
--- a/app/views/groups/show.atom.builder
+++ b/app/views/groups/show.atom.builder
@@ -1,10 +1,7 @@
-xml.instruct!
-xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
- xml.title "#{@group.name} activity"
- xml.link href: group_url(@group, rss_url_options), rel: "self", type: "application/atom+xml"
- xml.link href: group_url(@group), rel: "alternate", type: "text/html"
- xml.id group_url(@group)
- xml.updated @events[0].updated_at.xmlschema if @events[0]
+xml.title "#{@group.name} activity"
+xml.link href: group_url(@group, rss_url_options), rel: "self", type: "application/atom+xml"
+xml.link href: group_url(@group), rel: "alternate", type: "text/html"
+xml.id group_url(@group)
+xml.updated @events[0].updated_at.xmlschema if @events[0]
- xml << render(@events) if @events.any?
-end
+xml << render(@events) if @events.any?
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index ea8bbe92d86..331d1181220 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -29,6 +29,10 @@
%td Focus Filter
%tr
%td.shortcut
+ .key p b
+ %td Show/hide the Performance Bar
+ %tr
+ %td.shortcut
.key ?
%td Show/hide this dialog
%tr
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index b20e3a22133..31d0e589c26 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -4,7 +4,7 @@
Community Edition
- if user_signed_in?
%span= Gitlab::VERSION
- %small= Gitlab::REVISION
+ %small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab::REVISION)
= version_status_badge
%p.slead
GitLab is open source software to collaborate on code.
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 1ef0d524dbb..eea33b5966f 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -28,6 +28,7 @@
= stylesheet_link_tag "application", media: "all"
= stylesheet_link_tag "print", media: "print"
= stylesheet_link_tag "test", media: "all" if Rails.env.test?
+ = stylesheet_link_tag 'peek' if peek_enabled?
= Gon::Base.render_data
@@ -37,6 +38,7 @@
= webpack_bundle_tag "main"
= webpack_bundle_tag "raven" if current_application_settings.clientside_sentry_enabled
= webpack_bundle_tag "test" if Rails.env.test?
+ = webpack_bundle_tag 'peek' if peek_enabled?
- if content_for?(:page_specific_javascripts)
= yield :page_specific_javascripts
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 03688e9ff21..2b07273a0a8 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -3,6 +3,7 @@
= render "layouts/head"
%body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } }
= render "layouts/init_auto_complete" if @gfm_form
+ = render 'peek/bar'
= render "layouts/header/default", title: header_title
= render 'layouts/page', sidebar: sidebar, nav: nav
diff --git a/app/views/layouts/xml.atom.builder b/app/views/layouts/xml.atom.builder
new file mode 100644
index 00000000000..4ee09cb87a1
--- /dev/null
+++ b/app/views/layouts/xml.atom.builder
@@ -0,0 +1,4 @@
+xml.instruct!
+xml.feed 'xmlns' => 'http://www.w3.org/2005/Atom', 'xmlns:media' => 'http://search.yahoo.com/mrss/' do
+ xml << yield
+end
diff --git a/app/views/peek/views/_mysql2.html.haml b/app/views/peek/views/_mysql2.html.haml
new file mode 100644
index 00000000000..ac811a10ef5
--- /dev/null
+++ b/app/views/peek/views/_mysql2.html.haml
@@ -0,0 +1,4 @@
+- local_assigns.fetch(:view)
+
+= render 'peek/views/sql', view: view
+mysql
diff --git a/app/views/peek/views/_pg.html.haml b/app/views/peek/views/_pg.html.haml
new file mode 100644
index 00000000000..ee94c2f3274
--- /dev/null
+++ b/app/views/peek/views/_pg.html.haml
@@ -0,0 +1,4 @@
+- local_assigns.fetch(:view)
+
+= render 'peek/views/sql', view: view
+pg
diff --git a/app/views/peek/views/_sql.html.haml b/app/views/peek/views/_sql.html.haml
new file mode 100644
index 00000000000..16fc010f66f
--- /dev/null
+++ b/app/views/peek/views/_sql.html.haml
@@ -0,0 +1,13 @@
+%strong
+ %a#peek-show-queries{ href: '#' }
+ %span{ data: { defer_to: "#{view.defer_key}-duration" } }...
+ \/
+ %span{ data: { defer_to: "#{view.defer_key}-calls" } }...
+#modal-peek-pg-queries.modal{ tabindex: -1 }
+ .modal-dialog
+ #modal-peek-pg-queries-content.modal-content
+ .modal-header
+ %a.close{ href: "#", "data-dismiss" => "modal" } ×
+ %h4
+ SQL queries
+ .modal-body{ data: { defer_to: "#{view.defer_key}-queries" } }...
diff --git a/app/views/projects/blob/_remove.html.haml b/app/views/projects/blob/_remove.html.haml
index db6662a95ac..c8ca0406213 100644
--- a/app/views/projects/blob/_remove.html.haml
+++ b/app/views/projects/blob/_remove.html.haml
@@ -6,7 +6,7 @@
%h3.page-title Delete #{@blob.name}
.modal-body
- = form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal js-replace-blob-form js-quick-submit js-requires-input' do
+ = form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal js-delete-blob-form js-quick-submit js-requires-input' do
= render 'shared/new_commit_form', placeholder: "Delete #{@blob.name}"
.form-group
@@ -15,4 +15,4 @@
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
:javascript
- new NewCommitForm($('.js-replace-blob-form'))
+ new NewCommitForm($('.js-delete-blob-form'))
diff --git a/app/views/projects/blob/_viewer.html.haml b/app/views/projects/blob/_viewer.html.haml
index 4252f27d007..013f1c267c8 100644
--- a/app/views/projects/blob/_viewer.html.haml
+++ b/app/views/projects/blob/_viewer.html.haml
@@ -1,13 +1,19 @@
- hidden = local_assigns.fetch(:hidden, false)
- render_error = viewer.render_error
-- load_async = local_assigns.fetch(:load_async, viewer.load_async?)
+- load_async = local_assigns.fetch(:load_async, viewer.load_async? && render_error.nil?)
- viewer_url = local_assigns.fetch(:viewer_url) { url_for(params.merge(viewer: viewer.type, format: :json)) } if load_async
.blob-viewer{ data: { type: viewer.type, url: viewer_url }, class: ('hidden' if hidden) }
- - if load_async
- = render viewer.loading_partial_path, viewer: viewer
- - elsif render_error
+ - if render_error
= render 'projects/blob/render_error', viewer: viewer
+ - elsif load_async
+ = render viewer.loading_partial_path, viewer: viewer
- else
- viewer.prepare!
+
+ -# In the rare case where the first kilobyte of the file looks like text,
+ -# but the file turns out to actually be binary after loading all data,
+ -# we fall back on the binary Download viewer.
+ - viewer = BlobViewer::Download.new(viewer.blob) if viewer.binary_detected_after_load?
+
= render viewer.partial_path, viewer: viewer
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index b5f67cae341..281d823da52 100644
--- a/app/views/projects/commit/_change.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -18,14 +18,13 @@
= label_tag 'start_branch', branch_label, class: 'control-label'
.col-sm-10
= hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch'
- = dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown js-target-branch dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false } })
+ = dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false } })
- if can?(current_user, :push_code, @project)
- .js-create-merge-request-container
- .checkbox
- = label_tag do
- = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: nil
- Start a <strong>new merge request</strong> with these changes
+ .checkbox
+ = label_tag do
+ = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: nil
+ Start a <strong>new merge request</strong> with these changes
- else
= hidden_field_tag 'create_merge_request', 1, id: nil
.form-actions
@@ -35,6 +34,3 @@
- unless can?(current_user, :push_code, @project)
.inline.prepend-left-10
= commit_in_fork_help
-
-:javascript
- new NewCommitForm($('.js-#{type}-form'), 'start_branch')
diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder
index 2f0b6e39800..9cf792e1721 100644
--- a/app/views/projects/commits/show.atom.builder
+++ b/app/views/projects/commits/show.atom.builder
@@ -1,10 +1,7 @@
-xml.instruct!
-xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
- xml.title "#{@project.name}:#{@ref} commits"
- xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref, rss_url_options), rel: "self", type: "application/atom+xml"
- xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref), rel: "alternate", type: "text/html"
- xml.id namespace_project_commits_url(@project.namespace, @project, @ref)
- xml.updated @commits.first.committed_date.xmlschema if @commits.any?
+xml.title "#{@project.name}:#{@ref} commits"
+xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref, rss_url_options), rel: "self", type: "application/atom+xml"
+xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref), rel: "alternate", type: "text/html"
+xml.id namespace_project_commits_url(@project.namespace, @project, @ref)
+xml.updated @commits.first.committed_date.xmlschema if @commits.any?
- xml << render(@commits) if @commits.any?
-end
+xml << render(@commits) if @commits.any?
diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml
index 59844bc00cd..ec1c434a4b8 100644
--- a/app/views/projects/diffs/_content.html.haml
+++ b/app/views/projects/diffs/_content.html.haml
@@ -6,7 +6,7 @@
- elsif blob.truncated?
.nothing-here-block The file could not be displayed because it is too large.
- elsif blob.readable_text?
- - if !diff_file.repository.diffable?(blob)
+ - if !diff_file.diffable?
.nothing-here-block This diff was suppressed by a .gitattributes entry.
- elsif diff_file.collapsed?
- url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier))
diff --git a/app/views/projects/diffs/viewers/_text.html.haml b/app/views/projects/diffs/viewers/_text.html.haml
index e4b89671724..120d3540223 100644
--- a/app/views/projects/diffs/viewers/_text.html.haml
+++ b/app/views/projects/diffs/viewers/_text.html.haml
@@ -1,5 +1,5 @@
- blob = diff_file.blob
-- blob.load_all_data!(diff_file.repository)
+- blob.load_all_data!
- total_lines = blob.lines.size
- total_lines -= 1 if total_lines > 0 && blob.lines.last.blank?
- if diff_view == :parallel
diff --git a/app/views/projects/group_links/_index.html.haml b/app/views/projects/group_links/_index.html.haml
deleted file mode 100644
index debb0214d06..00000000000
--- a/app/views/projects/group_links/_index.html.haml
+++ /dev/null
@@ -1,53 +0,0 @@
-- page_title "Groups"
-.row.prepend-top-default
- .col-lg-3.settings-sidebar
- %h4.prepend-top-0
- Share project with other groups
- %p
- Projects can be stored in only one group at once. However you can share a project with other groups here.
- .col-lg-9
- = form_tag namespace_project_group_links_path(@project.namespace, @project), class: 'js-requires-input', method: :post do
- .form-group
- = label_tag :link_group_id, "Select a group to share with", class: "label-light"
- = groups_select_tag(:link_group_id, data: { skip_groups: @skip_groups }, required: true)
- .form-group
- = label_tag :link_group_access, "Max access level", class: "label-light"
- .select-wrapper
- = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control"
- = icon('caret-down')
- .form-group
- = label_tag :expires_at, 'Access expiration date', class: 'label-light'
- .clearable-input
- = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date-groups', placeholder: 'Select access expiration date', id: 'expires_at_groups'
- %i.clear-icon.js-clear-input
- .help-block
- On this date, all members in the group will automatically lose access to this project.
- = submit_tag "Share", class: "btn btn-create"
- .col-lg-9.col-lg-offset-3
- %hr
- .col-lg-9.col-lg-offset-3.append-bottom-default.enabled-groups
- %h5.prepend-top-0
- Groups you share with (#{@group_links.size})
- - if @group_links.present?
- %ul.well-list
- - @group_links.each do |group_link|
- - group = group_link.group
- %li
- .pull-left.append-right-10.hidden-xs
- = icon("folder-open-o", class: "settings-list-icon")
- .pull-left
- = link_to group do
- = group.full_name
- %br
- up to #{group_link.human_access}
- - if group_link.expires?
- ·
- %span{ class: ('text-warning' if group_link.expires_soon?) }
- expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
- .pull-right
- = link_to namespace_project_group_link_path(@project.namespace, @project, group_link), method: :delete, class: "btn btn-transparent" do
- %span.sr-only disable sharing
- = icon("trash")
- - else
- .settings-message.text-center
- There are no groups with access to your project, add one in the form above
diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder
index 4feec09bb5d..61346884346 100644
--- a/app/views/projects/issues/index.atom.builder
+++ b/app/views/projects/issues/index.atom.builder
@@ -1,10 +1,7 @@
-xml.instruct!
-xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
- xml.title "#{@project.name} issues"
- xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
- xml.link href: namespace_project_issues_url(@project.namespace, @project), rel: "alternate", type: "text/html"
- xml.id namespace_project_issues_url(@project.namespace, @project)
- xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
+xml.title "#{@project.name} issues"
+xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
+xml.link href: namespace_project_issues_url(@project.namespace, @project), rel: "alternate", type: "text/html"
+xml.id namespace_project_issues_url(@project.namespace, @project)
+xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
- xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any?
-end
+xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any?
diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml
index 09d4ddc243b..8b9e6e57ec4 100644
--- a/app/views/projects/jobs/_sidebar.html.haml
+++ b/app/views/projects/jobs/_sidebar.html.haml
@@ -1,19 +1,15 @@
- builds = @build.pipeline.builds.to_a
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } }
- .block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default
- Job
- %strong ##{@build.id}
- %a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" }
- = icon('angle-double-right')
- - if @build.coverage
- .block.coverage
- .title
- Test coverage
- %p.build-detail-row
- #{@build.coverage}%
-
.blocks-container
+ .block
+ %strong
+ = @build.name
+ %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-build-toggle{ href: "#", 'aria-label': 'Toggle Sidebar', role: 'button' }
+ = icon('angle-double-right')
+
+ #js-details-block-vue
+
- if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
.block{ class: ("block-first" if !@build.coverage) }
.title
@@ -40,37 +36,6 @@
= link_to browse_namespace_project_job_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
Browse
- .block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) }
- .title
- Job details
- - if can?(current_user, :update_build, @build) && @build.retryable?
- = link_to "Retry job", retry_namespace_project_job_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post
- - if @build.merge_request
- %p.build-detail-row
- %span.build-light-text Merge Request:
- = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request), class: 'bold'
- - if @build.duration
- %p.build-detail-row
- %span.build-light-text Duration:
- = time_interval_in_words(@build.duration)
- - if @build.finished_at
- %p.build-detail-row
- %span.build-light-text Finished:
- #{time_ago_with_tooltip(@build.finished_at)}
- - if @build.erased_at
- %p.build-detail-row
- %span.build-light-text Erased:
- #{time_ago_with_tooltip(@build.erased_at)}
- %p.build-detail-row
- %span.build-light-text Runner:
- - if @build.runner && current_user && current_user.admin
- = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id)
- - elsif @build.runner
- \##{@build.runner.id}
- .btn-group.btn-group-justified{ role: :group }
- - if @build.active?
- = link_to "Cancel", cancel_namespace_project_job_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post
-
- if @build.trigger_request
.build-widget
%h4.title
@@ -87,26 +52,29 @@
- @build.trigger_request.variables.each do |key, value|
.hide.js-build
- .js-build-variable= key
- .js-build-value= value
+ .js-build-variable.trigger-build-variable= key
+ .js-build-value.trigger-build-value= value
.block
- .title
- Commit title
+ %p
+ Commit
+ = link_to @build.pipeline.short_sha, namespace_project_commit_path(@project.namespace, @project, @build.pipeline.sha), class: 'commit-sha link-commit'
+ = clipboard_button(text: @build.pipeline.short_sha, title: "Copy commit SHA to clipboard")
+ - if @build.merge_request
+ in
+ = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request), class: 'link-commit'
+
%p.build-light-text.append-bottom-0
#{@build.pipeline.git_commit_title}
- - if @build.tags.any?
- .block
- .title
- Tags
- - @build.tag_list.each do |tag|
- %span.label.label-primary
- = tag
-
- if @build.pipeline.stages_count > 1
.dropdown.build-dropdown
- .title Stage
+ .title
+ %span{ class: "ci-status-icon-#{@build.pipeline.status}" }
+ = ci_icon_for_status(@build.pipeline.status)
+ = link_to "##{@build.pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, @build.pipeline), class: 'link-commit'
+ from
+ = link_to "#{@build.pipeline.ref}", namespace_project_branch_path(@project.namespace, @project, @build.pipeline.ref), class: 'link-commit'
%button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.stage-selection More
= icon('chevron-down')
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index 0d10dfcef70..c73bae0a2c9 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -3,9 +3,8 @@
= render "projects/pipelines/head"
%div{ class: container_class }
- .build-page
- = render "header"
-
+ .build-page.js-build-page
+ #js-build-header-vue
- if @build.stuck?
- unless @build.any_runners_online?
.bs-callout.bs-callout-warning.js-build-stuck
@@ -47,52 +46,52 @@
- if environment.try(:last_deployment)
and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')}
- .prepend-top-default.js-build-erased
- - if @build.erased?
+ - if @build.erased?
+ .prepend-top-default.js-build-erased
.erased.alert.alert-warning
- if @build.erased_by_user?
Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)}
- else
Job has been erased #{time_ago_with_tooltip(@build.erased_at)}
- .prepend-top-default
- .build-trace-container#build-trace
- .top-bar.sticky
- .js-truncated-info.truncated-info.hidden<
- Showing last
- %span.js-truncated-info-size.truncated-info-size><
- KiB of log -
- %a.js-raw-link.raw-link{ href: raw_namespace_project_job_path(@project.namespace, @project, @build) }>< Complete Raw
- .controllers
- - if @build.has_trace?
- = link_to raw_namespace_project_job_path(@project.namespace, @project, @build),
- title: 'Open raw trace',
- data: { placement: 'top', container: 'body' },
- class: 'js-raw-link-controller has-tooltip' do
- = icon('download')
-
- - if can?(current_user, :update_build, @project) && @build.erasable?
- = link_to erase_namespace_project_job_path(@project.namespace, @project, @build),
- method: :post,
- data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' },
- title: 'Erase Build',
- class: 'has-tooltip js-erase-link' do
- = icon('trash')
+ .build-trace-container#build-trace
+ .top-bar.sticky
+ .js-truncated-info.truncated-info.hidden<
+ Showing last
+ %span.js-truncated-info-size.truncated-info-size><
+ KiB of log -
+ %a.js-raw-link.raw-link{ href: raw_namespace_project_job_path(@project.namespace, @project, @build) }>< Complete Raw
+ .controllers
+ - if @build.has_trace?
+ = link_to raw_namespace_project_job_path(@project.namespace, @project, @build),
+ title: 'Show complete raw',
+ data: { placement: 'top', container: 'body' },
+ class: 'js-raw-link-controller has-tooltip controllers-buttons' do
+ = icon('file-text-o')
- %button.js-scroll-up.btn-scroll.btn-transparent.btn-blank.has-tooltip{ type: 'button',
- disabled: true,
- title: 'Scroll Up',
- data: { placement: 'top', container: 'body'} }
+ - if can?(current_user, :update_build, @project) && @build.erasable?
+ = link_to erase_namespace_project_job_path(@project.namespace, @project, @build),
+ method: :post,
+ data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' },
+ title: 'Erase job log',
+ class: 'has-tooltip js-erase-link controllers-buttons' do
+ = icon('trash')
+ .has-tooltip.controllers-buttons{ title: 'Scroll to top', data: { placement: 'top', container: 'body'} }
+ %button.js-scroll-up.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true }
= custom_icon('scroll_up')
- %button.js-scroll-down.btn-scroll.btn-transparent.btn-blank.has-tooltip{ type: 'button',
- disabled: true,
- title: 'Scroll Down',
- data: { placement: 'top', container: 'body'} }
+ .has-tooltip.controllers-buttons{ title: 'Scroll to bottom', data: { placement: 'top', container: 'body'} }
+ %button.js-scroll-down.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true }
= custom_icon('scroll_down')
- .bash.sticky.js-scroll-container
- %code.js-build-output
- .build-loader-animation.js-build-refresh
+ .bash.sticky.js-scroll-container
+ %code.js-build-output
+ .build-loader-animation.js-build-refresh
= render "sidebar"
.js-build-options{ data: javascript_build_options }
+
+#js-job-details-vue{ data: { endpoint: namespace_project_job_path(@project.namespace, @project, @build, format: :json) } }
+
+- content_for :page_specific_javascripts do
+ = webpack_bundle_tag('common_vue')
+ = webpack_bundle_tag('job_details')
diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml
index bbed10039af..25ae4e0e18f 100644
--- a/app/views/projects/pipeline_schedules/_form.html.haml
+++ b/app/views/projects/pipeline_schedules/_form.html.haml
@@ -6,28 +6,28 @@
= form_errors(@schedule)
.form-group
.col-md-9
- = f.label :description, 'Description', class: 'label-light'
- = f.text_field :description, class: 'form-control', required: true, autofocus: true, placeholder: 'Provide a short description for this pipeline'
+ = f.label :description, _('Description'), class: 'label-light'
+ = f.text_field :description, class: 'form-control', required: true, autofocus: true, placeholder: _('PipelineSchedules|Provide a short description for this pipeline')
.form-group
.col-md-9
- = f.label :cron, 'Interval Pattern', class: 'label-light'
+ = f.label :cron, _('Interval Pattern'), class: 'label-light'
#interval-pattern-input{ data: { initial_interval: @schedule.cron } }
.form-group
.col-md-9
- = f.label :cron_timezone, 'Cron Timezone', class: 'label-light'
- = dropdown_tag("Select a timezone", options: { toggle_class: 'btn js-timezone-dropdown', title: "Select a timezone", filter: true, placeholder: "Filter", data: { data: timezone_data } } )
+ = f.label :cron_timezone, _('Cron Timezone'), class: 'label-light'
+ = dropdown_tag(_("Select a timezone"), options: { toggle_class: 'btn js-timezone-dropdown', title: _("Select a timezone"), filter: true, placeholder: _("Filter"), data: { data: timezone_data } } )
= f.text_field :cron_timezone, value: @schedule.cron_timezone, id: 'schedule_cron_timezone', class: 'hidden', name: 'schedule[cron_timezone]', required: true
.form-group
.col-md-9
- = f.label :ref, 'Target Branch', class: 'label-light'
- = dropdown_tag("Select target branch", options: { toggle_class: 'btn js-target-branch-dropdown git-revision-dropdown-toggle', dropdown_class: 'git-revision-dropdown', title: "Select target branch", filter: true, placeholder: "Filter", data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } )
+ = f.label :ref, _('Target Branch'), class: 'label-light'
+ = dropdown_tag(_("Select target branch"), options: { toggle_class: 'btn js-target-branch-dropdown git-revision-dropdown-toggle', dropdown_class: 'git-revision-dropdown', title: _("Select target branch"), filter: true, placeholder: _("Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } )
= f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true
.form-group
.col-md-9
- = f.label :active, 'Activated', class: 'label-light'
+ = f.label :active, _('PipelineSchedules|Activated'), class: 'label-light'
%div
= f.check_box :active, required: false, value: @schedule.active?
Active
.footer-block.row-content-block
- = f.submit 'Save pipeline schedule', class: 'btn btn-create', tabindex: 3
- = link_to 'Cancel', pipeline_schedules_path(@project), class: 'btn btn-cancel'
+ = f.submit _('Save pipeline schedule'), class: 'btn btn-create', tabindex: 3
+ = link_to _('Cancel'), pipeline_schedules_path(@project), class: 'btn btn-cancel'
diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
index 7bde839e26f..2d3344a4aaf 100644
--- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
+++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
@@ -13,12 +13,12 @@
= ci_icon_for_status(pipeline_schedule.last_pipeline.status)
%span ##{pipeline_schedule.last_pipeline.id}
- else
- None
+ = _("PipelineSchedules|None")
%td.next-run-cell
- if pipeline_schedule.active?
= time_ago_with_tooltip(pipeline_schedule.real_next_run)
- else
- Inactive
+ = _("PipelineSchedules|Inactive")
%td
- if pipeline_schedule.owner
= image_tag avatar_icon(pipeline_schedule.owner, 20), class: "avatar s20"
@@ -27,11 +27,11 @@
%td
.pull-right.btn-group
- if can?(current_user, :update_pipeline_schedule, @project) && !pipeline_schedule.owned_by?(current_user)
- = link_to take_ownership_pipeline_schedule_path(pipeline_schedule), method: :post, title: 'Take Ownership', class: 'btn' do
- Take ownership
+ = link_to take_ownership_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('PipelineSchedules|Take ownership'), class: 'btn' do
+ = s_('PipelineSchedules|Take ownership')
- if can?(current_user, :update_pipeline_schedule, pipeline_schedule)
- = link_to edit_pipeline_schedule_path(pipeline_schedule), title: 'Edit', class: 'btn' do
+ = link_to edit_pipeline_schedule_path(pipeline_schedule), title: _('Edit'), class: 'btn' do
= icon('pencil')
- if can?(current_user, :admin_pipeline_schedule, pipeline_schedule)
- = link_to pipeline_schedule_path(pipeline_schedule), title: 'Delete', method: :delete, class: 'btn btn-remove', data: { confirm: "Are you sure you want to cancel this pipeline?" } do
+ = link_to pipeline_schedule_path(pipeline_schedule), title: _('Delete'), method: :delete, class: 'btn btn-remove', data: { confirm: _("Are you sure you want to delete this pipeline schedule?") } do
= icon('trash')
diff --git a/app/views/projects/pipeline_schedules/_table.html.haml b/app/views/projects/pipeline_schedules/_table.html.haml
index 25c7604eb24..d0c7ea77263 100644
--- a/app/views/projects/pipeline_schedules/_table.html.haml
+++ b/app/views/projects/pipeline_schedules/_table.html.haml
@@ -2,11 +2,11 @@
%table.table.ci-table
%thead
%tr
- %th Description
- %th Target
- %th Last Pipeline
- %th Next Run
- %th Owner
+ %th= _("Description")
+ %th= s_("PipelineSchedules|Target")
+ %th= _("Last Pipeline")
+ %th= s_("PipelineSchedules|Next Run")
+ %th= _("Owner")
%th
= render partial: "pipeline_schedule", collection: @schedules
diff --git a/app/views/projects/pipeline_schedules/_tabs.html.haml b/app/views/projects/pipeline_schedules/_tabs.html.haml
index 2a1fb16876a..7fcb624a9dd 100644
--- a/app/views/projects/pipeline_schedules/_tabs.html.haml
+++ b/app/views/projects/pipeline_schedules/_tabs.html.haml
@@ -1,18 +1,18 @@
%ul.nav-links
%li{ class: active_when(scope.nil?) }>
= link_to schedule_path_proc.call(nil) do
- All
+ = s_("PipelineSchedules|All")
%span.badge.js-totalbuilds-count
= number_with_delimiter(all_schedules.count(:id))
%li{ class: active_when(scope == 'active') }>
= link_to schedule_path_proc.call('active') do
- Active
+ = s_("PipelineSchedules|Active")
%span.badge
= number_with_delimiter(all_schedules.active.count(:id))
%li{ class: active_when(scope == 'inactive') }>
= link_to schedule_path_proc.call('inactive') do
- Inactive
+ = s_("PipelineSchedules|Inactive")
%span.badge
= number_with_delimiter(all_schedules.inactive.count(:id))
diff --git a/app/views/projects/pipeline_schedules/edit.html.haml b/app/views/projects/pipeline_schedules/edit.html.haml
index e16fe0b7a98..9b2a7b5821d 100644
--- a/app/views/projects/pipeline_schedules/edit.html.haml
+++ b/app/views/projects/pipeline_schedules/edit.html.haml
@@ -1,7 +1,7 @@
-- page_title "Edit", @schedule.description, "Pipeline Schedule"
+- page_title _("Edit"), @schedule.description, _("Pipeline Schedule")
%h3.page-title
- Edit Pipeline Schedule #{@schedule.id}
+ = _("Edit Pipeline Schedule %{id}") % { id: @schedule.id }
%hr
= render "form"
diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml
index 6751efaaf2f..4a96ee652d2 100644
--- a/app/views/projects/pipeline_schedules/index.html.haml
+++ b/app/views/projects/pipeline_schedules/index.html.haml
@@ -3,7 +3,7 @@
= webpack_bundle_tag 'schedules_index'
- @no_container = true
-- page_title "Pipeline Schedules"
+- page_title _("Pipeline Schedules")
= render "projects/pipelines/head"
%div{ class: container_class }
@@ -21,4 +21,4 @@
= render partial: "table"
- else
.light-well
- .nothing-here-block No schedules
+ .nothing-here-block= _("No schedules")
diff --git a/app/views/projects/pipeline_schedules/new.html.haml b/app/views/projects/pipeline_schedules/new.html.haml
index b89e170ad3c..87390d4dd02 100644
--- a/app/views/projects/pipeline_schedules/new.html.haml
+++ b/app/views/projects/pipeline_schedules/new.html.haml
@@ -1,7 +1,7 @@
-- page_title "New Pipeline Schedule"
+- page_title _("New Pipeline Schedule")
%h3.page-title
- Schedule a new pipeline
+ = _("Schedule a new pipeline")
%hr
= render "form"
diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml
index a33da149c62..d2f0cb0806f 100644
--- a/app/views/projects/pipelines/_head.html.haml
+++ b/app/views/projects/pipelines/_head.html.haml
@@ -10,7 +10,7 @@
Pipelines
- if project_nav_tab? :builds
- = nav_link(controller: [:builds, :artifacts]) do
+ = nav_link(controller: [:jobs, :artifacts]) do
= link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
%span
Jobs
diff --git a/app/views/projects/project_members/_index.html.haml b/app/views/projects/project_members/_index.html.haml
index d080b6c83d4..cfae371e169 100644
--- a/app/views/projects/project_members/_index.html.haml
+++ b/app/views/projects/project_members/_index.html.haml
@@ -1,11 +1,12 @@
.row.prepend-top-default
.col-lg-3.settings-sidebar
%h4.prepend-top-0
- Members
+ Project members
- if can?(current_user, :admin_project_member, @project)
%p
- Add a new member to
+ You can add a new member to
%strong= @project.name
+ or share it with another group.
- else
%p
Members can be added by project
@@ -13,9 +14,20 @@
or
%i Owners
.col-lg-9
- .light.prepend-top-default
+ .light
- if can?(current_user, :admin_project_member, @project)
- = render "projects/project_members/new_project_member"
+ %ul.nav-links.project-member-tabs{ role: 'tablist' }
+ %li.active{ role: 'presentation' }
+ %a{ href: '#add-member-pane', id: 'add-member-tab', data: { toggle: 'tab' }, role: 'tab' } Add member
+ - if @project.allowed_to_share_with_group?
+ %li{ role: 'presentation' }
+ %a{ href: '#share-with-group-pane', id: 'share-with-group-tab', data: { toggle: 'tab' }, role: 'tab' } Share with group
+
+ .tab-content.project-member-tab-content
+ .tab-pane.active{ id: 'add-member-pane', role: 'tabpanel' }
+ = render 'projects/project_members/new_project_member', tab_title: 'Add member'
+ .tab-pane{ id: 'share-with-group-pane', role: 'tabpanel' }
+ = render 'projects/project_members/new_shared_group', tab_title: 'Share with group'
= render 'shared/members/requests', membership_source: @project, requesters: @requesters
.clearfix
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
index 2b1c23f7dda..247c4bdbe2d 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -1,18 +1,19 @@
-= form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'users-project-form' } do |f|
- .form-group
- = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true, placeholder: "Search for members to update or invite")
- .help-block.append-bottom-10
- Search for members by name, username, or email, or invite new ones using their email address.
- .form-group
- = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select"
- .help-block.append-bottom-10
- = link_to "Read more", help_page_path("user/permissions"), class: "vlink"
- about role permissions
- .form-group
- .clearable-input
- = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
- %i.clear-icon.js-clear-input
- .help-block.append-bottom-10
- On this date, the member(s) will automatically lose access to this project.
- = f.submit "Add to project", class: "btn btn-create"
- = link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default", title: "Import members from another project"
+.row
+ .col-sm-12
+ = form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'users-project-form' } do |f|
+ .form-group
+ = label_tag :user_ids, "Select members to invite", class: "label-light"
+ = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true, placeholder: "Search for members to update or invite")
+ .form-group
+ = label_tag :access_level, "Choose a role permission", class: "label-light"
+ = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select"
+ .help-block.append-bottom-10
+ = link_to "Read more", help_page_path("user/permissions"), class: "vlink"
+ about role permissions
+ .form-group
+ .clearable-input
+ = label_tag :expires_at, 'Access expiration date', class: 'label-light'
+ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
+ %i.clear-icon.js-clear-input
+ = f.submit "Add to project", class: "btn btn-create"
+ = link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default", title: "Import members from another project"
diff --git a/app/views/projects/project_members/_new_shared_group.html.haml b/app/views/projects/project_members/_new_shared_group.html.haml
new file mode 100644
index 00000000000..b7cc8dd7062
--- /dev/null
+++ b/app/views/projects/project_members/_new_shared_group.html.haml
@@ -0,0 +1,20 @@
+.row
+ .col-sm-12
+ = form_tag namespace_project_group_links_path(@project.namespace, @project), class: 'js-requires-input', method: :post do
+ .form-group
+ = label_tag :link_group_id, "Select a group to share with", class: "label-light"
+ = groups_select_tag(:link_group_id, data: { skip_groups: @skip_groups }, class: "input-clamp", required: true)
+ .form-group
+ = label_tag :link_group_access, "Max access level", class: "label-light"
+ .select-wrapper
+ = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control"
+ = icon('caret-down')
+ .help-block.append-bottom-10
+ = link_to "Read more", help_page_path("user/permissions"), class: "vlink"
+ about role permissions
+ .form-group
+ = label_tag :expires_at, 'Access expiration date', class: 'label-light'
+ .clearable-input
+ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date-groups', placeholder: 'Expiration date', id: 'expires_at_groups'
+ %i.clear-icon.js-clear-input
+ = submit_tag "Share", class: "btn btn-create"
diff --git a/app/views/projects/settings/members/show.html.haml b/app/views/projects/settings/members/show.html.haml
index 20e1ad68244..343807b87cd 100644
--- a/app/views/projects/settings/members/show.html.haml
+++ b/app/views/projects/settings/members/show.html.haml
@@ -2,6 +2,3 @@
= render "projects/settings/head"
= render "projects/project_members/index"
-- if can?(current_user, :admin_project, @project)
- - if @project.allowed_to_share_with_group?
- = render "projects/group_links/index"
diff --git a/app/views/projects/show.atom.builder b/app/views/projects/show.atom.builder
index 5c7f2e315f0..ed34f5c0520 100644
--- a/app/views/projects/show.atom.builder
+++ b/app/views/projects/show.atom.builder
@@ -1,10 +1,7 @@
-xml.instruct!
-xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
- xml.title "#{@project.name} activity"
- xml.link href: namespace_project_url(@project.namespace, @project, rss_url_options), rel: "self", type: "application/atom+xml"
- xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html"
- xml.id namespace_project_url(@project.namespace, @project)
- xml.updated @events[0].updated_at.xmlschema if @events[0]
+xml.title "#{@project.name} activity"
+xml.link href: namespace_project_url(@project.namespace, @project, rss_url_options), rel: "self", type: "application/atom+xml"
+xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html"
+xml.id namespace_project_url(@project.namespace, @project)
+xml.updated @events[0].updated_at.xmlschema if @events[0]
- xml << render(@events) if @events.any?
-end
+xml << render(@events) if @events.any?
diff --git a/app/views/shared/_branch_switcher.html.haml b/app/views/shared/_branch_switcher.html.haml
deleted file mode 100644
index 69e3f3042a9..00000000000
--- a/app/views/shared/_branch_switcher.html.haml
+++ /dev/null
@@ -1,8 +0,0 @@
-- dropdown_toggle_text = @branch_name || tree_edit_branch
-= hidden_field_tag 'branch_name', dropdown_toggle_text
-
-.dropdown
- = dropdown_toggle dropdown_toggle_text, { toggle: 'dropdown', selected: dropdown_toggle_text, field_name: 'branch_name', form_id: '.js-edit-blob-form', refs_url: namespace_project_branches_path(@project.namespace, @project) }, { toggle_class: 'js-project-branches-dropdown js-target-branch' }
- .dropdown-menu.dropdown-menu-selectable.dropdown-menu-paging.dropdown-menu-branches
- = render partial: 'shared/projects/blob/branch_page_default'
- = render partial: 'shared/projects/blob/branch_page_create'
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index bd994cdad01..c185e9b73ee 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -64,7 +64,7 @@
%a.js-subscribe-button{ data: { url: toggle_subscription_group_label_path(label.group, label) } }
Group level
- - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_group, label.project.group)
+ - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group)
= link_to promote_namespace_project_label_path(label.project.namespace, label.project, label), title: "Promote to Group Label", class: 'btn btn-transparent btn-action', data: {confirm: "Promoting this label will make this label available to all projects inside this group. Existing project labels with the same name will be merged. Are you sure?", toggle: "tooltip"}, method: :post do
%span.sr-only Promote to Group
= icon('level-up')
diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml
index 0b37fe3013b..25a56f84ec5 100644
--- a/app/views/shared/_new_commit_form.html.haml
+++ b/app/views/shared/_new_commit_form.html.haml
@@ -7,7 +7,7 @@
.form-group.branch
= label_tag 'branch_name', 'Target branch', class: 'control-label'
.col-sm-10
- = render 'shared/branch_switcher'
+ = text_field_tag 'branch_name', @branch_name || tree_edit_branch, required: true, class: "form-control js-branch-name ref-name"
.js-create-merge-request-container
.checkbox
diff --git a/app/views/shared/groups/_dropdown.html.haml b/app/views/shared/groups/_dropdown.html.haml
index 37589b634fa..760370a6984 100644
--- a/app/views/shared/groups/_dropdown.html.haml
+++ b/app/views/shared/groups/_dropdown.html.haml
@@ -1,10 +1,10 @@
-.dropdown.inline
+.dropdown.inline.js-group-filter-dropdown-wrap
%button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
- %span.light
- - if @sort.present?
- = sort_options_hash[@sort]
- - else
- = sort_title_recently_created
+ %span.dropdown-label
+ - if @sort.present?
+ = sort_options_hash[@sort]
+ - else
+ = sort_title_recently_created
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
diff --git a/app/views/shared/projects/blob/_branch_page_create.html.haml b/app/views/shared/projects/blob/_branch_page_create.html.haml
deleted file mode 100644
index c279a0d8846..00000000000
--- a/app/views/shared/projects/blob/_branch_page_create.html.haml
+++ /dev/null
@@ -1,8 +0,0 @@
-.dropdown-page-two.dropdown-new-branch
- = dropdown_title('Create new branch', back: true)
- = dropdown_content do
- %input#new_branch_name.default-dropdown-input.append-bottom-10{ type: "text", placeholder: "Name new branch" }
- %button.btn.btn-primary.pull-left.js-new-branch-btn{ type: "button" }
- Create
- %button.btn.btn-default.pull-right.js-cancel-branch-btn{ type: "button" }
- Cancel
diff --git a/app/views/shared/projects/blob/_branch_page_default.html.haml b/app/views/shared/projects/blob/_branch_page_default.html.haml
deleted file mode 100644
index 9bf78d10878..00000000000
--- a/app/views/shared/projects/blob/_branch_page_default.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-.dropdown-page-one
- = dropdown_title "Select branch"
- = dropdown_filter "Search branches"
- = dropdown_content
- = dropdown_loading
- = dropdown_footer do
- %ul.dropdown-footer-list
- %li
- %a.create-new-branch.dropdown-toggle-page{ href: "#" }
- Create new branch
diff --git a/app/views/users/show.atom.builder b/app/views/users/show.atom.builder
index 6c85e5f9fbd..e95814875f1 100644
--- a/app/views/users/show.atom.builder
+++ b/app/views/users/show.atom.builder
@@ -1,10 +1,7 @@
-xml.instruct!
-xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
- xml.title "#{@user.name} activity"
- xml.link href: user_url(@user, :atom), rel: "self", type: "application/atom+xml"
- xml.link href: user_url(@user), rel: "alternate", type: "text/html"
- xml.id user_url(@user)
- xml.updated @events[0].updated_at.xmlschema if @events[0]
+xml.title "#{@user.name} activity"
+xml.link href: user_url(@user, :atom), rel: "self", type: "application/atom+xml"
+xml.link href: user_url(@user), rel: "alternate", type: "text/html"
+xml.id user_url(@user)
+xml.updated @events[0].updated_at.xmlschema if @events[0]
- xml << render(@events) if @events.any?
-end
+xml << render(@events) if @events.any?
diff --git a/app/workers/background_migration_worker.rb b/app/workers/background_migration_worker.rb
new file mode 100644
index 00000000000..e85e221d353
--- /dev/null
+++ b/app/workers/background_migration_worker.rb
@@ -0,0 +1,23 @@
+class BackgroundMigrationWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ # Schedules a number of jobs in bulk
+ #
+ # The `jobs` argument should be an Array of Arrays, each sub-array must be in
+ # the form:
+ #
+ # [migration-class, [arg1, arg2, ...]]
+ def self.perform_bulk(*jobs)
+ Sidekiq::Client.push_bulk('class' => self,
+ 'queue' => sidekiq_options['queue'],
+ 'args' => jobs)
+ end
+
+ # Performs the background migration.
+ #
+ # See Gitlab::BackgroundMigration.perform for more information.
+ def perform(class_name, arguments = [])
+ Gitlab::BackgroundMigration.perform(class_name, arguments)
+ end
+end
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index c29571d3c62..89286595ca6 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -17,14 +17,15 @@ class PostReceive
post_received = Gitlab::GitPostReceive.new(project, identifier, changes)
if is_wiki
- # Nothing defined here yet.
+ process_wiki_changes(post_received)
else
process_project_changes(post_received)
- process_repository_update(post_received)
end
end
- def process_repository_update(post_received)
+ private
+
+ def process_project_changes(post_received)
changes = []
refs = Set.new
@@ -36,32 +37,27 @@ class PostReceive
return false
end
- changes << Gitlab::DataBuilder::Repository.single_change(oldrev, newrev, ref)
- refs << ref
- end
-
- hook_data = Gitlab::DataBuilder::Repository.update(post_received.project, @user, changes, refs.to_a)
- SystemHooksService.new.execute_hooks(hook_data, :repository_update_hooks)
- end
-
- def process_project_changes(post_received)
- post_received.changes_refs do |oldrev, newrev, ref|
- @user ||= post_received.identify(newrev)
-
- unless @user
- log("Triggered hook for non-existing user \"#{post_received.identifier}\"")
- return false
- end
-
if Gitlab::Git.tag_ref?(ref)
GitTagPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute
elsif Gitlab::Git.branch_ref?(ref)
GitPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute
end
+
+ changes << Gitlab::DataBuilder::Repository.single_change(oldrev, newrev, ref)
+ refs << ref
end
+
+ after_project_changes_hooks(post_received, @user, refs.to_a, changes)
end
- private
+ def after_project_changes_hooks(post_received, user, refs, changes)
+ hook_data = Gitlab::DataBuilder::Repository.update(post_received.project, user, changes, refs)
+ SystemHooksService.new.execute_hooks(hook_data, :repository_update_hooks)
+ end
+
+ def process_wiki_changes(post_received)
+ # Nothing defined here yet.
+ end
# To maintain backwards compatibility, we accept both gl_repository or
# repository paths as project identifiers. Our plan is to migrate to
diff --git a/changelogs/unreleased/12614-fix-long-message.yml b/changelogs/unreleased/12614-fix-long-message.yml
new file mode 100644
index 00000000000..94f8127c3c1
--- /dev/null
+++ b/changelogs/unreleased/12614-fix-long-message.yml
@@ -0,0 +1,4 @@
+---
+title: Fix long urls in the title of commit
+merge_request: 10938
+author: Alexander Randa
diff --git a/changelogs/unreleased/25426-group-dashboard-ui.yml b/changelogs/unreleased/25426-group-dashboard-ui.yml
new file mode 100644
index 00000000000..cc2bf62d07b
--- /dev/null
+++ b/changelogs/unreleased/25426-group-dashboard-ui.yml
@@ -0,0 +1,4 @@
+---
+title: Update Dashboard Groups UI with better support for subgroups
+merge_request:
+author:
diff --git a/changelogs/unreleased/28607-forking-and-configuring-project-via-api-works-very-unreliable.yml b/changelogs/unreleased/28607-forking-and-configuring-project-via-api-works-very-unreliable.yml
new file mode 100644
index 00000000000..9cf8d745f92
--- /dev/null
+++ b/changelogs/unreleased/28607-forking-and-configuring-project-via-api-works-very-unreliable.yml
@@ -0,0 +1,4 @@
+---
+title: Confirm Project forking behaviour via the API
+merge_request:
+author:
diff --git a/changelogs/unreleased/29010-perf-bar.yml b/changelogs/unreleased/29010-perf-bar.yml
new file mode 100644
index 00000000000..f4167e5562f
--- /dev/null
+++ b/changelogs/unreleased/29010-perf-bar.yml
@@ -0,0 +1,4 @@
+---
+title: Add an optional performance bar to view performance metrics for the current page
+merge_request: 11439
+author:
diff --git a/changelogs/unreleased/31397-job-detail-real-time.yml b/changelogs/unreleased/31397-job-detail-real-time.yml
new file mode 100644
index 00000000000..90487a1e75a
--- /dev/null
+++ b/changelogs/unreleased/31397-job-detail-real-time.yml
@@ -0,0 +1,4 @@
+---
+title: Adds realtime feature to job show view header and sidebar info. Updates UX.
+merge_request:
+author:
diff --git a/changelogs/unreleased/32517-disable-hover-state.yml b/changelogs/unreleased/32517-disable-hover-state.yml
new file mode 100644
index 00000000000..31b02778963
--- /dev/null
+++ b/changelogs/unreleased/32517-disable-hover-state.yml
@@ -0,0 +1,5 @@
+---
+title: Removes hover style for nodes that are either links or buttons in the pipeline
+ graph
+merge_request:
+author:
diff --git a/changelogs/unreleased/32720-emoji-spacing.yml b/changelogs/unreleased/32720-emoji-spacing.yml
new file mode 100644
index 00000000000..da3df0f9093
--- /dev/null
+++ b/changelogs/unreleased/32720-emoji-spacing.yml
@@ -0,0 +1,4 @@
+---
+title: Create equal padding for emoji
+merge_request:
+author:
diff --git a/changelogs/unreleased/32834-task-note-only.yml b/changelogs/unreleased/32834-task-note-only.yml
new file mode 100644
index 00000000000..c9ea61ec4ec
--- /dev/null
+++ b/changelogs/unreleased/32834-task-note-only.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent description change notes when toggling tasks
+merge_request: 12057
+author: Jared Deckard <jared.deckard@gmail.com>
diff --git a/changelogs/unreleased/33003-avatar-in-project-api.yml b/changelogs/unreleased/33003-avatar-in-project-api.yml
new file mode 100644
index 00000000000..41d796ebb32
--- /dev/null
+++ b/changelogs/unreleased/33003-avatar-in-project-api.yml
@@ -0,0 +1,4 @@
+---
+title: Accept image for avatar in project API
+merge_request: 11988
+author: Ivan Chernov
diff --git a/changelogs/unreleased/33132-change-icon-color.yml b/changelogs/unreleased/33132-change-icon-color.yml
new file mode 100644
index 00000000000..c0e148f985b
--- /dev/null
+++ b/changelogs/unreleased/33132-change-icon-color.yml
@@ -0,0 +1,4 @@
+---
+title: Render CI statuses with warnings in orange
+merge_request:
+author:
diff --git a/changelogs/unreleased/33208-singup-active-state-underline.yml b/changelogs/unreleased/33208-singup-active-state-underline.yml
new file mode 100644
index 00000000000..cddb43214ea
--- /dev/null
+++ b/changelogs/unreleased/33208-singup-active-state-underline.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes "sign in / Register" active state underline misalignment
+merge_request: 11890
+author: Frank Sierra
diff --git a/changelogs/unreleased/33334-portuguese_brazil_translation_of_cycle_analytics_page.yml b/changelogs/unreleased/33334-portuguese_brazil_translation_of_cycle_analytics_page.yml
new file mode 100644
index 00000000000..a0e0458da16
--- /dev/null
+++ b/changelogs/unreleased/33334-portuguese_brazil_translation_of_cycle_analytics_page.yml
@@ -0,0 +1,4 @@
+---
+title: Add Portuguese Brazil of Cycle Analytics Page to I18N
+merge_request: 11920
+author:Huang Tao
diff --git a/changelogs/unreleased/33381-display-issue-state-in-mr-widget-issue-links.yml b/changelogs/unreleased/33381-display-issue-state-in-mr-widget-issue-links.yml
new file mode 100644
index 00000000000..4a7b02fec94
--- /dev/null
+++ b/changelogs/unreleased/33381-display-issue-state-in-mr-widget-issue-links.yml
@@ -0,0 +1,4 @@
+---
+title: Display issue state in issue links section of merge request widget
+merge_request: 12021
+author:
diff --git a/changelogs/unreleased/33383-bulgarian_translation_of_cycle_analytics_page.yml b/changelogs/unreleased/33383-bulgarian_translation_of_cycle_analytics_page.yml
new file mode 100644
index 00000000000..71bd5505be7
--- /dev/null
+++ b/changelogs/unreleased/33383-bulgarian_translation_of_cycle_analytics_page.yml
@@ -0,0 +1,4 @@
+---
+title: add bulgarian translation of cycle analytics page to I18N
+merge_request: 11958
+author: Lyubomir Vasilev
diff --git a/changelogs/unreleased/allow-reporters-to-promote-group-labels.yml b/changelogs/unreleased/allow-reporters-to-promote-group-labels.yml
new file mode 100644
index 00000000000..2364ce6d068
--- /dev/null
+++ b/changelogs/unreleased/allow-reporters-to-promote-group-labels.yml
@@ -0,0 +1,4 @@
+---
+title: Allow reporters to promote project labels to group labels
+merge_request:
+author:
diff --git a/changelogs/unreleased/artifacts-keyboard-shortcuts.yml b/changelogs/unreleased/artifacts-keyboard-shortcuts.yml
new file mode 100644
index 00000000000..69569504c4f
--- /dev/null
+++ b/changelogs/unreleased/artifacts-keyboard-shortcuts.yml
@@ -0,0 +1,4 @@
+---
+title: Enabled keyboard shortcuts on artifacts pages
+merge_request:
+author:
diff --git a/changelogs/unreleased/ce-31853-projects-shared-groups.yml b/changelogs/unreleased/ce-31853-projects-shared-groups.yml
new file mode 100644
index 00000000000..ffa3aed682d
--- /dev/null
+++ b/changelogs/unreleased/ce-31853-projects-shared-groups.yml
@@ -0,0 +1,4 @@
+---
+title: Remove duplication for sharing projects with groups in project settings
+merge_request:
+author:
diff --git a/changelogs/unreleased/dashboard-milestone-tabs-loading-async.yml b/changelogs/unreleased/dashboard-milestone-tabs-loading-async.yml
new file mode 100644
index 00000000000..357a623e0e8
--- /dev/null
+++ b/changelogs/unreleased/dashboard-milestone-tabs-loading-async.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed dashboard milestone tabs not loading
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-blob-binaryness-change.yml b/changelogs/unreleased/dm-blob-binaryness-change.yml
new file mode 100644
index 00000000000..f3e3af26f12
--- /dev/null
+++ b/changelogs/unreleased/dm-blob-binaryness-change.yml
@@ -0,0 +1,5 @@
+---
+title: Detect if file that appears to be text in the first 1024 bytes is actually
+ binary afer loading all data
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-mail-room-check-without-omnibus.yml b/changelogs/unreleased/dm-mail-room-check-without-omnibus.yml
new file mode 100644
index 00000000000..7fd252e9b8b
--- /dev/null
+++ b/changelogs/unreleased/dm-mail-room-check-without-omnibus.yml
@@ -0,0 +1,4 @@
+---
+title: Don't check if MailRoom is running on Omnibus
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-revert-mr-8427.yml b/changelogs/unreleased/dm-revert-mr-8427.yml
new file mode 100644
index 00000000000..a91cff2e9cd
--- /dev/null
+++ b/changelogs/unreleased/dm-revert-mr-8427.yml
@@ -0,0 +1,4 @@
+---
+title: Revert 'New file from interface on existing branch'
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-u2f-for-opera.yml b/changelogs/unreleased/fix-u2f-for-opera.yml
new file mode 100644
index 00000000000..0eafb8eff9a
--- /dev/null
+++ b/changelogs/unreleased/fix-u2f-for-opera.yml
@@ -0,0 +1,4 @@
+---
+title: Fix FIDO U2F for Opera browser
+merge_request: 12082
+author: Jakub Kramarz and Jonas Kalderstam
diff --git a/changelogs/unreleased/fix_docs_commits_multiple_files.yml b/changelogs/unreleased/fix_docs_commits_multiple_files.yml
new file mode 100644
index 00000000000..36567354b28
--- /dev/null
+++ b/changelogs/unreleased/fix_docs_commits_multiple_files.yml
@@ -0,0 +1,5 @@
+---
+title: Documentation bugfix of invalid JSON payload example of Create a commit with
+ multiple files and actions
+merge_request: 12117
+author: @blackst0ne
diff --git a/changelogs/unreleased/issuable-sidebar-edit-button-field-focus.yml b/changelogs/unreleased/issuable-sidebar-edit-button-field-focus.yml
new file mode 100644
index 00000000000..05d52fcad0f
--- /dev/null
+++ b/changelogs/unreleased/issuable-sidebar-edit-button-field-focus.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed dropdown filter input not focusing after transition
+merge_request:
+author:
diff --git a/changelogs/unreleased/pat-msg-on-auth-failure.yml b/changelogs/unreleased/pat-msg-on-auth-failure.yml
new file mode 100644
index 00000000000..c1b1528bb7a
--- /dev/null
+++ b/changelogs/unreleased/pat-msg-on-auth-failure.yml
@@ -0,0 +1,4 @@
+---
+title: Instruct user to use personal access token for Git over HTTP
+merge_request: 11986
+author: Robin Bobbitt
diff --git a/changelogs/unreleased/sh-bump-oauth2-gem.yml b/changelogs/unreleased/sh-bump-oauth2-gem.yml
new file mode 100644
index 00000000000..b894a64968b
--- /dev/null
+++ b/changelogs/unreleased/sh-bump-oauth2-gem.yml
@@ -0,0 +1,4 @@
+---
+title: Bump Faraday and dependent OAuth2 gem version to support no_proxy variable
+merge_request:
+author:
diff --git a/changelogs/unreleased/sh-fix-refactor-uploader-work-dir.yml b/changelogs/unreleased/sh-fix-refactor-uploader-work-dir.yml
new file mode 100644
index 00000000000..255608bd89a
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-refactor-uploader-work-dir.yml
@@ -0,0 +1,4 @@
+---
+title: Set artifact working directory to be in the destination store to prevent unnecessary I/O
+merge_request:
+author:
diff --git a/changelogs/unreleased/sh-recaptcha-fix-try2.yml b/changelogs/unreleased/sh-recaptcha-fix-try2.yml
new file mode 100644
index 00000000000..94729252c6f
--- /dev/null
+++ b/changelogs/unreleased/sh-recaptcha-fix-try2.yml
@@ -0,0 +1,4 @@
+---
+title: Make sure reCAPTCHA configuration is loaded when spam checks are initiated
+merge_request:
+author:
diff --git a/changelogs/unreleased/tc-link-to-commit-on-help-page.yml b/changelogs/unreleased/tc-link-to-commit-on-help-page.yml
new file mode 100644
index 00000000000..3d11ba43d1f
--- /dev/null
+++ b/changelogs/unreleased/tc-link-to-commit-on-help-page.yml
@@ -0,0 +1,4 @@
+---
+title: Make the revision on the `/help` page clickable
+merge_request: 12016
+author:
diff --git a/changelogs/unreleased/zj-i18n-pipeline-schedules.yml b/changelogs/unreleased/zj-i18n-pipeline-schedules.yml
new file mode 100644
index 00000000000..51c82a16359
--- /dev/null
+++ b/changelogs/unreleased/zj-i18n-pipeline-schedules.yml
@@ -0,0 +1,4 @@
+---
+title: Allow translation of Pipeline Schedules
+merge_request:
+author:
diff --git a/changelogs/unreleased/zj-prom-pipeline-count.yml b/changelogs/unreleased/zj-prom-pipeline-count.yml
new file mode 100644
index 00000000000..191e4f2f949
--- /dev/null
+++ b/changelogs/unreleased/zj-prom-pipeline-count.yml
@@ -0,0 +1,4 @@
+---
+title: Add prometheus metrics on pipeline creation
+merge_request:
+author:
diff --git a/config/application.rb b/config/application.rb
index b0533759252..8bbecf3ed0f 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -105,6 +105,7 @@ module Gitlab
config.assets.precompile << "katex.css"
config.assets.precompile << "katex.js"
config.assets.precompile << "xterm/xterm.css"
+ config.assets.precompile << "peek.css"
config.assets.precompile << "lib/ace.js"
config.assets.precompile << "vendor/assets/fonts/*"
config.assets.precompile << "test.css"
diff --git a/config/boot.rb b/config/boot.rb
index f2830ae3166..17a71148370 100644
--- a/config/boot.rb
+++ b/config/boot.rb
@@ -4,3 +4,15 @@ require 'rubygems'
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
+
+# Default Bootsnap configuration from https://github.com/Shopify/bootsnap#usage
+require 'bootsnap'
+Bootsnap.setup(
+ cache_dir: 'tmp/cache',
+ development_mode: ENV['RAILS_ENV'] == 'development',
+ load_path_cache: true,
+ autoload_paths_cache: true,
+ disable_trace: false,
+ compile_cache_iseq: true,
+ compile_cache_yaml: true
+)
diff --git a/config/initializers/peek.rb b/config/initializers/peek.rb
new file mode 100644
index 00000000000..65432caac2a
--- /dev/null
+++ b/config/initializers/peek.rb
@@ -0,0 +1,32 @@
+Rails.application.config.peek.adapter = :redis, { client: ::Redis.new(Gitlab::Redis.params) }
+
+Peek.into Peek::Views::Host
+Peek.into Peek::Views::PerformanceBar
+if Gitlab::Database.mysql?
+ require 'peek-mysql2'
+ PEEK_DB_CLIENT = ::Mysql2::Client
+ PEEK_DB_VIEW = Peek::Views::Mysql2
+else
+ require 'peek-pg'
+ PEEK_DB_CLIENT = ::PG::Connection
+ PEEK_DB_VIEW = Peek::Views::PG
+end
+Peek.into PEEK_DB_VIEW
+Peek.into Peek::Views::Redis
+Peek.into Peek::Views::Sidekiq
+Peek.into Peek::Views::Rblineprof
+Peek.into Peek::Views::GC
+
+# rubocop:disable Style/ClassAndModuleCamelCase
+class PEEK_DB_CLIENT
+ class << self
+ attr_accessor :query_details
+ end
+ self.query_details = Concurrent::Array.new
+end
+
+PEEK_DB_VIEW.prepend ::Gitlab::PerformanceBar::PeekQueryTracker
+
+class Peek::Views::PerformanceBar::ProcessUtilization
+ prepend ::Gitlab::PerformanceBar::PeekPerformanceBarWithRackBody
+end
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 533663a2704..38c3711c6c7 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -62,6 +62,43 @@ de:
- :month
- :year
datetime:
+ # used in a custom scope that has been created to fix https://gitlab.com/gitlab-org/gitlab-ce/issues/32747
+ time_ago_in_words:
+ half_a_minute: vor einer halben Minute
+ less_than_x_seconds:
+ one: vor weniger als einer Sekunde
+ other: "vor weniger als %{count} Sekunden"
+ x_seconds:
+ one: vor einer Sekunde
+ other: "vor %{count} Sekunden"
+ less_than_x_minutes:
+ one: vor weniger als einer Minute
+ other: vor weniger als %{count} Minuten
+ x_minutes:
+ one: vor einer Minute
+ other: "vor %{count} Minuten"
+ about_x_hours:
+ one: vor etwa einer Stunde
+ other: "vor etwa %{count} Stunden"
+ x_days:
+ one: vor einem Tag
+ other: "vor %{count} Tagen"
+ about_x_months:
+ one: vor etwa einem Monat
+ other: "vor etwa %{count} Monaten"
+ x_months:
+ one: vor einem Monat
+ other: "vor %{count} Monaten"
+ about_x_years:
+ one: vor etwa einem Jahr
+ other: "vor etwa %{count} Jahren"
+ over_x_years:
+ one: vor mehr als einem Jahr
+ other: "vor mehr als %{count} Jahren"
+ almost_x_years:
+ one: vor fast einem Jahr
+ other: "vor fast %{count} Jahren"
+ # Used in distance_of_time_in_words(), distance_of_time_in_words_to_now(), time_ago_in_words()
distance_in_words:
about_x_hours:
one: etwa eine Stunde
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 0f9dc39535d..d71c6eb5047 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -61,6 +61,7 @@ es:
- :month
- :year
datetime:
+ # used in a custom scope that has been created to fix https://gitlab.com/gitlab-org/gitlab-ce/issues/32747
time_ago_in_words:
half_a_minute: "hace medio minuto"
less_than_x_seconds:
@@ -96,6 +97,7 @@ es:
almost_x_years:
one: "hace casi 1 año"
other: "hace casi %{count} años"
+ # Used in distance_of_time_in_words(), distance_of_time_in_words_to_now(), time_ago_in_words()
distance_in_words:
about_x_hours:
one: alrededor de 1 hora
diff --git a/config/routes.rb b/config/routes.rb
index d909be38b42..4fd6cb5d439 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -42,6 +42,7 @@ Rails.application.routes.draw do
get 'liveness' => 'health#liveness'
get 'readiness' => 'health#readiness'
resources :metrics, only: [:index]
+ mount Peek::Railtie => '/peek'
end
# Koding route
diff --git a/config/routes/dashboard.rb b/config/routes/dashboard.rb
index 8e380a0b0ac..d2437285cdf 100644
--- a/config/routes/dashboard.rb
+++ b/config/routes/dashboard.rb
@@ -4,7 +4,13 @@ resource :dashboard, controller: 'dashboard', only: [] do
get :activity
scope module: :dashboard do
- resources :milestones, only: [:index, :show]
+ resources :milestones, only: [:index, :show] do
+ member do
+ get :merge_requests
+ get :participants
+ get :labels
+ end
+ end
resources :labels, only: [:index]
resources :groups, only: [:index]
diff --git a/config/routes/uploads.rb b/config/routes/uploads.rb
index ae8747d766d..a49e244af1a 100644
--- a/config/routes/uploads.rb
+++ b/config/routes/uploads.rb
@@ -1,6 +1,6 @@
scope path: :uploads do
# Note attachments and User/Group/Project avatars
- get ":model/:mounted_as/:id/:filename",
+ get "system/:model/:mounted_as/:id/:filename",
to: "uploads#show",
constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /[^\/]+/ }
@@ -15,7 +15,7 @@ scope path: :uploads do
constraints: { filename: /[^\/]+/ }
# Appearance
- get ":model/:mounted_as/:id/:filename",
+ get "system/:model/:mounted_as/:id/:filename",
to: "uploads#show",
constraints: { model: /appearance/, mounted_as: /logo|header_logo/, filename: /.+/ }
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 93df2d6f5ff..1d9e69a2408 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -54,3 +54,4 @@
- [system_hook_push, 1]
- [update_user_activity, 1]
- [propagate_service_template, 1]
+ - [background_migration, 1]
diff --git a/config/webpack.config.js b/config/webpack.config.js
index cbcf5ce996d..120f9d3193d 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -18,6 +18,15 @@ var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false';
var WEBPACK_REPORT = process.env.WEBPACK_REPORT;
var NO_COMPRESSION = process.env.NO_COMPRESSION;
+// optional dependency `node-zopfli` is unavailable on CentOS 6
+var ZOPFLI_AVAILABLE;
+try {
+ require.resolve('node-zopfli');
+ ZOPFLI_AVAILABLE = true;
+} catch(err) {
+ ZOPFLI_AVAILABLE = false;
+}
+
var config = {
// because sqljs requires fs.
node: {
@@ -40,9 +49,11 @@ var config = {
filtered_search: './filtered_search/filtered_search_bundle.js',
graphs: './graphs/graphs_bundle.js',
group: './group.js',
+ groups: './groups/index.js',
groups_list: './groups_list.js',
issue_show: './issue_show/index.js',
integrations: './integrations',
+ job_details: './jobs/job_details_bundle.js',
locale: './locale/index.js',
main: './main.js',
merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js',
@@ -67,6 +78,7 @@ var config = {
raven: './raven/index.js',
vue_merge_request_widget: './vue_merge_request_widget/index.js',
test: './test.js',
+ peek: './peek.js',
},
output: {
@@ -155,7 +167,9 @@ var config = {
'environments',
'environments_folder',
'filtered_search',
+ 'groups',
'issue_show',
+ 'job_details',
'merge_conflicts',
'notebook_viewer',
'pdf_viewer',
@@ -222,7 +236,7 @@ if (IS_PRODUCTION) {
config.plugins.push(
new CompressionPlugin({
asset: '[path].gz[query]',
- algorithm: 'zopfli',
+ algorithm: ZOPFLI_AVAILABLE ? 'zopfli' : 'gzip',
})
);
}
diff --git a/db/migrate/20170316163800_rename_system_namespaces.rb b/db/migrate/20170316163800_rename_system_namespaces.rb
new file mode 100644
index 00000000000..b5408fbf112
--- /dev/null
+++ b/db/migrate/20170316163800_rename_system_namespaces.rb
@@ -0,0 +1,231 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+class RenameSystemNamespaces < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ include Gitlab::ShellAdapter
+ disable_ddl_transaction!
+
+ class User < ActiveRecord::Base
+ self.table_name = 'users'
+ end
+
+ class Namespace < ActiveRecord::Base
+ self.table_name = 'namespaces'
+ belongs_to :parent, class_name: 'RenameSystemNamespaces::Namespace'
+ has_one :route, as: :source
+ has_many :children, class_name: 'RenameSystemNamespaces::Namespace', foreign_key: :parent_id
+ belongs_to :owner, class_name: 'RenameSystemNamespaces::User'
+
+ # Overridden to have the correct `source_type` for the `route` relation
+ def self.name
+ 'Namespace'
+ end
+
+ def full_path
+ if route && route.path.present?
+ @full_path ||= route.path
+ else
+ update_route if persisted?
+
+ build_full_path
+ end
+ end
+
+ def build_full_path
+ if parent && path
+ parent.full_path + '/' + path
+ else
+ path
+ end
+ end
+
+ def update_route
+ prepare_route
+ route.save
+ end
+
+ def prepare_route
+ route || build_route(source: self)
+ route.path = build_full_path
+ route.name = build_full_name
+ @full_path = nil
+ @full_name = nil
+ end
+
+ def build_full_name
+ if parent && name
+ parent.human_name + ' / ' + name
+ else
+ name
+ end
+ end
+
+ def human_name
+ owner&.name
+ end
+ end
+
+ class Route < ActiveRecord::Base
+ self.table_name = 'routes'
+ belongs_to :source, polymorphic: true
+ end
+
+ class Project < ActiveRecord::Base
+ self.table_name = 'projects'
+
+ def repository_storage_path
+ Gitlab.config.repositories.storages[repository_storage]['path']
+ end
+ end
+
+ DOWNTIME = false
+
+ def up
+ return unless system_namespace
+
+ old_path = system_namespace.path
+ old_full_path = system_namespace.full_path
+ # Only remove the last occurrence of the path name to get the parent namespace path
+ namespace_path = remove_last_occurrence(old_full_path, old_path)
+ new_path = rename_path(namespace_path, old_path)
+ new_full_path = join_namespace_path(namespace_path, new_path)
+
+ Namespace.where(id: system_namespace).update_all(path: new_path) # skips callbacks & validations
+
+ replace_statement = replace_sql(Route.arel_table[:path], old_full_path, new_full_path)
+ route_matches = [old_full_path, "#{old_full_path}/%"]
+
+ update_column_in_batches(:routes, :path, replace_statement) do |table, query|
+ query.where(Route.arel_table[:path].matches_any(route_matches))
+ end
+
+ clear_cache_for_namespace(system_namespace)
+
+ # tasks here are based on `Namespace#move_dir`
+ move_repositories(system_namespace, old_full_path, new_full_path)
+ move_namespace_folders(uploads_dir, old_full_path, new_full_path) if file_storage?
+ move_namespace_folders(pages_dir, old_full_path, new_full_path)
+ end
+
+ def down
+ # nothing to do
+ end
+
+ def remove_last_occurrence(string, pattern)
+ string.reverse.sub(pattern.reverse, "").reverse
+ end
+
+ def move_namespace_folders(directory, old_relative_path, new_relative_path)
+ old_path = File.join(directory, old_relative_path)
+ return unless File.directory?(old_path)
+
+ new_path = File.join(directory, new_relative_path)
+ FileUtils.mv(old_path, new_path)
+ end
+
+ def move_repositories(namespace, old_full_path, new_full_path)
+ repo_paths_for_namespace(namespace).each do |repository_storage_path|
+ # Ensure old directory exists before moving it
+ gitlab_shell.add_namespace(repository_storage_path, old_full_path)
+
+ unless gitlab_shell.mv_namespace(repository_storage_path, old_full_path, new_full_path)
+ say "Exception moving path #{repository_storage_path} from #{old_full_path} to #{new_full_path}"
+ end
+ end
+ end
+
+ def rename_path(namespace_path, path_was)
+ counter = 0
+ path = "#{path_was}#{counter}"
+
+ while route_exists?(join_namespace_path(namespace_path, path))
+ counter += 1
+ path = "#{path_was}#{counter}"
+ end
+
+ path
+ end
+
+ def route_exists?(full_path)
+ Route.where(Route.arel_table[:path].matches(full_path)).any?
+ end
+
+ def join_namespace_path(namespace_path, path)
+ if namespace_path.present?
+ File.join(namespace_path, path)
+ else
+ path
+ end
+ end
+
+ def system_namespace
+ @system_namespace ||= Namespace.where(parent_id: nil).
+ where(arel_table[:path].matches(system_namespace_path)).
+ first
+ end
+
+ def system_namespace_path
+ "system"
+ end
+
+ def clear_cache_for_namespace(namespace)
+ project_ids = projects_for_namespace(namespace).pluck(:id)
+
+ update_column_in_batches(:projects, :description_html, nil) do |table, query|
+ query.where(table[:id].in(project_ids))
+ end
+
+ update_column_in_batches(:issues, :description_html, nil) do |table, query|
+ query.where(table[:project_id].in(project_ids))
+ end
+
+ update_column_in_batches(:merge_requests, :description_html, nil) do |table, query|
+ query.where(table[:target_project_id].in(project_ids))
+ end
+
+ update_column_in_batches(:notes, :note_html, nil) do |table, query|
+ query.where(table[:project_id].in(project_ids))
+ end
+
+ update_column_in_batches(:milestones, :description_html, nil) do |table, query|
+ query.where(table[:project_id].in(project_ids))
+ end
+ end
+
+ def projects_for_namespace(namespace)
+ namespace_ids = child_ids_for_parent(namespace, ids: [namespace.id])
+ namespace_or_children = Project.arel_table[:namespace_id].in(namespace_ids)
+ Project.unscoped.where(namespace_or_children)
+ end
+
+ # This won't scale to huge trees, but it should do for a handful of namespaces
+ # called `system`.
+ def child_ids_for_parent(namespace, ids: [])
+ namespace.children.each do |child|
+ ids << child.id
+ child_ids_for_parent(child, ids: ids) if child.children.any?
+ end
+ ids
+ end
+
+ def repo_paths_for_namespace(namespace)
+ projects_for_namespace(namespace).distinct.
+ select(:repository_storage).map(&:repository_storage_path)
+ end
+
+ def uploads_dir
+ File.join(Rails.root, "public", "uploads")
+ end
+
+ def pages_dir
+ Settings.pages.path
+ end
+
+ def file_storage?
+ CarrierWave::Uploader::Base.storage == CarrierWave::Storage::File
+ end
+
+ def arel_table
+ Namespace.arel_table
+ end
+end
diff --git a/db/migrate/20170316163845_move_uploads_to_system_dir.rb b/db/migrate/20170316163845_move_uploads_to_system_dir.rb
new file mode 100644
index 00000000000..564ee10b5ab
--- /dev/null
+++ b/db/migrate/20170316163845_move_uploads_to_system_dir.rb
@@ -0,0 +1,59 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MoveUploadsToSystemDir < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+ DIRECTORIES_TO_MOVE = %w(user project note group appearance).freeze
+
+ def up
+ return unless file_storage?
+
+ FileUtils.mkdir_p(new_upload_dir)
+
+ DIRECTORIES_TO_MOVE.each do |dir|
+ source = File.join(old_upload_dir, dir)
+ destination = File.join(new_upload_dir, dir)
+ next unless File.directory?(source)
+ next if File.directory?(destination)
+
+ say "Moving #{source} -> #{destination}"
+ FileUtils.mv(source, destination)
+ FileUtils.ln_s(destination, source)
+ end
+ end
+
+ def down
+ return unless file_storage?
+ return unless File.directory?(new_upload_dir)
+
+ DIRECTORIES_TO_MOVE.each do |dir|
+ source = File.join(new_upload_dir, dir)
+ destination = File.join(old_upload_dir, dir)
+ next unless File.directory?(source)
+ next if File.directory?(destination) && !File.symlink?(destination)
+
+ say "Moving #{source} -> #{destination}"
+ FileUtils.rm(destination) if File.symlink?(destination)
+ FileUtils.mv(source, destination)
+ end
+ end
+
+ def file_storage?
+ CarrierWave::Uploader::Base.storage == CarrierWave::Storage::File
+ end
+
+ def base_directory
+ Rails.root
+ end
+
+ def old_upload_dir
+ File.join(base_directory, "public", "uploads")
+ end
+
+ def new_upload_dir
+ File.join(base_directory, "public", "uploads", "system")
+ end
+end
diff --git a/db/post_migrate/20170317162059_update_upload_paths_to_system.rb b/db/post_migrate/20170317162059_update_upload_paths_to_system.rb
new file mode 100644
index 00000000000..9a77b0bbdfb
--- /dev/null
+++ b/db/post_migrate/20170317162059_update_upload_paths_to_system.rb
@@ -0,0 +1,55 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class UpdateUploadPathsToSystem < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ AFFECTED_MODELS = %w(User Project Note Namespace Appearance)
+
+ def up
+ update_column_in_batches(:uploads, :path, replace_sql(arel_table[:path], base_directory, new_upload_dir)) do |_table, query|
+ query.where(uploads_to_switch_to_new_path)
+ end
+ end
+
+ def down
+ update_column_in_batches(:uploads, :path, replace_sql(arel_table[:path], new_upload_dir, base_directory)) do |_table, query|
+ query.where(uploads_to_switch_to_old_path)
+ end
+ end
+
+ # "SELECT \"uploads\".* FROM \"uploads\" WHERE \"uploads\".\"model_type\" IN ('User', 'Project', 'Note', 'Namespace', 'Appearance') AND (\"uploads\".\"path\" ILIKE 'uploads/%' AND NOT (\"uploads\".\"path\" ILIKE 'uploads/system/%'))"
+ def uploads_to_switch_to_new_path
+ affected_uploads.and(starting_with_base_directory).and(starting_with_new_upload_directory.not)
+ end
+
+ # "SELECT \"uploads\".* FROM \"uploads\" WHERE \"uploads\".\"model_type\" IN ('User', 'Project', 'Note', 'Namespace', 'Appearance') AND (\"uploads\".\"path\" ILIKE 'uploads/%' AND \"uploads\".\"path\" ILIKE 'uploads/system/%')"
+ def uploads_to_switch_to_old_path
+ affected_uploads.and(starting_with_new_upload_directory)
+ end
+
+ def starting_with_base_directory
+ arel_table[:path].matches("#{base_directory}/%")
+ end
+
+ def starting_with_new_upload_directory
+ arel_table[:path].matches("#{new_upload_dir}/%")
+ end
+
+ def affected_uploads
+ arel_table[:model_type].in(AFFECTED_MODELS)
+ end
+
+ def base_directory
+ "uploads"
+ end
+
+ def new_upload_dir
+ File.join(base_directory, "system")
+ end
+
+ def arel_table
+ Arel::Table.new(:uploads)
+ end
+end
diff --git a/db/post_migrate/20170406111121_clean_upload_symlinks.rb b/db/post_migrate/20170406111121_clean_upload_symlinks.rb
new file mode 100644
index 00000000000..3ac9a6c10bc
--- /dev/null
+++ b/db/post_migrate/20170406111121_clean_upload_symlinks.rb
@@ -0,0 +1,52 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CleanUploadSymlinks < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+ DIRECTORIES_TO_MOVE = %w(user project note group appeareance)
+
+ def up
+ return unless file_storage?
+
+ DIRECTORIES_TO_MOVE.each do |dir|
+ symlink_location = File.join(old_upload_dir, dir)
+ next unless File.symlink?(symlink_location)
+ say "removing symlink: #{symlink_location}"
+ FileUtils.rm(symlink_location)
+ end
+ end
+
+ def down
+ return unless file_storage?
+
+ DIRECTORIES_TO_MOVE.each do |dir|
+ symlink = File.join(old_upload_dir, dir)
+ destination = File.join(new_upload_dir, dir)
+
+ next if File.directory?(symlink)
+ next unless File.directory?(destination)
+
+ say "Creating symlink #{symlink} -> #{destination}"
+ FileUtils.ln_s(destination, symlink)
+ end
+ end
+
+ def file_storage?
+ CarrierWave::Uploader::Base.storage == CarrierWave::Storage::File
+ end
+
+ def base_directory
+ Rails.root
+ end
+
+ def old_upload_dir
+ File.join(base_directory, "public", "uploads")
+ end
+
+ def new_upload_dir
+ File.join(base_directory, "public", "uploads", "system")
+ end
+end
diff --git a/db/post_migrate/20170606202615_move_appearance_to_system_dir.rb b/db/post_migrate/20170606202615_move_appearance_to_system_dir.rb
new file mode 100644
index 00000000000..561de59ec69
--- /dev/null
+++ b/db/post_migrate/20170606202615_move_appearance_to_system_dir.rb
@@ -0,0 +1,57 @@
+class MoveAppearanceToSystemDir < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+ DIRECTORY_TO_MOVE = 'appearance'.freeze
+
+ def up
+ source = File.join(old_upload_dir, DIRECTORY_TO_MOVE)
+ destination = File.join(new_upload_dir, DIRECTORY_TO_MOVE)
+
+ move_directory(source, destination)
+ end
+
+ def down
+ source = File.join(new_upload_dir, DIRECTORY_TO_MOVE)
+ destination = File.join(old_upload_dir, DIRECTORY_TO_MOVE)
+
+ move_directory(source, destination)
+ end
+
+ def move_directory(source, destination)
+ unless file_storage?
+ say 'Not using file storage, skipping'
+ return
+ end
+
+ unless File.directory?(source)
+ say "#{source} did not exist, skipping"
+ return
+ end
+
+ if File.directory?(destination)
+ say "#{destination} already existed, skipping"
+ return
+ end
+
+ say "Moving #{source} -> #{destination}"
+ FileUtils.mv(source, destination)
+ end
+
+ def file_storage?
+ CarrierWave::Uploader::Base.storage == CarrierWave::Storage::File
+ end
+
+ def base_directory
+ Rails.root
+ end
+
+ def old_upload_dir
+ File.join(base_directory, "public", "uploads")
+ end
+
+ def new_upload_dir
+ File.join(base_directory, "public", "uploads", "system")
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 83172a92b49..b93630a410d 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,8 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20170603200744) do
-
+ActiveRecord::Schema.define(version: 20170606202615) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
enable_extension "pg_trgm"
diff --git a/doc/api/README.md b/doc/api/README.md
index e1d4009dedc..1241801a81c 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -55,6 +55,19 @@ following locations:
- [V3 to V4](v3_to_v4.md)
- [Version](version.md)
+## Road to GraphQL
+
+Going forward, we will start on moving to
+[GraphQL](http://graphql.org/learn/best-practices/) and deprecate the use of
+controller-specific endpoints. GraphQL has a number of benefits:
+
+1. We avoid having to maintain two different APIs.
+2. Callers of the API can request only what they need.
+3. It is versioned by default.
+
+It will co-exist with the current V4 REST API. If we have a V5 API, this should be
+compatability layer on top of GraphQL.
+
### Internal CI API
The following documentation is for the [internal CI API](ci/README.md):
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 9cb58dd3ae9..c91f9ecbdaf 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -97,7 +97,7 @@ PAYLOAD=$(cat << 'JSON'
},
{
"action": "delete",
- "file_path": "foo/bar2",
+ "file_path": "foo/bar2"
},
{
"action": "move",
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 0debdcfae89..58f18105e21 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -2,10 +2,10 @@
### Project visibility level
-Project in GitLab has be either private, internal or public.
-You can determine it by `visibility` field in project.
+Project in GitLab can be either private, internal or public.
+This is determined by the `visibility` field in the project.
-Constants for project visibility levels are next:
+Values for the project visibility level are:
* `private`:
Project access must be granted explicitly for each user.
@@ -18,7 +18,7 @@ Constants for project visibility levels are next:
## List projects
-Get a list of visible projects for authenticated user. When being accessed without authentication, all public projects are returned.
+Get a list of visible projects for authenticated user. When accessed without authentication, only public projects are returned.
```
GET /projects
@@ -343,6 +343,7 @@ Parameters:
| `lfs_enabled` | boolean | no | Enable LFS |
| `request_access_enabled` | boolean | no | Allow users to request member access |
| `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project |
+| `avatar` | mixed | no | Image file for avatar of the project |
### Create project for user
@@ -377,6 +378,7 @@ Parameters:
| `lfs_enabled` | boolean | no | Enable LFS |
| `request_access_enabled` | boolean | no | Allow users to request member access |
| `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project |
+| `avatar` | mixed | no | Image file for avatar of the project |
### Edit project
@@ -410,11 +412,14 @@ Parameters:
| `lfs_enabled` | boolean | no | Enable LFS |
| `request_access_enabled` | boolean | no | Allow users to request member access |
| `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project |
+| `avatar` | mixed | no | Image file for avatar of the project |
### Fork project
Forks a project into the user namespace of the authenticated user or the one provided.
+The forking operation for a project is asynchronous and is completed in a background job. The request will return immediately. To determine whether the fork of the project has completed, query the `import_status` for the new project.
+
```
POST /projects/:id/fork
```
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index 1bd1ee93ac5..73aee3c3f56 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -1,124 +1,168 @@
# Runners
-In GitLab CI, Runners run your [yaml](../yaml/README.md).
-A Runner is an isolated (virtual) machine that picks up jobs
-through the coordinator API of GitLab CI.
+In GitLab CI, Runners run the code defined in [`.gitlab-ci.yml`](../yaml/README.md).
+They are isolated (virtual) machines that pick up jobs through the coordinator
+API of GitLab CI.
A Runner can be specific to a certain project or serve any project
in GitLab CI. A Runner that serves all projects is called a shared Runner.
-Ideally, GitLab Runner should not be installed on the same machine as GitLab.
+Ideally, the GitLab Runner should not be installed on the same machine as GitLab.
Read the [requirements documentation](../../install/requirements.md#gitlab-runner)
for more information.
-## Shared vs. Specific Runners
-
-A Runner that is specific only runs for the specified project. A shared Runner
-can run jobs for every project that has enabled the option **Allow shared Runners**.
-
-**Shared Runners** are useful for jobs that have similar requirements,
-between multiple projects. Rather than having multiple Runners idling for
-many projects, you can have a single or a small number of Runners that handle
-multiple projects. This makes it easier to maintain and update Runners.
-
-**Specific Runners** are useful for jobs that have special requirements or for
-projects with a specific demand. If a job has certain requirements, you can set
-up the specific Runner with this in mind, while not having to do this for all
-Runners. For example, if you want to deploy a certain project, you can setup
-a specific Runner to have the right credentials for this.
-
-Projects with high demand of CI activity can also benefit from using specific Runners.
-By having dedicated Runners you are guaranteed that the Runner is not being held
-up by another project's jobs.
+## Shared vs specific Runners
+
+After [installing the Runner][install], you can either register it as shared or
+specific. You can only register a shared Runner if you have admin access to
+the GitLab instance. The main differences between a shared and a specific Runner
+are:
+
+- **Shared Runners** are useful for jobs that have similar requirements,
+ between multiple projects. Rather than having multiple Runners idling for
+ many projects, you can have a single or a small number of Runners that handle
+ multiple projects. This makes it easier to maintain and update them.
+ Shared Runners process jobs using a [fair usage queue](#how-shared-runners-pick-jobs).
+ In contrast to specific Runners that use a FIFO queue, this prevents
+ cases where projects create hundreds of jobs which can lead to eating all
+ available shared Runners resources.
+- **Specific Runners** are useful for jobs that have special requirements or for
+ projects with a specific demand. If a job has certain requirements, you can set
+ up the specific Runner with this in mind, while not having to do this for all
+ Runners. For example, if you want to deploy a certain project, you can setup
+ a specific Runner to have the right credentials for this. The [usage of tags](#using-tags)
+ may be useful in this case. Specific Runners process jobs using a [FIFO] queue.
+
+A Runner that is specific only runs for the specified project(s). A shared Runner
+can run jobs for every project that has enabled the option **Allow shared Runners**
+under **Settings ➔ Pipelines**.
+
+Projects with high demand of CI activity can also benefit from using specific
+Runners. By having dedicated Runners you are guaranteed that the Runner is not
+being held up by another project's jobs.
You can set up a specific Runner to be used by multiple projects. The difference
with a shared Runner is that you have to enable each project explicitly for
the Runner to be able to run its jobs.
Specific Runners do not get shared with forked projects automatically.
-A fork does copy the CI settings (jobs, allow shared, etc) of the cloned repository.
-
-# Creating and Registering a Runner
-
-There are several ways to create a Runner. Only after creation, upon
-registration its status as Shared or Specific is determined.
-
-[See the documentation for](https://docs.gitlab.com/runner/install)
-the different methods of installing a Runner instance.
+A fork does copy the CI settings (jobs, allow shared, etc) of the cloned
+repository.
-After installing the Runner, you can either register it as `Shared` or as `Specific`.
-You can only register a Shared Runner if you have admin access to the GitLab instance.
+## Registering a shared Runner
-## Registering a Shared Runner
+You can only register a shared Runner if you are an admin of the GitLab instance.
-You can only register a shared Runner if you are an admin on the linked
-GitLab instance.
+1. Grab the shared-Runner token on the `admin/runners` page
-Grab the shared-Runner token on the `admin/runners` page of your GitLab CI
-instance.
+ ![Shared Runners admin area](img/shared_runners_admin.png)
-![shared token](shared_runner.png)
+1. [Register the Runner][register]
-Now simply register the Runner as any Runner:
+Shared Runners are enabled by default as of GitLab 8.2, but can be disabled
+with the **Disable shared Runners** button which is present under each project's
+**Settings ➔ Pipelines** page. Previous versions of GitLab defaulted shared
+Runners to disabled.
-```
-sudo gitlab-ci-multi-runner register
-```
-
-Shared Runners are enabled by default as of GitLab 8.2, but can be disabled with the
-`DISABLE SHARED RUNNERS` button. Previous versions of GitLab defaulted shared Runners to
-disabled.
-
-## Registering a Specific Runner
+## Registering a specific Runner
Registering a specific can be done in two ways:
1. Creating a Runner with the project registration token
1. Converting a shared Runner into a specific Runner (one-way, admin only)
-There are several ways to create a Runner instance. The steps below only
-concern registering the Runner on GitLab CI.
-
-### Registering a Specific Runner with a Project Registration token
+### Registering a specific Runner with a project registration token
To create a specific Runner without having admin rights to the GitLab instance,
-visit the project you want to make the Runner work for in GitLab CI.
+visit the project you want to make the Runner work for in GitLab:
-Click on the Runner tab and use the registration token you find there to
-setup a specific Runner for this project.
+1. Go to **Settings ➔ Pipelines** to obtain the token
+1. [Register the Runner][register]
-![project Runners in GitLab CI](project_specific.png)
+### Making an existing shared Runner specific
-To register the Runner, run the command below and follow instructions:
+If you are an admin on your GitLab instance, you can turn any shared Runner into
+a specific one, but not the other way around. Keep in mind that this is a one
+way transition.
-```
-sudo gitlab-ci-multi-runner register
-```
+1. Go to the Runners in the admin area **Overview ➔ Runners** (`/admin/runners`)
+ and find your Runner
+1. Enable any projects under **Restrict projects for this Runner** to be used
+ with the Runner
-### Lock a specific Runner from being enabled for other projects
+From now on, the shared Runner will be specific to those projects.
+
+## Locking a specific Runner from being enabled for other projects
You can configure a Runner to assign it exclusively to a project. When a
Runner is locked this way, it can no longer be enabled for other projects.
-This setting is available on each Runner in *Project Settings* > *Runners*.
+This setting can be enabled the first time you [register a Runner][register] and
+can be changed afterwards under each Runner's settings.
+
+To lock/unlock a Runner:
+
+1. Visit your project's **Settings ➔ Pipelines**
+1. Find the Runner you wish to lock/unlock and make sure it's enabled
+1. Click the pencil button
+1. Check the **Lock to current projects** option
+1. Click **Save changes** for the changes to take effect
-### Making an existing Shared Runner Specific
+## How shared Runners pick jobs
-If you are an admin on your GitLab instance,
-you can make any shared Runner a specific Runner, _but you can not
-make a specific Runner a shared Runner_.
+Shared Runners abide to a process queue we call fair usage. The fair usage
+algorithm tries to assign jobs to shared Runners from projects that have the
+lowest number of jobs currently running on shared Runners.
-To make a shared Runner specific, go to the Runner page (`/admin/runners`)
-and find your Runner. Add any projects on the left to make this Runner
-run exclusively for these projects, therefore making it a specific Runner.
+**Example 1**
-![making a shared Runner specific](shared_to_specific_admin.png)
+We have following jobs in queue:
-## Using Shared Runners Effectively
+- job 1 for project 1
+- job 2 for project 1
+- job 3 for project 1
+- job 4 for project 2
+- job 5 for project 2
+- job 6 for project 3
+
+With the fair usage algorithm jobs are assigned in following order:
+
+1. We choose job 1, because project 1 doesn't run currently any jobs and has the lowest job number from projects that doesn't run jobs
+1. We choose job 4, because project 2 doesn't run currently any jobs and has the lowest job number from projects that doesn't run jobs
+1. We choose job 6, because project 3 doesn't run currently any jobs and has the lowest job number from projects that doesn't run jobs
+1. We choose job 2, because project 1 as other it runs 1 job
+1. We choose job 5, because project 2 runs 1 job, where project 1 runs 2 jobs now
+1. We choose job 3, because project 1 and runs 2 jobs
+
+---
+
+**Example 2**
+
+We have following jobs in queue:
+
+- job 1 for project 1
+- job 2 for project 1
+- job 3 for project 1
+- job 4 for project 2
+- job 5 for project 2
+- job 6 for project 3
+
+With the fair usage algorithm jobs are assigned in following order:
+
+1. We choose job 1, because project 1 doesn't run currently any jobs and has the lowest job number from projects that doesn't run jobs
+1. We finish job 1
+1. We choose job 2, because project 1 doesn't run currently any jobs and has the lowest job number from projects that doesn't run jobs
+1. We choose job 4, because project 2 doesn't run currently any jobs and has the lowest job number from projects that doesn't run jobs
+1. We finish job 4
+1. We choose job 5, because project 2 doesn't run currently any jobs and has the lowest job number from projects that doesn't run jobs
+1. We choose job 6, because project 3 doesn't run currently any jobs
+1. We choose job 3, because project 1, 2 and 3 runs exactly one job now
+
+## Using shared Runners effectively
If you are planning to use shared Runners, there are several things you
should keep in mind.
-### Use Tags
+### Using tags
You must setup a Runner to be able to run all the different types of jobs
that it may encounter on the projects it's shared over. This would be
@@ -130,17 +174,27 @@ shared Runners will only run the jobs they are equipped to run.
For instance, at GitLab we have Runners tagged with "rails" if they contain
the appropriate dependencies to run Rails test suites.
-### Prevent Runner with tags from picking jobs without tags
+### Preventing Runners with tags from picking jobs without tags
You can configure a Runner to prevent it from picking jobs with tags when
-the Runner does not have tags assigned. This setting is available on each
-Runner in *Project Settings* > *Runners*.
+the Runner does not have tags assigned. This setting can be enabled the first
+time you [register a Runner][register] and can be changed afterwards under
+each Runner's settings.
+
+To make a Runner pick tagged/untagged jobs:
+
+1. Visit your project's **Settings ➔ Pipelines**
+1. Find the Runner you wish and make sure it's enabled
+1. Click the pencil button
+1. Check the **Run untagged jobs** option
+1. Click **Save changes** for the changes to take effect
### Be careful with sensitive information
If you can run a job on a Runner, you can get access to any code it runs
and get the token of the Runner. With shared Runners, this means that anyone
-that runs jobs on the Runner, can access anyone else's code that runs on the Runner.
+that runs jobs on the Runner, can access anyone else's code that runs on the
+Runner.
In addition, because you can get access to the Runner token, it is possible
to create a clone of a Runner and submit false jobs, for example.
@@ -160,3 +214,7 @@ project.
Mentioned briefly earlier, but the following things of Runners can be exploited.
We're always looking for contributions that can mitigate these
[Security Considerations](https://docs.gitlab.com/runner/security/).
+
+[install]: http://docs.gitlab.com/runner/install/
+[fifo]: https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics)
+[register]: http://docs.gitlab.com/runner/register/
diff --git a/doc/ci/runners/img/shared_runners_admin.png b/doc/ci/runners/img/shared_runners_admin.png
new file mode 100644
index 00000000000..e049b339b36
--- /dev/null
+++ b/doc/ci/runners/img/shared_runners_admin.png
Binary files differ
diff --git a/doc/ci/runners/project_specific.png b/doc/ci/runners/project_specific.png
deleted file mode 100644
index c812defa67b..00000000000
--- a/doc/ci/runners/project_specific.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/runners/shared_runner.png b/doc/ci/runners/shared_runner.png
deleted file mode 100644
index 31574a17764..00000000000
--- a/doc/ci/runners/shared_runner.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index cb646827fb4..7ec7136d8c6 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -4,15 +4,22 @@
- [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.
+Triggers can be used to force a pipeline rerun of a specific `ref` (branch or
+tag) with an API call.
-## Add a trigger
+## Authentication tokens
+
+The following methods of authentication are supported.
+
+### Trigger token
+
+A unique trigger token can be obtained when [adding a new trigger](#adding-a-new-trigger).
+
+## Adding a new trigger
You can add a new trigger by going to your project's
-**Settings ➔ Pipelines ➔ Triggers**. The **Add trigger** button will
+**Settings ➔ Pipelines** under **Triggers**. The **Add trigger** button will
create a new token which you can then use to trigger a rerun of this
particular project's pipeline.
@@ -22,7 +29,10 @@ overview of the time the triggers were last used.
![Triggers page overview](img/triggers_page.png)
-## Take ownership
+## Taking ownership of a trigger
+
+> **Note**:
+GitLab 9.0 introduced a trigger ownership to solve permission problems.
Each created trigger when run will impersonate their associated user including
their access to projects and their project permissions.
@@ -30,26 +40,20 @@ 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
+## Revoking 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.
+**Settings ➔ Pipelines** under **Triggers** and hitting the **Revoke** button.
+The action is irreversible.
-## Trigger a pipeline
+## Triggering a pipeline
-> **Note**:
-Valid refs are only the branches and tags. If you pass a commit SHA as a ref,
-it will not trigger a job.
+> **Notes**:
+- Valid refs are only the branches and tags. If you pass a commit SHA as a ref,
+ it will not trigger a job.
+- If your project is public, passing the token in plain text is probably not the
+ wisest idea, so you might want to use a
+ [secret variable](../variables/README.md#secret-variables) for that purpose.
To trigger a job you need to send a `POST` request to GitLab's API endpoint:
@@ -57,11 +61,11 @@ To trigger a job you need to send a `POST` request to GitLab's API endpoint:
POST /projects/:id/trigger/pipeline
```
-The required parameters are the trigger's `token` and the Git `ref` on which
-the trigger will be performed. Valid refs are the branch and the tag. The `:id`
-of a project can be found by [querying the API](../../api/projects.md)
-or by visiting the **Pipelines** settings page which provides
-self-explanatory examples.
+The required parameters are the [trigger's `token`](#authentication-tokens)
+and the Git `ref` on which the trigger will be performed. Valid refs are the
+branch and the tag. The `:id` of a project can be found by
+[querying the API](../../api/projects.md) or by visiting the **Pipelines**
+settings page which provides self-explanatory examples.
When a rerun of a pipeline is triggered, the information is exposed in GitLab's
UI under the **Jobs** page and the jobs are marked as triggered 'by API'.
@@ -78,46 +82,7 @@ below.
---
-See the [Examples](#examples) section for more details on how to actually
-trigger a rebuild.
-
-## Trigger a pipeline from webhook
-
-> Introduced in GitLab 8.14.
-
-To trigger a job from webhook of another project you need to add the following
-webhook url for Push and Tag push events:
-
-```
-https://gitlab.example.com/api/v4/projects/:id/ref/:ref/trigger/pipeline?token=TOKEN
-```
-
-> **Note**:
-- `ref` should be passed as part of url in order to take precedence over `ref`
- from webhook body that designates the branchref that fired the trigger in the source repository.
-- `ref` should be url encoded if contains slashes.
-
-## Pass job variables to a trigger
-
-You can pass any number of arbitrary variables in the trigger API call and they
-will be available in GitLab CI so that they can be used in your `.gitlab-ci.yml`
-file. The parameter is of the form:
-
-```
-variables[key]=value
-```
-
-This information is also exposed in the UI.
-
-![Job variables in UI](img/trigger_variables.png)
-
----
-
-See the [Examples](#examples) section below for more details.
-
-## Examples
-
-Using cURL you can trigger a rebuild with minimal effort, for example:
+By using cURL you can trigger a pipeline rerun with minimal effort, for example:
```bash
curl --request POST \
@@ -135,8 +100,6 @@ curl --request POST \
"https://gitlab.example.com/api/v4/projects/9/trigger/pipeline?token=TOKEN&ref=master"
```
-### 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`
branch of project B whenever a tag on project A is created. This is the job you
@@ -156,14 +119,37 @@ Now, whenever a new tag is pushed on project A, the job will run and the
`stage: deploy` ensures that this job will run only after all jobs with
`stage: test` complete successfully.
-_**Note:** If your project is public, passing the token in plain text is
-probably not the wisest idea, so you might want to use a
-[secure variable](../variables/README.md#user-defined-variables-secure-variables)
-for that purpose._
+## Triggering a pipeline from a webhook
-### Making use of trigger variables
+> **Notes**:
+- Introduced in GitLab 8.14.
+- `ref` should be passed as part of the URL in order to take precedence over
+ `ref` from the webhook body that designates the branch ref that fired the
+ trigger in the source repository.
+- `ref` should be URL-encoded if it contains slashes.
-Using trigger variables can be proven useful for a variety of reasons.
+To trigger a job from a webhook of another project you need to add the following
+webhook URL for Push and Tag events (change the project ID, ref and token):
+
+```
+https://gitlab.example.com/api/v4/projects/9/ref/master/trigger/pipeline?token=TOKEN
+```
+
+## Making use of trigger variables
+
+You can pass any number of arbitrary variables in the trigger API call and they
+will be available in GitLab CI so that they can be used in your `.gitlab-ci.yml`
+file. The parameter is of the form:
+
+```
+variables[key]=value
+```
+
+This information is also exposed in the UI.
+
+![Job variables in UI](img/trigger_variables.png)
+
+Using trigger variables can be proven useful for a variety of reasons:
* Identifiable jobs. Since the variable is exposed in the UI you can know
why the rebuild was triggered if you pass a variable that explains the
@@ -208,15 +194,7 @@ curl --request POST \
https://gitlab.example.com/api/v4/projects/9/trigger/pipeline
```
-### Using a webhook to trigger a pipeline
-
-You can add the following webhook to another project in order to trigger a job:
-
-```
-https://gitlab.example.com/api/v4/projects/9/ref/master/trigger/pipeline?token=TOKEN&variables[UPLOAD_TO_S3]=true
-```
-
-### Using cron to trigger nightly pipelines
+## Using cron to trigger nightly pipelines
>**Note:**
The following behavior can also be achieved through GitLab's UI with
@@ -230,4 +208,18 @@ branch of project with ID `9` every night at `00:30`:
30 0 * * * curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v4/projects/9/trigger/pipeline
```
+## Legacy triggers
+
+Old triggers, created before GitLab 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. They are considered deprecated and will be
+removed with one of the future versions of GitLab. You are advised to
+[take ownership](#taking-ownership) of any legacy triggers.
+
+[ee-2017]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2017
[ci-229]: https://gitlab.com/gitlab-org/gitlab-ci/merge_requests/229
+[ee]: https://about.gitlab.com/gitlab-ee/
+[variables]: ../variables/README.md
+[predef]: ../variables/README.md#predefined-variables-environment-variables
+[registry]: ../../user/project/container_registry.md
diff --git a/doc/ci/triggers/img/triggers_page.png b/doc/ci/triggers/img/triggers_page.png
index eafd8519a23..7dc8f91cf7e 100644
--- a/doc/ci/triggers/img/triggers_page.png
+++ b/doc/ci/triggers/img/triggers_page.png
Binary files differ
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 76ba78ea7be..d1f9881e51b 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -431,3 +431,4 @@ export CI_REGISTRY_PASSWORD="longalfanumstring"
[protected branches]: ../../user/project/protected_branches.md
[protected tags]: ../../user/project/protected_tags.md
[shellexecutors]: https://docs.gitlab.com/runner/executors/
+[eep]: https://about.gitlab.com/gitlab-ee/ "Available only in GitLab Enterprise Edition Premium"
diff --git a/doc/development/README.md b/doc/development/README.md
index 40addfd8a4c..9496a87d84d 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -53,6 +53,7 @@
- [Serializing Data](serializing_data.md)
- [Polymorphic Associations](polymorphic_associations.md)
- [Single Table Inheritance](single_table_inheritance.md)
+- [Background Migrations](background_migrations.md)
## i18n
diff --git a/doc/development/background_migrations.md b/doc/development/background_migrations.md
new file mode 100644
index 00000000000..0239e6b3163
--- /dev/null
+++ b/doc/development/background_migrations.md
@@ -0,0 +1,205 @@
+# Background Migrations
+
+Background migrations can be used to perform data migrations that would
+otherwise take a very long time (hours, days, years, etc) to complete. For
+example, you can use background migrations to migrate data so that instead of
+storing data in a single JSON column the data is stored in a separate table.
+
+## When To Use Background Migrations
+
+In the vast majority of cases you will want to use a regular Rails migration
+instead. Background migrations should _only_ be used when migrating _data_ in
+tables that have so many rows this process would take hours when performed in a
+regular Rails migration.
+
+Background migrations _may not_ be used to perform schema migrations, they
+should only be used for data migrations.
+
+Some examples where background migrations can be useful:
+
+* Migrating events from one table to multiple separate tables.
+* Populating one column based on JSON stored in another column.
+* Migrating data that depends on the output of exernal services (e.g. an API).
+
+## Isolation
+
+Background migrations must be isolated and can not use application code (e.g.
+models defined in `app/models`). Since these migrations can take a long time to
+run it's possible for new versions to be deployed while they are still running.
+
+It's also possible for different migrations to be executed at the same time.
+This means that different background migrations should not migrate data in a
+way that would cause conflicts.
+
+## How It Works
+
+Background migrations are simple classes that define a `perform` method. A
+Sidekiq worker will then execute such a class, passing any arguments to it. All
+migration classes must be defined in the namespace
+`Gitlab::BackgroundMigration`, the files should be placed in the directory
+`lib/gitlab/background_migration/`.
+
+## Scheduling
+
+Scheduling a migration can be done in either a regular migration or a
+post-deployment migration. To do so, simply use the following code while
+replacing the class name and arguments with whatever values are necessary for
+your migration:
+
+```ruby
+BackgroundMigrationWorker.perform_async('BackgroundMigrationClassName', [arg1, arg2, ...])
+```
+
+Usually it's better to schedule jobs in bulk, for this you can use
+`BackgroundMigrationWorker.perform_bulk`:
+
+```ruby
+BackgroundMigrationWorker.perform_bulk(
+ ['BackgroundMigrationClassName', [1]],
+ ['BackgroundMigrationClassName', [2]],
+ ...
+)
+```
+
+You'll also need to make sure that newly created data is either migrated, or
+saved in both the old and new version upon creation. For complex and time
+consuming migrations it's best to schedule a background job using an
+`after_create` hook so this doesn't affect response timings. The same applies to
+updates. Removals in turn can be handled by simply defining foreign keys with
+cascading deletes.
+
+## Cleaning Up
+
+Because background migrations can take a long time you can't immediately clean
+things up after scheduling them. For example, you can't drop a column that's
+used in the migration process as this would cause jobs to fail. This means that
+you'll need to add a separate _post deployment_ migration in a future release
+that finishes any remaining jobs before cleaning things up (e.g. removing a
+column).
+
+As an example, say you want to migrate the data from column `foo` (containing a
+big JSON blob) to column `bar` (containing a string). The process for this would
+roughly be as follows:
+
+1. Release A:
+ 1. Create a migration class that perform the migration for a row with a given ID.
+ 1. Deploy the code for this release, this should include some code that will
+ schedule jobs for newly created data (e.g. using an `after_create` hook).
+ 1. Schedule jobs for all existing rows in a post-deployment migration. It's
+ possible some newly created rows may be scheduled twice so your migration
+ should take care of this.
+1. Release B:
+ 1. Deploy code so that the application starts using the new column and stops
+ scheduling jobs for newly created data.
+ 1. In a post-deployment migration you'll need to ensure no jobs remain. To do
+ so you can use `Gitlab::BackgroundMigration.steal` to process any remaining
+ jobs before continueing.
+ 1. Remove the old column.
+
+## Example
+
+To explain all this, let's use the following example: the table `services` has a
+field called `properties` which is stored in JSON. For all rows you want to
+extract the `url` key from this JSON object and store it in the `services.url`
+column. There are millions of services and parsing JSON is slow, thus you can't
+do this in a regular migration.
+
+To do this using a background migration we'll start with defining our migration
+class:
+
+```ruby
+class Gitlab::BackgroundMigration::ExtractServicesUrl
+ class Service < ActiveRecord::Base
+ self.table_name = 'services'
+ end
+
+ def perform(service_id)
+ # A row may be removed between scheduling and starting of a job, thus we
+ # need to make sure the data is still present before doing any work.
+ service = Service.select(:properties).find_by(id: service_id)
+
+ return unless service
+
+ begin
+ json = JSON.load(service.properties)
+ rescue JSON::ParserError
+ # If the JSON is invalid we don't want to keep the job around forever,
+ # instead we'll just leave the "url" field to whatever the default value
+ # is.
+ return
+ end
+
+ service.update(url: json['url']) if json['url']
+ end
+end
+```
+
+Next we'll need to adjust our code so we schedule the above migration for newly
+created and updated services. We can do this using something along the lines of
+the following:
+
+```ruby
+class Service < ActiveRecord::Base
+ after_commit :schedule_service_migration, on: :update
+ after_commit :schedule_service_migration, on: :create
+
+ def schedule_service_migration
+ BackgroundMigrationWorker.perform_async('ExtractServicesUrl', [id])
+ end
+end
+```
+
+We're using `after_commit` here to ensure the Sidekiq job is not scheduled
+before the transaction completes as doing so can lead to race conditions where
+the changes are not yet visible to the worker.
+
+Next we'll need a post-deployment migration that schedules the migration for
+existing data. Since we're dealing with a lot of rows we'll schedule jobs in
+batches instead of doing this one by one:
+
+```ruby
+class ScheduleExtractServicesUrl < ActiveRecord::Migration
+ disable_ddl_transaction!
+
+ class Service < ActiveRecord::Base
+ self.table_name = 'services'
+ end
+
+ def up
+ Service.select(:id).in_batches do |relation|
+ jobs = relation.pluck(:id).map do |id|
+ ['ExtractServicesUrl', [id]]
+ end
+
+ BackgroundMigrationWorker.perform_bulk(jobs)
+ end
+ end
+
+ def down
+ end
+end
+```
+
+Once deployed our application will continue using the data as before but at the
+same time will ensure that both existing and new data is migrated.
+
+In the next release we can remove the `after_commit` hooks and related code. We
+will also need to add a post-deployment migration that consumes any remaining
+jobs. Such a migration would look like this:
+
+```ruby
+class ConsumeRemainingExtractServicesUrlJobs < ActiveRecord::Migration
+ disable_ddl_transaction!
+
+ def up
+ Gitlab::BackgroundMigration.steal('ExtractServicesUrl')
+ end
+
+ def down
+ end
+end
+```
+
+This migration will then process any jobs for the ExtractServicesUrl migration
+and continue once all jobs have been processed. Once done you can safely remove
+the `services.properties` column.
diff --git a/doc/development/i18n_guide.md b/doc/development/i18n_guide.md
index bfb0779fbfa..756535e28bc 100644
--- a/doc/development/i18n_guide.md
+++ b/doc/development/i18n_guide.md
@@ -127,6 +127,14 @@ New translations will be added with their default content and will be marked
fuzzy. To use the translation, look for the `#, fuzzy` mention in `gitlab.edit.po`
and remove it.
+We need to make sure we remove the `fuzzy` translations before generating the
+`locale/**/gitlab.po` file. When they aren't removed, the resulting `.po` will
+be treated as a binary file which could overwrite translations that were merged
+before the new translations.
+
+When we are just preparing a page to be translated, but not actually adding any
+translations. There's no need to generate `.po` files.
+
Translations that aren't used in the source code anymore will be marked with
`~#`; these can be removed to keep our translation files clutter-free.
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 5338ccb9d3a..197a92905c8 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -129,7 +129,8 @@ We _highly_ recommend the use of PostgreSQL instead of MySQL/MariaDB as not all
features of GitLab may work with MySQL/MariaDB. For example, MySQL does not have
the right features to support nested groups in an efficient manner; see
<https://gitlab.com/gitlab-org/gitlab-ce/issues/30472> for more information
-about this. Existing users using GitLab with MySQL/MariaDB are advised to
+about this. GitLab Geo also does [not support MySQL](https://docs.gitlab.com/ee/gitlab-geo/database.html#mysql-replication).
+Existing users using GitLab with MySQL/MariaDB are advised to
migrate to PostgreSQL instead.
The server running the database should have _at least_ 5-10 GB of storage
diff --git a/doc/integration/google.md b/doc/integration/google.md
index 1e7ad90c5a8..d5b523e6dc0 100644
--- a/doc/integration/google.md
+++ b/doc/integration/google.md
@@ -72,6 +72,21 @@ To enable the Google OAuth2 OmniAuth provider you must register your application
1. Change 'YOUR_APP_SECRET' to the client secret from the Google Developer page from step 10.
+1. Make sure that you configure GitLab to use an FQDN as Google will not accept raw IP addresses.
+
+ For Omnibus packages:
+
+ ```ruby
+ external_url 'https://gitlab.example.com'
+ ```
+
+ For installations from source:
+
+ ```yaml
+ gitlab:
+ host: https://gitlab.example.com
+ ```
+
1. Save the configuration file.
1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md
index 591d1524061..9544de41b9a 100644
--- a/doc/university/glossary/README.md
+++ b/doc/university/glossary/README.md
@@ -1,4 +1,3 @@
-
## What is the Glossary
This contains a simplified list and definitions of some of the terms that you will encounter in your day to day activities when working with GitLab.
@@ -10,7 +9,7 @@ User authentication by combination of 2 different steps during login. This allow
### Access Levels
-Process of selective restriction to create, view, modify or delete a resource based on a set of assigned permissions. See [GitLab's Permission Guidelines](../../permissions/permissions.md
+Process of selective restriction to create, view, modify or delete a resource based on a set of assigned permissions. See [GitLab's Permission Guidelines](../../user/permissions.md)
### Active Directory (AD)
diff --git a/doc/update/9.1-to-9.2.md b/doc/update/9.1-to-9.2.md
index 19db6e5763e..e7d97fde14e 100644
--- a/doc/update/9.1-to-9.2.md
+++ b/doc/update/9.1-to-9.2.md
@@ -110,8 +110,8 @@ sudo -u git -H bin/compile
### 7. Update gitlab-workhorse
Install and compile gitlab-workhorse. This requires
-[Go 1.5](https://golang.org/dl) which should already be on your system from
-GitLab 8.1. GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/).
+[Go 1.8](https://golang.org/dl). Go (at least 1.5) should already be on your system from
+GitLab 8.1 and shall be upgraded if necessary. Please note that starting in Gitlab 9.3, only Go 1.8.3 and above will be supported. GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/).
If you are not using Linux you may have to run `gmake` instead of
`make` below.
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
index 10c281448a3..75ea911b9bc 100644
--- a/doc/user/project/container_registry.md
+++ b/doc/user/project/container_registry.md
@@ -39,6 +39,14 @@ You can read more about Docker Registry at https://docs.docker.com/registry/intr
## Build and push images
+>**Notes:**
+- Moving or renaming existing container registry repositories is not supported
+once you have pushed images because the images are signed, and the
+signature includes the repository name.
+- To move or rename a repository with a container registry you will have to
+delete all existing images.
+
+
If you visit the **Registry** link under your project's menu, you can see the
explicit instructions to login to the Container Registry using your GitLab
credentials.
diff --git a/doc/user/project/img/protected_branches_delete.png b/doc/user/project/img/protected_branches_delete.png
new file mode 100644
index 00000000000..cfdfe6c6c29
--- /dev/null
+++ b/doc/user/project/img/protected_branches_delete.png
Binary files differ
diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md
index d19d184f9b0..17cc21238ff 100644
--- a/doc/user/project/pipelines/schedules.md
+++ b/doc/user/project/pipelines/schedules.md
@@ -31,6 +31,26 @@ is installed on.
![Schedules list](img/pipeline_schedules_list.png)
+## Using only and except
+
+To configure that a job can be executed only when the pipeline has been
+scheduled (or the opposite), you can use
+[only and except](../../../ci/yaml/README.md#only-and-except) configuration keywords.
+
+```
+job:on-schedule:
+ only:
+ - schedules
+ script:
+ - make world
+
+job:
+ except:
+ - schedules
+ script:
+ - make build
+```
+
## Taking ownership
Pipelines are executed as a user, who owns a schedule. This influences what
diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md
index 7650020b37e..0570d9f471f 100644
--- a/doc/user/project/protected_branches.md
+++ b/doc/user/project/protected_branches.md
@@ -94,8 +94,33 @@ all matching branches:
![Protected branch matches](img/protected_branches_matches.png)
+## Deleting a protected branch
+
+> [Introduced][ce-21393] in GitLab 9.3.
+
+From time to time, it may be required to delete or clean up branches that are
+protected.
+
+User with [Master permissions][perm] and up can manually delete protected
+branches via GitLab's web interface:
+
+1. Visit **Repository > Branches**
+1. Click on the delete icon next to the branch you wish to delete
+1. In order to prevent accidental deletion, an additional confirmation is
+ required
+
+ ![Delete protected branches](img/protected_branches_delete.png)
+
+Deleting a protected branch is only allowed via the web interface, not via Git.
+This means that you can't accidentally delete a protected branch from your
+command line or a Git client application.
+
## Changelog
+**9.2**
+
+- Allow deletion of protected branches via the web interface [gitlab-org/gitlab-ce#21393][ce-21393]
+
**8.11**
- Allow creating protected branches that can't be pushed to [gitlab-org/gitlab-ce!5081][ce-5081]
@@ -110,4 +135,6 @@ all matching branches:
[ce-4665]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4665 "Allow specifying protected branches using wildcards"
[ce-4892]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4892 "Allow developers to merge into a protected branch without having push access"
[ce-5081]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5081 "Allow creating protected branches that can't be pushed to"
+[ce-21393]: https://gitlab.com/gitlab-org/gitlab-ce/issues/21393
[ee-restrict]: http://docs.gitlab.com/ee/user/project/protected_branches.html#restricting-push-and-merge-access-to-certain-users
+[perm]: ../permissions.md
diff --git a/doc/workflow/groups.md b/doc/workflow/groups.md
index 1cb3c940f00..1645e7e8d65 100644
--- a/doc/workflow/groups.md
+++ b/doc/workflow/groups.md
@@ -23,9 +23,10 @@ You can use the 'New project' button to add a project to the new group.
## Transferring an existing project into a group
-You can transfer an existing project into a group you own from the project settings page. The option to transfer a project is only available if you are the Owner of the project.
+You can transfer an existing project into a group you have at least Master access in from the project settings page.
+The option to transfer a project is only available if you are the Owner of the project.
First scroll down to the 'Dangerous settings' and click 'Show them to me'.
-Now you can pick any of the groups you manage as the new namespace for the group.
+Now you can pick any of the groups you have at least Master access in as the new namespace for the group.
![Transfer a project to a new namespace](groups/transfer_project.png)
diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md
index c5b7488be69..87416008e98 100644
--- a/doc/workflow/shortcuts.md
+++ b/doc/workflow/shortcuts.md
@@ -6,7 +6,10 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| Keyboard Shortcut | Description |
| ----------------- | ----------- |
+| <kbd>n</kbd> | Main navigation |
| <kbd>s</kbd> | Focus search |
+| <kbd>f</kbd> | Focus filter |
+| <kbd>p b</kbd> | Show/hide the Performance Bar |
| <kbd>?</kbd> | Show/hide this dialog |
| <kbd>⌘</kbd> + <kbd>shift</kbd> + <kbd>p</kbd> | Toggle markdown preview |
| <kbd>↑</kbd> | Edit last comment (when focused on an empty textarea) |
diff --git a/features/project/builds/permissions.feature b/features/project/builds/permissions.feature
index 3c7f72335d9..db15968db06 100644
--- a/features/project/builds/permissions.feature
+++ b/features/project/builds/permissions.feature
@@ -27,6 +27,7 @@ Feature: Project Builds Permissions
When I visit project builds page
Then page status code should be 404
+ @javascript
Scenario: I try to visit build details of internal project with access to builds
Given The project is internal
And public access for builds is enabled
diff --git a/features/project/builds/summary.feature b/features/project/builds/summary.feature
index 550ebccf0d7..3bf15b0cf87 100644
--- a/features/project/builds/summary.feature
+++ b/features/project/builds/summary.feature
@@ -6,16 +6,19 @@ Feature: Project Builds Summary
And project has coverage enabled
And project has a recent build
+ @javascript
Scenario: I browse build details page
When I visit recent build details page
Then I see details of a build
And I see build trace
+ @javascript
Scenario: I browse project builds page
When I visit project builds page
Then I see coverage
Then I see button to CI Lint
+ @javascript
Scenario: I erase a build
Given recent build is successful
And recent build has a build trace
diff --git a/features/steps/groups.rb b/features/steps/groups.rb
index 83d8abbab1f..25bb374b868 100644
--- a/features/steps/groups.rb
+++ b/features/steps/groups.rb
@@ -81,7 +81,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
step 'I should see new group "Owned" avatar' do
expect(owned_group.avatar).to be_instance_of AvatarUploader
- expect(owned_group.avatar.url).to eq "/uploads/group/avatar/#{Group.find_by(name: "Owned").id}/banana_sample.gif"
+ expect(owned_group.avatar.url).to eq "/uploads/system/group/avatar/#{Group.find_by(name: "Owned").id}/banana_sample.gif"
end
step 'I should see the "Remove avatar" button' do
diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb
index 24cfbaad7fe..254c26bb6af 100644
--- a/features/steps/profile/profile.rb
+++ b/features/steps/profile/profile.rb
@@ -36,7 +36,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
step 'I should see new avatar' do
expect(@user.avatar).to be_instance_of AvatarUploader
- expect(@user.avatar.url).to eq "/uploads/user/avatar/#{@user.id}/banana_sample.gif"
+ expect(@user.avatar.url).to eq "/uploads/system/user/avatar/#{@user.id}/banana_sample.gif"
end
step 'I should see the "Remove avatar" button' do
diff --git a/features/steps/project/builds/summary.rb b/features/steps/project/builds/summary.rb
index 229e5d7cdf4..20a5c873ecd 100644
--- a/features/steps/project/builds/summary.rb
+++ b/features/steps/project/builds/summary.rb
@@ -13,7 +13,7 @@ class Spinach::Features::ProjectBuildsSummary < Spinach::FeatureSteps
step 'I see button to CI Lint' do
page.within('.nav-controls') do
ci_lint_tool_link = page.find_link('CI lint')
- expect(ci_lint_tool_link[:href]).to eq ci_lint_path
+ expect(ci_lint_tool_link[:href]).to end_with(ci_lint_path)
end
end
diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb
index de32c9afcca..7d34331db46 100644
--- a/features/steps/project/project.rb
+++ b/features/steps/project/project.rb
@@ -38,7 +38,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps
step 'I should see new project avatar' do
expect(@project.avatar).to be_instance_of AvatarUploader
url = @project.avatar.url
- expect(url).to eq "/uploads/project/avatar/#{@project.id}/banana_sample.gif"
+ expect(url).to eq "/uploads/system/project/avatar/#{@project.id}/banana_sample.gif"
end
step 'I should see the "Remove avatar" button' do
diff --git a/features/steps/project/project_group_links.rb b/features/steps/project/project_group_links.rb
index 739a85e5fa4..5280a38ce81 100644
--- a/features/steps/project/project_group_links.rb
+++ b/features/steps/project/project_group_links.rb
@@ -5,18 +5,19 @@ class Spinach::Features::ProjectGroupLinks < Spinach::FeatureSteps
include Select2Helper
step 'I should see project already shared with group "Ops"' do
- page.within '.enabled-groups' do
+ page.within '.project-members-groups' do
expect(page).to have_content "Ops"
end
end
step 'I should see project is not shared with group "Market"' do
- page.within '.enabled-groups' do
+ page.within '.project-members-groups' do
expect(page).not_to have_content "Market"
end
end
step 'I select group "Market" for share' do
+ click_link 'Share with group'
group = Group.find_by(path: 'market')
select2(group.id, from: "#link_group_id")
select "Master", from: 'link_group_access'
@@ -24,7 +25,7 @@ class Spinach::Features::ProjectGroupLinks < Spinach::FeatureSteps
end
step 'I should see project is shared with group "Market"' do
- page.within '.enabled-groups' do
+ page.within '.project-members-groups' do
expect(page).to have_content "Market"
end
end
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index d099d7af167..80aa3a047a0 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -89,10 +89,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I fill the new branch name' do
- first('button.js-target-branch', visible: true).click
- find('.create-new-branch', visible: true).click
- find('#new_branch_name', visible: true).set('new_branch_name')
- find('.js-new-branch-btn', visible: true).click
+ fill_in :branch_name, with: 'new_branch_name', visible: true
end
step 'I fill the new file name with an illegal name' do
diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb
index 0fee158d590..cf31e61437e 100644
--- a/features/steps/project/source/markdown_render.rb
+++ b/features/steps/project/source/markdown_render.rb
@@ -90,6 +90,8 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
click_link "api"
end
+ wait_for_requests
+
page.within '.tree-table' do
click_link "README.md"
end
diff --git a/features/support/capybara.rb b/features/support/capybara.rb
index 6da8aaac6cb..f4691647d4b 100644
--- a/features/support/capybara.rb
+++ b/features/support/capybara.rb
@@ -11,8 +11,10 @@ Capybara.register_driver :poltergeist do |app|
js_errors: true,
timeout: timeout,
window_size: [1366, 768],
+ url_whitelist: %w[localhost 127.0.0.1],
+ url_blacklist: %w[.mp4 .png .gif .avi .bmp .jpg .jpeg],
phantomjs_options: [
- '--load-images=no'
+ '--load-images=yes'
]
)
end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 88f91c07194..d767af36e8e 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -45,6 +45,7 @@ module API
end
before { allow_access_with_scope :api }
+ before { header['X-Frame-Options'] = 'SAMEORIGIN' }
before { Gitlab::I18n.locale = current_user&.preferred_language }
after { Gitlab::I18n.use_default_locale }
diff --git a/lib/api/files.rb b/lib/api/files.rb
index 25b0968a271..521287ee2b4 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -25,7 +25,7 @@ module API
@blob = @repo.blob_at(@commit.sha, params[:file_path])
not_found!('File') unless @blob
- @blob.load_all_data!(@repo)
+ @blob.load_all_data!
end
def commit_response(attrs)
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 56046742e08..50d34e8a738 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -22,6 +22,7 @@ module API
optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved'
optional :tag_list, type: Array[String], desc: 'The list of tags for a project'
+ optional :avatar, type: File, desc: 'Avatar image for project'
end
params :optional_params do
diff --git a/lib/api/v3/files.rb b/lib/api/v3/files.rb
index c76acc86504..7b4b3448b6d 100644
--- a/lib/api/v3/files.rb
+++ b/lib/api/v3/files.rb
@@ -56,7 +56,7 @@ module API
blob = repo.blob_at(commit.sha, params[:file_path])
not_found!('File') unless blob
- blob.load_all_data!(repo)
+ blob.load_all_data!
status(200)
{
diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb
index d99a3bfa625..1e2536231d8 100644
--- a/lib/banzai/reference_parser/base_parser.rb
+++ b/lib/banzai/reference_parser/base_parser.rb
@@ -62,7 +62,7 @@ module Banzai
nodes.select do |node|
if node.has_attribute?(project_attr)
- can_read_reference?(user, projects[node])
+ can_read_reference?(user, projects[node], node)
else
true
end
@@ -231,7 +231,7 @@ module Banzai
# see reference comments.
# Override this method on subclasses
# to check if user can read resource
- def can_read_reference?(user, ref_project)
+ def can_read_reference?(user, ref_project, node)
raise NotImplementedError
end
diff --git a/lib/banzai/reference_parser/commit_parser.rb b/lib/banzai/reference_parser/commit_parser.rb
index 8c54a041cb8..30dc87248b4 100644
--- a/lib/banzai/reference_parser/commit_parser.rb
+++ b/lib/banzai/reference_parser/commit_parser.rb
@@ -32,7 +32,7 @@ module Banzai
private
- def can_read_reference?(user, ref_project)
+ def can_read_reference?(user, ref_project, node)
can?(user, :download_code, ref_project)
end
end
diff --git a/lib/banzai/reference_parser/commit_range_parser.rb b/lib/banzai/reference_parser/commit_range_parser.rb
index 0878b6afba3..a50e6f8ef8f 100644
--- a/lib/banzai/reference_parser/commit_range_parser.rb
+++ b/lib/banzai/reference_parser/commit_range_parser.rb
@@ -36,7 +36,7 @@ module Banzai
private
- def can_read_reference?(user, ref_project)
+ def can_read_reference?(user, ref_project, node)
can?(user, :download_code, ref_project)
end
end
diff --git a/lib/banzai/reference_parser/external_issue_parser.rb b/lib/banzai/reference_parser/external_issue_parser.rb
index 6e7b7669578..6307c1b571a 100644
--- a/lib/banzai/reference_parser/external_issue_parser.rb
+++ b/lib/banzai/reference_parser/external_issue_parser.rb
@@ -23,7 +23,7 @@ module Banzai
private
- def can_read_reference?(user, ref_project)
+ def can_read_reference?(user, ref_project, node)
can?(user, :read_issue, ref_project)
end
end
diff --git a/lib/banzai/reference_parser/label_parser.rb b/lib/banzai/reference_parser/label_parser.rb
index aa76c64ac5f..30e2a012f09 100644
--- a/lib/banzai/reference_parser/label_parser.rb
+++ b/lib/banzai/reference_parser/label_parser.rb
@@ -9,7 +9,7 @@ module Banzai
private
- def can_read_reference?(user, ref_project)
+ def can_read_reference?(user, ref_project, node)
can?(user, :read_label, ref_project)
end
end
diff --git a/lib/banzai/reference_parser/merge_request_parser.rb b/lib/banzai/reference_parser/merge_request_parser.rb
index 8b0662749fd..75cbc7fdac4 100644
--- a/lib/banzai/reference_parser/merge_request_parser.rb
+++ b/lib/banzai/reference_parser/merge_request_parser.rb
@@ -40,6 +40,10 @@ module Banzai
self.class.data_attribute
)
end
+
+ def can_read_reference?(user, ref_project, node)
+ can?(user, :read_merge_request, ref_project)
+ end
end
end
end
diff --git a/lib/banzai/reference_parser/milestone_parser.rb b/lib/banzai/reference_parser/milestone_parser.rb
index d3968d6b229..68675abe22a 100644
--- a/lib/banzai/reference_parser/milestone_parser.rb
+++ b/lib/banzai/reference_parser/milestone_parser.rb
@@ -9,7 +9,7 @@ module Banzai
private
- def can_read_reference?(user, ref_project)
+ def can_read_reference?(user, ref_project, node)
can?(user, :read_milestone, ref_project)
end
end
diff --git a/lib/banzai/reference_parser/snippet_parser.rb b/lib/banzai/reference_parser/snippet_parser.rb
index 63b592137bb..3ade168b566 100644
--- a/lib/banzai/reference_parser/snippet_parser.rb
+++ b/lib/banzai/reference_parser/snippet_parser.rb
@@ -9,8 +9,8 @@ module Banzai
private
- def can_read_reference?(user, ref_project)
- can?(user, :read_project_snippet, ref_project)
+ def can_read_reference?(user, ref_project, node)
+ can?(user, :read_project_snippet, referenced_by([node]).first)
end
end
end
diff --git a/lib/banzai/reference_parser/user_parser.rb b/lib/banzai/reference_parser/user_parser.rb
index 09b66cbd8fb..3efbd2fd631 100644
--- a/lib/banzai/reference_parser/user_parser.rb
+++ b/lib/banzai/reference_parser/user_parser.rb
@@ -103,7 +103,7 @@ module Banzai
flat_map { |p| p.team.members.to_a }
end
- def can_read_reference?(user, ref_project)
+ def can_read_reference?(user, ref_project, node)
can?(user, :read_project, ref_project)
end
end
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index c3064163e07..11f7c8b9510 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -1,9 +1,11 @@
require_dependency 'gitlab/git'
module Gitlab
+ COM_URL = 'https://gitlab.com'.freeze
+
def self.com?
# Check `staging?` as well to keep parity with gitlab.com
- Gitlab.config.gitlab.url == 'https://gitlab.com' || staging?
+ Gitlab.config.gitlab.url == COM_URL || staging?
end
def self.staging?
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index da07ba2f2a3..3933c3b04dd 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -37,7 +37,11 @@ module Gitlab
rate_limit!(ip, success: result.success?, login: login)
Gitlab::Auth::UniqueIpsLimiter.limit_user!(result.actor)
- result
+ return result if result.success? || current_application_settings.signin_enabled? || Gitlab::LDAP::Config.enabled?
+
+ # If sign-in is disabled and LDAP is not configured, recommend a
+ # personal access token on failed auth attempts
+ raise Gitlab::Auth::MissingPersonalTokenError
end
def find_with_user_password(login, password)
diff --git a/lib/gitlab/background_migration.rb b/lib/gitlab/background_migration.rb
new file mode 100644
index 00000000000..914a3b72abd
--- /dev/null
+++ b/lib/gitlab/background_migration.rb
@@ -0,0 +1,31 @@
+module Gitlab
+ module BackgroundMigration
+ # Begins stealing jobs from the background migrations queue, blocking the
+ # caller until all jobs have been completed.
+ #
+ # steal_class - The name of the class for which to steal jobs.
+ def self.steal(steal_class)
+ queue = Sidekiq::Queue.
+ new(BackgroundMigrationWorker.sidekiq_options['queue'])
+
+ queue.each do |job|
+ migration_class, migration_args = job.args
+
+ next unless migration_class == steal_class
+
+ perform(migration_class, migration_args)
+
+ job.delete
+ end
+ end
+
+ # class_name - The name of the background migration class as defined in the
+ # Gitlab::BackgroundMigration namespace.
+ #
+ # arguments - The arguments to pass to the background migration's "perform"
+ # method.
+ def self.perform(class_name, arguments)
+ const_get(class_name).new.perform(*arguments)
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/.gitkeep b/lib/gitlab/background_migration/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/lib/gitlab/background_migration/.gitkeep
diff --git a/lib/gitlab/blame.rb b/lib/gitlab/blame.rb
index d62bc50ce78..169aac79854 100644
--- a/lib/gitlab/blame.rb
+++ b/lib/gitlab/blame.rb
@@ -40,7 +40,7 @@ module Gitlab
end
def highlighted_lines
- @blob.load_all_data!(repository)
+ @blob.load_all_data!
@highlighted_lines ||=
Gitlab::Highlight.highlight(@blob.path, @blob.data, repository: repository).lines
end
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index 2aef7fdaa35..4212a0dbe2e 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -58,19 +58,19 @@ module Gitlab
diff_refs&.head_sha
end
- def content_sha
- return old_content_sha if deleted_file?
- return @content_sha if defined?(@content_sha)
+ def new_content_sha
+ return if deleted_file?
+ return @new_content_sha if defined?(@new_content_sha)
refs = diff_refs || fallback_diff_refs
- @content_sha = refs&.head_sha
+ @new_content_sha = refs&.head_sha
end
- def content_commit
- return @content_commit if defined?(@content_commit)
+ def new_content_commit
+ return @new_content_commit if defined?(@new_content_commit)
- sha = content_sha
- @content_commit = repository.commit(sha) if sha
+ sha = new_content_commit
+ @new_content_commit = repository.commit(sha) if sha
end
def old_content_sha
@@ -88,13 +88,13 @@ module Gitlab
@old_content_commit = repository.commit(sha) if sha
end
- def blob
- return @blob if defined?(@blob)
+ def new_blob
+ return @new_blob if defined?(@new_blob)
- sha = content_sha
- return @blob = nil unless sha
+ sha = new_content_sha
+ return @new_blob = nil unless sha
- repository.blob_at(sha, file_path)
+ @new_blob = repository.blob_at(sha, file_path)
end
def old_blob
@@ -106,6 +106,18 @@ module Gitlab
@old_blob = repository.blob_at(sha, old_path)
end
+ def content_sha
+ new_content_sha || old_content_sha
+ end
+
+ def content_commit
+ new_content_commit || old_content_commit
+ end
+
+ def blob
+ new_blob || old_blob
+ end
+
attr_writer :highlighted_diff_lines
# Array of Gitlab::Diff::Line objects
@@ -153,6 +165,18 @@ module Gitlab
def file_identifier
"#{file_path}-#{new_file?}-#{deleted_file?}-#{renamed_file?}"
end
+
+ def diffable?
+ repository.attributes(file_path).fetch('diff') { true }
+ end
+
+ def binary?
+ old_blob&.binary? || new_blob&.binary?
+ end
+
+ def text?
+ !binary?
+ end
end
end
end
diff --git a/lib/gitlab/diff/file_collection/merge_request_diff.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb
index 9a58b500a2c..fcda1fe2233 100644
--- a/lib/gitlab/diff/file_collection/merge_request_diff.rb
+++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb
@@ -66,10 +66,7 @@ module Gitlab
end
def cacheable?(diff_file)
- @merge_request_diff.present? &&
- diff_file.blob &&
- diff_file.blob.text? &&
- @project.repository.diffable?(diff_file.blob)
+ @merge_request_diff.present? && diff_file.text? && diff_file.diffable?
end
def cache_key
diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb
index ed2f541977a..b669ee5b799 100644
--- a/lib/gitlab/diff/highlight.rb
+++ b/lib/gitlab/diff/highlight.rb
@@ -42,9 +42,9 @@ module Gitlab
rich_line =
if diff_line.unchanged? || diff_line.added?
- new_lines[diff_line.new_pos - 1]
+ new_lines[diff_line.new_pos - 1]&.html_safe
elsif diff_line.removed?
- old_lines[diff_line.old_pos - 1]
+ old_lines[diff_line.old_pos - 1]&.html_safe
end
# Only update text if line is found. This will prevent
@@ -60,13 +60,18 @@ module Gitlab
end
def old_lines
- return unless diff_file
- @old_lines ||= Gitlab::Highlight.highlight_lines(self.repository, diff_old_sha, diff_old_path)
+ @old_lines ||= highlighted_blob_lines(diff_file.old_blob)
end
def new_lines
- return unless diff_file
- @new_lines ||= Gitlab::Highlight.highlight_lines(self.repository, diff_new_sha, diff_new_path)
+ @new_lines ||= highlighted_blob_lines(diff_file.new_blob)
+ end
+
+ def highlighted_blob_lines(blob)
+ return [] unless blob
+
+ blob.load_all_data!
+ Gitlab::Highlight.highlight(blob.path, blob.data, repository: repository).lines
end
end
end
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index 38e27513281..6d326ee213a 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -76,13 +76,9 @@ module Gitlab
step(
"Generating the patch against origin/master in #{patch_path}",
- %w[git format-patch origin/master --stdout]
+ %W[git diff --binary origin/master > #{patch_path}]
) do |output, status|
- throw(:halt_check, :ko) unless status.zero?
-
- File.write(patch_path, output)
-
- throw(:halt_check, :ko) unless File.exist?(patch_path)
+ throw(:halt_check, :ko) unless status.zero? && File.exist?(patch_path)
end
end
@@ -296,7 +292,7 @@ module Gitlab
# In the CE repo
$ git fetch origin master
- $ git format-patch origin/master --stdout > #{ce_branch}.patch
+ $ git diff --binary origin/master > #{ce_branch}.patch
# In the EE repo
$ git fetch origin master
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index d60e607b02b..33a7624e303 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -123,6 +123,7 @@ module Gitlab
@loaded_all_data = true
@data = repository.lookup(id).content
@loaded_size = @data.bytesize
+ @binary = nil
end
def name
diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb
index 8926aa19925..88ad760bea3 100644
--- a/lib/gitlab/git/diff.rb
+++ b/lib/gitlab/git/diff.rb
@@ -23,6 +23,23 @@ module Gitlab
class << self
# The maximum size of a diff to display.
def size_limit
+ if RequestStore.active?
+ RequestStore['gitlab_git_diff_size_limit'] ||= find_size_limit
+ else
+ find_size_limit
+ end
+ end
+
+ # The maximum size before a diff is collapsed.
+ def collapse_limit
+ if RequestStore.active?
+ RequestStore['gitlab_git_diff_collapse_limit'] ||= find_collapse_limit
+ else
+ find_collapse_limit
+ end
+ end
+
+ def find_size_limit
if Feature.enabled?('gitlab_git_diff_size_limit_increase')
200.kilobytes
else
@@ -30,8 +47,7 @@ module Gitlab
end
end
- # The maximum size before a diff is collapsed.
- def collapse_limit
+ def find_collapse_limit
if Feature.enabled?('gitlab_git_diff_size_limit_increase')
100.kilobytes
else
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 9d6adbdb4ac..85695d0a4df 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -962,11 +962,6 @@ module Gitlab
end
end
- # Checks if the blob should be diffable according to its attributes
- def diffable?(blob)
- attributes(blob.path).fetch('diff') { blob.text? }
- end
-
# Returns the Git attributes for the given file path.
#
# See `Gitlab::Git::Attributes` for more information.
diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb
index 83bc230df3e..6b24da030df 100644
--- a/lib/gitlab/highlight.rb
+++ b/lib/gitlab/highlight.rb
@@ -5,14 +5,6 @@ module Gitlab
highlight(blob_content, continue: false, plain: plain)
end
- def self.highlight_lines(repository, ref, file_name)
- blob = repository.blob_at(ref, file_name)
- return [] unless blob
-
- blob.load_all_data!(repository)
- highlight(file_name, blob.data, repository: repository).lines.map!(&:html_safe)
- end
-
attr_reader :blob_name
def initialize(blob_name, blob_content, repository: nil)
diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb
index f7ac48f7dbd..328dd17e452 100644
--- a/lib/gitlab/i18n.rb
+++ b/lib/gitlab/i18n.rb
@@ -6,9 +6,11 @@ module Gitlab
'en' => 'English',
'es' => 'Español',
'de' => 'Deutsch',
+ 'pt_BR' => 'Português(Brasil)',
'zh_CN' => '简体中文',
'zh_HK' => '繁體中文(香港)',
- 'zh_TW' => '繁體中文(臺灣)'
+ 'zh_TW' => '繁體中文(臺灣)',
+ 'bg' => 'български'
}.freeze
def available_locales
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
index 9ff6829cd49..10eb99fb461 100644
--- a/lib/gitlab/path_regex.rb
+++ b/lib/gitlab/path_regex.rb
@@ -49,6 +49,7 @@ module Gitlab
sent_notifications
services
snippets
+ system
teams
u
unicorn_test
diff --git a/lib/gitlab/performance_bar.rb b/lib/gitlab/performance_bar.rb
new file mode 100644
index 00000000000..163a40ad306
--- /dev/null
+++ b/lib/gitlab/performance_bar.rb
@@ -0,0 +1,7 @@
+module Gitlab
+ module PerformanceBar
+ def self.enabled?
+ Feature.enabled?('gitlab_performance_bar')
+ end
+ end
+end
diff --git a/lib/gitlab/performance_bar/peek_performance_bar_with_rack_body.rb b/lib/gitlab/performance_bar/peek_performance_bar_with_rack_body.rb
new file mode 100644
index 00000000000..d939a6ea18d
--- /dev/null
+++ b/lib/gitlab/performance_bar/peek_performance_bar_with_rack_body.rb
@@ -0,0 +1,22 @@
+# This solves a bug with a X-Senfile header that wouldn't be set properly, see
+# https://github.com/peek/peek-performance_bar/pull/27
+module Gitlab
+ module PerformanceBar
+ module PeekPerformanceBarWithRackBody
+ def call(env)
+ @env = env
+ reset_stats
+
+ @total_requests += 1
+ first_request if @total_requests == 1
+
+ env['process.request_start'] = @start.to_f
+ env['process.total_requests'] = total_requests
+
+ status, headers, body = @app.call(env)
+ body = Rack::BodyProxy.new(body) { record_request }
+ [status, headers, body]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/performance_bar/peek_query_tracker.rb b/lib/gitlab/performance_bar/peek_query_tracker.rb
new file mode 100644
index 00000000000..7ab80f5ee0f
--- /dev/null
+++ b/lib/gitlab/performance_bar/peek_query_tracker.rb
@@ -0,0 +1,39 @@
+# Inspired by https://github.com/peek/peek-pg/blob/master/lib/peek/views/pg.rb
+module Gitlab
+ module PerformanceBar
+ module PeekQueryTracker
+ def sorted_queries
+ PEEK_DB_CLIENT.query_details.
+ sort { |a, b| b[:duration] <=> a[:duration] }
+ end
+
+ def results
+ super.merge(queries: sorted_queries)
+ end
+
+ private
+
+ def setup_subscribers
+ super
+
+ # Reset each counter when a new request starts
+ before_request do
+ PEEK_DB_CLIENT.query_details = []
+ end
+
+ subscribe('sql.active_record') do |_, start, finish, _, data|
+ if RequestStore.active? && RequestStore.store[:peek_enabled]
+ track_query(data[:sql].strip, data[:binds], start, finish)
+ end
+ end
+ end
+
+ def track_query(raw_query, bindings, start, finish)
+ query = Gitlab::Sherlock::Query.new(raw_query, start, finish)
+ query_info = { duration: '%.3f' % query.duration, sql: query.formatted_query }
+
+ PEEK_DB_CLIENT.query_details << query_info
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/uploads_transfer.rb b/lib/gitlab/uploads_transfer.rb
index 7d0c47c5361..b5f41240529 100644
--- a/lib/gitlab/uploads_transfer.rb
+++ b/lib/gitlab/uploads_transfer.rb
@@ -1,7 +1,7 @@
module Gitlab
class UploadsTransfer < ProjectTransfer
def root_dir
- File.join(CarrierWave.root, GitlabUploader.base_dir)
+ File.join(CarrierWave.root, FileUploader.base_dir)
end
end
end
diff --git a/lib/peek/rblineprof/custom_controller_helpers.rb b/lib/peek/rblineprof/custom_controller_helpers.rb
new file mode 100644
index 00000000000..99f9c2c9b04
--- /dev/null
+++ b/lib/peek/rblineprof/custom_controller_helpers.rb
@@ -0,0 +1,96 @@
+module Peek
+ module Rblineprof
+ module CustomControllerHelpers
+ extend ActiveSupport::Concern
+
+ # This will become useless once https://github.com/peek/peek-rblineprof/pull/5
+ # is merged
+ def pygmentize(file_name, code, lexer = nil)
+ if lexer.present?
+ Gitlab::Highlight.highlight(file_name, code)
+ else
+ "<pre>#{Rack::Utils.escape_html(code)}</pre>"
+ end
+ end
+
+ # rubocop:disable all
+ def inject_rblineprof
+ ret = nil
+ profile = lineprof(rblineprof_profiler_regex) do
+ ret = yield
+ end
+
+ if response.content_type =~ %r|text/html|
+ sort = params[:lineprofiler_sort]
+ mode = params[:lineprofiler_mode] || 'cpu'
+ min = (params[:lineprofiler_min] || 5).to_i * 1000
+ summary = params[:lineprofiler_summary]
+
+ # Sort each file by the longest calculated time
+ per_file = profile.map do |file, lines|
+ total, child, excl, total_cpu, child_cpu, excl_cpu = lines[0]
+
+ wall = summary == 'exclusive' ? excl : total
+ cpu = summary == 'exclusive' ? excl_cpu : total_cpu
+ idle = summary == 'exclusive' ? (excl - excl_cpu) : (total - total_cpu)
+
+ [
+ file, lines,
+ wall, cpu, idle,
+ sort == 'idle' ? idle : sort == 'cpu' ? cpu : wall
+ ]
+ end.sort_by{ |a,b,c,d,e,f| -f }
+
+ output = ''
+ per_file.each do |file_name, lines, file_wall, file_cpu, file_idle, file_sort|
+
+ output << "<div class='peek-rblineprof-file'><div class='heading'>"
+
+ show_src = file_sort > min
+ tmpl = show_src ? "<a href='#' class='js-lineprof-file'>%s</a>" : "%s"
+
+ if mode == 'cpu'
+ output << sprintf("<span class='duration'>% 8.1fms + % 8.1fms</span> #{tmpl}", file_cpu / 1000.0, file_idle / 1000.0, file_name.sub(Rails.root.to_s + '/', ''))
+ else
+ output << sprintf("<span class='duration'>% 8.1fms</span> #{tmpl}", file_wall/1000.0, file_name.sub(Rails.root.to_s + '/', ''))
+ end
+
+ output << "</div>" # .heading
+
+ next unless show_src
+
+ output << "<div class='data'>"
+ code = []
+ times = []
+ File.readlines(file_name).each_with_index do |line, i|
+ code << line
+ wall, cpu, calls = lines[i + 1]
+
+ if calls && calls > 0
+ if mode == 'cpu'
+ idle = wall - cpu
+ times << sprintf("% 8.1fms + % 8.1fms (% 5d)", cpu / 1000.0, idle / 1000.0, calls)
+ else
+ times << sprintf("% 8.1fms (% 5d)", wall / 1000.0, calls)
+ end
+ else
+ times << ' '
+ end
+ end
+ output << "<pre class='duration'>#{times.join("\n")}</pre>"
+ # The following line was changed from
+ # https://github.com/peek/peek-rblineprof/blob/8d3b7a283a27de2f40abda45974516693d882258/lib/peek/rblineprof/controller_helpers.rb#L125
+ # This will become useless once https://github.com/peek/peek-rblineprof/pull/16
+ # is merged and is implemented.
+ output << "<pre class='code highlight white'>#{pygmentize(file_name, code.join, 'ruby')}</pre>"
+ output << "</div></div>" # .data then .peek-rblineprof-file
+ end
+
+ response.body += "<div class='peek-rblineprof-modal' id='line-profile'>#{output}</div>".html_safe
+ end
+
+ ret
+ end
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 63c5e9b9c83..858f1cd7b34 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -336,12 +336,9 @@ namespace :gitlab do
########################
def check_initd_configured_correctly
- print "Init.d configured correctly? ... "
+ return if omnibus_gitlab?
- if omnibus_gitlab?
- puts 'skipped (omnibus-gitlab has no init script)'.color(:magenta)
- return
- end
+ print "Init.d configured correctly? ... "
path = "/etc/default/gitlab"
@@ -379,6 +376,8 @@ namespace :gitlab do
end
def check_mail_room_running
+ return if omnibus_gitlab?
+
print "MailRoom running? ... "
path = "/etc/default/gitlab"
diff --git a/locale/bg/gitlab.po b/locale/bg/gitlab.po
new file mode 100644
index 00000000000..e6caf83252d
--- /dev/null
+++ b/locale/bg/gitlab.po
@@ -0,0 +1,260 @@
+# Lyubomir Vasilev <lyubomirv@abv.bg>, 2017. #zanata
+msgid ""
+msgstr ""
+"Project-Id-Version: gitlab 1.0.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2017-05-04 19:24-0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"PO-Revision-Date: 2017-06-05 09:40-0400\n"
+"Last-Translator: Lyubomir Vasilev <lyubomirv@abv.bg>\n"
+"Language-Team: Bulgarian\n"
+"Language: bg\n"
+"X-Generator: Zanata 3.9.6\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+
+msgid "ByAuthor|by"
+msgstr "от"
+
+msgid "Commit"
+msgid_plural "Commits"
+msgstr[0] "Подаване"
+msgstr[1] "Подавания"
+
+msgid ""
+"Cycle Analytics gives an overview of how much time it takes to go from idea "
+"to production in your project."
+msgstr ""
+"Анализът на циклите дава общ поглед върху това колко време е нужно на една "
+"идея да се превърне в завършена функционалност в проекта."
+
+msgid "CycleAnalyticsStage|Code"
+msgstr "Програмиране"
+
+msgid "CycleAnalyticsStage|Issue"
+msgstr "Проблем"
+
+msgid "CycleAnalyticsStage|Plan"
+msgstr "Планиране"
+
+msgid "CycleAnalyticsStage|Production"
+msgstr "Издаване"
+
+msgid "CycleAnalyticsStage|Review"
+msgstr "Преглед и одобрение"
+
+msgid "CycleAnalyticsStage|Staging"
+msgstr "Подготовка за издаване"
+
+msgid "CycleAnalyticsStage|Test"
+msgstr "Тестване"
+
+msgid "Deploy"
+msgid_plural "Deploys"
+msgstr[0] "Внедряване"
+msgstr[1] "Внедрявания"
+
+msgid "FirstPushedBy|First"
+msgstr "Първо"
+
+msgid "FirstPushedBy|pushed by"
+msgstr "изпращане на промени от"
+
+msgid "From issue creation until deploy to production"
+msgstr "От създаването на проблема до внедряването в крайната версия"
+
+msgid "From merge request merge until deploy to production"
+msgstr ""
+"От прилагането на заявката за сливане до внедряването в крайната версия"
+
+msgid "Introducing Cycle Analytics"
+msgstr "Представяме Ви анализът на циклите"
+
+msgid "Last %d day"
+msgid_plural "Last %d days"
+msgstr[0] "Последния %d ден"
+msgstr[1] "Последните %d дни"
+
+msgid "Limited to showing %d event at most"
+msgid_plural "Limited to showing %d events at most"
+msgstr[0] "Ограничено до показване на последното %d събитие"
+msgstr[1] "Ограничено до показване на последните %d събития"
+
+msgid "Median"
+msgstr "Медиана"
+
+msgid "New Issue"
+msgid_plural "New Issues"
+msgstr[0] "Нов проблем"
+msgstr[1] "Нови проблема"
+
+msgid "Not available"
+msgstr "Не е налично"
+
+msgid "Not enough data"
+msgstr "Няма достатъчно данни"
+
+msgid "OpenedNDaysAgo|Opened"
+msgstr "Отворен"
+
+msgid "Pipeline Health"
+msgstr "Състояние"
+
+msgid "ProjectLifecycle|Stage"
+msgstr "Етап"
+
+msgid "Read more"
+msgstr "Прочетете повече"
+
+msgid "Related Commits"
+msgstr "Свързани подавания"
+
+msgid "Related Deployed Jobs"
+msgstr "Свързани задачи за внедряване"
+
+msgid "Related Issues"
+msgstr "Свързани проблеми"
+
+msgid "Related Jobs"
+msgstr "Свързани задачи"
+
+msgid "Related Merge Requests"
+msgstr "Свързани заявки за сливане"
+
+msgid "Related Merged Requests"
+msgstr "Свързани приложени заявки за сливане"
+
+msgid "Showing %d event"
+msgid_plural "Showing %d events"
+msgstr[0] "Показване на %d събитие"
+msgstr[1] "Показване на %d събития"
+
+msgid ""
+"The coding stage shows the time from the first commit to creating the merge "
+"request. The data will automatically be added here once you create your "
+"first merge request."
+msgstr ""
+"Етапът на програмиране показва времето от първото подаване до създаването на "
+"заявката за сливане. Данните ще бъдат добавени тук автоматично след като "
+"бъде създадена първата заявка за сливане."
+
+msgid "The collection of events added to the data gathered for that stage."
+msgstr "Съвкупността от събития добавени към данните събрани за този етап."
+
+msgid ""
+"The issue stage shows the time it takes from creating an issue to assigning "
+"the issue to a milestone, or add the issue to a list on your Issue Board. "
+"Begin creating issues to see data for this stage."
+msgstr ""
+"Етапът на проблемите показва колко е времето от създаването на проблем до "
+"определянето на целеви етап на проекта за него, или до добавянето му в "
+"списък на дъската за проблеми. Започнете да добавяте проблеми, за да видите "
+"данните за този етап."
+
+msgid "The phase of the development lifecycle."
+msgstr "Етапът от цикъла на разработка"
+
+msgid ""
+"The planning stage shows the time from the previous step to pushing your "
+"first commit. This time will be added automatically once you push your first "
+"commit."
+msgstr ""
+"Етапът на планиране показва колко е времето от преходната стъпка до "
+"изпращането на първото подаване. Това време ще бъде добавено автоматично "
+"след като изпратите първото си подаване."
+
+msgid ""
+"The production stage shows the total time it takes between creating an issue "
+"and deploying the code to production. The data will be automatically added "
+"once you have completed the full idea to production cycle."
+msgstr ""
+"Етапът на издаване показва общото време, което е нужно от създаването на "
+"проблем до внедряването на кода в крайната версия."
+
+msgid ""
+"The review stage shows the time from creating the merge request to merging "
+"it. The data will automatically be added after you merge your first merge "
+"request."
+msgstr ""
+"Етапът на преглед и одобрение показва времето от създаването на заявката за "
+"сливане до прилагането ѝ. Данните ще бъдат добавени автоматично след като "
+"приложите първата си заявка за сливане."
+
+msgid ""
+"The staging stage shows the time between merging the MR and deploying code "
+"to the production environment. The data will be automatically added once you "
+"deploy to production for the first time."
+msgstr ""
+"Етапът на подготовка за издаване показва времето между прилагането на "
+"заявката за сливане и внедряването на кода в средата на работещата крайна "
+"версия. Данните ще бъдат добавени автоматично след като направите първото си "
+"внедряване в крайната версия."
+
+msgid ""
+"The testing stage shows the time GitLab CI takes to run every pipeline for "
+"the related merge request. The data will automatically be added after your "
+"first pipeline finishes running."
+msgstr ""
+"Етапът на тестване показва времето, което е нужно на „Gitlab CI“ да изпълни "
+"всички задачи за свързаната заявка за сливане. Данните ще бъдат добавени "
+"автоматично след като приключи изпълнените на първата Ви такава задача."
+
+msgid "The time taken by each data entry gathered by that stage."
+msgstr "Времето, което отнема всеки запис от данни за съответния етап."
+
+msgid ""
+"The value lying at the midpoint of a series of observed values. E.g., "
+"between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 ="
+" 6."
+msgstr ""
+"Стойността, която се намира в средата на последователността от наблюдавани "
+"данни. Например: медианата на 3, 5 и 9 е 5, а медианата на 3, 5, 7 и 8 е "
+"(5+7)/2 = 6."
+
+msgid "Time before an issue gets scheduled"
+msgstr "Време преди един проблем да бъде планиран за работа"
+
+msgid "Time before an issue starts implementation"
+msgstr "Време преди работата по проблем да започне"
+
+msgid "Time between merge request creation and merge/close"
+msgstr ""
+"Време между създаване на заявка за сливане и прилагането/отхвърлянето ѝ"
+
+msgid "Time until first merge request"
+msgstr "Време преди първата заявка за сливане"
+
+msgid "Time|hr"
+msgid_plural "Time|hrs"
+msgstr[0] "час"
+msgstr[1] "часа"
+
+msgid "Time|min"
+msgid_plural "Time|mins"
+msgstr[0] "мин"
+msgstr[1] "мин"
+
+msgid "Time|s"
+msgstr "сек"
+
+msgid "Total Time"
+msgstr "Общо време"
+
+msgid "Total test time for all commits/merges"
+msgstr "Общо време за тестване на всички подавания/сливания"
+
+msgid "Want to see the data? Please ask an administrator for access."
+msgstr "Искате ли да видите данните? Помолете администратор за достъп."
+
+msgid "We don't have enough data to show this stage."
+msgstr "Няма достатъчно данни за този етап."
+
+msgid "You need permission."
+msgstr "Нуждаете се от разрешение."
+
+msgid "day"
+msgid_plural "days"
+msgstr[0] "ден"
+msgstr[1] "дни"
+
diff --git a/locale/bg/gitlab.po.time_stamp b/locale/bg/gitlab.po.time_stamp
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/locale/bg/gitlab.po.time_stamp
diff --git a/locale/de/gitlab.po b/locale/de/gitlab.po
index 60a0c219e00..9a660571db9 100644
--- a/locale/de/gitlab.po
+++ b/locale/de/gitlab.po
@@ -7,115 +7,23 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"PO-Revision-Date: 2017-06-07 12:17+0200\n"
+"PO-Revision-Date: 2017-05-09 13:44+0200\n"
"Language-Team: German\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
-"Last-Translator: Bob Van Landuyt <bob@gitlab.com>\n"
-"X-Generator: Poedit 2.0.2\n"
+"Last-Translator: \n"
+"X-Generator: Poedit 2.0.1\n"
-msgid "About auto deploy"
-msgstr ""
-
-msgid "Activity"
-msgstr ""
-
-msgid "Add Changelog"
-msgstr ""
-
-msgid "Add Contribution guide"
-msgstr ""
-
-msgid "Add License"
-msgstr ""
-
-msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr ""
-
-msgid "Add new directory"
-msgstr ""
-
-msgid "Archived project! Repository is read-only"
-msgstr ""
-
-msgid "Branch"
-msgid_plural "Branches"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
-msgstr ""
-
-msgid "Branches"
+msgid "Are you sure you want to delete this pipeline schedule?"
msgstr ""
msgid "ByAuthor|by"
msgstr "Von"
-msgid "CI configuration"
-msgstr ""
-
-msgid "Changelog"
-msgstr ""
-
-msgid "Charts"
-msgstr ""
-
-msgid "CiStatusLabel|canceled"
-msgstr ""
-
-msgid "CiStatusLabel|created"
-msgstr ""
-
-msgid "CiStatusLabel|failed"
-msgstr ""
-
-msgid "CiStatusLabel|manual action"
-msgstr ""
-
-msgid "CiStatusLabel|passed"
-msgstr ""
-
-msgid "CiStatusLabel|passed with warnings"
-msgstr ""
-
-msgid "CiStatusLabel|pending"
-msgstr ""
-
-msgid "CiStatusLabel|skipped"
-msgstr ""
-
-msgid "CiStatusLabel|waiting for manual action"
-msgstr ""
-
-msgid "CiStatusText|blocked"
-msgstr ""
-
-msgid "CiStatusText|canceled"
-msgstr ""
-
-msgid "CiStatusText|created"
-msgstr ""
-
-msgid "CiStatusText|failed"
-msgstr ""
-
-msgid "CiStatusText|manual"
-msgstr ""
-
-msgid "CiStatusText|passed"
-msgstr ""
-
-msgid "CiStatusText|pending"
-msgstr ""
-
-msgid "CiStatusText|skipped"
-msgstr ""
-
-msgid "CiStatus|running"
+msgid "Cancel"
msgstr ""
msgid "Commit"
@@ -123,52 +31,7 @@ msgid_plural "Commits"
msgstr[0] "Commit"
msgstr[1] "Commits"
-msgid "CommitMessage|Add %{file_name}"
-msgstr ""
-
-msgid "Commits"
-msgstr "Commits"
-
-msgid "Commits|History"
-msgstr "Commits"
-
-msgid "Compare"
-msgstr ""
-
-msgid "Contribution guide"
-msgstr ""
-
-msgid "Contributors"
-msgstr ""
-
-msgid "Copy URL to clipboard"
-msgstr ""
-
-msgid "Copy commit SHA to clipboard"
-msgstr ""
-
-msgid "Create New Directory"
-msgstr ""
-
-msgid "Create directory"
-msgstr ""
-
-msgid "Create empty bare repository"
-msgstr ""
-
-msgid "Create merge request"
-msgstr ""
-
-msgid "CreateNewFork|Fork"
-msgstr ""
-
-msgid "Custom notification events"
-msgstr ""
-
-msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}."
-msgstr ""
-
-msgid "Cycle Analytics"
+msgid "Cron Timezone"
msgstr ""
msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project."
@@ -195,42 +58,30 @@ msgstr "Staging"
msgid "CycleAnalyticsStage|Test"
msgstr "Test"
+msgid "Delete"
+msgstr ""
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "Deployment"
msgstr[1] "Deployments"
-msgid "Directory name"
-msgstr ""
-
-msgid "Don't show again"
-msgstr ""
-
-msgid "Download tar"
-msgstr ""
-
-msgid "Download tar.bz2"
-msgstr ""
-
-msgid "Download tar.gz"
-msgstr ""
-
-msgid "Download zip"
+msgid "Description"
msgstr ""
-msgid "DownloadArtifacts|Download"
+msgid "Edit"
msgstr ""
-msgid "DownloadSource|Download"
+msgid "Edit Pipeline Schedule %{id}"
msgstr ""
-msgid "Files"
+msgid "Failed to change the owner"
msgstr ""
-msgid "Find by path"
+msgid "Failed to remove the pipeline schedule"
msgstr ""
-msgid "Find file"
+msgid "Filter"
msgstr ""
msgid "FirstPushedBy|First"
@@ -239,57 +90,24 @@ msgstr "Erster"
msgid "FirstPushedBy|pushed by"
msgstr "gepusht von"
-msgid "ForkedFromProjectPath|Forked from"
-msgstr ""
-
-msgid "Forks"
-msgstr ""
-
msgid "From issue creation until deploy to production"
msgstr "Vom Anlegen des Issues bis zum Produktivdeployment"
msgid "From merge request merge until deploy to production"
msgstr "Vom Merge Request bis zum Produktivdeployment"
-msgid "Go to your fork"
-msgstr ""
-
-msgid "GoToYourFork|Fork"
-msgstr ""
-
-msgid "Home"
-msgstr ""
-
-msgid "Housekeeping successfully started"
-msgstr ""
-
-msgid "Import repository"
+msgid "Interval Pattern"
msgstr ""
msgid "Introducing Cycle Analytics"
msgstr "Was sind Cycle Analytics?"
-msgid "LFSStatus|Disabled"
-msgstr ""
-
-msgid "LFSStatus|Enabled"
-msgstr ""
-
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] "Letzter %d Tag"
msgstr[1] "Letzten %d Tage"
-msgid "Last Update"
-msgstr ""
-
-msgid "Last commit"
-msgstr ""
-
-msgid "Leave group"
-msgstr ""
-
-msgid "Leave project"
+msgid "Last Pipeline"
msgstr ""
msgid "Limited to showing %d event at most"
@@ -300,36 +118,15 @@ msgstr[1] "Eingeschränkt auf maximal %d Ereignisse"
msgid "Median"
msgstr "Median"
-msgid "MissingSSHKeyWarningLink|add an SSH key"
-msgstr ""
-
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Neues Issue"
msgstr[1] "Neue Issues"
-msgid "New branch"
-msgstr ""
-
-msgid "New directory"
-msgstr ""
-
-msgid "New file"
-msgstr ""
-
-msgid "New issue"
-msgstr "Neues Issue"
-
-msgid "New merge request"
+msgid "New Pipeline Schedule"
msgstr ""
-msgid "New snippet"
-msgstr ""
-
-msgid "New tag"
-msgstr ""
-
-msgid "No repository"
+msgid "No schedules"
msgstr ""
msgid "Not available"
@@ -338,129 +135,54 @@ msgstr "Nicht verfügbar"
msgid "Not enough data"
msgstr "Nicht genügend Daten"
-msgid "Notification events"
-msgstr ""
-
-msgid "NotificationEvent|Close issue"
-msgstr ""
-
-msgid "NotificationEvent|Close merge request"
-msgstr ""
-
-msgid "NotificationEvent|Failed pipeline"
-msgstr ""
-
-msgid "NotificationEvent|Merge merge request"
-msgstr ""
-
-msgid "NotificationEvent|New issue"
-msgstr ""
-
-msgid "NotificationEvent|New merge request"
-msgstr ""
-
-msgid "NotificationEvent|New note"
-msgstr ""
-
-msgid "NotificationEvent|Reassign issue"
-msgstr ""
-
-msgid "NotificationEvent|Reassign merge request"
-msgstr ""
-
-msgid "NotificationEvent|Reopen issue"
-msgstr ""
-
-msgid "NotificationEvent|Successful pipeline"
-msgstr ""
-
-msgid "NotificationLevel|Custom"
-msgstr ""
-
-msgid "NotificationLevel|Disabled"
-msgstr ""
-
-msgid "NotificationLevel|Global"
-msgstr ""
-
-msgid "NotificationLevel|On mention"
-msgstr ""
-
-msgid "NotificationLevel|Participate"
-msgstr ""
-
-msgid "NotificationLevel|Watch"
-msgstr ""
-
msgid "OpenedNDaysAgo|Opened"
msgstr "Erstellt"
-msgid "Pipeline Health"
-msgstr "Pipeline Kennzahlen"
-
-msgid "Project '%{project_name}' queued for deletion."
+msgid "Owner"
msgstr ""
-msgid "Project '%{project_name}' was successfully created."
-msgstr ""
-
-msgid "Project '%{project_name}' was successfully updated."
-msgstr ""
-
-msgid "Project '%{project_name}' will be deleted."
-msgstr ""
+msgid "Pipeline Health"
+msgstr "Pipeline Kennzahlen"
-msgid "Project access must be granted explicitly to each user."
+msgid "Pipeline Schedule"
msgstr ""
-msgid "Project export could not be deleted."
+msgid "Pipeline Schedules"
msgstr ""
-msgid "Project export has been deleted."
+msgid "PipelineSchedules|Activated"
msgstr ""
-msgid "Project export link has expired. Please generate a new export from your project settings."
+msgid "PipelineSchedules|Active"
msgstr ""
-msgid "Project export started. A download link will be sent by email."
+msgid "PipelineSchedules|All"
msgstr ""
-msgid "Project home"
+msgid "PipelineSchedules|Inactive"
msgstr ""
-msgid "ProjectFeature|Disabled"
+msgid "PipelineSchedules|Next Run"
msgstr ""
-msgid "ProjectFeature|Everyone with access"
+msgid "PipelineSchedules|None"
msgstr ""
-msgid "ProjectFeature|Only team members"
+msgid "PipelineSchedules|Provide a short description for this pipeline"
msgstr ""
-msgid "ProjectFileTree|Name"
+msgid "PipelineSchedules|Take ownership"
msgstr ""
-msgid "ProjectLastActivity|Never"
+msgid "PipelineSchedules|Target"
msgstr ""
msgid "ProjectLifecycle|Stage"
msgstr "Phase"
-msgid "ProjectNetworkGraph|Graph"
-msgstr ""
-
msgid "Read more"
msgstr "Mehr"
-msgid "Readme"
-msgstr ""
-
-msgid "RefSwitcher|Branches"
-msgstr ""
-
-msgid "RefSwitcher|Tags"
-msgstr ""
-
msgid "Related Commits"
msgstr "Zugehörige Commits"
@@ -479,34 +201,16 @@ msgstr "Zugehörige Merge Requests"
msgid "Related Merged Requests"
msgstr "Zugehörige abgeschlossene Merge Requests"
-msgid "Remind later"
-msgstr ""
-
-msgid "Remove project"
-msgstr ""
-
-msgid "Request Access"
-msgstr ""
-
-msgid "Search branches and tags"
-msgstr ""
-
-msgid "Select Archive Format"
-msgstr ""
-
-msgid "Set a password on your account to pull or push via %{protocol}"
+msgid "Save pipeline schedule"
msgstr ""
-msgid "Set up CI"
+msgid "Schedule a new pipeline"
msgstr ""
-msgid "Set up Koding"
+msgid "Select a timezone"
msgstr ""
-msgid "Set up auto deploy"
-msgstr ""
-
-msgid "SetPasswordToCloneLink|set a password"
+msgid "Select target branch"
msgstr ""
msgid "Showing %d event"
@@ -514,21 +218,7 @@ msgid_plural "Showing %d events"
msgstr[0] "Zeige %d Ereignis"
msgstr[1] "Zeige %d Ereignisse"
-msgid "Source code"
-msgstr ""
-
-msgid "StarProject|Star"
-msgstr ""
-
-msgid "Switch branch/tag"
-msgstr ""
-
-msgid "Tag"
-msgid_plural "Tags"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "Tags"
+msgid "Target Branch"
msgstr ""
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
@@ -537,9 +227,6 @@ msgstr "Die Code-Phase stellt die Zeit vom ersten Commit bis zum Erstellen eines
msgid "The collection of events added to the data gathered for that stage."
msgstr "Ereignisse, die für diese Phase ausgewertet wurden."
-msgid "The fork relationship has been removed."
-msgstr ""
-
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr "Die Issue-Phase stellt die Zeit vom Anlegen eines Issues bis zum Zuweisen eines Meilensteins oder Hinzufügen zum Issue Board dar. Erstellen Sie einen Issue, damit dessen Daten hier erscheinen."
@@ -552,15 +239,6 @@ msgstr "Die Planungsphase stellt die Zeit von der vorherigen Phase bis zum Pushe
msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle."
msgstr "Die Produktiv-Phase stellt die Gesamtzeit vom Anlegen eines Issues bis zum Deployment auf dem Produktivsystem dar. Sobald Sie den vollständigen Entwicklungszyklus von einer Idee bis zum Produktivdeployment durchlaufen haben, erscheinen die zugehörigen Daten hier."
-msgid "The project can be accessed by any logged in user."
-msgstr ""
-
-msgid "The project can be accessed without any authentication."
-msgstr ""
-
-msgid "The repository for this project does not exist."
-msgstr ""
-
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr "Die Review-Phase stellt die Zeit vom Anlegen eines Merge Requests bis zum Mergen dar. Sobald Sie Ihren ersten Merge Request abschließen, werden dessen Daten hier automatisch angezeigt."
@@ -576,9 +254,6 @@ msgstr "Zeit die für das jeweilige Ereignis in der Phase ermittelt wurde."
msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6."
msgstr "Der mittlere aller erfassten Werte. Zum Beispiel ist für 3, 5, 9 der Median 5. Bei 3, 5, 7, 8 ist der Median (5+7)/2 = 6."
-msgid "This means you can not push code until you create an empty repository or import existing one."
-msgstr ""
-
msgid "Time before an issue gets scheduled"
msgstr "Zeit bis ein Issue geplant wird"
@@ -591,129 +266,6 @@ msgstr "Zeit zwischen Anlegen und Mergen/Schließen eines Merge Requests"
msgid "Time until first merge request"
msgstr "Zeit bis zum ersten Merge Request"
-msgid "Timeago|%s days ago"
-msgstr ""
-
-msgid "Timeago|%s days remaining"
-msgstr ""
-
-msgid "Timeago|%s hours remaining"
-msgstr ""
-
-msgid "Timeago|%s minutes ago"
-msgstr ""
-
-msgid "Timeago|%s minutes remaining"
-msgstr ""
-
-msgid "Timeago|%s months ago"
-msgstr ""
-
-msgid "Timeago|%s months remaining"
-msgstr ""
-
-msgid "Timeago|%s seconds remaining"
-msgstr ""
-
-msgid "Timeago|%s weeks ago"
-msgstr ""
-
-msgid "Timeago|%s weeks remaining"
-msgstr ""
-
-msgid "Timeago|%s years ago"
-msgstr ""
-
-msgid "Timeago|%s years remaining"
-msgstr ""
-
-msgid "Timeago|1 day remaining"
-msgstr ""
-
-msgid "Timeago|1 hour remaining"
-msgstr ""
-
-msgid "Timeago|1 minute remaining"
-msgstr ""
-
-msgid "Timeago|1 month remaining"
-msgstr ""
-
-msgid "Timeago|1 week remaining"
-msgstr ""
-
-msgid "Timeago|1 year remaining"
-msgstr ""
-
-msgid "Timeago|Past due"
-msgstr ""
-
-msgid "Timeago|a day ago"
-msgstr ""
-
-msgid "Timeago|a month ago"
-msgstr ""
-
-msgid "Timeago|a week ago"
-msgstr ""
-
-msgid "Timeago|a while"
-msgstr ""
-
-msgid "Timeago|a year ago"
-msgstr ""
-
-msgid "Timeago|about %s hours ago"
-msgstr ""
-
-msgid "Timeago|about a minute ago"
-msgstr ""
-
-msgid "Timeago|about an hour ago"
-msgstr ""
-
-msgid "Timeago|in %s days"
-msgstr ""
-
-msgid "Timeago|in %s hours"
-msgstr ""
-
-msgid "Timeago|in %s minutes"
-msgstr ""
-
-msgid "Timeago|in %s months"
-msgstr ""
-
-msgid "Timeago|in %s seconds"
-msgstr ""
-
-msgid "Timeago|in %s weeks"
-msgstr ""
-
-msgid "Timeago|in %s years"
-msgstr ""
-
-msgid "Timeago|in 1 day"
-msgstr ""
-
-msgid "Timeago|in 1 hour"
-msgstr ""
-
-msgid "Timeago|in 1 minute"
-msgstr ""
-
-msgid "Timeago|in 1 month"
-msgstr ""
-
-msgid "Timeago|in 1 week"
-msgstr ""
-
-msgid "Timeago|in 1 year"
-msgstr ""
-
-msgid "Timeago|less than a minute ago"
-msgstr ""
-
msgid "Time|hr"
msgid_plural "Time|hrs"
msgstr[0] "h"
@@ -733,88 +285,16 @@ msgstr "Gesamtzeit"
msgid "Total test time for all commits/merges"
msgstr "Gesamte Testlaufzeit für alle Commits/Merges"
-msgid "Unstar"
-msgstr ""
-
-msgid "Upload New File"
-msgstr ""
-
-msgid "Upload file"
-msgstr ""
-
-msgid "Use your global notification setting"
-msgstr ""
-
-msgid "VisibilityLevel|Internal"
-msgstr ""
-
-msgid "VisibilityLevel|Private"
-msgstr ""
-
-msgid "VisibilityLevel|Public"
-msgstr ""
-
msgid "Want to see the data? Please ask an administrator for access."
msgstr "Um diese Daten einsehen zu können, wenden Sie sich bitte an Ihren Administrator."
msgid "We don't have enough data to show this stage."
msgstr "Es liegen nicht genügend Daten vor, um diese Phase anzuzeigen."
-msgid "Withdraw Access Request"
-msgstr ""
-
-msgid ""
-"You are going to remove %{project_name_with_namespace}.\n"
-"Removed project CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr ""
-
-msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
-msgstr ""
-
-msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
-msgstr ""
-
-msgid "You can only add files when you are on a branch"
-msgstr ""
-
-msgid "You must sign in to star a project"
-msgstr ""
-
msgid "You need permission."
msgstr "Sie benötigen Zugriffsrechte."
-msgid "You will not get any notifications via email"
-msgstr ""
-
-msgid "You will only receive notifications for the events you choose"
-msgstr ""
-
-msgid "You will only receive notifications for threads you have participated in"
-msgstr ""
-
-msgid "You will receive notifications for any activity"
-msgstr ""
-
-msgid "You will receive notifications only for comments in which you were @mentioned"
-msgstr ""
-
-msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
-msgstr ""
-
-msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
-msgstr ""
-
-msgid "Your name"
-msgstr ""
-
-msgid "committed"
-msgstr ""
-
msgid "day"
msgid_plural "days"
msgstr[0] "Tag"
msgstr[1] "Tage"
-
-msgid "notification emails"
-msgstr ""
diff --git a/locale/en/gitlab.po b/locale/en/gitlab.po
index fca2bc15b1f..4e44731fc5a 100644
--- a/locale/en/gitlab.po
+++ b/locale/en/gitlab.po
@@ -7,115 +7,23 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"PO-Revision-Date: 2017-06-07 12:14+0200\n"
+"PO-Revision-Date: 2017-04-12 22:36-0500\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: English\n"
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
-"Last-Translator: Bob Van Landuyt <bob@gitlab.com>\n"
-"X-Generator: Poedit 2.0.2\n"
+"\n"
-msgid "About auto deploy"
-msgstr ""
-
-msgid "Activity"
-msgstr ""
-
-msgid "Add Changelog"
-msgstr ""
-
-msgid "Add Contribution guide"
-msgstr ""
-
-msgid "Add License"
-msgstr ""
-
-msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr ""
-
-msgid "Add new directory"
-msgstr ""
-
-msgid "Archived project! Repository is read-only"
-msgstr ""
-
-msgid "Branch"
-msgid_plural "Branches"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
-msgstr ""
-
-msgid "Branches"
+msgid "Are you sure you want to delete this pipeline schedule?"
msgstr ""
msgid "ByAuthor|by"
msgstr ""
-msgid "CI configuration"
-msgstr ""
-
-msgid "Changelog"
-msgstr ""
-
-msgid "Charts"
-msgstr ""
-
-msgid "CiStatusLabel|canceled"
-msgstr ""
-
-msgid "CiStatusLabel|created"
-msgstr ""
-
-msgid "CiStatusLabel|failed"
-msgstr ""
-
-msgid "CiStatusLabel|manual action"
-msgstr ""
-
-msgid "CiStatusLabel|passed"
-msgstr ""
-
-msgid "CiStatusLabel|passed with warnings"
-msgstr ""
-
-msgid "CiStatusLabel|pending"
-msgstr ""
-
-msgid "CiStatusLabel|skipped"
-msgstr ""
-
-msgid "CiStatusLabel|waiting for manual action"
-msgstr ""
-
-msgid "CiStatusText|blocked"
-msgstr ""
-
-msgid "CiStatusText|canceled"
-msgstr ""
-
-msgid "CiStatusText|created"
-msgstr ""
-
-msgid "CiStatusText|failed"
-msgstr ""
-
-msgid "CiStatusText|manual"
-msgstr ""
-
-msgid "CiStatusText|passed"
-msgstr ""
-
-msgid "CiStatusText|pending"
-msgstr ""
-
-msgid "CiStatusText|skipped"
-msgstr ""
-
-msgid "CiStatus|running"
+msgid "Cancel"
msgstr ""
msgid "Commit"
@@ -123,52 +31,7 @@ msgid_plural "Commits"
msgstr[0] ""
msgstr[1] ""
-msgid "CommitMessage|Add %{file_name}"
-msgstr ""
-
-msgid "Commits"
-msgstr ""
-
-msgid "Commits|History"
-msgstr ""
-
-msgid "Compare"
-msgstr ""
-
-msgid "Contribution guide"
-msgstr ""
-
-msgid "Contributors"
-msgstr ""
-
-msgid "Copy URL to clipboard"
-msgstr ""
-
-msgid "Copy commit SHA to clipboard"
-msgstr ""
-
-msgid "Create New Directory"
-msgstr ""
-
-msgid "Create directory"
-msgstr ""
-
-msgid "Create empty bare repository"
-msgstr ""
-
-msgid "Create merge request"
-msgstr ""
-
-msgid "CreateNewFork|Fork"
-msgstr ""
-
-msgid "Custom notification events"
-msgstr ""
-
-msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}."
-msgstr ""
-
-msgid "Cycle Analytics"
+msgid "Cron Timezone"
msgstr ""
msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project."
@@ -195,42 +58,30 @@ msgstr ""
msgid "CycleAnalyticsStage|Test"
msgstr ""
+msgid "Delete"
+msgstr ""
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] ""
msgstr[1] ""
-msgid "Directory name"
-msgstr ""
-
-msgid "Don't show again"
-msgstr ""
-
-msgid "Download tar"
-msgstr ""
-
-msgid "Download tar.bz2"
+msgid "Description"
msgstr ""
-msgid "Download tar.gz"
+msgid "Edit"
msgstr ""
-msgid "Download zip"
+msgid "Edit Pipeline Schedule %{id}"
msgstr ""
-msgid "DownloadArtifacts|Download"
+msgid "Failed to change the owner"
msgstr ""
-msgid "DownloadSource|Download"
+msgid "Failed to remove the pipeline schedule"
msgstr ""
-msgid "Files"
-msgstr ""
-
-msgid "Find by path"
-msgstr ""
-
-msgid "Find file"
+msgid "Filter"
msgstr ""
msgid "FirstPushedBy|First"
@@ -239,57 +90,24 @@ msgstr ""
msgid "FirstPushedBy|pushed by"
msgstr ""
-msgid "ForkedFromProjectPath|Forked from"
-msgstr ""
-
-msgid "Forks"
-msgstr ""
-
msgid "From issue creation until deploy to production"
msgstr ""
msgid "From merge request merge until deploy to production"
msgstr ""
-msgid "Go to your fork"
-msgstr ""
-
-msgid "GoToYourFork|Fork"
-msgstr ""
-
-msgid "Home"
-msgstr ""
-
-msgid "Housekeeping successfully started"
-msgstr ""
-
-msgid "Import repository"
+msgid "Interval Pattern"
msgstr ""
msgid "Introducing Cycle Analytics"
msgstr ""
-msgid "LFSStatus|Disabled"
-msgstr ""
-
-msgid "LFSStatus|Enabled"
-msgstr ""
-
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] ""
msgstr[1] ""
-msgid "Last Update"
-msgstr ""
-
-msgid "Last commit"
-msgstr ""
-
-msgid "Leave group"
-msgstr ""
-
-msgid "Leave project"
+msgid "Last Pipeline"
msgstr ""
msgid "Limited to showing %d event at most"
@@ -300,36 +118,15 @@ msgstr[1] ""
msgid "Median"
msgstr ""
-msgid "MissingSSHKeyWarningLink|add an SSH key"
-msgstr ""
-
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] ""
msgstr[1] ""
-msgid "New branch"
-msgstr ""
-
-msgid "New directory"
-msgstr ""
-
-msgid "New file"
-msgstr ""
-
-msgid "New issue"
-msgstr ""
-
-msgid "New merge request"
+msgid "New Pipeline Schedule"
msgstr ""
-msgid "New snippet"
-msgstr ""
-
-msgid "New tag"
-msgstr ""
-
-msgid "No repository"
+msgid "No schedules"
msgstr ""
msgid "Not available"
@@ -338,129 +135,54 @@ msgstr ""
msgid "Not enough data"
msgstr ""
-msgid "Notification events"
-msgstr ""
-
-msgid "NotificationEvent|Close issue"
-msgstr ""
-
-msgid "NotificationEvent|Close merge request"
-msgstr ""
-
-msgid "NotificationEvent|Failed pipeline"
-msgstr ""
-
-msgid "NotificationEvent|Merge merge request"
-msgstr ""
-
-msgid "NotificationEvent|New issue"
-msgstr ""
-
-msgid "NotificationEvent|New merge request"
-msgstr ""
-
-msgid "NotificationEvent|New note"
-msgstr ""
-
-msgid "NotificationEvent|Reassign issue"
-msgstr ""
-
-msgid "NotificationEvent|Reassign merge request"
-msgstr ""
-
-msgid "NotificationEvent|Reopen issue"
-msgstr ""
-
-msgid "NotificationEvent|Successful pipeline"
-msgstr ""
-
-msgid "NotificationLevel|Custom"
-msgstr ""
-
-msgid "NotificationLevel|Disabled"
-msgstr ""
-
-msgid "NotificationLevel|Global"
-msgstr ""
-
-msgid "NotificationLevel|On mention"
-msgstr ""
-
-msgid "NotificationLevel|Participate"
-msgstr ""
-
-msgid "NotificationLevel|Watch"
-msgstr ""
-
msgid "OpenedNDaysAgo|Opened"
msgstr ""
-msgid "Pipeline Health"
+msgid "Owner"
msgstr ""
-msgid "Project '%{project_name}' queued for deletion."
-msgstr ""
-
-msgid "Project '%{project_name}' was successfully created."
-msgstr ""
-
-msgid "Project '%{project_name}' was successfully updated."
-msgstr ""
-
-msgid "Project '%{project_name}' will be deleted."
+msgid "Pipeline Health"
msgstr ""
-msgid "Project access must be granted explicitly to each user."
+msgid "Pipeline Schedule"
msgstr ""
-msgid "Project export could not be deleted."
+msgid "Pipeline Schedules"
msgstr ""
-msgid "Project export has been deleted."
+msgid "PipelineSchedules|Activated"
msgstr ""
-msgid "Project export link has expired. Please generate a new export from your project settings."
+msgid "PipelineSchedules|Active"
msgstr ""
-msgid "Project export started. A download link will be sent by email."
+msgid "PipelineSchedules|All"
msgstr ""
-msgid "Project home"
+msgid "PipelineSchedules|Inactive"
msgstr ""
-msgid "ProjectFeature|Disabled"
+msgid "PipelineSchedules|Next Run"
msgstr ""
-msgid "ProjectFeature|Everyone with access"
+msgid "PipelineSchedules|None"
msgstr ""
-msgid "ProjectFeature|Only team members"
+msgid "PipelineSchedules|Provide a short description for this pipeline"
msgstr ""
-msgid "ProjectFileTree|Name"
+msgid "PipelineSchedules|Take ownership"
msgstr ""
-msgid "ProjectLastActivity|Never"
+msgid "PipelineSchedules|Target"
msgstr ""
msgid "ProjectLifecycle|Stage"
msgstr ""
-msgid "ProjectNetworkGraph|Graph"
-msgstr ""
-
msgid "Read more"
msgstr ""
-msgid "Readme"
-msgstr ""
-
-msgid "RefSwitcher|Branches"
-msgstr ""
-
-msgid "RefSwitcher|Tags"
-msgstr ""
-
msgid "Related Commits"
msgstr ""
@@ -479,34 +201,16 @@ msgstr ""
msgid "Related Merged Requests"
msgstr ""
-msgid "Remind later"
-msgstr ""
-
-msgid "Remove project"
-msgstr ""
-
-msgid "Request Access"
-msgstr ""
-
-msgid "Search branches and tags"
-msgstr ""
-
-msgid "Select Archive Format"
-msgstr ""
-
-msgid "Set a password on your account to pull or push via %{protocol}"
+msgid "Save pipeline schedule"
msgstr ""
-msgid "Set up CI"
+msgid "Schedule a new pipeline"
msgstr ""
-msgid "Set up Koding"
+msgid "Select a timezone"
msgstr ""
-msgid "Set up auto deploy"
-msgstr ""
-
-msgid "SetPasswordToCloneLink|set a password"
+msgid "Select target branch"
msgstr ""
msgid "Showing %d event"
@@ -514,21 +218,7 @@ msgid_plural "Showing %d events"
msgstr[0] ""
msgstr[1] ""
-msgid "Source code"
-msgstr ""
-
-msgid "StarProject|Star"
-msgstr ""
-
-msgid "Switch branch/tag"
-msgstr ""
-
-msgid "Tag"
-msgid_plural "Tags"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "Tags"
+msgid "Target Branch"
msgstr ""
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
@@ -537,9 +227,6 @@ msgstr ""
msgid "The collection of events added to the data gathered for that stage."
msgstr ""
-msgid "The fork relationship has been removed."
-msgstr ""
-
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr ""
@@ -552,15 +239,6 @@ msgstr ""
msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle."
msgstr ""
-msgid "The project can be accessed by any logged in user."
-msgstr ""
-
-msgid "The project can be accessed without any authentication."
-msgstr ""
-
-msgid "The repository for this project does not exist."
-msgstr ""
-
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr ""
@@ -576,9 +254,6 @@ msgstr ""
msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6."
msgstr ""
-msgid "This means you can not push code until you create an empty repository or import existing one."
-msgstr ""
-
msgid "Time before an issue gets scheduled"
msgstr ""
@@ -591,129 +266,6 @@ msgstr ""
msgid "Time until first merge request"
msgstr ""
-msgid "Timeago|%s days ago"
-msgstr ""
-
-msgid "Timeago|%s days remaining"
-msgstr ""
-
-msgid "Timeago|%s hours remaining"
-msgstr ""
-
-msgid "Timeago|%s minutes ago"
-msgstr ""
-
-msgid "Timeago|%s minutes remaining"
-msgstr ""
-
-msgid "Timeago|%s months ago"
-msgstr ""
-
-msgid "Timeago|%s months remaining"
-msgstr ""
-
-msgid "Timeago|%s seconds remaining"
-msgstr ""
-
-msgid "Timeago|%s weeks ago"
-msgstr ""
-
-msgid "Timeago|%s weeks remaining"
-msgstr ""
-
-msgid "Timeago|%s years ago"
-msgstr ""
-
-msgid "Timeago|%s years remaining"
-msgstr ""
-
-msgid "Timeago|1 day remaining"
-msgstr ""
-
-msgid "Timeago|1 hour remaining"
-msgstr ""
-
-msgid "Timeago|1 minute remaining"
-msgstr ""
-
-msgid "Timeago|1 month remaining"
-msgstr ""
-
-msgid "Timeago|1 week remaining"
-msgstr ""
-
-msgid "Timeago|1 year remaining"
-msgstr ""
-
-msgid "Timeago|Past due"
-msgstr ""
-
-msgid "Timeago|a day ago"
-msgstr ""
-
-msgid "Timeago|a month ago"
-msgstr ""
-
-msgid "Timeago|a week ago"
-msgstr ""
-
-msgid "Timeago|a while"
-msgstr ""
-
-msgid "Timeago|a year ago"
-msgstr ""
-
-msgid "Timeago|about %s hours ago"
-msgstr ""
-
-msgid "Timeago|about a minute ago"
-msgstr ""
-
-msgid "Timeago|about an hour ago"
-msgstr ""
-
-msgid "Timeago|in %s days"
-msgstr ""
-
-msgid "Timeago|in %s hours"
-msgstr ""
-
-msgid "Timeago|in %s minutes"
-msgstr ""
-
-msgid "Timeago|in %s months"
-msgstr ""
-
-msgid "Timeago|in %s seconds"
-msgstr ""
-
-msgid "Timeago|in %s weeks"
-msgstr ""
-
-msgid "Timeago|in %s years"
-msgstr ""
-
-msgid "Timeago|in 1 day"
-msgstr ""
-
-msgid "Timeago|in 1 hour"
-msgstr ""
-
-msgid "Timeago|in 1 minute"
-msgstr ""
-
-msgid "Timeago|in 1 month"
-msgstr ""
-
-msgid "Timeago|in 1 week"
-msgstr ""
-
-msgid "Timeago|in 1 year"
-msgstr ""
-
-msgid "Timeago|less than a minute ago"
-msgstr ""
-
msgid "Time|hr"
msgid_plural "Time|hrs"
msgstr[0] ""
@@ -733,88 +285,16 @@ msgstr ""
msgid "Total test time for all commits/merges"
msgstr ""
-msgid "Unstar"
-msgstr ""
-
-msgid "Upload New File"
-msgstr ""
-
-msgid "Upload file"
-msgstr ""
-
-msgid "Use your global notification setting"
-msgstr ""
-
-msgid "VisibilityLevel|Internal"
-msgstr ""
-
-msgid "VisibilityLevel|Private"
-msgstr ""
-
-msgid "VisibilityLevel|Public"
-msgstr ""
-
msgid "Want to see the data? Please ask an administrator for access."
msgstr ""
msgid "We don't have enough data to show this stage."
msgstr ""
-msgid "Withdraw Access Request"
-msgstr ""
-
-msgid ""
-"You are going to remove %{project_name_with_namespace}.\n"
-"Removed project CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr ""
-
-msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
-msgstr ""
-
-msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
-msgstr ""
-
-msgid "You can only add files when you are on a branch"
-msgstr ""
-
-msgid "You must sign in to star a project"
-msgstr ""
-
msgid "You need permission."
msgstr ""
-msgid "You will not get any notifications via email"
-msgstr ""
-
-msgid "You will only receive notifications for the events you choose"
-msgstr ""
-
-msgid "You will only receive notifications for threads you have participated in"
-msgstr ""
-
-msgid "You will receive notifications for any activity"
-msgstr ""
-
-msgid "You will receive notifications only for comments in which you were @mentioned"
-msgstr ""
-
-msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
-msgstr ""
-
-msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
-msgstr ""
-
-msgid "Your name"
-msgstr ""
-
-msgid "committed"
-msgstr ""
-
msgid "day"
msgid_plural "days"
msgstr[0] ""
msgstr[1] ""
-
-msgid "notification emails"
-msgstr ""
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 483412baa05..050f6c446c1 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-06-07 17:36+0200\n"
-"PO-Revision-Date: 2017-06-07 17:36+0200\n"
+"POT-Creation-Date: 2017-06-07 21:22+0200\n"
+"PO-Revision-Date: 2017-06-07 21:22+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -18,105 +18,13 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
-msgid "About auto deploy"
-msgstr ""
-
-msgid "Activity"
-msgstr ""
-
-msgid "Add Changelog"
-msgstr ""
-
-msgid "Add Contribution guide"
-msgstr ""
-
-msgid "Add License"
-msgstr ""
-
-msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr ""
-
-msgid "Add new directory"
-msgstr ""
-
-msgid "Archived project! Repository is read-only"
-msgstr ""
-
-msgid "Branch"
-msgid_plural "Branches"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
-msgstr ""
-
-msgid "Branches"
+msgid "Are you sure you want to delete this pipeline schedule?"
msgstr ""
msgid "ByAuthor|by"
msgstr ""
-msgid "CI configuration"
-msgstr ""
-
-msgid "Changelog"
-msgstr ""
-
-msgid "Charts"
-msgstr ""
-
-msgid "CiStatusLabel|canceled"
-msgstr ""
-
-msgid "CiStatusLabel|created"
-msgstr ""
-
-msgid "CiStatusLabel|failed"
-msgstr ""
-
-msgid "CiStatusLabel|manual action"
-msgstr ""
-
-msgid "CiStatusLabel|passed"
-msgstr ""
-
-msgid "CiStatusLabel|passed with warnings"
-msgstr ""
-
-msgid "CiStatusLabel|pending"
-msgstr ""
-
-msgid "CiStatusLabel|skipped"
-msgstr ""
-
-msgid "CiStatusLabel|waiting for manual action"
-msgstr ""
-
-msgid "CiStatusText|blocked"
-msgstr ""
-
-msgid "CiStatusText|canceled"
-msgstr ""
-
-msgid "CiStatusText|created"
-msgstr ""
-
-msgid "CiStatusText|failed"
-msgstr ""
-
-msgid "CiStatusText|manual"
-msgstr ""
-
-msgid "CiStatusText|passed"
-msgstr ""
-
-msgid "CiStatusText|pending"
-msgstr ""
-
-msgid "CiStatusText|skipped"
-msgstr ""
-
-msgid "CiStatus|running"
+msgid "Cancel"
msgstr ""
msgid "Commit"
@@ -124,52 +32,7 @@ msgid_plural "Commits"
msgstr[0] ""
msgstr[1] ""
-msgid "CommitMessage|Add %{file_name}"
-msgstr ""
-
-msgid "Commits"
-msgstr ""
-
-msgid "Commits|History"
-msgstr ""
-
-msgid "Compare"
-msgstr ""
-
-msgid "Contribution guide"
-msgstr ""
-
-msgid "Contributors"
-msgstr ""
-
-msgid "Copy URL to clipboard"
-msgstr ""
-
-msgid "Copy commit SHA to clipboard"
-msgstr ""
-
-msgid "Create New Directory"
-msgstr ""
-
-msgid "Create directory"
-msgstr ""
-
-msgid "Create empty bare repository"
-msgstr ""
-
-msgid "Create merge request"
-msgstr ""
-
-msgid "CreateNewFork|Fork"
-msgstr ""
-
-msgid "Custom notification events"
-msgstr ""
-
-msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}."
-msgstr ""
-
-msgid "Cycle Analytics"
+msgid "Cron Timezone"
msgstr ""
msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project."
@@ -196,42 +59,30 @@ msgstr ""
msgid "CycleAnalyticsStage|Test"
msgstr ""
+msgid "Delete"
+msgstr ""
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] ""
msgstr[1] ""
-msgid "Directory name"
-msgstr ""
-
-msgid "Don't show again"
-msgstr ""
-
-msgid "Download tar"
-msgstr ""
-
-msgid "Download tar.bz2"
+msgid "Description"
msgstr ""
-msgid "Download tar.gz"
+msgid "Edit"
msgstr ""
-msgid "Download zip"
+msgid "Edit Pipeline Schedule %{id}"
msgstr ""
-msgid "DownloadArtifacts|Download"
+msgid "Failed to change the owner"
msgstr ""
-msgid "DownloadSource|Download"
+msgid "Failed to remove the pipeline schedule"
msgstr ""
-msgid "Files"
-msgstr ""
-
-msgid "Find by path"
-msgstr ""
-
-msgid "Find file"
+msgid "Filter"
msgstr ""
msgid "FirstPushedBy|First"
@@ -240,57 +91,24 @@ msgstr ""
msgid "FirstPushedBy|pushed by"
msgstr ""
-msgid "ForkedFromProjectPath|Forked from"
-msgstr ""
-
-msgid "Forks"
-msgstr ""
-
msgid "From issue creation until deploy to production"
msgstr ""
msgid "From merge request merge until deploy to production"
msgstr ""
-msgid "Go to your fork"
-msgstr ""
-
-msgid "GoToYourFork|Fork"
-msgstr ""
-
-msgid "Home"
-msgstr ""
-
-msgid "Housekeeping successfully started"
-msgstr ""
-
-msgid "Import repository"
+msgid "Interval Pattern"
msgstr ""
msgid "Introducing Cycle Analytics"
msgstr ""
-msgid "LFSStatus|Disabled"
-msgstr ""
-
-msgid "LFSStatus|Enabled"
-msgstr ""
-
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] ""
msgstr[1] ""
-msgid "Last Update"
-msgstr ""
-
-msgid "Last commit"
-msgstr ""
-
-msgid "Leave group"
-msgstr ""
-
-msgid "Leave project"
+msgid "Last Pipeline"
msgstr ""
msgid "Limited to showing %d event at most"
@@ -301,36 +119,15 @@ msgstr[1] ""
msgid "Median"
msgstr ""
-msgid "MissingSSHKeyWarningLink|add an SSH key"
-msgstr ""
-
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] ""
msgstr[1] ""
-msgid "New branch"
-msgstr ""
-
-msgid "New directory"
-msgstr ""
-
-msgid "New file"
-msgstr ""
-
-msgid "New issue"
-msgstr ""
-
-msgid "New merge request"
+msgid "New Pipeline Schedule"
msgstr ""
-msgid "New snippet"
-msgstr ""
-
-msgid "New tag"
-msgstr ""
-
-msgid "No repository"
+msgid "No schedules"
msgstr ""
msgid "Not available"
@@ -339,129 +136,54 @@ msgstr ""
msgid "Not enough data"
msgstr ""
-msgid "Notification events"
-msgstr ""
-
-msgid "NotificationEvent|Close issue"
-msgstr ""
-
-msgid "NotificationEvent|Close merge request"
-msgstr ""
-
-msgid "NotificationEvent|Failed pipeline"
-msgstr ""
-
-msgid "NotificationEvent|Merge merge request"
-msgstr ""
-
-msgid "NotificationEvent|New issue"
-msgstr ""
-
-msgid "NotificationEvent|New merge request"
-msgstr ""
-
-msgid "NotificationEvent|New note"
-msgstr ""
-
-msgid "NotificationEvent|Reassign issue"
-msgstr ""
-
-msgid "NotificationEvent|Reassign merge request"
-msgstr ""
-
-msgid "NotificationEvent|Reopen issue"
-msgstr ""
-
-msgid "NotificationEvent|Successful pipeline"
-msgstr ""
-
-msgid "NotificationLevel|Custom"
-msgstr ""
-
-msgid "NotificationLevel|Disabled"
-msgstr ""
-
-msgid "NotificationLevel|Global"
-msgstr ""
-
-msgid "NotificationLevel|On mention"
-msgstr ""
-
-msgid "NotificationLevel|Participate"
-msgstr ""
-
-msgid "NotificationLevel|Watch"
-msgstr ""
-
msgid "OpenedNDaysAgo|Opened"
msgstr ""
-msgid "Pipeline Health"
+msgid "Owner"
msgstr ""
-msgid "Project '%{project_name}' queued for deletion."
-msgstr ""
-
-msgid "Project '%{project_name}' was successfully created."
-msgstr ""
-
-msgid "Project '%{project_name}' was successfully updated."
-msgstr ""
-
-msgid "Project '%{project_name}' will be deleted."
+msgid "Pipeline Health"
msgstr ""
-msgid "Project access must be granted explicitly to each user."
+msgid "Pipeline Schedule"
msgstr ""
-msgid "Project export could not be deleted."
+msgid "Pipeline Schedules"
msgstr ""
-msgid "Project export has been deleted."
+msgid "PipelineSchedules|Activated"
msgstr ""
-msgid "Project export link has expired. Please generate a new export from your project settings."
+msgid "PipelineSchedules|Active"
msgstr ""
-msgid "Project export started. A download link will be sent by email."
+msgid "PipelineSchedules|All"
msgstr ""
-msgid "Project home"
+msgid "PipelineSchedules|Inactive"
msgstr ""
-msgid "ProjectFeature|Disabled"
+msgid "PipelineSchedules|Next Run"
msgstr ""
-msgid "ProjectFeature|Everyone with access"
+msgid "PipelineSchedules|None"
msgstr ""
-msgid "ProjectFeature|Only team members"
+msgid "PipelineSchedules|Provide a short description for this pipeline"
msgstr ""
-msgid "ProjectFileTree|Name"
+msgid "PipelineSchedules|Take ownership"
msgstr ""
-msgid "ProjectLastActivity|Never"
+msgid "PipelineSchedules|Target"
msgstr ""
msgid "ProjectLifecycle|Stage"
msgstr ""
-msgid "ProjectNetworkGraph|Graph"
-msgstr ""
-
msgid "Read more"
msgstr ""
-msgid "Readme"
-msgstr ""
-
-msgid "RefSwitcher|Branches"
-msgstr ""
-
-msgid "RefSwitcher|Tags"
-msgstr ""
-
msgid "Related Commits"
msgstr ""
@@ -480,34 +202,16 @@ msgstr ""
msgid "Related Merged Requests"
msgstr ""
-msgid "Remind later"
-msgstr ""
-
-msgid "Remove project"
-msgstr ""
-
-msgid "Request Access"
-msgstr ""
-
-msgid "Search branches and tags"
-msgstr ""
-
-msgid "Select Archive Format"
-msgstr ""
-
-msgid "Set a password on your account to pull or push via %{protocol}"
+msgid "Save pipeline schedule"
msgstr ""
-msgid "Set up CI"
+msgid "Schedule a new pipeline"
msgstr ""
-msgid "Set up Koding"
+msgid "Select a timezone"
msgstr ""
-msgid "Set up auto deploy"
-msgstr ""
-
-msgid "SetPasswordToCloneLink|set a password"
+msgid "Select target branch"
msgstr ""
msgid "Showing %d event"
@@ -515,21 +219,7 @@ msgid_plural "Showing %d events"
msgstr[0] ""
msgstr[1] ""
-msgid "Source code"
-msgstr ""
-
-msgid "StarProject|Star"
-msgstr ""
-
-msgid "Switch branch/tag"
-msgstr ""
-
-msgid "Tag"
-msgid_plural "Tags"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "Tags"
+msgid "Target Branch"
msgstr ""
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
@@ -538,9 +228,6 @@ msgstr ""
msgid "The collection of events added to the data gathered for that stage."
msgstr ""
-msgid "The fork relationship has been removed."
-msgstr ""
-
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr ""
@@ -553,15 +240,6 @@ msgstr ""
msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle."
msgstr ""
-msgid "The project can be accessed by any logged in user."
-msgstr ""
-
-msgid "The project can be accessed without any authentication."
-msgstr ""
-
-msgid "The repository for this project does not exist."
-msgstr ""
-
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr ""
@@ -577,9 +255,6 @@ msgstr ""
msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6."
msgstr ""
-msgid "This means you can not push code until you create an empty repository or import existing one."
-msgstr ""
-
msgid "Time before an issue gets scheduled"
msgstr ""
@@ -592,129 +267,6 @@ msgstr ""
msgid "Time until first merge request"
msgstr ""
-msgid "Timeago|%s days ago"
-msgstr ""
-
-msgid "Timeago|%s days remaining"
-msgstr ""
-
-msgid "Timeago|%s hours remaining"
-msgstr ""
-
-msgid "Timeago|%s minutes ago"
-msgstr ""
-
-msgid "Timeago|%s minutes remaining"
-msgstr ""
-
-msgid "Timeago|%s months ago"
-msgstr ""
-
-msgid "Timeago|%s months remaining"
-msgstr ""
-
-msgid "Timeago|%s seconds remaining"
-msgstr ""
-
-msgid "Timeago|%s weeks ago"
-msgstr ""
-
-msgid "Timeago|%s weeks remaining"
-msgstr ""
-
-msgid "Timeago|%s years ago"
-msgstr ""
-
-msgid "Timeago|%s years remaining"
-msgstr ""
-
-msgid "Timeago|1 day remaining"
-msgstr ""
-
-msgid "Timeago|1 hour remaining"
-msgstr ""
-
-msgid "Timeago|1 minute remaining"
-msgstr ""
-
-msgid "Timeago|1 month remaining"
-msgstr ""
-
-msgid "Timeago|1 week remaining"
-msgstr ""
-
-msgid "Timeago|1 year remaining"
-msgstr ""
-
-msgid "Timeago|Past due"
-msgstr ""
-
-msgid "Timeago|a day ago"
-msgstr ""
-
-msgid "Timeago|a month ago"
-msgstr ""
-
-msgid "Timeago|a week ago"
-msgstr ""
-
-msgid "Timeago|a while"
-msgstr ""
-
-msgid "Timeago|a year ago"
-msgstr ""
-
-msgid "Timeago|about %s hours ago"
-msgstr ""
-
-msgid "Timeago|about a minute ago"
-msgstr ""
-
-msgid "Timeago|about an hour ago"
-msgstr ""
-
-msgid "Timeago|in %s days"
-msgstr ""
-
-msgid "Timeago|in %s hours"
-msgstr ""
-
-msgid "Timeago|in %s minutes"
-msgstr ""
-
-msgid "Timeago|in %s months"
-msgstr ""
-
-msgid "Timeago|in %s seconds"
-msgstr ""
-
-msgid "Timeago|in %s weeks"
-msgstr ""
-
-msgid "Timeago|in %s years"
-msgstr ""
-
-msgid "Timeago|in 1 day"
-msgstr ""
-
-msgid "Timeago|in 1 hour"
-msgstr ""
-
-msgid "Timeago|in 1 minute"
-msgstr ""
-
-msgid "Timeago|in 1 month"
-msgstr ""
-
-msgid "Timeago|in 1 week"
-msgstr ""
-
-msgid "Timeago|in 1 year"
-msgstr ""
-
-msgid "Timeago|less than a minute ago"
-msgstr ""
-
msgid "Time|hr"
msgid_plural "Time|hrs"
msgstr[0] ""
@@ -734,88 +286,16 @@ msgstr ""
msgid "Total test time for all commits/merges"
msgstr ""
-msgid "Unstar"
-msgstr ""
-
-msgid "Upload New File"
-msgstr ""
-
-msgid "Upload file"
-msgstr ""
-
-msgid "Use your global notification setting"
-msgstr ""
-
-msgid "VisibilityLevel|Internal"
-msgstr ""
-
-msgid "VisibilityLevel|Private"
-msgstr ""
-
-msgid "VisibilityLevel|Public"
-msgstr ""
-
msgid "Want to see the data? Please ask an administrator for access."
msgstr ""
msgid "We don't have enough data to show this stage."
msgstr ""
-msgid "Withdraw Access Request"
-msgstr ""
-
-msgid ""
-"You are going to remove %{project_name_with_namespace}.\n"
-"Removed project CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr ""
-
-msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
-msgstr ""
-
-msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
-msgstr ""
-
-msgid "You can only add files when you are on a branch"
-msgstr ""
-
-msgid "You must sign in to star a project"
-msgstr ""
-
msgid "You need permission."
msgstr ""
-msgid "You will not get any notifications via email"
-msgstr ""
-
-msgid "You will only receive notifications for the events you choose"
-msgstr ""
-
-msgid "You will only receive notifications for threads you have participated in"
-msgstr ""
-
-msgid "You will receive notifications for any activity"
-msgstr ""
-
-msgid "You will receive notifications only for comments in which you were @mentioned"
-msgstr ""
-
-msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
-msgstr ""
-
-msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
-msgstr ""
-
-msgid "Your name"
-msgstr ""
-
-msgid "committed"
-msgstr ""
-
msgid "day"
msgid_plural "days"
msgstr[0] ""
msgstr[1] ""
-
-msgid "notification emails"
-msgstr ""
diff --git a/locale/pt_BR/gitlab.po b/locale/pt_BR/gitlab.po
new file mode 100644
index 00000000000..5ad41f92b64
--- /dev/null
+++ b/locale/pt_BR/gitlab.po
@@ -0,0 +1,260 @@
+# Alexandre Alencar <alexandre.alencar@gmail.com>, 2017. #zanata
+# Fabio Beneditto <fabiobeneditto@gmail.com>, 2017. #zanata
+# Leandro Nunes dos Santos <leandronunes@gmail.com>, 2017. #zanata
+msgid ""
+msgstr ""
+"Project-Id-Version: gitlab 1.0.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2017-05-04 19:24-0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"PO-Revision-Date: 2017-06-05 03:29-0400\n"
+"Last-Translator: Alexandre Alencar <alexandre.alencar@gmail.com>\n"
+"Language-Team: Portuguese (Brazil)\n"
+"Language: pt-BR\n"
+"X-Generator: Zanata 3.9.6\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+
+msgid "ByAuthor|by"
+msgstr "por"
+
+msgid "Commit"
+msgid_plural "Commits"
+msgstr[0] "Commit"
+msgstr[1] "Commits"
+
+msgid ""
+"Cycle Analytics gives an overview of how much time it takes to go from idea "
+"to production in your project."
+msgstr ""
+"A Análise de Ciclo fornece uma visão geral de quanto tempo uma ideia demora "
+"para ir para produção em seu projeto."
+
+msgid "CycleAnalyticsStage|Code"
+msgstr "Código"
+
+msgid "CycleAnalyticsStage|Issue"
+msgstr "Tarefa"
+
+msgid "CycleAnalyticsStage|Plan"
+msgstr "Plano"
+
+msgid "CycleAnalyticsStage|Production"
+msgstr "Produção"
+
+msgid "CycleAnalyticsStage|Review"
+msgstr "Revisão"
+
+msgid "CycleAnalyticsStage|Staging"
+msgstr "Homologação"
+
+msgid "CycleAnalyticsStage|Test"
+msgstr "Teste"
+
+msgid "Deploy"
+msgid_plural "Deploys"
+msgstr[0] "Implantação"
+msgstr[1] "Implantações"
+
+msgid "FirstPushedBy|First"
+msgstr "Primeiro"
+
+msgid "FirstPushedBy|pushed by"
+msgstr "publicado por"
+
+msgid "From issue creation until deploy to production"
+msgstr "Da criação de tarefas até a implantação para a produção"
+
+msgid "From merge request merge until deploy to production"
+msgstr "Da incorporação do merge request até a implantação em produção"
+
+msgid "Introducing Cycle Analytics"
+msgstr "Apresentando a Análise de Ciclo"
+
+msgid "Last %d day"
+msgid_plural "Last %d days"
+msgstr[0] "Último %d dia"
+msgstr[1] "Últimos %d dias"
+
+msgid "Limited to showing %d event at most"
+msgid_plural "Limited to showing %d events at most"
+msgstr[0] "Limitado a mostrar %d evento no máximo"
+msgstr[1] "Limitado a mostrar %d eventos no máximo"
+
+msgid "Median"
+msgstr "Mediana"
+
+msgid "New Issue"
+msgid_plural "New Issues"
+msgstr[0] "Nova Tarefa"
+msgstr[1] "Novas Tarefas"
+
+msgid "Not available"
+msgstr "Não disponível"
+
+msgid "Not enough data"
+msgstr "Dados insuficientes"
+
+msgid "OpenedNDaysAgo|Opened"
+msgstr "Aberto"
+
+msgid "Pipeline Health"
+msgstr "Saúde da Pipeline"
+
+msgid "ProjectLifecycle|Stage"
+msgstr "Etapa"
+
+msgid "Read more"
+msgstr "Ler mais"
+
+msgid "Related Commits"
+msgstr "Commits Relacionados"
+
+msgid "Related Deployed Jobs"
+msgstr "Jobs Relacionados Incorporados"
+
+msgid "Related Issues"
+msgstr "Tarefas Relacionadas"
+
+msgid "Related Jobs"
+msgstr "Jobs Relacionados"
+
+msgid "Related Merge Requests"
+msgstr "Merge Requests Relacionados"
+
+msgid "Related Merged Requests"
+msgstr "Merge Requests Relacionados"
+
+msgid "Showing %d event"
+msgid_plural "Showing %d events"
+msgstr[0] "Mostrando %d evento"
+msgstr[1] "Mostrando %d eventos"
+
+msgid ""
+"The coding stage shows the time from the first commit to creating the merge "
+"request. The data will automatically be added here once you create your "
+"first merge request."
+msgstr ""
+"O estágio de codificação mostra o tempo desde o primeiro commit até a "
+"criação do merge request. \n"
+"Os dados serão automaticamente adicionados aqui uma vez que você tenha "
+"criado seu primeiro merge request."
+
+msgid "The collection of events added to the data gathered for that stage."
+msgstr ""
+"A coleção de eventos adicionados aos dados coletados para esse estágio."
+
+msgid ""
+"The issue stage shows the time it takes from creating an issue to assigning "
+"the issue to a milestone, or add the issue to a list on your Issue Board. "
+"Begin creating issues to see data for this stage."
+msgstr ""
+"O estágio em questão mostra o tempo que leva desde a criação de uma tarefa "
+"até a sua assinatura para um milestone, ou a sua adição para a lista no seu "
+"Painel de Tarefas. Comece a criar tarefas para ver dados para esta etapa."
+
+msgid "The phase of the development lifecycle."
+msgstr "A fase do ciclo de vida do desenvolvimento."
+
+msgid ""
+"The planning stage shows the time from the previous step to pushing your "
+"first commit. This time will be added automatically once you push your first "
+"commit."
+msgstr ""
+"A fase de planejamento mostra o tempo do passo anterior até empurrar o seu "
+"primeiro commit. Este tempo será adicionado automaticamente assim que você "
+"realizar seu primeiro commit."
+
+msgid ""
+"The production stage shows the total time it takes between creating an issue "
+"and deploying the code to production. The data will be automatically added "
+"once you have completed the full idea to production cycle."
+msgstr ""
+"O estágio de produção mostra o tempo total que leva entre criar uma tarefa e "
+"implantar o código na produção. Os dados serão adicionados automaticamente "
+"até que você complete todo o ciclo de produção."
+
+msgid ""
+"The review stage shows the time from creating the merge request to merging "
+"it. The data will automatically be added after you merge your first merge "
+"request."
+msgstr ""
+"A etapa de revisão mostra o tempo de criação de um merge request até que o "
+"merge seja feito. Os dados serão automaticamente adicionados depois que você "
+"fizer seu primeiro merge request."
+
+msgid ""
+"The staging stage shows the time between merging the MR and deploying code "
+"to the production environment. The data will be automatically added once you "
+"deploy to production for the first time."
+msgstr ""
+"O estágio de estágio mostra o tempo entre a fusão do MR e o código de "
+"implantação para o ambiente de produção. Os dados serão automaticamente "
+"adicionados depois de implantar na produção pela primeira vez."
+
+msgid ""
+"The testing stage shows the time GitLab CI takes to run every pipeline for "
+"the related merge request. The data will automatically be added after your "
+"first pipeline finishes running."
+msgstr ""
+"A fase de teste mostra o tempo que o GitLab CI leva para executar cada "
+"pipeline para o merge request relacionado. Os dados serão automaticamente "
+"adicionados após a conclusão do primeiro pipeline."
+
+msgid "The time taken by each data entry gathered by that stage."
+msgstr "O tempo necessário para cada entrada de dados reunida por essa etapa."
+
+msgid ""
+"The value lying at the midpoint of a series of observed values. E.g., "
+"between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 ="
+" 6."
+msgstr ""
+"O valor situado no ponto médio de uma série de valores observados. Ex., "
+"entre 3, 5, 9, a mediana é 5. Entre 3, 5, 7, 8, a mediana é (5 + 7) / 2 = 6."
+
+msgid "Time before an issue gets scheduled"
+msgstr "Tempo até que uma tarefa seja planejada"
+
+msgid "Time before an issue starts implementation"
+msgstr "Tempo até que uma tarefa comece a ser implementada"
+
+msgid "Time between merge request creation and merge/close"
+msgstr "Tempo entre a criação do merge request e o merge/fechamento"
+
+msgid "Time until first merge request"
+msgstr "Tempo até o primeiro merge request"
+
+msgid "Time|hr"
+msgid_plural "Time|hrs"
+msgstr[0] "h"
+msgstr[1] "hs"
+
+msgid "Time|min"
+msgid_plural "Time|mins"
+msgstr[0] "min"
+msgstr[1] "mins"
+
+msgid "Time|s"
+msgstr "s"
+
+msgid "Total Time"
+msgstr "Tempo Total"
+
+msgid "Total test time for all commits/merges"
+msgstr "Tempo de teste total para todos os commits/merges"
+
+msgid "Want to see the data? Please ask an administrator for access."
+msgstr "Precisa visualizar os dados? Solicite acesso ao administrador."
+
+msgid "We don't have enough data to show this stage."
+msgstr "Não temos dados suficientes para mostrar esta fase."
+
+msgid "You need permission."
+msgstr "Você precisa de permissão."
+
+msgid "day"
+msgid_plural "days"
+msgstr[0] "dia"
+msgstr[1] "dias"
+
diff --git a/locale/pt_BR/gitlab.po.time_stamp b/locale/pt_BR/gitlab.po.time_stamp
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/locale/pt_BR/gitlab.po.time_stamp
diff --git a/locale/zh_CN/gitlab.po b/locale/zh_CN/gitlab.po
index c2d69b122e2..11434460207 100644
--- a/locale/zh_CN/gitlab.po
+++ b/locale/zh_CN/gitlab.po
@@ -2,32 +2,38 @@
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the gitlab package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
-#
-#, fuzzy
+#
msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-05-04 19:24-0500\n"
"PO-Revision-Date: 2017-05-04 19:24-0500\n"
"Last-Translator: HuangTao <htve@outlook.com>, 2017\n"
-"Language-Team: Chinese (China) (https://www.transifex.com/gitlab-zh/teams/75177/zh_CN/)\n"
+"Language-Team: Chinese (China) (https://www.transifex.com/gitlab-zh/teams/7517"
+"7/zh_CN/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: zh_CN\n"
"Plural-Forms: nplurals=1; plural=0;\n"
+msgid "Are you sure you want to delete this pipeline schedule?"
+msgstr ""
+
msgid "ByAuthor|by"
msgstr "作者:"
+msgid "Cancel"
+msgstr ""
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "提交"
-msgid ""
-"Cycle Analytics gives an overview of how much time it takes to go from idea "
-"to production in your project."
+msgid "Cron Timezone"
+msgstr ""
+
+msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project."
msgstr "周期分析概述了项目从想法到产品实现的各阶段所需的时间。"
msgid "CycleAnalyticsStage|Code"
@@ -51,10 +57,31 @@ msgstr "预发布"
msgid "CycleAnalyticsStage|Test"
msgstr "测试"
+msgid "Delete"
+msgstr ""
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "部署"
+msgid "Description"
+msgstr ""
+
+msgid "Edit"
+msgstr ""
+
+msgid "Edit Pipeline Schedule %{id}"
+msgstr ""
+
+msgid "Failed to change the owner"
+msgstr ""
+
+msgid "Failed to remove the pipeline schedule"
+msgstr ""
+
+msgid "Filter"
+msgstr ""
+
msgid "FirstPushedBy|First"
msgstr "首次推送"
@@ -67,6 +94,9 @@ msgstr "从创建议题到部署至生产环境"
msgid "From merge request merge until deploy to production"
msgstr "从合并请求被合并后到部署至生产环境"
+msgid "Interval Pattern"
+msgstr ""
+
msgid "Introducing Cycle Analytics"
msgstr "周期分析简介"
@@ -74,6 +104,9 @@ msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] "最后 %d 天"
+msgid "Last Pipeline"
+msgstr ""
+
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] "最多显示 %d 个事件"
@@ -85,6 +118,12 @@ msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "新议题"
+msgid "New Pipeline Schedule"
+msgstr ""
+
+msgid "No schedules"
+msgstr ""
+
msgid "Not available"
msgstr "数据不足"
@@ -94,9 +133,45 @@ msgstr "数据不足"
msgid "OpenedNDaysAgo|Opened"
msgstr "开始于"
+msgid "Owner"
+msgstr ""
+
msgid "Pipeline Health"
msgstr "流水线健康指标"
+msgid "Pipeline Schedule"
+msgstr ""
+
+msgid "Pipeline Schedules"
+msgstr ""
+
+msgid "PipelineSchedules|Activated"
+msgstr ""
+
+msgid "PipelineSchedules|Active"
+msgstr ""
+
+msgid "PipelineSchedules|All"
+msgstr ""
+
+msgid "PipelineSchedules|Inactive"
+msgstr ""
+
+msgid "PipelineSchedules|Next Run"
+msgstr ""
+
+msgid "PipelineSchedules|None"
+msgstr ""
+
+msgid "PipelineSchedules|Provide a short description for this pipeline"
+msgstr ""
+
+msgid "PipelineSchedules|Take ownership"
+msgstr ""
+
+msgid "PipelineSchedules|Target"
+msgstr ""
+
msgid "ProjectLifecycle|Stage"
msgstr "项目生命周期"
@@ -121,65 +196,56 @@ msgstr "相关的合并请求"
msgid "Related Merged Requests"
msgstr "相关已合并的合并请求"
+msgid "Save pipeline schedule"
+msgstr ""
+
+msgid "Schedule a new pipeline"
+msgstr ""
+
+msgid "Select a timezone"
+msgstr ""
+
+msgid "Select target branch"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "显示 %d 个事件"
-msgid ""
-"The coding stage shows the time from the first commit to creating the merge "
-"request. The data will automatically be added here once you create your "
-"first merge request."
+msgid "Target Branch"
+msgstr ""
+
+msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr "编码阶段概述了从第一次提交到创建合并请求的时间。创建第一个合并请求后,数据将自动添加到此处。"
msgid "The collection of events added to the data gathered for that stage."
msgstr "与该阶段相关的事件。"
-msgid ""
-"The issue stage shows the time it takes from creating an issue to assigning "
-"the issue to a milestone, or add the issue to a list on your Issue Board. "
-"Begin creating issues to see data for this stage."
+msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr "议题阶段概述了从创建议题到将议题设置里程碑或将议题添加到议题看板的时间。开始创建议题以查看此阶段的数据。"
msgid "The phase of the development lifecycle."
msgstr "项目生命周期中的各个阶段。"
-msgid ""
-"The planning stage shows the time from the previous step to pushing your "
-"first commit. This time will be added automatically once you push your first"
-" commit."
+msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr "计划阶段概述了从议题添加到日程后到推送首次提交的时间。当首次推送提交后,数据将自动添加到此处。"
-msgid ""
-"The production stage shows the total time it takes between creating an issue"
-" and deploying the code to production. The data will be automatically added "
-"once you have completed the full idea to production cycle."
+msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle."
msgstr "生产阶段概述了从创建一个议题到将代码部署到生产环境的总时间。当完成想法到部署生产的循环,数据将自动添加到此处。"
-msgid ""
-"The review stage shows the time from creating the merge request to merging "
-"it. The data will automatically be added after you merge your first merge "
-"request."
+msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr "评审阶段概述了从创建合并请求到被合并的时间。当创建第一个合并请求后,数据将自动添加到此处。"
-msgid ""
-"The staging stage shows the time between merging the MR and deploying code "
-"to the production environment. The data will be automatically added once you"
-" deploy to production for the first time."
+msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
msgstr "预发布阶段概述了从合并请求被合并到部署至生产环境的总时间。首次部署到生产环境后,数据将自动添加到此处。"
-msgid ""
-"The testing stage shows the time GitLab CI takes to run every pipeline for "
-"the related merge request. The data will automatically be added after your "
-"first pipeline finishes running."
+msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
msgstr "测试阶段概述了GitLab CI为相关合并请求运行每个流水线所需的时间。当第一个流水线运行完成后,数据将自动添加到此处。"
msgid "The time taken by each data entry gathered by that stage."
msgstr "该阶段每条数据所花的时间"
-msgid ""
-"The value lying at the midpoint of a series of observed values. E.g., "
-"between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 "
-"= 6."
+msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6."
msgstr "中位数是一个数列中最中间的值。例如在 3、5、9 之间,中位数是 5。在 3、5、7、8 之间,中位数是 (5 + 7)/ 2 = 6。"
msgid "Time before an issue gets scheduled"
diff --git a/locale/zh_HK/gitlab.po b/locale/zh_HK/gitlab.po
index 22fe5e4f807..8fbc38ba5af 100644
--- a/locale/zh_HK/gitlab.po
+++ b/locale/zh_HK/gitlab.po
@@ -7,7 +7,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"PO-Revision-Date: 2017-06-12 11:08-0400\n"
+"PO-Revision-Date: 2017-06-13 04:07-0400\n"
"Last-Translator: Huang Tao <htve@outlook.com>\n"
"Language-Team: Chinese (Hong Kong) (https://translate.zanata.org/project/view/GitLab)\n"
"Language: zh-HK\n"
@@ -36,7 +36,7 @@ msgid "Add License"
msgstr "添加許可證"
msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr "新增壹個用於推送或拉取的 SSH 秘鑰到您的個人賬戶中。"
+msgstr "新增壹個用於推送或拉取的 SSH 秘鑰到賬號中。"
msgid "Add new directory"
msgstr "添加新目錄"
@@ -230,7 +230,7 @@ msgid ""
"notification levels you will also receive notifications for select events. "
"To find out more, check out %{notification_link}."
msgstr ""
-"自定義通知級別與參與級別相同。使用自定義通知級別,您只能收到所選定事件通知。想瞭解更多信息,請查看 %{notification_link}."
+"自定義通知級別繼承自參與級別。使用自定義通知級別,您會收到參與級別及選定事件的通知。想了解更多信息,請查看 %{notification_link}."
msgid "Cycle Analytics"
msgstr "週期分析"
@@ -357,10 +357,10 @@ msgid "From merge request merge until deploy to production"
msgstr "從合併請求的合併到部署至生產環境"
msgid "Go to your fork"
-msgstr "跳轉到您的派生項目"
+msgstr "跳轉到派生項目"
msgid "GoToYourFork|Fork"
-msgstr "跳轉到您的派生項目"
+msgstr "跳轉到派生項目"
msgid "Home"
msgstr "首頁"
@@ -378,7 +378,7 @@ msgid "Introducing Cycle Analytics"
msgstr "週期分析簡介"
msgid "LFSStatus|Disabled"
-msgstr "禁用"
+msgstr "停用"
msgid "LFSStatus|Enabled"
msgstr "啟用"
@@ -498,7 +498,7 @@ msgid "NotificationLevel|Custom"
msgstr "自定義"
msgid "NotificationLevel|Disabled"
-msgstr "禁用"
+msgstr "停用"
msgid "NotificationLevel|Global"
msgstr "全局"
@@ -596,7 +596,7 @@ msgid "Project home"
msgstr "項目首頁"
msgid "ProjectFeature|Disabled"
-msgstr "禁用"
+msgstr "停用"
msgid "ProjectFeature|Everyone with access"
msgstr "任何人都可訪問"
@@ -683,7 +683,7 @@ msgid "Select target branch"
msgstr "選擇目標分支"
msgid "Set a password on your account to pull or push via %{protocol}"
-msgstr "為您的賬戶添加壹個用於推送或拉取的 %{protocol} 密碼。"
+msgstr "為賬號添加壹個用於推送或拉取的 %{protocol} 密碼。"
msgid "Set up CI"
msgstr "設置 CI"
@@ -1033,12 +1033,12 @@ msgstr "您只會收到評論中提及(@)您的通知"
msgid ""
"You won't be able to pull or push project code via %{protocol} until you "
"%{set_password_link} on your account"
-msgstr "在您的賬戶上 %{set_password_link} 之前, 您將無法通過 %{protocol} 拉取或推送代碼。"
+msgstr "在賬號上 %{set_password_link} 之前將無法通過 %{protocol} 拉取或推送代碼。"
msgid ""
"You won't be able to pull or push project code via SSH until you "
"%{add_ssh_key_link} to your profile"
-msgstr "在您的個人資料中添加 %{add_ssh_key_link} 之前, 您將無法通過 SSH 拉取或推送代碼。"
+msgstr "在賬號中添加 %{add_ssh_key_link} 之前您將無法通過 SSH 拉取或推送代碼。"
msgid "Your name"
msgstr "您的名字"
diff --git a/locale/zh_HK/gitlab.po.time_stamp b/locale/zh_HK/gitlab.po.time_stamp
index 0519ecba6ea..e69de29bb2d 100644
--- a/locale/zh_HK/gitlab.po.time_stamp
+++ b/locale/zh_HK/gitlab.po.time_stamp
@@ -1 +0,0 @@
- \ No newline at end of file
diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po
index 0caf35a915b..e40723a9d8d 100644
--- a/locale/zh_TW/gitlab.po
+++ b/locale/zh_TW/gitlab.po
@@ -2,32 +2,38 @@
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the gitlab package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
-#
-#, fuzzy
+#
msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-05-04 19:24-0500\n"
"PO-Revision-Date: 2017-05-04 19:24-0500\n"
"Last-Translator: HuangTao <htve@outlook.com>, 2017\n"
-"Language-Team: Chinese (Taiwan) (https://www.transifex.com/gitlab-zh/teams/75177/zh_TW/)\n"
+"Language-Team: Chinese (Taiwan) (https://www.transifex.com/gitlab-zh/teams/751"
+"77/zh_TW/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: zh_TW\n"
"Plural-Forms: nplurals=1; plural=0;\n"
+msgid "Are you sure you want to delete this pipeline schedule?"
+msgstr ""
+
msgid "ByAuthor|by"
msgstr "作者:"
+msgid "Cancel"
+msgstr ""
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "送交"
-msgid ""
-"Cycle Analytics gives an overview of how much time it takes to go from idea "
-"to production in your project."
+msgid "Cron Timezone"
+msgstr ""
+
+msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project."
msgstr "週期分析概述了你的專案從想法到產品實現,各階段所需的時間。"
msgid "CycleAnalyticsStage|Code"
@@ -51,10 +57,31 @@ msgstr "預備"
msgid "CycleAnalyticsStage|Test"
msgstr "測試"
+msgid "Delete"
+msgstr ""
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "部署"
+msgid "Description"
+msgstr ""
+
+msgid "Edit"
+msgstr ""
+
+msgid "Edit Pipeline Schedule %{id}"
+msgstr ""
+
+msgid "Failed to change the owner"
+msgstr ""
+
+msgid "Failed to remove the pipeline schedule"
+msgstr ""
+
+msgid "Filter"
+msgstr ""
+
msgid "FirstPushedBy|First"
msgstr "首次推送"
@@ -67,6 +94,9 @@ msgstr "從議題建立至線上部署"
msgid "From merge request merge until deploy to production"
msgstr "從請求被合併後至線上部署"
+msgid "Interval Pattern"
+msgstr ""
+
msgid "Introducing Cycle Analytics"
msgstr "週期分析簡介"
@@ -74,6 +104,9 @@ msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] "最後 %d 天"
+msgid "Last Pipeline"
+msgstr ""
+
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] "最多顯示 %d 個事件"
@@ -85,6 +118,12 @@ msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "新議題"
+msgid "New Pipeline Schedule"
+msgstr ""
+
+msgid "No schedules"
+msgstr ""
+
msgid "Not available"
msgstr "無法使用"
@@ -94,9 +133,45 @@ msgstr "資料不足"
msgid "OpenedNDaysAgo|Opened"
msgstr "開始於"
+msgid "Owner"
+msgstr ""
+
msgid "Pipeline Health"
msgstr "流水線健康指標"
+msgid "Pipeline Schedule"
+msgstr ""
+
+msgid "Pipeline Schedules"
+msgstr ""
+
+msgid "PipelineSchedules|Activated"
+msgstr ""
+
+msgid "PipelineSchedules|Active"
+msgstr ""
+
+msgid "PipelineSchedules|All"
+msgstr ""
+
+msgid "PipelineSchedules|Inactive"
+msgstr ""
+
+msgid "PipelineSchedules|Next Run"
+msgstr ""
+
+msgid "PipelineSchedules|None"
+msgstr ""
+
+msgid "PipelineSchedules|Provide a short description for this pipeline"
+msgstr ""
+
+msgid "PipelineSchedules|Take ownership"
+msgstr ""
+
+msgid "PipelineSchedules|Target"
+msgstr ""
+
msgid "ProjectLifecycle|Stage"
msgstr "專案生命週期"
@@ -121,69 +196,60 @@ msgstr "相關的合併請求"
msgid "Related Merged Requests"
msgstr "相關已合併的請求"
+msgid "Save pipeline schedule"
+msgstr ""
+
+msgid "Schedule a new pipeline"
+msgstr ""
+
+msgid "Select a timezone"
+msgstr ""
+
+msgid "Select target branch"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "顯示 %d 個事件"
-msgid ""
-"The coding stage shows the time from the first commit to creating the merge "
-"request. The data will automatically be added here once you create your "
-"first merge request."
+msgid "Target Branch"
+msgstr ""
+
+msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr "程式開發階段顯示從第一次送交到建立合併請求的時間。建立第一個合併請求後,資料將自動填入。"
msgid "The collection of events added to the data gathered for that stage."
msgstr "與該階段相關的事件。"
-msgid ""
-"The issue stage shows the time it takes from creating an issue to assigning "
-"the issue to a milestone, or add the issue to a list on your Issue Board. "
-"Begin creating issues to see data for this stage."
+msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr "議題階段顯示從議題建立到設置里程碑、或將該議題加至議題看板的時間。建立第一個議題後,資料將自動填入。"
msgid "The phase of the development lifecycle."
msgstr "專案開發生命週期的各個階段。"
-msgid ""
-"The planning stage shows the time from the previous step to pushing your "
-"first commit. This time will be added automatically once you push your first"
-" commit."
-msgstr "計劃階段顯示從議題添加到日程後至推送第一個送交的時間。當第一次推送送交後,資料將自動填入。"
+msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
+msgstr "計劃階段所顯示的是議題被排程後至第一個送交被推送的時間。一旦完成(或執行)首次的推送,資料將自動填入。"
-msgid ""
-"The production stage shows the total time it takes between creating an issue"
-" and deploying the code to production. The data will be automatically added "
-"once you have completed the full idea to production cycle."
+msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle."
msgstr "上線階段顯示從建立一個議題到部署程式至線上的總時間。當完成從想法到產品實現的循環後,資料將自動填入。"
-msgid ""
-"The review stage shows the time from creating the merge request to merging "
-"it. The data will automatically be added after you merge your first merge "
-"request."
+msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr "複閱階段顯示從合併請求建立後至被合併的時間。當建立第一個合併請求後,資料將自動填入。"
-msgid ""
-"The staging stage shows the time between merging the MR and deploying code "
-"to the production environment. The data will be automatically added once you"
-" deploy to production for the first time."
+msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
msgstr "預備階段顯示從合併請求被合併後至部署上線的時間。當第一次部署上線後,資料將自動填入。"
-msgid ""
-"The testing stage shows the time GitLab CI takes to run every pipeline for "
-"the related merge request. The data will automatically be added after your "
-"first pipeline finishes running."
+msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
msgstr "測試階段顯示相關合併請求的流水線所花的時間。當第一個流水線運作完畢後,資料將自動填入。"
msgid "The time taken by each data entry gathered by that stage."
msgstr "每筆該階段相關資料所花的時間。"
-msgid ""
-"The value lying at the midpoint of a series of observed values. E.g., "
-"between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 "
-"= 6."
+msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6."
msgstr "中位數是一個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。"
msgid "Time before an issue gets scheduled"
-msgstr "議題被列入日程表的時間"
+msgstr "議題等待排程的時間"
msgid "Time before an issue starts implementation"
msgstr "議題等待開始實作的時間"
diff --git a/scripts/static-analysis b/scripts/static-analysis
index 7dc8f679036..6d35684b97f 100755
--- a/scripts/static-analysis
+++ b/scripts/static-analysis
@@ -3,7 +3,7 @@
require ::File.expand_path('../lib/gitlab/popen', __dir__)
tasks = [
- %w[bundle exec bundle-audit check --update --ignore CVE-2016-4658],
+ %w[bundle exec bundle-audit check --update --ignore CVE-2016-4658 CVE-2017-5029],
%w[bundle exec rake config_lint],
%w[bundle exec rake flay],
%w[bundle exec rake haml_lint],
diff --git a/scripts/trigger-build b/scripts/trigger-build
index e4603533872..dcda70d7ed8 100755
--- a/scripts/trigger-build
+++ b/scripts/trigger-build
@@ -9,7 +9,7 @@ params = {
"token" => ENV["BUILD_TRIGGER_TOKEN"],
"variables[GITLAB_VERSION]" => ENV["CI_COMMIT_SHA"],
"variables[ALTERNATIVE_SOURCES]" => true,
- "variables[ee]" => ENV["EE_PACKAGE"]
+ "variables[ee]" => ENV["EE_PACKAGE"] || "false"
}
Dir.glob("*_VERSION").each do |version_file|
@@ -19,4 +19,9 @@ end
res = Net::HTTP.post_form(uri, params)
pipeline_id = JSON.parse(res.body)['id']
-puts "Triggered pipeline can be found at https://gitlab.com/gitlab-org/omnibus-gitlab/pipelines/#{pipeline_id}"
+unless pipeline_id.nil?
+ puts "Triggered pipeline can be found at https://gitlab.com/gitlab-org/omnibus-gitlab/pipelines/#{pipeline_id}"
+else
+ puts "Trigger failed. The response from trigger is: "
+ puts res.body
+end
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index 2c9d1ffc9c2..4c3a5ec49ef 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -170,22 +170,32 @@ describe AutocompleteController do
end
context 'author of issuable included' do
- before do
- sign_in(user)
- end
-
let(:body) { JSON.parse(response.body) }
- it 'includes the author' do
- get(:users, author_id: non_member.id)
+ context 'authenticated' do
+ before do
+ sign_in(user)
+ end
+
+ it 'includes the author' do
+ get(:users, author_id: non_member.id)
+
+ expect(body.first["username"]).to eq non_member.username
+ end
+
+ it 'rejects non existent user ids' do
+ get(:users, author_id: 99999)
- expect(body.first["username"]).to eq non_member.username
+ expect(body.collect { |u| u['id'] }).not_to include(99999)
+ end
end
- it 'rejects non existent user ids' do
- get(:users, author_id: 99999)
+ context 'without authenticating' do
+ it 'returns empty result' do
+ get(:users, author_id: non_member.id)
- expect(body.collect { |u| u['id'] }).not_to include(99999)
+ expect(body).to be_empty
+ end
end
end
diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb
new file mode 100644
index 00000000000..424f39fd3b8
--- /dev/null
+++ b/spec/controllers/dashboard/milestones_controller_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe Dashboard::MilestonesController do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+ let(:project_milestone) { create(:milestone, project: project) }
+ let(:milestone) do
+ DashboardMilestone.build(
+ [project],
+ project_milestone.title
+ )
+ end
+ let(:issue) { create(:issue, project: project, milestone: project_milestone) }
+ let!(:label) { create(:label, project: project, title: 'Issue Label', issues: [issue]) }
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: project_milestone) }
+ let(:milestone_path) { dashboard_milestone_path(milestone.safe_title, title: milestone.title) }
+
+ before do
+ sign_in(user)
+ project.team << [user, :master]
+ end
+
+ it_behaves_like 'milestone tabs'
+
+ describe "#show" do
+ render_views
+
+ def view_milestone
+ get :show, id: milestone.safe_title, title: milestone.title
+ end
+
+ it 'shows milestone page' do
+ view_milestone
+
+ expect(response).to have_http_status(200)
+ end
+ end
+end
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index 60db0192dfd..ed4ad7b600e 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -124,6 +124,13 @@ describe Groups::GroupMembersController do
expect(response).to redirect_to(dashboard_groups_path)
expect(group.users).not_to include user
end
+
+ it 'supports json request' do
+ delete :leave, group_id: group, format: :json
+
+ expect(response).to have_http_status(200)
+ expect(json_response['notice']).to eq "You left the \"#{group.name}\" group."
+ end
end
context 'and is an owner' do
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index f285e5333d6..f9e21f9d8f6 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -367,19 +367,5 @@ describe Projects::BranchesController do
expect(parsed_response.first).to eq 'master'
end
end
-
- context 'show_all = true' do
- it 'returns all the branches name' do
- get :index,
- namespace_id: project.namespace,
- project_id: project,
- format: :json,
- show_all: true
-
- parsed_response = JSON.parse(response.body)
-
- expect(parsed_response.length).to eq(project.repository.branches.count)
- end
- end
end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index a38ae2eb990..b65e9e0dfc0 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -260,6 +260,7 @@ describe Projects::IssuesController do
before { allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) }
it 'rejects an issue recognized as a spam' do
+ expect(Gitlab::Recaptcha).to receive(:load_configurations!).and_return(true)
expect { update_spam_issue }.not_to change{ issue.reload.title }
end
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index 7211acc53dc..4a737587899 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -69,18 +69,11 @@ describe Projects::JobsController do
Ci::Build::AVAILABLE_STATUSES.each do |status|
create_build(status, status)
end
-
- RequestStore.begin!
- end
-
- after do
- RequestStore.end!
- RequestStore.clear!
end
- it "verifies number of queries" do
+ it 'verifies number of queries', :request_store do
recorded = ActiveRecord::QueryRecorder.new { get_index }
- expect(recorded.count).to be_within(5).of(8)
+ expect(recorded.count).to be_within(5).of(7)
end
def create_build(name, status)
diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb
index 130b0b744b5..bf1776eb320 100644
--- a/spec/controllers/projects/labels_controller_spec.rb
+++ b/spec/controllers/projects/labels_controller_spec.rb
@@ -117,7 +117,7 @@ describe Projects::LabelsController do
let!(:promoted_label_name) { "Promoted Label" }
let!(:label_1) { create(:label, title: promoted_label_name, project: project) }
- context 'not group owner' do
+ context 'not group reporters' do
it 'denies access' do
post :promote, namespace_id: project.namespace.to_param, project_id: project, id: label_1.to_param
@@ -125,9 +125,9 @@ describe Projects::LabelsController do
end
end
- context 'group owner' do
+ context 'group reporter' do
before do
- GroupMember.add_users(group, [user], :owner)
+ group.add_reporter(user)
end
it 'gives access' do
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 08024a2148b..6e1c91738db 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -119,14 +119,14 @@ describe Projects::MergeRequestsController do
end
end
- context 'number of queries' do
+ context 'number of queries', :request_store do
it 'verifies number of queries' do
# pre-create objects
merge_request
recorded = ActiveRecord::QueryRecorder.new { go(format: :json) }
- expect(recorded.count).to be_within(5).of(50)
+ expect(recorded.count).to be_within(5).of(30)
expect(recorded.cached_count).to eq(0)
end
end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index c880da1e36a..954f89e3854 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -49,21 +49,14 @@ describe Projects::PipelinesController do
expect(json_response['details']).to have_key 'stages'
end
- context 'when the pipeline has multiple stages and groups' do
+ context 'when the pipeline has multiple stages and groups', :request_store do
before do
- RequestStore.begin!
-
create_build('build', 0, 'build')
create_build('test', 1, 'rspec 0')
create_build('deploy', 2, 'production')
create_build('post deploy', 3, 'pages 0')
end
- after do
- RequestStore.end!
- RequestStore.clear!
- end
-
let(:project) { create(:project) }
let(:pipeline) do
create(:ci_empty_pipeline, project: project, user: user, sha: project.commit.id)
diff --git a/spec/factories/uploads.rb b/spec/factories/uploads.rb
new file mode 100644
index 00000000000..1383420fb44
--- /dev/null
+++ b/spec/factories/uploads.rb
@@ -0,0 +1,8 @@
+FactoryGirl.define do
+ factory :upload do
+ model { build(:project) }
+ path { "uploads/system/project/avatar/avatar.jpg" }
+ size 100.kilobytes
+ uploader "AvatarUploader"
+ end
+end
diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb
index 96d715ef383..595366ce352 100644
--- a/spec/features/admin/admin_appearance_spec.rb
+++ b/spec/features/admin/admin_appearance_spec.rb
@@ -63,11 +63,11 @@ feature 'Admin Appearance', feature: true do
end
def logo_selector
- '//img[@src^="/uploads/appearance/logo"]'
+ '//img[@src^="/uploads/system/appearance/logo"]'
end
def header_logo_selector
- '//img[@src^="/uploads/appearance/header_logo"]'
+ '//img[@src^="/uploads/system/appearance/header_logo"]'
end
def logo_fixture
diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb
index b0e2953dda2..7eb254f8451 100644
--- a/spec/features/dashboard/groups_list_spec.rb
+++ b/spec/features/dashboard/groups_list_spec.rb
@@ -6,40 +6,124 @@ describe 'Dashboard Groups page', js: true, feature: true do
let!(:nested_group) { create(:group, :nested) }
let!(:another_group) { create(:group) }
- before do
+ it 'shows groups user is member of' do
group.add_owner(user)
nested_group.add_owner(user)
login_as(user)
-
visit dashboard_groups_path
- end
- it 'shows groups user is member of' do
expect(page).to have_content(group.full_name)
expect(page).to have_content(nested_group.full_name)
expect(page).not_to have_content(another_group.full_name)
end
- it 'filters groups' do
- fill_in 'filter_groups', with: group.name
- wait_for_requests
+ describe 'when filtering groups' do
+ before do
+ group.add_owner(user)
+ nested_group.add_owner(user)
- expect(page).to have_content(group.full_name)
- expect(page).not_to have_content(nested_group.full_name)
- expect(page).not_to have_content(another_group.full_name)
+ login_as(user)
+
+ visit dashboard_groups_path
+ end
+
+ it 'filters groups' do
+ fill_in 'filter_groups', with: group.name
+ wait_for_requests
+
+ expect(page).to have_content(group.full_name)
+ expect(page).not_to have_content(nested_group.full_name)
+ expect(page).not_to have_content(another_group.full_name)
+ end
+
+ it 'resets search when user cleans the input' do
+ fill_in 'filter_groups', with: group.name
+ wait_for_requests
+
+ fill_in 'filter_groups', with: ""
+ wait_for_requests
+
+ expect(page).to have_content(group.full_name)
+ expect(page).to have_content(nested_group.full_name)
+ expect(page).not_to have_content(another_group.full_name)
+ expect(page.all('.js-groups-list-holder .content-list li').length).to eq 2
+ end
end
- it 'resets search when user cleans the input' do
- fill_in 'filter_groups', with: group.name
- wait_for_requests
+ describe 'group with subgroups' do
+ let!(:subgroup) { create(:group, :public, parent: group) }
- fill_in 'filter_groups', with: ""
- wait_for_requests
+ before do
+ group.add_owner(user)
+ subgroup.add_owner(user)
- expect(page).to have_content(group.full_name)
- expect(page).to have_content(nested_group.full_name)
- expect(page).not_to have_content(another_group.full_name)
- expect(page.all('.js-groups-list-holder .content-list li').length).to eq 2
+ login_as(user)
+
+ visit dashboard_groups_path
+ end
+
+ it 'shows subgroups inside of its parent group' do
+ expect(page).to have_selector('.groups-list-tree-container .group-list-tree', count: 2)
+ expect(page).to have_selector(".groups-list-tree-container #group-#{group.id} #group-#{subgroup.id}", count: 1)
+ end
+
+ it 'can toggle parent group' do
+ # Expanded by default
+ expect(page).to have_selector("#group-#{group.id} .fa-caret-down", count: 1)
+ expect(page).not_to have_selector("#group-#{group.id} .fa-caret-right")
+
+ # Collapse
+ find("#group-#{group.id}").trigger('click')
+
+ expect(page).not_to have_selector("#group-#{group.id} .fa-caret-down")
+ expect(page).to have_selector("#group-#{group.id} .fa-caret-right", count: 1)
+ expect(page).not_to have_selector("#group-#{group.id} #group-#{subgroup.id}")
+
+ # Expand
+ find("#group-#{group.id}").trigger('click')
+
+ expect(page).to have_selector("#group-#{group.id} .fa-caret-down", count: 1)
+ expect(page).not_to have_selector("#group-#{group.id} .fa-caret-right")
+ expect(page).to have_selector("#group-#{group.id} #group-#{subgroup.id}")
+ end
+ end
+
+ describe 'when using pagination' do
+ let(:group2) { create(:group) }
+
+ before do
+ group.add_owner(user)
+ group2.add_owner(user)
+
+ allow(Kaminari.config).to receive(:default_per_page).and_return(1)
+
+ login_as(user)
+ visit dashboard_groups_path
+ end
+
+ it 'shows pagination' do
+ expect(page).to have_selector('.gl-pagination')
+ expect(page).to have_selector('.gl-pagination .page', count: 2)
+ end
+
+ it 'loads results for next page' do
+ # Check first page
+ expect(page).to have_content(group2.full_name)
+ expect(page).to have_selector("#group-#{group2.id}")
+ expect(page).not_to have_content(group.full_name)
+ expect(page).not_to have_selector("#group-#{group.id}")
+
+ # Go to next page
+ find(".gl-pagination .page:not(.active) a").trigger('click')
+
+ wait_for_requests
+
+ # Check second page
+ expect(page).to have_content(group.full_name)
+ expect(page).to have_selector("#group-#{group.id}")
+ expect(page).not_to have_content(group2.full_name)
+ expect(page).not_to have_selector("#group-#{group2.id}")
+ end
end
end
diff --git a/spec/features/dashboard/milestone_tabs_spec.rb b/spec/features/dashboard/milestone_tabs_spec.rb
new file mode 100644
index 00000000000..0c7b992c500
--- /dev/null
+++ b/spec/features/dashboard/milestone_tabs_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe 'Dashboard milestone tabs', :js, :feature do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let!(:label) { create(:label, project: project) }
+ let(:project_milestone) { create(:milestone, project: project) }
+ let(:milestone) do
+ DashboardMilestone.build(
+ [project],
+ project_milestone.title
+ )
+ end
+ let!(:merge_request) { create(:labeled_merge_request, source_project: project, target_project: project, milestone: project_milestone, labels: [label]) }
+
+ before do
+ project.add_master(user)
+ login_as(user)
+
+ visit dashboard_milestone_path(milestone.safe_title, title: milestone.title)
+ end
+
+ it 'loads merge requests async' do
+ click_link 'Merge Requests'
+
+ expect(page).to have_selector('.merge_requests-sortable-list')
+ end
+
+ it 'loads participants async' do
+ click_link 'Participants'
+
+ expect(page).to have_selector('#tab-participants .bordered-list')
+ end
+
+ it 'loads labels async' do
+ click_link 'Labels'
+
+ expect(page).to have_selector('#tab-labels .bordered-list')
+ end
+end
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index e0b2404e60a..31014f5cad2 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -34,7 +34,7 @@ describe 'Help Pages', feature: true do
end
end
- context 'in a production environment with version check enabled', js: true do
+ context 'in a production environment with version check enabled', :js do
before do
allow(Rails.env).to receive(:production?) { true }
allow(current_application_settings).to receive(:version_check_enabled) { true }
@@ -44,18 +44,12 @@ describe 'Help Pages', feature: true do
visit help_path
end
- it 'should display a version check image' do
- expect(find('.js-version-status-badge')).to be_visible
+ it 'has a version check image' do
+ expect(find('.js-version-status-badge', visible: false)['src']).to end_with('/version-check-url')
end
- it 'should have a src url' do
- expect(find('.js-version-status-badge')['src']).to match(/\/version-check-url/)
- end
-
- it 'should hide the version check image if the image request fails' do
- # We use '--load-images=no' with poltergeist so we must trigger manually
- execute_script("$('.js-version-status-badge').trigger('error');")
-
+ it 'hides the version check image if the image request fails' do
+ # We use '--load-images=yes' with poltergeist so the image fails to load
expect(find('.js-version-status-badge', visible: false)).not_to be_visible
end
end
diff --git a/spec/features/projects/artifacts/file_spec.rb b/spec/features/projects/artifacts/file_spec.rb
index 25c4f3c87a2..860373e531b 100644
--- a/spec/features/projects/artifacts/file_spec.rb
+++ b/spec/features/projects/artifacts/file_spec.rb
@@ -39,6 +39,7 @@ feature 'Artifact file', :js, feature: true do
context 'JPG file' do
before do
+ page.driver.browser.url_blacklist = []
visit_file('rails_sample.jpg')
wait_for_requests
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index 82cfbfda157..45fdb36e506 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -3,8 +3,8 @@ require 'spec_helper'
feature 'File blob', :js, feature: true do
let(:project) { create(:project, :public) }
- def visit_blob(path, fragment = nil)
- visit namespace_project_blob_path(project.namespace, project, File.join('master', path), anchor: fragment)
+ def visit_blob(path, anchor: nil, ref: 'master')
+ visit namespace_project_blob_path(project.namespace, project, File.join(ref, path), anchor: anchor)
wait_for_requests
end
@@ -101,7 +101,7 @@ feature 'File blob', :js, feature: true do
context 'visiting with a line number anchor' do
before do
- visit_blob('files/markdown/ruby-style-guide.md', 'L1')
+ visit_blob('files/markdown/ruby-style-guide.md', anchor: 'L1')
end
it 'displays the blob using the simple viewer' do
@@ -352,6 +352,37 @@ feature 'File blob', :js, feature: true do
end
end
+ context 'binary file that appears to be text in the first 1024 bytes' do
+ before do
+ visit_blob('encoding/binary-1.bin', ref: 'binary-encoding')
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows a download link
+ expect(page).to have_link('Download (23.8 KB)')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # The specs below verify an arguably incorrect result, but since we only
+ # learn that the file is not actually text once the text viewer content
+ # is loaded asynchronously, there is no straightforward way to get these
+ # synchronously loaded elements to display correctly.
+ #
+ # Clicking the copy button will result in nothing being copied.
+ # Clicking the raw button will result in the binary file being downloaded,
+ # as expected.
+
+ # shows an enabled copy button, incorrectly
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+
+ # shows a raw button, incorrectly
+ expect(page).to have_link('Open raw')
+ end
+ end
+ end
+
context '.gitlab-ci.yml' do
before do
project.add_master(project.creator)
diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb
index 1a38997450d..d04c3248ead 100644
--- a/spec/features/projects/blobs/edit_spec.rb
+++ b/spec/features/projects/blobs/edit_spec.rb
@@ -102,7 +102,7 @@ feature 'Editing file blob', feature: true, js: true do
it 'shows blob editor with same branch' do
expect(page).to have_current_path(namespace_project_edit_blob_path(project.namespace, project, tree_join(branch, file_path)))
- expect(find('.js-target-branch .dropdown-toggle-text').text).to eq(branch)
+ expect(find('.js-branch-name').value).to eq(branch)
end
end
@@ -112,7 +112,7 @@ feature 'Editing file blob', feature: true, js: true do
end
it 'shows blob editor with patch branch' do
- expect(find('.js-target-branch .dropdown-toggle-text').text).to eq('patch-1')
+ expect(find('.js-branch-name').value).to eq('patch-1')
end
end
end
@@ -128,7 +128,7 @@ feature 'Editing file blob', feature: true, js: true do
it 'shows blob editor with same branch' do
expect(page).to have_current_path(namespace_project_edit_blob_path(project.namespace, project, tree_join(branch, file_path)))
- expect(find('.js-target-branch .dropdown-toggle-text').text).to eq(branch)
+ expect(find('.js-branch-name').value).to eq(branch)
end
end
end
diff --git a/spec/features/projects/blobs/user_create_spec.rb b/spec/features/projects/blobs/user_create_spec.rb
deleted file mode 100644
index 4b6c55f5f44..00000000000
--- a/spec/features/projects/blobs/user_create_spec.rb
+++ /dev/null
@@ -1,94 +0,0 @@
-require 'spec_helper'
-
-feature 'New blob creation', feature: true, js: true do
- include TargetBranchHelpers
-
- given(:user) { create(:user) }
- given(:role) { :developer }
- given(:project) { create(:project) }
- given(:content) { 'class NextFeature\nend\n' }
-
- background do
- login_as(user)
- project.team << [user, role]
- visit namespace_project_new_blob_path(project.namespace, project, 'master')
- end
-
- def edit_file
- wait_for_requests
- fill_in 'file_name', with: 'feature.rb'
- execute_script("ace.edit('editor').setValue('#{content}')")
- end
-
- def commit_file
- click_button 'Commit changes'
- end
-
- context 'with default target branch' do
- background do
- edit_file
- commit_file
- end
-
- scenario 'creates the blob in the default branch' do
- expect(page).to have_content 'master'
- expect(page).to have_content 'successfully created'
- expect(page).to have_content 'NextFeature'
- end
- end
-
- context 'with different target branch' do
- background do
- edit_file
- select_branch('feature')
- commit_file
- end
-
- scenario 'creates the blob in the different branch' do
- expect(page).to have_content 'feature'
- expect(page).to have_content 'successfully created'
- end
- end
-
- context 'with a new target branch' do
- given(:new_branch_name) { 'new-feature' }
-
- background do
- edit_file
- create_new_branch(new_branch_name)
- commit_file
- end
-
- scenario 'creates the blob in the new branch' do
- expect(page).to have_content new_branch_name
- expect(page).to have_content 'successfully created'
- end
- scenario 'returns you to the mr' do
- expect(page).to have_content 'New Merge Request'
- expect(page).to have_content "From #{new_branch_name} into master"
- expect(page).to have_content 'Add new file'
- end
- end
-
- context 'the file already exist in the source branch' do
- background do
- Files::CreateService.new(
- project,
- user,
- start_branch: 'master',
- branch_name: 'master',
- commit_message: 'Create file',
- file_path: 'feature.rb',
- file_content: content
- ).execute
- edit_file
- commit_file
- end
-
- scenario 'shows error message' do
- expect(page).to have_content('A file with this name already exists')
- expect(page).to have_content('New file')
- expect(page).to have_content('NextFeature')
- end
- end
-end
diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb
index 4e5682c8636..1b680a56492 100644
--- a/spec/features/projects/group_links_spec.rb
+++ b/spec/features/projects/group_links_spec.rb
@@ -16,15 +16,17 @@ feature 'Project group links', :feature, :js do
before do
visit namespace_project_settings_members_path(project.namespace, project)
+ click_on 'share-with-group-tab'
+
select2 group.id, from: '#link_group_id'
fill_in 'expires_at_groups', with: (Time.current + 4.5.days).strftime('%Y-%m-%d')
page.find('body').click
- click_on 'Share'
+ find('.btn-create').trigger('click')
end
it 'shows the expiration time with a warning class' do
- page.within('.enabled-groups') do
- expect(page).to have_content('expires in 4 days')
+ page.within('.project-members-groups') do
+ expect(page).to have_content('Expires in 4 days')
expect(page).to have_selector('.text-warning')
end
end
@@ -43,6 +45,7 @@ feature 'Project group links', :feature, :js do
it 'does not show ancestors', :nested_groups do
visit namespace_project_settings_members_path(project.namespace, project)
+ click_on 'share-with-group-tab'
click_link 'Search for a group'
page.within '.select2-drop' do
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 0eda46649db..727ae7081b0 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -5,6 +5,7 @@ feature 'Jobs', :feature do
let(:user) { create(:user) }
let(:user_access_level) { :developer }
let(:project) { create(:project) }
+ let(:namespace) { project.namespace }
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, :trace, pipeline: pipeline) }
@@ -113,10 +114,16 @@ feature 'Jobs', :feature do
describe "GET /:project/jobs/:id" do
context "Job from project" do
+ let(:build) { create(:ci_build, :success, pipeline: pipeline) }
+
before do
visit namespace_project_job_path(project.namespace, project, build)
end
+ it 'shows status name', :js do
+ expect(page).to have_css('.ci-status.ci-success', text: 'passed')
+ end
+
it 'shows commit`s data' do
expect(page.status_code).to eq(200)
expect(page).to have_content pipeline.sha[0..7]
@@ -129,6 +136,48 @@ feature 'Jobs', :feature do
end
end
+ context 'when job is not running', :js do
+ let(:build) { create(:ci_build, :success, pipeline: pipeline) }
+
+ before do
+ visit namespace_project_job_path(project.namespace, project, build)
+ end
+
+ it 'shows retry button' do
+ expect(page).to have_link('Retry')
+ end
+
+ context 'if build passed' do
+ it 'does not show New issue button' do
+ expect(page).not_to have_link('New issue')
+ end
+ end
+
+ context 'if build failed' do
+ let(:build) { create(:ci_build, :failed, pipeline: pipeline) }
+
+ before do
+ visit namespace_project_job_path(namespace, project, build)
+ end
+
+ it 'shows New issue button' do
+ expect(page).to have_link('New issue')
+ end
+
+ it 'links to issues/new with the title and description filled in' do
+ button_title = "Build Failed ##{build.id}"
+ build_path = namespace_project_job_path(namespace, project, build)
+ options = { issue: { title: button_title, description: build_path } }
+
+ href = new_namespace_project_issue_path(namespace, project, options)
+
+ page.within('.header-action-buttons') do
+ expect(find('.js-new-issue')['href']).to include(href)
+ end
+ end
+ end
+ end
+
context "Job from other project" do
before do
visit namespace_project_job_path(project.namespace, project, build2)
@@ -305,63 +354,38 @@ feature 'Jobs', :feature do
end
end
- describe "POST /:project/jobs/:id/cancel" do
+ describe "POST /:project/jobs/:id/cancel", :js do
context "Job from project" do
before do
build.run!
visit namespace_project_job_path(project.namespace, project, build)
- click_link "Cancel"
+ find('.js-cancel-job').click()
end
it 'loads the page and shows all needed controls' do
expect(page.status_code).to eq(200)
- expect(page).to have_content 'canceled'
expect(page).to have_content 'Retry'
end
end
-
- context "Job from other project" do
- before do
- build.run!
- visit namespace_project_job_path(project.namespace, project, build)
- page.driver.post(cancel_namespace_project_job_path(project.namespace, project, build2))
- end
-
- it { expect(page.status_code).to eq(404) }
- end
end
describe "POST /:project/jobs/:id/retry" do
- context "Job from project" do
+ context "Job from project", :js do
before do
build.run!
visit namespace_project_job_path(project.namespace, project, build)
- click_link 'Cancel'
- page.within('.build-header') do
- click_link 'Retry job'
- end
+ find('.js-cancel-job').click()
+ find('.js-retry-button').trigger('click')
end
- it 'shows the right status and buttons' do
+ it 'shows the right status and buttons', :js do
expect(page).to have_http_status(200)
- expect(page).to have_content 'pending'
page.within('aside.right-sidebar') do
expect(page).to have_content 'Cancel'
end
end
end
- context "Job from other project" do
- before do
- build.run!
- visit namespace_project_job_path(project.namespace, project, build)
- click_link 'Cancel'
- page.driver.post(retry_namespace_project_job_path(project.namespace, project, build2))
- end
-
- it { expect(page).to have_http_status(404) }
- end
-
context "Job that current user is not allowed to retry" do
before do
build.run!
@@ -435,20 +459,17 @@ feature 'Jobs', :feature do
Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' }
build.run!
-
- allow_any_instance_of(Gitlab::Ci::Trace).to receive(:paths)
- .and_return(paths)
-
- visit namespace_project_job_path(project.namespace, project, build)
end
context 'when build has trace in file', :js do
- let(:paths) do
- [existing_file]
- end
-
before do
- find('.js-raw-link-controller').click()
+ allow_any_instance_of(Gitlab::Ci::Trace)
+ .to receive(:paths)
+ .and_return([existing_file])
+
+ visit namespace_project_job_path(namespace, project, build)
+
+ find('.js-raw-link-controller').click
end
it 'sends the right headers' do
@@ -458,11 +479,17 @@ feature 'Jobs', :feature do
end
end
- context 'when job has trace in DB' do
- let(:paths) { [] }
+ context 'when job has trace in the database', :js do
+ before do
+ allow_any_instance_of(Gitlab::Ci::Trace)
+ .to receive(:paths)
+ .and_return([])
+
+ visit namespace_project_job_path(namespace, project, build)
+ end
it 'sends the right headers' do
- expect(page.status_code).not_to have_selector('.js-raw-link-controller')
+ expect(page).not_to have_selector('.js-raw-link-controller')
end
end
end
diff --git a/spec/features/projects/user_create_dir_spec.rb b/spec/features/projects/user_create_dir_spec.rb
index 5dfdc465d7d..aeb7e0b7c33 100644
--- a/spec/features/projects/user_create_dir_spec.rb
+++ b/spec/features/projects/user_create_dir_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'New directory creation', feature: true, js: true do
- include TargetBranchHelpers
-
given(:user) { create(:user) }
given(:role) { :developer }
given(:project) { create(:project) }
@@ -36,23 +34,11 @@ feature 'New directory creation', feature: true, js: true do
end
end
- context 'with different target branch' do
- background do
- select_branch('feature')
- create_directory
- end
-
- scenario 'creates the directory in the different branch' do
- expect(page).to have_content 'feature'
- expect(page).to have_content 'The directory has been successfully created'
- end
- end
-
context 'with a new target branch' do
given(:new_branch_name) { 'new-feature' }
background do
- create_new_branch(new_branch_name)
+ fill_in :branch_name, with: new_branch_name
create_directory
end
diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb
index 49d7ef09e64..94f6bb16730 100644
--- a/spec/features/projects/wiki/markdown_preview_spec.rb
+++ b/spec/features/projects/wiki/markdown_preview_spec.rb
@@ -14,11 +14,12 @@ feature 'Projects > Wiki > User previews markdown changes', feature: true, js: t
background do
project.team << [user, :master]
+ WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute
+
login_as(user)
visit namespace_project_path(project.namespace, project)
find('.shortcuts-wiki').trigger('click')
- WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute
end
context "while creating a new wiki page" do
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index bb4b2aed0e3..feb2fe8a7d1 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -333,29 +333,6 @@ describe 'Dashboard Todos', feature: true do
end
end
- context 'User have large number of todos' do
- before do
- create_list(:todo, 101, :mentioned, user: user, project: project, target: issue, author: author)
-
- login_as(user)
- visit dashboard_todos_path
- end
-
- it 'shows 99+ for count >= 100 in notification' do
- expect(page).to have_selector('.todos-count', text: '99+')
- end
-
- it 'shows exact number in To do tab' do
- expect(page).to have_selector('.todos-pending .badge', text: '101')
- end
-
- it 'shows exact number for count < 100' do
- 3.times { first('.js-done-todo').click }
-
- expect(page).to have_selector('.todos-count', text: '98')
- end
- end
-
context 'User has a Build Failed todo' do
let!(:todo) { create(:todo, :build_failed, user: user, project: project, author: author) }
diff --git a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb
index f88a515f7fc..d9d6f2e2382 100644
--- a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb
+++ b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb
@@ -18,7 +18,7 @@ feature 'User uploads avatar to group', feature: true do
visit group_path(group)
- expect(page).to have_selector(%Q(img[src$="/uploads/group/avatar/#{group.id}/dk.png"]))
+ expect(page).to have_selector(%Q(img[src$="/uploads/system/group/avatar/#{group.id}/dk.png"]))
# Cheating here to verify something that isn't user-facing, but is important
expect(group.reload.avatar.file).to exist
diff --git a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
index 0dfd29045e5..eb8dbd76aab 100644
--- a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
+++ b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
@@ -16,7 +16,7 @@ feature 'User uploads avatar to profile', feature: true do
visit user_path(user)
- expect(page).to have_selector(%Q(img[src$="/uploads/user/avatar/#{user.id}/dk.png"]))
+ expect(page).to have_selector(%Q(img[src$="/uploads/system/user/avatar/#{user.id}/dk.png"]))
# Cheating here to verify something that isn't user-facing, but is important
expect(user.reload.avatar.file).to exist
diff --git a/spec/features/user_can_display_performance_bar_spec.rb b/spec/features/user_can_display_performance_bar_spec.rb
new file mode 100644
index 00000000000..c2842255b86
--- /dev/null
+++ b/spec/features/user_can_display_performance_bar_spec.rb
@@ -0,0 +1,81 @@
+require 'rails_helper'
+
+describe 'User can display performacne bar', :js do
+ shared_examples 'performance bar is disabled' do
+ it 'does not show the performance bar by default' do
+ expect(page).not_to have_css('#peek')
+ end
+
+ context 'when user press `pb`' do
+ before do
+ find('body').native.send_keys('pb')
+ end
+
+ it 'does not show the performance bar by default' do
+ expect(page).not_to have_css('#peek')
+ end
+ end
+ end
+
+ shared_examples 'performance bar is enabled' do
+ it 'does not show the performance bar by default' do
+ expect(page).not_to have_css('#peek')
+ end
+
+ context 'when user press `pb`' do
+ before do
+ find('body').native.send_keys('pb')
+ end
+
+ it 'does not show the performance bar by default' do
+ expect(page).not_to have_css('#peek')
+ end
+ end
+ end
+
+ context 'when user is logged-out' do
+ before do
+ visit root_path
+ end
+
+ context 'when the gitlab_performance_bar feature is disabled' do
+ before do
+ Feature.disable('gitlab_performance_bar')
+ end
+
+ it_behaves_like 'performance bar is disabled'
+ end
+
+ context 'when the gitlab_performance_bar feature is enabled' do
+ before do
+ Feature.enable('gitlab_performance_bar')
+ end
+
+ it_behaves_like 'performance bar is disabled'
+ end
+ end
+
+ context 'when user is logged-in' do
+ before do
+ login_as :user
+
+ visit root_path
+ end
+
+ context 'when the gitlab_performance_bar feature is disabled' do
+ before do
+ Feature.disable('gitlab_performance_bar')
+ end
+
+ it_behaves_like 'performance bar is disabled'
+ end
+
+ context 'when the gitlab_performance_bar feature is enabled' do
+ before do
+ Feature.enable('gitlab_performance_bar')
+ end
+
+ it_behaves_like 'performance bar is enabled'
+ end
+ end
+end
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 785fb724132..49df91b236f 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -1,3 +1,4 @@
+# coding: utf-8
require 'spec_helper'
describe ApplicationHelper do
@@ -58,13 +59,13 @@ describe ApplicationHelper do
describe 'project_icon' do
it 'returns an url for the avatar' do
project = create(:empty_project, avatar: File.open(uploaded_image_temp_path))
- avatar_url = "/uploads/project/avatar/#{project.id}/banana_sample.gif"
+ avatar_url = "/uploads/system/project/avatar/#{project.id}/banana_sample.gif"
expect(helper.project_icon(project.full_path).to_s).
to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />"
allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host)
- avatar_url = "#{gitlab_host}/uploads/project/avatar/#{project.id}/banana_sample.gif"
+ avatar_url = "#{gitlab_host}/uploads/system/project/avatar/#{project.id}/banana_sample.gif"
expect(helper.project_icon(project.full_path).to_s).
to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />"
@@ -84,12 +85,12 @@ describe ApplicationHelper do
it 'returns an url for the avatar' do
user = create(:user, avatar: File.open(uploaded_image_temp_path))
- avatar_url = "/uploads/user/avatar/#{user.id}/banana_sample.gif"
+ avatar_url = "/uploads/system/user/avatar/#{user.id}/banana_sample.gif"
expect(helper.avatar_icon(user.email).to_s).to match(avatar_url)
allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host)
- avatar_url = "#{gitlab_host}/uploads/user/avatar/#{user.id}/banana_sample.gif"
+ avatar_url = "#{gitlab_host}/uploads/system/user/avatar/#{user.id}/banana_sample.gif"
expect(helper.avatar_icon(user.email).to_s).to match(avatar_url)
end
@@ -102,7 +103,7 @@ describe ApplicationHelper do
user = create(:user, avatar: File.open(uploaded_image_temp_path))
expect(helper.avatar_icon(user.email).to_s).
- to match("/gitlab/uploads/user/avatar/#{user.id}/banana_sample.gif")
+ to match("/gitlab/uploads/system/user/avatar/#{user.id}/banana_sample.gif")
end
it 'calls gravatar_icon when no User exists with the given email' do
@@ -116,7 +117,7 @@ describe ApplicationHelper do
user = create(:user, avatar: File.open(uploaded_image_temp_path))
expect(helper.avatar_icon(user).to_s).
- to match("/uploads/user/avatar/#{user.id}/banana_sample.gif")
+ to match("/uploads/system/user/avatar/#{user.id}/banana_sample.gif")
end
end
end
diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb
index cd112dbb2fb..c68e4f56b05 100644
--- a/spec/helpers/emails_helper_spec.rb
+++ b/spec/helpers/emails_helper_spec.rb
@@ -52,7 +52,7 @@ describe EmailsHelper do
)
expect(header_logo).to eq(
- %{<img style="height: 50px" src="/uploads/appearance/header_logo/#{appearance.id}/dk.png" alt="Dk" />}
+ %{<img style="height: 50px" src="/uploads/system/appearance/header_logo/#{appearance.id}/dk.png" alt="Dk" />}
)
end
end
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index c8b0d86425f..0337afa4452 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -9,7 +9,7 @@ describe GroupsHelper do
group.avatar = fixture_file_upload(avatar_file_path)
group.save!
expect(group_icon(group.path).to_s).
- to match("/uploads/group/avatar/#{group.id}/banana_sample.gif")
+ to match("/uploads/system/group/avatar/#{group.id}/banana_sample.gif")
end
it 'gives default avatar_icon when no avatar is present' do
diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb
index 2cc0b40b2d0..dff2784f21f 100644
--- a/spec/helpers/page_layout_helper_spec.rb
+++ b/spec/helpers/page_layout_helper_spec.rb
@@ -60,7 +60,7 @@ describe PageLayoutHelper do
%w(project user group).each do |type|
context "with @#{type} assigned" do
it "uses #{type.titlecase} avatar if available" do
- object = double(avatar_url: 'http://example.com/uploads/avatar.png')
+ object = double(avatar_url: 'http://example.com/uploads/system/avatar.png')
assign(type, object)
expect(helper.page_image).to eq object.avatar_url
diff --git a/spec/helpers/todos_helper_spec.rb b/spec/helpers/todos_helper_spec.rb
index 50060a0925d..18a41ca24e3 100644
--- a/spec/helpers/todos_helper_spec.rb
+++ b/spec/helpers/todos_helper_spec.rb
@@ -1,6 +1,19 @@
require "spec_helper"
describe TodosHelper do
+ describe '#todos_count_format' do
+ it 'shows fuzzy count for 100 or more items' do
+ expect(helper.todos_count_format(100)).to eq '99+'
+ expect(helper.todos_count_format(1000)).to eq '99+'
+ end
+
+ it 'shows exact count for 99 or fewer items' do
+ expect(helper.todos_count_format(99)).to eq '99'
+ expect(helper.todos_count_format(50)).to eq '50'
+ expect(helper.todos_count_format(1)).to eq '1'
+ end
+ end
+
describe '#todo_projects_options' do
let(:projects) { create_list(:empty_project, 3) }
let(:user) { create(:user) }
diff --git a/spec/helpers/u2f_helper_spec.rb b/spec/helpers/u2f_helper_spec.rb
new file mode 100644
index 00000000000..0d65b4fe0b8
--- /dev/null
+++ b/spec/helpers/u2f_helper_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe U2fHelper do
+ describe 'when not on mobile' do
+ it 'does not inject u2f on chrome 40' do
+ device = double(mobile?: false)
+ browser = double(chrome?: true, opera?: false, version: 40, device: device)
+ allow(helper).to receive(:browser).and_return(browser)
+ expect(helper.inject_u2f_api?).to eq false
+ end
+
+ it 'injects u2f on chrome 41' do
+ device = double(mobile?: false)
+ browser = double(chrome?: true, opera?: false, version: 41, device: device)
+ allow(helper).to receive(:browser).and_return(browser)
+ expect(helper.inject_u2f_api?).to eq true
+ end
+
+ it 'does not inject u2f on opera 39' do
+ device = double(mobile?: false)
+ browser = double(chrome?: false, opera?: true, version: 39, device: device)
+ allow(helper).to receive(:browser).and_return(browser)
+ expect(helper.inject_u2f_api?).to eq false
+ end
+
+ it 'injects u2f on opera 40' do
+ device = double(mobile?: false)
+ browser = double(chrome?: false, opera?: true, version: 40, device: device)
+ allow(helper).to receive(:browser).and_return(browser)
+ expect(helper.inject_u2f_api?).to eq true
+ end
+ end
+
+ describe 'when on mobile' do
+ it 'does not inject u2f on chrome 41' do
+ device = double(mobile?: true)
+ browser = double(chrome?: true, opera?: false, version: 41, device: device)
+ allow(helper).to receive(:browser).and_return(browser)
+ expect(helper.inject_u2f_api?).to eq false
+ end
+
+ it 'does not inject u2f on opera 40' do
+ device = double(mobile?: true)
+ browser = double(chrome?: false, opera?: true, version: 40, device: device)
+ allow(helper).to receive(:browser).and_return(browser)
+ expect(helper.inject_u2f_api?).to eq false
+ end
+ end
+end
diff --git a/spec/javascripts/blob/create_branch_dropdown_spec.js b/spec/javascripts/blob/create_branch_dropdown_spec.js
deleted file mode 100644
index 6dbaa47c544..00000000000
--- a/spec/javascripts/blob/create_branch_dropdown_spec.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import '~/gl_dropdown';
-import '~/blob/create_branch_dropdown';
-import '~/blob/target_branch_dropdown';
-
-describe('CreateBranchDropdown', () => {
- const fixtureTemplate = 'static/target_branch_dropdown.html.raw';
- // selectors
- const createBranchSel = '.js-new-branch-btn';
- const backBtnSel = '.dropdown-menu-back';
- const cancelBtnSel = '.js-cancel-branch-btn';
- const branchNameSel = '#new_branch_name';
- const branchName = 'new_name';
- let dropdown;
-
- function createDropdown() {
- const dropdownEl = document.querySelector('.js-project-branches-dropdown');
- const projectBranches = getJSONFixture('project_branches.json');
- dropdown = new gl.TargetBranchDropDown(dropdownEl);
- dropdown.cachedRefs = projectBranches;
- return dropdown;
- }
-
- function createBranchBtn() {
- return document.querySelector(createBranchSel);
- }
-
- function backBtn() {
- return document.querySelector(backBtnSel);
- }
-
- function cancelBtn() {
- return document.querySelector(cancelBtnSel);
- }
-
- function branchNameEl() {
- return document.querySelector(branchNameSel);
- }
-
- function changeBranchName(text) {
- branchNameEl().value = text;
- branchNameEl().dispatchEvent(new Event('change'));
- }
-
- preloadFixtures(fixtureTemplate);
-
- beforeEach(() => {
- loadFixtures(fixtureTemplate);
- createDropdown();
- });
-
- it('disable submit when branch name is empty', () => {
- expect(createBranchBtn()).toBeDisabled();
- });
-
- it('enable submit when branch name is present', () => {
- changeBranchName(branchName);
-
- expect(createBranchBtn()).not.toBeDisabled();
- });
-
- it('resets the form when cancel btn is clicked and triggers dropdownback', () => {
- const spyBackEvent = spyOnEvent(backBtnSel, 'click');
- changeBranchName(branchName);
-
- cancelBtn().click();
-
- expect(branchNameEl()).toHaveValue('');
- expect(spyBackEvent).toHaveBeenTriggered();
- });
-
- it('resets the form when back btn is clicked', () => {
- changeBranchName(branchName);
-
- backBtn().click();
-
- expect(branchNameEl()).toHaveValue('');
- });
-
- describe('new branch creation', () => {
- beforeEach(() => {
- changeBranchName(branchName);
- });
- it('sets the new branch name and updates the dropdown', () => {
- spyOn(dropdown, 'setNewBranch');
-
- createBranchBtn().click();
-
- expect(dropdown.setNewBranch).toHaveBeenCalledWith(branchName);
- });
-
- it('resets the form', () => {
- createBranchBtn().click();
-
- expect(branchNameEl()).toHaveValue('');
- });
-
- it('is triggered with enter keypress', () => {
- spyOn(dropdown, 'setNewBranch');
- const enterEvent = new Event('keydown');
- enterEvent.which = 13;
- branchNameEl().dispatchEvent(enterEvent);
-
- expect(dropdown.setNewBranch).toHaveBeenCalledWith(branchName);
- });
- });
-});
diff --git a/spec/javascripts/blob/target_branch_dropdown_spec.js b/spec/javascripts/blob/target_branch_dropdown_spec.js
deleted file mode 100644
index 99c9537d2ec..00000000000
--- a/spec/javascripts/blob/target_branch_dropdown_spec.js
+++ /dev/null
@@ -1,118 +0,0 @@
-import '~/gl_dropdown';
-import '~/blob/create_branch_dropdown';
-import '~/blob/target_branch_dropdown';
-
-describe('TargetBranchDropdown', () => {
- const fixtureTemplate = 'static/target_branch_dropdown.html.raw';
- let dropdown;
-
- function createDropdown() {
- const projectBranches = getJSONFixture('project_branches.json');
- const dropdownEl = document.querySelector('.js-project-branches-dropdown');
- dropdown = new gl.TargetBranchDropDown(dropdownEl);
- dropdown.cachedRefs = projectBranches;
- dropdown.refreshData();
- return dropdown;
- }
-
- function submitBtn() {
- return document.querySelector('button[type="submit"]');
- }
-
- function searchField() {
- return document.querySelector('.dropdown-page-one .dropdown-input-field');
- }
-
- function element() {
- return document.querySelectorAll('div.dropdown-content li a');
- }
-
- function elementAtIndex(index) {
- return element()[index];
- }
-
- function clickElementAtIndex(index) {
- elementAtIndex(index).click();
- }
-
- preloadFixtures(fixtureTemplate);
-
- beforeEach(() => {
- loadFixtures(fixtureTemplate);
- createDropdown();
- });
-
- it('disable submit when branch is not selected', () => {
- document.querySelector('input[name="target_branch"]').value = null;
- clickElementAtIndex(1);
-
- expect(submitBtn().getAttribute('disabled')).toEqual('');
- });
-
- it('enable submit when a branch is selected', () => {
- clickElementAtIndex(1);
-
- expect(submitBtn().getAttribute('disabled')).toBe(null);
- });
-
- it('triggers change.branch event on a branch click', () => {
- spyOnEvent(dropdown.$dropdown, 'change.branch');
- clickElementAtIndex(0);
-
- expect('change.branch').toHaveBeenTriggeredOn(dropdown.$dropdown);
- });
-
- describe('dropdownData', () => {
- it('cache the refs', () => {
- const refs = dropdown.cachedRefs;
- dropdown.cachedRefs = null;
-
- dropdown.dropdownData(refs);
-
- expect(dropdown.cachedRefs).toEqual(refs);
- });
-
- it('returns the Branches with the newBranch and defaultBranch', () => {
- const refs = dropdown.cachedRefs;
- dropdown.branchInput.value = 'master';
- dropdown.newBranch = { id: 'new_branch', text: 'new_branch', title: 'new_branch' };
-
- const branches = dropdown.dropdownData(refs).Branches;
-
- expect(branches.length).toEqual(4);
- expect(branches[0]).toEqual(dropdown.newBranch);
- expect(branches[1]).toEqual({ id: 'master', text: 'master', title: 'master' });
- expect(branches[2]).toEqual({ id: 'development', text: 'development', title: 'development' });
- expect(branches[3]).toEqual({ id: 'staging', text: 'staging', title: 'staging' });
- });
- });
-
- describe('setNewBranch', () => {
- it('adds the new branch and select it', () => {
- const branchName = 'new_branch';
-
- dropdown.setNewBranch(branchName);
-
- expect(elementAtIndex(0)).toHaveClass('is-active');
- expect(elementAtIndex(0)).toContainHtml(branchName);
- });
-
- it("doesn't add a new branch if already exists in the list", () => {
- const branchName = elementAtIndex(0).text;
- const initialLength = element().length;
-
- dropdown.setNewBranch(branchName);
-
- expect(element().length).toEqual(initialLength);
- });
-
- it('clears the search filter', () => {
- const branchName = elementAtIndex(0).text;
- searchField().value = 'searching';
-
- dropdown.setNewBranch(branchName);
-
- expect(searchField().value).toEqual('');
- });
- });
-});
diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js
index 4c8a48580d7..be90dbdd88a 100644
--- a/spec/javascripts/build_spec.js
+++ b/spec/javascripts/build_spec.js
@@ -132,23 +132,6 @@ describe('Build', () => {
expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/);
expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
});
-
- it('reloads the page when the build is done', () => {
- spyOn(gl.utils, 'visitUrl');
- const deferred = $.Deferred();
-
- spyOn($, 'ajax').and.returnValue(deferred.promise());
- deferred.resolve({
- html: '<span>Final</span>',
- status: 'passed',
- append: true,
- complete: true,
- });
-
- this.build = new Build();
-
- expect(gl.utils.visitUrl).toHaveBeenCalledWith(BUILD_URL);
- });
});
describe('truncated information', () => {
diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/datetime_utility_spec.js
index c82ad0bea48..e54ea11b08c 100644
--- a/spec/javascripts/datetime_utility_spec.js
+++ b/spec/javascripts/datetime_utility_spec.js
@@ -1,4 +1,4 @@
-import '~/lib/utils/datetime_utility';
+import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
(() => {
describe('Date time utils', () => {
@@ -82,4 +82,13 @@ import '~/lib/utils/datetime_utility';
});
});
});
+
+ describe('timeIntervalInWords', () => {
+ it('should return string with number of minutes and seconds', () => {
+ expect(timeIntervalInWords(9.54)).toEqual('9 seconds');
+ expect(timeIntervalInWords(1)).toEqual('1 second');
+ expect(timeIntervalInWords(200)).toEqual('3 minutes 20 seconds');
+ expect(timeIntervalInWords(6008)).toEqual('100 minutes 8 seconds');
+ });
+ });
})();
diff --git a/spec/javascripts/fixtures/project_branches.json b/spec/javascripts/fixtures/project_branches.json
deleted file mode 100644
index a96a4c0c095..00000000000
--- a/spec/javascripts/fixtures/project_branches.json
+++ /dev/null
@@ -1,5 +0,0 @@
-[
- "master",
- "development",
- "staging"
-]
diff --git a/spec/javascripts/fixtures/target_branch_dropdown.html.haml b/spec/javascripts/fixtures/target_branch_dropdown.html.haml
deleted file mode 100644
index 821fb7940a0..00000000000
--- a/spec/javascripts/fixtures/target_branch_dropdown.html.haml
+++ /dev/null
@@ -1,28 +0,0 @@
-%form.js-edit-blob-form
- %input{type: 'hidden', name: 'target_branch', value: 'master'}
- %div
- .dropdown
- %button.dropdown-menu-toggle.js-project-branches-dropdown.js-target-branch{type: 'button', data: {toggle: 'dropdown', selected: 'master', field_name: 'target_branch', form_id: '.js-edit-blob-form'}}
- .dropdown-menu.dropdown-menu-selectable.dropdown-menu-paging
- .dropdown-page-one
- .dropdown-title 'Select branch'
- .dropdown-input
- %input.dropdown-input-field{type: 'search', value: ''}
- %i.fa.fa-search.dropdown-input-search
- %i.fa.fa-times-dropdown-input-clear.js-dropdown-input-clear{role: 'button'}
- .dropdown-content
- .dropdown-footer
- %ul.dropdown-footer-list
- %li
- %a.create-new-branch.dropdown-toggle-page{href: "#"}
- Create new branch
- .dropdown-page-two.dropdown-new-branch
- %button.dropdown-title-button.dropdown-menu-back{type: 'button'}
- .dropdown_title 'Create new branch'
- .dropdown_content
- %input#new_branch_name.default-dropdown-input{ type: "text", placeholder: "Name new branch" }
- %button.btn.btn-primary.pull-left.js-new-branch-btn{ type: "button" }
- Create
- %button.btn.btn-default.pull-right.js-cancel-branch-btn{ type: "button" }
- Cancel
- %button{type: 'submit'}
diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js
index 3292590b9ed..10fcc590c89 100644
--- a/spec/javascripts/gl_dropdown_spec.js
+++ b/spec/javascripts/gl_dropdown_spec.js
@@ -185,7 +185,7 @@ import '~/lib/utils/url_utility';
expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
});
- it('should focus on input when opening for the second time', () => {
+ it('should focus on input when opening for the second time after transition', () => {
remoteCallback();
this.dropdownContainerElement.trigger({
type: 'keyup',
@@ -193,6 +193,7 @@ import '~/lib/utils/url_utility';
keyCode: ARROW_KEYS.ESC
});
this.dropdownButtonElement.click();
+ this.dropdownContainerElement.trigger('transitionend');
expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
});
});
@@ -201,6 +202,7 @@ import '~/lib/utils/url_utility';
it('should focus input when passing array data to drop down', () => {
initDropDown.call(this, false, true);
this.dropdownButtonElement.click();
+ this.dropdownContainerElement.trigger('transitionend');
expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
});
});
diff --git a/spec/javascripts/groups/group_item_spec.js b/spec/javascripts/groups/group_item_spec.js
new file mode 100644
index 00000000000..25e10552d95
--- /dev/null
+++ b/spec/javascripts/groups/group_item_spec.js
@@ -0,0 +1,102 @@
+import Vue from 'vue';
+import groupItemComponent from '~/groups/components/group_item.vue';
+import GroupsStore from '~/groups/stores/groups_store';
+import { group1 } from './mock_data';
+
+describe('Groups Component', () => {
+ let GroupItemComponent;
+ let component;
+ let store;
+ let group;
+
+ describe('group with default data', () => {
+ beforeEach((done) => {
+ GroupItemComponent = Vue.extend(groupItemComponent);
+ store = new GroupsStore();
+ group = store.decorateGroup(group1);
+
+ component = new GroupItemComponent({
+ propsData: {
+ group,
+ },
+ }).$mount();
+
+ Vue.nextTick(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ component.$destroy();
+ });
+
+ it('should render the group item correctly', () => {
+ expect(component.$el.classList.contains('group-row')).toBe(true);
+ expect(component.$el.classList.contains('.no-description')).toBe(false);
+ expect(component.$el.querySelector('.number-projects').textContent).toContain(group.numberProjects);
+ expect(component.$el.querySelector('.number-users').textContent).toContain(group.numberUsers);
+ expect(component.$el.querySelector('.group-visibility')).toBeDefined();
+ expect(component.$el.querySelector('.avatar-container')).toBeDefined();
+ expect(component.$el.querySelector('.title').textContent).toContain(group.name);
+ expect(component.$el.querySelector('.access-type').textContent).toContain(group.permissions.humanGroupAccess);
+ expect(component.$el.querySelector('.description').textContent).toContain(group.description);
+ expect(component.$el.querySelector('.edit-group')).toBeDefined();
+ expect(component.$el.querySelector('.leave-group')).toBeDefined();
+ });
+ });
+
+ describe('group without description', () => {
+ beforeEach((done) => {
+ GroupItemComponent = Vue.extend(groupItemComponent);
+ store = new GroupsStore();
+ group1.description = '';
+ group = store.decorateGroup(group1);
+
+ component = new GroupItemComponent({
+ propsData: {
+ group,
+ },
+ }).$mount();
+
+ Vue.nextTick(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ component.$destroy();
+ });
+
+ it('should render group item correctly', () => {
+ expect(component.$el.querySelector('.description').textContent).toBe('');
+ expect(component.$el.classList.contains('.no-description')).toBe(false);
+ });
+ });
+
+ describe('user has not access to group', () => {
+ beforeEach((done) => {
+ GroupItemComponent = Vue.extend(groupItemComponent);
+ store = new GroupsStore();
+ group1.permissions.human_group_access = null;
+ group = store.decorateGroup(group1);
+
+ component = new GroupItemComponent({
+ propsData: {
+ group,
+ },
+ }).$mount();
+
+ Vue.nextTick(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ component.$destroy();
+ });
+
+ it('should not display access type', () => {
+ expect(component.$el.querySelector('.access-type')).toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/groups/groups_spec.js b/spec/javascripts/groups/groups_spec.js
new file mode 100644
index 00000000000..2a77f7259da
--- /dev/null
+++ b/spec/javascripts/groups/groups_spec.js
@@ -0,0 +1,64 @@
+import Vue from 'vue';
+import groupFolderComponent from '~/groups/components/group_folder.vue';
+import groupItemComponent from '~/groups/components/group_item.vue';
+import groupsComponent from '~/groups/components/groups.vue';
+import GroupsStore from '~/groups/stores/groups_store';
+import { groupsData } from './mock_data';
+
+describe('Groups Component', () => {
+ let GroupsComponent;
+ let store;
+ let component;
+ let groups;
+
+ beforeEach((done) => {
+ Vue.component('group-folder', groupFolderComponent);
+ Vue.component('group-item', groupItemComponent);
+
+ store = new GroupsStore();
+ groups = store.setGroups(groupsData.groups);
+
+ store.storePagination(groupsData.pagination);
+
+ GroupsComponent = Vue.extend(groupsComponent);
+
+ component = new GroupsComponent({
+ propsData: {
+ groups: store.state.groups,
+ pageInfo: store.state.pageInfo,
+ },
+ }).$mount();
+
+ Vue.nextTick(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ component.$destroy();
+ });
+
+ describe('with data', () => {
+ it('should render a list of groups', () => {
+ expect(component.$el.classList.contains('groups-list-tree-container')).toBe(true);
+ expect(component.$el.querySelector('#group-12')).toBeDefined();
+ expect(component.$el.querySelector('#group-1119')).toBeDefined();
+ expect(component.$el.querySelector('#group-1120')).toBeDefined();
+ });
+
+ it('should render group and its subgroup', () => {
+ const lists = component.$el.querySelectorAll('.group-list-tree');
+
+ expect(lists.length).toBe(3); // one parent and two subgroups
+
+ expect(lists[0].querySelector('#group-1119').classList.contains('is-open')).toBe(true);
+ expect(lists[0].querySelector('#group-1119').classList.contains('has-subgroups')).toBe(true);
+
+ expect(lists[2].querySelector('#group-1120').textContent).toContain(groups[1119].subGroups[1120].name);
+ });
+
+ it('should remove prefix of parent group', () => {
+ expect(component.$el.querySelector('#group-12 #group-1128 .title').textContent).toContain('level2 / level3 / level4');
+ });
+ });
+});
diff --git a/spec/javascripts/groups/mock_data.js b/spec/javascripts/groups/mock_data.js
new file mode 100644
index 00000000000..1c0ec7a97d0
--- /dev/null
+++ b/spec/javascripts/groups/mock_data.js
@@ -0,0 +1,110 @@
+const group1 = {
+ id: '12',
+ name: 'level1',
+ path: 'level1',
+ description: 'foo',
+ visibility: 'public',
+ avatar_url: null,
+ web_url: 'http://localhost:3000/groups/level1',
+ full_name: 'level1',
+ full_path: 'level1',
+ parent_id: null,
+ created_at: '2017-05-15T19:01:23.670Z',
+ updated_at: '2017-05-15T19:01:23.670Z',
+ number_projects_with_delimiter: '1',
+ number_users_with_delimiter: '1',
+ has_subgroups: true,
+ permissions: {
+ human_group_access: 'Master',
+ },
+};
+
+// This group has no direct parent, should be placed as subgroup of group1
+const group14 = {
+ id: 1128,
+ name: 'level4',
+ path: 'level4',
+ description: 'foo',
+ visibility: 'public',
+ avatar_url: null,
+ web_url: 'http://localhost:3000/groups/level1/level2/level3/level4',
+ full_name: 'level1 / level2 / level3 / level4',
+ full_path: 'level1/level2/level3/level4',
+ parent_id: 1127,
+ created_at: '2017-05-15T19:02:01.645Z',
+ updated_at: '2017-05-15T19:02:01.645Z',
+ number_projects_with_delimiter: '1',
+ number_users_with_delimiter: '1',
+ has_subgroups: true,
+ permissions: {
+ human_group_access: 'Master',
+ },
+};
+
+const group2 = {
+ id: 1119,
+ name: 'devops',
+ path: 'devops',
+ description: 'foo',
+ visibility: 'public',
+ avatar_url: null,
+ web_url: 'http://localhost:3000/groups/devops',
+ full_name: 'devops',
+ full_path: 'devops',
+ parent_id: null,
+ created_at: '2017-05-11T19:35:09.635Z',
+ updated_at: '2017-05-11T19:35:09.635Z',
+ number_projects_with_delimiter: '1',
+ number_users_with_delimiter: '1',
+ has_subgroups: true,
+ permissions: {
+ human_group_access: 'Master',
+ },
+};
+
+const group21 = {
+ id: 1120,
+ name: 'chef',
+ path: 'chef',
+ description: 'foo',
+ visibility: 'public',
+ avatar_url: null,
+ web_url: 'http://localhost:3000/groups/devops/chef',
+ full_name: 'devops / chef',
+ full_path: 'devops/chef',
+ parent_id: 1119,
+ created_at: '2017-05-11T19:51:04.060Z',
+ updated_at: '2017-05-11T19:51:04.060Z',
+ number_projects_with_delimiter: '1',
+ number_users_with_delimiter: '1',
+ has_subgroups: true,
+ permissions: {
+ human_group_access: 'Master',
+ },
+};
+
+const groupsData = {
+ groups: [group1, group14, group2, group21],
+ pagination: {
+ Date: 'Mon, 22 May 2017 22:31:52 GMT',
+ 'X-Prev-Page': '1',
+ 'X-Content-Type-Options': 'nosniff',
+ 'X-Total': '31',
+ 'Transfer-Encoding': 'chunked',
+ 'X-Runtime': '0.611144',
+ 'X-Xss-Protection': '1; mode=block',
+ 'X-Request-Id': 'f5db8368-3ce5-4aa4-89d2-a125d9dead09',
+ 'X-Ua-Compatible': 'IE=edge',
+ 'X-Per-Page': '20',
+ Link: '<http://localhost:3000/dashboard/groups.json?page=1&per_page=20>; rel="prev", <http://localhost:3000/dashboard/groups.json?page=1&per_page=20>; rel="first", <http://localhost:3000/dashboard/groups.json?page=2&per_page=20>; rel="last"',
+ 'X-Next-Page': '',
+ Etag: 'W/"a82f846947136271cdb7d55d19ef33d2"',
+ 'X-Frame-Options': 'DENY',
+ 'Content-Type': 'application/json; charset=utf-8',
+ 'Cache-Control': 'max-age=0, private, must-revalidate',
+ 'X-Total-Pages': '2',
+ 'X-Page': '2',
+ },
+};
+
+export { groupsData, group1 };
diff --git a/spec/javascripts/jobs/header_spec.js b/spec/javascripts/jobs/header_spec.js
new file mode 100644
index 00000000000..c7179b3e03d
--- /dev/null
+++ b/spec/javascripts/jobs/header_spec.js
@@ -0,0 +1,63 @@
+import Vue from 'vue';
+import headerComponent from '~/jobs/components/header.vue';
+
+describe('Job details header', () => {
+ let HeaderComponent;
+ let vm;
+ let props;
+
+ beforeEach(() => {
+ HeaderComponent = Vue.extend(headerComponent);
+
+ const threeWeeksAgo = new Date();
+ threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
+
+ props = {
+ job: {
+ status: {
+ group: 'failed',
+ icon: 'ci-status-failed',
+ label: 'failed',
+ text: 'failed',
+ details_path: 'path',
+ },
+ id: 123,
+ created_at: threeWeeksAgo.toISOString(),
+ user: {
+ web_url: 'path',
+ name: 'Foo',
+ username: 'foobar',
+ email: 'foo@bar.com',
+ avatar_url: 'link',
+ },
+ retry_path: 'path',
+ new_issue_path: 'path',
+ },
+ isLoading: false,
+ };
+
+ vm = new HeaderComponent({ propsData: props }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render provided job information', () => {
+ expect(
+ vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(),
+ ).toEqual('failed Job #123 triggered 3 weeks ago by Foo');
+ });
+
+ it('should render retry link', () => {
+ expect(
+ vm.$el.querySelector('.js-retry-button').getAttribute('href'),
+ ).toEqual(props.job.retry_path);
+ });
+
+ it('should render new issue link', () => {
+ expect(
+ vm.$el.querySelector('.js-new-issue').getAttribute('href'),
+ ).toEqual(props.job.new_issue_path);
+ });
+});
diff --git a/spec/javascripts/jobs/job_details_mediator_spec.js b/spec/javascripts/jobs/job_details_mediator_spec.js
new file mode 100644
index 00000000000..1d7fa7e12fc
--- /dev/null
+++ b/spec/javascripts/jobs/job_details_mediator_spec.js
@@ -0,0 +1,43 @@
+import Vue from 'vue';
+import JobMediator from '~/jobs/job_details_mediator';
+import job from './mock_data';
+
+describe('JobMediator', () => {
+ let mediator;
+
+ beforeEach(() => {
+ mediator = new JobMediator({ endpoint: 'foo' });
+ });
+
+ it('should set defaults', () => {
+ expect(mediator.store).toBeDefined();
+ expect(mediator.service).toBeDefined();
+ expect(mediator.options).toEqual({ endpoint: 'foo' });
+ expect(mediator.state.isLoading).toEqual(false);
+ });
+
+ describe('request and store data', () => {
+ const interceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify(job), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(interceptor);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptor, interceptor);
+ });
+
+ it('should store received data', (done) => {
+ mediator.fetchJob();
+
+ setTimeout(() => {
+ expect(mediator.store.state.job).toEqual(job);
+ done();
+ }, 0);
+ });
+ });
+});
diff --git a/spec/javascripts/jobs/job_store_spec.js b/spec/javascripts/jobs/job_store_spec.js
new file mode 100644
index 00000000000..d00faf29d1e
--- /dev/null
+++ b/spec/javascripts/jobs/job_store_spec.js
@@ -0,0 +1,26 @@
+import JobStore from '~/jobs/stores/job_store';
+import job from './mock_data';
+
+describe('Job Store', () => {
+ let store;
+
+ beforeEach(() => {
+ store = new JobStore();
+ });
+
+ it('should set defaults', () => {
+ expect(store.state.job).toEqual({});
+ });
+
+ describe('storeJob', () => {
+ it('should store empty object if none is provided', () => {
+ store.storeJob();
+ expect(store.state.job).toEqual({});
+ });
+
+ it('should store provided argument', () => {
+ store.storeJob(job);
+ expect(store.state.job).toEqual(job);
+ });
+ });
+});
diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js
new file mode 100644
index 00000000000..17e4ef26b2c
--- /dev/null
+++ b/spec/javascripts/jobs/mock_data.js
@@ -0,0 +1,123 @@
+const threeWeeksAgo = new Date();
+threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
+
+export default {
+ id: 4757,
+ name: 'test',
+ build_path: '/root/ci-mock/-/jobs/4757',
+ retry_path: '/root/ci-mock/-/jobs/4757/retry',
+ cancel_path: '/root/ci-mock/-/jobs/4757/cancel',
+ new_issue_path: '/root/ci-mock/issues/new',
+ playable: false,
+ created_at: threeWeeksAgo.toISOString(),
+ updated_at: threeWeeksAgo.toISOString(),
+ finished_at: threeWeeksAgo.toISOString(),
+ queued: 9.54,
+ status: {
+ icon: 'icon_status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: '/root/ci-mock/-/jobs/4757',
+ favicon: '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ action: {
+ icon: 'icon_action_retry',
+ title: 'Retry',
+ path: '/root/ci-mock/-/jobs/4757/retry',
+ method: 'post',
+ },
+ },
+ coverage: 20,
+ erased_at: threeWeeksAgo.toISOString(),
+ duration: 6.785563,
+ tags: ['tag'],
+ 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',
+ },
+ erase_path: '/root/ci-mock/-/jobs/4757/erase',
+ artifacts: [null],
+ runner: {
+ id: 1,
+ description: 'local ci runner',
+ edit_path: '/root/ci-mock/runners/1/edit',
+ },
+ pipeline: {
+ id: 140,
+ 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',
+ },
+ active: false,
+ coverage: null,
+ source: 'unknown',
+ created_at: '2017-05-24T09:59:58.634Z',
+ updated_at: '2017-06-01T17:32:00.062Z',
+ path: '/root/ci-mock/pipelines/140',
+ flags: {
+ latest: true,
+ stuck: false,
+ yaml_errors: false,
+ retryable: false,
+ cancelable: false,
+ },
+ details: {
+ status: {
+ icon: 'icon_status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: '/root/ci-mock/pipelines/140',
+ favicon: '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ },
+ duration: 6,
+ finished_at: '2017-06-01T17:32:00.042Z',
+ },
+ ref: {
+ name: 'abc',
+ path: '/root/ci-mock/commits/abc',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: 'c58647773a6b5faf066d4ad6ff2c9fbba5f180f6',
+ short_id: 'c5864777',
+ title: 'Add new file',
+ created_at: '2017-05-24T10:59:52.000+01:00',
+ parent_ids: ['798e5f902592192afaba73f4668ae30e56eae492'],
+ message: 'Add new file',
+ author_name: 'Root',
+ author_email: 'admin@example.com',
+ authored_date: '2017-05-24T10:59:52.000+01:00',
+ committer_name: 'Root',
+ committer_email: 'admin@example.com',
+ committed_date: '2017-05-24T10:59:52.000+01: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/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6',
+ commit_path: '/root/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6',
+ },
+ },
+ merge_request: {
+ iid: 2,
+ path: '/root/ci-mock/merge_requests/2',
+ },
+ raw_path: '/root/ci-mock/builds/4757/raw',
+};
diff --git a/spec/javascripts/jobs/sidebar_detail_row_spec.js b/spec/javascripts/jobs/sidebar_detail_row_spec.js
new file mode 100644
index 00000000000..3ac65709c4a
--- /dev/null
+++ b/spec/javascripts/jobs/sidebar_detail_row_spec.js
@@ -0,0 +1,40 @@
+import Vue from 'vue';
+import sidebarDetailRow from '~/jobs/components/sidebar_detail_row.vue';
+
+describe('Sidebar detail row', () => {
+ let SidebarDetailRow;
+ let vm;
+
+ beforeEach(() => {
+ SidebarDetailRow = Vue.extend(sidebarDetailRow);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render no title', () => {
+ vm = new SidebarDetailRow({
+ propsData: {
+ value: 'this is the value',
+ },
+ }).$mount();
+
+ expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual('this is the value');
+ });
+
+ beforeEach(() => {
+ vm = new SidebarDetailRow({
+ propsData: {
+ title: 'this is the title',
+ value: 'this is the value',
+ },
+ }).$mount();
+ });
+
+ it('should render provided title and value', () => {
+ expect(
+ vm.$el.textContent.replace(/\s+/g, ' ').trim(),
+ ).toEqual('this is the title: this is the value');
+ });
+});
diff --git a/spec/javascripts/jobs/sidebar_details_block_spec.js b/spec/javascripts/jobs/sidebar_details_block_spec.js
new file mode 100644
index 00000000000..95532ef5382
--- /dev/null
+++ b/spec/javascripts/jobs/sidebar_details_block_spec.js
@@ -0,0 +1,111 @@
+import Vue from 'vue';
+import sidebarDetailsBlock from '~/jobs/components/sidebar_details_block.vue';
+import job from './mock_data';
+
+describe('Sidebar details block', () => {
+ let SidebarComponent;
+ let vm;
+
+ function trimWhitespace(element) {
+ return element.textContent.replace(/\s+/g, ' ').trim();
+ }
+
+ beforeEach(() => {
+ SidebarComponent = Vue.extend(sidebarDetailsBlock);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('when it is loading', () => {
+ it('should render a loading spinner', () => {
+ vm = new SidebarComponent({
+ propsData: {
+ job: {},
+ isLoading: true,
+ },
+ }).$mount();
+
+ expect(vm.$el.querySelector('.fa-spinner')).toBeDefined();
+ });
+ });
+
+ beforeEach(() => {
+ vm = new SidebarComponent({
+ propsData: {
+ job,
+ isLoading: false,
+ },
+ }).$mount();
+ });
+
+ describe('actions', () => {
+ it('should render link to new issue', () => {
+ expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual(job.new_issue_path);
+ expect(vm.$el.querySelector('.js-new-issue').textContent.trim()).toEqual('New issue');
+ });
+
+ it('should render link to retry job', () => {
+ expect(vm.$el.querySelector('.js-retry-job').getAttribute('href')).toEqual(job.retry_path);
+ });
+
+ it('should render link to cancel job', () => {
+ expect(vm.$el.querySelector('.js-cancel-job').getAttribute('href')).toEqual(job.cancel_path);
+ });
+ });
+
+ describe('information', () => {
+ it('should render merge request link', () => {
+ expect(
+ trimWhitespace(vm.$el.querySelector('.js-job-mr')),
+ ).toEqual('Merge Request: !2');
+
+ expect(
+ vm.$el.querySelector('.js-job-mr a').getAttribute('href'),
+ ).toEqual(job.merge_request.path);
+ });
+
+ it('should render job duration', () => {
+ expect(
+ trimWhitespace(vm.$el.querySelector('.js-job-duration')),
+ ).toEqual('Duration: 6 seconds');
+ });
+
+ it('should render erased date', () => {
+ expect(
+ trimWhitespace(vm.$el.querySelector('.js-job-erased')),
+ ).toEqual('Erased: 3 weeks ago');
+ });
+
+ it('should render finished date', () => {
+ expect(
+ trimWhitespace(vm.$el.querySelector('.js-job-finished')),
+ ).toEqual('Finished: 3 weeks ago');
+ });
+
+ it('should render queued date', () => {
+ expect(
+ trimWhitespace(vm.$el.querySelector('.js-job-queued')),
+ ).toEqual('Queued: 9 seconds');
+ });
+
+ it('should render runner ID', () => {
+ expect(
+ trimWhitespace(vm.$el.querySelector('.js-job-runner')),
+ ).toEqual('Runner: #1');
+ });
+
+ it('should render coverage', () => {
+ expect(
+ trimWhitespace(vm.$el.querySelector('.js-job-coverage')),
+ ).toEqual('Coverage: 20%');
+ });
+
+ it('should render tags', () => {
+ expect(
+ trimWhitespace(vm.$el.querySelector('.js-job-tags')),
+ ).toEqual('Tags: tag');
+ });
+ });
+});
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index e3938a77680..52cf217c25f 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -150,6 +150,14 @@ import '~/lib/utils/common_utils';
const value = gl.utils.getParameterByName('fakeParameter');
expect(value).toBe(null);
});
+
+ it('should return valid paramentes if URL is provided', () => {
+ let value = gl.utils.getParameterByName('foo', 'http://cocteau.twins/?foo=bar');
+ expect(value).toBe('bar');
+
+ value = gl.utils.getParameterByName('manan', 'http://cocteau.twins/?foo=bar&manan=canchu');
+ expect(value).toBe('canchu');
+ });
});
describe('gl.utils.normalizedHeaders', () => {
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 24335614e09..bfd8b8648a6 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -461,6 +461,45 @@ import '~/notes';
});
});
+ describe('update comment with script tags', () => {
+ const sampleComment = '<script></script>';
+ const updatedComment = '<script></script>';
+ const note = {
+ id: 1234,
+ html: `<li class="note note-row-1234 timeline-entry" id="note_1234">
+ <div class="note-text">${sampleComment}</div>
+ </li>`,
+ note: sampleComment,
+ valid: true
+ };
+ let $form;
+ let $notesContainer;
+
+ beforeEach(() => {
+ this.notes = new Notes('', []);
+ window.gon.current_username = 'root';
+ window.gon.current_user_fullname = 'Administrator';
+ $form = $('form.js-main-target-form');
+ $notesContainer = $('ul.main-notes-list');
+ $form.find('textarea.js-note-text').html(sampleComment);
+ });
+
+ it('should not render a script tag', () => {
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+ $('.js-comment-button').click();
+
+ deferred.resolve(note);
+ const $noteEl = $notesContainer.find(`#note_${note.id}`);
+ $noteEl.find('.js-note-edit').click();
+ $noteEl.find('textarea.js-note-text').html(updatedComment);
+ $noteEl.find('.js-comment-save-button').click();
+
+ const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`).find('.js-task-list-container');
+ expect($updatedNoteEl.find('.note-text').text().trim()).toEqual('');
+ });
+ });
+
describe('getFormData', () => {
let $form;
let sampleComment;
diff --git a/spec/javascripts/pipelines/nav_controls_spec.js b/spec/javascripts/pipelines/nav_controls_spec.js
index 601eebce38a..f1697840fcd 100644
--- a/spec/javascripts/pipelines/nav_controls_spec.js
+++ b/spec/javascripts/pipelines/nav_controls_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import navControlsComp from '~/pipelines/components/nav_controls';
+import navControlsComp from '~/pipelines/components/nav_controls.vue';
describe('Pipelines Nav Controls', () => {
let NavControlsComponent;
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index 13827a26571..2c34402576b 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -51,7 +51,6 @@ if (process.env.BABEL_ENV === 'coverage') {
'./environments/environments_bundle.js',
'./filtered_search/filtered_search_bundle.js',
'./graphs/graphs_bundle.js',
- './issuable/issuable_bundle.js',
'./issuable/time_tracking/time_tracking_bundle.js',
'./main.js',
'./merge_conflicts/merge_conflicts_bundle.js',
diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js
index 050170a54e9..540245fe71e 100644
--- a/spec/javascripts/vue_shared/components/commit_spec.js
+++ b/spec/javascripts/vue_shared/components/commit_spec.js
@@ -22,7 +22,7 @@ describe('Commit component', () => {
shortSha: 'b7836edd',
title: 'Commit message',
author: {
- avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png',
+ avatar_url: 'https://gitlab.com/uploads/system/user/avatar/300478/avatar.png',
web_url: 'https://gitlab.com/jschatz1',
path: '/jschatz1',
username: 'jschatz1',
@@ -45,7 +45,7 @@ describe('Commit component', () => {
shortSha: 'b7836edd',
title: 'Commit message',
author: {
- avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png',
+ avatar_url: 'https://gitlab.com/uploads/system/user/avatar/300478/avatar.png',
web_url: 'https://gitlab.com/jschatz1',
path: '/jschatz1',
username: 'jschatz1',
diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
index 2b51c89f311..e28639f12f3 100644
--- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js
+++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
@@ -43,6 +43,7 @@ describe('Header CI Component', () => {
isLoading: false,
},
],
+ hasSidebarButton: true,
};
vm = new HeaderCi({
@@ -90,4 +91,8 @@ describe('Header CI Component', () => {
done();
});
});
+
+ it('should render sidebar toggle button', () => {
+ expect(vm.$el.querySelector('.js-sidebar-build-toggle')).toBeDefined();
+ });
});
diff --git a/spec/lib/banzai/filter/abstract_reference_filter_spec.rb b/spec/lib/banzai/filter/abstract_reference_filter_spec.rb
index 27684882435..787c2372c5b 100644
--- a/spec/lib/banzai/filter/abstract_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/abstract_reference_filter_spec.rb
@@ -47,16 +47,7 @@ describe Banzai::Filter::AbstractReferenceFilter do
end
end
- context 'with RequestStore enabled' do
- before do
- RequestStore.begin!
- end
-
- after do
- RequestStore.end!
- RequestStore.clear!
- end
-
+ context 'with RequestStore enabled', :request_store do
it 'returns a list of Projects for a list of paths' do
expect(filter.find_projects_for_paths([project.path_with_namespace])).
to eq([project])
diff --git a/spec/lib/banzai/issuable_extractor_spec.rb b/spec/lib/banzai/issuable_extractor_spec.rb
index e5d332efb08..866297f94a9 100644
--- a/spec/lib/banzai/issuable_extractor_spec.rb
+++ b/spec/lib/banzai/issuable_extractor_spec.rb
@@ -29,16 +29,7 @@ describe Banzai::IssuableExtractor, lib: true do
expect(result).to eq(issue_link => issue, merge_request_link => merge_request)
end
- describe 'caching' do
- before do
- RequestStore.begin!
- end
-
- after do
- RequestStore.end!
- RequestStore.clear!
- end
-
+ describe 'caching', :request_store do
it 'saves records to cache' do
extractor.extract([issue_link, merge_request_link])
diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb
index d5746107ee1..f4f42bfc3ed 100644
--- a/spec/lib/banzai/reference_parser/base_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb
@@ -30,7 +30,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do
it 'checks if user can read the resource' do
link['data-project'] = project.id.to_s
- expect(subject).to receive(:can_read_reference?).with(user, project)
+ expect(subject).to receive(:can_read_reference?).with(user, project, link)
subject.nodes_visible_to_user(user, [link])
end
diff --git a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
index d217a775802..620875ece20 100644
--- a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
@@ -4,20 +4,199 @@ describe Banzai::ReferenceParser::SnippetParser, lib: true do
include ReferenceParserHelpers
let(:project) { create(:empty_project, :public) }
+
let(:user) { create(:user) }
- let(:snippet) { create(:snippet, project: project) }
+ let(:external_user) { create(:user, :external) }
+ let(:project_member) { create(:user) }
+
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
+ def visible_references(snippet_visibility, user = nil)
+ snippet = create(:project_snippet, snippet_visibility, project: project)
+ link['data-project'] = project.id.to_s
+ link['data-snippet'] = snippet.id.to_s
+
+ subject.nodes_visible_to_user(user, [link])
+ end
+
+ before do
+ project.add_user(project_member, :developer)
+ end
+
describe '#nodes_visible_to_user' do
- context 'when the link has a data-issue attribute' do
- before { link['data-snippet'] = snippet.id.to_s }
+ context 'when a project is public and the snippets feature is enabled for everyone' do
+ before do
+ project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::ENABLED)
+ end
+
+ it 'creates a reference for guest for a public snippet' do
+ expect(visible_references(:public)).to eq([link])
+ end
+
+ it 'creates a reference for a regular user for a public snippet' do
+ expect(visible_references(:public, user)).to eq([link])
+ end
+
+ it 'creates a reference for a regular user for an internal snippet' do
+ expect(visible_references(:internal, user)).to eq([link])
+ end
+
+ it 'does not create a reference for an external user for an internal snippet' do
+ expect(visible_references(:internal, external_user)).to be_empty
+ end
+
+ it 'creates a reference for a project member for a private snippet' do
+ expect(visible_references(:private, project_member)).to eq([link])
+ end
+
+ it 'does not create a reference for a regular user for a private snippet' do
+ expect(visible_references(:private, user)).to be_empty
+ end
+ end
+
+ context 'when a project is public and the snippets feature is enabled for project team members' do
+ before do
+ project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::PRIVATE)
+ end
+
+ it 'creates a reference for a project member for a public snippet' do
+ expect(visible_references(:public, project_member)).to eq([link])
+ end
+
+ it 'does not create a reference for guest for a public snippet' do
+ expect(visible_references(:public, nil)).to be_empty
+ end
+
+ it 'does not create a reference for a regular user for a public snippet' do
+ expect(visible_references(:public, user)).to be_empty
+ end
+
+ it 'creates a reference for a project member for an internal snippet' do
+ expect(visible_references(:internal, project_member)).to eq([link])
+ end
+
+ it 'does not create a reference for a regular user for an internal snippet' do
+ expect(visible_references(:internal, user)).to be_empty
+ end
+
+ it 'creates a reference for a project member for a private snippet' do
+ expect(visible_references(:private, project_member)).to eq([link])
+ end
+
+ it 'does not create a reference for a regular user for a private snippet' do
+ expect(visible_references(:private, user)).to be_empty
+ end
+ end
+
+ context 'when a project is internal and the snippets feature is enabled for everyone' do
+ before do
+ project.update_attribute(:visibility, Gitlab::VisibilityLevel::INTERNAL)
+ project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::ENABLED)
+ end
+
+ it 'does not create a reference for guest for a public snippet' do
+ expect(visible_references(:public)).to be_empty
+ end
+
+ it 'does not create a reference for an external user for a public snippet' do
+ expect(visible_references(:public, external_user)).to be_empty
+ end
- it_behaves_like "referenced feature visibility", "snippets"
+ it 'creates a reference for a regular user for a public snippet' do
+ expect(visible_references(:public, user)).to eq([link])
+ end
+
+ it 'creates a reference for a regular user for an internal snippet' do
+ expect(visible_references(:internal, user)).to eq([link])
+ end
+
+ it 'does not create a reference for an external user for an internal snippet' do
+ expect(visible_references(:internal, external_user)).to be_empty
+ end
+
+ it 'creates a reference for a project member for a private snippet' do
+ expect(visible_references(:private, project_member)).to eq([link])
+ end
+
+ it 'does not create a reference for a regular user for a private snippet' do
+ expect(visible_references(:private, user)).to be_empty
+ end
+ end
+
+ context 'when a project is internal and the snippets feature is enabled for project team members' do
+ before do
+ project.update_attribute(:visibility, Gitlab::VisibilityLevel::INTERNAL)
+ project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::PRIVATE)
+ end
+
+ it 'creates a reference for a project member for a public snippet' do
+ expect(visible_references(:public, project_member)).to eq([link])
+ end
+
+ it 'does not create a reference for guest for a public snippet' do
+ expect(visible_references(:public, nil)).to be_empty
+ end
+
+ it 'does not create reference for a regular user for a public snippet' do
+ expect(visible_references(:public, user)).to be_empty
+ end
+
+ it 'creates a reference for a project member for an internal snippet' do
+ expect(visible_references(:internal, project_member)).to eq([link])
+ end
+
+ it 'does not create a reference for a regular user for an internal snippet' do
+ expect(visible_references(:internal, user)).to be_empty
+ end
+
+ it 'creates a reference for a project member for a private snippet' do
+ expect(visible_references(:private, project_member)).to eq([link])
+ end
+
+ it 'does not create reference for a regular user for a private snippet' do
+ expect(visible_references(:private, user)).to be_empty
+ end
+ end
+
+ context 'when a project is private and the snippets feature is enabled for project team members' do
+ before do
+ project.update_attribute(:visibility, Gitlab::VisibilityLevel::PRIVATE)
+ project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::PRIVATE)
+ end
+
+ it 'creates a reference for a project member for a public snippet' do
+ expect(visible_references(:public, project_member)).to eq([link])
+ end
+
+ it 'does not create a reference for guest for a public snippet' do
+ expect(visible_references(:public, nil)).to be_empty
+ end
+
+ it 'does not create a reference for a regular user for a public snippet' do
+ expect(visible_references(:public, user)).to be_empty
+ end
+
+ it 'creates a reference for a project member for an internal snippet' do
+ expect(visible_references(:internal, project_member)).to eq([link])
+ end
+
+ it 'does not create a reference for a regular user for an internal snippet' do
+ expect(visible_references(:internal, user)).to be_empty
+ end
+
+ it 'creates a reference for a project member for a private snippet' do
+ expect(visible_references(:private, project_member)).to eq([link])
+ end
+
+ it 'does not create a reference for a regular user for a private snippet' do
+ expect(visible_references(:private, user)).to be_empty
+ end
end
end
describe '#referenced_by' do
+ let(:snippet) { create(:snippet, project: project) }
describe 'when the link has a data-snippet attribute' do
context 'using an existing snippet ID' do
it 'returns an Array of snippets' do
@@ -31,7 +210,7 @@ describe Banzai::ReferenceParser::SnippetParser, lib: true do
it 'returns an empty Array' do
link['data-snippet'] = ''
- expect(subject.referenced_by([link])).to eq([])
+ expect(subject.referenced_by([link])).to be_empty
end
end
end
diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb
index 592ed0d2b98..4d560667342 100644
--- a/spec/lib/banzai/reference_parser/user_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb
@@ -43,18 +43,9 @@ describe Banzai::ReferenceParser::UserParser, lib: true do
expect(subject.referenced_by([link])).to eq([user])
end
- context 'when RequestStore is active' do
+ context 'when RequestStore is active', :request_store do
let(:other_user) { create(:user) }
- before do
- RequestStore.begin!
- end
-
- after do
- RequestStore.end!
- RequestStore.clear!
- end
-
it 'does not return users from the first call in the second' do
link['data-user'] = user.id.to_s
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index d6006eab0c9..d09da951869 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -204,6 +204,12 @@ describe Gitlab::Auth, lib: true do
expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login)
expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new)
end
+
+ it 'throws an error suggesting user create a PAT when internal auth is disabled' do
+ allow_any_instance_of(ApplicationSetting).to receive(:signin_enabled?) { false }
+
+ expect { gl_auth.find_for_git_client('foo', 'bar', project: nil, ip: 'ip') }.to raise_error(Gitlab::Auth::MissingPersonalTokenError)
+ end
end
describe 'find_with_user_password' do
diff --git a/spec/lib/gitlab/background_migration_spec.rb b/spec/lib/gitlab/background_migration_spec.rb
new file mode 100644
index 00000000000..f2073b9bcb3
--- /dev/null
+++ b/spec/lib/gitlab/background_migration_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration do
+ describe '.steal' do
+ it 'steals jobs from a queue' do
+ queue = [double(:job, args: ['Foo', [10, 20]])]
+
+ allow(Sidekiq::Queue).to receive(:new).
+ with(BackgroundMigrationWorker.sidekiq_options['queue']).
+ and_return(queue)
+
+ expect(queue[0]).to receive(:delete)
+
+ expect(described_class).to receive(:perform).with('Foo', [10, 20])
+
+ described_class.steal('Foo')
+ end
+
+ it 'does not steal jobs for a different migration' do
+ queue = [double(:job, args: ['Foo', [10, 20]])]
+
+ allow(Sidekiq::Queue).to receive(:new).
+ with(BackgroundMigrationWorker.sidekiq_options['queue']).
+ and_return(queue)
+
+ expect(described_class).not_to receive(:perform)
+
+ expect(queue[0]).not_to receive(:delete)
+
+ described_class.steal('Bar')
+ end
+ end
+
+ describe '.perform' do
+ it 'performs a background migration' do
+ instance = double(:instance)
+ klass = double(:klass, new: instance)
+
+ expect(described_class).to receive(:const_get).
+ with('Foo').
+ and_return(klass)
+
+ expect(instance).to receive(:perform).with(10, 20)
+
+ described_class.perform('Foo', [10, 20])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
index f2bc15d39d7..d81774c8b8f 100644
--- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
+++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
@@ -5,15 +5,7 @@ describe Gitlab::Diff::FileCollection::MergeRequestDiff do
let(:diff_files) { described_class.new(merge_request.merge_request_diff, diff_options: nil).diff_files }
it 'does not highlight binary files' do
- allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(double("text?" => false))
-
- expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines)
-
- diff_files
- end
-
- it 'does not highlight file if blob is not accessable' do
- allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(nil)
+ allow_any_instance_of(Gitlab::Diff::File).to receive(:text?).and_return(false)
expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines)
@@ -21,7 +13,7 @@ describe Gitlab::Diff::FileCollection::MergeRequestDiff do
end
it 'does not files marked as undiffable in .gitattributes' do
- allow_any_instance_of(Repository).to receive(:diffable?).and_return(false)
+ allow_any_instance_of(Gitlab::Diff::File).to receive(:diffable?).and_return(false)
expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines)
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 050689b7c9a..a9953bb0d01 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Diff::File, lib: true do
let(:project) { create(:project, :repository) }
let(:commit) { project.commit(sample_commit.id) }
let(:diff) { commit.raw_diffs.first }
- let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: project.repository) }
+ let(:diff_file) { described_class.new(diff, diff_refs: commit.diff_refs, repository: project.repository) }
describe '#diff_lines' do
let(:diff_lines) { diff_file.diff_lines }
@@ -63,11 +63,33 @@ describe Gitlab::Diff::File, lib: true do
end
end
- describe '#blob' do
+ describe '#new_blob' do
it 'returns blob of new commit' do
- data = diff_file.blob.data
+ data = diff_file.new_blob.data
expect(data).to include('raise RuntimeError, "System commands must be given as an array of strings"')
end
end
+
+ describe '#diffable?' do
+ let(:commit) { project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') }
+ let(:diffs) { commit.diffs }
+
+ before do
+ info_dir_path = File.join(project.repository.path_to_repo, 'info')
+
+ FileUtils.mkdir(info_dir_path) unless File.exist?(info_dir_path)
+ File.write(File.join(info_dir_path, 'attributes'), "*.md -diff\n")
+ end
+
+ it "returns true for files that do not have attributes" do
+ diff_file = diffs.diff_file_with_new_path('LICENSE')
+ expect(diff_file.diffable?).to be_truthy
+ end
+
+ it "returns false for files that have been marked as not being diffable in attributes" do
+ diff_file = diffs.diff_file_with_new_path('README.md')
+ expect(diff_file.diffable?).to be_falsey
+ end
+ end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 26215381cc4..e1e4aa9fde9 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -1235,47 +1235,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- describe '#diffable' do
- info_dir_path = attributes_path = File.join(SEED_STORAGE_PATH, TEST_REPO_PATH, 'info')
- attributes_path = File.join(info_dir_path, 'attributes')
-
- before(:all) do
- FileUtils.mkdir(info_dir_path) unless File.exist?(info_dir_path)
- File.write(attributes_path, "*.md -diff\n")
- end
-
- it "should return true for files which are text and do not have attributes" do
- blob = Gitlab::Git::Blob.find(
- repository,
- '33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
- 'LICENSE'
- )
- expect(repository.diffable?(blob)).to be_truthy
- end
-
- it "should return false for binary files which do not have attributes" do
- blob = Gitlab::Git::Blob.find(
- repository,
- '33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
- 'files/images/logo-white.png'
- )
- expect(repository.diffable?(blob)).to be_falsey
- end
-
- it "should return false for text files which have been marked as not being diffable in attributes" do
- blob = Gitlab::Git::Blob.find(
- repository,
- '33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
- 'README.md'
- )
- expect(repository.diffable?(blob)).to be_falsey
- end
-
- after(:all) do
- FileUtils.rm_rf(info_dir_path)
- end
- end
-
describe '#tag_exists?' do
it 'returns true for an existing tag' do
tag = repository.tag_names.first
diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb
index a20cef3b000..c2bb9f9a166 100644
--- a/spec/lib/gitlab/highlight_spec.rb
+++ b/spec/lib/gitlab/highlight_spec.rb
@@ -7,30 +7,6 @@ describe Gitlab::Highlight, lib: true do
let(:repository) { project.repository }
let(:commit) { project.commit(sample_commit.id) }
- describe '.highlight_lines' do
- let(:lines) do
- Gitlab::Highlight.highlight_lines(project.repository, commit.id, 'files/ruby/popen.rb')
- end
-
- it 'highlights all the lines properly' do
- expect(lines[4]).to eq(%Q{<span id="LC5" class="line" lang="ruby"> <span class="kp">extend</span> <span class="nb">self</span></span>\n})
- expect(lines[21]).to eq(%Q{<span id="LC22" class="line" lang="ruby"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n})
- expect(lines[26]).to eq(%Q{<span id="LC27" class="line" lang="ruby"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n})
- end
-
- describe 'with CRLF' do
- let(:branch) { 'crlf-diff' }
- let(:blob) { repository.blob_at_branch(branch, path) }
- let(:lines) do
- Gitlab::Highlight.highlight_lines(project.repository, 'crlf-diff', 'files/whitespace')
- end
-
- it 'strips extra LFs' do
- expect(lines[0]).to eq("<span id=\"LC1\" class=\"line\" lang=\"plaintext\">test </span>")
- end
- end
- end
-
describe 'custom highlighting from .gitattributes' do
let(:branch) { 'gitattributes' }
let(:blob) { repository.blob_at_branch(branch, path) }
@@ -59,6 +35,19 @@ describe Gitlab::Highlight, lib: true do
end
describe '#highlight' do
+ describe 'with CRLF' do
+ let(:branch) { 'crlf-diff' }
+ let(:path) { 'files/whitespace' }
+ let(:blob) { repository.blob_at_branch(branch, path) }
+ let(:lines) do
+ Gitlab::Highlight.highlight(blob.path, blob.data, repository: repository).lines
+ end
+
+ it 'strips extra LFs' do
+ expect(lines[0]).to eq("<span id=\"LC1\" class=\"line\" lang=\"plaintext\">test </span>")
+ end
+ end
+
it 'links dependencies via DependencyLinker' do
expect(Gitlab::DependencyLinker).to receive(:link).
with('file.name', 'Contents', anything).and_call_original
diff --git a/spec/lib/gitlab/uploads_transfer_spec.rb b/spec/lib/gitlab/uploads_transfer_spec.rb
new file mode 100644
index 00000000000..109559bb01c
--- /dev/null
+++ b/spec/lib/gitlab/uploads_transfer_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+describe Gitlab::UploadsTransfer do
+ it 'leaves avatar uploads where they are' do
+ project_with_avatar = create(:empty_project, :with_avatar)
+
+ described_class.new.rename_namespace('project', 'project-renamed')
+
+ expect(File.exist?(project_with_avatar.avatar.path)).to be_truthy
+ end
+end
diff --git a/spec/migrations/clean_upload_symlinks_spec.rb b/spec/migrations/clean_upload_symlinks_spec.rb
new file mode 100644
index 00000000000..cecb3ddac53
--- /dev/null
+++ b/spec/migrations/clean_upload_symlinks_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170406111121_clean_upload_symlinks.rb')
+
+describe CleanUploadSymlinks do
+ let(:migration) { described_class.new }
+ let(:test_dir) { File.join(Rails.root, "tmp", "tests", "move_uploads_test") }
+ let(:uploads_dir) { File.join(test_dir, "public", "uploads") }
+ let(:new_uploads_dir) { File.join(uploads_dir, "system") }
+ let(:original_path) { File.join(new_uploads_dir, 'user') }
+ let(:symlink_path) { File.join(uploads_dir, 'user') }
+
+ before do
+ FileUtils.remove_dir(test_dir) if File.directory?(test_dir)
+ FileUtils.mkdir_p(uploads_dir)
+ allow(migration).to receive(:base_directory).and_return(test_dir)
+ allow(migration).to receive(:say)
+ end
+
+ describe "#up" do
+ before do
+ FileUtils.mkdir_p(original_path)
+ FileUtils.ln_s(original_path, symlink_path)
+ end
+
+ it 'removes the symlink' do
+ migration.up
+
+ expect(File.symlink?(symlink_path)).to be(false)
+ end
+ end
+
+ describe '#down' do
+ before do
+ FileUtils.mkdir_p(File.join(original_path))
+ FileUtils.touch(File.join(original_path, 'dummy.file'))
+ end
+
+ it 'creates a symlink' do
+ expected_path = File.join(symlink_path, "dummy.file")
+ migration.down
+
+ expect(File.exist?(expected_path)).to be(true)
+ expect(File.symlink?(symlink_path)).to be(true)
+ end
+ end
+end
diff --git a/spec/migrations/move_uploads_to_system_dir_spec.rb b/spec/migrations/move_uploads_to_system_dir_spec.rb
new file mode 100644
index 00000000000..37d66452447
--- /dev/null
+++ b/spec/migrations/move_uploads_to_system_dir_spec.rb
@@ -0,0 +1,68 @@
+require "spec_helper"
+require Rails.root.join("db", "migrate", "20170316163845_move_uploads_to_system_dir.rb")
+
+describe MoveUploadsToSystemDir do
+ let(:migration) { described_class.new }
+ let(:test_dir) { File.join(Rails.root, "tmp", "move_uploads_test") }
+ let(:uploads_dir) { File.join(test_dir, "public", "uploads") }
+ let(:new_uploads_dir) { File.join(uploads_dir, "system") }
+
+ before do
+ FileUtils.remove_dir(test_dir) if File.directory?(test_dir)
+ FileUtils.mkdir_p(uploads_dir)
+ allow(migration).to receive(:base_directory).and_return(test_dir)
+ allow(migration).to receive(:say)
+ end
+
+ describe "#up" do
+ before do
+ FileUtils.mkdir_p(File.join(uploads_dir, 'user'))
+ FileUtils.touch(File.join(uploads_dir, 'user', 'dummy.file'))
+ end
+
+ it 'moves the directory to the new path' do
+ expected_path = File.join(new_uploads_dir, 'user', 'dummy.file')
+
+ migration.up
+
+ expect(File.exist?(expected_path)).to be(true)
+ end
+
+ it 'creates a symlink in the old location' do
+ symlink_path = File.join(uploads_dir, 'user')
+ expected_path = File.join(symlink_path, 'dummy.file')
+
+ migration.up
+
+ expect(File.exist?(expected_path)).to be(true)
+ expect(File.symlink?(symlink_path)).to be(true)
+ end
+ end
+
+ describe "#down" do
+ before do
+ FileUtils.mkdir_p(File.join(new_uploads_dir, 'user'))
+ FileUtils.touch(File.join(new_uploads_dir, 'user', 'dummy.file'))
+ end
+
+ it 'moves the directory to the old path' do
+ expected_path = File.join(uploads_dir, 'user', 'dummy.file')
+
+ migration.down
+
+ expect(File.exist?(expected_path)).to be(true)
+ end
+
+ it 'removes the symlink if it existed' do
+ FileUtils.ln_s(File.join(new_uploads_dir, 'user'), File.join(uploads_dir, 'user'))
+
+ directory = File.join(uploads_dir, 'user')
+ expected_path = File.join(directory, 'dummy.file')
+
+ migration.down
+
+ expect(File.exist?(expected_path)).to be(true)
+ expect(File.symlink?(directory)).to be(false)
+ end
+ end
+end
diff --git a/spec/migrations/rename_system_namespaces_spec.rb b/spec/migrations/rename_system_namespaces_spec.rb
new file mode 100644
index 00000000000..626a6005838
--- /dev/null
+++ b/spec/migrations/rename_system_namespaces_spec.rb
@@ -0,0 +1,254 @@
+require "spec_helper"
+require Rails.root.join("db", "migrate", "20170316163800_rename_system_namespaces.rb")
+
+describe RenameSystemNamespaces, truncate: true do
+ let(:migration) { described_class.new }
+ let(:test_dir) { File.join(Rails.root, "tmp", "tests", "rename_namespaces_test") }
+ let(:uploads_dir) { File.join(test_dir, "public", "uploads") }
+ let(:system_namespace) do
+ namespace = build(:namespace, path: "system")
+ namespace.save(validate: false)
+ namespace
+ end
+
+ def save_invalid_routable(routable)
+ routable.__send__(:prepare_route)
+ routable.save(validate: false)
+ end
+
+ before do
+ FileUtils.remove_dir(test_dir) if File.directory?(test_dir)
+ FileUtils.mkdir_p(uploads_dir)
+ FileUtils.remove_dir(TestEnv.repos_path) if File.directory?(TestEnv.repos_path)
+ allow(migration).to receive(:say)
+ allow(migration).to receive(:uploads_dir).and_return(uploads_dir)
+ end
+
+ describe "#system_namespace" do
+ it "only root namespaces called with path `system`" do
+ system_namespace
+ system_namespace_with_parent = build(:namespace, path: 'system', parent: create(:namespace))
+ system_namespace_with_parent.save(validate: false)
+
+ expect(migration.system_namespace.id).to eq(system_namespace.id)
+ end
+ end
+
+ describe "#up" do
+ before do
+ system_namespace
+ end
+
+ it "doesn't break if there are no namespaces called system" do
+ Namespace.delete_all
+
+ migration.up
+ end
+
+ it "renames namespaces called system" do
+ migration.up
+
+ expect(system_namespace.reload.path).to eq("system0")
+ end
+
+ it "renames the route to the namespace" do
+ migration.up
+
+ expect(system_namespace.reload.full_path).to eq("system0")
+ end
+
+ it "renames the route for projects of the namespace" do
+ project = build(:project, path: "project-path", namespace: system_namespace)
+ save_invalid_routable(project)
+
+ migration.up
+
+ expect(project.route.reload.path).to eq("system0/project-path")
+ end
+
+ it "doesn't touch routes of namespaces that look like system" do
+ namespace = create(:group, path: 'systemlookalike')
+ project = create(:project, namespace: namespace, path: 'the-project')
+
+ migration.up
+
+ expect(project.route.reload.path).to eq('systemlookalike/the-project')
+ expect(namespace.route.reload.path).to eq('systemlookalike')
+ end
+
+ it "moves the the repository for a project in the namespace" do
+ project = build(:project, namespace: system_namespace, path: "system-project")
+ save_invalid_routable(project)
+ TestEnv.copy_repo(project,
+ bare_repo: TestEnv.factory_repo_path_bare,
+ refs: TestEnv::BRANCH_SHA)
+ expected_repo = File.join(TestEnv.repos_path, "system0", "system-project.git")
+
+ migration.up
+
+ expect(File.directory?(expected_repo)).to be(true)
+ end
+
+ it "moves the uploads for the namespace" do
+ allow(migration).to receive(:move_namespace_folders).with(Settings.pages.path, "system", "system0")
+ expect(migration).to receive(:move_namespace_folders).with(uploads_dir, "system", "system0")
+
+ migration.up
+ end
+
+ it "moves the pages for the namespace" do
+ allow(migration).to receive(:move_namespace_folders).with(uploads_dir, "system", "system0")
+ expect(migration).to receive(:move_namespace_folders).with(Settings.pages.path, "system", "system0")
+
+ migration.up
+ end
+
+ describe "clears the markdown cache for projects in the system namespace" do
+ let!(:project) do
+ project = build(:project, namespace: system_namespace)
+ save_invalid_routable(project)
+ project
+ end
+
+ it 'removes description_html from projects' do
+ migration.up
+
+ expect(project.reload.description_html).to be_nil
+ end
+
+ it 'removes issue descriptions' do
+ issue = create(:issue, project: project, description_html: 'Issue description')
+
+ migration.up
+
+ expect(issue.reload.description_html).to be_nil
+ end
+
+ it 'removes merge request descriptions' do
+ merge_request = create(:merge_request,
+ source_project: project,
+ target_project: project,
+ description_html: 'MergeRequest description')
+
+ migration.up
+
+ expect(merge_request.reload.description_html).to be_nil
+ end
+
+ it 'removes note html' do
+ note = create(:note,
+ project: project,
+ noteable: create(:issue, project: project),
+ note_html: 'note description')
+
+ migration.up
+
+ expect(note.reload.note_html).to be_nil
+ end
+
+ it 'removes milestone description' do
+ milestone = create(:milestone,
+ project: project,
+ description_html: 'milestone description')
+
+ migration.up
+
+ expect(milestone.reload.description_html).to be_nil
+ end
+ end
+
+ context "system namespace -> subgroup -> system0 project" do
+ it "updates the route of the project correctly" do
+ subgroup = build(:group, path: "subgroup", parent: system_namespace)
+ save_invalid_routable(subgroup)
+ project = build(:project, path: "system0", namespace: subgroup)
+ save_invalid_routable(project)
+
+ migration.up
+
+ expect(project.route.reload.path).to eq("system0/subgroup/system0")
+ end
+ end
+ end
+
+ describe "#move_repositories" do
+ let(:namespace) { create(:group, name: "hello-group") }
+ it "moves a project for a namespace" do
+ create(:project, namespace: namespace, path: "hello-project")
+ expected_path = File.join(TestEnv.repos_path, "bye-group", "hello-project.git")
+
+ migration.move_repositories(namespace, "hello-group", "bye-group")
+
+ expect(File.directory?(expected_path)).to be(true)
+ end
+
+ it "moves a namespace in a subdirectory correctly" do
+ child_namespace = create(:group, name: "sub-group", parent: namespace)
+ create(:project, namespace: child_namespace, path: "hello-project")
+
+ expected_path = File.join(TestEnv.repos_path, "hello-group", "renamed-sub-group", "hello-project.git")
+
+ migration.move_repositories(child_namespace, "hello-group/sub-group", "hello-group/renamed-sub-group")
+
+ expect(File.directory?(expected_path)).to be(true)
+ end
+
+ it "moves a parent namespace with subdirectories" do
+ child_namespace = create(:group, name: "sub-group", parent: namespace)
+ create(:project, namespace: child_namespace, path: "hello-project")
+ expected_path = File.join(TestEnv.repos_path, "renamed-group", "sub-group", "hello-project.git")
+
+ migration.move_repositories(child_namespace, "hello-group", "renamed-group")
+
+ expect(File.directory?(expected_path)).to be(true)
+ end
+ end
+
+ describe "#move_namespace_folders" do
+ it "moves a namespace with files" do
+ source = File.join(uploads_dir, "parent-group", "sub-group")
+ FileUtils.mkdir_p(source)
+ destination = File.join(uploads_dir, "parent-group", "moved-group")
+ FileUtils.touch(File.join(source, "test.txt"))
+ expected_file = File.join(destination, "test.txt")
+
+ migration.move_namespace_folders(uploads_dir, File.join("parent-group", "sub-group"), File.join("parent-group", "moved-group"))
+
+ expect(File.exist?(expected_file)).to be(true)
+ end
+
+ it "moves a parent namespace uploads" do
+ source = File.join(uploads_dir, "parent-group", "sub-group")
+ FileUtils.mkdir_p(source)
+ destination = File.join(uploads_dir, "moved-parent", "sub-group")
+ FileUtils.touch(File.join(source, "test.txt"))
+ expected_file = File.join(destination, "test.txt")
+
+ migration.move_namespace_folders(uploads_dir, "parent-group", "moved-parent")
+
+ expect(File.exist?(expected_file)).to be(true)
+ end
+ end
+
+ describe "#child_ids_for_parent" do
+ it "collects child ids for all levels" do
+ parent = create(:group)
+ first_child = create(:group, parent: parent)
+ second_child = create(:group, parent: parent)
+ third_child = create(:group, parent: second_child)
+ all_ids = [parent.id, first_child.id, second_child.id, third_child.id]
+
+ collected_ids = migration.child_ids_for_parent(parent, ids: [parent.id])
+
+ expect(collected_ids).to contain_exactly(*all_ids)
+ end
+ end
+
+ describe "#remove_last_ocurrence" do
+ it "removes only the last occurance of a string" do
+ input = "this/is/system/namespace/with/system"
+
+ expect(migration.remove_last_occurrence(input, "system")).to eq("this/is/system/namespace/with/")
+ end
+ end
+end
diff --git a/spec/migrations/update_upload_paths_to_system_spec.rb b/spec/migrations/update_upload_paths_to_system_spec.rb
new file mode 100644
index 00000000000..7df44515424
--- /dev/null
+++ b/spec/migrations/update_upload_paths_to_system_spec.rb
@@ -0,0 +1,53 @@
+require "spec_helper"
+require Rails.root.join("db", "post_migrate", "20170317162059_update_upload_paths_to_system.rb")
+
+describe UpdateUploadPathsToSystem do
+ let(:migration) { described_class.new }
+
+ before do
+ allow(migration).to receive(:say)
+ end
+
+ describe "#uploads_to_switch_to_new_path" do
+ it "contains only uploads with the old path for the correct models" do
+ _upload_for_other_type = create(:upload, model: create(:ci_pipeline), path: "uploads/ci_pipeline/avatar.jpg")
+ _upload_with_system_path = create(:upload, model: create(:empty_project), path: "uploads/system/project/avatar.jpg")
+ _upload_with_other_path = create(:upload, model: create(:empty_project), path: "thelongsecretforafileupload/avatar.jpg")
+ old_upload = create(:upload, model: create(:empty_project), path: "uploads/project/avatar.jpg")
+ group_upload = create(:upload, model: create(:group), path: "uploads/group/avatar.jpg")
+
+ expect(Upload.where(migration.uploads_to_switch_to_new_path)).to contain_exactly(old_upload, group_upload)
+ end
+ end
+
+ describe "#uploads_to_switch_to_old_path" do
+ it "contains only uploads with the new path for the correct models" do
+ _upload_for_other_type = create(:upload, model: create(:ci_pipeline), path: "uploads/ci_pipeline/avatar.jpg")
+ upload_with_system_path = create(:upload, model: create(:empty_project), path: "uploads/system/project/avatar.jpg")
+ _upload_with_other_path = create(:upload, model: create(:empty_project), path: "thelongsecretforafileupload/avatar.jpg")
+ _old_upload = create(:upload, model: create(:empty_project), path: "uploads/project/avatar.jpg")
+
+ expect(Upload.where(migration.uploads_to_switch_to_old_path)).to contain_exactly(upload_with_system_path)
+ end
+ end
+
+ describe "#up", truncate: true do
+ it "updates old upload records to the new path" do
+ old_upload = create(:upload, model: create(:empty_project), path: "uploads/project/avatar.jpg")
+
+ migration.up
+
+ expect(old_upload.reload.path).to eq("uploads/system/project/avatar.jpg")
+ end
+ end
+
+ describe "#down", truncate: true do
+ it "updates the new system patsh to the old paths" do
+ new_upload = create(:upload, model: create(:empty_project), path: "uploads/system/project/avatar.jpg")
+
+ migration.down
+
+ expect(new_upload.reload.path).to eq("uploads/project/avatar.jpg")
+ end
+ end
+end
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index f19e1af65a6..e1193e0d19a 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -199,6 +199,14 @@ describe Blob do
end
end
+ describe '#file_type' do
+ it 'returns the file type' do
+ blob = fake_blob(path: 'README.md')
+
+ expect(blob.file_type).to eq(:readme)
+ end
+ end
+
describe '#simple_viewer' do
context 'when the blob is empty' do
it 'returns an empty viewer' do
diff --git a/spec/models/blob_viewer/base_spec.rb b/spec/models/blob_viewer/base_spec.rb
index d56379eb59d..574438838d8 100644
--- a/spec/models/blob_viewer/base_spec.rb
+++ b/spec/models/blob_viewer/base_spec.rb
@@ -106,9 +106,9 @@ describe BlobViewer::Base, model: true do
end
describe '#render_error' do
- context 'when expanded' do
+ context 'when the blob is expanded' do
before do
- viewer.expanded = true
+ blob.expand!
end
context 'when the blob size is larger than the size limit' do
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 72f83d63224..ba247dcc5cf 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -20,8 +20,8 @@ describe Commit, models: true do
end
it 'caches the author' do
+ allow(RequestStore).to receive(:active?).and_return(true)
user = create(:user, email: commit.author_email)
- expect(RequestStore).to receive(:active?).twice.and_return(true)
expect_any_instance_of(Commit).to receive(:find_author_by_any_email).and_call_original
expect(commit.author).to eq(user)
@@ -67,11 +67,11 @@ describe Commit, models: true do
expect(commit.title).to eq("--no commit message")
end
- it "truncates a message without a newline at 80 characters" do
+ it 'truncates a message without a newline at natural break to 80 characters' do
message = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris.'
allow(commit).to receive(:safe_message).and_return(message)
- expect(commit.title).to eq("#{message[0..79]}…")
+ expect(commit.title).to eq('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis…')
end
it "truncates a message with a newline before 80 characters at the newline" do
@@ -113,6 +113,28 @@ eos
end
end
+ describe 'description' do
+ it 'returns description of commit message if title less than 100 characters' do
+ message = <<eos
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit.
+Vivamus egestas lacinia lacus, sed rutrum mauris.
+eos
+
+ allow(commit).to receive(:safe_message).and_return(message)
+ expect(commit.description).to eq('Vivamus egestas lacinia lacus, sed rutrum mauris.')
+ end
+
+ it 'returns full commit message if commit title more than 100 characters' do
+ message = <<eos
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris.
+Vivamus egestas lacinia lacus, sed rutrum mauris.
+eos
+
+ allow(commit).to receive(:safe_message).and_return(message)
+ expect(commit.description).to eq(message)
+ end
+ end
+
describe "delegation" do
subject { commit }
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index 0e10d91836d..65f05121b40 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -122,16 +122,7 @@ describe Group, 'Routable' do
it { expect(group.full_path).to eq(group.path) }
it { expect(nested_group.full_path).to eq("#{group.full_path}/#{nested_group.path}") }
- context 'with RequestStore active' do
- before do
- RequestStore.begin!
- end
-
- after do
- RequestStore.end!
- RequestStore.clear!
- end
-
+ context 'with RequestStore active', :request_store do
it 'does not load the route table more than once' do
expect(group).to receive(:uncached_full_path).once.and_call_original
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 316bf153660..3d437ca0fcc 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -179,7 +179,7 @@ describe Group, models: true do
let!(:group) { create(:group, :access_requestable, :with_avatar) }
let(:user) { create(:user) }
let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" }
- let(:avatar_path) { "/uploads/group/avatar/#{group.id}/dk.png" }
+ let(:avatar_path) { "/uploads/system/group/avatar/#{group.id}/dk.png" }
context 'when avatar file is uploaded' do
before { group.add_master(user) }
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 0e74f1ab1bd..145c7ad5770 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -43,6 +43,12 @@ describe Namespace, models: true do
end
end
+ context "is case insensitive" do
+ let(:group) { build(:group, path: "System") }
+
+ it { expect(group).not_to be_valid }
+ end
+
context 'top-level group' do
let(:group) { build(:group, path: 'tree') }
@@ -178,8 +184,8 @@ describe Namespace, models: true do
let(:parent) { create(:group, name: 'parent', path: 'parent') }
let(:child) { create(:group, name: 'child', path: 'child', parent: parent) }
let!(:project) { create(:project_empty_repo, path: 'the-project', namespace: child) }
- let(:uploads_dir) { File.join(CarrierWave.root, 'uploads') }
- let(:pages_dir) { TestEnv.pages_path }
+ let(:uploads_dir) { File.join(CarrierWave.root, FileUploader.base_dir) }
+ let(:pages_dir) { File.join(TestEnv.pages_path) }
before do
FileUtils.mkdir_p(File.join(uploads_dir, 'parent', 'child', 'the-project'))
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 3ed52d42f86..454eeb58ecd 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -812,7 +812,7 @@ describe Project, models: true do
context 'when avatar file is uploaded' do
let(:project) { create(:empty_project, :with_avatar) }
- let(:avatar_path) { "/uploads/project/avatar/#{project.id}/dk.png" }
+ let(:avatar_path) { "/uploads/system/project/avatar/#{project.id}/dk.png" }
let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" }
it 'shows correct url' do
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 362565506e5..497e3cdf415 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -389,16 +389,7 @@ describe ProjectTeam, models: true do
end
describe '#max_member_access_for_user_ids' do
- context 'with RequestStore enabled' do
- before do
- RequestStore.begin!
- end
-
- after do
- RequestStore.end!
- RequestStore.clear!
- end
-
+ context 'with RequestStore enabled', :request_store do
include_examples 'max member access for users'
def access_levels(users)
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index a83726b48a0..d5bd9946ab6 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -987,7 +987,7 @@ describe User, models: true do
context 'when avatar file is uploaded' do
let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" }
- let(:avatar_path) { "/uploads/user/avatar/#{user.id}/dk.png" }
+ let(:avatar_path) { "/uploads/system/user/avatar/#{user.id}/dk.png" }
it 'shows correct avatar url' do
expect(user.avatar_url).to eq(avatar_path)
diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb
index e1771b636b8..ddbed5f781e 100644
--- a/spec/policies/project_snippet_policy_spec.rb
+++ b/spec/policies/project_snippet_policy_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe ProjectSnippetPolicy, models: true do
let(:regular_user) { create(:user) }
let(:external_user) { create(:user, :external) }
- let(:project) { create(:empty_project) }
+ let(:project) { create(:empty_project, :public) }
let(:author_permissions) do
[
@@ -107,7 +107,7 @@ describe ProjectSnippetPolicy, models: true do
end
context 'snippet author' do
- let(:snippet) { create(:project_snippet, :private, author: regular_user) }
+ let(:snippet) { create(:project_snippet, :private, author: regular_user, project: project) }
subject { described_class.abilities(regular_user, snippet).to_set }
diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb
index 44720fc4448..f5a14b1d04d 100644
--- a/spec/presenters/merge_request_presenter_spec.rb
+++ b/spec/presenters/merge_request_presenter_spec.rb
@@ -132,6 +132,11 @@ describe MergeRequestPresenter do
it 'does not present related issues links' do
is_expected.not_to match("#{project.full_path}/issues/#{issue_b.iid}")
end
+
+ it 'appends status when closing issue is already closed' do
+ issue_a.close
+ is_expected.to match('(closed)')
+ end
end
describe '#mentioned_issues_links' do
@@ -147,6 +152,11 @@ describe MergeRequestPresenter do
it 'does not present closing issues links' do
is_expected.not_to match("#{project.full_path}/issues/#{issue_a.iid}")
end
+
+ it 'appends status when mentioned issue is already closed' do
+ issue_b.close
+ is_expected.to match('(closed)')
+ end
end
describe '#assign_to_closing_issues_link' do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 86c57204971..3e831373514 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -398,6 +398,15 @@ describe API::Projects do
expect(json_response['tag_list']).to eq(%w[tagFirst tagSecond])
end
+ it 'uploads avatar for project a project' do
+ project = attributes_for(:project, avatar: fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif'))
+
+ post api('/projects', user), project
+
+ project_id = json_response['id']
+ expect(json_response['avatar_url']).to eq("http://localhost/uploads/system/project/avatar/#{project_id}/banana_sample.gif")
+ end
+
it 'sets a project as allowing merge even if build fails' do
project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: false })
post api('/projects', user), project
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index be83514ed9c..9556c99dea1 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -431,8 +431,29 @@ describe API::Runner do
expect(response).to have_http_status(201)
expect(json_response['id']).to eq(test_job.id)
expect(json_response['dependencies'].count).to eq(2)
- expect(json_response['dependencies']).to include({ 'id' => job.id, 'name' => job.name, 'token' => job.token },
- { 'id' => job2.id, 'name' => job2.name, 'token' => job2.token })
+ expect(json_response['dependencies']).to include(
+ { 'id' => job.id, 'name' => job.name, 'token' => job.token },
+ { 'id' => job2.id, 'name' => job2.name, 'token' => job2.token })
+ end
+ end
+
+ context 'when pipeline have jobs with artifacts' do
+ let!(:job) { create(:ci_build_tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
+
+ before do
+ job.success
+ end
+
+ it 'returns dependent jobs' do
+ request_job
+
+ expect(response).to have_http_status(201)
+ expect(json_response['id']).to eq(test_job.id)
+ expect(json_response['dependencies'].count).to eq(1)
+ expect(json_response['dependencies']).to include(
+ { 'id' => job.id, 'name' => job.name, 'token' => job.token,
+ 'artifacts_file' => { 'filename' => 'ci_build_artifacts.zip', 'size' => 106365 } })
end
end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index f018b48ceb2..6a83024d0d5 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -418,17 +418,17 @@ describe 'Git HTTP requests', lib: true do
end
context 'when username and password are provided' do
- it 'rejects pulls with 2FA error message' do
+ it 'rejects pulls with personal access token error message' do
download(path, user: user.username, password: user.password) do |response|
expect(response).to have_http_status(:unauthorized)
- expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
+ expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
end
end
- it 'rejects the push attempt' do
+ it 'rejects the push attempt with personal access token error message' do
upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_http_status(:unauthorized)
- expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
+ expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
end
end
end
@@ -441,6 +441,41 @@ describe 'Git HTTP requests', lib: true do
end
end
+ context 'when internal auth is disabled' do
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:signin_enabled?) { false }
+ end
+
+ it 'rejects pulls with personal access token error message' do
+ download(path, user: 'foo', password: 'bar') do |response|
+ expect(response).to have_http_status(:unauthorized)
+ expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
+ end
+ end
+
+ it 'rejects pushes with personal access token error message' do
+ upload(path, user: 'foo', password: 'bar') do |response|
+ expect(response).to have_http_status(:unauthorized)
+ expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
+ end
+ end
+
+ context 'when LDAP is configured' do
+ before do
+ allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
+ allow_any_instance_of(Gitlab::LDAP::Authentication).
+ to receive(:login).and_return(nil)
+ end
+
+ it 'does not display the personal access token error message' do
+ upload(path, user: 'foo', password: 'bar') do |response|
+ expect(response).to have_http_status(:unauthorized)
+ expect(response.body).not_to include('You must use a personal access token with \'api\' scope for Git over HTTP')
+ end
+ end
+ end
+ end
+
context "when blank password attempts follow a valid login" do
def attempt_login(include_password)
password = include_password ? user.password : ""
@@ -648,7 +683,7 @@ describe 'Git HTTP requests', lib: true do
# Provide a dummy file in its place
allow_any_instance_of(Repository).to receive(:blob_at).and_call_original
allow_any_instance_of(Repository).to receive(:blob_at).with('b83d6e391c22777fca1ed3012fce84f633d7fed0', 'info/refs') do
- Gitlab::Git::Blob.find(project.repository, 'master', 'bar/branch-test.txt')
+ Blob.decorate(Gitlab::Git::Blob.find(project.repository, 'master', 'bar/branch-test.txt'), project)
end
get "/#{project.path_with_namespace}/blob/master/info/refs"
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index e056353fa6f..54d7cf5f10d 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -70,7 +70,7 @@ describe JwtController do
context 'without personal token' do
it 'rejects the authorization attempt' do
expect(response).to have_http_status(401)
- expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
+ expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
end
end
@@ -88,9 +88,24 @@ describe JwtController do
context 'using invalid login' do
let(:headers) { { authorization: credentials('invalid', 'password') } }
- subject! { get '/jwt/auth', parameters, headers }
+ context 'when internal auth is enabled' do
+ it 'rejects the authorization attempt' do
+ get '/jwt/auth', parameters, headers
+
+ expect(response).to have_http_status(401)
+ expect(response.body).not_to include('You must use a personal access token with \'api\' scope for Git over HTTP')
+ end
+ end
- it { expect(response).to have_http_status(401) }
+ context 'when internal auth is disabled' do
+ it 'rejects the authorization attempt with personal access token message' do
+ allow_any_instance_of(ApplicationSetting).to receive(:signin_enabled?) { false }
+ get '/jwt/auth', parameters, headers
+
+ expect(response).to have_http_status(401)
+ expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
+ end
+ end
end
end
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
index 05176c3beaa..6d1f0b24196 100644
--- a/spec/requests/openid_connect_spec.rb
+++ b/spec/requests/openid_connect_spec.rb
@@ -79,7 +79,7 @@ describe 'OpenID Connect requests' do
'email_verified' => true,
'website' => 'https://example.com',
'profile' => 'http://localhost/alice',
- 'picture' => "http://localhost/uploads/user/avatar/#{user.id}/dk.png"
+ 'picture' => "http://localhost/uploads/system/user/avatar/#{user.id}/dk.png"
})
end
end
diff --git a/spec/serializers/build_entity_spec.rb b/spec/serializers/build_entity_spec.rb
index 46d43a80ef7..e51ff9fc709 100644
--- a/spec/serializers/build_entity_spec.rb
+++ b/spec/serializers/build_entity_spec.rb
@@ -2,12 +2,13 @@ require 'spec_helper'
describe BuildEntity do
let(:user) { create(:user) }
- let(:build) { create(:ci_build, :failed) }
+ let(:build) { create(:ci_build) }
let(:project) { build.project }
let(:request) { double('request') }
before do
allow(request).to receive(:current_user).and_return(user)
+ project.add_developer(user)
end
let(:entity) do
@@ -16,9 +17,8 @@ describe BuildEntity do
subject { entity.as_json }
- it 'contains paths to build page and retry action' do
- expect(subject).to include(:build_path, :retry_path)
- expect(subject[:retry_path]).not_to be_nil
+ it 'contains paths to build page action' do
+ expect(subject).to include(:build_path)
end
it 'does not contain sensitive information' do
@@ -39,12 +39,32 @@ describe BuildEntity do
expect(subject[:status]).to include :icon, :favicon, :text, :label
end
- context 'when build is a regular job' do
+ context 'when build is retryable' do
+ before do
+ build.update(status: :failed)
+ end
+
+ it 'contains cancel path' do
+ expect(subject).to include(:retry_path)
+ end
+ end
+
+ context 'when build is cancelable' do
+ before do
+ build.update(status: :running)
+ end
+
+ it 'contains cancel path' do
+ expect(subject).to include(:cancel_path)
+ end
+ end
+
+ context 'when build is a regular build' do
it 'does not contain path to play action' do
expect(subject).not_to include(:play_path)
end
- it 'is not a playable job' do
+ it 'is not a playable build' do
expect(subject[:playable]).to be false
end
end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index 088f24eb180..34b19fb9fc4 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -102,18 +102,11 @@ describe PipelineSerializer do
Ci::Pipeline::AVAILABLE_STATUSES.each do |status|
create_pipeline(status)
end
-
- RequestStore.begin!
- end
-
- after do
- RequestStore.end!
- RequestStore.clear!
end
- it "verifies number of queries" do
+ it 'verifies number of queries', :request_store do
recorded = ActiveRecord::QueryRecorder.new { subject }
- expect(recorded.count).to be_within(1).of(60)
+ expect(recorded.count).to be_within(1).of(57)
expect(recorded.cached_count).to eq(0)
end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index e9c2b865b47..77c07b71c68 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -38,6 +38,14 @@ describe Ci::CreatePipelineService, :services do
expect(pipeline.builds.first).to be_kind_of(Ci::Build)
end
+ it 'increments the prometheus counter' do
+ expect(Gitlab::Metrics).to receive(:counter)
+ .with(:pipelines_created_count, "Pipelines created count")
+ .and_call_original
+
+ pipeline
+ end
+
context 'when merge requests already exist for this source branch' do
it 'updates head pipeline of each merge request' do
merge_request_1 = create(:merge_request, source_branch: 'master', target_branch: "branch_1", source_project: project)
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 5184c1d5f19..a78866a2c32 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -31,6 +31,13 @@ describe Issues::UpdateService, services: true do
end
end
+ def find_notes(action)
+ issue
+ .notes
+ .joins(:system_note_metadata)
+ .where(system_note_metadata: { action: action })
+ end
+
def update_issue(opts)
described_class.new(project, user, opts).execute(issue)
end
@@ -330,6 +337,9 @@ describe Issues::UpdateService, services: true do
expect(note1).not_to be_nil
expect(note2).not_to be_nil
+
+ description_notes = find_notes('description')
+ expect(description_notes.length).to eq(1)
end
end
@@ -345,6 +355,9 @@ describe Issues::UpdateService, services: true do
expect(note1).not_to be_nil
expect(note2).not_to be_nil
+
+ description_notes = find_notes('description')
+ expect(description_notes.length).to eq(1)
end
end
@@ -354,10 +367,12 @@ describe Issues::UpdateService, services: true do
update_issue(description: "- [x] Task 1\n- [ ] Task 3\n- [ ] Task 2")
end
- it 'does not create a system note' do
- note = find_note('marked the task **Task 2** as incomplete')
+ it 'does not create a system note for the task' do
+ task_note = find_note('marked the task **Task 2** as incomplete')
+ description_notes = find_notes('description')
- expect(note).to be_nil
+ expect(task_note).to be_nil
+ expect(description_notes.length).to eq(2)
end
end
@@ -368,9 +383,11 @@ describe Issues::UpdateService, services: true do
end
it 'does not create a system note referencing the position the old item' do
- note = find_note('marked the task **Two** as incomplete')
+ task_note = find_note('marked the task **Two** as incomplete')
+ description_notes = find_notes('description')
- expect(note).to be_nil
+ expect(task_note).to be_nil
+ expect(description_notes.length).to eq(2)
end
it 'does not generate a new note at all' do
diff --git a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
index 935f4710851..bb46e1dd9ab 100644
--- a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
+++ b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
@@ -10,8 +10,8 @@ describe MergeRequests::MergeRequestDiffCacheService do
expect(Rails.cache).to receive(:read).with(cache_key).and_return({})
expect(Rails.cache).to receive(:write).with(cache_key, anything)
- allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(double("text?" => true))
- allow_any_instance_of(Repository).to receive(:diffable?).and_return(true)
+ allow_any_instance_of(Gitlab::Diff::File).to receive(:text?).and_return(true)
+ allow_any_instance_of(Gitlab::Diff::File).to receive(:diffable?).and_return(true)
subject.execute(merge_request)
end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index d371fc68312..091c193aaa6 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -30,6 +30,13 @@ describe MergeRequests::UpdateService, services: true do
end
end
+ def find_notes(action)
+ @merge_request
+ .notes
+ .joins(:system_note_metadata)
+ .where(system_note_metadata: { action: action })
+ end
+
def update_merge_request(opts)
@merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
@merge_request.reload
@@ -394,6 +401,9 @@ describe MergeRequests::UpdateService, services: true do
expect(note1).not_to be_nil
expect(note2).not_to be_nil
+
+ description_notes = find_notes('description')
+ expect(description_notes.length).to eq(1)
end
end
@@ -409,6 +419,9 @@ describe MergeRequests::UpdateService, services: true do
expect(note1).not_to be_nil
expect(note2).not_to be_nil
+
+ description_notes = find_notes('description')
+ expect(description_notes.length).to eq(1)
end
end
end
diff --git a/spec/services/projects/participants_service_spec.rb b/spec/services/projects/participants_service_spec.rb
index 0657b7e93fe..d75851134ee 100644
--- a/spec/services/projects/participants_service_spec.rb
+++ b/spec/services/projects/participants_service_spec.rb
@@ -13,7 +13,7 @@ describe Projects::ParticipantsService, services: true do
groups = participants.groups
expect(groups.size).to eq 1
- expect(groups.first[:avatar_url]).to eq("/uploads/group/avatar/#{group.id}/dk.png")
+ expect(groups.first[:avatar_url]).to eq("/uploads/system/group/avatar/#{group.id}/dk.png")
end
it 'should return an url for the avatar with relative url' do
@@ -24,7 +24,7 @@ describe Projects::ParticipantsService, services: true do
groups = participants.groups
expect(groups.size).to eq 1
- expect(groups.first[:avatar_url]).to eq("/gitlab/uploads/group/avatar/#{group.id}/dk.png")
+ expect(groups.first[:avatar_url]).to eq("/gitlab/uploads/system/group/avatar/#{group.id}/dk.png")
end
end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index c499b1bb343..9295c09aefc 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -1052,7 +1052,7 @@ describe SystemNoteService, services: true do
let(:action) { 'task' }
end
- it "posts the 'marked as a Work In Progress from commit' system note" do
+ it "posts the 'marked the task as complete' system note" do
expect(subject.note).to eq("marked the task **task** as completed")
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 8b8fbf6e862..1979347a178 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -74,10 +74,18 @@ RSpec.configure do |config|
TestEnv.cleanup
end
+ config.before(:example, :request_store) do
+ RequestStore.begin!
+ end
+
+ config.after(:example, :request_store) do
+ RequestStore.end!
+ RequestStore.clear!
+ end
+
if ENV['CI']
- # Retry only on feature specs that use JS
- config.around :each, :js do |ex|
- ex.run_with_retry retry: 3
+ config.around(:each) do |ex|
+ ex.run_with_retry retry: 2
end
end
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index b8ca8f22a3d..c34e76fa72f 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -14,8 +14,10 @@ Capybara.register_driver :poltergeist do |app|
js_errors: true,
timeout: timeout,
window_size: [1366, 768],
+ url_whitelist: %w[localhost 127.0.0.1],
+ url_blacklist: %w[.mp4 .png .gif .avi .bmp .jpg .jpeg],
phantomjs_options: [
- '--load-images=no'
+ '--load-images=yes'
]
)
end
diff --git a/spec/support/milestone_tabs_examples.rb b/spec/support/milestone_tabs_examples.rb
index 4ad8b0a16e1..7cfc1e06975 100644
--- a/spec/support/milestone_tabs_examples.rb
+++ b/spec/support/milestone_tabs_examples.rb
@@ -1,10 +1,14 @@
shared_examples 'milestone tabs' do
def go(path, extra_params = {})
- params = if milestone.is_a?(GlobalMilestone)
- { group_id: group.to_param, id: milestone.safe_title, title: milestone.title }
- else
- { namespace_id: project.namespace.to_param, project_id: project, id: milestone.iid }
- end
+ params =
+ case milestone
+ when DashboardMilestone
+ { id: milestone.safe_title, title: milestone.title }
+ when GroupMilestone
+ { group_id: group.to_param, id: milestone.safe_title, title: milestone.title }
+ else
+ { namespace_id: project.namespace.to_param, project_id: project, id: milestone.iid }
+ end
get path, params.merge(extra_params)
end
diff --git a/spec/support/target_branch_helpers.rb b/spec/support/target_branch_helpers.rb
deleted file mode 100644
index 01d1c53fe6c..00000000000
--- a/spec/support/target_branch_helpers.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-module TargetBranchHelpers
- def select_branch(name)
- first('button.js-target-branch').click
- wait_for_requests
- all('a[data-group="Branches"]').find do |el|
- el.text == name
- end.click
- end
-
- def create_new_branch(name)
- first('button.js-target-branch').click
- click_link 'Create new branch'
- fill_in 'new_branch_name', with: name
- click_button 'Create'
- end
-end
diff --git a/spec/unicorn/unicorn_spec.rb b/spec/unicorn/unicorn_spec.rb
index 8518c047a47..41de94d35c2 100644
--- a/spec/unicorn/unicorn_spec.rb
+++ b/spec/unicorn/unicorn_spec.rb
@@ -67,8 +67,8 @@ describe 'Unicorn' do
end
def wait_unicorn_boot!(master_pid, ready_file)
- # Unicorn should boot in under 60 seconds so 120 seconds seems like a good timeout.
- timeout = 120
+ # We have seen the boot timeout after 2 minutes in CI so let's set it to 5 minutes.
+ timeout = 5 * 60
timeout.times do
return if File.exist?(ready_file)
pid = Process.waitpid(master_pid, Process::WNOHANG)
diff --git a/spec/uploaders/artifact_uploader_spec.rb b/spec/uploaders/artifact_uploader_spec.rb
index 24e2e3a9f0e..2a3bd0e3bb2 100644
--- a/spec/uploaders/artifact_uploader_spec.rb
+++ b/spec/uploaders/artifact_uploader_spec.rb
@@ -17,22 +17,45 @@ describe ArtifactUploader do
describe '.artifacts_upload_path' do
subject { described_class.artifacts_upload_path }
-
+
it { is_expected.to start_with(path) }
it { is_expected.to end_with('tmp/uploads/') }
end
describe '#store_dir' do
subject { uploader.store_dir }
-
+
it { is_expected.to start_with(path) }
it { is_expected.to end_with("#{job.project_id}/#{job.id}") }
end
describe '#cache_dir' do
subject { uploader.cache_dir }
-
+
+ it { is_expected.to start_with(path) }
+ it { is_expected.to end_with('/tmp/cache') }
+ end
+
+ describe '#work_dir' do
+ subject { uploader.work_dir }
+
it { is_expected.to start_with(path) }
- it { is_expected.to end_with('tmp/cache') }
+ it { is_expected.to end_with('/tmp/work') }
+ end
+
+ describe '#filename' do
+ # we need to use uploader, as this makes to use mounter
+ # which initialises uploader.file object
+ let(:uploader) { job.artifacts_file }
+
+ subject { uploader.filename }
+
+ it { is_expected.to be_nil }
+
+ context 'with artifacts' do
+ let(:job) { create(:ci_build, :artifacts) }
+
+ it { is_expected.not_to be_nil }
+ end
end
end
diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb
index ea714fb08f0..d82dbe871d5 100644
--- a/spec/uploaders/attachment_uploader_spec.rb
+++ b/spec/uploaders/attachment_uploader_spec.rb
@@ -3,6 +3,17 @@ require 'spec_helper'
describe AttachmentUploader do
let(:uploader) { described_class.new(build_stubbed(:user)) }
+ describe "#store_dir" do
+ it "stores in the system dir" do
+ expect(uploader.store_dir).to start_with("uploads/system/user")
+ end
+
+ it "uses the old path when using object storage" do
+ expect(described_class).to receive(:file_storage?).and_return(false)
+ expect(uploader.store_dir).to start_with("uploads/user")
+ end
+ end
+
describe '#move_to_cache' do
it 'is true' do
expect(uploader.move_to_cache).to eq(true)
diff --git a/spec/uploaders/avatar_uploader_spec.rb b/spec/uploaders/avatar_uploader_spec.rb
index c4d558805ab..201fe6949aa 100644
--- a/spec/uploaders/avatar_uploader_spec.rb
+++ b/spec/uploaders/avatar_uploader_spec.rb
@@ -3,6 +3,17 @@ require 'spec_helper'
describe AvatarUploader do
let(:uploader) { described_class.new(build_stubbed(:user)) }
+ describe "#store_dir" do
+ it "stores in the system dir" do
+ expect(uploader.store_dir).to start_with("uploads/system/user")
+ end
+
+ it "uses the old path when using object storage" do
+ expect(described_class).to receive(:file_storage?).and_return(false)
+ expect(uploader.store_dir).to start_with("uploads/user")
+ end
+ end
+
describe '#move_to_cache' do
it 'is false' do
expect(uploader.move_to_cache).to eq(false)
diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb
index d9113ef4095..47e9365e13d 100644
--- a/spec/uploaders/file_uploader_spec.rb
+++ b/spec/uploaders/file_uploader_spec.rb
@@ -15,6 +15,16 @@ describe FileUploader do
end
end
+ describe "#store_dir" do
+ it "stores in the namespace path" do
+ project = build_stubbed(:empty_project)
+ uploader = described_class.new(project)
+
+ expect(uploader.store_dir).to include(project.path_with_namespace)
+ expect(uploader.store_dir).not_to include("system")
+ end
+ end
+
describe 'initialize' do
it 'generates a secret if none is provided' do
expect(SecureRandom).to receive(:hex).and_return('secret')
diff --git a/spec/uploaders/gitlab_uploader_spec.rb b/spec/uploaders/gitlab_uploader_spec.rb
index 78e9d9cf46c..a144b39f74f 100644
--- a/spec/uploaders/gitlab_uploader_spec.rb
+++ b/spec/uploaders/gitlab_uploader_spec.rb
@@ -53,4 +53,19 @@ describe GitlabUploader do
expect(subject.move_to_store).to eq(true)
end
end
+
+ describe '#cache!' do
+ it 'moves the file from the working directory to the cache directory' do
+ # One to get the work dir, the other to remove it
+ expect(subject).to receive(:workfile_path).exactly(2).times.and_call_original
+ # Test https://github.com/carrierwavesubject/carrierwave/blob/v1.0.0/lib/carrierwave/sanitized_file.rb#L200
+ expect(FileUtils).to receive(:mv).with(anything, /^#{subject.work_dir}/).and_call_original
+ expect(FileUtils).to receive(:mv).with(/^#{subject.work_dir}/, /#{subject.cache_dir}/).and_call_original
+
+ fixture = Rails.root.join('spec', 'fixtures', 'rails_sample.jpg')
+ subject.cache!(fixture_file_upload(fixture))
+
+ expect(subject.file.path).to match(/#{subject.cache_dir}/)
+ end
+ end
end
diff --git a/spec/uploaders/lfs_object_uploader_spec.rb b/spec/uploaders/lfs_object_uploader_spec.rb
index c3b72e7d677..7088bc23334 100644
--- a/spec/uploaders/lfs_object_uploader_spec.rb
+++ b/spec/uploaders/lfs_object_uploader_spec.rb
@@ -1,21 +1,9 @@
require 'spec_helper'
describe LfsObjectUploader do
- let(:uploader) { described_class.new(build_stubbed(:empty_project)) }
-
- describe '#cache!' do
- it 'caches the file in the cache directory' do
- # One to get the work dir, the other to remove it
- expect(uploader).to receive(:workfile_path).exactly(2).times.and_call_original
- expect(FileUtils).to receive(:mv).with(anything, /^#{uploader.work_dir}/).and_call_original
- expect(FileUtils).to receive(:mv).with(/^#{uploader.work_dir}/, /^#{uploader.cache_dir}/).and_call_original
-
- fixture = Rails.root.join('spec', 'fixtures', 'rails_sample.jpg')
- uploader.cache!(fixture_file_upload(fixture))
-
- expect(uploader.file.path).to start_with(uploader.cache_dir)
- end
- end
+ let(:lfs_object) { create(:lfs_object, :with_file) }
+ let(:uploader) { described_class.new(lfs_object) }
+ let(:path) { Gitlab.config.lfs.storage_path }
describe '#move_to_cache' do
it 'is true' do
@@ -28,4 +16,25 @@ describe LfsObjectUploader do
expect(uploader.move_to_store).to eq(true)
end
end
+
+ describe '#store_dir' do
+ subject { uploader.store_dir }
+
+ it { is_expected.to start_with(path) }
+ it { is_expected.to end_with("#{lfs_object.oid[0, 2]}/#{lfs_object.oid[2, 2]}") }
+ end
+
+ describe '#cache_dir' do
+ subject { uploader.cache_dir }
+
+ it { is_expected.to start_with(path) }
+ it { is_expected.to end_with('/tmp/cache') }
+ end
+
+ describe '#work_dir' do
+ subject { uploader.work_dir }
+
+ it { is_expected.to start_with(path) }
+ it { is_expected.to end_with('/tmp/work') }
+ end
end
diff --git a/spec/views/help/index.html.haml_spec.rb b/spec/views/help/index.html.haml_spec.rb
index 6b07fcfc987..1f8261cc46b 100644
--- a/spec/views/help/index.html.haml_spec.rb
+++ b/spec/views/help/index.html.haml_spec.rb
@@ -21,7 +21,7 @@ describe 'help/index' do
render
expect(rendered).to match '8.0.2'
- expect(rendered).to match 'abcdefg'
+ expect(rendered).to have_link('abcdefg', 'https://gitlab.com/gitlab-org/gitlab-ce/commits/abcdefg')
end
end
diff --git a/spec/views/projects/jobs/show.html.haml_spec.rb b/spec/views/projects/jobs/show.html.haml_spec.rb
index 8f2822f5dc5..d9a7ba265f8 100644
--- a/spec/views/projects/jobs/show.html.haml_spec.rb
+++ b/spec/views/projects/jobs/show.html.haml_spec.rb
@@ -15,36 +15,6 @@ describe 'projects/jobs/show', :view do
allow(view).to receive(:can?).and_return(true)
end
- describe 'job information in header' do
- let(:build) do
- create(:ci_build, :success, environment: 'staging')
- end
-
- before do
- render
- end
-
- it 'shows status name' do
- expect(rendered).to have_css('.ci-status.ci-success', text: 'passed')
- end
-
- it 'does not render a link to the job' do
- expect(rendered).not_to have_link('passed')
- end
-
- it 'shows job id' do
- expect(rendered).to have_css('.js-build-id', text: build.id)
- end
-
- it 'shows a link to the pipeline' do
- expect(rendered).to have_link(build.pipeline.id)
- end
-
- it 'shows a link to the commit' do
- expect(rendered).to have_link(build.pipeline.short_sha)
- end
- end
-
describe 'environment info in job view' do
context 'job with latest deployment' do
let(:build) do
@@ -215,34 +185,6 @@ describe 'projects/jobs/show', :view do
end
end
- context 'when job is not running' do
- before do
- build.success!
- render
- end
-
- it 'shows retry button' do
- expect(rendered).to have_link('Retry')
- end
-
- context 'if build passed' do
- it 'does not show New issue button' do
- expect(rendered).not_to have_link('New issue')
- end
- end
-
- context 'if build failed' do
- before do
- build.status = 'failed'
- render
- end
-
- it 'shows New issue button' do
- expect(rendered).to have_link('New issue')
- end
- end
- end
-
describe 'commit title in sidebar' do
let(:commit_title) { project.commit.title }
@@ -269,25 +211,4 @@ describe 'projects/jobs/show', :view do
expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_2')
end
end
-
- describe 'New issue button' do
- before do
- build.status = 'failed'
- render
- end
-
- it 'links to issues/new with the title and description filled in' do
- title = "Build Failed ##{build.id}"
- build_url = namespace_project_job_url(project.namespace, project, build)
- href = new_namespace_project_issue_path(
- project.namespace,
- project,
- issue: {
- title: title,
- description: build_url
- }
- )
- expect(rendered).to have_link('New issue', href: href)
- end
- end
end
diff --git a/spec/workers/background_migration_worker_spec.rb b/spec/workers/background_migration_worker_spec.rb
new file mode 100644
index 00000000000..0d742ae9dc7
--- /dev/null
+++ b/spec/workers/background_migration_worker_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+
+describe BackgroundMigrationWorker do
+ describe '.perform' do
+ it 'performs a background migration' do
+ expect(Gitlab::BackgroundMigration).
+ to receive(:perform).
+ with('Foo', [10, 20])
+
+ described_class.new.perform('Foo', [10, 20])
+ end
+ end
+end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index f4bc63bcc6a..44163c735ba 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -94,26 +94,23 @@ describe PostReceive do
it { expect{ subject }.not_to change{ Ci::Pipeline.count } }
end
end
- end
- describe '#process_repository_update' do
- let(:changes) {'123456 789012 refs/heads/tést'}
- let(:fake_hook_data) do
- { event_name: 'repository_update' }
- end
+ context 'after project changes hooks' do
+ let(:changes) { '123456 789012 refs/heads/tést' }
+ let(:fake_hook_data) { Hash.new(event_name: 'repository_update') }
- before do
- allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner)
- allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data)
- # silence hooks so we can isolate
- allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true)
- allow(subject).to receive(:process_project_changes).and_return(true)
- end
+ before do
+ allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data)
+ # silence hooks so we can isolate
+ allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true)
+ allow_any_instance_of(GitPushService).to receive(:execute).and_return(true)
+ end
- it 'calls SystemHooksService' do
- expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true)
+ it 'calls SystemHooksService' do
+ expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true)
- subject.perform(pwd(project), key_id, base64_changes)
+ described_class.new.perform(project_identifier, key_id, base64_changes)
+ end
end
end
diff --git a/vendor/assets/javascripts/peek.js b/vendor/assets/javascripts/peek.js
new file mode 100644
index 00000000000..f7e77de34ff
--- /dev/null
+++ b/vendor/assets/javascripts/peek.js
@@ -0,0 +1,78 @@
+(function($) {
+ var fetchRequestResults, getRequestId, peekEnabled, toggleBar, updatePerformanceBar;
+ getRequestId = function() {
+ return $('#peek').data('request-id');
+ };
+ peekEnabled = function() {
+ return $('#peek').length;
+ };
+ updatePerformanceBar = function(results) {
+ var key, label, data, table, html, tr, duration_td, sql_td, strong;
+
+ Object.keys(results.data).forEach(function(key) {
+ Object.keys(results.data[key]).forEach(function(label) {
+ data = results.data[key][label];
+
+ if (label == 'queries') {
+ table = document.createElement('table');
+
+ for (var i = 0; i < data.length; i += 1) {
+ tr = document.createElement('tr');
+ duration_td = document.createElement('td');
+ sql_td = document.createElement('td');
+ strong = document.createElement('strong');
+
+ strong.append(data[i]['duration'] + 'ms');
+ duration_td.appendChild(strong);
+ tr.appendChild(duration_td);
+
+ sql_td.appendChild(document.createTextNode(data[i]['sql']));
+ tr.appendChild(sql_td);
+
+ table.appendChild(tr);
+ }
+
+ table.className = 'table';
+ $("[data-defer-to=" + key + "-" + label + "]").html(table);
+ } else {
+ $("[data-defer-to=" + key + "-" + label + "]").text(results.data[key][label]);
+ }
+ });
+ });
+ return $(document).trigger('peek:render', [getRequestId(), results]);
+ };
+ toggleBar = function(event) {
+ var wrapper;
+ if ($(event.target).is(':input')) {
+ return;
+ }
+ if (event.which === 96 && !event.metaKey) {
+ wrapper = $('#peek');
+ if (wrapper.hasClass('disabled')) {
+ wrapper.removeClass('disabled');
+ return document.cookie = "peek=true; path=/";
+ } else {
+ wrapper.addClass('disabled');
+ return document.cookie = "peek=false; path=/";
+ }
+ }
+ };
+ fetchRequestResults = function() {
+ return $.ajax('/-/peek/results', {
+ data: {
+ request_id: getRequestId()
+ },
+ success: function(data, textStatus, xhr) {
+ return updatePerformanceBar(data);
+ },
+ error: function(xhr, textStatus, error) {}
+ });
+ };
+ $(document).on('keypress', toggleBar);
+ $(document).on('peek:update', fetchRequestResults);
+ return $(function() {
+ if (peekEnabled()) {
+ return $(this).trigger('peek:update');
+ }
+ });
+})(jQuery);
diff --git a/vendor/assets/javascripts/peek.performance_bar.js b/vendor/assets/javascripts/peek.performance_bar.js
new file mode 100644
index 00000000000..6ed86dce2f2
--- /dev/null
+++ b/vendor/assets/javascripts/peek.performance_bar.js
@@ -0,0 +1,182 @@
+var PerformanceBar, ajaxStart, renderPerformanceBar, updateStatus;
+
+PerformanceBar = (function() {
+ PerformanceBar.prototype.appInfo = null;
+
+ PerformanceBar.prototype.width = null;
+
+ PerformanceBar.formatTime = function(value) {
+ if (value >= 1000) {
+ return ((value / 1000).toFixed(3)) + "s";
+ } else {
+ return (value.toFixed(0)) + "ms";
+ }
+ };
+
+ function PerformanceBar(options) {
+ var k, v;
+ if (options == null) {
+ options = {};
+ }
+ this.el = $('#peek-view-performance-bar .performance-bar');
+ for (k in options) {
+ v = options[k];
+ this[k] = v;
+ }
+ if (this.width == null) {
+ this.width = this.el.width();
+ }
+ if (this.timing == null) {
+ this.timing = window.performance.timing;
+ }
+ }
+
+ PerformanceBar.prototype.render = function(serverTime) {
+ var networkTime, perfNetworkTime;
+ if (serverTime == null) {
+ serverTime = 0;
+ }
+ this.el.empty();
+ this.addBar('frontend', '#90d35b', 'domLoading', 'domInteractive');
+ perfNetworkTime = this.timing.responseEnd - this.timing.requestStart;
+ if (serverTime && serverTime <= perfNetworkTime) {
+ networkTime = perfNetworkTime - serverTime;
+ this.addBar('latency / receiving', '#f1faff', this.timing.requestStart + serverTime, this.timing.requestStart + serverTime + networkTime);
+ this.addBar('app', '#90afcf', this.timing.requestStart, this.timing.requestStart + serverTime, this.appInfo);
+ } else {
+ this.addBar('backend', '#c1d7ee', 'requestStart', 'responseEnd');
+ }
+ this.addBar('tcp / ssl', '#45688e', 'connectStart', 'connectEnd');
+ this.addBar('redirect', '#0c365e', 'redirectStart', 'redirectEnd');
+ this.addBar('dns', '#082541', 'domainLookupStart', 'domainLookupEnd');
+ return this.el;
+ };
+
+ PerformanceBar.prototype.isLoaded = function() {
+ return this.timing.domInteractive;
+ };
+
+ PerformanceBar.prototype.start = function() {
+ return this.timing.navigationStart;
+ };
+
+ PerformanceBar.prototype.end = function() {
+ return this.timing.domInteractive;
+ };
+
+ PerformanceBar.prototype.total = function() {
+ return this.end() - this.start();
+ };
+
+ PerformanceBar.prototype.addBar = function(name, color, start, end, info) {
+ var bar, left, offset, time, title, width;
+ if (typeof start === 'string') {
+ start = this.timing[start];
+ }
+ if (typeof end === 'string') {
+ end = this.timing[end];
+ }
+ if (!((start != null) && (end != null))) {
+ return;
+ }
+ time = end - start;
+ offset = start - this.start();
+ left = this.mapH(offset);
+ width = this.mapH(time);
+ title = name + ": " + (PerformanceBar.formatTime(time));
+ bar = $('<li></li>', {
+ 'data-title': title,
+ 'data-toggle': 'tooltip',
+ 'data-container': 'body'
+ });
+ bar.css({
+ width: width + "px",
+ left: left + "px",
+ background: color
+ });
+ return this.el.append(bar);
+ };
+
+ PerformanceBar.prototype.mapH = function(offset) {
+ return offset * (this.width / this.total());
+ };
+
+ return PerformanceBar;
+
+})();
+
+renderPerformanceBar = function() {
+ var bar, resp, span, time;
+ resp = $('#peek-server_response_time');
+ time = Math.round(resp.data('time') * 1000);
+ bar = new PerformanceBar;
+ bar.render(time);
+ span = $('<span>', {
+ 'data-toggle': 'tooltip',
+ 'data-title': 'Total navigation time for this page.',
+ 'data-container': 'body'
+ }).text(PerformanceBar.formatTime(bar.total()));
+ return updateStatus(span);
+};
+
+updateStatus = function(html) {
+ return $('#serverstats').html(html);
+};
+
+ajaxStart = null;
+
+$(document).on('pjax:start page:fetch turbolinks:request-start', function(event) {
+ return ajaxStart = event.timeStamp;
+});
+
+$(document).on('pjax:end page:load turbolinks:load', function(event, xhr) {
+ var ajaxEnd, serverTime, total;
+ if (ajaxStart == null) {
+ return;
+ }
+ ajaxEnd = event.timeStamp;
+ total = ajaxEnd - ajaxStart;
+ serverTime = xhr ? parseInt(xhr.getResponseHeader('X-Runtime')) : 0;
+ return setTimeout(function() {
+ var bar, now, span, tech;
+ now = new Date().getTime();
+ bar = new PerformanceBar({
+ timing: {
+ requestStart: ajaxStart,
+ responseEnd: ajaxEnd,
+ domLoading: ajaxEnd,
+ domInteractive: now
+ },
+ isLoaded: function() {
+ return true;
+ },
+ start: function() {
+ return ajaxStart;
+ },
+ end: function() {
+ return now;
+ }
+ });
+ bar.render(serverTime);
+ if ($.fn.pjax != null) {
+ tech = 'PJAX';
+ } else {
+ tech = 'Turbolinks';
+ }
+ span = $('<span>', {
+ 'data-toggle': 'tooltip',
+ 'data-title': tech + " navigation time",
+ 'data-container': 'body'
+ }).text(PerformanceBar.formatTime(total));
+ updateStatus(span);
+ return ajaxStart = null;
+ }, 0);
+});
+
+$(function() {
+ if (window.performance) {
+ return renderPerformanceBar();
+ } else {
+ return $('#peek-view-performance-bar').remove();
+ }
+});
diff --git a/vendor/assets/stylesheets/peek.scss b/vendor/assets/stylesheets/peek.scss
new file mode 100644
index 00000000000..f1845fb9044
--- /dev/null
+++ b/vendor/assets/stylesheets/peek.scss
@@ -0,0 +1,94 @@
+//= require peek/views/performance_bar
+//= require peek/views/rblineprof
+
+header.navbar-gitlab.with-peek {
+ top: 35px;
+}
+
+#peek {
+ height: 35px;
+ background: #000;
+ line-height: 35px;
+ color: #999;
+
+ &.disabled {
+ display: none;
+ }
+
+ &.production {
+ background-color: #222;
+ }
+
+ &.staging {
+ background-color: #291430;
+ }
+
+ &.development {
+ background-color: #4c1210;
+ }
+
+ .wrapper {
+ width: 800px;
+ margin: 0 auto;
+ }
+
+ // UI Elements
+ .bucket {
+ background: #111;
+ display: inline-block;
+ padding: 4px 6px;
+ font-family: Consolas, "Liberation Mono", Courier, monospace;
+ line-height: 1;
+ color: #ccc;
+ border-radius: 3px;
+ box-shadow: 0 1px 0 rgba(255,255,255,.2), inset 0 1px 2px rgba(0,0,0,.25);
+
+ .hidden {
+ display: none;
+ }
+
+ &:hover .hidden {
+ display: inline;
+ }
+ }
+
+ strong {
+ color: #fff;
+ }
+
+ table {
+ strong {
+ color: #000;
+ }
+ }
+
+ .view {
+ margin-right: 15px;
+ float: left;
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+
+ .css-truncate {
+ &.css-truncate-target,
+ .css-truncate-target {
+ display: inline-block;
+ max-width: 125px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ vertical-align: top;
+ }
+
+ &.expandable:hover .css-truncate-target,
+ &.expandable:hover.css-truncate-target {
+ max-width: 10000px !important;
+ }
+ }
+}
+
+#modal-peek-pg-queries-content {
+ color: #000;
+}