summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFilipa Lacerda <filipa@gitlab.com>2016-12-22 10:15:49 +0000
committerFilipa Lacerda <filipa@gitlab.com>2016-12-22 10:15:49 +0000
commit7fc64dd18d9b2b6e3a2a01dab0007f7dd25c37ed (patch)
tree428602d5265cd981a2e33ace8aed6fc9594dd37c
parentfd3ab00cf90ddf081c61fb701721ca9180378bba (diff)
parent6d9c1d3efce00da95832feaaf36227bcbffecadf (diff)
downloadgitlab-ce-pipeline-ui-updates.tar.gz
Merge branch 'master' into pipeline-ui-updatespipeline-ui-updates
* master: (259 commits) Exclude non existent repository storages. fixed minor animation glitch in mini pipeline graph animation Update Bitbucket callback URL documentation Update build step for KaTeX. Add KaTeX fonts to assets paths and precompile Replace url('...') to url(font-path('...')) Rname katex.css to katex.scss Revert conflicting EE changes Added Autodeploy script for OpenShift Whitelist next project names: notes, services Put back progress bar CSS Remove unneeded bundle refs. Adds entry to changelog Reduce MR widget title by one pixel Use same font size for all items in issue title Adds background color for disabled state to merge when succeeds dropdown Filter protocol-relative URLs in ExternalLinkFilter. Fixes issue #22742. Move javascript for widget check to ci_bundle. Introduce "Set up autodeploy" button to help configure GitLab CI for deployment Whitelist next project names: help, ci, admin, search ...
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile3
-rw-r--r--Gemfile.lock7
-rw-r--r--app/assets/images/auth_buttons/authentiq_64.pngbin0 -> 17679 bytes
-rw-r--r--app/assets/javascripts/build.js125
-rw-r--r--app/assets/javascripts/dispatcher.js.es617
-rw-r--r--app/assets/javascripts/environments/components/environment.js.es613
-rw-r--r--app/assets/javascripts/environments/components/environment_item.js.es616
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.js.es627
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es613
-rw-r--r--app/assets/javascripts/issuable.js.es614
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js13
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.es629
-rw-r--r--app/assets/javascripts/merge_request_widget.js.es63
-rw-r--r--app/assets/javascripts/merge_request_widget/ci_bundle.js.es642
-rw-r--r--app/assets/javascripts/mini_pipeline_graph_dropdown.js.es696
-rw-r--r--app/assets/javascripts/network/branch_graph.js18
-rw-r--r--app/assets/javascripts/terminal/terminal.js.es662
-rw-r--r--app/assets/javascripts/terminal/terminal_bundle.js.es65
-rw-r--r--app/assets/javascripts/u2f/authenticate.js3
-rw-r--r--app/assets/javascripts/u2f/error.js1
-rw-r--r--app/assets/javascripts/u2f/register.js3
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/avatar.scss2
-rw-r--r--app/assets/stylesheets/framework/badges.scss11
-rw-r--r--app/assets/stylesheets/framework/blocks.scss2
-rw-r--r--app/assets/stylesheets/framework/buttons.scss7
-rw-r--r--app/assets/stylesheets/framework/forms.scss4
-rw-r--r--app/assets/stylesheets/framework/layout.scss6
-rw-r--r--app/assets/stylesheets/framework/nav.scss9
-rw-r--r--app/assets/stylesheets/framework/page-header.scss1
-rw-r--r--app/assets/stylesheets/framework/panels.scss14
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss12
-rw-r--r--app/assets/stylesheets/pages/builds.scss150
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss1
-rw-r--r--app/assets/stylesheets/pages/environments.scss9
-rw-r--r--app/assets/stylesheets/pages/issuable.scss1
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss10
-rw-r--r--app/assets/stylesheets/pages/notes.scss8
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss363
-rw-r--r--app/assets/stylesheets/pages/projects.scss21
-rw-r--r--app/assets/stylesheets/pages/status.scss3
-rw-r--r--app/assets/stylesheets/pages/tree.scss1
-rw-r--r--app/controllers/application_controller.rb6
-rw-r--r--app/controllers/groups_controller.rb2
-rw-r--r--app/controllers/import/gitea_controller.rb45
-rw-r--r--app/controllers/import/github_controller.rb95
-rw-r--r--app/controllers/projects/commit_controller.rb33
-rw-r--r--app/controllers/projects/environments_controller.rb33
-rw-r--r--app/controllers/projects/mattermosts_controller.rb43
-rw-r--r--app/controllers/projects/merge_requests_controller.rb15
-rw-r--r--app/controllers/projects/pipelines_controller.rb10
-rw-r--r--app/helpers/auth_helper.rb2
-rw-r--r--app/helpers/blob_helper.rb2
-rw-r--r--app/helpers/ci_status_helper.rb2
-rw-r--r--app/helpers/commits_helper.rb65
-rw-r--r--app/helpers/import_helper.rb10
-rw-r--r--app/helpers/mattermost_helper.rb9
-rw-r--r--app/helpers/projects_helper.rb6
-rw-r--r--app/models/ci/pipeline.rb5
-rw-r--r--app/models/ci/stage.rb4
-rw-r--r--app/models/concerns/milestoneish.rb28
-rw-r--r--app/models/concerns/reactive_caching.rb114
-rw-r--r--app/models/environment.rb8
-rw-r--r--app/models/global_milestone.rb10
-rw-r--r--app/models/issue.rb2
-rw-r--r--app/models/milestone.rb4
-rw-r--r--app/models/namespace.rb4
-rw-r--r--app/models/project.rb10
-rw-r--r--app/models/project_authorization.rb13
-rw-r--r--app/models/project_services/chat_service.rb21
-rw-r--r--app/models/project_services/chat_slash_commands_service.rb56
-rw-r--r--app/models/project_services/deployment_service.rb18
-rw-r--r--app/models/project_services/kubernetes_service.rb67
-rw-r--r--app/models/project_services/mattermost_service.rb (renamed from app/models/project_services/mattermost_notification_service.rb)4
-rw-r--r--app/models/project_services/mattermost_slash_commands_service.rb44
-rw-r--r--app/models/project_services/slack_service.rb (renamed from app/models/project_services/slack_notification_service.rb)4
-rw-r--r--app/models/project_services/slack_slash_commands_service.rb28
-rw-r--r--app/models/route.rb2
-rw-r--r--app/models/service.rb5
-rw-r--r--app/models/user.rb42
-rw-r--r--app/serializers/environment_entity.rb8
-rw-r--r--app/serializers/request_aware_entity.rb4
-rw-r--r--app/services/groups/update_service.rb8
-rw-r--r--app/services/projects/import_service.rb16
-rw-r--r--app/services/system_note_service.rb2
-rw-r--r--app/services/users/refresh_authorized_projects_service.rb128
-rw-r--r--app/validators/project_path_validator.rb3
-rw-r--r--app/views/ci/status/_graph_badge.html.haml12
-rw-r--r--app/views/import/_githubish_status.html.haml61
-rw-r--r--app/views/import/gitea/new.html.haml23
-rw-r--r--app/views/import/gitea/status.html.haml7
-rw-r--r--app/views/import/github/status.html.haml64
-rw-r--r--app/views/layouts/nav/_admin.html.haml2
-rw-r--r--app/views/layouts/nav/_group.html.haml4
-rw-r--r--app/views/layouts/nav/_project.html.haml4
-rw-r--r--app/views/projects/_last_commit.html.haml3
-rw-r--r--app/views/projects/builds/_header.html.haml2
-rw-r--r--app/views/projects/builds/show.html.haml19
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml27
-rw-r--r--app/views/projects/commit/_builds.html.haml2
-rw-r--r--app/views/projects/commit/_ci_menu.html.haml4
-rw-r--r--app/views/projects/commit/_pipelines_list.haml16
-rw-r--r--app/views/projects/commit/builds.html.haml9
-rw-r--r--app/views/projects/environments/_terminal_button.html.haml3
-rw-r--r--app/views/projects/environments/index.html.haml5
-rw-r--r--app/views/projects/environments/show.html.haml1
-rw-r--r--app/views/projects/environments/terminal.html.haml22
-rw-r--r--app/views/projects/mattermosts/_no_teams.html.haml12
-rw-r--r--app/views/projects/mattermosts/_team_selection.html.haml44
-rw-r--r--app/views/projects/mattermosts/new.html.haml8
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml11
-rw-r--r--app/views/projects/merge_requests/_show.html.haml7
-rw-r--r--app/views/projects/merge_requests/show/_builds.html.haml1
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/_merged_buttons.haml6
-rw-r--r--app/views/projects/merge_requests/widget/_show.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/open/_accept.html.haml21
-rw-r--r--app/views/projects/merge_requests/widget/open/_check.html.haml9
-rw-r--r--app/views/projects/new.html.haml5
-rw-r--r--app/views/projects/pipelines/_stage.html.haml4
-rw-r--r--app/views/projects/pipelines/index.html.haml14
-rw-r--r--app/views/projects/services/_form.html.haml1
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml91
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_help.html.haml94
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml7
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml93
-rw-r--r--app/views/projects/show.html.haml4
-rw-r--r--app/views/shared/empty_states/_issues.html.haml8
-rw-r--r--app/views/shared/icons/_go_logo.svg.erb1
-rw-r--r--app/views/shared/icons/_icon_status_canceled_borderless.svg1
-rw-r--r--app/views/shared/icons/_icon_status_created_borderless.svg1
-rw-r--r--app/views/shared/icons/_icon_status_failed_borderless.svg1
-rw-r--r--app/views/shared/icons/_icon_status_manual_borderless.svg1
-rw-r--r--app/views/shared/icons/_icon_status_pending_borderless.svg1
-rw-r--r--app/views/shared/icons/_icon_status_running_borderless.svg1
-rw-r--r--app/views/shared/icons/_icon_status_skipped_borderless.svg1
-rw-r--r--app/views/shared/icons/_icon_status_success_borderless.svg1
-rw-r--r--app/views/shared/icons/_icon_status_warning_borderless.svg1
-rw-r--r--app/views/shared/icons/_icon_terminal.svg1
-rw-r--r--app/views/shared/icons/_mattermost_logo.svg.erb1
-rw-r--r--app/views/shared/icons/_scroll_down.svg3
-rw-r--r--app/views/shared/icons/_scroll_down_hover_active.svg3
-rw-r--r--app/views/shared/icons/_scroll_up.svg3
-rw-r--r--app/views/shared/icons/_scroll_up_hover_active.svg3
-rw-r--r--app/views/shared/issuable/_milestone_dropdown.html.haml2
-rw-r--r--app/views/shared/milestones/_issuables.html.haml8
-rw-r--r--app/views/shared/milestones/_tabs.html.haml2
-rw-r--r--app/views/u2f/_authenticate.html.haml2
-rw-r--r--app/views/u2f/_register.html.haml2
-rw-r--r--app/workers/authorized_projects_worker.rb22
-rw-r--r--app/workers/reactive_caching_worker.rb15
-rwxr-xr-xbin/changelog11
-rw-r--r--changelogs/unreleased/19620-auto-scroll-log.yml4
-rw-r--r--changelogs/unreleased/19703-direct-link-pipelines.yml4
-rw-r--r--changelogs/unreleased/22348-gitea-importer.yml4
-rw-r--r--changelogs/unreleased/22742-filter-protocol-relative-urls.yml4
-rw-r--r--changelogs/unreleased/23638-remove-builds-tab.yml4
-rw-r--r--changelogs/unreleased/25093-hide-new-issue-btn-non-loggedin-user.yml4
-rw-r--r--changelogs/unreleased/25368-fix-left-align-system-note.yml4
-rw-r--r--changelogs/unreleased/25678-remove-user-build.yml4
-rw-r--r--changelogs/unreleased/25740-fix-new-branch-button-padding.yml4
-rw-r--r--changelogs/unreleased/25895-fix-headers-in-ci-api-helpers.yml4
-rw-r--r--changelogs/unreleased/25898-ci-icon-color-mr.yml4
-rw-r--r--changelogs/unreleased/25905-mr-when-succeeds-dropdown.yml4
-rw-r--r--changelogs/unreleased/25906-title-size.yml4
-rw-r--r--changelogs/unreleased/25908-fix-grape-after-update.yml4
-rw-r--r--changelogs/unreleased/25938-progress-bar-gone.yml4
-rw-r--r--changelogs/unreleased/4269-public-api.yml2
-rw-r--r--changelogs/unreleased/4269-public-files-api.yml4
-rw-r--r--changelogs/unreleased/4269-public-repositories-api.yml4
-rw-r--r--changelogs/unreleased/8038-authentiq-id-oauth-support.yml4
-rw-r--r--changelogs/unreleased/adam-auto-deploy.yml4
-rw-r--r--changelogs/unreleased/badge-color-on-white-bg.yml4
-rw-r--r--changelogs/unreleased/dz-fix-route-rename.yml4
-rw-r--r--changelogs/unreleased/dz-rename-invalid-groups.yml4
-rw-r--r--changelogs/unreleased/dz-whitelist-dashboard-project-path.yml4
-rw-r--r--changelogs/unreleased/dz-whitelist-more-project-names-2.yml4
-rw-r--r--changelogs/unreleased/dz-whitelist-more-project-names.yml4
-rw-r--r--changelogs/unreleased/fix-copy-issues-empty-state.yml4
-rw-r--r--changelogs/unreleased/fix-group-path-rename-error.yml4
-rw-r--r--changelogs/unreleased/fix-import-labels-error.yml4
-rw-r--r--changelogs/unreleased/jej-fix-n-1-queries-milestones-show.yml4
-rw-r--r--changelogs/unreleased/jej-memoize-milestoneish-visible-to-user.yml4
-rw-r--r--changelogs/unreleased/mattermost-slash-auto-config.yml4
-rw-r--r--changelogs/unreleased/nick-thomas-gitlab-ce-22864-kubernetes-deploy-with-terminal.yml4
-rw-r--r--changelogs/unreleased/nuke-ugly-spaces-in-changelog-generator.yml4
-rw-r--r--changelogs/unreleased/remove-u2f-error-logging.yml4
-rw-r--r--changelogs/unreleased/zj-remove-unused-services.yml4
-rw-r--r--changelogs/unreleased/zj-slack-slash-commands.yml4
-rw-r--r--config/application.rb5
-rw-r--r--config/gitlab.yml.example12
-rw-r--r--config/initializers/1_settings.rb2
-rw-r--r--config/routes/import.rb6
-rw-r--r--config/routes/project.rb9
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--db/migrate/20141006143943_move_slack_service_to_webhook.rb2
-rw-r--r--db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb13
-rw-r--r--db/migrate/20161220141214_remove_dot_git_from_group_names.rb82
-rw-r--r--db/post_migrate/20161221140236_remove_unneeded_services.rb13
-rw-r--r--db/schema.rb4
-rw-r--r--doc/README.md1
-rw-r--r--doc/administration/auth/README.md2
-rw-r--r--doc/administration/auth/authentiq.md69
-rw-r--r--doc/administration/high_availability/load_balancer.md26
-rw-r--r--doc/administration/integration/terminal.md73
-rw-r--r--doc/api/repositories.md18
-rw-r--r--doc/api/repository_files.md4
-rw-r--r--doc/ci/README.md1
-rw-r--r--doc/ci/autodeploy/img/autodeploy_button.pngbin0 -> 41799 bytes
-rw-r--r--doc/ci/autodeploy/img/autodeploy_dropdown.pngbin0 -> 46761 bytes
-rw-r--r--doc/ci/autodeploy/index.md39
-rw-r--r--doc/ci/environments.md45
-rw-r--r--doc/ci/img/environments_terminal_button_on_index.pngbin0 -> 79725 bytes
-rw-r--r--doc/ci/img/environments_terminal_button_on_show.pngbin0 -> 73210 bytes
-rw-r--r--doc/ci/img/environments_terminal_page.pngbin0 -> 117863 bytes
-rw-r--r--doc/ci/pipelines.md31
-rw-r--r--doc/integration/README.md2
-rw-r--r--doc/integration/bitbucket.md6
-rw-r--r--doc/integration/omniauth.md1
-rw-r--r--doc/project_services/kubernetes.md14
-rw-r--r--doc/user/permissions.md1
-rw-r--r--doc/workflow/importing/README.md2
-rw-r--r--doc/workflow/importing/img/import_projects_from_gitea_new_import.pngbin0 -> 15561 bytes
-rw-r--r--doc/workflow/importing/import_projects_from_gitea.md80
-rw-r--r--doc/workflow/importing/import_projects_from_github.md8
-rw-r--r--features/admin/projects.feature47
-rw-r--r--features/project/commits/commits.feature2
-rw-r--r--features/project/service.feature8
-rw-r--r--features/steps/admin/projects.rb104
-rw-r--r--features/steps/project/commits/commits.rb9
-rw-r--r--features/steps/project/services.rb8
-rw-r--r--lib/api/files.rb2
-rw-r--r--lib/api/repositories.rb6
-rw-r--r--lib/api/services.rb13
-rw-r--r--lib/api/users.rb2
-rw-r--r--lib/banzai/filter/external_link_filter.rb2
-rw-r--r--lib/ci/api/builds.rb19
-rw-r--r--lib/ci/api/helpers.rb17
-rw-r--r--lib/gitlab/chat_commands/base_command.rb4
-rw-r--r--lib/gitlab/chat_commands/command.rb10
-rw-r--r--lib/gitlab/chat_commands/deploy.rb2
-rw-r--r--lib/gitlab/chat_commands/presenter.rb (renamed from lib/mattermost/presenter.rb)10
-rw-r--r--lib/gitlab/current_settings.rb2
-rw-r--r--lib/gitlab/git/rev_list.rb4
-rw-r--r--lib/gitlab/github_import/base_formatter.rb4
-rw-r--r--lib/gitlab/github_import/client.rb16
-rw-r--r--lib/gitlab/github_import/importer.rb70
-rw-r--r--lib/gitlab/github_import/issuable_formatter.rb60
-rw-r--r--lib/gitlab/github_import/issue_formatter.rb52
-rw-r--r--lib/gitlab/github_import/milestone_formatter.rb12
-rw-r--r--lib/gitlab/github_import/project_creator.rb9
-rw-r--r--lib/gitlab/github_import/pull_request_formatter.rb60
-rw-r--r--lib/gitlab/import_export/relation_factory.rb28
-rw-r--r--lib/gitlab/import_sources.rb39
-rw-r--r--lib/gitlab/kubernetes.rb80
-rw-r--r--lib/gitlab/metrics/rack_middleware.rb4
-rw-r--r--lib/gitlab/middleware/multipart.rb8
-rw-r--r--lib/gitlab/template/gitlab_ci_yml_template.rb10
-rw-r--r--lib/gitlab/update_path_error.rb3
-rw-r--r--lib/gitlab/workhorse.rb13
-rw-r--r--lib/mattermost/client.rb41
-rw-r--r--lib/mattermost/command.rb10
-rw-r--r--lib/mattermost/error.rb3
-rw-r--r--lib/mattermost/session.rb63
-rw-r--r--lib/mattermost/team.rb7
-rw-r--r--spec/controllers/groups_controller_spec.rb21
-rw-r--r--spec/controllers/import/gitea_controller_spec.rb43
-rw-r--r--spec/controllers/import/github_controller_spec.rb216
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb69
-rw-r--r--spec/controllers/projects/mattermosts_controller_spec.rb58
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb4
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb47
-rw-r--r--spec/factories/projects.rb12
-rw-r--r--spec/features/admin/admin_projects_spec.rb99
-rw-r--r--spec/features/admin/admin_settings_spec.rb6
-rw-r--r--spec/features/auto_deploy_spec.rb64
-rw-r--r--spec/features/environment_spec.rb32
-rw-r--r--spec/features/environments_spec.rb28
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb27
-rw-r--r--spec/features/merge_requests/created_from_fork_spec.rb8
-rw-r--r--spec/features/milestones/show_spec.rb26
-rw-r--r--spec/features/projects/commit/builds_spec.rb12
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin679415 -> 682154 bytes
-rw-r--r--spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb4
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb2
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb72
-rw-r--r--spec/features/projects/services/mattermost_slash_command_spec.rb34
-rw-r--r--spec/features/projects/services/slack_service_spec.rb4
-rw-r--r--spec/features/projects/services/slack_slash_command_spec.rb40
-rw-r--r--spec/helpers/import_helper_spec.rb33
-rw-r--r--spec/javascripts/fixtures/issuable_filter.html.haml8
-rw-r--r--spec/javascripts/fixtures/mini_dropdown_graph.html.haml8
-rw-r--r--spec/javascripts/issuable_spec.js.es681
-rw-r--r--spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es651
-rw-r--r--spec/lib/banzai/filter/external_link_filter_spec.rb14
-rw-r--r--spec/lib/gitlab/chat_commands/command_spec.rb28
-rw-r--r--spec/lib/gitlab/git/rev_list_spec.rb7
-rw-r--r--spec/lib/gitlab/github_import/client_spec.rb46
-rw-r--r--spec/lib/gitlab/github_import/importer_spec.rb370
-rw-r--r--spec/lib/gitlab/github_import/issuable_formatter_spec.rb21
-rw-r--r--spec/lib/gitlab/github_import/issue_formatter_spec.rb38
-rw-r--r--spec/lib/gitlab/github_import/milestone_formatter_spec.rb27
-rw-r--r--spec/lib/gitlab/github_import/pull_request_formatter_spec.rb34
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml5
-rw-r--r--spec/lib/gitlab/import_export/project.json22
-rw-r--r--spec/lib/gitlab/import_sources_spec.rb94
-rw-r--r--spec/lib/gitlab/kubernetes_spec.rb39
-rw-r--r--spec/lib/gitlab/metrics/rack_middleware_spec.rb4
-rw-r--r--spec/lib/gitlab/middleware/multipart_spec.rb6
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb36
-rw-r--r--spec/lib/mattermost/client_spec.rb24
-rw-r--r--spec/lib/mattermost/command_spec.rb61
-rw-r--r--spec/lib/mattermost/session_spec.rb24
-rw-r--r--spec/lib/mattermost/team_spec.rb66
-rw-r--r--spec/models/ci/pipeline_spec.rb24
-rw-r--r--spec/models/ci/stage_spec.rb13
-rw-r--r--spec/models/concerns/reactive_caching_spec.rb145
-rw-r--r--spec/models/environment_spec.rb60
-rw-r--r--spec/models/project_authorization_spec.rb25
-rw-r--r--spec/models/project_services/chat_message/build_message_spec.rb8
-rw-r--r--spec/models/project_services/chat_message/issue_message_spec.rb10
-rw-r--r--spec/models/project_services/chat_message/merge_message_spec.rb12
-rw-r--r--spec/models/project_services/chat_message/note_message_spec.rb22
-rw-r--r--spec/models/project_services/chat_message/pipeline_message_spec.rb8
-rw-r--r--spec/models/project_services/chat_message/push_message_spec.rb26
-rw-r--r--spec/models/project_services/chat_message/wiki_page_message_spec.rb8
-rw-r--r--spec/models/project_services/chat_service_spec.rb15
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb109
-rw-r--r--spec/models/project_services/mattermost_notification_service_spec.rb5
-rw-r--r--spec/models/project_services/mattermost_service_spec.rb5
-rw-r--r--spec/models/project_services/mattermost_slash_commands_service_spec.rb163
-rw-r--r--spec/models/project_services/slack_notification_service_spec.rb5
-rw-r--r--spec/models/project_services/slack_service_spec.rb5
-rw-r--r--spec/models/project_services/slack_slash_commands_service.rb40
-rw-r--r--spec/models/project_spec.rb18
-rw-r--r--spec/models/route_spec.rb8
-rw-r--r--spec/requests/api/files_spec.rb87
-rw-r--r--spec/requests/api/repositories_spec.rb473
-rw-r--r--spec/requests/ci/api/builds_spec.rb25
-rw-r--r--spec/routing/import_routing_spec.rb165
-rw-r--r--spec/services/groups/update_service_spec.rb51
-rw-r--r--spec/services/users/refresh_authorized_projects_service_spec.rb185
-rw-r--r--spec/support/api/repositories_shared_context.rb10
-rw-r--r--spec/support/api/status_shared_examples.rb42
-rw-r--r--spec/support/chat_slash_commands_shared_examples.rb97
-rw-r--r--spec/support/controllers/githubish_import_controller_shared_context.rb10
-rw-r--r--spec/support/controllers/githubish_import_controller_shared_examples.rb232
-rw-r--r--spec/support/kubernetes_helpers.rb52
-rw-r--r--spec/support/query_recorder.rb40
-rw-r--r--spec/support/reactive_caching_helpers.rb38
-rw-r--r--spec/support/slack_mattermost_notifications_shared_examples.rb (renamed from spec/support/slack_mattermost_shared_examples.rb)2
-rw-r--r--spec/views/projects/pipelines/_stage.html.haml_spec.rb21
-rw-r--r--spec/workers/authorized_projects_worker_spec.rb14
-rw-r--r--spec/workers/reactive_caching_worker_spec.rb15
-rw-r--r--vendor/assets/javascripts/katex.js6
-rw-r--r--vendor/assets/javascripts/xterm/fit.js86
-rw-r--r--vendor/assets/javascripts/xterm/xterm.js2235
-rw-r--r--vendor/assets/stylesheets/katex.scss (renamed from vendor/assets/stylesheets/katex.css)70
-rw-r--r--vendor/assets/stylesheets/xterm/xterm.css2206
-rw-r--r--vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml74
362 files changed, 11610 insertions, 2028 deletions
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 26aaba0e866..6085e946503 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-1.2.0
+1.2.1
diff --git a/Gemfile b/Gemfile
index bea31b53b1c..9dfaf7a48a2 100644
--- a/Gemfile
+++ b/Gemfile
@@ -32,6 +32,7 @@ gem 'omniauth-saml', '~> 1.7.0'
gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0'
+gem 'omniauth-authentiq', '~> 0.2.0'
gem 'rack-oauth2', '~> 1.2.1'
gem 'jwt'
@@ -169,7 +170,7 @@ gem 'gitlab-flowdock-git-hook', '~> 1.0.1'
gem 'gemnasium-gitlab-service', '~> 0.2'
# Slack integration
-gem 'slack-notifier', '~> 1.2.0'
+gem 'slack-notifier', '~> 1.5.1'
# Asana integration
gem 'asana', '~> 0.4.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 811adfc5c1d..9f8367b420a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -428,6 +428,8 @@ GEM
rack (>= 1.0, < 3)
omniauth-auth0 (1.4.1)
omniauth-oauth2 (~> 1.1)
+ omniauth-authentiq (0.2.2)
+ omniauth-oauth2 (~> 1.3, >= 1.3.1)
omniauth-azure-oauth2 (0.0.6)
jwt (~> 1.0)
omniauth (~> 1.0)
@@ -683,7 +685,7 @@ GEM
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.0)
- slack-notifier (1.2.1)
+ slack-notifier (1.5.1)
slop (3.6.0)
spinach (0.8.10)
colorize
@@ -897,6 +899,7 @@ DEPENDENCIES
oj (~> 2.17.4)
omniauth (~> 1.3.1)
omniauth-auth0 (~> 1.4.1)
+ omniauth-authentiq (~> 0.2.0)
omniauth-azure-oauth2 (~> 0.0.6)
omniauth-cas3 (~> 1.1.2)
omniauth-facebook (~> 4.0.0)
@@ -952,7 +955,7 @@ DEPENDENCIES
sidekiq-cron (~> 0.4.4)
sidekiq-limit_fetch (~> 3.4)
simplecov (= 0.12.0)
- slack-notifier (~> 1.2.0)
+ slack-notifier (~> 1.5.1)
spinach-rails (~> 0.2.1)
spinach-rerun-reporter (~> 0.0.2)
spring (~> 1.7.0)
diff --git a/app/assets/images/auth_buttons/authentiq_64.png b/app/assets/images/auth_buttons/authentiq_64.png
new file mode 100644
index 00000000000..81767bbcc54
--- /dev/null
+++ b/app/assets/images/auth_buttons/authentiq_64.png
Binary files differ
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 824febe3fd3..5e449170cd3 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -4,6 +4,7 @@
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var AUTO_SCROLL_OFFSET = 75;
this.Build = (function() {
Build.interval = null;
@@ -19,6 +20,17 @@
this.buildStage = options.buildStage;
this.updateDropdown = bind(this.updateDropdown, this);
this.$document = $(document);
+ this.$body = $('body');
+ this.$buildTrace = $('#build-trace');
+ this.$autoScrollContainer = $('.autoscroll-container');
+ this.$autoScrollStatus = $('#autoscroll-status');
+ this.$autoScrollStatusText = this.$autoScrollStatus.find('.status-text');
+ this.$upBuildTrace = $('#up-build-trace');
+ this.$downBuildTrace = $('#down-build-trace');
+ this.$scrollTopBtn = $('#scroll-top');
+ this.$scrollBottomBtn = $('#scroll-bottom');
+ this.$buildRefreshAnimation = $('.js-build-refresh');
+
clearInterval(Build.interval);
// Init breakpoint checker
this.bp = Breakpoints.get();
@@ -32,6 +44,7 @@
this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
+ this.$document.on('scroll', this.initScrollMonitor.bind(this));
$(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this));
$('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace);
this.updateArtifactRemoveDate();
@@ -40,18 +53,6 @@
this.initScrollButtonAffix();
}
if (this.buildStatus === "running" || this.buildStatus === "pending") {
- // Bind autoscroll button to follow build output
- $('#autoscroll-button').on('click', function() {
- var state;
- state = $(this).data("state");
- if ("enabled" === state) {
- $(this).data("state", "disabled");
- return $(this).text("Enable autoscroll");
- } else {
- $(this).data("state", "enabled");
- return $(this).text("Disable autoscroll");
- }
- });
Build.interval = setInterval((function(_this) {
// Check for new build output if user still watching build page
// Only valid for runnig build when output changes during time
@@ -91,9 +92,10 @@
success: function(buildData) {
$('.js-build-output').html(buildData.trace_html);
if (removeRefreshStatuses.indexOf(buildData.status) >= 0) {
- return $('.js-build-refresh').remove();
+ this.initScrollMonitor();
+ return this.$buildRefreshAnimation.remove();
}
- }
+ }.bind(this)
});
};
@@ -122,22 +124,95 @@
};
Build.prototype.checkAutoscroll = function() {
- if ("enabled" === $("#autoscroll-button").data("state")) {
- return $("html,body").scrollTop($("#build-trace").height());
+ if (this.$autoScrollStatus.data("state") === "enabled") {
+ return $("html,body").scrollTop(this.$buildTrace.height());
+ }
+
+ // Handle a situation where user started new build
+ // but never scrolled a page
+ if (!this.$scrollTopBtn.is(':visible') &&
+ !this.$scrollBottomBtn.is(':visible') &&
+ !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ this.$scrollBottomBtn.show();
}
};
Build.prototype.initScrollButtonAffix = function() {
- var $body, $buildTrace;
- $body = $('body');
- $buildTrace = $('#build-trace');
- return this.$buildScroll.affix({
- offset: {
- bottom: function() {
- return $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top);
- }
+ // Hide everything initially
+ this.$scrollTopBtn.hide();
+ this.$scrollBottomBtn.hide();
+ this.$autoScrollContainer.hide();
+ }
+
+ // Page scroll listener to detect if user has scrolling page
+ // and handle following cases
+ // 1) User is at Top of Build Log;
+ // - Hide Top Arrow button
+ // - Show Bottom Arrow button
+ // - Disable Autoscroll and hide indicator (when build is running)
+ // 2) User is at Bottom of Build Log;
+ // - Show Top Arrow button
+ // - Hide Bottom Arrow button
+ // - Enable Autoscroll and show indicator (when build is running)
+ // 3) User is somewhere in middle of Build Log;
+ // - Show Top Arrow button
+ // - Show Bottom Arrow button
+ // - Disable Autoscroll and hide indicator (when build is running)
+ Build.prototype.initScrollMonitor = function() {
+ if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ // User is somewhere in middle of Build Log
+
+ this.$scrollTopBtn.show();
+
+ if (this.buildStatus === 'success' || this.buildStatus === 'failed') { // Check if Build is completed
+ this.$scrollBottomBtn.show();
+ } else if (this.$buildRefreshAnimation.is(':visible') && !gl.utils.isInViewport(this.$buildRefreshAnimation.get(0))) {
+ this.$scrollBottomBtn.show();
+ } else {
+ this.$scrollBottomBtn.hide();
}
- });
+
+ // Hide Autoscroll Status Indicator
+ if (this.$scrollBottomBtn.is(':visible')) {
+ this.$autoScrollContainer.hide();
+ this.$autoScrollStatusText.removeClass('animate');
+ } else {
+ this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show();
+ this.$autoScrollStatusText.addClass('animate');
+ }
+ } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ // User is at Top of Build Log
+
+ this.$scrollTopBtn.hide();
+ this.$scrollBottomBtn.show();
+
+ this.$autoScrollContainer.hide();
+ this.$autoScrollStatusText.removeClass('animate');
+ } else if ((!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) ||
+ (this.$buildRefreshAnimation.is(':visible') && gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) {
+ // User is at Bottom of Build Log
+
+ this.$scrollTopBtn.show();
+ this.$scrollBottomBtn.hide();
+
+ // Show and Reposition Autoscroll Status Indicator
+ this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show();
+ this.$autoScrollStatusText.addClass('animate');
+ } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ // Build Log height is small
+
+ this.$scrollTopBtn.hide();
+ this.$scrollBottomBtn.hide();
+
+ // Hide Autoscroll Status Indicator
+ this.$autoScrollContainer.hide();
+ this.$autoScrollStatusText.removeClass('animate');
+ }
+
+ if (this.buildStatus === "running" || this.buildStatus === "pending") {
+ // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise.
+ this.$autoScrollStatus.data("state", gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)) ? 'enabled' : 'disabled');
+ }
};
Build.prototype.shouldHideSidebarForViewport = function() {
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
index 1e259a16f06..5245c5aa494 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -138,9 +138,13 @@
new MergedButtons();
break;
case 'projects:merge_requests:commits':
- case 'projects:merge_requests:builds':
new MergedButtons();
break;
+ case 'projects:merge_requests:pipelines':
+ new gl.MiniPipelineGraph({
+ container: '.js-pipeline-table',
+ });
+ break;
case "projects:merge_requests:diffs":
new gl.Diff();
new ZenMode();
@@ -158,8 +162,10 @@
new ZenMode();
shortcut_handler = new ShortcutsNavigation();
break;
- case 'projects:commit:builds':
- new gl.Pipelines();
+ case 'projects:commit:pipelines':
+ new gl.MiniPipelineGraph({
+ container: '.js-pipeline-table',
+ });
break;
case 'projects:commits:show':
case 'projects:activity':
@@ -172,6 +178,11 @@
new TreeView();
}
break;
+ case 'projects:pipelines:index':
+ new gl.MiniPipelineGraph({
+ container: '.js-pipeline-table',
+ });
+ break;
case 'projects:pipelines:builds':
case 'projects:pipelines:show':
const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6
index 88c3d257cea..facd653fd72 100644
--- a/app/assets/javascripts/environments/components/environment.js.es6
+++ b/app/assets/javascripts/environments/components/environment.js.es6
@@ -18,7 +18,7 @@
* The environments array is a recursive tree structure and we need to filter
* both root level environments and children environments.
*
- * In order to acomplish that, both `filterState` and `filterEnvironmnetsByState`
+ * In order to acomplish that, both `filterState` and `filterEnvironmentsByState`
* functions work together.
* The first one works as the filter that verifies if the given environment matches
* the given state.
@@ -34,9 +34,9 @@
* @param {Array} array
* @return {Array}
*/
- const filterEnvironmnetsByState = (fn, arr) => arr.map((item) => {
+ const filterEnvironmentsByState = (fn, arr) => arr.map((item) => {
if (item.children) {
- const filteredChildren = filterEnvironmnetsByState(fn, item.children).filter(Boolean);
+ const filteredChildren = filterEnvironmentsByState(fn, item.children).filter(Boolean);
if (filteredChildren.length) {
item.children = filteredChildren;
return item;
@@ -76,12 +76,13 @@
helpPagePath: environmentsData.helpPagePath,
commitIconSvg: environmentsData.commitIconSvg,
playIconSvg: environmentsData.playIconSvg,
+ terminalIconSvg: environmentsData.terminalIconSvg,
};
},
computed: {
filteredEnvironments() {
- return filterEnvironmnetsByState(filterState(this.visibility), this.state.environments);
+ return filterEnvironmentsByState(filterState(this.visibility), this.state.environments);
},
scope() {
@@ -102,7 +103,7 @@
},
/**
- * Fetches all the environmnets and stores them.
+ * Fetches all the environments and stores them.
* Toggles loading property.
*/
created() {
@@ -230,6 +231,7 @@
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
+ :terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg"></tr>
<tr v-if="model.isOpen && model.children && model.children.length > 0"
@@ -240,6 +242,7 @@
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
+ :terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg">
</tr>
diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6
index 4674d5202e6..b26a40aa268 100644
--- a/app/assets/javascripts/environments/components/environment_item.js.es6
+++ b/app/assets/javascripts/environments/components/environment_item.js.es6
@@ -8,6 +8,7 @@
/*= require ./environment_external_url */
/*= require ./environment_stop */
/*= require ./environment_rollback */
+/*= require ./environment_terminal_button */
(() => {
/**
@@ -33,6 +34,7 @@
'external-url-component': window.gl.environmentsList.ExternalUrlComponent,
'stop-component': window.gl.environmentsList.StopComponent,
'rollback-component': window.gl.environmentsList.RollbackComponent,
+ 'terminal-button-component': window.gl.environmentsList.TerminalButtonComponent,
},
props: {
@@ -68,6 +70,12 @@
type: String,
required: false,
},
+
+ terminalIconSvg: {
+ type: String,
+ required: false,
+ },
+
},
data() {
@@ -506,6 +514,14 @@
</stop-component>
</div>
+ <div v-if="model.terminal_path"
+ class="inline js-terminal-button-container">
+ <terminal-button-component
+ :terminal-icon-svg="terminalIconSvg"
+ :terminal-path="model.terminal_path">
+ </terminal-button-component>
+ </div>
+
<div v-if="canRetry && canCreateDeployment"
class="inline js-rollback-component-container">
<rollback-component
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6
new file mode 100644
index 00000000000..25e6ac7f3c9
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6
@@ -0,0 +1,27 @@
+/*= require vue */
+/* global Vue */
+
+(() => {
+ window.gl = window.gl || {};
+ window.gl.environmentsList = window.gl.environmentsList || {};
+
+ window.gl.environmentsList.TerminalButtonComponent = Vue.component('terminal-button-component', {
+ props: {
+ terminalPath: {
+ type: String,
+ default: '',
+ },
+ terminalIconSvg: {
+ type: String,
+ default: '',
+ },
+ },
+
+ template: `
+ <a class="btn terminal-button"
+ :href="terminalPath">
+ <span class="js-terminal-icon-container" v-html="terminalIconSvg"></span>
+ </a>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
index 17d03c87bf5..cbd8ac4eddd 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -112,7 +112,6 @@
return value.path != null ? this.Emoji.template : this.Loading.template;
}.bind(this),
insertTpl: ':${name}:',
- startWithSpace: false,
skipSpecialCharacterTest: true,
data: this.defaultLoadingData,
callbacks: {
@@ -129,7 +128,6 @@
}.bind(this),
insertTpl: '${atwho-at}${username}',
searchKey: 'search',
- startWithSpace: false,
alwaysHighlightFirst: true,
skipSpecialCharacterTest: true,
data: this.defaultLoadingData,
@@ -172,7 +170,6 @@
}.bind(this),
data: this.defaultLoadingData,
insertTpl: '${atwho-at}${id}',
- startWithSpace: false,
callbacks: {
sorter: this.DefaultOptions.sorter,
filter: this.DefaultOptions.filter,
@@ -200,7 +197,6 @@
displayTpl: function(value) {
return value.title != null ? this.Milestones.template : this.Loading.template;
}.bind(this),
- startWithSpace: false,
data: this.defaultLoadingData,
callbacks: {
matcher: this.DefaultOptions.matcher,
@@ -225,7 +221,6 @@
at: '!',
alias: 'mergerequests',
searchKey: 'search',
- startWithSpace: false,
displayTpl: function(value) {
return value.title != null ? this.Issues.template : this.Loading.template;
}.bind(this),
@@ -259,7 +254,6 @@
return this.isLoading(value) ? this.Loading.template : this.Labels.template;
}.bind(this),
insertTpl: '${atwho-at}${title}',
- startWithSpace: false,
callbacks: {
matcher: this.DefaultOptions.matcher,
sorter: this.DefaultOptions.sorter,
@@ -379,14 +373,7 @@
togglePreventSelection(isPrevented = !!this.setting.tabSelectsMatch) {
this.setting.tabSelectsMatch = !isPrevented;
this.setting.spaceSelectsMatch = !isPrevented;
- const eventListenerAction = `${isPrevented ? 'add' : 'remove'}EventListener`;
- this.$inputor[0][eventListenerAction]('keydown', gl.GfmAutoComplete.preventSpaceTabEnter);
},
- preventSpaceTabEnter(e) {
- const key = e.which || e.keyCode;
- const preventables = [9, 13, 32];
- if (preventables.indexOf(key) > -1) e.preventDefault();
- }
};
}).call(this);
diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6
index 1c10a7445bb..9c3c96c20ed 100644
--- a/app/assets/javascripts/issuable.js.es6
+++ b/app/assets/javascripts/issuable.js.es6
@@ -1,13 +1,13 @@
-/* eslint-disable func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, prefer-const, padded-blocks, wrap-iife, max-len */
+/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, prefer-const, padded-blocks, wrap-iife, max-len */
/* global Issuable */
/* global Turbolinks */
-(function() {
+((global) => {
var issuable_created;
issuable_created = false;
- this.Issuable = {
+ global.Issuable = {
init: function() {
Issuable.initTemplates();
Issuable.initSearch();
@@ -111,7 +111,11 @@
filterResults: (function(_this) {
return function(form) {
var formAction, formData, issuesUrl;
- formData = form.serialize();
+ formData = form.serializeArray();
+ formData = formData.filter(function(data) {
+ return data.value !== '';
+ });
+ formData = $.param(formData);
formAction = form.attr('action');
issuesUrl = formAction;
issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&');
@@ -184,4 +188,4 @@
}
};
-}).call(this);
+})(window);
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 8fa80502d92..0a0e73e0ccc 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -93,6 +93,19 @@
}
};
+ // Check if element scrolled into viewport from above or below
+ // Courtesy http://stackoverflow.com/a/7557433/414749
+ w.gl.utils.isInViewport = function(el) {
+ var rect = el.getBoundingClientRect();
+
+ return (
+ rect.top >= 0 &&
+ rect.left >= 0 &&
+ rect.bottom <= window.innerHeight &&
+ rect.right <= window.innerWidth
+ );
+ };
+
gl.utils.getPagePath = function() {
return $('body').data('page').split(':')[0];
};
diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6
index 3ec0f1fd613..42015a02477 100644
--- a/app/assets/javascripts/merge_request_tabs.js.es6
+++ b/app/assets/javascripts/merge_request_tabs.js.es6
@@ -59,16 +59,13 @@
class MergeRequestTabs {
- constructor({ action, setUrl, buildsLoaded, stubLocation } = {}) {
+ constructor({ action, setUrl, stubLocation } = {}) {
this.diffsLoaded = false;
- this.buildsLoaded = false;
this.pipelinesLoaded = false;
this.commitsLoaded = false;
this.fixedLayoutPref = null;
this.setUrl = setUrl !== undefined ? setUrl : true;
- this.buildsLoaded = buildsLoaded || false;
-
this.setCurrentAction = this.setCurrentAction.bind(this);
this.tabShown = this.tabShown.bind(this);
this.showTab = this.showTab.bind(this);
@@ -119,10 +116,6 @@
$.scrollTo('.merge-request-details .merge-request-tabs', {
offset: -navBarHeight,
});
- } else if (action === 'builds') {
- this.loadBuilds($target.attr('href'));
- this.expandView();
- this.resetViewContainer();
} else if (action === 'pipelines') {
this.loadPipelines($target.attr('href'));
this.expandView();
@@ -180,8 +173,8 @@
setCurrentAction(action) {
this.currentAction = action === 'show' ? 'notes' : action;
- // Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs'
- let newState = location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
+ // Remove a trailing '/commits' '/diffs' '/pipelines' '/new' '/new/diffs'
+ let newState = location.pathname.replace(/\/(commits|diffs|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
// Append the new action if we're on a tab other than 'notes'
if (this.currentAction !== 'notes') {
@@ -255,22 +248,6 @@
});
}
- loadBuilds(source) {
- if (this.buildsLoaded) {
- return;
- }
- this.ajaxGet({
- url: `${source}.json`,
- success: (data) => {
- document.querySelector('div#builds').innerHTML = data.html;
- gl.utils.localTimeAgo($('.js-timeago', 'div#builds'));
- this.buildsLoaded = true;
- new gl.Pipelines();
- this.scrollToElement('#builds');
- },
- });
- }
-
loadPipelines(source) {
if (this.pipelinesLoaded) {
return;
diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6
index e47047c4cca..0305aeb07d9 100644
--- a/app/assets/javascripts/merge_request_widget.js.es6
+++ b/app/assets/javascripts/merge_request_widget.js.es6
@@ -74,7 +74,7 @@
MergeRequestWidget.prototype.addEventListeners = function() {
var allowedPages;
- allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes'];
+ allowedPages = ['show', 'commits', 'pipelines', 'changes'];
$(document).on('page:change.merge_request', (function(_this) {
return function() {
var page;
@@ -173,7 +173,6 @@
message = message.replace('{{title}}', data.title);
notify(title, message, _this.opts.gitlab_icon, function() {
this.close();
- return Turbolinks.visit(_this.opts.builds_path);
});
}
}
diff --git a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6
new file mode 100644
index 00000000000..2b074994b4a
--- /dev/null
+++ b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6
@@ -0,0 +1,42 @@
+/* global merge_request_widget */
+
+(() => {
+ $(() => {
+ /* TODO: This needs a better home, or should be refactored. It was previously contained
+ * in a script tag in app/views/projects/merge_requests/widget/open/_accept.html.haml,
+ * but Vue chokes on script tags and prevents their execution. So it was moved here
+ * temporarily.
+ * */
+
+ if ($('.accept-mr-form').length) {
+ $('.accept-mr-form').on('ajax:send', () => {
+ $('.accept-mr-form :input').disable();
+ });
+
+ $('.accept_merge_request').on('click', () => {
+ $('.js-merge-button').html('<i class="fa fa-spinner fa-spin"></i> Merge in progress');
+ });
+
+ $('.merge_when_build_succeeds').on('click', () => {
+ $('#merge_when_build_succeeds').val('1');
+ });
+
+ $('.js-merge-dropdown a').on('click', (e) => {
+ e.preventDefault();
+ $(this).closest('form').submit();
+ });
+ } else if ($('.rebase-in-progress').length) {
+ merge_request_widget.rebaseInProgress();
+ } else if ($('.rebase-mr-form').length) {
+ $('.rebase-mr-form').on('ajax:send', () => {
+ $('.rebase-mr-form :input').disable();
+ });
+
+ $('.js-rebase-button').on('click', () => {
+ $('.js-rebase-button').html("<i class='fa fa-spinner fa-spin'></i> Rebase in progress");
+ });
+ } else {
+ merge_request_widget.getMergeStatus();
+ }
+ });
+})();
diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
new file mode 100644
index 00000000000..90b3366f14b
--- /dev/null
+++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
@@ -0,0 +1,96 @@
+/* eslint-disable no-new */
+/* global Flash */
+
+/**
+ * In each pipelines table we have a mini pipeline graph for each pipeline.
+ *
+ * When we click in a pipeline stage, we need to make an API call to get the
+ * builds list to render in a dropdown.
+ *
+ * The container should be the table element.
+ *
+ * The stage icon clicked needs to have the following HTML structure:
+ * <div>
+ * <button class="dropdown js-builds-dropdown-button"></button>
+ * <div class="js-builds-dropdown-container"></div>
+ * </div>
+ */
+(() => {
+ class MiniPipelineGraph {
+ constructor(opts = {}) {
+ this.container = opts.container || '';
+ this.dropdownListSelector = '.js-builds-dropdown-container';
+ this.getBuildsList = this.getBuildsList.bind(this);
+
+ this.bindEvents();
+ }
+
+ /**
+ * Adds and removes the event listener.
+ */
+ bindEvents() {
+ const dropdownButtonSelector = 'button.js-builds-dropdown-button';
+
+ $(this.container).off('click', dropdownButtonSelector, this.getBuildsList)
+ .on('click', dropdownButtonSelector, this.getBuildsList);
+ }
+
+ /**
+ * For the clicked stage, renders the given data in the dropdown list.
+ *
+ * @param {HTMLElement} stageContainer
+ * @param {Object} data
+ */
+ renderBuildsList(stageContainer, data) {
+ const dropdownContainer = stageContainer.parentElement.querySelector(
+ `${this.dropdownListSelector} .js-builds-dropdown-list`,
+ );
+
+ dropdownContainer.innerHTML = data;
+ }
+
+ /**
+ * For the clicked stage, gets the list of builds.
+ *
+ * @param {Object} e
+ * @return {Promise}
+ */
+ getBuildsList(e) {
+ const button = e.currentTarget;
+ const endpoint = button.dataset.stageEndpoint;
+
+ return $.ajax({
+ dataType: 'json',
+ type: 'GET',
+ url: endpoint,
+ beforeSend: () => {
+ this.renderBuildsList(button, '');
+ this.toggleLoading(button);
+ },
+ success: (data) => {
+ this.toggleLoading(button);
+ this.renderBuildsList(button, data.html);
+ },
+ error: () => {
+ this.toggleLoading(button);
+ new Flash('An error occurred while fetching the builds.', 'alert');
+ },
+ });
+ }
+
+ /**
+ * Toggles the visibility of the loading icon.
+ *
+ * @param {HTMLElement} stageContainer
+ * @return {type}
+ */
+ toggleLoading(stageContainer) {
+ stageContainer.parentElement.querySelector(
+ `${this.dropdownListSelector} .js-builds-dropdown-loading`,
+ ).classList.toggle('hidden');
+ }
+ }
+
+ window.gl = window.gl || {};
+ window.gl.MiniPipelineGraph = MiniPipelineGraph;
+})();
diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js
index 64b19a54893..20a68780cd5 100644
--- a/app/assets/javascripts/network/branch_graph.js
+++ b/app/assets/javascripts/network/branch_graph.js
@@ -356,7 +356,7 @@
icon = this.image(gon.relative_url_root + commit.author.icon, x, y, 20, 20);
nameText = this.text(x + 25, y + 10, commit.author.name);
idText = this.text(x, y + 35, commit.id);
- messageText = this.text(x, y + 50, commit.message);
+ messageText = this.text(x, y + 50, commit.message.replace(/\r?\n/g, " \n "));
textSet = this.set(icon, nameText, idText, messageText).attr({
"text-anchor": "start",
font: "12px Monaco, monospace"
@@ -368,6 +368,7 @@
idText.attr({
fill: "#AAA"
});
+ messageText.node.style["white-space"] = "pre";
this.textWrap(messageText, boxWidth - 50);
rect = this.rect(x - 10, y - 10, boxWidth, 100, 4).attr({
fill: "#FFF",
@@ -404,16 +405,21 @@
s.push("\n");
x = 0;
}
- x += word.length * letterWidth;
- s.push(word + " ");
+ if (word === "\n") {
+ s.push("\n");
+ x = 0;
+ } else {
+ s.push(word + " ");
+ x += word.length * letterWidth;
+ }
}
t.attr({
- text: s.join("")
+ text: s.join("").trim()
});
b = t.getBBox();
- h = Math.abs(b.y2) - Math.abs(b.y) + 1;
+ h = Math.abs(b.y2) + 1;
return t.attr({
- y: b.y + h
+ y: h
});
};
diff --git a/app/assets/javascripts/terminal/terminal.js.es6 b/app/assets/javascripts/terminal/terminal.js.es6
new file mode 100644
index 00000000000..6b9422b1816
--- /dev/null
+++ b/app/assets/javascripts/terminal/terminal.js.es6
@@ -0,0 +1,62 @@
+/* global Terminal */
+
+(() => {
+ class GLTerminal {
+
+ constructor(options) {
+ this.options = options || {};
+
+ this.options.cursorBlink = options.cursorBlink || true;
+ this.options.screenKeys = options.screenKeys || true;
+ this.container = document.querySelector(options.selector);
+
+ this.setSocketUrl();
+ this.createTerminal();
+ $(window).off('resize.terminal').on('resize.terminal', () => {
+ this.terminal.fit();
+ });
+ }
+
+ setSocketUrl() {
+ const { protocol, hostname, port } = window.location;
+ const wsProtocol = protocol === 'https:' ? 'wss://' : 'ws://';
+ const path = this.container.dataset.projectPath;
+
+ this.socketUrl = `${wsProtocol}${hostname}:${port}${path}`;
+ }
+
+ createTerminal() {
+ this.terminal = new Terminal(this.options);
+ this.socket = new WebSocket(this.socketUrl, ['terminal.gitlab.com']);
+ this.socket.binaryType = 'arraybuffer';
+
+ this.terminal.open(this.container);
+ this.socket.onopen = () => { this.runTerminal(); };
+ this.socket.onerror = () => { this.handleSocketFailure(); };
+ }
+
+ runTerminal() {
+ const decoder = new TextDecoder('utf-8');
+ const encoder = new TextEncoder('utf-8');
+
+ this.terminal.on('data', (data) => {
+ this.socket.send(encoder.encode(data));
+ });
+
+ this.socket.addEventListener('message', (ev) => {
+ this.terminal.write(decoder.decode(ev.data));
+ });
+
+ this.isTerminalInitialized = true;
+ this.terminal.fit();
+ }
+
+ handleSocketFailure() {
+ this.terminal.write('\r\nConnection failure');
+ }
+
+ }
+
+ window.gl = window.gl || {};
+ gl.Terminal = GLTerminal;
+})();
diff --git a/app/assets/javascripts/terminal/terminal_bundle.js.es6 b/app/assets/javascripts/terminal/terminal_bundle.js.es6
new file mode 100644
index 00000000000..ded7ee6e9fe
--- /dev/null
+++ b/app/assets/javascripts/terminal/terminal_bundle.js.es6
@@ -0,0 +1,5 @@
+//= require xterm/xterm.js
+//= require xterm/fit.js
+//= require ./terminal.js
+
+$(() => new gl.Terminal({ selector: '#terminal' }));
diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js
index d2aa3c7a841..e407b856e10 100644
--- a/app/assets/javascripts/u2f/authenticate.js
+++ b/app/assets/javascripts/u2f/authenticate.js
@@ -89,7 +89,8 @@
U2FAuthenticate.prototype.renderError = function(error) {
this.renderTemplate('error', {
- error_message: error.message()
+ error_message: error.message(),
+ error_code: error.errorCode
});
return this.container.find('#js-u2f-try-again').on('click', this.renderSetup);
};
diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js
index 69f98c9c0ad..bb9942a3aa0 100644
--- a/app/assets/javascripts/u2f/error.js
+++ b/app/assets/javascripts/u2f/error.js
@@ -9,7 +9,6 @@
this.errorCode = errorCode;
this.message = bind(this.message, this);
this.httpsDisabled = window.location.protocol !== 'https:';
- console.error("U2F Error Code: " + this.errorCode);
}
U2FError.prototype.message = function() {
diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js
index 4f5d68f546b..050c9bfc02e 100644
--- a/app/assets/javascripts/u2f/register.js
+++ b/app/assets/javascripts/u2f/register.js
@@ -76,7 +76,8 @@
U2FRegister.prototype.renderError = function(error) {
this.renderTemplate('error', {
- error_message: error.message()
+ error_message: error.message(),
+ error_code: error.errorCode
});
return this.container.find('#js-u2f-try-again').on('click', this.renderSetup);
};
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 40bc0579393..3cf49f4ff1b 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -9,6 +9,7 @@
@import "framework/asciidoctor.scss";
@import "framework/blocks.scss";
@import "framework/buttons.scss";
+@import "framework/badges.scss";
@import "framework/calendar.scss";
@import "framework/callout.scss";
@import "framework/common.scss";
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index 000e591e09c..48827578d94 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -64,7 +64,7 @@
&.s32 { font-size: 20px; line-height: 30px; }
&.s40 { font-size: 16px; line-height: 38px; }
&.s60 { font-size: 32px; line-height: 58px; }
- &.s70 { font-size: 34px; line-height: 68px; }
+ &.s70 { font-size: 34px; line-height: 70px; }
&.s90 { font-size: 36px; line-height: 88px; }
&.s110 { font-size: 40px; line-height: 108px; font-weight: 300; }
&.s140 { font-size: 72px; line-height: 138px; }
diff --git a/app/assets/stylesheets/framework/badges.scss b/app/assets/stylesheets/framework/badges.scss
new file mode 100644
index 00000000000..e9d7cda0647
--- /dev/null
+++ b/app/assets/stylesheets/framework/badges.scss
@@ -0,0 +1,11 @@
+.badge {
+ font-weight: normal;
+ background-color: $badge-bg;
+ color: $badge-color;
+ vertical-align: baseline;
+}
+
+.badge-dark {
+ background-color: $badge-bg-dark;
+ color: $badge-color-dark;
+}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 9f02749f5ab..e9aadffc73c 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -9,7 +9,7 @@
padding: 20px;
color: $gl-gray;
font-weight: normal;
- font-size: 16px;
+ font-size: 14px;
line-height: 36px;
&.diff-collapsed {
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 59ff17ad2c1..a11f1cd7735 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -230,6 +230,13 @@
}
}
+.btn-terminal {
+ svg {
+ height: 14px;
+ width: 18px;
+ }
+}
+
.btn-lg {
padding: 12px 20px;
}
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 940807fc399..8726a69867b 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -96,6 +96,10 @@ label {
code {
line-height: 1.8;
}
+
+ img {
+ margin-right: $gl-padding;
+ }
}
@media(max-width: $screen-xs-max) {
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 59fae61a44f..5365b62e456 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -33,10 +33,12 @@ body {
}
.alert-wrapper {
- margin-bottom: $gl-padding;
-
.alert {
margin-bottom: 0;
+
+ &:last-child {
+ margin-bottom: $gl-padding;
+ }
}
/* Stripe the background colors so that adjacent alert-warnings are distinct from one another */
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index e4affbb1be1..bbf9de06630 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -76,13 +76,6 @@
color: $black;
}
}
-
- .badge {
- font-weight: normal;
- background-color: $nav-badge-bg;
- color: $gl-gray-light;
- vertical-align: baseline;
- }
}
&.sub-nav {
@@ -434,4 +427,4 @@
border-bottom: none;
}
}
-}
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/framework/page-header.scss b/app/assets/stylesheets/framework/page-header.scss
index fff7d7f7524..625bea96aaa 100644
--- a/app/assets/stylesheets/framework/page-header.scss
+++ b/app/assets/stylesheets/framework/page-header.scss
@@ -57,7 +57,6 @@
}
.ci-status-link {
-
svg {
position: relative;
top: 2px;
diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss
index 5ba0486177f..9d8d08dff88 100644
--- a/app/assets/stylesheets/framework/panels.scss
+++ b/app/assets/stylesheets/framework/panels.scss
@@ -18,6 +18,20 @@
margin-top: -2px;
margin-left: 5px;
}
+
+ &.split {
+ display: flex;
+ align-items: center;
+ }
+
+ .left {
+ flex: 1 1 auto;
+ }
+
+ .right {
+ flex: 0 0 auto;
+ text-align: right;
+ }
}
.panel-body {
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index 718dbbfea27..55bc325b858 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap.scss
@@ -33,7 +33,7 @@
@import "bootstrap/labels";
@import "bootstrap/badges";
@import "bootstrap/alerts";
-// @import "bootstrap/progress-bars";
+@import "bootstrap/progress-bars";
@import "bootstrap/list-group";
@import "bootstrap/wells";
@import "bootstrap/close";
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 460c5d995be..3e1fded6b6b 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -134,8 +134,8 @@ $md-area-border: #ddd;
/*
* Code
*/
-$code_font_size: 13px;
-$code_line_height: 1.5;
+$code_font_size: 12px;
+$code_line_height: 1.6;
/*
* Padding
@@ -280,6 +280,14 @@ $btn-active-gray: #ececec;
$btn-active-gray-light: e4e7ed;
/*
+* Badges
+*/
+$badge-bg: #f3f3f3;
+$badge-bg-dark: #eee;
+$badge-color: #929292;
+$badge-color-dark: #8f8f8f;
+
+/*
* Award emoji
*/
$award-emoji-menu-shadow: rgba(0,0,0,.175);
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 66f7e7f97c8..f9e8d297c05 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -1,3 +1,34 @@
+@keyframes fade-out-status {
+ 0%, 50% { opacity: 1; }
+ 100% { opacity: 0; }
+}
+
+@keyframes blinking-dots {
+ 0% {
+ background-color: rgba($white-light, 1);
+ box-shadow: 12px 0 0 0 rgba($white-light,0.2),
+ 24px 0 0 0 rgba($white-light,0.2);
+ }
+
+ 25% {
+ background-color: rgba($white-light, 0.4);
+ box-shadow: 12px 0 0 0 rgba($white-light,2),
+ 24px 0 0 0 rgba($white-light,0.2);
+ }
+
+ 75% {
+ background-color: rgba($white-light, 0.4);
+ box-shadow: 12px 0 0 0 rgba($white-light,0.2),
+ 24px 0 0 0 rgba($white-light,1);
+ }
+
+ 100% {
+ background-color: rgba($white-light, 1);
+ box-shadow: 12px 0 0 0 rgba($white-light,0.2),
+ 24px 0 0 0 rgba($white-light,0.2);
+ }
+}
+
.build-page {
pre.trace {
background: $builds-trace-bg;
@@ -14,47 +45,101 @@
}
}
- .scroll-controls {
- .scroll-step {
- width: 31px;
- margin: 0 0 0 auto;
+ .environment-information {
+ background-color: $gray-light;
+ border: 1px solid $border-color;
+ padding: 12px $gl-padding;
+ border-radius: $border-radius-default;
+
+ svg {
+ position: relative;
+ top: 1px;
+ margin-right: 5px;
}
+ }
+}
+
+.scroll-controls {
+ height: 100%;
+
+ .scroll-step {
+ width: 31px;
+ margin: 0 0 0 auto;
+ }
+
+ .scroll-link,
+ .autoscroll-container {
+ right: 25px;
+ z-index: 1;
+ }
+
+ .scroll-link {
+ position: fixed;
+ display: block;
+ margin-bottom: 10px;
- &.affix-bottom {
- position: absolute;
- right: 25px;
+ &.scroll-top .gitlab-icon-scroll-up-hover,
+ &.scroll-top:hover .gitlab-icon-scroll-up,
+ &.scroll-bottom .gitlab-icon-scroll-down-hover,
+ &.scroll-bottom:hover .gitlab-icon-scroll-down {
+ display: none;
}
- &.affix {
- right: 25px;
- bottom: 15px;
- z-index: 1;
+ &.scroll-top:hover .gitlab-icon-scroll-up-hover,
+ &.scroll-bottom:hover .gitlab-icon-scroll-down-hover {
+ display: inline-block;
}
- &.sidebar-expanded {
- right: #{$gutter_width + ($gl-padding * 2)};
+ &.scroll-top {
+ top: 110px;
}
- a {
- display: block;
- margin-bottom: 10px;
+ &.scroll-bottom {
+ bottom: -2px;
}
}
- .environment-information {
- background-color: $gray-light;
- border: 1px solid $border-color;
- padding: 12px $gl-padding;
- border-radius: $border-radius-default;
+ .autoscroll-container {
+ position: absolute;
+ }
- svg {
- position: relative;
- top: 1px;
- margin-right: 5px;
+ &.sidebar-expanded {
+
+ .scroll-link,
+ .autoscroll-container {
+ right: ($gutter_width + ($gl-padding * 2));
}
}
}
+.status-message {
+ display: inline-block;
+ color: $white-light;
+
+ .status-icon {
+ display: inline-block;
+ width: 16px;
+ height: 33px;
+ }
+
+ .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;
+ }
+ }
+
+ &:hover .status-text {
+ opacity: 1;
+ }
+}
+
.build-header {
position: relative;
padding: 0;
@@ -109,6 +194,15 @@
.bash {
display: block;
}
+
+ .build-loader-animation {
+ position: relative;
+ width: 6px;
+ height: 6px;
+ margin: auto auto 12px 2px;
+ border-radius: 50%;
+ animation: blinking-dots 1s linear infinite;
+ }
}
.right-sidebar.build-sidebar {
@@ -248,6 +342,12 @@
}
}
+.build-sidebar {
+ .container-fluid.container-limited {
+ max-width: 100%;
+ }
+}
+
.build-detail-row {
margin-bottom: 5px;
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index 80baebd5ea3..9b28df1afc5 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -2,7 +2,6 @@
padding: $gl-padding-top 0;
border-bottom: 1px solid $border-color;
color: $gl-text-color-dark;
- font-size: 16px;
line-height: 34px;
.author {
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 3d60426de01..5517dc5dcbd 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -121,13 +121,6 @@
.folder-name {
cursor: pointer;
-
- .badge {
- font-weight: normal;
- background-color: $gray-darker;
- color: $gl-gray-light;
- vertical-align: baseline;
- }
}
}
@@ -142,4 +135,4 @@
margin-right: 0;
}
}
-}
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 4fac0cfb0ba..eeb5b590625 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -434,6 +434,7 @@
.issuable-meta {
display: inline-block;
line-height: 18px;
+ font-size: 14px;
}
.js-issuable-selector-wrap {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index e779e65eca3..41b1b47713d 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -21,6 +21,14 @@
display: inline-block;
float: left;
+ .btn-success.dropdown-toggle .fa {
+ color: inherit;
+ }
+
+ .btn-success.dropdown-toggle:disabled {
+ background-color: $gl-success;
+ }
+
.accept_merge_request {
&.ci-pending,
&.ci-running {
@@ -96,7 +104,7 @@
.mr-widget-body {
h4 {
font-weight: 600;
- font-size: 17px;
+ font-size: 16px;
margin: 5px 0;
color: $gl-gray-dark;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 106c5d4d390..6ac4ec6ea0d 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -43,7 +43,7 @@ ul.notes {
}
.system-note-message {
- display: inline-block;
+ display: inline;
&::first-letter {
text-transform: lowercase;
@@ -55,7 +55,7 @@ ul.notes {
}
p {
- display: inline-block;
+ display: inline;
margin: 0;
&::first-letter {
@@ -151,6 +151,10 @@ ul.notes {
}
}
}
+
+ .note-headline-light {
+ display: inline;
+ }
}
.discussion-body {
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 09233e36a7b..0b318b61079 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -22,17 +22,22 @@
.table.ci-table {
min-width: 1200px;
+ table-layout: fixed;
.pipeline-id {
color: $black;
}
- .branch-commit {
- width: 30%;
+ .pipeline-date,
+ .pipeline-status {
+ width: 10%;
+ }
- .branch-name {
- max-width: 195px;
- }
+ .pipeline-info,
+ .pipeline-commit,
+ .pipeline-actions,
+ .pipeline-stages {
+ width: 20%;
}
}
}
@@ -75,6 +80,10 @@
td {
padding: 10px 8px;
}
+
+ .commit-link {
+ padding: 9px 8px 10px;
+ }
}
tbody {
@@ -106,7 +115,7 @@
.branch-name {
font-weight: bold;
- max-width: 150px;
+ max-width: 120px;
overflow: hidden;
display: inline-block;
white-space: nowrap;
@@ -132,7 +141,7 @@
.commit-title {
margin-top: 4px;
- max-width: 300px;
+ max-width: 225px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
@@ -188,14 +197,10 @@
width: 8px;
position: absolute;
right: -7px;
- bottom: 9px;
+ bottom: 10px;
border-bottom: 2px solid $border-color;
}
}
-
- a {
- display: block;
- }
}
}
@@ -462,6 +467,25 @@
white-space: normal;
color: $gl-text-color-light;
+ .dropdown-menu-toggle {
+ background-color: transparent;
+ border: none;
+ padding: 0;
+ color: $gl-text-color-light;
+
+ &:focus {
+ outline: none;
+ }
+
+ &:hover {
+ color: $gl-text-color;
+
+ .dropdown-counter-badge {
+ color: $gl-text-color;
+ }
+ }
+ }
+
> .build-content {
display: inline-block;
padding: 8px 10px 9px;
@@ -479,15 +503,10 @@
> .ci-action-icon-container {
position: absolute;
- right: 4px;
+ right: 5px;
top: 5px;
}
- .ci-status-icon {
- position: relative;
- top: 1px;
- }
-
.ci-status-icon svg {
height: 20px;
width: 20px;
@@ -527,7 +546,7 @@
content: '';
position: absolute;
top: 48%;
- right: -49px;
+ right: -48px;
border-top: 2px solid $border-color;
width: 48px;
height: 1px;
@@ -574,36 +593,33 @@
}
}
}
+}
- .ci-status-text {
- max-width: 110px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- vertical-align: bottom;
+.dropdown-counter-badge {
+ float: right;
+ color: $border-color;
+ font-weight: 100;
+ font-size: 15px;
+ margin-right: 2px;
+}
+
+.grouped-pipeline-dropdown {
+ padding: 0;
+ width: 191px;
+ left: auto;
+ right: -195px;
+ top: -4px;
+ box-shadow: 0 1px 5px $black-transparent;
+
+ a {
display: inline-block;
- position: relative;
- font-weight: 100;
}
- .dropdown-menu-toggle {
- background-color: transparent;
- border: none;
- padding: 0;
- color: $gl-text-color-light;
- white-space: normal;
- overflow: visible;
-
- &:focus {
- outline: none;
- }
+ .build-content {
+ width: 138px;
&:hover {
- color: $gl-text-color;
-
- .dropdown-counter-badge {
- color: $gl-text-color;
- }
+ background-color: $stage-hover-bg;
}
}
@@ -678,27 +694,44 @@
border-radius: 3px;
color: $gl-text-color;
}
+=======
+ ul {
+ max-height: 245px;
+ overflow: auto;
+ margin: 3px 0;
+>>>>>>> master
- .stage {
- max-width: 100px;
- width: 100px;
- }
+ li {
+ padding-top: 2px;
+ margin: 4px 7px;
+ padding: 0 3px;
+ padding-left: 0;
+ padding-bottom: 0;
+ line-height: 0;
- .ci-status-icon svg {
- height: 18px;
- width: 18px;
+ .ci-action-icon-container:hover {
+ background-color: transparent;
}
- .ci-status-text {
- max-width: 95px;
- padding-bottom: 3px;
+ .ci-status-icon {
position: relative;
- top: 3px;
+ top: 2px;
}
}
}
}
+.ci-status-text {
+ max-width: 110px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: bottom;
+ display: inline-block;
+ position: relative;
+ font-weight: 100;
+}
+
// Action Icons
.ci-action-icon-container .ci-action-icon-wrapper {
i {
@@ -728,3 +761,225 @@
padding: 5px 5px 5px 7px;
}
}
+
+.dropdown-build {
+ color: $gl-text-color-light;
+
+ .build-content {
+ padding: 3px 7px 6px;
+ }
+
+ .ci-action-icon-container {
+ padding: 0;
+ font-size: 11px;
+ float: right;
+ margin-top: 3px;
+ display: inline-block;
+ position: relative;
+
+ i {
+ font-size: 11px;
+ margin-top: 0;
+ }
+ }
+
+ .ci-action-icon-container {
+ i {
+ width: 24px;
+ height: 24px;
+
+ &::before {
+ top: 1px;
+ left: 1px;
+ }
+ }
+ }
+
+ .stage {
+ max-width: 100px;
+ width: 100px;
+ }
+
+ .ci-status-icon svg {
+ height: 18px;
+ width: 18px;
+ }
+
+ .ci-status-text {
+ max-width: 95px;
+ }
+}
+
+/**
+ * Builds dropdown in mini pipeline
+ */
+.mini-pipeline-graph {
+ .builds-dropdown {
+ background-color: transparent;
+ border: none;
+ padding: 0;
+ color: $gl-text-color-light;
+ border: none;
+ margin: 0;
+ }
+
+ .dropdown-build .build-content {
+ padding: 3px 7px 7px;
+ }
+
+ .builds-dropdown-loading {
+ margin: 10px auto;
+ width: 18px;
+ }
+
+ .grouped-pipeline-dropdown {
+ right: -172px;
+ top: 23px;
+ min-height: 50px;
+
+ a {
+ color: $gl-text-color-light;
+ }
+ }
+
+ .arrow-up {
+ &::before,
+ &::after {
+ content: '';
+ display: inline-block;
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid;
+ top: -6px;
+ left: 2px;
+ border-width: 0 5px 6px;
+ }
+
+ &::before {
+ border-width: 0 5px 5px;
+ border-bottom-color: $border-color;
+ }
+
+ &::after {
+ margin-top: 1px;
+ border-bottom-color: $white-light;
+ }
+ }
+}
+
+/**
+ * Icons in mini pipeline graph
+ */
+.mini-pipeline-graph-icon-container .ci-status-icon {
+ display: inline-block;
+ border: 1px solid;
+ border-radius: 22px;
+ margin-right: 1px;
+ width: 22px;
+ height: 22px;
+ position: relative;
+ z-index: 2;
+ transition: all 0.2s cubic-bezier(0.25, 0, 1, 1);
+
+ svg {
+ top: -1px;
+ left: -1px;
+ }
+}
+
+.stage-cell .mini-pipeline-graph-icon-container .ci-status-icon svg {
+ width: 22px;
+ height: 22px;
+}
+
+.builds-dropdown {
+ &:focus {
+ outline: none;
+ margin-right: -8px;
+
+ .ci-status-icon {
+ width: 32px;
+ padding: 0 8px 0 0;
+ transition: width 0.2s cubic-bezier(0.25, 0, 1, 1);
+
+ + .dropdown-caret {
+ display: inline-block;
+ }
+ }
+ }
+
+ &:focus,
+ &:active {
+ .ci-status-icon-success {
+ background-color: rgba($gl-success, .1);
+ }
+
+ .ci-status-icon-failed {
+ background-color: rgba($gl-danger, .1);
+ }
+
+ .ci-status-icon-pending,
+ .ci-status-icon-success_with_warnings {
+ background-color: rgba($gl-warning, .1);
+ }
+
+ .ci-status-icon-running {
+ background-color: rgba($blue-normal, .1);
+ }
+
+ .ci-status-icon-canceled,
+ .ci-status-icon-disabled,
+ .ci-status-icon-not-found {
+ background-color: rgba($gl-gray, .1);
+ }
+
+ .ci-status-icon-created,
+ .ci-status-icon-skipped {
+ background-color: rgba($gray-darkest, .1);
+ }
+ }
+
+ .mini-pipeline-graph-icon-container {
+ .ci-status-icon:hover,
+ .ci-status-icon:focus {
+ width: 32px;
+ padding: 0 8px 0 0;
+
+ + .dropdown-caret {
+ display: inline-block;
+ }
+ }
+
+ .dropdown-caret {
+ font-size: 11px;
+ position: relative;
+ top: 3px;
+ left: -14px;
+ margin-right: -6px;
+ display: none;
+ z-index: 2;
+ }
+ }
+}
+
+.terminal-icon {
+ margin-left: 3px;
+}
+
+.terminal-container {
+ .content-block {
+ border-bottom: none;
+ }
+
+ #terminal {
+ margin-top: 10px;
+ min-height: 450px;
+ box-sizing: border-box;
+
+ > div {
+ min-height: 450px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index a443b6a37b3..d6aa4c4c032 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -93,7 +93,6 @@
.group-avatar {
float: none;
margin: 0 auto;
- border: none;
&.identicon {
border-radius: 50%;
@@ -881,3 +880,23 @@ pre.light-well {
width: 30%;
}
}
+
+.services-installation-info .row {
+ margin-bottom: 10px;
+}
+
+.service-installation {
+ padding: 32px;
+ margin: 32px;
+ border-radius: 3px;
+ background-color: $white-light;
+
+ h3 {
+ margin-top: 0;
+ }
+
+ hr {
+ margin: 32px 0;
+ border-color: $border-color;
+ }
+}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 055dacd81f4..4acd17360c1 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -1,7 +1,6 @@
.container-fluid {
.ci-status {
- display: inline-block;
- padding: 2px 7px;
+ padding: 2px 7px 4px;
margin-right: 10px;
border: 1px solid $gray-darker;
white-space: nowrap;
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 05c0a4c29f4..cad4e149845 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -14,6 +14,7 @@
.add-to-tree {
vertical-align: top;
+ padding: 6px 10px;
}
.tree-table {
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 4df80195ae1..bb47e2a8bf7 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -25,7 +25,7 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
helper_method :can?, :current_application_settings
- helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
+ helper_method :import_sources_enabled?, :github_import_enabled?, :gitea_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
@@ -245,6 +245,10 @@ class ApplicationController < ActionController::Base
current_application_settings.import_sources.include?('github')
end
+ def gitea_import_enabled?
+ current_application_settings.import_sources.include?('gitea')
+ end
+
def github_import_configured?
Gitlab::OAuth::Provider.enabled?(:github)
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index b83c3a872cf..efe9c001bcf 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -82,6 +82,8 @@ class GroupsController < Groups::ApplicationController
if Groups::UpdateService.new(@group, current_user, group_params).execute
redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated."
else
+ @group.reset_path!
+
render action: "edit"
end
end
diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb
new file mode 100644
index 00000000000..fbd851c64a7
--- /dev/null
+++ b/app/controllers/import/gitea_controller.rb
@@ -0,0 +1,45 @@
+class Import::GiteaController < Import::GithubController
+ def new
+ if session[access_token_key].present? && session[host_key].present?
+ redirect_to status_import_url
+ end
+ end
+
+ def personal_access_token
+ session[host_key] = params[host_key]
+ super
+ end
+
+ def status
+ @gitea_host_url = session[host_key]
+ super
+ end
+
+ private
+
+ def host_key
+ :"#{provider}_host_url"
+ end
+
+ # Overriden methods
+ def provider
+ :gitea
+ end
+
+ # Gitea is not yet an OAuth provider
+ # See https://github.com/go-gitea/gitea/issues/27
+ def logged_in_with_provider?
+ false
+ end
+
+ def provider_auth
+ if session[access_token_key].blank? || session[host_key].blank?
+ redirect_to new_import_gitea_url,
+ alert: 'You need to specify both an Access Token and a Host URL.'
+ end
+ end
+
+ def client_options
+ { host: session[host_key], api_version: 'v1' }
+ end
+end
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index ee7d498c59c..53a5981e564 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -1,39 +1,37 @@
class Import::GithubController < Import::BaseController
- before_action :verify_github_import_enabled
- before_action :github_auth, only: [:status, :jobs, :create]
+ before_action :verify_import_enabled
+ before_action :provider_auth, only: [:status, :jobs, :create]
- rescue_from Octokit::Unauthorized, with: :github_unauthorized
-
- helper_method :logged_in_with_github?
+ rescue_from Octokit::Unauthorized, with: :provider_unauthorized
def new
- if logged_in_with_github?
- go_to_github_for_permissions
- elsif session[:github_access_token]
- redirect_to status_import_github_url
+ if logged_in_with_provider?
+ go_to_provider_for_permissions
+ elsif session[access_token_key]
+ redirect_to status_import_url
end
end
def callback
- session[:github_access_token] = client.get_token(params[:code])
- redirect_to status_import_github_url
+ session[access_token_key] = client.get_token(params[:code])
+ redirect_to status_import_url
end
def personal_access_token
- session[:github_access_token] = params[:personal_access_token]
- redirect_to status_import_github_url
+ session[access_token_key] = params[:personal_access_token]
+ redirect_to status_import_url
end
def status
@repos = client.repos
- @already_added_projects = current_user.created_projects.where(import_type: "github")
+ @already_added_projects = current_user.created_projects.where(import_type: provider)
already_added_projects_names = @already_added_projects.pluck(:import_source)
- @repos.reject!{ |repo| already_added_projects_names.include? repo.full_name }
+ @repos.reject! { |repo| already_added_projects_names.include? repo.full_name }
end
def jobs
- jobs = current_user.created_projects.where(import_type: "github").to_json(only: [:id, :import_status])
+ jobs = current_user.created_projects.where(import_type: provider).to_json(only: [:id, :import_status])
render json: jobs
end
@@ -44,8 +42,8 @@ class Import::GithubController < Import::BaseController
namespace_path = params[:target_namespace].presence || current_user.namespace_path
@target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path)
- if current_user.can?(:create_projects, @target_namespace)
- @project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params).execute
+ if can?(current_user, :create_projects, @target_namespace)
+ @project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params, type: provider).execute
else
render 'unauthorized'
end
@@ -54,34 +52,63 @@ class Import::GithubController < Import::BaseController
private
def client
- @client ||= Gitlab::GithubImport::Client.new(session[:github_access_token])
+ @client ||= Gitlab::GithubImport::Client.new(session[access_token_key], client_options)
end
- def verify_github_import_enabled
- render_404 unless github_import_enabled?
+ def verify_import_enabled
+ render_404 unless import_enabled?
end
- def github_auth
- if session[:github_access_token].blank?
- go_to_github_for_permissions
- end
+ def go_to_provider_for_permissions
+ redirect_to client.authorize_url(callback_import_url)
end
- def go_to_github_for_permissions
- redirect_to client.authorize_url(callback_import_github_url)
+ def import_enabled?
+ __send__("#{provider}_import_enabled?")
end
- def github_unauthorized
- session[:github_access_token] = nil
- redirect_to new_import_github_url,
- alert: 'Access denied to your GitHub account.'
+ def new_import_url
+ public_send("new_import_#{provider}_url")
end
- def logged_in_with_github?
- current_user.identities.exists?(provider: 'github')
+ def status_import_url
+ public_send("status_import_#{provider}_url")
+ end
+
+ def callback_import_url
+ public_send("callback_import_#{provider}_url")
+ end
+
+ def provider_unauthorized
+ session[access_token_key] = nil
+ redirect_to new_import_url,
+ alert: "Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account."
+ end
+
+ def access_token_key
+ :"#{provider}_access_token"
end
def access_params
- { github_access_token: session[:github_access_token] }
+ { github_access_token: session[access_token_key] }
+ end
+
+ # The following methods are overriden in subclasses
+ def provider
+ :github
+ end
+
+ def logged_in_with_provider?
+ current_user.identities.exists?(provider: provider)
+ end
+
+ def provider_auth
+ if session[access_token_key].blank?
+ go_to_provider_for_permissions
+ end
+ end
+
+ def client_options
+ {}
end
end
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 8197d9e4c99..791ed88db30 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -8,13 +8,11 @@ class Projects::CommitController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
- before_action :authorize_download_code!, except: [:cancel_builds, :retry_builds]
- before_action :authorize_update_build!, only: [:cancel_builds, :retry_builds]
+ before_action :authorize_download_code!
before_action :authorize_read_pipeline!, only: [:pipelines]
- before_action :authorize_read_commit_status!, only: [:builds]
before_action :commit
- before_action :define_commit_vars, only: [:show, :diff_for_path, :builds, :pipelines]
- before_action :define_status_vars, only: [:show, :builds, :pipelines]
+ before_action :define_commit_vars, only: [:show, :diff_for_path, :pipelines]
+ before_action :define_status_vars, only: [:show, :pipelines]
before_action :define_note_vars, only: [:show, :diff_for_path]
before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
@@ -35,25 +33,6 @@ class Projects::CommitController < Projects::ApplicationController
def pipelines
end
- def builds
- end
-
- def cancel_builds
- ci_builds.running_or_pending.each(&:cancel)
-
- redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha)
- end
-
- def retry_builds
- ci_builds.latest.failed.each do |build|
- if build.retryable?
- Ci::Build.retry(build, current_user)
- end
- end
-
- redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha)
- end
-
def branches
@branches = @project.repository.branch_names_contains(commit.id)
@tags = @project.repository.tag_names_contains(commit.id)
@@ -98,10 +77,6 @@ class Projects::CommitController < Projects::ApplicationController
@noteable = @commit ||= @project.commit(params[:id])
end
- def ci_builds
- @ci_builds ||= Ci::Build.where(pipeline: pipelines)
- end
-
def define_commit_vars
return git_not_found! unless commit
@@ -133,8 +108,6 @@ class Projects::CommitController < Projects::ApplicationController
def define_status_vars
@ci_pipelines = project.pipelines.where(sha: commit.sha)
- @statuses = CommitStatus.where(pipeline: @ci_pipelines).relevant
- @builds = Ci::Build.where(pipeline: @ci_pipelines).relevant
end
def assign_change_commit_vars(mr_source_branch)
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 6bd4cb3f2f5..87cc36253f1 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -4,17 +4,19 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_create_deployment!, only: [:stop]
before_action :authorize_update_environment!, only: [:edit, :update]
- before_action :environment, only: [:show, :edit, :update, :stop]
+ before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize]
+ before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize]
+ before_action :verify_api_request!, only: :terminal_websocket_authorize
def index
@scope = params[:scope]
@environments = project.environments
-
+
respond_to do |format|
format.html
format.json do
render json: EnvironmentSerializer
- .new(project: @project)
+ .new(project: @project, user: current_user)
.represent(@environments)
end
end
@@ -56,8 +58,33 @@ class Projects::EnvironmentsController < Projects::ApplicationController
redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action])
end
+ def terminal
+ # Currently, this acts as a hint to load the terminal details into the cache
+ # if they aren't there already. In the future, users will need these details
+ # to choose between terminals to connect to.
+ @terminals = environment.terminals
+ end
+
+ # GET .../terminal.ws : implemented in gitlab-workhorse
+ def terminal_websocket_authorize
+ # Just return the first terminal for now. If the list is in the process of
+ # being looked up, this may result in a 404 response, so the frontend
+ # should retry those errors
+ terminal = environment.terminals.try(:first)
+ if terminal
+ set_workhorse_internal_api_content_type
+ render json: Gitlab::Workhorse.terminal_websocket(terminal)
+ else
+ render text: 'Not found', status: 404
+ end
+ end
+
private
+ def verify_api_request!
+ Gitlab::Workhorse.verify_api_request!(request.headers)
+ end
+
def environment_params
params.require(:environment).permit(:name, :external_url)
end
diff --git a/app/controllers/projects/mattermosts_controller.rb b/app/controllers/projects/mattermosts_controller.rb
new file mode 100644
index 00000000000..d87dff2a80e
--- /dev/null
+++ b/app/controllers/projects/mattermosts_controller.rb
@@ -0,0 +1,43 @@
+class Projects::MattermostsController < Projects::ApplicationController
+ include TriggersHelper
+ include ActionView::Helpers::AssetUrlHelper
+
+ layout 'project_settings'
+
+ before_action :authorize_admin_project!
+ before_action :service
+ before_action :teams, only: [:new]
+
+ def new
+ end
+
+ def create
+ result, message = @service.configure(current_user, configure_params)
+
+ if result
+ flash[:notice] = 'This service is now configured'
+ redirect_to edit_namespace_project_service_path(
+ @project.namespace, @project, service)
+ else
+ flash[:alert] = message || 'Failed to configure service'
+ redirect_to new_namespace_project_mattermost_path(
+ @project.namespace, @project)
+ end
+ end
+
+ private
+
+ def configure_params
+ params.require(:mattermost).permit(:trigger, :team_id).merge(
+ url: service_trigger_url(@service),
+ icon_url: asset_url('gitlab_logo.png'))
+ end
+
+ def teams
+ @teams ||= @service.list_teams(current_user)
+ end
+
+ def service
+ @service ||= @project.find_or_initialize_service('mattermost_slash_commands')
+ end
+end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index f0cb5a9d4b4..3abebdfd032 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -9,10 +9,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled
before_action :merge_request, only: [
- :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines, :merge, :merge_check,
+ :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :merge_check,
:ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues
]
- before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines]
+ before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines]
before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines]
before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check]
before_action :define_commit_vars, only: [:diffs]
@@ -201,17 +201,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
- def builds
- respond_to do |format|
- format.html do
- define_discussion_vars
-
- render 'show'
- end
- format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_builds') } }
- end
- end
-
def pipelines
@pipelines = @merge_request.all_pipelines
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 85188cfdd4c..cc347922c6a 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -8,6 +8,7 @@ class Projects::PipelinesController < Projects::ApplicationController
def index
@scope = params[:scope]
@pipelines = PipelinesFinder.new(project).execute(scope: @scope).page(params[:page]).per(30)
+ @pipelines = @pipelines.includes(project: :namespace)
@running_or_pending_count = PipelinesFinder.new(project).execute(scope: 'running').count
@pipelines_count = PipelinesFinder.new(project).execute.count
@@ -40,6 +41,15 @@ class Projects::PipelinesController < Projects::ApplicationController
end
end
+ def stage
+ @stage = pipeline.stage(params[:stage])
+ return not_found unless @stage
+
+ respond_to do |format|
+ format.json { render json: { html: view_to_html_string('projects/pipelines/_stage') } }
+ end
+ end
+
def retry
pipeline.retry_failed(current_user)
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index 92bac149313..1ee6c1d3afa 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -1,5 +1,5 @@
module AuthHelper
- PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2).freeze
+ PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze
FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze
def ldap_enabled?
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index f31d4fb897d..c3508443d8a 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -188,7 +188,7 @@ module BlobHelper
end
def gitlab_ci_ymls
- @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names
+ @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names(params[:context])
end
def dockerfile_names
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index d9f5e01f0dc..94f3b480178 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -1,7 +1,7 @@
module CiStatusHelper
def ci_status_path(pipeline)
project = pipeline.project
- builds_namespace_project_commit_path(project.namespace, project, pipeline.sha)
+ namespace_project_pipeline_path(project.namespace, project, pipeline)
end
# Is used by Commit and Merge Request Widget
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 66a720a9426..e9461b9f859 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -128,50 +128,11 @@ module CommitsHelper
end
def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
- return unless current_user
-
- tooltip = "Revert this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip
-
- if can_collaborate_with_project?
- btn_class = "btn btn-warning btn-#{btn_class}" unless btn_class.nil?
- link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
- elsif can?(current_user, :fork_project, @project)
- continue_params = {
- to: continue_to_path,
- notice: edit_in_new_fork_notice + ' Try to revert this commit again.',
- notice_now: edit_in_new_fork_notice_now
- }
- fork_path = namespace_project_forks_path(@project.namespace, @project,
- namespace_key: current_user.namespace.id,
- continue: continue_params)
-
- btn_class = "btn btn-grouped btn-warning" unless btn_class.nil?
-
- link_to 'Revert', fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip)
- end
+ commit_action_link('revert', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip)
end
def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
- return unless current_user
-
- tooltip = "Cherry-pick this #{commit.change_type_title(current_user)} in a new merge request"
-
- if can_collaborate_with_project?
- btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil?
- link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
- elsif can?(current_user, :fork_project, @project)
- continue_params = {
- to: continue_to_path,
- notice: edit_in_new_fork_notice + ' Try to cherry-pick this commit again.',
- notice_now: edit_in_new_fork_notice_now
- }
- fork_path = namespace_project_forks_path(@project.namespace, @project,
- namespace_key: current_user.namespace.id,
- continue: continue_params)
-
- btn_class = "btn btn-grouped btn-close" unless btn_class.nil?
- link_to 'Cherry-pick', fork_path, class: "#{btn_class}", method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip)
- end
+ commit_action_link('cherry-pick', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip)
end
protected
@@ -211,6 +172,28 @@ module CommitsHelper
end
end
+ def commit_action_link(action, commit, continue_to_path, btn_class: nil, has_tooltip: true)
+ return unless current_user
+
+ tooltip = "#{action.capitalize} this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip
+ btn_class = "btn btn-#{btn_class}" unless btn_class.nil?
+
+ if can_collaborate_with_project?
+ link_to action.capitalize, "#modal-#{action}-commit", 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
+ elsif can?(current_user, :fork_project, @project)
+ continue_params = {
+ to: continue_to_path,
+ notice: "#{edit_in_new_fork_notice} Try to #{action} this commit again.",
+ notice_now: edit_in_new_fork_notice_now
+ }
+ fork_path = namespace_project_forks_path(@project.namespace, @project,
+ namespace_key: current_user.namespace.id,
+ continue: continue_params)
+
+ link_to action.capitalize, fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip)
+ end
+ end
+
def view_file_btn(commit_sha, diff_new_path, project)
link_to(
namespace_project_blob_path(project.namespace, project,
diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb
index 021d2b14718..a0642a1894b 100644
--- a/app/helpers/import_helper.rb
+++ b/app/helpers/import_helper.rb
@@ -4,8 +4,10 @@ module ImportHelper
"#{namespace}/#{name}"
end
- def github_project_link(path_with_namespace)
- link_to path_with_namespace, github_project_url(path_with_namespace), target: '_blank'
+ def provider_project_link(provider, path_with_namespace)
+ url = __send__("#{provider}_project_url", path_with_namespace)
+
+ link_to path_with_namespace, url, target: '_blank'
end
private
@@ -20,4 +22,8 @@ module ImportHelper
provider = Gitlab.config.omniauth.providers.find { |p| p.name == 'github' }
@github_url = provider.fetch('url', 'https://github.com') if provider
end
+
+ def gitea_project_url(path_with_namespace)
+ "#{@gitea_host_url.sub(%r{/+\z}, '')}/#{path_with_namespace}"
+ end
end
diff --git a/app/helpers/mattermost_helper.rb b/app/helpers/mattermost_helper.rb
new file mode 100644
index 00000000000..49ac12db832
--- /dev/null
+++ b/app/helpers/mattermost_helper.rb
@@ -0,0 +1,9 @@
+module MattermostHelper
+ def mattermost_teams_options(teams)
+ teams_options = teams.map do |id, options|
+ [options['display_name'] || options['name'], id]
+ end
+
+ teams_options.compact.unshift(['Select team...', '0'])
+ end
+end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index d2177f683a1..7445f3c113c 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -280,13 +280,15 @@ module ProjectsHelper
end
end
- def add_special_file_path(project, file_name:, commit_message: nil)
+ def add_special_file_path(project, file_name:, commit_message: nil, target_branch: nil, context: nil)
namespace_project_new_blob_path(
project.namespace,
project,
project.default_branch || 'master',
file_name: file_name,
- commit_message: commit_message || "Add #{file_name.downcase}"
+ commit_message: commit_message || "Add #{file_name.downcase}",
+ target_branch: target_branch,
+ context: context
)
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 48354cdbefb..f2f6453b3b9 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -116,6 +116,11 @@ module Ci
where.not(duration: nil).sum(:duration)
end
+ def stage(name)
+ stage = Ci::Stage.new(self, name: name)
+ stage unless stage.statuses_count.zero?
+ end
+
def stages_count
statuses.select(:stage).distinct.count
end
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 7ef59445d77..d035eda6df5 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -18,6 +18,10 @@ module Ci
name
end
+ def statuses_count
+ @statuses_count ||= statuses.count
+ end
+
def status
@status ||= statuses.latest.status
end
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index 4359f1d7b06..8f02c226f0b 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -1,10 +1,15 @@
module Milestoneish
def closed_items_count(user)
- issues_visible_to_user(user).closed.size + merge_requests.closed_and_merged.size
+ memoize_per_user(user, :closed_items_count) do
+ (count_issues_by_state(user)['closed'] || 0) + merge_requests.closed_and_merged.size
+ end
end
def total_items_count(user)
- issues_visible_to_user(user).size + merge_requests.size
+ memoize_per_user(user, :total_items_count) do
+ issues_count = count_issues_by_state(user).values.sum
+ issues_count + merge_requests.size
+ end
end
def complete?(user)
@@ -30,7 +35,10 @@ module Milestoneish
end
def issues_visible_to_user(user)
- IssuesFinder.new(user).execute.where(id: issues)
+ memoize_per_user(user, :issues_visible_to_user) do
+ params = try(:project_id) ? { project_id: project_id } : {}
+ IssuesFinder.new(user, params).execute.where(milestone_id: milestoneish_ids)
+ end
end
def upcoming?
@@ -50,4 +58,18 @@ module Milestoneish
def expired?
due_date && due_date.past?
end
+
+ private
+
+ def count_issues_by_state(user)
+ memoize_per_user(user, :count_issues_by_state) do
+ issues_visible_to_user(user).reorder(nil).group(:state).count
+ end
+ end
+
+ def memoize_per_user(user, method_name)
+ @memoized ||= {}
+ @memoized[method_name] ||= {}
+ @memoized[method_name][user.try!(:id)] ||= yield
+ end
end
diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb
new file mode 100644
index 00000000000..944519a3070
--- /dev/null
+++ b/app/models/concerns/reactive_caching.rb
@@ -0,0 +1,114 @@
+# The ReactiveCaching concern is used to fetch some data in the background and
+# store it in the Rails cache, keeping it up-to-date for as long as it is being
+# requested. If the data hasn't been requested for +reactive_cache_lifetime+,
+# it stop being refreshed, and then be removed.
+#
+# Example of use:
+#
+# class Foo < ActiveRecord::Base
+# include ReactiveCaching
+#
+# self.reactive_cache_key = ->(thing) { ["foo", thing.id] }
+#
+# after_save :clear_reactive_cache!
+#
+# def calculate_reactive_cache
+# # Expensive operation here. The return value of this method is cached
+# end
+#
+# def result
+# with_reactive_cache do |data|
+# # ...
+# end
+# end
+# end
+#
+# In this example, the first time `#result` is called, it will return `nil`.
+# However, it will enqueue a background worker to call `#calculate_reactive_cache`
+# and set an initial cache lifetime of ten minutes.
+#
+# Each time the background job completes, it stores the return value of
+# `#calculate_reactive_cache`. It is also re-enqueued to run again after
+# `reactive_cache_refresh_interval`, so keeping the stored value up to date.
+# Calculations are never run concurrently.
+#
+# Calling `#result` while a value is in the cache will call the block given to
+# `#with_reactive_cache`, yielding the cached value. It will also extend the
+# lifetime by `reactive_cache_lifetime`.
+#
+# Once the lifetime has expired, no more background jobs will be enqueued and
+# calling `#result` will again return `nil` - starting the process all over
+# again
+module ReactiveCaching
+ extend ActiveSupport::Concern
+
+ included do
+ class_attribute :reactive_cache_lease_timeout
+
+ class_attribute :reactive_cache_key
+ class_attribute :reactive_cache_lifetime
+ class_attribute :reactive_cache_refresh_interval
+
+ # defaults
+ self.reactive_cache_lease_timeout = 2.minutes
+
+ self.reactive_cache_refresh_interval = 1.minute
+ self.reactive_cache_lifetime = 10.minutes
+
+ def calculate_reactive_cache
+ raise NotImplementedError
+ end
+
+ def with_reactive_cache(&blk)
+ within_reactive_cache_lifetime do
+ data = Rails.cache.read(full_reactive_cache_key)
+ yield data if data.present?
+ end
+ ensure
+ Rails.cache.write(full_reactive_cache_key('alive'), true, expires_in: self.class.reactive_cache_lifetime)
+ ReactiveCachingWorker.perform_async(self.class, id)
+ end
+
+ def clear_reactive_cache!
+ Rails.cache.delete(full_reactive_cache_key)
+ end
+
+ def exclusively_update_reactive_cache!
+ locking_reactive_cache do
+ within_reactive_cache_lifetime do
+ enqueuing_update do
+ value = calculate_reactive_cache
+ Rails.cache.write(full_reactive_cache_key, value)
+ end
+ end
+ end
+ end
+
+ private
+
+ def full_reactive_cache_key(*qualifiers)
+ prefix = self.class.reactive_cache_key
+ prefix = prefix.call(self) if prefix.respond_to?(:call)
+
+ ([prefix].flatten + qualifiers).join(':')
+ end
+
+ def locking_reactive_cache
+ lease = Gitlab::ExclusiveLease.new(full_reactive_cache_key, timeout: reactive_cache_lease_timeout)
+ uuid = lease.try_obtain
+ yield if uuid
+ ensure
+ Gitlab::ExclusiveLease.cancel(full_reactive_cache_key, uuid)
+ end
+
+ def within_reactive_cache_lifetime
+ yield if Rails.cache.read(full_reactive_cache_key('alive'))
+ end
+
+ def enqueuing_update
+ yield
+ ensure
+ ReactiveCachingWorker.perform_in(self.class.reactive_cache_refresh_interval, self.class, id)
+ end
+ end
+end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 8ef1c841ea3..5cde94b3509 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -128,6 +128,14 @@ class Environment < ActiveRecord::Base
end
end
+ def has_terminals?
+ project.deployment_service.present? && available? && last_deployment.present?
+ end
+
+ def terminals
+ project.deployment_service.terminals(self) if has_terminals?
+ end
+
# An environment name is not necessarily suitable for use in URLs, DNS
# or other third-party contexts, so provide a slugified version. A slug has
# the following properties:
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index b01607dcda9..a54e478f5f8 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -24,12 +24,16 @@ class GlobalMilestone
@first_milestone = milestones.find {|m| m.description.present? } || milestones.first
end
+ def milestoneish_ids
+ milestones.select(:id)
+ end
+
def safe_title
@title.to_slug.normalize.to_s
end
def projects
- @projects ||= Project.for_milestones(milestones.select(:id))
+ @projects ||= Project.for_milestones(milestoneish_ids)
end
def state
@@ -49,11 +53,11 @@ class GlobalMilestone
end
def issues
- @issues ||= Issue.of_milestones(milestones.select(:id)).includes(:project, :assignee, :labels)
+ @issues ||= Issue.of_milestones(milestoneish_ids).includes(:project, :assignee, :labels)
end
def merge_requests
- @merge_requests ||= MergeRequest.of_milestones(milestones.select(:id)).includes(:target_project, :assignee, :labels)
+ @merge_requests ||= MergeRequest.of_milestones(milestoneish_ids).includes(:target_project, :assignee, :labels)
end
def participants
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 738c96e4db3..6825553512f 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -39,6 +39,8 @@ class Issue < ActiveRecord::Base
scope :created_after, -> (datetime) { where("created_at >= ?", datetime) }
+ scope :include_associations, -> { includes(:assignee, :labels, project: :namespace) }
+
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 45ca97adad1..0dcfec89f14 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -129,6 +129,10 @@ class Milestone < ActiveRecord::Base
self.title
end
+ def milestoneish_ids
+ id
+ end
+
def can_be_closed?
active? && issues.opened.count.zero?
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index fd42f2328d8..b52f08c7081 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -98,7 +98,7 @@ class Namespace < ActiveRecord::Base
def move_dir
if any_project_has_container_registry_tags?
- raise Exception.new('Namespace cannot be moved, because at least one project has tags in container registry')
+ raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry')
end
# Move the namespace directory in all storages paths used by member projects
@@ -111,7 +111,7 @@ class Namespace < ActiveRecord::Base
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
- raise Exception.new('namespace directory cannot be moved')
+ raise Gitlab::UpdatePathError.new('namespace directory cannot be moved')
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 5d5d6737dad..26fa20f856d 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -79,7 +79,6 @@ class Project < ActiveRecord::Base
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
has_many :boards, before_add: :validate_board_limit, dependent: :destroy
- has_many :chat_services
# Project services
has_one :campfire_service, dependent: :destroy
@@ -95,8 +94,9 @@ class Project < ActiveRecord::Base
has_one :asana_service, dependent: :destroy
has_one :gemnasium_service, dependent: :destroy
has_one :mattermost_slash_commands_service, dependent: :destroy
- has_one :mattermost_notification_service, dependent: :destroy
- has_one :slack_notification_service, dependent: :destroy
+ has_one :mattermost_service, dependent: :destroy
+ has_one :slack_slash_commands_service, dependent: :destroy
+ has_one :slack_service, dependent: :destroy
has_one :buildkite_service, dependent: :destroy
has_one :bamboo_service, dependent: :destroy
has_one :teamcity_service, dependent: :destroy
@@ -533,6 +533,10 @@ class Project < ActiveRecord::Base
import_type == 'gitlab_project'
end
+ def gitea_import?
+ import_type == 'gitea'
+ end
+
def check_limit
unless creator.can_create_project? or namespace.kind == 'group'
projects_limit = creator.projects_limit
diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb
index a00d43773d9..4c7f4f5a429 100644
--- a/app/models/project_authorization.rb
+++ b/app/models/project_authorization.rb
@@ -5,4 +5,17 @@ class ProjectAuthorization < ActiveRecord::Base
validates :project, presence: true
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
validates :user, uniqueness: { scope: [:project, :access_level] }, presence: true
+
+ def self.insert_authorizations(rows, per_batch = 1000)
+ rows.each_slice(per_batch) do |slice|
+ tuples = slice.map do |tuple|
+ tuple.map { |value| connection.quote(value) }
+ end
+
+ connection.execute <<-EOF.strip_heredoc
+ INSERT INTO project_authorizations (user_id, project_id, access_level)
+ VALUES #{tuples.map { |tuple| "(#{tuple.join(', ')})" }.join(', ')}
+ EOF
+ end
+ end
end
diff --git a/app/models/project_services/chat_service.rb b/app/models/project_services/chat_service.rb
deleted file mode 100644
index 574788462de..00000000000
--- a/app/models/project_services/chat_service.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# Base class for Chat services
-# This class is not meant to be used directly, but only to inherit from.
-class ChatService < Service
- default_value_for :category, 'chat'
-
- has_many :chat_names, foreign_key: :service_id
-
- def valid_token?(token)
- self.respond_to?(:token) &&
- self.token.present? &&
- ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
- end
-
- def supported_events
- []
- end
-
- def trigger(params)
- raise NotImplementedError
- end
-end
diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb
new file mode 100644
index 00000000000..0bc160af604
--- /dev/null
+++ b/app/models/project_services/chat_slash_commands_service.rb
@@ -0,0 +1,56 @@
+# Base class for Chat services
+# This class is not meant to be used directly, but only to inherrit from.
+class ChatSlashCommandsService < Service
+ default_value_for :category, 'chat'
+
+ prop_accessor :token
+
+ has_many :chat_names, foreign_key: :service_id, dependent: :destroy
+
+ def valid_token?(token)
+ self.respond_to?(:token) &&
+ self.token.present? &&
+ ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
+ end
+
+ def supported_events
+ []
+ end
+
+ def can_test?
+ false
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'token', placeholder: '' }
+ ]
+ end
+
+ def trigger(params)
+ return unless valid_token?(params[:token])
+
+ user = find_chat_user(params)
+ unless user
+ url = authorize_chat_name_url(params)
+ return presenter.authorize_chat_name(url)
+ end
+
+ Gitlab::ChatCommands::Command.new(project, user,
+ params).execute
+ end
+
+ private
+
+ def find_chat_user(params)
+ ChatNames::FindUserService.new(self, params).execute
+ end
+
+ def authorize_chat_name_url(params)
+ ChatNames::AuthorizeUserService.new(self, params).execute
+ end
+
+ def presenter
+ Gitlab::ChatCommands::Presenter.new
+ end
+end
diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb
index da6be9dd7b7..ab353a1abe6 100644
--- a/app/models/project_services/deployment_service.rb
+++ b/app/models/project_services/deployment_service.rb
@@ -12,4 +12,22 @@ class DeploymentService < Service
def predefined_variables
[]
end
+
+ # Environments may have a number of terminals. Should return an array of
+ # hashes describing them, e.g.:
+ #
+ # [{
+ # :selectors => {"a" => "b", "foo" => "bar"},
+ # :url => "wss://external.example.com/exec",
+ # :headers => {"Authorization" => "Token xxx"},
+ # :subprotocols => ["foo"],
+ # :ca_pem => "----BEGIN CERTIFICATE...", # optional
+ # :created_at => Time.now.utc
+ # }]
+ #
+ # Selectors should be a set of values that uniquely identify a particular
+ # terminal
+ def terminals(environment)
+ raise NotImplementedError
+ end
end
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index f5fbf8b353b..085125ca9dc 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -1,4 +1,9 @@
class KubernetesService < DeploymentService
+ include Gitlab::Kubernetes
+ include ReactiveCaching
+
+ self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] }
+
# Namespace defaults to the project path, but can be overridden in case that
# is an invalid or inappropriate name
prop_accessor :namespace
@@ -25,6 +30,8 @@ class KubernetesService < DeploymentService
length: 1..63
end
+ after_save :clear_reactive_cache!
+
def initialize_properties
if properties.nil?
self.properties = {}
@@ -41,7 +48,8 @@ class KubernetesService < DeploymentService
end
def help
- ''
+ 'To enable terminal access to Kubernetes environments, label your ' \
+ 'deployments with `app=$CI_ENVIRONMENT_SLUG`'
end
def to_param
@@ -75,9 +83,9 @@ class KubernetesService < DeploymentService
# Check we can connect to the Kubernetes API
def test(*args)
- kubeclient = build_kubeclient
- kubeclient.discover
+ kubeclient = build_kubeclient!
+ kubeclient.discover
{ success: kubeclient.discovered, result: "Checked API discovery endpoint" }
rescue => err
{ success: false, result: err }
@@ -93,20 +101,48 @@ class KubernetesService < DeploymentService
variables
end
- private
+ # Constructs a list of terminals from the reactive cache
+ #
+ # Returns nil if the cache is empty, in which case you should try again a
+ # short time later
+ def terminals(environment)
+ with_reactive_cache do |data|
+ pods = data.fetch(:pods, nil)
+ filter_pods(pods, app: environment.slug).
+ flat_map { |pod| terminals_for_pod(api_url, namespace, pod) }.
+ map { |terminal| add_terminal_auth(terminal, token, ca_pem) }
+ end
+ end
- def build_kubeclient(api_path = '/api', api_version = 'v1')
- return nil unless api_url && namespace && token
+ # Caches all pods in the namespace so other calls don't need to block on
+ # network access.
+ def calculate_reactive_cache
+ return unless active? && project && !project.pending_delete?
- url = URI.parse(api_url)
- url.path = url.path[0..-2] if url.path[-1] == "/"
- url.path += api_path
+ kubeclient = build_kubeclient!
+
+ # Store as hashes, rather than as third-party types
+ pods = begin
+ kubeclient.get_pods(namespace: namespace).as_json
+ rescue KubeException => err
+ raise err unless err.error_code == 404
+ []
+ end
+
+ # We may want to cache extra things in the future
+ { pods: pods }
+ end
+
+ private
+
+ def build_kubeclient!(api_path: 'api', api_version: 'v1')
+ raise "Incomplete settings" unless api_url && namespace && token
::Kubeclient::Client.new(
- url,
+ join_api_url(api_path),
api_version,
- ssl_options: kubeclient_ssl_options,
auth_options: kubeclient_auth_options,
+ ssl_options: kubeclient_ssl_options,
http_proxy_uri: ENV['http_proxy']
)
end
@@ -125,4 +161,13 @@ class KubernetesService < DeploymentService
def kubeclient_auth_options
{ bearer_token: token }
end
+
+ def join_api_url(*parts)
+ url = URI.parse(api_url)
+ prefix = url.path.sub(%r{/+\z}, '')
+
+ url.path = [ prefix, *parts ].join("/")
+
+ url.to_s
+ end
end
diff --git a/app/models/project_services/mattermost_notification_service.rb b/app/models/project_services/mattermost_service.rb
index de18c4b1f00..0650f930402 100644
--- a/app/models/project_services/mattermost_notification_service.rb
+++ b/app/models/project_services/mattermost_service.rb
@@ -1,4 +1,4 @@
-class MattermostNotificationService < ChatNotificationService
+class MattermostService < ChatNotificationService
def title
'Mattermost notifications'
end
@@ -8,7 +8,7 @@ class MattermostNotificationService < ChatNotificationService
end
def to_param
- 'mattermost_notification'
+ 'mattermost'
end
def help
diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb
index 33431f41dc2..6c78c0af71c 100644
--- a/app/models/project_services/mattermost_slash_commands_service.rb
+++ b/app/models/project_services/mattermost_slash_commands_service.rb
@@ -1,4 +1,4 @@
-class MattermostSlashCommandsService < ChatService
+class MattermostSlashCommandsService < ChatSlashCommandsService
include TriggersHelper
prop_accessor :token
@@ -19,31 +19,33 @@ class MattermostSlashCommandsService < ChatService
'mattermost_slash_commands'
end
- def fields
- [
- { type: 'text', name: 'token', placeholder: '' }
- ]
- end
-
- def trigger(params)
- return nil unless valid_token?(params[:token])
+ def configure(user, params)
+ token = Mattermost::Command.new(user).
+ create(command(params))
- user = find_chat_user(params)
- unless user
- url = authorize_chat_name_url(params)
- return Mattermost::Presenter.authorize_chat_name(url)
- end
+ update(active: true, token: token) if token
+ rescue Mattermost::Error => e
+ [false, e.message]
+ end
- Gitlab::ChatCommands::Command.new(project, user, params).execute
+ def list_teams(user)
+ Mattermost::Team.new(user).all
+ rescue Mattermost::Error => e
+ [[], e.message]
end
private
- def find_chat_user(params)
- ChatNames::FindUserService.new(self, params).execute
- end
-
- def authorize_chat_name_url(params)
- ChatNames::AuthorizeUserService.new(self, params).execute
+ def command(params)
+ pretty_project_name = project.name_with_namespace
+
+ params.merge(
+ auto_complete: true,
+ auto_complete_desc: "Perform common operations on: #{pretty_project_name}",
+ auto_complete_hint: '[help]',
+ description: "Perform common operations on: #{pretty_project_name}",
+ display_name: "GitLab / #{pretty_project_name}",
+ method: 'P',
+ user_name: 'GitLab')
end
end
diff --git a/app/models/project_services/slack_notification_service.rb b/app/models/project_services/slack_service.rb
index 3cbf89efba4..0583470d3b5 100644
--- a/app/models/project_services/slack_notification_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -1,4 +1,4 @@
-class SlackNotificationService < ChatNotificationService
+class SlackService < ChatNotificationService
def title
'Slack notifications'
end
@@ -8,7 +8,7 @@ class SlackNotificationService < ChatNotificationService
end
def to_param
- 'slack_notification'
+ 'slack'
end
def help
diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb
new file mode 100644
index 00000000000..cb19ebf4cad
--- /dev/null
+++ b/app/models/project_services/slack_slash_commands_service.rb
@@ -0,0 +1,28 @@
+class SlackSlashCommandsService < ChatSlashCommandsService
+ include TriggersHelper
+
+ def title
+ 'Slack Command'
+ end
+
+ def description
+ "Perform common operations on GitLab in Slack"
+ end
+
+ def to_param
+ 'slack_slash_commands'
+ end
+
+ def trigger(params)
+ # Format messages to be Slack-compatible
+ super.tap do |result|
+ result[:text] = format(result[:text])
+ end
+ end
+
+ private
+
+ def format(text)
+ Slack::Notifier::LinkFormatter.format(text) if text
+ end
+end
diff --git a/app/models/route.rb b/app/models/route.rb
index d40214b9da6..caf596efa79 100644
--- a/app/models/route.rb
+++ b/app/models/route.rb
@@ -13,7 +13,7 @@ class Route < ActiveRecord::Base
def rename_children
# We update each row separately because MySQL does not have regexp_replace.
# rubocop:disable Rails/FindEach
- Route.where('path LIKE ?', "#{path_was}%").each do |route|
+ Route.where('path LIKE ?', "#{path_was}/%").each do |route|
# Note that update column skips validation and callbacks.
# We need this to avoid recursive call of rename_children method
route.update_column(:path, route.path.sub(path_was, path))
diff --git a/app/models/service.rb b/app/models/service.rb
index 0bbab078cf6..19ef3ba9c23 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -216,12 +216,13 @@ class Service < ActiveRecord::Base
jira
kubernetes
mattermost_slash_commands
+ mattermost
pipelines_email
pivotaltracker
pushover
redmine
- mattermost_notification
- slack_notification
+ slack_slash_commands
+ slack
teamcity
]
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 3a17c98eff6..899a89a4eaa 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -311,10 +311,6 @@ class User < ActiveRecord::Base
find_by(id: Key.unscoped.select(:user_id).where(id: key_id))
end
- def build_user(attrs = {})
- User.new(attrs)
- end
-
def reference_prefix
'@'
end
@@ -443,22 +439,16 @@ class User < ActiveRecord::Base
end
def refresh_authorized_projects
- transaction do
- project_authorizations.delete_all
-
- # project_authorizations_union can return multiple records for the same
- # project/user with different access_level so we take row with the maximum
- # access_level
- project_authorizations.connection.execute <<-SQL
- INSERT INTO project_authorizations (user_id, project_id, access_level)
- SELECT user_id, project_id, MAX(access_level) AS access_level
- FROM (#{project_authorizations_union.to_sql}) sub
- GROUP BY user_id, project_id
- SQL
-
- unless authorized_projects_populated
- update_column(:authorized_projects_populated, true)
- end
+ Users::RefreshAuthorizedProjectsService.new(self).execute
+ end
+
+ def remove_project_authorizations(project_ids)
+ project_authorizations.where(id: project_ids).delete_all
+ end
+
+ def set_authorized_projects_column
+ unless authorized_projects_populated
+ update_column(:authorized_projects_populated, true)
end
end
@@ -905,18 +895,6 @@ class User < ActiveRecord::Base
private
- # Returns a union query of projects that the user is authorized to access
- def project_authorizations_union
- relations = [
- personal_projects.select("#{id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"),
- groups_projects.select_for_project_authorization,
- projects.select_for_project_authorization,
- groups.joins(:shared_projects).select_for_project_authorization
- ]
-
- Gitlab::SQL::Union.new(relations)
- end
-
def ci_projects_union
scope = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] }
groups = groups_projects.where(members: scope)
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
index 7e0fc9c071e..5d15eb8d3d3 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -23,5 +23,13 @@ class EnvironmentEntity < Grape::Entity
environment)
end
+ expose :terminal_path, if: ->(environment, _) { environment.has_terminals? } do |environment|
+ can?(request.user, :admin_environment, environment.project) &&
+ terminal_namespace_project_environment_path(
+ environment.project.namespace,
+ environment.project,
+ environment)
+ end
+
expose :created_at, :updated_at
end
diff --git a/app/serializers/request_aware_entity.rb b/app/serializers/request_aware_entity.rb
index ff8c1142abc..e159d750cb7 100644
--- a/app/serializers/request_aware_entity.rb
+++ b/app/serializers/request_aware_entity.rb
@@ -8,4 +8,8 @@ module RequestAwareEntity
def request
@options.fetch(:request)
end
+
+ def can?(object, action, subject)
+ Ability.allowed?(object, action, subject)
+ end
end
diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb
index fff2273f402..4e878ec556a 100644
--- a/app/services/groups/update_service.rb
+++ b/app/services/groups/update_service.rb
@@ -14,7 +14,13 @@ module Groups
group.assign_attributes(params)
- group.save
+ begin
+ group.save
+ rescue Gitlab::UpdatePathError => e
+ group.errors.add(:base, e.message)
+
+ false
+ end
end
end
end
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index d7221fe993c..cd230528743 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -4,15 +4,6 @@ module Projects
class Error < StandardError; end
- ALLOWED_TYPES = [
- 'bitbucket',
- 'fogbugz',
- 'gitlab',
- 'github',
- 'google_code',
- 'gitlab_project'
- ]
-
def execute
add_repository_to_project unless project.gitlab_project_import?
@@ -64,14 +55,11 @@ module Projects
end
def has_importer?
- ALLOWED_TYPES.include?(project.import_type)
+ Gitlab::ImportSources.importer_names.include?(project.import_type)
end
def importer
- return Gitlab::ImportExport::Importer.new(project) if @project.gitlab_project_import?
-
- class_name = "Gitlab::#{project.import_type.camelize}Import::Importer"
- class_name.constantize.new(project)
+ Gitlab::ImportSources.importer(project.import_type).new(project)
end
def unknown_url?
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 8b48d90f60b..7613ecd5021 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -146,7 +146,7 @@ module SystemNoteService
end
def remove_merge_request_wip(noteable, project, author)
- body = 'unmarked as a Work In Progress'
+ body = 'unmarked as a **Work In Progress**'
create_note(noteable: noteable, project: project, author: author, note: body)
end
diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb
new file mode 100644
index 00000000000..7d38ac3a374
--- /dev/null
+++ b/app/services/users/refresh_authorized_projects_service.rb
@@ -0,0 +1,128 @@
+module Users
+ # Service for refreshing the authorized projects of a user.
+ #
+ # This particular service class can not be used to update data for the same
+ # user concurrently. Doing so could lead to an incorrect state. To ensure this
+ # doesn't happen a caller must synchronize access (e.g. using
+ # `Gitlab::ExclusiveLease`).
+ #
+ # Usage:
+ #
+ # user = User.find_by(username: 'alice')
+ # service = Users::RefreshAuthorizedProjectsService.new(some_user)
+ # service.execute
+ class RefreshAuthorizedProjectsService
+ attr_reader :user
+
+ LEASE_TIMEOUT = 1.minute.to_i
+
+ # user - The User for which to refresh the authorized projects.
+ def initialize(user)
+ @user = user
+
+ # We need an up to date User object that has access to all relations that
+ # may have been created earlier. The only way to ensure this is to reload
+ # the User object.
+ user.reload
+ end
+
+ # This method returns the updated User object.
+ def execute
+ current = current_authorizations_per_project
+ fresh = fresh_access_levels_per_project
+
+ remove = current.each_with_object([]) do |(project_id, row), array|
+ # rows not in the new list or with a different access level should be
+ # removed.
+ if !fresh[project_id] || fresh[project_id] != row.access_level
+ array << row.id
+ end
+ end
+
+ add = fresh.each_with_object([]) do |(project_id, level), array|
+ # rows not in the old list or with a different access level should be
+ # added.
+ if !current[project_id] || current[project_id].access_level != level
+ array << [user.id, project_id, level]
+ end
+ end
+
+ update_with_lease(remove, add)
+ end
+
+ # Updates the list of authorizations using an exclusive lease.
+ def update_with_lease(remove = [], add = [])
+ lease_key = "refresh_authorized_projects:#{user.id}"
+ lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)
+
+ until uuid = lease.try_obtain
+ # Keep trying until we obtain the lease. If we don't do so we may end up
+ # not updating the list of authorized projects properly. To prevent
+ # hammering Redis too much we'll wait for a bit between retries.
+ sleep(1)
+ end
+
+ begin
+ update_authorizations(remove, add)
+ ensure
+ Gitlab::ExclusiveLease.cancel(lease_key, uuid)
+ end
+ end
+
+ # Updates the list of authorizations for the current user.
+ #
+ # remove - The IDs of the authorization rows to remove.
+ # add - Rows to insert in the form `[user id, project id, access level]`
+ def update_authorizations(remove = [], add = [])
+ return if remove.empty? && add.empty?
+
+ User.transaction do
+ user.remove_project_authorizations(remove) unless remove.empty?
+ ProjectAuthorization.insert_authorizations(add) unless add.empty?
+ user.set_authorized_projects_column
+ end
+
+ # Since we batch insert authorization rows, Rails' associations may get
+ # out of sync. As such we force a reload of the User object.
+ user.reload
+ end
+
+ def fresh_access_levels_per_project
+ fresh_authorizations.each_with_object({}) do |row, hash|
+ hash[row.project_id] = row.access_level
+ end
+ end
+
+ def current_authorizations_per_project
+ current_authorizations.each_with_object({}) do |row, hash|
+ hash[row.project_id] = row
+ end
+ end
+
+ def current_authorizations
+ user.project_authorizations.select(:id, :project_id, :access_level)
+ end
+
+ def fresh_authorizations
+ ProjectAuthorization.
+ unscoped.
+ select('project_id, MAX(access_level) AS access_level').
+ from("(#{project_authorizations_union.to_sql}) #{ProjectAuthorization.table_name}").
+ group(:project_id)
+ end
+
+ private
+
+ # Returns a union query of projects that the user is authorized to access
+ def project_authorizations_union
+ relations = [
+ user.personal_projects.select("#{user.id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"),
+ user.groups_projects.select_for_project_authorization,
+ user.projects.select_for_project_authorization,
+ user.groups.joins(:shared_projects).select_for_project_authorization
+ ]
+
+ Gitlab::SQL::Union.new(relations)
+ end
+ end
+end
diff --git a/app/validators/project_path_validator.rb b/app/validators/project_path_validator.rb
index 927c67b65b0..aec0d0ce44e 100644
--- a/app/validators/project_path_validator.rb
+++ b/app/validators/project_path_validator.rb
@@ -14,7 +14,8 @@ class ProjectPathValidator < ActiveModel::EachValidator
# without tree as reserved name routing can match 'group/project' as group name,
# 'tree' as project name and 'deploy_keys' as route.
#
- RESERVED = (NamespaceValidator::RESERVED +
+ RESERVED = (NamespaceValidator::RESERVED -
+ %w[dashboard help ci admin search notes services] +
%w[tree commits wikis new edit create update logs_tree
preview blob blame raw files create_dir find_file]).freeze
diff --git a/app/views/ci/status/_graph_badge.html.haml b/app/views/ci/status/_graph_badge.html.haml
index 52b4d77d074..dd2f649de9a 100644
--- a/app/views/ci/status/_graph_badge.html.haml
+++ b/app/views/ci/status/_graph_badge.html.haml
@@ -3,18 +3,18 @@
- subject = local_assigns.fetch(:subject)
- status = subject.detailed_status(current_user)
- klass = "ci-status-icon ci-status-icon-#{status.group}"
+- tooltip = "#{subject.name} - #{status.label}"
- if status.has_details?
- = link_to status.details_path, class: 'build-content' do
+ = link_to status.details_path, class: 'build-content has-tooltip', data: { toggle: 'tooltip', title: tooltip } do
%span{ class: klass }= custom_icon(status.icon)
- .ci-status-text{ 'data-toggle' => 'tooltip', 'data-title' => "#{subject.name} - #{status.label}" }= subject.name
+ .ci-status-text= subject.name
- else
- .build-content
+ .build-content.has-tooltip{ data: { toggle: 'tooltip', title: tooltip } }
%span{ class: klass }= custom_icon(status.icon)
- .ci-status-text{ 'data-toggle' => 'tooltip', 'data-title' => "#{subject.name} - #{status.label}" }= subject.name
+ .ci-status-text= subject.name
- if status.has_action?
- = link_to status.action_path, method: status.action_method,
- title: status.action_title, class: 'ci-action-icon-container' do
+ = link_to status.action_path, class: 'ci-action-icon-container has-tooltip', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title } do
%i.ci-action-icon-wrapper
= icon(status.action_icon, class: status.action_class)
diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml
new file mode 100644
index 00000000000..f12f9482a51
--- /dev/null
+++ b/app/views/import/_githubish_status.html.haml
@@ -0,0 +1,61 @@
+- provider = local_assigns.fetch(:provider)
+- provider_title = Gitlab::ImportSources.title(provider)
+
+%p.light
+ Select projects you want to import.
+%hr
+%p
+ = button_tag class: "btn btn-import btn-success js-import-all" do
+ Import all projects
+ = icon("spinner spin", class: "loading-icon")
+
+.table-responsive
+ %table.table.import-jobs
+ %colgroup.import-jobs-from-col
+ %colgroup.import-jobs-to-col
+ %colgroup.import-jobs-status-col
+ %thead
+ %tr
+ %th= "From #{provider_title}"
+ %th To GitLab
+ %th Status
+ %tbody
+ - @already_added_projects.each do |project|
+ %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
+ %td
+ = provider_project_link(provider, project.import_source)
+ %td
+ = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
+ %td.job-status
+ - if project.import_status == 'finished'
+ %span
+ %i.fa.fa-check
+ done
+ - elsif project.import_status == 'started'
+ %i.fa.fa-spinner.fa-spin
+ started
+ - else
+ = project.human_import_status_name
+
+ - @repos.each do |repo|
+ %tr{id: "repo_#{repo.id}"}
+ %td
+ = provider_project_link(provider, repo.full_name)
+ %td.import-target
+ %fieldset.row
+ .input-group
+ .project-path.input-group-btn
+ - if current_user.can_select_namespace?
+ - selected = params[:namespace_id] || :current_user
+ - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {}
+ = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 }
+ - else
+ = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true
+ %span.input-group-addon /
+ = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true
+ %td.import-actions.job-status
+ = button_tag class: "btn btn-import js-add-to-import" do
+ Import
+ = icon("spinner spin", class: "loading-icon")
+
+.js-importer-status{ data: { jobs_import_path: "#{url_for([:jobs, :import, provider])}", import_path: "#{url_for([:import, provider])}" } }
diff --git a/app/views/import/gitea/new.html.haml b/app/views/import/gitea/new.html.haml
new file mode 100644
index 00000000000..02a116f996b
--- /dev/null
+++ b/app/views/import/gitea/new.html.haml
@@ -0,0 +1,23 @@
+- page_title "Gitea Import"
+- header_title "Projects", root_path
+
+%h3.page-title
+ = custom_icon('go_logo')
+ Import Projects from Gitea
+
+%p
+ To get started, please enter your Gitea Host URL and a
+ = succeed '.' do
+ = link_to 'Personal Access Token', 'https://github.com/gogits/go-gogs-client/wiki#access-token'
+
+= form_tag personal_access_token_import_gitea_path, class: 'form-horizontal' do
+ .form-group
+ = label_tag :gitea_host_url, 'Gitea Host URL', class: 'control-label'
+ .col-sm-4
+ = text_field_tag :gitea_host_url, nil, placeholder: 'https://try.gitea.io', class: 'form-control'
+ .form-group
+ = label_tag :personal_access_token, 'Personal Access Token', class: 'control-label'
+ .col-sm-4
+ = text_field_tag :personal_access_token, nil, class: 'form-control'
+ .form-actions
+ = submit_tag 'List Your Gitea Repositories', class: 'btn btn-create'
diff --git a/app/views/import/gitea/status.html.haml b/app/views/import/gitea/status.html.haml
new file mode 100644
index 00000000000..589ca27e45d
--- /dev/null
+++ b/app/views/import/gitea/status.html.haml
@@ -0,0 +1,7 @@
+- page_title "Gitea Import"
+- header_title "Projects", root_path
+%h3.page-title
+ = custom_icon('go_logo')
+ Import Projects from Gitea
+
+= render 'import/githubish_status', provider: 'gitea'
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index 4c721d40b55..0fe578a0036 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -1,64 +1,6 @@
-- page_title "GitHub import"
+- page_title "GitHub Import"
- header_title "Projects", root_path
%h3.page-title
- %i.fa.fa-github
- Import projects from GitHub
+ = icon 'github', text: 'Import Projects from GitHub'
-%p.light
- Select projects you want to import.
-%hr
-%p
- = button_tag class: "btn btn-import btn-success js-import-all" do
- Import all projects
- = icon("spinner spin", class: "loading-icon")
-
-.table-responsive
- %table.table.import-jobs
- %colgroup.import-jobs-from-col
- %colgroup.import-jobs-to-col
- %colgroup.import-jobs-status-col
- %thead
- %tr
- %th From GitHub
- %th To GitLab
- %th Status
- %tbody
- - @already_added_projects.each do |project|
- %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
- %td
- = github_project_link(project.import_source)
- %td
- = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
- %td.job-status
- - if project.import_status == 'finished'
- %span
- %i.fa.fa-check
- done
- - elsif project.import_status == 'started'
- %i.fa.fa-spinner.fa-spin
- started
- - else
- = project.human_import_status_name
-
- - @repos.each do |repo|
- %tr{id: "repo_#{repo.id}"}
- %td
- = github_project_link(repo.full_name)
- %td.import-target
- %fieldset.row
- .input-group
- .project-path.input-group-btn
- - if current_user.can_select_namespace?
- - selected = params[:namespace_id] || :current_user
- - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {}
- = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 }
- - else
- = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true
- %span.input-group-addon /
- = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true
- %td.import-actions.job-status
- = button_tag class: "btn btn-import js-add-to-import" do
- Import
- = icon("spinner spin", class: "loading-icon")
-
-.js-importer-status{ data: { jobs_import_path: "#{jobs_import_github_path}", import_path: "#{import_github_path}" } }
+= render 'import/githubish_status', provider: 'github'
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index ac04f57e217..b69114c96cc 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -31,7 +31,7 @@
= link_to admin_abuse_reports_path, title: "Abuse Reports" do
%span
Abuse Reports
- %span.badge.count= number_with_delimiter(AbuseReport.count(:all))
+ %span.badge.badge-dark.count= number_with_delimiter(AbuseReport.count(:all))
- if askimet_enabled?
= nav_link(controller: :spam_logs) do
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index f3539fd372d..221f3ec1ffe 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -26,13 +26,13 @@
%span
Issues
- issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
- %span.badge.count= number_with_delimiter(issues.count)
+ %span.badge.badge-dark.count= number_with_delimiter(issues.count)
= nav_link(path: 'groups#merge_requests') do
= link_to merge_requests_group_path(@group), title: 'Merge Requests' do
%span
Merge Requests
- merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
- %span.badge.count= number_with_delimiter(merge_requests.count)
+ %span.badge.badge-dark.count= number_with_delimiter(merge_requests.count)
= nav_link(controller: [:group_members]) do
= link_to group_group_members_path(@group), title: 'Members' do
%span
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 904d11c2cf4..cc1571cbb4f 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -61,14 +61,14 @@
%span
Issues
- if @project.default_issues_tracker?
- %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count)
+ %span.badge.badge-dark.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count)
- if project_nav_tab? :merge_requests
= nav_link(controller: :merge_requests) do
= link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
%span
Merge Requests
- %span.badge.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count)
+ %span.badge.badge-dark.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count)
- if project_nav_tab? :wiki
= nav_link(controller: :wikis) do
diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml
index 7f530708947..e1fea8ccf3d 100644
--- a/app/views/projects/_last_commit.html.haml
+++ b/app/views/projects/_last_commit.html.haml
@@ -1,7 +1,8 @@
+
- ref = local_assigns.fetch(:ref)
- status = commit.status(ref)
- if status
- = link_to builds_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{status}" do
+ = link_to pipelines_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{status}" do
= ci_icon_for_status(status)
= ci_label_for_status(status)
diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml
index 057a720a54a..b15be0d861d 100644
--- a/app/views/projects/builds/_header.html.haml
+++ b/app/views/projects/builds/_header.html.haml
@@ -7,7 +7,7 @@
= link_to pipeline_path(@build.pipeline) do
%strong ##{@build.pipeline.id}
for commit
- = link_to ci_status_path(@build.pipeline) do
+ = link_to namespace_project_commit_path(@project.namespace, @project, @build.pipeline.sha) do
%strong= @build.pipeline.short_sha
from
= link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index cdeb81372ee..c69c53b656f 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -56,17 +56,22 @@
- else
#js-build-scroll.scroll-controls
.scroll-step
- = link_to '#build-trace', class: 'btn' do
- %i.fa.fa-angle-up
- = link_to '#down-build-trace', class: 'btn' do
- %i.fa.fa-angle-down
+ %a{ href: '#up-build-trace', id: 'scroll-top', class: 'scroll-link scroll-top', title: 'Scroll to top' }
+ = custom_icon('scroll_up')
+ = custom_icon('scroll_up_hover_active')
+ %a{ href: '#down-build-trace', id: 'scroll-bottom', class: 'scroll-link scroll-bottom', title: 'Scroll to bottom' }
+ = custom_icon('scroll_down')
+ = custom_icon('scroll_down_hover_active')
- if @build.active?
.autoscroll-container
- %button.btn.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}}
- Enable autoscroll
+ %span.status-message#autoscroll-status{ data: { state: 'disabled' } }
+ %span.status-text Autoscroll active
+ %i.status-icon
+ = custom_icon('scroll_down_hover_active')
+ #up-build-trace
%pre.build-trace#build-trace
%code.bash.js-build-output
- = icon("refresh spin", class: "js-build-refresh")
+ .build-loader-animation.js-build-refresh
#down-build-trace
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index 3f05a21990f..2f8f153f9a9 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -43,10 +43,25 @@
%td.stage-cell
- pipeline.stages.each do |stage|
- if stage.status
- - tooltip = "#{stage.name.titleize}: #{stage.status || 'not found'}"
- .stage-container
- = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage.name), class: "has-tooltip ci-status-icon-#{stage.status}", title: tooltip do
- = ci_icon_for_status(stage.status)
+ - detailed_status = stage.detailed_status(current_user)
+ - icon_status = "#{detailed_status.icon}_borderless"
+ - status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}"
+
+ .stage-container.mini-pipeline-graph
+ .dropdown.inline.build-content
+ %button.has-tooltip.builds-dropdown.js-builds-dropdown-button{ type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name)}}
+ %span.has-tooltip{ class: status_klass }
+ %span.mini-pipeline-graph-icon-container
+ %span{ class: status_klass }= custom_icon(icon_status)
+ = icon('caret-down', class: 'dropdown-caret')
+
+ .js-builds-dropdown-container
+ .dropdown-menu.grouped-pipeline-dropdown
+ .arrow-up
+ .js-builds-dropdown-list
+
+ .js-builds-dropdown-loading.builds-dropdown-loading.hidden
+ %span.fa.fa-spinner.fa-spin
%td
- if pipeline.duration
@@ -66,7 +81,7 @@
.btn-group.inline
- if actions.any?
.btn-group
- %a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
+ %a.dropdown-toggle.btn.btn-default.js-pipeline-dropdown-manual-actions{type: 'button', 'data-toggle' => 'dropdown'}
= custom_icon('icon_play')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
@@ -77,7 +92,7 @@
%span= build.name.humanize
- if artifacts.present?
.btn-group
- %a.dropdown-toggle.btn.btn-default.build-artifacts{type: 'button', 'data-toggle' => 'dropdown'}
+ %a.dropdown-toggle.btn.btn-default.build-artifacts.js-pipeline-dropdown-download{type: 'button', 'data-toggle' => 'dropdown'}
= icon("download")
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml
deleted file mode 100644
index b7087749428..00000000000
--- a/app/views/projects/commit/_builds.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-- @ci_pipelines.each do |pipeline|
- = render "pipeline", pipeline: pipeline, pipeline_details: true
diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml
index cbfd99ca448..13ab2253733 100644
--- a/app/views/projects/commit/_ci_menu.html.haml
+++ b/app/views/projects/commit/_ci_menu.html.haml
@@ -8,7 +8,3 @@
= link_to pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) do
Pipelines
%span.badge= @ci_pipelines.count
- = nav_link(path: 'commit#builds') do
- = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id) do
- Builds
- %span.badge= @statuses.count
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index 7f42fde0fea..5a9f7295135 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -4,12 +4,12 @@
.nothing-here-block No pipelines to show
- else
.table-holder
- %table.table.ci-table
- %tbody
- %th Status
- %th Pipeline
- %th Commit
- %th Stages
- %th
- %th
+ %table.table.ci-table.js-pipeline-table
+ %thead
+ %th.pipeline-status Status
+ %th.pipeline-info Pipeline
+ %th.pipeline-commit Commit
+ %th.pipeline-stages Stages
+ %th.pipeline-date
+ %th.pipeline-actions
= render pipelines, commit_sha: true, stage: true, allow_retry: true, show_commit: false
diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml
deleted file mode 100644
index 077b2d2725b..00000000000
--- a/app/views/projects/commit/builds.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-- @no_container = true
-- page_title "Builds", "#{@commit.title} (#{@commit.short_id})", "Commits"
-= render "projects/commits/head"
-
-%div{ class: container_class }
- = render "commit_box"
-
- = render "ci_menu"
- = render "builds"
diff --git a/app/views/projects/environments/_terminal_button.html.haml b/app/views/projects/environments/_terminal_button.html.haml
new file mode 100644
index 00000000000..97de9c95de7
--- /dev/null
+++ b/app/views/projects/environments/_terminal_button.html.haml
@@ -0,0 +1,3 @@
+- if environment.has_terminals? && can?(current_user, :admin_environment, @project)
+ = link_to terminal_namespace_project_environment_path(@project.namespace, @project, environment), class: 'btn terminal-button' do
+ = icon('terminal')
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index a65a630f2d0..0c6f696f5b9 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -4,10 +4,6 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_tag("environments/environments_bundle.js")
-.commit-icon-svg.hidden
- = custom_icon("icon_commit")
-.play-icon-svg.hidden
- = custom_icon("icon_play")
#environments-list-view{ data: { environments_data: environments_list_data,
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
@@ -19,4 +15,5 @@
"help-page-path" => help_page_path("ci/environments"),
"css-class" => container_class,
"commit-icon-svg" => custom_icon("icon_commit"),
+ "terminal-icon-svg" => custom_icon("icon_terminal"),
"play-icon-svg" => custom_icon("icon_play")}}
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index bcac73d3698..6e0d9456900 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -8,6 +8,7 @@
%h3.page-title= @environment.name.capitalize
.col-md-3
.nav-controls
+ = render 'projects/environments/terminal_button', environment: @environment
= render 'projects/environments/external_url', environment: @environment
- if can?(current_user, :update_environment, @environment)
= link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn'
diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml
new file mode 100644
index 00000000000..a6726e509e0
--- /dev/null
+++ b/app/views/projects/environments/terminal.html.haml
@@ -0,0 +1,22 @@
+- @no_container = true
+- page_title "Terminal for environment", @environment.name
+= render "projects/pipelines/head"
+
+- content_for :page_specific_javascripts do
+ = stylesheet_link_tag "xterm/xterm"
+ = page_specific_javascript_tag("terminal/terminal_bundle.js")
+
+%div{class: container_class}
+ .top-area
+ .row
+ .col-sm-6
+ %h3.page-title
+ Terminal for environment
+ = @environment.name
+
+ .col-sm-6
+ .nav-controls
+ = render 'projects/deployments/actions', deployment: @environment.last_deployment
+
+.terminal-container{class: container_class}
+ #terminal{data:{project_path: "#{terminal_namespace_project_environment_path(@project.namespace, @project, @environment)}.ws"}}
diff --git a/app/views/projects/mattermosts/_no_teams.html.haml b/app/views/projects/mattermosts/_no_teams.html.haml
new file mode 100644
index 00000000000..605c7f61dee
--- /dev/null
+++ b/app/views/projects/mattermosts/_no_teams.html.haml
@@ -0,0 +1,12 @@
+%p
+ You aren’t a member of any team on the Mattermost instance at
+ %strong= Gitlab.config.mattermost.host
+%p
+ To install this service,
+ = link_to "#{Gitlab.config.mattermost.host}/select_team", target: '__blank' do
+ join a team
+ = icon('external-link')
+ and try again.
+%hr
+.clearfix
+ = link_to 'Go back', edit_namespace_project_service_path(@project.namespace, @project, @service), class: 'btn btn-lg pull-right'
diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml
new file mode 100644
index 00000000000..7980f7c9a72
--- /dev/null
+++ b/app/views/projects/mattermosts/_team_selection.html.haml
@@ -0,0 +1,44 @@
+%p
+ This service will be installed on the Mattermost instance at
+ %strong= link_to Gitlab.config.mattermost.host, Gitlab.config.mattermost.host
+%hr
+= form_for(:mattermost, method: :post, url: namespace_project_mattermost_path(@project.namespace, @project)) do |f|
+ %h4 Team
+ %p
+ = @teams.one? ? 'The team' : 'Select the team'
+ where the slash commands will be used in
+ - selected_id = @teams.keys.first if @teams.one?
+ = f.select(:team_id, mattermost_teams_options(@teams), {}, { class: 'form-control', selected: "#{selected_id}", disabled: @teams.one? })
+ .help-block
+ - if @teams.one?
+ This is the only team where you are an administrator.
+ - else
+ The list shows teams where you are administrator
+ To create a team, ask your Mattermost system administrator.
+ To create a team,
+ = link_to "#{Gitlab.config.mattermost.host}/create_team" do
+ use Mattermost's interface
+ = icon('external-link')
+ %hr
+ %h4 Command trigger word
+ %p Choose the word that will trigger commands
+ = f.text_field(:trigger, value: @project.path, class: 'form-control')
+ .help-block
+ %p
+ Trigger word must be unique, and can't begin with a slash or contain any spaces.
+ Use the word that works best for your team.
+ %p
+ Suggestions:
+ %code= 'gitlab'
+ %code= @project.path # Path contains no spaces, but dashes
+ %code= @project.path_with_namespace
+ %p
+ Reserved:
+ = link_to 'https://docs.mattermost.com/help/messaging/executing-commands.html#built-in-commands', target: '__blank' do
+ see list of built-in slash commands
+ = icon('external-link')
+ %hr
+ .clearfix
+ .pull-right
+ = link_to 'Cancel', edit_namespace_project_service_path(@project.namespace, @project, @service), class: 'btn btn-lg'
+ = f.submit 'Install', class: 'btn btn-save btn-lg'
diff --git a/app/views/projects/mattermosts/new.html.haml b/app/views/projects/mattermosts/new.html.haml
new file mode 100644
index 00000000000..96b1d2aee61
--- /dev/null
+++ b/app/views/projects/mattermosts/new.html.haml
@@ -0,0 +1,8 @@
+.service-installation
+ .inline.pull-right
+ = custom_icon('mattermost_logo', size: 48)
+ %h3 Install Mattermost Command
+ - if @teams.empty?
+ = render 'no_teams'
+ - else
+ = render 'team_selection'
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index 4a08ed045f4..349181be784 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -34,11 +34,6 @@
= link_to url_for(params), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do
Pipelines
%span.badge= @pipelines.size
- - if @pipeline.present?
- %li.builds-tab
- = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do
- Builds
- %span.badge= @statuses_count
%li.diffs-tab
= link_to url_for(params.merge(action: 'new_diffs')), data: {target: 'div#diffs', action: 'new/diffs', toggle: 'tab'} do
Changes
@@ -49,9 +44,6 @@
= render "projects/merge_requests/show/commits"
#diffs.diffs.tab-pane
- # This tab is always loaded via AJAX
- - if @pipeline.present?
- #builds.builds.tab-pane
- = render "projects/merge_requests/show/builds"
- if @pipelines.any?
#pipelines.pipelines.tab-pane
= render "projects/merge_requests/show/pipelines"
@@ -66,6 +58,5 @@
});
:javascript
var merge_request = new MergeRequest({
- action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}",
- buildsLoaded: "#{@pipeline.present? ? 'true' : 'false'}"
+ action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}"
});
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 7725558518f..d1fa51ae7ee 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -65,11 +65,6 @@
= link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do
Pipelines
%span.badge= @pipelines.size
- - if @pipeline.present?
- %li.builds-tab
- = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#builds', action: 'builds', toggle: 'tab' } do
- Builds
- %span.badge= @statuses_count
%li.diffs-tab
= link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do
Changes
@@ -98,8 +93,6 @@
#commits.commits.tab-pane
- # This tab is always loaded via AJAX
- #builds.builds.tab-pane
- - # This tab is always loaded via AJAX
#pipelines.pipelines.tab-pane
- # This tab is always loaded via AJAX
#diffs.diffs.tab-pane
diff --git a/app/views/projects/merge_requests/show/_builds.html.haml b/app/views/projects/merge_requests/show/_builds.html.haml
deleted file mode 100644
index 808ef7fed27..00000000000
--- a/app/views/projects/merge_requests/show/_builds.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-= render "projects/commit/pipeline", pipeline: @pipeline, link_to_commit: true
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index 9ab7971b56c..5bc417d1760 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -17,7 +17,7 @@
- # TODO, remove in later versions when services like Jenkins will set CI status via Commit status API
.mr-widget-heading
- %w[success skipped canceled failed running pending].each do |status|
- .ci_widget{class: "ci-#{status}", style: "display:none"}
+ .ci_widget{class: "ci-#{status} ci-status-icon-#{status}", style: "display:none"}
= ci_icon_for_status(status)
%span
CI build
diff --git a/app/views/projects/merge_requests/widget/_merged_buttons.haml b/app/views/projects/merge_requests/widget/_merged_buttons.haml
index d836a253507..9eef011b591 100644
--- a/app/views/projects/merge_requests/widget/_merged_buttons.haml
+++ b/app/views/projects/merge_requests/widget/_merged_buttons.haml
@@ -5,10 +5,10 @@
- if can_remove_source_branch || mr_can_be_reverted || mr_can_be_cherry_picked
.clearfix.merged-buttons
- if can_remove_source_branch
- = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default btn-sm remove_source_branch" do
+ = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default remove_source_branch" do
= icon('trash-o')
Remove Source Branch
- if mr_can_be_reverted
- = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm')
+ = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "warning")
- if mr_can_be_cherry_picked
- = cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm')
+ = cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "default")
diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml
index a8918c85dde..38328501ffd 100644
--- a/app/views/projects/merge_requests/widget/_show.html.haml
+++ b/app/views/projects/merge_requests/widget/_show.html.haml
@@ -24,12 +24,10 @@
preparing: "{{status}} build",
normal: "Build {{status}}"
},
- builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
pipelines_path: "#{pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}"
};
if (typeof merge_request_widget !== 'undefined') {
- clearInterval(merge_request_widget.fetchBuildStatusInterval);
merge_request_widget.cancelPolling();
merge_request_widget.clearEventListeners();
}
diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml
index d6f7f23533c..7809e9c8c72 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -1,3 +1,6 @@
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('merge_request_widget/ci_bundle.js')
+
- status_class = @pipeline ? " ci-#{@pipeline.status}" : nil
= form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-quick-submit js-requires-input' } do |f|
@@ -47,21 +50,3 @@
rows: 14, hint: true
= hidden_field_tag :merge_when_build_succeeds, "", autocomplete: "off"
-
- :javascript
- $('.accept-mr-form').on('ajax:send', function() {
- $(".accept-mr-form :input").disable();
- });
-
- $('.accept_merge_request').on('click', function() {
- $('.js-merge-button').html("<i class='fa fa-spinner fa-spin'></i> Merge in progress");
- });
-
- $('.merge_when_build_succeeds').on('click', function() {
- $("#merge_when_build_succeeds").val("1");
- });
-
- $('.js-merge-dropdown a').on('click', function(e) {
- e.preventDefault();
- $(this).closest("form").submit();
- });
diff --git a/app/views/projects/merge_requests/widget/open/_check.html.haml b/app/views/projects/merge_requests/widget/open/_check.html.haml
index e16878ba513..50086767446 100644
--- a/app/views/projects/merge_requests/widget/open/_check.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_check.html.haml
@@ -1,9 +1,6 @@
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('merge_request_widget/ci_bundle.js')
+
%strong
= icon("spinner spin")
Checking ability to merge automatically&hellip;
-
-:javascript
- $(function() {
- merge_request_widget.getMergeStatus();
- });
-
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 0788924d44a..866b278ce57 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -69,6 +69,11 @@
= link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
= icon('bug', text: 'Fogbugz')
%div
+ - if gitea_import_enabled?
+ = link_to new_import_gitea_url, class: 'btn import_gitea' do
+ = custom_icon('go_logo')
+ Gitea
+ %div
- if git_import_enabled?
= link_to "#", class: 'btn js-toggle-button import_git' do
= icon('git', text: 'Repo by URL')
diff --git a/app/views/projects/pipelines/_stage.html.haml b/app/views/projects/pipelines/_stage.html.haml
new file mode 100644
index 00000000000..20456e792e7
--- /dev/null
+++ b/app/views/projects/pipelines/_stage.html.haml
@@ -0,0 +1,4 @@
+%ul
+ - @stage.statuses.each do |status|
+ %li.dropdown-build
+ = render 'ci/status/graph_badge', subject: status
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 030cd8ef78f..28026ccf861 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -42,14 +42,14 @@
.nothing-here-block No pipelines to show
- else
.table-holder
- %table.table.ci-table
+ %table.table.ci-table.js-pipeline-table
%thead
- %th Status
- %th Pipeline
- %th Commit
- %th Stages
- %th
- %th.hidden-xs
+ %th.pipeline-status Status
+ %th.pipeline-info Pipeline
+ %th.pipeline-commit Commit
+ %th.pipeline-stages Stages
+ %th.pipeline-date
+ %th.pipeline-actions.hidden-xs
= render @pipelines, commit_sha: true, stage: true, allow_retry: true
= paginate @pipelines, theme: 'gitlab'
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index db51c4f8a4e..fc338dcf887 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -8,7 +8,6 @@
.col-lg-9
= form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form|
= render 'shared/service_settings', form: form, subject: @service
-
.footer-block.row-content-block
= form.submit 'Save changes', class: 'btn btn-save'
&nbsp;
diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
new file mode 100644
index 00000000000..8ca4c51a064
--- /dev/null
+++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
@@ -0,0 +1,91 @@
+- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}"
+
+To setup this service:
+%ul.list-unstyled
+ %li
+ 1.
+ = link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands'
+ on your Mattermost installation
+ %li
+ 2.
+ = link_to 'Add a slash command', 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command'
+ in Mattermost with these options:
+
+%hr
+
+.help-form
+ .form-group
+ = label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :display_name, "GitLab / #{@project.name_with_namespace}", class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#display_name')
+
+ .form-group
+ = label_tag :description, 'Description', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#description')
+
+ .form-group
+ = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block
+ %p Fill in the word that works best for your team.
+ %p
+ Suggestions:
+ %code= 'gitlab'
+ %code= @project.path # Path contains no spaces, but dashes
+ %code= @project.path_with_namespace
+
+ .form-group
+ = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#request_url')
+
+ .form-group
+ = label_tag nil, 'Request method', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block POST
+
+ .form-group
+ = label_tag :response_username, 'Response username', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :response_username, 'GitLab', class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#response_username')
+
+ .form-group
+ = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#response_icon')
+
+ .form-group
+ = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block Yes
+
+ .form-group
+ = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :autocomplete_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#autocomplete_hint')
+
+ .form-group
+ = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#autocomplete_description')
+
+%hr
+
+%ul.list-unstyled
+ %li
+ 3. After adding the slash command, paste the
+
+ %strong token
+ into the field below
diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
index 01a77a952d1..63b797cd391 100644
--- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
@@ -1,4 +1,4 @@
-- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}"
+- enabled = Gitlab.config.mattermost.enabled
.well
This service allows GitLab users to perform common operations on this
@@ -7,93 +7,9 @@
See list of available commands in Mattermost after setting up this service,
by entering
%code /&lt;command_trigger_word&gt; help
- %br
- %br
- To setup this service:
- %ul.list-unstyled
- %li
- 1.
- = link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands'
- on your Mattermost installation
- %li
- 2.
- = link_to 'Add a slash command', 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command'
- in Mattermost with these options:
-
- %hr
-
- .help-form
- .form-group
- = label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :display_name, "GitLab / #{@project.name_with_namespace}", class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#display_name')
-
- .form-group
- = label_tag :description, 'Description', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#description')
-
- .form-group
- = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block
- %p Fill in the word that works best for your team.
- %p
- Suggestions:
- %code= 'gitlab'
- %code= @project.path # Path contains no spaces, but dashes
- %code= @project.path_with_namespace
-
- .form-group
- = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#request_url')
-
- .form-group
- = label_tag nil, 'Request method', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block POST
-
- .form-group
- = label_tag :response_username, 'Response username', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :response_username, 'GitLab', class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#response_username')
-
- .form-group
- = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#response_icon')
-
- .form-group
- = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block Yes
-
- .form-group
- = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :autocomplete_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#autocomplete_hint')
-
- .form-group
- = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#autocomplete_description')
- %hr
+ - unless enabled
+ = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
- %ul.list-unstyled
- %li
- 3. After adding the slash command, paste the
- %strong token
- into the field below
+- if enabled
+ = render 'projects/services/mattermost_slash_commands/installation_info', subject: @service
diff --git a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml
new file mode 100644
index 00000000000..c929eee3bb9
--- /dev/null
+++ b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml
@@ -0,0 +1,7 @@
+.services-installation-info
+ - unless @service.activated?
+ .row
+ .col-sm-9.col-sm-offset-3
+ = link_to new_namespace_project_mattermost_path(@project.namespace, @project), class: 'btn btn-lg' do
+ = custom_icon('mattermost_logo', size: 15)
+ = 'Add to Mattermost'
diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml
new file mode 100644
index 00000000000..c45052e3954
--- /dev/null
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -0,0 +1,93 @@
+- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}"
+
+.well
+ This service allows GitLab users to perform common operations on this
+ project by entering slash commands in Slack.
+ %br
+ See list of available commands in Slack after setting up this service,
+ by entering
+ %code /&lt;command&gt; help
+ %br
+ %br
+ To setup this service:
+ %ul.list-unstyled
+ %li
+ 1.
+ = link_to 'Add a slash command', 'https://my.slack.com/services/new/slash-commands'
+ in your Slack team with these options:
+
+ %hr
+
+ .help-form
+ .form-group
+ = label_tag nil, 'Command', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block
+ %p Fill in the word that works best for your team.
+ %p
+ Suggestions:
+ %code= 'gitlab'
+ %code= @project.path # Path contains no spaces, but dashes
+ %code= @project.path_with_namespace
+
+ .form-group
+ = label_tag :url, 'URL', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#url')
+
+ .form-group
+ = label_tag nil, 'Method', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block POST
+
+ .form-group
+ = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :customize_name, 'GitLab', class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#customize_name')
+
+ .form-group
+ = label_tag nil, 'Customize icon', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block
+ = image_tag(asset_url('gitlab_logo.png'), width: 36, height: 36)
+ = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank')
+
+ .form-group
+ = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block Show this command in the autocomplete list
+
+ .form-group
+ = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#autocomplete_description')
+
+ .form-group
+ = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#autocomplete_usage_hint')
+
+ .form-group
+ = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#descriptive_label')
+
+ %hr
+
+ %ul.list-unstyled
+ %li
+ 2. Paste the
+ %strong Token
+ into the field below
+ %li
+ 3. Select the
+ %strong Active
+ checkbox, press
+ %strong Save changes
+ and start using GitLab inside Slack!
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 8a214e1de58..a915c159cb4 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -68,6 +68,10 @@
- if koding_enabled? && @repository.koding_yml.blank?
%li.missing
= link_to 'Set up Koding', add_koding_stack_path(@project)
+ - if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present?
+ %li.missing
+ = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up autodeploy', target_branch: 'autodeploy', context: 'autodeploy') do
+ Set up autodeploy
- if @repository.commit
.project-last-commit{ class: container_class }
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index e939278bc07..e2033654018 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -8,12 +8,12 @@
= render 'shared/empty_states/icons/issues.svg'
.col-xs-12{ class: "#{'col-sm-6' if has_button}" }
.text-content
- - if has_button
+ - if has_button && current_user
%h4
- The Issue Tracker is a good place to add things that need to be improved or solved in a project!
+ The Issue Tracker is the place to add things that need to be improved or solved in a project
%p
- An issue can be a bug, a todo or a feature request that needs to be discussed in a project.
- Besides, issues are searchable and filterable.
+ Issues can be bugs, tasks or ideas to be discussed.
+ Also, issues are searchable and filterable.
- if project_select_button
= render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue'
- else
diff --git a/app/views/shared/icons/_go_logo.svg.erb b/app/views/shared/icons/_go_logo.svg.erb
new file mode 100644
index 00000000000..5052651c110
--- /dev/null
+++ b/app/views/shared/icons/_go_logo.svg.erb
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="<%= size %>" height="<%= size %>" viewBox="0 0 16 16"><g fill-rule="evenodd" transform="translate(0 1)"><path d="m14 15.01h1v-8.02c0-3.862-3.134-6.991-7-6.991-3.858 0-7 3.13-7 6.991v8.02h1v-8.02c0-3.306 2.691-5.991 6-5.991 3.314 0 6 2.682 6 5.991v8.02m-10.52-13.354c-.366-.402-.894-.655-1.48-.655-1.105 0-2 .895-2 2 0 .868.552 1.606 1.325 1.883.102-.321.226-.631.371-.93-.403-.129-.695-.507-.695-.953 0-.552.448-1 1-1 .306 0 .58.138.764.354.222-.25.461-.483.717-.699m9.04-.002c.366-.401.893-.653 1.479-.653 1.105 0 2 .895 2 2 0 .867-.552 1.606-1.324 1.883-.101-.321-.225-.632-.37-.931.403-.129.694-.507.694-.952 0-.552-.448-1-1-1-.305 0-.579.137-.762.353-.222-.25-.461-.483-.717-.699"/><path d="m5.726 7.04h1.557v.124c0 .283-.033.534-.1.752-.065.202-.175.391-.33.566-.35.394-.795.591-1.335.591-.527 0-.979-.19-1.355-.571-.376-.382-.564-.841-.564-1.377 0-.547.191-1.01.574-1.391.382-.382.848-.574 1.396-.574.295 0 .57.06.825.181.244.12.484.316.72.586l-.405.388c-.309-.412-.686-.618-1.13-.618-.399 0-.733.138-1 .413-.27.27-.405.609-.405 1.015 0 .42.151.766.452 1.037.282.252.587.378.915.378.28 0 .531-.094.754-.283.223-.19.347-.418.373-.683h-.94v-.535m2.884.061c0-.53.194-.986.583-1.367.387-.381.853-.571 1.396-.571.537 0 .998.192 1.382.576.386.384.578.845.578 1.384 0 .542-.194 1-.581 1.379-.389.379-.858.569-1.408.569-.487 0-.923-.168-1.311-.505-.426-.373-.64-.861-.64-1.465m.574.007c0 .417.14.759.42 1.028.278.269.6.403.964.403.395 0 .729-.137 1-.41.272-.277.408-.613.408-1.01 0-.402-.134-.739-.403-1.01-.267-.273-.597-.41-.991-.41-.392 0-.723.137-.993.41-.27.27-.405.604-.405 1m-.184 3.918c.525.026.812.063.812.063.271.025.324-.096.116-.273 0 0-.775-.813-1.933-.813-1.159 0-1.923.813-1.923.813-.211.174-.153.3.12.273 0 0 .286-.037.81-.063v.477c0 .268.224.5.5.5.268 0 .5-.223.5-.498v-.252.25c0 .268.224.5.5.5.268 0 .5-.223.5-.498v-.478m-1-1.023c.552 0 1-.224 1-.5 0-.276-.448-.5-1-.5-.552 0-1 .224-1 .5 0 .276.448.5 1 .5"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_canceled_borderless.svg b/app/views/shared/icons/_icon_status_canceled_borderless.svg
new file mode 100644
index 00000000000..bf7fb29185f
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_canceled_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"><path d="M8.17142857,5.97142857 L15.8714286,13.6714286 C16.1857143,13.9857143 16.1857143,14.4571429 15.8714286,14.7714286 L14.7714286,15.8714286 C14.4571429,16.1857143 13.9857143,16.1857143 13.6714286,15.8714286 L5.97142857,8.17142857 C5.65714286,7.85714286 5.65714286,7.38571429 5.97142857,7.07142857 L7.07142857,5.97142857 C7.38571429,5.65714286 7.85714286,5.65714286 8.17142857,5.97142857" id="Shape"></path></svg>
diff --git a/app/views/shared/icons/_icon_status_created_borderless.svg b/app/views/shared/icons/_icon_status_created_borderless.svg
new file mode 100644
index 00000000000..1810d023be8
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_created_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><circle id="Oval" cx="11" cy="11" r="5.10714286"></circle></svg>
diff --git a/app/views/shared/icons/_icon_status_failed_borderless.svg b/app/views/shared/icons/_icon_status_failed_borderless.svg
new file mode 100644
index 00000000000..b7022350c74
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_failed_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M12.1458333,9.85416667 L12.1458333,6.74047388 C12.1458333,6.4826434 11.9382041,6.28571429 11.6820804,6.28571429 L10.3179196,6.28571429 C10.0656535,6.28571429 9.85416667,6.48931709 9.85416667,6.74047388 L9.85416667,9.85416667 L6.74047388,9.85416667 C6.4826434,9.85416667 6.28571429,10.0617959 6.28571429,10.3179196 L6.28571429,11.6820804 C6.28571429,11.9343465 6.48931709,12.1458333 6.74047388,12.1458333 L9.85416667,12.1458333 L9.85416667,15.2595261 C9.85416667,15.5173566 10.0617959,15.7142857 10.3179196,15.7142857 L11.6820804,15.7142857 C11.9343465,15.7142857 12.1458333,15.5106829 12.1458333,15.2595261 L12.1458333,12.1458333 L15.2595261,12.1458333 C15.5173566,12.1458333 15.7142857,11.9382041 15.7142857,11.6820804 L15.7142857,10.3179196 C15.7142857,10.0656535 15.5106829,9.85416667 15.2595261,9.85416667 L12.1458333,9.85416667 Z" id="Combined-Shape" transform="translate(11.000000, 11.000000) rotate(-45.000000) translate(-11.000000, -11.000000) "></path></svg>
diff --git a/app/views/shared/icons/_icon_status_manual_borderless.svg b/app/views/shared/icons/_icon_status_manual_borderless.svg
new file mode 100644
index 00000000000..5eec665688b
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_manual_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M16.5,11.9906832 L16.5,10.0093168 L15.2625,9.80434783 C15.19375,9.5310559 15.05625,9.25776398 14.85,8.84782609 L15.60625,7.82298137 L14.1625,6.38819876 L13.13125,7.13975155 C12.7875,6.93478261 12.44375,6.79813665 12.16875,6.72981366 L12.03125,5.5 L10.0375,5.5 L9.83125,6.72981366 C9.4875,6.79813665 9.2125,6.93478261 8.86875,7.13975155 L7.8375,6.38819876 L6.39375,7.82298137 L7.08125,8.84782609 C6.875,9.18944099 6.80625,9.46273292 6.66875,9.80434783 L5.5,9.94099379 L5.5,11.9223602 L6.7375,12.1273292 C6.80625,12.4689441 6.94375,12.742236 7.15,13.0838509 L6.4625,14.1086957 L7.90625,15.5434783 L8.9375,14.8602484 C9.2125,14.9968944 9.55625,15.1335404 9.9,15.2701863 L10.10625,16.5 L12.16875,16.5 L12.375,15.2701863 C12.71875,15.2018634 12.99375,15.0652174 13.3375,14.8602484 L14.36875,15.6118012 L15.8125,14.1770186 L15.05625,13.1521739 C15.2625,12.810559 15.4,12.4689441 15.46875,12.1956522 L16.5,11.9906832 L16.5,11.9906832 Z M11,13.015528 C9.83125,13.015528 8.9375,12.1273292 8.9375,10.9658385 C8.9375,9.80434783 9.83125,8.91614907 11,8.91614907 C12.16875,8.91614907 13.0625,9.80434783 13.0625,10.9658385 C13.0625,12.1273292 12.16875,13.015528 11,13.015528 L11,13.015528 Z" id="Shape" ></path></svg>
diff --git a/app/views/shared/icons/_icon_status_pending_borderless.svg b/app/views/shared/icons/_icon_status_pending_borderless.svg
new file mode 100644
index 00000000000..8d66e9e6c9c
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_pending_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M7.38571429,8.32857143 C7.38571429,8.01428571 7.54285714,7.85714286 7.85714286,7.85714286 L9.27142857,7.85714286 C9.58571429,7.85714286 9.74285714,8.01428571 9.74285714,8.32857143 L9.74285714,13.6714286 C9.74285714,13.9857143 9.58571429,14.1428571 9.27142857,14.1428571 L7.85714286,14.1428571 C7.54285714,14.1428571 7.38571429,13.9857143 7.38571429,13.6714286 L7.38571429,8.32857143 M12.1,8.32857143 C12.1,8.01428571 12.2571429,7.85714286 12.5714286,7.85714286 L13.9857143,7.85714286 C14.3,7.85714286 14.4571429,8.01428571 14.4571429,8.32857143 L14.4571429,13.6714286 C14.4571429,13.9857143 14.3,14.1428571 13.9857143,14.1428571 L12.5714286,14.1428571 C12.2571429,14.1428571 12.1,13.9857143 12.1,13.6714286 L12.1,8.32857143" id="Shape"></path></svg>
diff --git a/app/views/shared/icons/_icon_status_running_borderless.svg b/app/views/shared/icons/_icon_status_running_borderless.svg
new file mode 100644
index 00000000000..2757a168ed5
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_running_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M11,4.71428571 C14.4571429,4.71428571 17.2857143,7.54285714 17.2857143,11 C17.2857143,14.4571429 14.4571429,17.2857143 11,17.2857143 C8.95714286,17.2857143 7.07142857,16.1857143 5.81428571,14.6142857 L11,11 L11,4.71428571" id="Shape"></path></svg>
diff --git a/app/views/shared/icons/_icon_status_skipped_borderless.svg b/app/views/shared/icons/_icon_status_skipped_borderless.svg
new file mode 100644
index 00000000000..fb3e930b3cb
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_skipped_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M12.0846,12.1 L10.6623,13.5223 C10.2454306,13.9539168 10.2513924,14.6399933 10.6756996,15.0643004 C11.1000067,15.4886076 11.7860832,15.4945694 12.2177,15.0777 L15.1261,12.1693 C15.7708612,11.5230891 15.7708612,10.4769109 15.1261,9.8307 L12.2177,6.9223 C11.7860832,6.50543057 11.1000067,6.51139239 10.6756996,6.93569957 C10.2513924,7.36000675 10.2454306,8.04608322 10.6623,8.4777 L12.0846,9.9 L7.04,9.9 C6.43248678,9.9 5.94,10.3924868 5.94,11 C5.94,11.6075132 6.43248678,12.1 7.04,12.1 L12.0846,12.1 L12.0846,12.1 Z" id="Shape"></path></svg>
diff --git a/app/views/shared/icons/_icon_status_success_borderless.svg b/app/views/shared/icons/_icon_status_success_borderless.svg
new file mode 100644
index 00000000000..8ee5be7ab78
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_success_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M11.4583333,12.375 L8.70008808,12.375 C8.45889044,12.375 8.25,12.5826293 8.25,12.8387529 L8.25,14.2029137 C8.25,14.4551799 8.4515113,14.6666667 8.70008808,14.6666667 L12.9619841,14.6666667 C13.3891296,14.6666667 13.75,14.3193051 13.75,13.8908129 L13.75,13.2899463 L13.75,6.42552703 C13.75,6.16226705 13.5423707,5.95833333 13.2862471,5.95833333 L11.9220863,5.95833333 C11.6698201,5.95833333 11.4583333,6.16750307 11.4583333,6.42552703 L11.4583333,12.375 Z" id="Combined-Shape" transform="translate(11.000000, 10.312500) rotate(-315.000000) translate(-11.000000, -10.312500) "></path></svg>
diff --git a/app/views/shared/icons/_icon_status_warning_borderless.svg b/app/views/shared/icons/_icon_status_warning_borderless.svg
new file mode 100644
index 00000000000..7b061624521
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_warning_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M9.42857143,5.5 C9.42857143,5.02857143 9.74285714,4.71428571 10.2142857,4.71428571 L11.7857143,4.71428571 C12.2571429,4.71428571 12.5714286,5.02857143 12.5714286,5.5 L12.5714286,11.7857143 C12.5714286,12.2571429 12.2571429,12.5714286 11.7857143,12.5714286 L10.2142857,12.5714286 C9.74285714,12.5714286 9.42857143,12.2571429 9.42857143,11.7857143 L9.42857143,5.5 M9.42857143,14.9285714 C9.42857143,14.4571429 9.74285714,14.1428571 10.2142857,14.1428571 L11.7857143,14.1428571 C12.2571429,14.1428571 12.5714286,14.4571429 12.5714286,14.9285714 L12.5714286,16.5 C12.5714286,16.9714286 12.2571429,17.2857143 11.7857143,17.2857143 L10.2142857,17.2857143 C9.74285714,17.2857143 9.42857143,16.9714286 9.42857143,16.5 L9.42857143,14.9285714" id="Shape"></path></svg>
diff --git a/app/views/shared/icons/_icon_terminal.svg b/app/views/shared/icons/_icon_terminal.svg
new file mode 100644
index 00000000000..c80f44c3edf
--- /dev/null
+++ b/app/views/shared/icons/_icon_terminal.svg
@@ -0,0 +1 @@
+<svg width="19" height="14" viewBox="0 0 19 14" xmlns="http://www.w3.org/2000/svg"><rect fill="#848484" x="7.2" y="9.25" width="6.46" height="1.5" rx=".5"/><path d="M5.851 7.016L3.81 9.103a.503.503 0 0 0 .017.709l.35.334c.207.198.524.191.717-.006l2.687-2.748a.493.493 0 0 0 .137-.376.493.493 0 0 0-.137-.376L4.894 3.892a.507.507 0 0 0-.717-.006l-.35.334a.503.503 0 0 0-.017.709L5.85 7.016z"/><path d="M1.25 11.497c0 .691.562 1.253 1.253 1.253h13.994c.694 0 1.253-.56 1.253-1.253V2.503c0-.691-.562-1.253-1.253-1.253H2.503c-.694 0-1.253.56-1.253 1.253v8.994zM2.503 0h13.994A2.504 2.504 0 0 1 19 2.503v8.994A2.501 2.501 0 0 1 16.497 14H2.503A2.504 2.504 0 0 1 0 11.497V2.503A2.501 2.501 0 0 1 2.503 0z"/></svg>
diff --git a/app/views/shared/icons/_mattermost_logo.svg.erb b/app/views/shared/icons/_mattermost_logo.svg.erb
new file mode 100644
index 00000000000..83fbd1a407d
--- /dev/null
+++ b/app/views/shared/icons/_mattermost_logo.svg.erb
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="<%= size %>" height="<%= size %>" version="1" viewBox="0 0 501 501"><path d="M236 .7C137.7 7.5 54 68.2 18.2 158.5c-32 81-19.6 172.8 33 242.5 39.8 53 97.2 87 164.3 97 16.5 2.7 48 3.2 63.5 1.2 48.7-6.3 92.2-24.6 129-54.2 13-10.5 33-31.2 42.2-43.7 26.4-35.5 42.8-75.8 49-120.3 1.6-12.3 1.6-48.7 0-61-4-28.3-12-54.8-24.2-79.5-12.8-26-26.5-45.3-46.8-65.8C417.8 64 400.2 49 398.4 49c-.6 0-.4 10.5.3 26l1.3 26 7 8.7c19 23.7 32.8 53.5 38.2 83 2.5 14 3 43 1 55.8-4.5 27.8-15.2 54-31 76.5-8.6 12.2-28 31.6-40.2 40.2-24 17-50 27.6-80 33-10 1.8-49 1.8-59 0-43-7.7-78.8-26-107.2-54.8-29.3-29.7-46.5-64-52.4-104.4-2-14-1.5-42 1-55C90 121.4 132 72 192 49.7c8-3 18.4-5.8 29.5-8.2 1.7-.4 34.4-38 35.3-40.6.3-1-10.2-1-20.8-.4z"/><path d="M322.2 24.6c-1.3.8-8.4 9.3-16 18.7-7.4 9.5-22.4 28-33.2 41.2-51 62.2-66 81.6-70.6 91-6 12-8.4 21-9 33-1.2 19.8 5 36 19 50C222 268 230 273 243 277.2c9 3 10.4 3.2 24 3.2 13.8 0 15 0 22.6-3 23.2-9 39-28.4 45-55.7 2-8.2 2-28.7.4-79.7l-2-72c-1-36.8-1.4-41.8-3-44-2-3-4.8-3.6-7.8-1.4z"/></svg>
diff --git a/app/views/shared/icons/_scroll_down.svg b/app/views/shared/icons/_scroll_down.svg
new file mode 100644
index 00000000000..acf22ac9314
--- /dev/null
+++ b/app/views/shared/icons/_scroll_down.svg
@@ -0,0 +1,3 @@
+<svg width="16" height="33" class="gitlab-icon-scroll-down" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
+ <path fill="#ffffff" d="M1.385 5.534v12.47a4.145 4.145 0 0 0 4.144 4.15h4.942a4.151 4.151 0 0 0 4.144-4.15V5.535a4.145 4.145 0 0 0-4.144-4.15H5.53a4.151 4.151 0 0 0-4.144 4.15zM8.88 30.27v-4.351a.688.688 0 0 0-.69-.688.687.687 0 0 0-.69.688v4.334l-1.345-1.346a.69.69 0 0 0-.976.976l2.526 2.526a.685.685 0 0 0 .494.2.685.685 0 0 0 .493-.2l2.526-2.526a.69.69 0 1 0-.976-.976L8.88 30.27zM0 5.534A5.536 5.536 0 0 1 5.529 0h4.942A5.53 5.53 0 0 1 16 5.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 18.005V5.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0V6.544z" fill-rule="evenodd"/>
+</svg>
diff --git a/app/views/shared/icons/_scroll_down_hover_active.svg b/app/views/shared/icons/_scroll_down_hover_active.svg
new file mode 100644
index 00000000000..262576acf54
--- /dev/null
+++ b/app/views/shared/icons/_scroll_down_hover_active.svg
@@ -0,0 +1,3 @@
+<svg width="16" height="33" class="gitlab-icon-scroll-down-hover" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
+ <path fill="#ffffff" d="M8.88 30.27v-4.351a.688.688 0 0 0-.69-.688.687.687 0 0 0-.69.688v4.334l-1.345-1.346a.69.69 0 0 0-.976.976l2.526 2.526a.685.685 0 0 0 .494.2.685.685 0 0 0 .493-.2l2.526-2.526a.69.69 0 1 0-.976-.976L8.88 30.27zM0 5.534A5.536 5.536 0 0 1 5.529 0h4.942A5.53 5.53 0 0 1 16 5.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 18.005V5.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0V6.544z" fill-rule="evenodd"/>
+</svg>
diff --git a/app/views/shared/icons/_scroll_up.svg b/app/views/shared/icons/_scroll_up.svg
new file mode 100644
index 00000000000..f11288fd59c
--- /dev/null
+++ b/app/views/shared/icons/_scroll_up.svg
@@ -0,0 +1,3 @@
+<svg width="16" height="33" class="gitlab-icon-scroll-up" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
+ <path fill="#ffffff" d="M1.385 14.534v12.47a4.145 4.145 0 0 0 4.144 4.15h4.942a4.151 4.151 0 0 0 4.144-4.15v-12.47a4.145 4.145 0 0 0-4.144-4.15H5.53a4.151 4.151 0 0 0-4.144 4.15zM8.88 2.609V6.96a.688.688 0 0 1-.69.688.687.687 0 0 1-.69-.688V2.627L6.155 3.972a.69.69 0 0 1-.976-.976L7.705.47a.685.685 0 0 1 .494-.2.685.685 0 0 1 .493.2l2.526 2.526a.69.69 0 1 1-.976.976L8.88 2.609zM0 14.534A5.536 5.536 0 0 1 5.529 9h4.942A5.53 5.53 0 0 1 16 14.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 27.005V14.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0v-2.143z" fill-rule="evenodd"/>
+</svg>
diff --git a/app/views/shared/icons/_scroll_up_hover_active.svg b/app/views/shared/icons/_scroll_up_hover_active.svg
new file mode 100644
index 00000000000..4658dbb1bb7
--- /dev/null
+++ b/app/views/shared/icons/_scroll_up_hover_active.svg
@@ -0,0 +1,3 @@
+<svg width="16" height="33" class="gitlab-icon-scroll-up-hover" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
+ <path fill="#ffffff" d="M8.88 2.646l1.362 1.362a.69.69 0 0 0 .976-.976L8.692.507A.685.685 0 0 0 8.2.306a.685.685 0 0 0-.494.2L5.179 3.033a.69.69 0 1 0 .976.976L7.5 2.663v4.179c0 .38.306.688.69.688.381 0 .69-.306.69-.688V2.646zM0 14.534A5.536 5.536 0 0 1 5.529 9h4.942A5.53 5.53 0 0 1 16 14.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 27.005V14.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0v-2.143z" fill-rule="evenodd"/>
+</svg>
diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml
index 40fe53e6a8d..415361f8fbf 100644
--- a/app/views/shared/issuable/_milestone_dropdown.html.haml
+++ b/app/views/shared/issuable/_milestone_dropdown.html.haml
@@ -3,7 +3,7 @@
- show_menu_above = show_menu_above || false
- selected_text = selected.try(:title) || params[:milestone_title]
- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone")
-- if selected.present?
+- if selected.present? || params[:milestone_title].present?
= hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id)
= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone",
placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml
index 8619939dde7..15ff5b8a27e 100644
--- a/app/views/shared/milestones/_issuables.html.haml
+++ b/app/views/shared/milestones/_issuables.html.haml
@@ -3,10 +3,12 @@
- panel_class = primary ? 'panel-primary' : 'panel-default'
.panel{ class: panel_class }
- .panel-heading
- = title
+ .panel-heading.split
+ .left
+ = title
- if show_counter
- .pull-right= issuables.size
+ .right
+ = issuables.size
- class_prefix = dom_class(issuables).pluralize
%ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id }
diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml
index 2b6ce2d7e7a..c8f2319d95a 100644
--- a/app/views/shared/milestones/_tabs.html.haml
+++ b/app/views/shared/milestones/_tabs.html.haml
@@ -21,7 +21,7 @@
.tab-content.milestone-content
.tab-pane.active#tab-issues
- = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user), show_project_name: show_project_name, show_full_project_name: show_full_project_name
+ = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user).include_associations, show_project_name: show_project_name, show_full_project_name: show_full_project_name
.tab-pane#tab-merge-requests
= render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name
.tab-pane#tab-participants
diff --git a/app/views/u2f/_authenticate.html.haml b/app/views/u2f/_authenticate.html.haml
index 232ca26c1af..fa998c91f72 100644
--- a/app/views/u2f/_authenticate.html.haml
+++ b/app/views/u2f/_authenticate.html.haml
@@ -13,7 +13,7 @@
%script#js-authenticate-u2f-error{ type: "text/template" }
%div
- %p <%= error_message %>
+ %p <%= error_message %> (error code: <%= error_code %>)
%a.btn.btn-warning#js-u2f-try-again Try again?
%script#js-authenticate-u2f-authenticated{ type: "text/template" }
diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml
index 8f7b42eb351..fcc33f04237 100644
--- a/app/views/u2f/_register.html.haml
+++ b/app/views/u2f/_register.html.haml
@@ -23,7 +23,7 @@
%script#js-register-u2f-error{ type: "text/template" }
%div
%p
- %span <%= error_message %>
+ %span <%= error_message %> (error code: <%= error_code %>)
%a.btn.btn-warning#js-u2f-try-again Try again?
%script#js-register-u2f-registered{ type: "text/template" }
diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb
index fccddb70d18..2badd0680fb 100644
--- a/app/workers/authorized_projects_worker.rb
+++ b/app/workers/authorized_projects_worker.rb
@@ -2,8 +2,6 @@ class AuthorizedProjectsWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
- LEASE_TIMEOUT = 1.minute.to_i
-
def self.bulk_perform_async(args_list)
Sidekiq::Client.push_bulk('class' => self, 'args' => args_list)
end
@@ -11,24 +9,6 @@ class AuthorizedProjectsWorker
def perform(user_id)
user = User.find_by(id: user_id)
- refresh(user) if user
- end
-
- def refresh(user)
- lease_key = "refresh_authorized_projects:#{user.id}"
- lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)
-
- until uuid = lease.try_obtain
- # Keep trying until we obtain the lease. If we don't do so we may end up
- # not updating the list of authorized projects properly. To prevent
- # hammering Redis too much we'll wait for a bit between retries.
- sleep(1)
- end
-
- begin
- user.refresh_authorized_projects
- ensure
- Gitlab::ExclusiveLease.cancel(lease_key, uuid)
- end
+ user.refresh_authorized_projects if user
end
end
diff --git a/app/workers/reactive_caching_worker.rb b/app/workers/reactive_caching_worker.rb
new file mode 100644
index 00000000000..9af9dae04f0
--- /dev/null
+++ b/app/workers/reactive_caching_worker.rb
@@ -0,0 +1,15 @@
+class ReactiveCachingWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ def perform(class_name, id)
+ klass = begin
+ Kernel.const_get(class_name)
+ rescue NameError
+ nil
+ end
+ return unless klass
+
+ klass.find_by(id: id).try(:exclusively_update_reactive_cache!)
+ end
+end
diff --git a/bin/changelog b/bin/changelog
index e07b1ad237a..4c894f8ff5b 100755
--- a/bin/changelog
+++ b/bin/changelog
@@ -84,12 +84,15 @@ class ChangelogEntry
end
end
+ private
+
def contents
- YAML.dump(
+ yaml_content = YAML.dump(
'title' => title,
'merge_request' => options.merge_request,
'author' => options.author
)
+ remove_trailing_whitespace(yaml_content)
end
def write
@@ -101,8 +104,6 @@ class ChangelogEntry
exec("git commit --amend")
end
- private
-
def fail_with(message)
$stderr.puts "\e[31merror\e[0m #{message}"
exit 1
@@ -160,6 +161,10 @@ class ChangelogEntry
def branch_name
@branch_name ||= %x{git symbolic-ref --short HEAD}.strip
end
+
+ def remove_trailing_whitespace(yaml_content)
+ yaml_content.gsub(/ +$/, '')
+ end
end
if $0 == __FILE__
diff --git a/changelogs/unreleased/19620-auto-scroll-log.yml b/changelogs/unreleased/19620-auto-scroll-log.yml
new file mode 100644
index 00000000000..cf38096683b
--- /dev/null
+++ b/changelogs/unreleased/19620-auto-scroll-log.yml
@@ -0,0 +1,4 @@
+---
+title: Improve Build Log scrolling experience
+merge_request: 7895
+author:
diff --git a/changelogs/unreleased/19703-direct-link-pipelines.yml b/changelogs/unreleased/19703-direct-link-pipelines.yml
new file mode 100644
index 00000000000..d846ad41e0f
--- /dev/null
+++ b/changelogs/unreleased/19703-direct-link-pipelines.yml
@@ -0,0 +1,4 @@
+---
+title: Adds Direct link from pipeline list to builds
+merge_request: 8097
+author:
diff --git a/changelogs/unreleased/22348-gitea-importer.yml b/changelogs/unreleased/22348-gitea-importer.yml
new file mode 100644
index 00000000000..2aeefb0b259
--- /dev/null
+++ b/changelogs/unreleased/22348-gitea-importer.yml
@@ -0,0 +1,4 @@
+---
+title: New Gitea importer
+merge_request: 8116
+author:
diff --git a/changelogs/unreleased/22742-filter-protocol-relative-urls.yml b/changelogs/unreleased/22742-filter-protocol-relative-urls.yml
new file mode 100644
index 00000000000..b331f5a4eb5
--- /dev/null
+++ b/changelogs/unreleased/22742-filter-protocol-relative-urls.yml
@@ -0,0 +1,4 @@
+---
+title: 'Filter protocol-relative URLs in ExternalLinkFilter. Fixes issue #22742'
+merge_request: 6635
+author: Makoto Scott-Hinkle
diff --git a/changelogs/unreleased/23638-remove-builds-tab.yml b/changelogs/unreleased/23638-remove-builds-tab.yml
new file mode 100644
index 00000000000..86d63208761
--- /dev/null
+++ b/changelogs/unreleased/23638-remove-builds-tab.yml
@@ -0,0 +1,4 @@
+---
+title: Resolve "Remove Builds tab from Merge Requests and Commits"
+merge_request: 7763
+author:
diff --git a/changelogs/unreleased/25093-hide-new-issue-btn-non-loggedin-user.yml b/changelogs/unreleased/25093-hide-new-issue-btn-non-loggedin-user.yml
new file mode 100644
index 00000000000..18836e7a90b
--- /dev/null
+++ b/changelogs/unreleased/25093-hide-new-issue-btn-non-loggedin-user.yml
@@ -0,0 +1,4 @@
+---
+title: Hides new issue button for non loggedin user
+merge_request: 8175
+author:
diff --git a/changelogs/unreleased/25368-fix-left-align-system-note.yml b/changelogs/unreleased/25368-fix-left-align-system-note.yml
new file mode 100644
index 00000000000..81fd0888773
--- /dev/null
+++ b/changelogs/unreleased/25368-fix-left-align-system-note.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes left align issue for long system notes
+merge_request: 7982
+author:
diff --git a/changelogs/unreleased/25678-remove-user-build.yml b/changelogs/unreleased/25678-remove-user-build.yml
new file mode 100644
index 00000000000..873e637d670
--- /dev/null
+++ b/changelogs/unreleased/25678-remove-user-build.yml
@@ -0,0 +1,4 @@
+---
+title: remove build_user
+merge_request: 8162
+author: Arsenev Vladislav
diff --git a/changelogs/unreleased/25740-fix-new-branch-button-padding.yml b/changelogs/unreleased/25740-fix-new-branch-button-padding.yml
new file mode 100644
index 00000000000..7da8f9357a7
--- /dev/null
+++ b/changelogs/unreleased/25740-fix-new-branch-button-padding.yml
@@ -0,0 +1,4 @@
+---
+title: Made the padding on the plus button in the breadcrumb menu even
+merge_request:
+author: Ryan Harris
diff --git a/changelogs/unreleased/25895-fix-headers-in-ci-api-helpers.yml b/changelogs/unreleased/25895-fix-headers-in-ci-api-helpers.yml
new file mode 100644
index 00000000000..b9a8e17c64a
--- /dev/null
+++ b/changelogs/unreleased/25895-fix-headers-in-ci-api-helpers.yml
@@ -0,0 +1,4 @@
+---
+title: Ensure nil User-Agent doesn't break the CI API
+merge_request:
+author:
diff --git a/changelogs/unreleased/25898-ci-icon-color-mr.yml b/changelogs/unreleased/25898-ci-icon-color-mr.yml
new file mode 100644
index 00000000000..dd0f93e176f
--- /dev/null
+++ b/changelogs/unreleased/25898-ci-icon-color-mr.yml
@@ -0,0 +1,4 @@
+---
+title: Adds CSS class to status icon on MR widget to prevent non-colored icon
+merge_request: 8219
+author:
diff --git a/changelogs/unreleased/25905-mr-when-succeeds-dropdown.yml b/changelogs/unreleased/25905-mr-when-succeeds-dropdown.yml
new file mode 100644
index 00000000000..39ce0b66768
--- /dev/null
+++ b/changelogs/unreleased/25905-mr-when-succeeds-dropdown.yml
@@ -0,0 +1,4 @@
+---
+title: Adds background color for disabled state to merge when succeeds dropdown
+merge_request: 8222
+author:
diff --git a/changelogs/unreleased/25906-title-size.yml b/changelogs/unreleased/25906-title-size.yml
new file mode 100644
index 00000000000..d92d06197e9
--- /dev/null
+++ b/changelogs/unreleased/25906-title-size.yml
@@ -0,0 +1,4 @@
+---
+title: Standardises font-size for titles in Issues, Merge Requests and Merge Request widget
+merge_request: 8235
+author:
diff --git a/changelogs/unreleased/25908-fix-grape-after-update.yml b/changelogs/unreleased/25908-fix-grape-after-update.yml
new file mode 100644
index 00000000000..026d5592441
--- /dev/null
+++ b/changelogs/unreleased/25908-fix-grape-after-update.yml
@@ -0,0 +1,4 @@
+---
+title: Use Grape's new Route methods
+merge_request:
+author:
diff --git a/changelogs/unreleased/25938-progress-bar-gone.yml b/changelogs/unreleased/25938-progress-bar-gone.yml
new file mode 100644
index 00000000000..841c4d445c6
--- /dev/null
+++ b/changelogs/unreleased/25938-progress-bar-gone.yml
@@ -0,0 +1,4 @@
+---
+title: Adds back CSS for progress-bars
+merge_request: 8237
+author:
diff --git a/changelogs/unreleased/4269-public-api.yml b/changelogs/unreleased/4269-public-api.yml
index 560bc6a4f13..9de739d0cad 100644
--- a/changelogs/unreleased/4269-public-api.yml
+++ b/changelogs/unreleased/4269-public-api.yml
@@ -1,4 +1,4 @@
---
-title: Allow public access to some Project API endpoints
+title: Allow unauthenticated access to some Project API GET endpoints
merge_request: 7843
author:
diff --git a/changelogs/unreleased/4269-public-files-api.yml b/changelogs/unreleased/4269-public-files-api.yml
new file mode 100644
index 00000000000..e8f9e9b5ed3
--- /dev/null
+++ b/changelogs/unreleased/4269-public-files-api.yml
@@ -0,0 +1,4 @@
+---
+title: Allow unauthenticated access to Repositories Files API GET endpoints
+merge_request:
+author:
diff --git a/changelogs/unreleased/4269-public-repositories-api.yml b/changelogs/unreleased/4269-public-repositories-api.yml
new file mode 100644
index 00000000000..38984eed904
--- /dev/null
+++ b/changelogs/unreleased/4269-public-repositories-api.yml
@@ -0,0 +1,4 @@
+---
+title: Allow unauthenticated access to Repositories API GET endpoints
+merge_request: 8148
+author:
diff --git a/changelogs/unreleased/8038-authentiq-id-oauth-support.yml b/changelogs/unreleased/8038-authentiq-id-oauth-support.yml
new file mode 100644
index 00000000000..36f8ac9c840
--- /dev/null
+++ b/changelogs/unreleased/8038-authentiq-id-oauth-support.yml
@@ -0,0 +1,4 @@
+---
+title: Add Authentiq as Oauth provider
+merge_request: 8038
+author: Alexandros Keramidas
diff --git a/changelogs/unreleased/adam-auto-deploy.yml b/changelogs/unreleased/adam-auto-deploy.yml
new file mode 100644
index 00000000000..9d3348468d5
--- /dev/null
+++ b/changelogs/unreleased/adam-auto-deploy.yml
@@ -0,0 +1,4 @@
+---
+title: Introduce "Set up autodeploy" button to help configure GitLab CI for deployment
+merge_request: 8135
+author:
diff --git a/changelogs/unreleased/badge-color-on-white-bg.yml b/changelogs/unreleased/badge-color-on-white-bg.yml
new file mode 100644
index 00000000000..680d7ff11f0
--- /dev/null
+++ b/changelogs/unreleased/badge-color-on-white-bg.yml
@@ -0,0 +1,4 @@
+---
+title: Added lighter count badge background-color for on white backgrounds
+merge_request: 7873
+author:
diff --git a/changelogs/unreleased/dz-fix-route-rename.yml b/changelogs/unreleased/dz-fix-route-rename.yml
new file mode 100644
index 00000000000..a649fb169a5
--- /dev/null
+++ b/changelogs/unreleased/dz-fix-route-rename.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Route#rename_children behavior
+merge_request:
+author:
diff --git a/changelogs/unreleased/dz-rename-invalid-groups.yml b/changelogs/unreleased/dz-rename-invalid-groups.yml
new file mode 100644
index 00000000000..90af42da01c
--- /dev/null
+++ b/changelogs/unreleased/dz-rename-invalid-groups.yml
@@ -0,0 +1,4 @@
+---
+title: Rename groups with .git in the end of the path
+merge_request: 8199
+author:
diff --git a/changelogs/unreleased/dz-whitelist-dashboard-project-path.yml b/changelogs/unreleased/dz-whitelist-dashboard-project-path.yml
new file mode 100644
index 00000000000..2787a5c57df
--- /dev/null
+++ b/changelogs/unreleased/dz-whitelist-dashboard-project-path.yml
@@ -0,0 +1,4 @@
+---
+title: Allow projects with 'dashboard' as path
+merge_request:
+author:
diff --git a/changelogs/unreleased/dz-whitelist-more-project-names-2.yml b/changelogs/unreleased/dz-whitelist-more-project-names-2.yml
new file mode 100644
index 00000000000..5d5f57d79e9
--- /dev/null
+++ b/changelogs/unreleased/dz-whitelist-more-project-names-2.yml
@@ -0,0 +1,4 @@
+---
+title: 'Whitelist next project names: notes, services'
+merge_request:
+author:
diff --git a/changelogs/unreleased/dz-whitelist-more-project-names.yml b/changelogs/unreleased/dz-whitelist-more-project-names.yml
new file mode 100644
index 00000000000..4a3f1511a0b
--- /dev/null
+++ b/changelogs/unreleased/dz-whitelist-more-project-names.yml
@@ -0,0 +1,4 @@
+---
+title: 'Whitelist next project names: help, ci, admin, search'
+merge_request: 8227
+author:
diff --git a/changelogs/unreleased/fix-copy-issues-empty-state.yml b/changelogs/unreleased/fix-copy-issues-empty-state.yml
new file mode 100644
index 00000000000..a87b7612217
--- /dev/null
+++ b/changelogs/unreleased/fix-copy-issues-empty-state.yml
@@ -0,0 +1,4 @@
+---
+title: Improve copy in Issue Tracker empty state
+merge_request: 8202
+author:
diff --git a/changelogs/unreleased/fix-group-path-rename-error.yml b/changelogs/unreleased/fix-group-path-rename-error.yml
new file mode 100644
index 00000000000..e3d97ae3987
--- /dev/null
+++ b/changelogs/unreleased/fix-group-path-rename-error.yml
@@ -0,0 +1,4 @@
+---
+title: Fix 500 error renaming group
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-import-labels-error.yml b/changelogs/unreleased/fix-import-labels-error.yml
new file mode 100644
index 00000000000..86cae3a49ff
--- /dev/null
+++ b/changelogs/unreleased/fix-import-labels-error.yml
@@ -0,0 +1,4 @@
+---
+title: Fix project import label priorities error
+merge_request:
+author:
diff --git a/changelogs/unreleased/jej-fix-n-1-queries-milestones-show.yml b/changelogs/unreleased/jej-fix-n-1-queries-milestones-show.yml
new file mode 100644
index 00000000000..ad6eba3faf2
--- /dev/null
+++ b/changelogs/unreleased/jej-fix-n-1-queries-milestones-show.yml
@@ -0,0 +1,4 @@
+---
+title: Fix N+1 queries on milestone show pages
+merge_request: 8185
+author:
diff --git a/changelogs/unreleased/jej-memoize-milestoneish-visible-to-user.yml b/changelogs/unreleased/jej-memoize-milestoneish-visible-to-user.yml
new file mode 100644
index 00000000000..ab7f39a4178
--- /dev/null
+++ b/changelogs/unreleased/jej-memoize-milestoneish-visible-to-user.yml
@@ -0,0 +1,4 @@
+---
+title: Milestoneish SQL performance partially improved and memoized
+merge_request: 8146
+author:
diff --git a/changelogs/unreleased/mattermost-slash-auto-config.yml b/changelogs/unreleased/mattermost-slash-auto-config.yml
new file mode 100644
index 00000000000..43014d38769
--- /dev/null
+++ b/changelogs/unreleased/mattermost-slash-auto-config.yml
@@ -0,0 +1,4 @@
+---
+title: Allow to auto-configure Mattermost
+merge_request: 8070
+author:
diff --git a/changelogs/unreleased/nick-thomas-gitlab-ce-22864-kubernetes-deploy-with-terminal.yml b/changelogs/unreleased/nick-thomas-gitlab-ce-22864-kubernetes-deploy-with-terminal.yml
new file mode 100644
index 00000000000..bb4edf80d94
--- /dev/null
+++ b/changelogs/unreleased/nick-thomas-gitlab-ce-22864-kubernetes-deploy-with-terminal.yml
@@ -0,0 +1,4 @@
+---
+title: Add online terminal support for Kubernetes
+merge_request: 7690
+author:
diff --git a/changelogs/unreleased/nuke-ugly-spaces-in-changelog-generator.yml b/changelogs/unreleased/nuke-ugly-spaces-in-changelog-generator.yml
new file mode 100644
index 00000000000..fd173031107
--- /dev/null
+++ b/changelogs/unreleased/nuke-ugly-spaces-in-changelog-generator.yml
@@ -0,0 +1,4 @@
+---
+title: Remove trailing whitespace when generating changelog entry
+merge_request: 7948
+author:
diff --git a/changelogs/unreleased/remove-u2f-error-logging.yml b/changelogs/unreleased/remove-u2f-error-logging.yml
new file mode 100644
index 00000000000..edbe576a976
--- /dev/null
+++ b/changelogs/unreleased/remove-u2f-error-logging.yml
@@ -0,0 +1,4 @@
+---
+title: Display error code for U2F errors
+merge_request: 7305
+author: winniehell
diff --git a/changelogs/unreleased/zj-remove-unused-services.yml b/changelogs/unreleased/zj-remove-unused-services.yml
new file mode 100644
index 00000000000..8ede95f5faa
--- /dev/null
+++ b/changelogs/unreleased/zj-remove-unused-services.yml
@@ -0,0 +1,4 @@
+---
+title: Remove unused and void services from the database
+merge_request:
+author:
diff --git a/changelogs/unreleased/zj-slack-slash-commands.yml b/changelogs/unreleased/zj-slack-slash-commands.yml
new file mode 100644
index 00000000000..9f4c8681ad0
--- /dev/null
+++ b/changelogs/unreleased/zj-slack-slash-commands.yml
@@ -0,0 +1,4 @@
+---
+title: Refactor presenters ChatCommands
+merge_request: 7846
+author:
diff --git a/config/application.rb b/config/application.rb
index 782a7a36895..d36c6d5c92e 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -83,18 +83,21 @@ module Gitlab
# Enable the asset pipeline
config.assets.enabled = true
config.assets.paths << Gemojione.images_path
+ config.assets.paths << "vendor/assets/fonts"
config.assets.precompile << "*.png"
config.assets.precompile << "print.css"
config.assets.precompile << "notify.css"
config.assets.precompile << "mailers/*.css"
config.assets.precompile << "katex.css"
config.assets.precompile << "katex.js"
+ config.assets.precompile << "xterm/xterm.css"
config.assets.precompile << "graphs/graphs_bundle.js"
config.assets.precompile << "users/users_bundle.js"
config.assets.precompile << "network/network_bundle.js"
config.assets.precompile << "profile/profile_bundle.js"
config.assets.precompile << "protected_branches/protected_branches_bundle.js"
config.assets.precompile << "diff_notes/diff_notes_bundle.js"
+ config.assets.precompile << "merge_request_widget/ci_bundle.js"
config.assets.precompile << "boards/boards_bundle.js"
config.assets.precompile << "cycle_analytics/cycle_analytics_bundle.js"
config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js"
@@ -102,9 +105,11 @@ module Gitlab
config.assets.precompile << "environments/environments_bundle.js"
config.assets.precompile << "blob_edit/blob_edit_bundle.js"
config.assets.precompile << "snippet/snippet_bundle.js"
+ config.assets.precompile << "terminal/terminal_bundle.js"
config.assets.precompile << "lib/utils/*.js"
config.assets.precompile << "lib/*.js"
config.assets.precompile << "u2f.js"
+ config.assets.precompile << "vendor/assets/fonts/*"
# Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0'
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index b8b41a0d86c..42e5f105d46 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -368,6 +368,16 @@ production: &base
# login_url: '/cas/login',
# service_validate_url: '/cas/p3/serviceValidate',
# logout_url: '/cas/logout'} }
+ # - { name: 'authentiq',
+ # # for client credentials (client ID and secret), go to https://www.authentiq.com/
+ # app_id: 'YOUR_CLIENT_ID',
+ # app_secret: 'YOUR_CLIENT_SECRET',
+ # args: {
+ # scope: 'aq:name email~rs address aq:push'
+ # # redirect_uri parameter is optional except when 'gitlab.host' in this file is set to 'localhost'
+ # # redirect_uri: 'YOUR_REDIRECT_URI'
+ # }
+ # }
# - { name: 'github',
# app_id: 'YOUR_APP_ID',
# app_secret: 'YOUR_APP_SECRET',
@@ -576,4 +586,4 @@ test:
admin_group: ''
staging:
- <<: *base
+ <<: *base \ No newline at end of file
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index ddea325c6ca..ee97b4e42b9 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -213,7 +213,7 @@ Settings.gitlab.default_projects_features['builds'] = true if Settin
Settings.gitlab.default_projects_features['container_registry'] = true if Settings.gitlab.default_projects_features['container_registry'].nil?
Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE)
Settings.gitlab['domain_whitelist'] ||= []
-Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab google_code fogbugz git gitlab_project]
+Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab google_code fogbugz git gitlab_project gitea]
Settings.gitlab['trusted_proxies'] ||= []
Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml'))
diff --git a/config/routes/import.rb b/config/routes/import.rb
index 89f3b3f6378..c378253bf15 100644
--- a/config/routes/import.rb
+++ b/config/routes/import.rb
@@ -6,6 +6,12 @@ namespace :import do
get :jobs
end
+ resource :gitea, only: [:create, :new], controller: :gitea do
+ post :personal_access_token
+ get :status
+ get :jobs
+ end
+
resource :gitlab, only: [:create], controller: :gitlab do
get :status
get :callback
diff --git a/config/routes/project.rb b/config/routes/project.rb
index e17d6bae10c..4d20acbef7a 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -32,10 +32,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
member do
get :branches
- get :builds
get :pipelines
- post :cancel_builds
- post :retry_builds
post :revert
post :cherry_pick
get :diff_for_path
@@ -76,6 +73,8 @@ constraints(ProjectUrlConstrainer.new) do
end
end
+ resource :mattermost, only: [:new, :create]
+
resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do
member do
put :enable
@@ -92,7 +91,6 @@ constraints(ProjectUrlConstrainer.new) do
get :diffs
get :conflicts
get :conflict_for_path
- get :builds
get :pipelines
get :merge_check
post :merge
@@ -139,6 +137,7 @@ constraints(ProjectUrlConstrainer.new) do
end
member do
+ get :stage
post :cancel
post :retry
get :builds
@@ -148,6 +147,8 @@ constraints(ProjectUrlConstrainer.new) do
resources :environments, except: [:destroy] do
member do
post :stop
+ get :terminal
+ get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil }
end
end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 69136b73946..c22964179d9 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -46,5 +46,6 @@
- [repository_check, 1]
- [system_hook, 1]
- [git_garbage_collect, 1]
+ - [reactive_caching, 1]
- [cronjob, 1]
- [default, 1]
diff --git a/db/migrate/20141006143943_move_slack_service_to_webhook.rb b/db/migrate/20141006143943_move_slack_service_to_webhook.rb
index 42e88d6d6e3..561184615cc 100644
--- a/db/migrate/20141006143943_move_slack_service_to_webhook.rb
+++ b/db/migrate/20141006143943_move_slack_service_to_webhook.rb
@@ -5,7 +5,7 @@ class MoveSlackServiceToWebhook < ActiveRecord::Migration
DOWNTIME_REASON = 'Move old fields "token" and "subdomain" to one single field "webhook"'
def change
- SlackNotificationService.all.each do |slack_service|
+ SlackService.all.each do |slack_service|
if ["token", "subdomain"].all? { |property| slack_service.properties.key? property }
token = slack_service.properties['token']
subdomain = slack_service.properties['subdomain']
diff --git a/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb b/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb
index a7278d7b5a6..dc38d0ac906 100644
--- a/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb
+++ b/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb
@@ -1,14 +1,11 @@
class ChangeSlackServiceToSlackNotificationService < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
- DOWNTIME = true
- DOWNTIME_REASON = 'Rename SlackService to SlackNotificationService'
+ DOWNTIME = false
- def up
- execute("UPDATE services SET type = 'SlackNotificationService' WHERE type = 'SlackService'")
- end
-
- def down
- execute("UPDATE services SET type = 'SlackService' WHERE type = 'SlackNotificationService'")
+ # This migration is a no-op, as it existed in an RC but we renamed
+ # SlackNotificationService back to SlackService:
+ # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8191#note_20310845
+ def change
end
end
diff --git a/db/migrate/20161220141214_remove_dot_git_from_group_names.rb b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb
new file mode 100644
index 00000000000..241afc6b097
--- /dev/null
+++ b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb
@@ -0,0 +1,82 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveDotGitFromGroupNames < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ include Gitlab::ShellAdapter
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ invalid_groups.each do |group|
+ path_was = group['path']
+ path_was_wildcard = quote_string("#{path_was}/%")
+ path = quote_string(rename_path(path_was))
+
+ move_namespace(group['id'], path_was, path)
+
+ execute "UPDATE routes SET path = '#{path}' WHERE source_type = 'Namespace' AND source_id = #{group['id']}"
+ execute "UPDATE namespaces SET path = '#{path}' WHERE id = #{group['id']}"
+
+ select_all("SELECT id, path FROM routes WHERE path LIKE '#{path_was_wildcard}'").each do |route|
+ new_path = "#{path}/#{route['path'].split('/').last}"
+ execute "UPDATE routes SET path = '#{new_path}' WHERE id = #{route['id']}"
+ end
+ end
+ end
+
+ def down
+ # nothing to do here
+ end
+
+ private
+
+ def invalid_groups
+ select_all("SELECT id, path FROM namespaces WHERE type = 'Group' AND path LIKE '%.git'")
+ end
+
+ def route_exists?(path)
+ select_all("SELECT id, path FROM routes WHERE path = '#{quote_string(path)}'").present?
+ end
+
+ # Accepts invalid path like test.git and returns test_git or
+ # test_git1 if test_git already taken
+ def rename_path(path)
+ # To stay closer with original name and reduce risk of duplicates
+ # we rename suffix instead of removing it
+ path = path.sub(/\.git\z/, '_git')
+
+ counter = 0
+ base = path
+
+ while route_exists?(path)
+ counter += 1
+ path = "#{base}#{counter}"
+ end
+
+ path
+ end
+
+ def move_namespace(group_id, path_was, path)
+ repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{group_id}").map do |row|
+ Gitlab.config.repositories.storages[row['repository_storage']]
+ end.compact
+
+ # Move the namespace directory in all storages paths used by member projects
+ repository_storage_paths.each do |repository_storage_path|
+ # Ensure old directory exists before moving it
+ gitlab_shell.add_namespace(repository_storage_path, path_was)
+
+ unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path)
+ Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}"
+
+ # if we cannot move namespace directory we should rollback
+ # db changes in order to prevent out of sync between db and fs
+ raise Exception.new('namespace directory cannot be moved')
+ end
+ end
+
+ Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
+ end
+end
diff --git a/db/post_migrate/20161221140236_remove_unneeded_services.rb b/db/post_migrate/20161221140236_remove_unneeded_services.rb
new file mode 100644
index 00000000000..a94ccc43a41
--- /dev/null
+++ b/db/post_migrate/20161221140236_remove_unneeded_services.rb
@@ -0,0 +1,13 @@
+class RemoveUnneededServices < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ execute("DELETE FROM services WHERE active = false AND properties = '{}';")
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 14801b581e6..05b6c807660 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20161213172958) do
+ActiveRecord::Schema.define(version: 20161221140236) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -854,7 +854,7 @@ ActiveRecord::Schema.define(version: 20161213172958) do
t.datetime "expires_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
- t.string "scopes", default: "--- []\n", null: false
+ t.string "scopes", default: "--- []\n", null: false
end
add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
diff --git a/doc/README.md b/doc/README.md
index a60a5359540..ee69684b53b 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -34,6 +34,7 @@
- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter.
- [Issue closing pattern](administration/issue_closing_pattern.md) Customize how to close an issue from commit messages.
- [Koding](administration/integration/koding.md) Set up Koding to use with GitLab.
+- [Web terminals](administration/integration/terminal.md) Provide terminal access to environments from within GitLab.
- [Libravatar](customization/libravatar.md) Use Libravatar instead of Gravatar for user avatars.
- [Log system](administration/logs.md) Log system.
- [Environment Variables](administration/environment_variables.md) to configure GitLab.
diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md
index 2fc5d0355b5..13bd501e397 100644
--- a/doc/administration/auth/README.md
+++ b/doc/administration/auth/README.md
@@ -6,7 +6,7 @@ providers.
- [LDAP](ldap.md) Includes Active Directory, Apple Open Directory, Open LDAP,
and 389 Server
- [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google,
- Bitbucket, Facebook, Shibboleth, Crowd and Azure
+ Bitbucket, Facebook, Shibboleth, Crowd, Azure and Authentiq ID
- [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS
- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider
- [Okta](okta.md) Configure GitLab to sign in using Okta
diff --git a/doc/administration/auth/authentiq.md b/doc/administration/auth/authentiq.md
new file mode 100644
index 00000000000..3f39539da95
--- /dev/null
+++ b/doc/administration/auth/authentiq.md
@@ -0,0 +1,69 @@
+# Authentiq OmniAuth Provider
+
+To enable the Authentiq OmniAuth provider for passwordless authentication you must register an application with Authentiq.
+
+Authentiq will generate a Client ID and the accompanying Client Secret for you to use.
+
+1. Get your Client credentials (Client ID and Client Secret) at [Authentiq](https://www.authentiq.com/register).
+
+2. On your GitLab server, open the configuration file:
+
+ For omnibus installation
+ ```sh
+ sudo editor /etc/gitlab/gitlab.rb
+ ```
+
+ For installations from source:
+
+ ```sh
+ sudo -u git -H editor /home/git/gitlab/config/gitlab.yml
+ ```
+
+3. See [Initial OmniAuth Configuration](../../integration/omniauth.md#initial-omniauth-configuration) for initial settings to enable single sign-on and add Authentiq as an OAuth provider.
+
+4. Add the provider configuration for Authentiq:
+
+ For Omnibus packages:
+
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ "name" => "authentiq",
+ "app_id" => "YOUR_CLIENT_ID",
+ "app_secret" => "YOUR_CLIENT_SECRET",
+ "args" => {
+ scope: 'aq:name email~rs aq:push'
+ }
+ }
+ ]
+ ```
+
+ For installations from source:
+
+ ```yaml
+ - { name: 'authentiq',
+ app_id: 'YOUR_CLIENT_ID',
+ app_secret: 'YOUR_CLIENT_SECRET',
+ args: {
+ scope: 'aq:name email~rs aq:push'
+ }
+ }
+ ```
+
+
+5. The `scope` is set to request the user's name, email (required and signed), and permission to send push notifications to sign in on subsequent visits.
+See [OmniAuth Authentiq strategy](https://github.com/AuthentiqID/omniauth-authentiq#scopes-and-redirect-uri-configuration) for more information on scopes and modifiers.
+
+6. Change 'YOUR_CLIENT_ID' and 'YOUR_CLIENT_SECRET' to the Client credentials you received in step 1.
+
+7. Save the configuration file.
+
+8. [Reconfigure](../restart_gitlab.md#omnibus-gitlab-reconfigure) or [restart GitLab](../restart_gitlab.md#installations-from-source)
+ for the changes to take effect if you installed GitLab via Omnibus or from source respectively.
+
+On the sign in page there should now be an Authentiq icon below the regular sign in form. Click the icon to begin the authentication process.
+
+- If the user has the Authentiq ID app installed in their iOS or Android device, they can scan the QR code, decide what personal details to share and sign in to your GitLab installation.
+- If not they will be prompted to download the app and then follow the procedure above.
+
+If everything goes right, the user will be returned to GitLab and will be signed in. \ No newline at end of file
diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md
index 136f570ac27..1824829903c 100644
--- a/doc/administration/high_availability/load_balancer.md
+++ b/doc/administration/high_availability/load_balancer.md
@@ -10,11 +10,11 @@ you need to use with GitLab.
## Basic ports
-| LB Port | Backend Port | Protocol |
-| ------- | ------------ | -------- |
-| 80 | 80 | HTTP |
-| 443 | 443 | HTTPS [^1] |
-| 22 | 22 | TCP |
+| LB Port | Backend Port | Protocol |
+| ------- | ------------ | --------------- |
+| 80 | 80 | HTTP [^1] |
+| 443 | 443 | HTTPS [^1] [^2] |
+| 22 | 22 | TCP |
## GitLab Pages Ports
@@ -25,8 +25,8 @@ GitLab Pages requires a separate VIP. Configure DNS to point the
| LB Port | Backend Port | Protocol |
| ------- | ------------ | -------- |
-| 80 | Varies [^2] | HTTP |
-| 443 | Varies [^2] | TCP [^3] |
+| 80 | Varies [^3] | HTTP |
+| 443 | Varies [^3] | TCP [^4] |
## Alternate SSH Port
@@ -50,13 +50,19 @@ Read more on high-availability configuration:
1. [Configure NFS](nfs.md)
1. [Configure the GitLab application servers](gitlab.md)
-[^1]: When using HTTPS protocol for port 443, you will need to add an SSL
+[^1]: [Web terminal](../../ci/environments.md#web-terminals) support requires
+ your load balancer to correctly handle WebSocket connections. When using
+ HTTP or HTTPS proxying, this means your load balancer must be configured
+ to pass through the `Connection` and `Upgrade` hop-by-hop headers. See the
+ [web terminal](../integration/terminal.md) integration guide for
+ more details.
+[^2]: When using HTTPS protocol for port 443, you will need to add an SSL
certificate to the load balancers. If you wish to terminate SSL at the
GitLab application server instead, use TCP protocol.
-[^2]: The backend port for GitLab Pages depends on the
+[^3]: The backend port for GitLab Pages depends on the
`gitlab_pages['external_http']` and `gitlab_pages['external_https']`
setting. See [GitLab Pages documentation][gitlab-pages] for more details.
-[^3]: Port 443 for GitLab Pages should always use the TCP protocol. Users can
+[^4]: Port 443 for GitLab Pages should always use the TCP protocol. Users can
configure custom domains with custom SSL, which would not be possible
if SSL was terminated at the load balancer.
diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md
new file mode 100644
index 00000000000..a1d1bb03b50
--- /dev/null
+++ b/doc/administration/integration/terminal.md
@@ -0,0 +1,73 @@
+# Web terminals
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690)
+in GitLab 8.15. Only project masters and owners can access web terminals.
+
+With the introduction of the [Kubernetes](../../project_services/kubernetes.md)
+project service, GitLab gained the ability to store and use credentials for a
+Kubernetes cluster. One of the things it uses these credentials for is providing
+access to [web terminals](../../ci/environments.html#web-terminals)
+for environments.
+
+## How it works
+
+A detailed overview of the architecture of web terminals and how they work
+can be found in [this document](https://gitlab.com/gitlab-org/gitlab-workhorse/blob/master/doc/terminal.md).
+In brief:
+
+* GitLab relies on the user to provide their own Kubernetes credentials, and to
+ appropriately label the pods they create when deploying.
+* When a user navigates to the terminal page for an environment, they are served
+ a JavaScript application that opens a WebSocket connection back to GitLab.
+* The WebSocket is handled in [Workhorse](https://gitlab.com/gitlab-org/gitlab-workhorse),
+ rather than the Rails application server.
+* Workhorse queries Rails for connection details and user permissions; Rails
+ queries Kubernetes for them in the background, using [Sidekiq](../troubleshooting/sidekiq.md)
+* Workhorse acts as a proxy server between the user's browser and the Kubernetes
+ API, passing WebSocket frames between the two.
+* Workhorse regularly polls Rails, terminating the WebSocket connection if the
+ user no longer has permission to access the terminal, or if the connection
+ details have changed.
+
+## Enabling and disabling terminal support
+
+As web terminals use WebSockets, every HTTP/HTTPS reverse proxy in front of
+Workhorse needs to be configured to pass the `Connection` and `Upgrade` headers
+through to the next one in the chain. If you installed Gitlab using Omnibus, or
+from source, starting with GitLab 8.15, this should be done by the default
+configuration, so there's no need for you to do anything.
+
+However, if you run a [load balancer](../high_availability/load_balancer.md) in
+front of GitLab, you may need to make some changes to your configuration. These
+guides document the necessary steps for a selection of popular reverse proxies:
+
+* [Apache](https://httpd.apache.org/docs/2.4/mod/mod_proxy_wstunnel.html)
+* [NGINX](https://www.nginx.com/blog/websocket-nginx/)
+* [HAProxy](http://blog.haproxy.com/2012/11/07/websockets-load-balancing-with-haproxy/)
+* [Varnish](https://www.varnish-cache.org/docs/4.1/users-guide/vcl-example-websockets.html)
+
+Workhorse won't let WebSocket requests through to non-WebSocket endpoints, so
+it's safe to enable support for these headers globally. If you'd rather had a
+narrower set of rules, you can restrict it to URLs ending with `/terminal.ws`
+(although this may still have a few false positives).
+
+If you installed from source, or have made any configuration changes to your
+Omnibus installation before upgrading to 8.15, you may need to make some
+changes to your configuration. See the [8.14 to 8.15 upgrade](../../update/8.14-to-8.15.md#nginx-configuration)
+document for more details.
+
+If you'd like to disable web terminal support in GitLab, just stop passing
+the `Connection` and `Upgrade` hop-by-hop headers in the *first* HTTP reverse
+proxy in the chain. For most users, this will be the NGINX server bundled with
+Omnibus Gitlab, in which case, you need to:
+
+* Find the `nginx['proxy_set_headers']` section of your `gitlab.rb` file
+* Ensure the whole block is uncommented, and then comment out or remove the
+ `Connection` and `Upgrade` lines.
+
+For your own load balancer, just reverse the configuration changes recommended
+by the above guides.
+
+When these headers are not passed through, Workhorse will return a
+`400 Bad Request` response to users attempting to use a web terminal. In turn,
+they will receive a `Connection failed` message.
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index bcf8b955044..727617f1ecc 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -2,7 +2,8 @@
## List repository tree
-Get a list of repository files and directories in a project.
+Get a list of repository files and directories in a project. This endpoint can
+be accessed without authentication if the repository is publicly accessible.
```
GET /projects/:id/repository/tree
@@ -71,7 +72,8 @@ Parameters:
## Raw file content
-Get the raw file contents for a file by commit SHA and path.
+Get the raw file contents for a file by commit SHA and path. This endpoint can
+be accessed without authentication if the repository is publicly accessible.
```
GET /projects/:id/repository/blobs/:sha
@@ -85,7 +87,8 @@ Parameters:
## Raw blob content
-Get the raw file contents for a blob by blob SHA.
+Get the raw file contents for a blob by blob SHA. This endpoint can be accessed
+without authentication if the repository is publicly accessible.
```
GET /projects/:id/repository/raw_blobs/:sha
@@ -98,7 +101,8 @@ Parameters:
## Get file archive
-Get an archive of the repository
+Get an archive of the repository. This endpoint can be accessed without
+authentication if the repository is publicly accessible.
```
GET /projects/:id/repository/archive
@@ -111,6 +115,9 @@ Parameters:
## Compare branches, tags or commits
+This endpoint can be accessed without authentication if the repository is
+publicly accessible.
+
```
GET /projects/:id/repository/compare
```
@@ -163,7 +170,8 @@ Response:
## Contributors
-Get repository contributors list
+Get repository contributors list. This endpoint can be accessed without
+authentication if the repository is publicly accessible.
```
GET /projects/:id/repository/contributors
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index b8c9eb2c9a8..8a6baed5987 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -6,7 +6,9 @@
## Get file from repository
-Allows you to receive information about file in repository like name, size, content. Note that file content is Base64 encoded.
+Allows you to receive information about file in repository like name, size,
+content. Note that file content is Base64 encoded. This endpoint can be accessed
+without authentication if the repository is publicly accessible.
```
GET /projects/:id/repository/files
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 73bd2516d46..6a9495f8892 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -23,6 +23,7 @@
- [CI/CD pipelines settings](../user/project/pipelines/settings.md)
- [Review Apps](review_apps/index.md)
- [Git submodules](git_submodules.md) Using Git submodules in your CI jobs
+- [Autodeploy](autodeploy/index.md)
## Breaking changes
diff --git a/doc/ci/autodeploy/img/autodeploy_button.png b/doc/ci/autodeploy/img/autodeploy_button.png
new file mode 100644
index 00000000000..9e2cd57a0ba
--- /dev/null
+++ b/doc/ci/autodeploy/img/autodeploy_button.png
Binary files differ
diff --git a/doc/ci/autodeploy/img/autodeploy_dropdown.png b/doc/ci/autodeploy/img/autodeploy_dropdown.png
new file mode 100644
index 00000000000..1486a8ec0ea
--- /dev/null
+++ b/doc/ci/autodeploy/img/autodeploy_dropdown.png
Binary files differ
diff --git a/doc/ci/autodeploy/index.md b/doc/ci/autodeploy/index.md
new file mode 100644
index 00000000000..503a00969d5
--- /dev/null
+++ b/doc/ci/autodeploy/index.md
@@ -0,0 +1,39 @@
+# Autodeploy
+
+> [Introduced][mr-8135] in GitLab 8.15.
+
+Autodeploy is an easy way to configure GitLab CI for the deployment of your
+application. GitLab Community maintains a list of `.gitlab-ci.yml`
+templates for various infrastructure providers and deployment scripts
+powering them. These scripts are responsible for packaging your application,
+setting up the infrastructure and spinning up necessary services (for
+example a database).
+
+You can use [project services][project-services] to store credentials to
+your infrastructure provider and they will be available during the
+deployment.
+
+## Supported templates
+
+The list of supported autodeploy templates is available [here][autodeploy-templates].
+
+## Configuration
+
+1. Enable a deployment [project service][project-services] to store your
+credentials. For example, if you want to deploy to a Kubernetes cluster
+you have to enable [Kubernetes service][kubernetes-service].
+1. Configure GitLab Runner to use [docker-in-docker executor][docker-in-docker].
+1. Navigate to the "Project" tab and click "Set up autodeploy" button.
+ ![Autodeploy button](img/autodeploy_button.png)
+1. Select a template.
+ ![Dropdown with autodeploy templates](img/autodeploy_dropdown.png)
+1. Commit your changes and create a merge request.
+1. Test your deployment configuration using a [Review App][review-app] that was
+created automatically for you.
+
+[mr-8135]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8135
+[project-services]: ../../project_services/project_services.md
+[autodeploy-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml/tree/master/autodeploy
+[kubernetes-service]: ../../project_services/kubernetes.md
+[docker-in-docker]: ../docker/using_docker_build.md#use-docker-in-docker-executor
+[review-app]: ../review_apps/index.md
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index bad0233a9ce..98cd29c9567 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -25,7 +25,9 @@ Environments are like tags for your CI jobs, describing where code gets deployed
Deployments are created when [jobs] deploy versions of code to environments,
so every environment can have one or more deployments. GitLab keeps track of
your deployments, so you always know what is currently being deployed on your
-servers.
+servers. If you have a deployment service such as [Kubernetes][kubernetes-service]
+enabled for your project, you can use it to assist with your deployments, and
+can even access a web terminal for your environment from within GitLab!
To better understand how environments and deployments work, let's consider an
example. We assume that you have already created a project in GitLab and set up
@@ -233,6 +235,46 @@ Remember that if your environment's name is `production` (all lowercase), then
it will get recorded in [Cycle Analytics](../user/project/cycle_analytics.md).
Double the benefit!
+## Web terminals
+
+>**Note:**
+Web terminals were added in GitLab 8.15 and are only available to project
+masters and owners.
+
+If you deploy to your environments with the help of a deployment service (e.g.,
+the [Kubernetes](../project_services/kubernetes.md) service), GitLab can open
+a terminal session to your environment! This is a very powerful feature that
+allows you to debug issues without leaving the comfort of your web browser. To
+enable it, just follow the instructions given in the service documentation.
+
+Once enabled, your environments will gain a "terminal" button:
+
+![Terminal button on environment index](img/environments_terminal_button_on_index.png)
+
+You can also access the terminal button from the page for a specific environment:
+
+![Terminal button for an environment](img/environments_terminal_button_on_show.png)
+
+Wherever you find it, clicking the button will take you to a separate page to
+establish the terminal session:
+
+![Terminal page](img/environments_terminal_page.png)
+
+This works just like any other terminal - you'll be in the container created
+by your deployment, so you can run shell commands and get responses in real
+time, check the logs, try out configuration or code tweaks, etc. You can open
+multiple terminals to the same environment - they each get their own shell
+session - and even a multiplexer like `screen` or `tmux`!
+
+>**Note:**
+Container-based deployments often lack basic tools (like an editor), and may
+be stopped or restarted at any time. If this happens, you will lose all your
+changes! Treat this as a debugging tool, not a comprehensive online IDE. You
+can use [Koding](../administration/integration/koding.md) for online
+development.
+
+---
+
While this is fine for deploying to some stable environments like staging or
production, what happens for branches? So far we haven't defined anything
regarding deployments for branches other than `master`. Dynamic environments
@@ -524,6 +566,7 @@ Below are some links you may find interesting:
[Pipelines]: pipelines.md
[jobs]: yaml/README.md#jobs
[yaml]: yaml/README.md
+[kubernetes-service]: ../project_services/kubernetes.md]
[environments]: #environments
[deployments]: #deployments
[permissions]: ../user/permissions.md
diff --git a/doc/ci/img/environments_terminal_button_on_index.png b/doc/ci/img/environments_terminal_button_on_index.png
new file mode 100644
index 00000000000..6f05b2aa343
--- /dev/null
+++ b/doc/ci/img/environments_terminal_button_on_index.png
Binary files differ
diff --git a/doc/ci/img/environments_terminal_button_on_show.png b/doc/ci/img/environments_terminal_button_on_show.png
new file mode 100644
index 00000000000..9469fab99ab
--- /dev/null
+++ b/doc/ci/img/environments_terminal_button_on_show.png
Binary files differ
diff --git a/doc/ci/img/environments_terminal_page.png b/doc/ci/img/environments_terminal_page.png
new file mode 100644
index 00000000000..fde1bf325a6
--- /dev/null
+++ b/doc/ci/img/environments_terminal_page.png
Binary files differ
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index 03b9c4bb444..f91b9d350f7 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -36,6 +36,37 @@ Clicking on a pipeline will show the builds that were run for that pipeline.
Clicking on an individual build will show you its build trace, and allow you to
cancel the build, retry it, or erase the build trace.
+## How the pipeline duration is calculated
+
+Total running time for a given pipeline would exclude retries and pending
+(queue) time. We could reduce this problem down to finding the union of
+periods.
+
+So each job would be represented as a `Period`, which consists of
+`Period#first` as when the job started and `Period#last` as when the
+job was finished. A simple example here would be:
+
+* A (1, 3)
+* B (2, 4)
+* C (6, 7)
+
+Here A begins from 1, and ends to 3. B begins from 2, and ends to 4.
+C begins from 6, and ends to 7. Visually it could be viewed as:
+
+```
+0 1 2 3 4 5 6 7
+ AAAAAAA
+ BBBBBBB
+ CCCC
+```
+
+The union of A, B, and C would be (1, 4) and (6, 7), therefore the
+total running time should be:
+
+```
+(4 - 1) + (7 - 6) => 4
+```
+
## Badges
Build status and test coverage report badges are available. You can find their
diff --git a/doc/integration/README.md b/doc/integration/README.md
index f8ffa6dcb7f..ed843c0bfa9 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -8,7 +8,7 @@ See the documentation below for details on how to configure these services.
- [JIRA](../project_services/jira.md) Integrate with the JIRA issue tracker
- [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc.
- [LDAP](ldap.md) Set up sign in via LDAP
-- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd and Azure
+- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd, Azure and Authentiq ID
- [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider
- [CAS](cas.md) Configure GitLab to sign in using CAS
- [OAuth2 provider](oauth_provider.md) OAuth2 application creation
diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md
index 1dfc985eaea..2a14c0397ca 100644
--- a/doc/integration/bitbucket.md
+++ b/doc/integration/bitbucket.md
@@ -40,9 +40,13 @@ you to use.
| :--- | :---------- |
| **Name** | This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive. |
| **Application description** | Fill this in if you wish. |
- | **Callback URL** | Leave blank. |
+ | **Callback URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. |
| **URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. |
+ NOTE: Starting in GitLab 8.15, you MUST specify a callback URL, or you will
+ see an "Invalid redirect_uri" message. For more details, see [the
+ Bitbucket documentation](https://confluence.atlassian.com/bitbucket/oauth-faq-338365710.html).
+
And grant at least the following permissions:
```
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index 8a55fce96fe..4c933cef9b7 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -30,6 +30,7 @@ contains some settings that are common for all providers.
- [Crowd](crowd.md)
- [Azure](azure.md)
- [Auth0](auth0.md)
+- [Authentiq](../administration/auth/authentiq.md)
## Initial OmniAuth Configuration
diff --git a/doc/project_services/kubernetes.md b/doc/project_services/kubernetes.md
index fda364b864e..59d5da702f8 100644
--- a/doc/project_services/kubernetes.md
+++ b/doc/project_services/kubernetes.md
@@ -47,3 +47,17 @@ GitLab CI build environment:
- `KUBE_TOKEN`
- `KUBE_NAMESPACE`
- `KUBE_CA_PEM` - only if a custom CA bundle was specified
+
+## Web terminals
+
+>**NOTE:**
+Added in GitLab 8.15. You must be the project owner or have `master` permissions
+to use terminals. Support is currently limited to the first container in the
+first pod of your environment.
+
+When enabled, the Kubernetes service adds [web terminal](../ci/environments.md#web-terminals)
+support to your environments. This is based on the `exec` functionality found in
+Docker and Kubernetes, so you get a new shell session within your existing
+containers. To use this integration, you should deploy to Kubernetes using
+the deployment variables above, ensuring any pods you create are labelled with
+`app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest!
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 39fe2409a29..5ada8748d85 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -33,6 +33,7 @@ The following table depicts the various user permission levels in a project.
| See a container registry | | ✓ | ✓ | ✓ | ✓ |
| See environments | | ✓ | ✓ | ✓ | ✓ |
| Create new environments | | | ✓ | ✓ | ✓ |
+| Use environment terminals | | | | ✓ | ✓ |
| Stop environments | | | ✓ | ✓ | ✓ |
| See a list of merge requests | | ✓ | ✓ | ✓ | ✓ |
| Manage/Accept merge requests | | | ✓ | ✓ | ✓ |
diff --git a/doc/workflow/importing/README.md b/doc/workflow/importing/README.md
index 18e5d950866..2d91bee0e94 100644
--- a/doc/workflow/importing/README.md
+++ b/doc/workflow/importing/README.md
@@ -4,6 +4,7 @@
1. [GitHub](import_projects_from_github.md)
1. [GitLab.com](import_projects_from_gitlab_com.md)
1. [FogBugz](import_projects_from_fogbugz.md)
+1. [Gitea](import_projects_from_gitea.md)
1. [SVN](migrating_from_svn.md)
In addition to the specific migration documentation above, you can import any
@@ -14,4 +15,3 @@ repository is too large the import can timeout.
You can copy your repos by changing the remote and pushing to the new server;
but issues and merge requests can't be imported.
-
diff --git a/doc/workflow/importing/img/import_projects_from_gitea_new_import.png b/doc/workflow/importing/img/import_projects_from_gitea_new_import.png
new file mode 100644
index 00000000000..a3f603cbd0a
--- /dev/null
+++ b/doc/workflow/importing/img/import_projects_from_gitea_new_import.png
Binary files differ
diff --git a/doc/workflow/importing/import_projects_from_gitea.md b/doc/workflow/importing/import_projects_from_gitea.md
new file mode 100644
index 00000000000..936cee89f45
--- /dev/null
+++ b/doc/workflow/importing/import_projects_from_gitea.md
@@ -0,0 +1,80 @@
+# Import your project from Gitea to GitLab
+
+Import your projects from Gitea to GitLab with minimal effort.
+
+## Overview
+
+>**Note:**
+As of Gitea `v1.0.0`, issue & pull-request comments cannot be imported! This is
+a [known issue][issue-401] that should be fixed in a near-future.
+
+- At its current state, Gitea importer can import:
+ - the repository description (GitLab 8.15+)
+ - the Git repository data (GitLab 8.15+)
+ - the issues (GitLab 8.15+)
+ - the pull requests (GitLab 8.15+)
+ - the milestones (GitLab 8.15+)
+ - the labels (GitLab 8.15+)
+- Repository public access is retained. If a repository is private in Gitea
+ it will be created as private in GitLab as well.
+
+## How it works
+
+Since Gitea is currently not an OAuth provider, author/assignee cannot be mapped
+to users in your GitLab's instance. This means that the project creator (most of
+the times the current user that started the import process) is set as the author,
+but a reference on the issue about the original Gitea author is kept.
+
+The importer will create any new namespaces (groups) if they don't exist or in
+the case the namespace is taken, the repository will be imported under the user's
+namespace that started the import process.
+
+## Importing your Gitea repositories
+
+The importer page is visible when you create a new project.
+
+![New project page on GitLab](img/import_projects_from_new_project_page.png)
+
+Click on the **Gitea** link and the import authorization process will start.
+
+![New Gitea project import](img/import_projects_from_gitea_new_import.png)
+
+### Authorize access to your repositories using a personal access token
+
+With this method, you will perform a one-off authorization with Gitea to grant
+GitLab access your repositories:
+
+1. Go to <https://you-gitea-instance/user/settings/applications> (replace
+ `you-gitea-instance` with the host of your Gitea instance).
+1. Click **Generate New Token**.
+1. Enter a token description.
+1. Click **Generate Token**.
+1. Copy the token hash.
+1. Go back to GitLab and provide the token to the Gitea importer.
+1. Hit the **List Your Gitea Repositories** button and wait while GitLab reads
+ your repositories' information. Once done, you'll be taken to the importer
+ page to select the repositories to import.
+
+### Select which repositories to import
+
+After you've authorized access to your Gitea repositories, you will be
+redirected to the Gitea importer page.
+
+From there, you can see the import statuses of your Gitea repositories.
+
+- Those that are being imported will show a _started_ status,
+- those already successfully imported will be green with a _done_ status,
+- whereas those that are not yet imported will have an **Import** button on the
+ right side of the table.
+
+If you want, you can import all your Gitea projects in one go by hitting
+**Import all projects** in the upper left corner.
+
+![Gitea importer page](img/import_projects_from_github_importer.png)
+
+---
+
+You can also choose a different name for the project and a different namespace,
+if you have the privileges to do so.
+
+[issue-401]: https://github.com/go-gitea/gitea/issues/401
diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md
index b3660aa8030..86a016fc6d6 100644
--- a/doc/workflow/importing/import_projects_from_github.md
+++ b/doc/workflow/importing/import_projects_from_github.md
@@ -6,8 +6,9 @@ Import your projects from GitHub to GitLab with minimal effort.
>**Note:**
If you are an administrator you can enable the [GitHub integration][gh-import]
-in your GitLab instance sitewide. This configuration is optional, users will be
-able import their GitHub repositories with a [personal access token][gh-token].
+in your GitLab instance sitewide. This configuration is optional, users will
+still be able to import their GitHub repositories with a
+[personal access token][gh-token].
- At its current state, GitHub importer can import:
- the repository description (GitLab 7.7+)
@@ -85,7 +86,7 @@ authorization with GitHub to grant GitLab access your repositories:
1. Click **Generate token**.
1. Copy the token hash.
1. Go back to GitLab and provide the token to the GitHub importer.
-1. Hit the **List your GitHub repositories** button and wait while GitLab reads
+1. Hit the **List Your GitHub Repositories** button and wait while GitLab reads
your repositories' information. Once done, you'll be taken to the importer
page to select the repositories to import.
@@ -112,7 +113,6 @@ You can also choose a different name for the project and a different namespace,
if you have the privileges to do so.
[gh-import]: ../../integration/github.md "GitHub integration"
-[new-project]: ../../gitlab-basics/create-project.md "How to create a new project in GitLab"
[gh-integration]: #authorize-access-to-your-repositories-using-the-github-integration
[gh-token]: #authorize-access-to-your-repositories-using-a-personal-access-token
[social sign-in]: ../../profile/account/social_sign_in.md
diff --git a/features/admin/projects.feature b/features/admin/projects.feature
deleted file mode 100644
index 8929bcf8d80..00000000000
--- a/features/admin/projects.feature
+++ /dev/null
@@ -1,47 +0,0 @@
-@admin
-Feature: Admin Projects
- Background:
- Given I sign in as an admin
- And there are projects in system
-
- Scenario: I should see non-archived projects in the list
- Given archived project "Archive"
- When I visit admin projects page
- Then I should see all non-archived projects
- And I should not see project "Archive"
-
- @javascript
- Scenario: I should see all projects in the list
- Given archived project "Archive"
- When I visit admin projects page
- And I select "Show archived projects"
- Then I should see all projects
- And I should see "archived" label
-
- Scenario: Projects show
- When I visit admin projects page
- And I click on first project
- Then I should see project details
-
- @javascript
- Scenario: Transfer project
- Given group 'Web'
- And I visit admin project page
- When I transfer project to group 'Web'
- Then I should see project transfered
-
- @javascript
- Scenario: Signed in admin should be able to add himself to a project
- Given "John Doe" owns private project "Enterprise"
- When I visit project "Enterprise" members page
- When I select current user as "Developer"
- Then I should see current user as "Developer"
-
- @javascript
- Scenario: Signed in admin should be able to remove himself from a project
- Given "John Doe" owns private project "Enterprise"
- And current user is developer of project "Enterprise"
- When I visit project "Enterprise" members page
- Then I should see current user as "Developer"
- When I click on the "Remove User From Project" button for current user
- Then I should not see current user as "Developer"
diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature
index 1776c07e60e..3459cce03f9 100644
--- a/features/project/commits/commits.feature
+++ b/features/project/commits/commits.feature
@@ -47,8 +47,6 @@ Feature: Project Commits
And repository contains ".gitlab-ci.yml" file
When I click on commit link
Then I see commit ci info
- And I click status link
- Then I see builds list
Scenario: I browse commit with side-by-side diff view
Given I click on commit link
diff --git a/features/project/service.feature b/features/project/service.feature
index 3a7b8308524..cce5f58adec 100644
--- a/features/project/service.feature
+++ b/features/project/service.feature
@@ -37,11 +37,11 @@ Feature: Project Services
And I fill Assembla settings
Then I should see Assembla service settings saved
- Scenario: Activate Slack service
+ Scenario: Activate Slack notifications service
When I visit project "Shop" services page
- And I click Slack service link
- And I fill Slack settings
- Then I should see Slack service settings saved
+ And I click Slack notifications service link
+ And I fill Slack notifications settings
+ Then I should see Slack Notifications service settings saved
Scenario: Activate Pushover service
When I visit project "Shop" services page
diff --git a/features/steps/admin/projects.rb b/features/steps/admin/projects.rb
deleted file mode 100644
index 2b8cd030ace..00000000000
--- a/features/steps/admin/projects.rb
+++ /dev/null
@@ -1,104 +0,0 @@
-class Spinach::Features::AdminProjects < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedAdmin
- include SharedProject
- include SharedUser
- include Select2Helper
-
- step 'I should see all non-archived projects' do
- Project.non_archived.each do |p|
- expect(page).to have_content p.name_with_namespace
- end
- end
-
- step 'I should see all projects' do
- Project.all.each do |p|
- expect(page).to have_content p.name_with_namespace
- end
- end
-
- step 'I select "Show archived projects"' do
- find(:css, '#sort-projects-dropdown').click
- click_link 'Show archived projects'
- end
-
- step 'I should see "archived" label' do
- expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived')
- end
-
- step 'I click on first project' do
- click_link Project.first.name_with_namespace
- end
-
- step 'I should see project details' do
- project = Project.first
- expect(current_path).to eq admin_namespace_project_path(project.namespace, project)
- expect(page).to have_content(project.name_with_namespace)
- expect(page).to have_content(project.creator.name)
- end
-
- step 'I visit admin project page' do
- visit admin_namespace_project_path(project.namespace, project)
- end
-
- step 'I transfer project to group \'Web\'' do
- allow_any_instance_of(Projects::TransferService).
- to receive(:move_uploads_to_new_namespace).and_return(true)
- click_button 'Search for Namespace'
- click_link 'group: web'
- click_button 'Transfer'
- end
-
- step 'group \'Web\'' do
- create(:group, name: 'Web')
- end
-
- step 'I should see project transfered' do
- expect(page).to have_content 'Web / ' + project.name
- expect(page).to have_content 'Namespace: Web'
- end
-
- step 'I visit project "Enterprise" members page' do
- project = Project.find_by!(name: "Enterprise")
- visit namespace_project_project_members_path(project.namespace, project)
- end
-
- step 'I select current user as "Developer"' do
- page.within ".users-project-form" do
- select2(current_user.id, from: "#user_ids", multiple: true)
- select "Developer", from: "access_level"
- end
-
- click_button "Add to project"
- end
-
- step 'I should see current user as "Developer"' do
- page.within '.content-list' do
- expect(page).to have_content(current_user.name)
- expect(page).to have_content('Developer')
- end
- end
-
- step 'current user is developer of project "Enterprise"' do
- project = Project.find_by!(name: "Enterprise")
- project.team << [current_user, :developer]
- end
-
- step 'I click on the "Remove User From Project" button for current user' do
- find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click
- # poltergeist always confirms popups.
- end
-
- step 'I should not see current_user as "Developer"' do
- expect(page).not_to have_selector(:css, '.content-list')
- end
-
- def project
- @project ||= Project.first
- end
-
- def group
- Group.find_by(name: 'Web')
- end
-end
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
index 007dfb67a77..18e267294e4 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -166,15 +166,6 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
expect(page).to have_content "Pipeline #1 for 570e7b2a pending"
end
- step 'I click status link' do
- find('.commit-ci-menu').click_link "Builds"
- end
-
- step 'I see builds list' do
- expect(page).to have_content "Pipeline #1 for 570e7b2a pending"
- expect(page).to have_content "1 build"
- end
-
step 'I search "submodules" commits' do
fill_in 'commits-search', with: 'submodules'
end
diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb
index bd6466f3686..a4d29770922 100644
--- a/features/steps/project/services.rb
+++ b/features/steps/project/services.rb
@@ -137,17 +137,17 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
expect(find_field('Colorize messages').value).to eq '1'
end
- step 'I click Slack service link' do
- click_link 'Slack'
+ step 'I click Slack notifications service link' do
+ click_link 'Slack notifications'
end
- step 'I fill Slack settings' do
+ step 'I fill Slack notifications settings' do
check 'Active'
fill_in 'Webhook', with: 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685'
click_button 'Save'
end
- step 'I should see Slack service settings saved' do
+ step 'I should see Slack Notifications service settings saved' do
expect(find_field('Webhook').value).to eq 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685'
end
diff --git a/lib/api/files.rb b/lib/api/files.rb
index 28f306e45f3..532a317c89e 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -1,8 +1,6 @@
module API
# Projects API
class Files < Grape::API
- before { authenticate! }
-
helpers do
def commit_params(attrs)
{
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index c287ee34a68..4ca6646a6f1 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -2,7 +2,6 @@ require 'mime/types'
module API
class Repositories < Grape::API
- before { authenticate! }
before { authorize! :download_code, user_project }
params do
@@ -79,8 +78,6 @@ module API
optional :format, type: String, desc: 'The archive format'
end
get ':id/repository/archive', requirements: { format: Gitlab::Regex.archive_formats_regex } do
- authorize! :download_code, user_project
-
begin
send_git_archive user_project.repository, ref: params[:sha], format: params[:format]
rescue
@@ -96,7 +93,6 @@ module API
requires :to, type: String, desc: 'The commit, branch name, or tag name to stop comparison'
end
get ':id/repository/compare' do
- authorize! :download_code, user_project
compare = Gitlab::Git::Compare.new(user_project.repository.raw_repository, params[:from], params[:to])
present compare, with: Entities::Compare
end
@@ -105,8 +101,6 @@ module API
success Entities::Contributor
end
get ':id/repository/contributors' do
- authorize! :download_code, user_project
-
begin
present user_project.repository.contributors,
with: Entities::Contributor
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 59232c84c24..d11cdce4e18 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -378,7 +378,6 @@ module API
desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)'
},
],
-
'mattermost-slash-commands' => [
{
required: true,
@@ -387,6 +386,14 @@ module API
desc: 'The Mattermost token'
}
],
+ 'slack-slash-commands' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Slack token'
+ }
+ ],
'pipelines-email' => [
{
required: true,
@@ -473,7 +480,7 @@ module API
desc: 'The description of the tracker'
}
],
- 'slack-notification' => [
+ 'slack' => [
{
required: true,
name: :webhook,
@@ -493,7 +500,7 @@ module API
desc: 'The channel name'
}
],
- 'mattermost-notification' => [
+ 'mattermost' => [
{
required: true,
name: :webhook,
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 0842c3874c5..4c22287b5c6 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -94,7 +94,7 @@ module API
identity_attrs = params.slice(:provider, :extern_uid)
confirm = params.delete(:confirm)
- user = User.build_user(declared_params(include_missing: false))
+ user = User.new(declared_params(include_missing: false))
user.skip_confirmation! unless confirm
if identity_attrs.any?
diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb
index 2f19b59e725..d67d466bce8 100644
--- a/lib/banzai/filter/external_link_filter.rb
+++ b/lib/banzai/filter/external_link_filter.rb
@@ -10,7 +10,7 @@ module Banzai
node.set_attribute('href', href)
end
- if href =~ /\Ahttp(s)?:\/\// && external_url?(href)
+ if href =~ %r{\A(https?:)?//[^/]} && external_url?(href)
node.set_attribute('rel', 'nofollow noreferrer')
node.set_attribute('target', '_blank')
end
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index ed87a2603e8..142bce82286 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -41,7 +41,7 @@ module Ci
put ":id" do
authenticate_runner!
build = Ci::Build.where(runner_id: current_runner.id).running.find(params[:id])
- forbidden!('Build has been erased!') if build.erased?
+ validate_build!(build)
update_runner_info
@@ -71,9 +71,7 @@ module Ci
# PATCH /builds/:id/trace.txt
patch ":id/trace.txt" do
build = Ci::Build.find_by_id(params[:id])
- not_found! unless build
- authenticate_build_token!(build)
- forbidden!('Build has been erased!') if build.erased?
+ authenticate_build!(build)
error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range')
content_range = request.headers['Content-Range']
@@ -104,8 +102,7 @@ module Ci
Gitlab::Workhorse.verify_api_request!(headers)
not_allowed! unless Gitlab.config.artifacts.enabled
build = Ci::Build.find_by_id(params[:id])
- not_found! unless build
- authenticate_build_token!(build)
+ authenticate_build!(build)
forbidden!('build is not running') unless build.running?
if params[:filesize]
@@ -142,10 +139,8 @@ module Ci
require_gitlab_workhorse!
not_allowed! unless Gitlab.config.artifacts.enabled
build = Ci::Build.find_by_id(params[:id])
- not_found! unless build
- authenticate_build_token!(build)
+ authenticate_build!(build)
forbidden!('Build is not running!') unless build.running?
- forbidden!('Build has been erased!') if build.erased?
artifacts_upload_path = ArtifactUploader.artifacts_upload_path
artifacts = uploaded_file(:file, artifacts_upload_path)
@@ -176,8 +171,7 @@ module Ci
# GET /builds/:id/artifacts
get ":id/artifacts" do
build = Ci::Build.find_by_id(params[:id])
- not_found! unless build
- authenticate_build_token!(build)
+ authenticate_build!(build)
artifacts_file = build.artifacts_file
unless artifacts_file.file_storage?
@@ -202,8 +196,7 @@ module Ci
# DELETE /builds/:id/artifacts
delete ":id/artifacts" do
build = Ci::Build.find_by_id(params[:id])
- not_found! unless build
- authenticate_build_token!(build)
+ authenticate_build!(build)
build.erase_artifacts!
end
diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb
index e608f5f6cad..5ff25a3a9b2 100644
--- a/lib/ci/api/helpers.rb
+++ b/lib/ci/api/helpers.rb
@@ -13,8 +13,19 @@ module Ci
forbidden! unless current_runner
end
- def authenticate_build_token!(build)
- forbidden! unless build_token_valid?(build)
+ def authenticate_build!(build)
+ validate_build!(build) do
+ forbidden! unless build_token_valid?(build)
+ end
+ end
+
+ def validate_build!(build)
+ not_found! unless build
+
+ yield if block_given?
+
+ forbidden!('Project has been deleted!') unless build.project
+ forbidden!('Build has been erased!') if build.erased?
end
def runner_registration_token_valid?
@@ -49,7 +60,7 @@ module Ci
end
def build_not_found!
- if headers['User-Agent'].match(/gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /)
+ if headers['User-Agent'].to_s.match(/gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /)
no_content!
else
not_found!
diff --git a/lib/gitlab/chat_commands/base_command.rb b/lib/gitlab/chat_commands/base_command.rb
index 25da8474e95..4fe53ce93a9 100644
--- a/lib/gitlab/chat_commands/base_command.rb
+++ b/lib/gitlab/chat_commands/base_command.rb
@@ -42,6 +42,10 @@ module Gitlab
def find_by_iid(iid)
collection.find_by(iid: iid)
end
+
+ def presenter
+ Gitlab::ChatCommands::Presenter.new
+ end
end
end
end
diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb
index b0d3fdbc48a..145086755e4 100644
--- a/lib/gitlab/chat_commands/command.rb
+++ b/lib/gitlab/chat_commands/command.rb
@@ -22,8 +22,6 @@ module Gitlab
end
end
- private
-
def match_command
match = nil
service = available_commands.find do |klass|
@@ -33,6 +31,8 @@ module Gitlab
[service, match]
end
+ private
+
def help_messages
available_commands.map(&:help_message)
end
@@ -48,15 +48,15 @@ module Gitlab
end
def help(messages)
- Mattermost::Presenter.help(messages, params[:command])
+ presenter.help(messages, params[:command])
end
def access_denied
- Mattermost::Presenter.access_denied
+ presenter.access_denied
end
def present(resource)
- Mattermost::Presenter.present(resource)
+ presenter.present(resource)
end
end
end
diff --git a/lib/gitlab/chat_commands/deploy.rb b/lib/gitlab/chat_commands/deploy.rb
index 0eed1fce0dc..6bb854dc080 100644
--- a/lib/gitlab/chat_commands/deploy.rb
+++ b/lib/gitlab/chat_commands/deploy.rb
@@ -4,7 +4,7 @@ module Gitlab
include Gitlab::Routing.url_helpers
def self.match(text)
- /\Adeploy\s+(?<from>.*)\s+to+\s+(?<to>.*)\z/.match(text)
+ /\Adeploy\s+(?<from>\S+.*)\s+to+\s+(?<to>\S+.*)\z/.match(text)
end
def self.help_message
diff --git a/lib/mattermost/presenter.rb b/lib/gitlab/chat_commands/presenter.rb
index 67eda983a74..caceaa25391 100644
--- a/lib/mattermost/presenter.rb
+++ b/lib/gitlab/chat_commands/presenter.rb
@@ -1,7 +1,7 @@
-module Mattermost
- class Presenter
- class << self
- include Gitlab::Routing.url_helpers
+module Gitlab
+ module ChatCommands
+ class Presenter
+ include Gitlab::Routing
def authorize_chat_name(url)
message = if url
@@ -64,7 +64,7 @@ module Mattermost
def single_resource(resource)
return error(resource) if resource.errors.any? || !resource.persisted?
- message = "### #{title(resource)}"
+ message = "#{title(resource)}:"
message << "\n\n#{resource.description}" if resource.try(:description)
in_channel_response(message)
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index c6bb8f9c8ed..9d142f1b82e 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -45,7 +45,7 @@ module Gitlab
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
domain_whitelist: Settings.gitlab['domain_whitelist'],
- import_sources: %w[github bitbucket gitlab google_code fogbugz git gitlab_project],
+ import_sources: %w[gitea github bitbucket gitlab google_code fogbugz git gitlab_project],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb
index 25e9d619697..79dd0cf7df2 100644
--- a/lib/gitlab/git/rev_list.rb
+++ b/lib/gitlab/git/rev_list.rb
@@ -22,7 +22,7 @@ module Gitlab
def valid?
environment_variables.all? do |(name, value)|
- value.start_with?(project.repository.path_to_repo)
+ value.to_s.start_with?(project.repository.path_to_repo)
end
end
@@ -35,7 +35,7 @@ module Gitlab
end
def environment_variables
- @environment_variables ||= env.slice(*ALLOWED_VARIABLES)
+ @environment_variables ||= env.slice(*ALLOWED_VARIABLES).compact
end
end
end
diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb
index 6dbae64a9fe..95dba9a327b 100644
--- a/lib/gitlab/github_import/base_formatter.rb
+++ b/lib/gitlab/github_import/base_formatter.rb
@@ -15,6 +15,10 @@ module Gitlab
end
end
+ def url
+ raw_data.url || ''
+ end
+
private
def gitlab_user_id(github_id)
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index 85df6547a67..ba869faa92e 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -4,10 +4,12 @@ module Gitlab
GITHUB_SAFE_REMAINING_REQUESTS = 100
GITHUB_SAFE_SLEEP_TIME = 500
- attr_reader :access_token
+ attr_reader :access_token, :host, :api_version
- def initialize(access_token)
+ def initialize(access_token, host: nil, api_version: 'v3')
@access_token = access_token
+ @host = host.to_s.sub(%r{/+\z}, '')
+ @api_version = api_version
if access_token
::Octokit.auto_paginate = false
@@ -17,7 +19,7 @@ module Gitlab
def api
@api ||= ::Octokit::Client.new(
access_token: access_token,
- api_endpoint: github_options[:site],
+ api_endpoint: api_endpoint,
# If there is no config, we're connecting to github.com and we
# should verify ssl.
connection_options: {
@@ -64,6 +66,14 @@ module Gitlab
private
+ def api_endpoint
+ if host.present? && api_version.present?
+ "#{host}/api/#{api_version}"
+ else
+ github_options[:site]
+ end
+ end
+
def config
Gitlab.config.omniauth.providers.find { |provider| provider.name == "github" }
end
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index 281b65bdeba..ec1318ab33c 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -3,7 +3,7 @@ module Gitlab
class Importer
include Gitlab::ShellAdapter
- attr_reader :client, :errors, :project, :repo, :repo_url
+ attr_reader :errors, :project, :repo, :repo_url
def initialize(project)
@project = project
@@ -11,12 +11,27 @@ module Gitlab
@repo_url = project.import_url
@errors = []
@labels = {}
+ end
+
+ def client
+ return @client if defined?(@client)
+ unless credentials
+ raise Projects::ImportService::Error,
+ "Unable to find project import data credentials for project ID: #{@project.id}"
+ end
- if credentials
- @client = Client.new(credentials[:user])
- else
- raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}"
+ opts = {}
+ # Gitea plan to be GitHub compliant
+ if project.gitea_import?
+ uri = URI.parse(project.import_url)
+ host = "#{uri.scheme}://#{uri.host}:#{uri.port}#{uri.path}".sub(%r{/?[\w-]+/[\w-]+\.git\z}, '')
+ opts = {
+ host: host,
+ api_version: 'v1'
+ }
end
+
+ @client = Client.new(credentials[:user], opts)
end
def execute
@@ -35,7 +50,13 @@ module Gitlab
import_comments(:issues)
import_comments(:pull_requests)
import_wiki
- import_releases
+
+ # Gitea doesn't have a Release API yet
+ # See https://github.com/go-gitea/gitea/issues/330
+ unless project.gitea_import?
+ import_releases
+ end
+
handle_errors
true
@@ -44,7 +65,9 @@ module Gitlab
private
def credentials
- @credentials ||= project.import_data.credentials if project.import_data
+ return @credentials if defined?(@credentials)
+
+ @credentials = project.import_data ? project.import_data.credentials : nil
end
def handle_errors
@@ -60,9 +83,10 @@ module Gitlab
fetch_resources(:labels, repo, per_page: 100) do |labels|
labels.each do |raw|
begin
- LabelFormatter.new(project, raw).create!
+ gh_label = LabelFormatter.new(project, raw)
+ gh_label.create!
rescue => e
- errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(gh_label.url), errors: e.message }
end
end
end
@@ -74,9 +98,10 @@ module Gitlab
fetch_resources(:milestones, repo, state: :all, per_page: 100) do |milestones|
milestones.each do |raw|
begin
- MilestoneFormatter.new(project, raw).create!
+ gh_milestone = MilestoneFormatter.new(project, raw)
+ gh_milestone.create!
rescue => e
- errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(gh_milestone.url), errors: e.message }
end
end
end
@@ -97,7 +122,7 @@ module Gitlab
apply_labels(issuable, raw)
rescue => e
- errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(gh_issue.url), errors: e.message }
end
end
end
@@ -106,18 +131,23 @@ module Gitlab
def import_pull_requests
fetch_resources(:pull_requests, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests|
pull_requests.each do |raw|
- pull_request = PullRequestFormatter.new(project, raw)
- next unless pull_request.valid?
+ gh_pull_request = PullRequestFormatter.new(project, raw)
+ next unless gh_pull_request.valid?
begin
- restore_source_branch(pull_request) unless pull_request.source_branch_exists?
- restore_target_branch(pull_request) unless pull_request.target_branch_exists?
+ restore_source_branch(gh_pull_request) unless gh_pull_request.source_branch_exists?
+ restore_target_branch(gh_pull_request) unless gh_pull_request.target_branch_exists?
+
+ merge_request = gh_pull_request.create!
- pull_request.create!
+ # Gitea doesn't return PR in the Issue API endpoint, so labels must be assigned at this stage
+ if project.gitea_import?
+ apply_labels(merge_request, raw)
+ end
rescue => e
- errors << { type: :pull_request, url: Gitlab::UrlSanitizer.sanitize(pull_request.url), errors: e.message }
+ errors << { type: :pull_request, url: Gitlab::UrlSanitizer.sanitize(gh_pull_request.url), errors: e.message }
ensure
- clean_up_restored_branches(pull_request)
+ clean_up_restored_branches(gh_pull_request)
end
end
end
@@ -233,7 +263,7 @@ module Gitlab
gh_release = ReleaseFormatter.new(project, raw)
gh_release.create! if gh_release.valid?
rescue => e
- errors << { type: :release, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ errors << { type: :release, url: Gitlab::UrlSanitizer.sanitize(gh_release.url), errors: e.message }
end
end
end
diff --git a/lib/gitlab/github_import/issuable_formatter.rb b/lib/gitlab/github_import/issuable_formatter.rb
new file mode 100644
index 00000000000..256f360efc7
--- /dev/null
+++ b/lib/gitlab/github_import/issuable_formatter.rb
@@ -0,0 +1,60 @@
+module Gitlab
+ module GithubImport
+ class IssuableFormatter < BaseFormatter
+ def project_association
+ raise NotImplementedError
+ end
+
+ def number
+ raw_data.number
+ end
+
+ def find_condition
+ { iid: number }
+ end
+
+ private
+
+ def state
+ raw_data.state == 'closed' ? 'closed' : 'opened'
+ end
+
+ def assigned?
+ raw_data.assignee.present?
+ end
+
+ def assignee_id
+ if assigned?
+ gitlab_user_id(raw_data.assignee.id)
+ end
+ end
+
+ def author
+ raw_data.user.login
+ end
+
+ def author_id
+ gitlab_author_id || project.creator_id
+ end
+
+ def body
+ raw_data.body || ""
+ end
+
+ def description
+ if gitlab_author_id
+ body
+ else
+ formatter.author_line(author) + body
+ end
+ end
+
+ def milestone
+ if raw_data.milestone.present?
+ milestone = MilestoneFormatter.new(project, raw_data.milestone)
+ project.milestones.find_by(milestone.find_condition)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb
index 887690bcc7c..6f5ac4dac0d 100644
--- a/lib/gitlab/github_import/issue_formatter.rb
+++ b/lib/gitlab/github_import/issue_formatter.rb
@@ -1,6 +1,6 @@
module Gitlab
module GithubImport
- class IssueFormatter < BaseFormatter
+ class IssueFormatter < IssuableFormatter
def attributes
{
iid: number,
@@ -24,59 +24,9 @@ module Gitlab
:issues
end
- def find_condition
- { iid: number }
- end
-
- def number
- raw_data.number
- end
-
def pull_request?
raw_data.pull_request.present?
end
-
- private
-
- def assigned?
- raw_data.assignee.present?
- end
-
- def assignee_id
- if assigned?
- gitlab_user_id(raw_data.assignee.id)
- end
- end
-
- def author
- raw_data.user.login
- end
-
- def author_id
- gitlab_author_id || project.creator_id
- end
-
- def body
- raw_data.body || ""
- end
-
- def description
- if gitlab_author_id
- body
- else
- formatter.author_line(author) + body
- end
- end
-
- def milestone
- if raw_data.milestone.present?
- project.milestones.find_by(iid: raw_data.milestone.number)
- end
- end
-
- def state
- raw_data.state == 'closed' ? 'closed' : 'opened'
- end
end
end
end
diff --git a/lib/gitlab/github_import/milestone_formatter.rb b/lib/gitlab/github_import/milestone_formatter.rb
index 401dd962521..dd782eff059 100644
--- a/lib/gitlab/github_import/milestone_formatter.rb
+++ b/lib/gitlab/github_import/milestone_formatter.rb
@@ -3,7 +3,7 @@ module Gitlab
class MilestoneFormatter < BaseFormatter
def attributes
{
- iid: raw_data.number,
+ iid: number,
project: project,
title: raw_data.title,
description: raw_data.description,
@@ -19,7 +19,15 @@ module Gitlab
end
def find_condition
- { iid: raw_data.number }
+ { iid: number }
+ end
+
+ def number
+ if project.gitea_import?
+ raw_data.id
+ else
+ raw_data.number
+ end
end
private
diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb
index a2410068845..3f635be22ba 100644
--- a/lib/gitlab/github_import/project_creator.rb
+++ b/lib/gitlab/github_import/project_creator.rb
@@ -1,14 +1,15 @@
module Gitlab
module GithubImport
class ProjectCreator
- attr_reader :repo, :name, :namespace, :current_user, :session_data
+ attr_reader :repo, :name, :namespace, :current_user, :session_data, :type
- def initialize(repo, name, namespace, current_user, session_data)
+ def initialize(repo, name, namespace, current_user, session_data, type: 'github')
@repo = repo
@name = name
@namespace = namespace
@current_user = current_user
@session_data = session_data
+ @type = type
end
def execute
@@ -19,7 +20,7 @@ module Gitlab
description: repo.description,
namespace_id: namespace.id,
visibility_level: visibility_level,
- import_type: "github",
+ import_type: type,
import_source: repo.full_name,
import_url: import_url,
skip_wiki: skip_wiki
@@ -29,7 +30,7 @@ module Gitlab
private
def import_url
- repo.clone_url.sub('https://', "https://#{session_data[:github_access_token]}@")
+ repo.clone_url.sub('://', "://#{session_data[:github_access_token]}@")
end
def visibility_level
diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb
index b9a227fb11a..4ea0200e89b 100644
--- a/lib/gitlab/github_import/pull_request_formatter.rb
+++ b/lib/gitlab/github_import/pull_request_formatter.rb
@@ -1,6 +1,6 @@
module Gitlab
module GithubImport
- class PullRequestFormatter < BaseFormatter
+ class PullRequestFormatter < IssuableFormatter
delegate :exists?, :project, :ref, :repo, :sha, to: :source_branch, prefix: true
delegate :exists?, :project, :ref, :repo, :sha, to: :target_branch, prefix: true
@@ -28,14 +28,6 @@ module Gitlab
:merge_requests
end
- def find_condition
- { iid: number }
- end
-
- def number
- raw_data.number
- end
-
def valid?
source_branch.valid? && target_branch.valid?
end
@@ -60,57 +52,15 @@ module Gitlab
end
end
- def url
- raw_data.url
- end
-
private
- def assigned?
- raw_data.assignee.present?
- end
-
- def assignee_id
- if assigned?
- gitlab_user_id(raw_data.assignee.id)
- end
- end
-
- def author
- raw_data.user.login
- end
-
- def author_id
- gitlab_author_id || project.creator_id
- end
-
- def body
- raw_data.body || ""
- end
-
- def description
- if gitlab_author_id
- body
+ def state
+ if raw_data.state == 'closed' && raw_data.merged_at.present?
+ 'merged'
else
- formatter.author_line(author) + body
+ super
end
end
-
- def milestone
- if raw_data.milestone.present?
- project.milestones.find_by(iid: raw_data.milestone.number)
- end
- end
-
- def state
- @state ||= if raw_data.state == 'closed' && raw_data.merged_at.present?
- 'merged'
- elsif raw_data.state == 'closed'
- 'closed'
- else
- 'opened'
- end
- end
end
end
end
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 65b229ca8ff..7a649f28340 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -22,7 +22,7 @@ module Gitlab
IMPORTED_OBJECT_MAX_RETRIES = 5.freeze
- EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels project_label group_label].freeze
+ EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels].freeze
def self.create(*args)
new(*args).create
@@ -189,7 +189,7 @@ module Gitlab
# Otherwise always create the record, skipping the extra SELECT clause.
@existing_or_new_object ||= begin
if EXISTING_OBJECT_CHECK.include?(@relation_name)
- attribute_hash = attribute_hash_for(['events', 'priorities'])
+ attribute_hash = attribute_hash_for(['events'])
existing_object.assign_attributes(attribute_hash) if attribute_hash.any?
@@ -210,9 +210,8 @@ module Gitlab
def existing_object
@existing_object ||=
begin
- finder_attributes = @relation_name == :group_label ? %w[title group_id] : %w[title project_id]
- finder_hash = parsed_relation_hash.slice(*finder_attributes)
- existing_object = relation_class.find_or_create_by(finder_hash)
+ existing_object = find_or_create_object!
+
# Done in two steps, as MySQL behaves differently than PostgreSQL using
# the +find_or_create_by+ method and does not return the ID the second time.
existing_object.update!(parsed_relation_hash)
@@ -224,6 +223,25 @@ module Gitlab
@relation_name == :services && parsed_relation_hash['type'] &&
!Object.const_defined?(parsed_relation_hash['type'])
end
+
+ def find_or_create_object!
+ finder_attributes = @relation_name == :group_label ? %w[title group_id] : %w[title project_id]
+ finder_hash = parsed_relation_hash.slice(*finder_attributes)
+
+ if label?
+ label = relation_class.find_or_initialize_by(finder_hash)
+ parsed_relation_hash.delete('priorities') if label.persisted?
+
+ label.save!
+ label
+ else
+ relation_class.find_or_create_by(finder_hash)
+ end
+ end
+
+ def label?
+ @relation_name.to_s.include?('label')
+ end
end
end
end
diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb
index 94261b7eeed..45958710c13 100644
--- a/lib/gitlab/import_sources.rb
+++ b/lib/gitlab/import_sources.rb
@@ -7,21 +7,38 @@ module Gitlab
module ImportSources
extend CurrentSettings
+ ImportSource = Struct.new(:name, :title, :importer)
+
+ ImportTable = [
+ ImportSource.new('github', 'GitHub', Gitlab::GithubImport::Importer),
+ ImportSource.new('bitbucket', 'Bitbucket', Gitlab::BitbucketImport::Importer),
+ ImportSource.new('gitlab', 'GitLab.com', Gitlab::GitlabImport::Importer),
+ ImportSource.new('google_code', 'Google Code', Gitlab::GoogleCodeImport::Importer),
+ ImportSource.new('fogbugz', 'FogBugz', Gitlab::FogbugzImport::Importer),
+ ImportSource.new('git', 'Repo by URL', nil),
+ ImportSource.new('gitlab_project', 'GitLab export', Gitlab::ImportExport::Importer),
+ ImportSource.new('gitea', 'Gitea', Gitlab::GithubImport::Importer)
+ ].freeze
+
class << self
+ def options
+ @options ||= Hash[ImportTable.map { |importer| [importer.title, importer.name] }]
+ end
+
def values
- options.values
+ @values ||= ImportTable.map(&:name)
end
- def options
- {
- 'GitHub' => 'github',
- 'Bitbucket' => 'bitbucket',
- 'GitLab.com' => 'gitlab',
- 'Google Code' => 'google_code',
- 'FogBugz' => 'fogbugz',
- 'Repo by URL' => 'git',
- 'GitLab export' => 'gitlab_project'
- }
+ def importer_names
+ @importer_names ||= ImportTable.select(&:importer).map(&:name)
+ end
+
+ def importer(name)
+ ImportTable.find { |import_source| import_source.name == name }.importer
+ end
+
+ def title(name)
+ options.key(name)
end
end
end
diff --git a/lib/gitlab/kubernetes.rb b/lib/gitlab/kubernetes.rb
new file mode 100644
index 00000000000..288771c1c12
--- /dev/null
+++ b/lib/gitlab/kubernetes.rb
@@ -0,0 +1,80 @@
+module Gitlab
+ # Helper methods to do with Kubernetes network services & resources
+ module Kubernetes
+ # This is the comand that is run to start a terminal session. Kubernetes
+ # expects `command=foo&command=bar, not `command[]=foo&command[]=bar`
+ EXEC_COMMAND = URI.encode_www_form(
+ ['sh', '-c', 'bash || sh'].map { |value| ['command', value] }
+ )
+
+ # Filters an array of pods (as returned by the kubernetes API) by their labels
+ def filter_pods(pods, labels = {})
+ pods.select do |pod|
+ metadata = pod.fetch("metadata", {})
+ pod_labels = metadata.fetch("labels", nil)
+ next unless pod_labels
+
+ labels.all? { |k, v| pod_labels[k.to_s] == v }
+ end
+ end
+
+ # Converts a pod (as returned by the kubernetes API) into a terminal
+ def terminals_for_pod(api_url, namespace, pod)
+ metadata = pod.fetch("metadata", {})
+ status = pod.fetch("status", {})
+ spec = pod.fetch("spec", {})
+
+ containers = spec["containers"]
+ pod_name = metadata["name"]
+ phase = status["phase"]
+
+ return unless containers.present? && pod_name.present? && phase == "Running"
+
+ created_at = DateTime.parse(metadata["creationTimestamp"]) rescue nil
+
+ containers.map do |container|
+ {
+ selectors: { pod: pod_name, container: container["name"] },
+ url: container_exec_url(api_url, namespace, pod_name, container["name"]),
+ subprotocols: ['channel.k8s.io'],
+ headers: Hash.new { |h, k| h[k] = [] },
+ created_at: created_at,
+ }
+ end
+ end
+
+ def add_terminal_auth(terminal, token, ca_pem = nil)
+ terminal[:headers]['Authorization'] << "Bearer #{token}"
+ terminal[:ca_pem] = ca_pem if ca_pem.present?
+ terminal
+ end
+
+ def container_exec_url(api_url, namespace, pod_name, container_name)
+ url = URI.parse(api_url)
+ url.path = [
+ url.path.sub(%r{/+\z}, ''),
+ 'api', 'v1',
+ 'namespaces', ERB::Util.url_encode(namespace),
+ 'pods', ERB::Util.url_encode(pod_name),
+ 'exec'
+ ].join('/')
+
+ url.query = {
+ container: container_name,
+ tty: true,
+ stdin: true,
+ stdout: true,
+ stderr: true,
+ }.to_query + '&' + EXEC_COMMAND
+
+ case url.scheme
+ when 'http'
+ url.scheme = 'ws'
+ when 'https'
+ url.scheme = 'wss'
+ end
+
+ url.to_s
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
index 01c96a6fe96..91fb0bb317a 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -70,8 +70,8 @@ module Gitlab
def tag_endpoint(trans, env)
endpoint = env[ENDPOINT_KEY]
- path = endpoint_paths_cache[endpoint.route.route_method][endpoint.route.route_path]
- trans.action = "Grape##{endpoint.route.route_method} #{path}"
+ path = endpoint_paths_cache[endpoint.route.request_method][endpoint.route.path]
+ trans.action = "Grape##{endpoint.route.request_method} #{path}"
end
private
diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb
index 65713e73a59..dd99f9bb7d7 100644
--- a/lib/gitlab/middleware/multipart.rb
+++ b/lib/gitlab/middleware/multipart.rb
@@ -42,7 +42,7 @@ module Gitlab
key, value = parsed_field.first
if value.nil?
- value = File.open(tmp_path)
+ value = open_file(tmp_path)
@open_files << value
else
value = decorate_params_value(value, @request.params[key], tmp_path)
@@ -68,7 +68,7 @@ module Gitlab
case path_value
when nil
- value_hash[path_key] = File.open(tmp_path)
+ value_hash[path_key] = open_file(tmp_path)
@open_files << value_hash[path_key]
value_hash
when Hash
@@ -78,6 +78,10 @@ module Gitlab
raise "unexpected path value: #{path_value.inspect}"
end
end
+
+ def open_file(path)
+ ::UploadedFile.new(path, File.basename(path), 'application/octet-stream')
+ end
end
def initialize(app)
diff --git a/lib/gitlab/template/gitlab_ci_yml_template.rb b/lib/gitlab/template/gitlab_ci_yml_template.rb
index 8d1a1ed54c9..d19b0a52043 100644
--- a/lib/gitlab/template/gitlab_ci_yml_template.rb
+++ b/lib/gitlab/template/gitlab_ci_yml_template.rb
@@ -13,8 +13,9 @@ module Gitlab
def categories
{
- "General" => '',
- "Pages" => 'Pages'
+ 'General' => '',
+ 'Pages' => 'Pages',
+ 'Autodeploy' => 'autodeploy'
}
end
@@ -25,6 +26,11 @@ module Gitlab
def finder(project = nil)
Gitlab::Template::Finders::GlobalTemplateFinder.new(self.base_dir, self.extension, self.categories)
end
+
+ def dropdown_names(context)
+ categories = context == 'autodeploy' ? ['Autodeploy'] : ['General', 'Pages']
+ super().slice(*categories)
+ end
end
end
end
diff --git a/lib/gitlab/update_path_error.rb b/lib/gitlab/update_path_error.rb
new file mode 100644
index 00000000000..ce14cc887d0
--- /dev/null
+++ b/lib/gitlab/update_path_error.rb
@@ -0,0 +1,3 @@
+module Gitlab
+ class UpdatePathError < StandardError; end
+end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index aeb1a26e1ba..d28bb583fe7 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -95,6 +95,19 @@ module Gitlab
]
end
+ def terminal_websocket(terminal)
+ details = {
+ 'Terminal' => {
+ 'Subprotocols' => terminal[:subprotocols],
+ 'Url' => terminal[:url],
+ 'Header' => terminal[:headers]
+ }
+ }
+ details['Terminal']['CAPem'] = terminal[:ca_pem] if terminal.has_key?(:ca_pem)
+
+ details
+ end
+
def version
path = Rails.root.join(VERSION_FILE)
path.readable? ? path.read.chomp : 'unknown'
diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb
new file mode 100644
index 00000000000..ec2903b7ec6
--- /dev/null
+++ b/lib/mattermost/client.rb
@@ -0,0 +1,41 @@
+module Mattermost
+ class ClientError < Mattermost::Error; end
+
+ class Client
+ attr_reader :user
+
+ def initialize(user)
+ @user = user
+ end
+
+ private
+
+ def with_session(&blk)
+ Mattermost::Session.new(user).with_session(&blk)
+ end
+
+ def json_get(path, options = {})
+ with_session do |session|
+ json_response session.get(path, options)
+ end
+ end
+
+ def json_post(path, options = {})
+ with_session do |session|
+ json_response session.post(path, options)
+ end
+ end
+
+ def json_response(response)
+ json_response = JSON.parse(response.body)
+
+ unless response.success?
+ raise Mattermost::ClientError.new(json_response['message'] || 'Undefined error')
+ end
+
+ json_response
+ rescue JSON::JSONError
+ raise Mattermost::ClientError.new('Cannot parse response')
+ end
+ end
+end
diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb
new file mode 100644
index 00000000000..d1e4bb0eccf
--- /dev/null
+++ b/lib/mattermost/command.rb
@@ -0,0 +1,10 @@
+module Mattermost
+ class Command < Client
+ def create(params)
+ response = json_post("/api/v3/teams/#{params[:team_id]}/commands/create",
+ body: params.to_json)
+
+ response['token']
+ end
+ end
+end
diff --git a/lib/mattermost/error.rb b/lib/mattermost/error.rb
new file mode 100644
index 00000000000..014df175be0
--- /dev/null
+++ b/lib/mattermost/error.rb
@@ -0,0 +1,3 @@
+module Mattermost
+ class Error < StandardError; end
+end
diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb
index fb8d7d97f8a..377cb7b1021 100644
--- a/lib/mattermost/session.rb
+++ b/lib/mattermost/session.rb
@@ -1,5 +1,12 @@
module Mattermost
- class NoSessionError < StandardError; end
+ class NoSessionError < Mattermost::Error
+ def message
+ 'No session could be set up, is Mattermost configured with Single Sign On?'
+ end
+ end
+
+ class ConnectionError < Mattermost::Error; end
+
# This class' prime objective is to obtain a session token on a Mattermost
# instance with SSO configured where this GitLab instance is the provider.
#
@@ -17,6 +24,8 @@ module Mattermost
include Doorkeeper::Helpers::Controller
include HTTParty
+ LEASE_TIMEOUT = 60
+
base_uri Settings.mattermost.host
attr_accessor :current_resource_owner, :token
@@ -26,12 +35,16 @@ module Mattermost
end
def with_session
- raise NoSessionError unless create
-
- begin
- yield self
- ensure
- destroy
+ with_lease do
+ raise Mattermost::NoSessionError unless create
+
+ begin
+ yield self
+ rescue Errno::ECONNREFUSED
+ raise Mattermost::NoSessionError
+ ensure
+ destroy
+ end
end
end
@@ -58,11 +71,15 @@ module Mattermost
end
def get(path, options = {})
- self.class.get(path, options.merge(headers: @headers))
+ handle_exceptions do
+ self.class.get(path, options.merge(headers: @headers))
+ end
end
def post(path, options = {})
- self.class.post(path, options.merge(headers: @headers))
+ handle_exceptions do
+ self.class.post(path, options.merge(headers: @headers))
+ end
end
private
@@ -111,5 +128,33 @@ module Mattermost
response.headers['token']
end
end
+
+ def with_lease
+ lease_uuid = lease_try_obtain
+ raise NoSessionError unless lease_uuid
+
+ begin
+ yield
+ ensure
+ Gitlab::ExclusiveLease.cancel(lease_key, lease_uuid)
+ end
+ end
+
+ def lease_key
+ "mattermost:session"
+ end
+
+ def lease_try_obtain
+ lease = ::Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)
+ lease.try_obtain
+ end
+
+ def handle_exceptions
+ yield
+ rescue HTTParty::Error => e
+ raise Mattermost::ConnectionError.new(e.message)
+ rescue Errno::ECONNREFUSED
+ raise Mattermost::ConnectionError.new(e.message)
+ end
end
end
diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb
new file mode 100644
index 00000000000..784eca6ab5a
--- /dev/null
+++ b/lib/mattermost/team.rb
@@ -0,0 +1,7 @@
+module Mattermost
+ class Team < Client
+ def all
+ json_get('/api/v3/teams/all')
+ end
+ end
+end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index a763e2c5ba8..98dfb3e5216 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -105,4 +105,25 @@ describe GroupsController do
end
end
end
+
+ describe 'PUT update' do
+ before do
+ sign_in(user)
+ end
+
+ it 'updates the path succesfully' do
+ post :update, id: group.to_param, group: { path: 'new_path' }
+
+ expect(response).to have_http_status(302)
+ expect(controller).to set_flash[:notice]
+ end
+
+ it 'does not update the path on error' do
+ allow_any_instance_of(Group).to receive(:move_dir).and_raise(Gitlab::UpdatePathError)
+ post :update, id: group.to_param, group: { path: 'new_path' }
+
+ expect(assigns(:group).errors).not_to be_empty
+ expect(assigns(:group).path).not_to eq('new_path')
+ end
+ end
end
diff --git a/spec/controllers/import/gitea_controller_spec.rb b/spec/controllers/import/gitea_controller_spec.rb
new file mode 100644
index 00000000000..5ba64ab3eed
--- /dev/null
+++ b/spec/controllers/import/gitea_controller_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Import::GiteaController do
+ include ImportSpecHelper
+
+ let(:provider) { :gitea }
+ let(:host_url) { 'https://try.gitea.io' }
+
+ include_context 'a GitHub-ish import controller'
+
+ def assign_host_url
+ session[:gitea_host_url] = host_url
+ end
+
+ describe "GET new" do
+ it_behaves_like 'a GitHub-ish import controller: GET new' do
+ before do
+ assign_host_url
+ end
+ end
+ end
+
+ describe "POST personal_access_token" do
+ it_behaves_like 'a GitHub-ish import controller: POST personal_access_token'
+ end
+
+ describe "GET status" do
+ it_behaves_like 'a GitHub-ish import controller: GET status' do
+ before do
+ assign_host_url
+ end
+ let(:extra_assign_expectations) { { gitea_host_url: host_url } }
+ end
+ end
+
+ describe 'POST create' do
+ it_behaves_like 'a GitHub-ish import controller: POST create' do
+ before do
+ assign_host_url
+ end
+ end
+ end
+end
diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb
index 4f96567192d..95696e14b6c 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -3,34 +3,18 @@ require 'spec_helper'
describe Import::GithubController do
include ImportSpecHelper
- let(:user) { create(:user) }
- let(:token) { "asdasd12345" }
- let(:access_params) { { github_access_token: token } }
+ let(:provider) { :github }
- def assign_session_token
- session[:github_access_token] = token
- end
-
- before do
- sign_in(user)
- allow(controller).to receive(:github_import_enabled?).and_return(true)
- end
+ include_context 'a GitHub-ish import controller'
describe "GET new" do
- it "redirects to GitHub for an access token if logged in with GitHub" do
- allow(controller).to receive(:logged_in_with_github?).and_return(true)
- expect(controller).to receive(:go_to_github_for_permissions)
+ it_behaves_like 'a GitHub-ish import controller: GET new'
- get :new
- end
-
- it "redirects to status if we already have a token" do
- assign_session_token
- allow(controller).to receive(:logged_in_with_github?).and_return(false)
+ it "redirects to GitHub for an access token if logged in with GitHub" do
+ allow(controller).to receive(:logged_in_with_provider?).and_return(true)
+ expect(controller).to receive(:go_to_provider_for_permissions)
get :new
-
- expect(controller).to redirect_to(status_import_github_url)
end
end
@@ -51,196 +35,14 @@ describe Import::GithubController do
end
describe "POST personal_access_token" do
- it "updates access token" do
- token = "asdfasdf9876"
-
- allow_any_instance_of(Gitlab::GithubImport::Client).
- to receive(:user).and_return(true)
-
- post :personal_access_token, personal_access_token: token
-
- expect(session[:github_access_token]).to eq(token)
- expect(controller).to redirect_to(status_import_github_url)
- end
+ it_behaves_like 'a GitHub-ish import controller: POST personal_access_token'
end
describe "GET status" do
- before do
- @repo = OpenStruct.new(login: 'vim', full_name: 'asd/vim')
- @org = OpenStruct.new(login: 'company')
- @org_repo = OpenStruct.new(login: 'company', full_name: 'company/repo')
- assign_session_token
- end
-
- it "assigns variables" do
- @project = create(:project, import_type: 'github', creator_id: user.id)
- stub_client(repos: [@repo, @org_repo], orgs: [@org], org_repos: [@org_repo])
-
- get :status
-
- expect(assigns(:already_added_projects)).to eq([@project])
- expect(assigns(:repos)).to eq([@repo, @org_repo])
- end
-
- it "does not show already added project" do
- @project = create(:project, import_type: 'github', creator_id: user.id, import_source: 'asd/vim')
- stub_client(repos: [@repo], orgs: [])
-
- get :status
-
- expect(assigns(:already_added_projects)).to eq([@project])
- expect(assigns(:repos)).to eq([])
- end
-
- it "handles an invalid access token" do
- allow_any_instance_of(Gitlab::GithubImport::Client).
- to receive(:repos).and_raise(Octokit::Unauthorized)
-
- get :status
-
- expect(session[:github_access_token]).to eq(nil)
- expect(controller).to redirect_to(new_import_github_url)
- expect(flash[:alert]).to eq('Access denied to your GitHub account.')
- end
+ it_behaves_like 'a GitHub-ish import controller: GET status'
end
describe "POST create" do
- let(:github_username) { user.username }
- let(:github_user) { OpenStruct.new(login: github_username) }
- let(:github_repo) do
- OpenStruct.new(
- name: 'vim',
- full_name: "#{github_username}/vim",
- owner: OpenStruct.new(login: github_username)
- )
- end
-
- before do
- stub_client(user: github_user, repo: github_repo)
- assign_session_token
- end
-
- context "when the repository owner is the GitHub user" do
- context "when the GitHub user and GitLab user's usernames match" do
- it "takes the current user's namespace" do
- expect(Gitlab::GithubImport::ProjectCreator).
- to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params).
- and_return(double(execute: true))
-
- post :create, format: :js
- end
- end
-
- context "when the GitHub user and GitLab user's usernames don't match" do
- let(:github_username) { "someone_else" }
-
- it "takes the current user's namespace" do
- expect(Gitlab::GithubImport::ProjectCreator).
- to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params).
- and_return(double(execute: true))
-
- post :create, format: :js
- end
- end
- end
-
- context "when the repository owner is not the GitHub user" do
- let(:other_username) { "someone_else" }
-
- before do
- github_repo.owner = OpenStruct.new(login: other_username)
- assign_session_token
- end
-
- context "when a namespace with the GitHub user's username already exists" do
- let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) }
-
- context "when the namespace is owned by the GitLab user" do
- it "takes the existing namespace" do
- expect(Gitlab::GithubImport::ProjectCreator).
- to receive(:new).with(github_repo, github_repo.name, existing_namespace, user, access_params).
- and_return(double(execute: true))
-
- post :create, format: :js
- end
- end
-
- context "when the namespace is not owned by the GitLab user" do
- before do
- existing_namespace.owner = create(:user)
- existing_namespace.save
- end
-
- it "creates a project using user's namespace" do
- expect(Gitlab::GithubImport::ProjectCreator).
- to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params).
- and_return(double(execute: true))
-
- post :create, format: :js
- end
- end
- end
-
- context "when a namespace with the GitHub user's username doesn't exist" do
- context "when current user can create namespaces" do
- it "creates the namespace" do
- expect(Gitlab::GithubImport::ProjectCreator).
- to receive(:new).and_return(double(execute: true))
-
- expect { post :create, target_namespace: github_repo.name, format: :js }.to change(Namespace, :count).by(1)
- end
-
- it "takes the new namespace" do
- expect(Gitlab::GithubImport::ProjectCreator).
- to receive(:new).with(github_repo, github_repo.name, an_instance_of(Group), user, access_params).
- and_return(double(execute: true))
-
- post :create, target_namespace: github_repo.name, format: :js
- end
- end
-
- context "when current user can't create namespaces" do
- before do
- user.update_attribute(:can_create_group, false)
- end
-
- it "doesn't create the namespace" do
- expect(Gitlab::GithubImport::ProjectCreator).
- to receive(:new).and_return(double(execute: true))
-
- expect { post :create, format: :js }.not_to change(Namespace, :count)
- end
-
- it "takes the current user's namespace" do
- expect(Gitlab::GithubImport::ProjectCreator).
- to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params).
- and_return(double(execute: true))
-
- post :create, format: :js
- end
- end
- end
-
- context 'user has chosen a namespace and name for the project' do
- let(:test_namespace) { create(:namespace, name: 'test_namespace', owner: user) }
- let(:test_name) { 'test_name' }
-
- it 'takes the selected namespace and name' do
- expect(Gitlab::GithubImport::ProjectCreator).
- to receive(:new).with(github_repo, test_name, test_namespace, user, access_params).
- and_return(double(execute: true))
-
- post :create, { target_namespace: test_namespace.name, new_name: test_name, format: :js }
- end
-
- it 'takes the selected name and default namespace' do
- expect(Gitlab::GithubImport::ProjectCreator).
- to receive(:new).with(github_repo, test_name, user.namespace, user, access_params).
- and_return(double(execute: true))
-
- post :create, { new_name: test_name, format: :js }
- end
- end
- end
+ it_behaves_like 'a GitHub-ish import controller: POST create'
end
end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index bc5e2711125..7ac1d62d1b1 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -71,6 +71,75 @@ describe Projects::EnvironmentsController do
end
end
+ describe 'GET #terminal' do
+ context 'with valid id' do
+ it 'responds with a status code 200' do
+ get :terminal, environment_params
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'loads the terminals for the enviroment' do
+ expect_any_instance_of(Environment).to receive(:terminals)
+
+ get :terminal, environment_params
+ end
+ end
+
+ context 'with invalid id' do
+ it 'responds with a status code 404' do
+ get :terminal, environment_params(id: 666)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'GET #terminal_websocket_authorize' do
+ context 'with valid workhorse signature' do
+ before do
+ allow(Gitlab::Workhorse).to receive(:verify_api_request!).and_return(nil)
+ end
+
+ context 'and valid id' do
+ it 'returns the first terminal for the environment' do
+ expect_any_instance_of(Environment).
+ to receive(:terminals).
+ and_return([:fake_terminal])
+
+ expect(Gitlab::Workhorse).
+ to receive(:terminal_websocket).
+ with(:fake_terminal).
+ and_return(workhorse: :response)
+
+ get :terminal_websocket_authorize, environment_params
+
+ expect(response).to have_http_status(200)
+ expect(response.headers["Content-Type"]).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(response.body).to eq('{"workhorse":"response"}')
+ end
+ end
+
+ context 'and invalid id' do
+ it 'returns 404' do
+ get :terminal_websocket_authorize, environment_params(id: 666)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'with invalid workhorse signature' do
+ it 'aborts with an exception' do
+ allow(Gitlab::Workhorse).to receive(:verify_api_request!).and_raise(JWT::DecodeError)
+
+ expect { get :terminal_websocket_authorize, environment_params }.to raise_error(JWT::DecodeError)
+ # controller tests don't set the response status correctly. It's enough
+ # to check that the action raised an exception
+ end
+ end
+ end
+
def environment_params(opts = {})
opts.reverse_merge(namespace_id: project.namespace,
project_id: project,
diff --git a/spec/controllers/projects/mattermosts_controller_spec.rb b/spec/controllers/projects/mattermosts_controller_spec.rb
new file mode 100644
index 00000000000..2ae635a1244
--- /dev/null
+++ b/spec/controllers/projects/mattermosts_controller_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe Projects::MattermostsController do
+ let!(:project) { create(:empty_project) }
+ let!(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ describe 'GET #new' do
+ before do
+ allow_any_instance_of(MattermostSlashCommandsService).
+ to receive(:list_teams).and_return([])
+
+ get(:new,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param)
+ end
+
+ it 'accepts the request' do
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ describe 'POST #create' do
+ let(:mattermost_params) { { trigger: 'http://localhost:3000/trigger', team_id: 'abc' } }
+
+ subject do
+ post(:create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ mattermost: mattermost_params)
+ end
+
+ context 'no request can be made to mattermost' do
+ it 'shows the error' do
+ allow_any_instance_of(MattermostSlashCommandsService).to receive(:configure).and_return([false, "error message"])
+
+ expect(subject).to redirect_to(new_namespace_project_mattermost_url(project.namespace, project))
+ end
+ end
+
+ context 'the request is succesull' do
+ before do
+ allow_any_instance_of(Mattermost::Command).to receive(:create).and_return('token')
+ end
+
+ it 'redirects to the new page' do
+ subject
+ service = project.services.last
+
+ expect(subject).to redirect_to(edit_namespace_project_service_url(project.namespace, project, service))
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 9e0b80205d8..440b897ddc6 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -649,10 +649,6 @@ describe Projects::MergeRequestsController do
end
end
- describe 'GET builds' do
- it_behaves_like "loads labels", :builds
- end
-
describe 'GET pipelines' do
it_behaves_like "loads labels", :pipelines
end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
new file mode 100644
index 00000000000..5fe7e6407cc
--- /dev/null
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Projects::PipelinesController do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :public) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET stages.json' do
+ context 'when accessing existing stage' do
+ before do
+ create(:ci_build, pipeline: pipeline, stage: 'build')
+
+ get_stage('build')
+ end
+
+ it 'returns html source for stage dropdown' do
+ expect(response).to have_http_status(:ok)
+ expect(response).to render_template('projects/pipelines/_stage')
+ expect(json_response).to include('html')
+ end
+ end
+
+ context 'when accessing unknown stage' do
+ before do
+ get_stage('test')
+ end
+
+ it 'responds with not found' do
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ def get_stage(name)
+ get :stage, namespace_id: project.namespace.path,
+ project_id: project.path,
+ id: pipeline.id,
+ stage: name,
+ format: :json
+ end
+ end
+end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 0d072d6a690..f7fa834d7a2 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -42,6 +42,12 @@ FactoryGirl.define do
end
end
+ trait :test_repo do
+ after :create do |project|
+ TestEnv.copy_repo(project)
+ end
+ end
+
# Nest Project Feature attributes
transient do
wiki_access_level ProjectFeature::ENABLED
@@ -91,9 +97,7 @@ FactoryGirl.define do
factory :project, parent: :empty_project do
path { 'gitlabhq' }
- after :create do |project|
- TestEnv.copy_repo(project)
- end
+ test_repo
end
factory :forked_project_with_submodules, parent: :empty_project do
@@ -140,7 +144,7 @@ FactoryGirl.define do
active: true,
properties: {
namespace: project.path,
- api_url: 'https://kubernetes.example.com/api',
+ api_url: 'https://kubernetes.example.com',
token: 'a' * 40,
}
)
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index a36bfd574cb..a5b88812b75 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -1,12 +1,17 @@
require 'spec_helper'
describe "Admin::Projects", feature: true do
- before do
- @project = create(:project)
+ include Select2Helper
+
+ let(:user) { create :user }
+ let!(:project) { create(:project) }
+ let!(:current_user) do
login_as :admin
end
describe "GET /admin/projects" do
+ let!(:archived_project) { create :project, :public, archived: true }
+
before do
visit admin_projects_path
end
@@ -15,20 +20,98 @@ describe "Admin::Projects", feature: true do
expect(current_path).to eq(admin_projects_path)
end
- it "has projects list" do
- expect(page).to have_content(@project.name)
+ it 'renders projects list without archived project' do
+ expect(page).to have_content(project.name)
+ expect(page).not_to have_content(archived_project.name)
+ end
+
+ it 'renders all projects', js: true do
+ find(:css, '#sort-projects-dropdown').click
+ click_link 'Show archived projects'
+
+ expect(page).to have_content(project.name)
+ expect(page).to have_content(archived_project.name)
+ expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived')
end
end
- describe "GET /admin/projects/:id" do
+ describe "GET /admin/projects/:namespace_id/:id" do
before do
visit admin_projects_path
- click_link "#{@project.name}"
+ click_link "#{project.name}"
+ end
+
+ it do
+ expect(current_path).to eq admin_namespace_project_path(project.namespace, project)
end
it "has project info" do
- expect(page).to have_content(@project.path)
- expect(page).to have_content(@project.name)
+ expect(page).to have_content(project.path)
+ expect(page).to have_content(project.name)
+ expect(page).to have_content(project.name_with_namespace)
+ expect(page).to have_content(project.creator.name)
+ end
+ end
+
+ describe 'transfer project' do
+ before do
+ create(:group, name: 'Web')
+
+ allow_any_instance_of(Projects::TransferService).
+ to receive(:move_uploads_to_new_namespace).and_return(true)
+ end
+
+ it 'transfers project to group web', js: true do
+ visit admin_namespace_project_path(project.namespace, project)
+
+ click_button 'Search for Namespace'
+ click_link 'group: web'
+ click_button 'Transfer'
+
+ expect(page).to have_content("Web / #{project.name}")
+ expect(page).to have_content('Namespace: Web')
+ end
+ end
+
+ describe 'add admin himself to a project' do
+ before do
+ project.team << [user, :master]
+ end
+
+ it 'adds admin a to a project as developer', js: true do
+ visit namespace_project_project_members_path(project.namespace, project)
+
+ page.within '.users-project-form' do
+ select2(current_user.id, from: '#user_ids', multiple: true)
+ select 'Developer', from: 'access_level'
+ end
+
+ click_button 'Add to project'
+
+ page.within '.content-list' do
+ expect(page).to have_content(current_user.name)
+ expect(page).to have_content('Developer')
+ end
+ end
+ end
+
+ describe 'admin remove himself from a project' do
+ before do
+ project.team << [user, :master]
+ project.team << [current_user, :developer]
+ end
+
+ it 'removes admin from the project' do
+ visit namespace_project_project_members_path(project.namespace, project)
+
+ page.within '.content-list' do
+ expect(page).to have_content(current_user.name)
+ expect(page).to have_content('Developer')
+ end
+
+ find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click
+
+ expect(page).not_to have_selector(:css, '.content-list')
end
end
end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 8cd66f189be..47fa2f14307 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -17,9 +17,9 @@ feature 'Admin updates settings', feature: true do
expect(page).to have_content "Application settings saved successfully"
end
- scenario 'Change Slack Service template settings' do
+ scenario 'Change Slack Notifications Service template settings' do
click_link 'Service Templates'
- click_link 'Slack'
+ click_link 'Slack notifications'
fill_in 'Webhook', with: 'http://localhost'
fill_in 'Username', with: 'test_user'
fill_in 'service_push_channel', with: '#test_channel'
@@ -30,7 +30,7 @@ feature 'Admin updates settings', feature: true do
expect(page).to have_content 'Application settings saved successfully'
- click_link 'Slack'
+ click_link 'Slack notifications'
page.all('input[type=checkbox]').each do |checkbox|
expect(checkbox).to be_checked
diff --git a/spec/features/auto_deploy_spec.rb b/spec/features/auto_deploy_spec.rb
new file mode 100644
index 00000000000..92f1ab90881
--- /dev/null
+++ b/spec/features/auto_deploy_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe 'Auto deploy' do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ before do
+ project.create_kubernetes_service(
+ active: true,
+ properties: {
+ namespace: project.path,
+ api_url: 'https://kubernetes.example.com',
+ token: 'a' * 40,
+ }
+ )
+ project.team << [user, :master]
+ login_as user
+ end
+
+ context 'when no deployment service is active' do
+ before do
+ project.kubernetes_service.update!(active: false)
+ end
+
+ it 'does not show a button to set up auto deploy' do
+ visit namespace_project_path(project.namespace, project)
+ expect(page).to have_no_content('Set up autodeploy')
+ end
+ end
+
+ context 'when a deployment service is active' do
+ before do
+ project.kubernetes_service.update!(active: true)
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ it 'shows a button to set up auto deploy' do
+ expect(page).to have_link('Set up autodeploy')
+ end
+
+ it 'includes Kubernetes as an available template', js: true do
+ click_link 'Set up autodeploy'
+ click_button 'Choose a GitLab CI Yaml template'
+
+ within '.gitlab-ci-yml-selector' do
+ expect(page).to have_content('OpenShift')
+ end
+ end
+
+ it 'creates a merge request using "autodeploy" branch', js: true do
+ click_link 'Set up autodeploy'
+ click_button 'Choose a GitLab CI Yaml template'
+ within '.gitlab-ci-yml-selector' do
+ click_on 'OpenShift'
+ end
+ wait_for_ajax
+ click_button 'Commit Changes'
+
+ expect(page).to have_content('New Merge Request From autodeploy into master')
+ end
+ end
+end
diff --git a/spec/features/environment_spec.rb b/spec/features/environment_spec.rb
index 0c1939fd885..56f6cd2e095 100644
--- a/spec/features/environment_spec.rb
+++ b/spec/features/environment_spec.rb
@@ -38,6 +38,10 @@ feature 'Environment', :feature do
scenario 'does not show a re-deploy button for deployment without build' do
expect(page).not_to have_link('Re-deploy')
end
+
+ scenario 'does not show terminal button' do
+ expect(page).not_to have_terminal_button
+ end
end
context 'with related deployable present' do
@@ -60,6 +64,10 @@ feature 'Environment', :feature do
expect(page).not_to have_link('Stop')
end
+ scenario 'does not show terminal button' do
+ expect(page).not_to have_terminal_button
+ end
+
context 'with manual action' do
given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') }
@@ -84,6 +92,26 @@ feature 'Environment', :feature do
end
end
+ context 'with terminal' do
+ let(:project) { create(:kubernetes_project, :test_repo) }
+
+ context 'for project master' do
+ let(:role) { :master }
+
+ scenario 'it shows the terminal button' do
+ expect(page).to have_terminal_button
+ end
+ end
+
+ context 'for developer' do
+ let(:role) { :developer }
+
+ scenario 'does not show terminal button' do
+ expect(page).not_to have_terminal_button
+ end
+ end
+ end
+
context 'with stop action' do
given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
@@ -158,4 +186,8 @@ feature 'Environment', :feature do
environment.project,
environment)
end
+
+ def have_terminal_button
+ have_link(nil, href: terminal_namespace_project_environment_path(project.namespace, project, environment))
+ end
end
diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb
index e1b97b31e5d..72b984cfab8 100644
--- a/spec/features/environments_spec.rb
+++ b/spec/features/environments_spec.rb
@@ -113,6 +113,10 @@ feature 'Environments page', :feature, :js do
expect(page).not_to have_css('external-url')
end
+ scenario 'does not show terminal button' do
+ expect(page).not_to have_terminal_button
+ end
+
context 'with external_url' do
given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
given(:build) { create(:ci_build, pipeline: pipeline) }
@@ -145,6 +149,26 @@ feature 'Environments page', :feature, :js do
end
end
end
+
+ context 'with terminal' do
+ let(:project) { create(:kubernetes_project, :test_repo) }
+
+ context 'for project master' do
+ let(:role) { :master }
+
+ scenario 'it shows the terminal button' do
+ expect(page).to have_terminal_button
+ end
+ end
+
+ context 'for developer' do
+ let(:role) { :developer }
+
+ scenario 'does not show terminal button' do
+ expect(page).not_to have_terminal_button
+ end
+ end
+ end
end
end
end
@@ -195,6 +219,10 @@ feature 'Environments page', :feature, :js do
end
end
+ def have_terminal_button
+ have_link(nil, href: terminal_namespace_project_environment_path(project.namespace, project, environment))
+ end
+
def visit_environments(project)
visit namespace_project_environments_path(project.namespace, project)
end
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index da64827b377..df3a467cbb7 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -39,7 +39,6 @@ feature 'GFM autocomplete', feature: true, js: true do
page.within '.timeline-content-form' do
note.native.send_keys('')
note.native.send_keys("~#{label.title[0]}")
- sleep 1
note.click
end
@@ -53,7 +52,6 @@ feature 'GFM autocomplete', feature: true, js: true do
page.within '.timeline-content-form' do
note.native.send_keys('')
note.native.send_keys("@#{user.username[0]}")
- sleep 1
note.click
end
@@ -67,7 +65,6 @@ feature 'GFM autocomplete', feature: true, js: true do
page.within '.timeline-content-form' do
note.native.send_keys('')
note.native.send_keys(":cartwheel")
- sleep 1
note.click
end
@@ -76,6 +73,22 @@ feature 'GFM autocomplete', feature: true, js: true do
expect_to_wrap(false, emoji_item, note, 'cartwheel_tone1')
end
+ it 'doesn\'t open autocomplete after non-word character' do
+ page.within '.timeline-content-form' do
+ find('#note_note').native.send_keys("@#{user.username[0..2]}!")
+ end
+
+ expect(page).not_to have_selector('.atwho-view')
+ end
+
+ it 'doesn\'t open autocomplete if there is no space before' do
+ page.within '.timeline-content-form' do
+ find('#note_note').native.send_keys("hello:#{user.username[0..2]}")
+ end
+
+ expect(page).not_to have_selector('.atwho-view')
+ end
+
def expect_to_wrap(should_wrap, item, note, value)
expect(item).to have_content(value)
expect(item).not_to have_content("\"#{value}\"")
@@ -89,12 +102,4 @@ feature 'GFM autocomplete', feature: true, js: true do
end
end
end
-
- it 'doesnt open autocomplete after non-word character' do
- page.within '.timeline-content-form' do
- find('#note_note').native.send_keys("@#{user.username[0..2]}!")
- end
-
- expect(page).not_to have_selector('.atwho-view')
- end
end
diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb
index 142649297cc..73c5ef31edc 100644
--- a/spec/features/merge_requests/created_from_fork_spec.rb
+++ b/spec/features/merge_requests/created_from_fork_spec.rb
@@ -54,14 +54,14 @@ feature 'Merge request created from fork' do
scenario 'user visits a pipelines page', js: true do
visit_merge_request(merge_request)
- page.within('.merge-request-tabs') { click_link 'Builds' }
+ page.within('.merge-request-tabs') { click_link 'Pipelines' }
page.within('table.ci-table') do
- expect(page).to have_content 'rspec'
- expect(page).to have_content 'spinach'
+ expect(page).to have_content pipeline.status
+ expect(page).to have_content pipeline.id
end
- expect(find_link('Cancel running')[:href])
+ expect(page.find('a.btn-remove')[:href])
.to include fork_project.path_with_namespace
end
end
diff --git a/spec/features/milestones/show_spec.rb b/spec/features/milestones/show_spec.rb
new file mode 100644
index 00000000000..40b4dc63697
--- /dev/null
+++ b/spec/features/milestones/show_spec.rb
@@ -0,0 +1,26 @@
+require 'rails_helper'
+
+describe 'Milestone show', feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:milestone) { create(:milestone, project: project) }
+ let(:labels) { create_list(:label, 2, project: project) }
+ let(:issue_params) { { project: project, assignee: user, author: user, milestone: milestone, labels: labels } }
+
+ before do
+ project.add_user(user, :developer)
+ login_as(user)
+ end
+
+ def visit_milestone
+ visit namespace_project_milestone_path(project.namespace, project, milestone)
+ end
+
+ it 'avoids N+1 database queries' do
+ create(:labeled_issue, issue_params)
+ control_count = ActiveRecord::QueryRecorder.new { visit_milestone }.count
+ create_list(:labeled_issue, 10, issue_params)
+
+ expect { visit_milestone }.not_to exceed_query_limit(control_count)
+ end
+end
diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb
index fcdf7870f34..33f1c323af1 100644
--- a/spec/features/projects/commit/builds_spec.rb
+++ b/spec/features/projects/commit/builds_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'project commit builds' do
+feature 'project commit pipelines' do
given(:project) { create(:project) }
background do
@@ -16,11 +16,13 @@ feature 'project commit builds' do
ref: 'master')
end
- scenario 'user views commit builds page' do
- visit builds_namespace_project_commit_path(project.namespace,
- project, project.commit.sha)
+ scenario 'user views commit pipelines page' do
+ visit pipelines_namespace_project_commit_path(project.namespace, project, project.commit.sha)
- expect(page).to have_content('Builds')
+ page.within('.table-holder') do
+ expect(page).to have_content project.pipelines[0].status # pipeline status
+ expect(page).to have_content project.pipelines[0].id # pipeline ids
+ end
end
end
end
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index d3165d07d7b..7655c2b351f 100644
--- a/spec/features/projects/import_export/test_project_export.tar.gz
+++ b/spec/features/projects/import_export/test_project_export.tar.gz
Binary files differ
diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
index 27a83fdcd1f..b7273021c95 100644
--- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
+++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
@@ -24,7 +24,7 @@ feature 'Projects > Members > Master adds member with expiration date', feature:
click_on 'Add to project'
end
- page.within '.project_member:first-child' do
+ page.within "#project_member_#{new_member.project_members.first.id}" do
expect(page).to have_content('Expires in 4 days')
end
end
@@ -35,7 +35,7 @@ feature 'Projects > Members > Master adds member with expiration date', feature:
project.team.add_users([new_member.id], :developer, expires_at: '2016-09-06')
visit namespace_project_project_members_path(project.namespace, project)
- page.within '.project_member:first-child' do
+ page.within "#project_member_#{new_member.project_members.first.id}" do
find('.js-access-expiration-date').set '2016-08-09'
wait_for_ajax
expect(page).to have_content('Expires in 3 days')
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 1210e2745db..14e009daba8 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe "Pipelines", feature: true, js: true do
+describe 'Pipeline', :feature, :js do
include GitlabRoutingHelper
let(:project) { create(:empty_project) }
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index f3731698a18..1ff57f92c4c 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
-describe "Pipelines" do
+describe 'Pipelines', :feature, :js do
include GitlabRoutingHelper
+ include WaitForAjax
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
@@ -69,16 +70,32 @@ describe "Pipelines" do
end
context 'with manual actions' do
- let!(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'manual build', stage: 'test', commands: 'test') }
+ let!(:manual) do
+ create(:ci_build, :manual, pipeline: pipeline,
+ name: 'manual build',
+ stage: 'test',
+ commands: 'test')
+ end
- before { visit namespace_project_pipelines_path(project.namespace, project) }
+ before do
+ visit namespace_project_pipelines_path(project.namespace, project)
+ end
- it { expect(page).to have_link('Manual build') }
+ it 'has link to the manual action' do
+ find('.js-pipeline-dropdown-manual-actions').click
- context 'when playing' do
- before { click_link('Manual build') }
+ expect(page).to have_link('Manual build')
+ end
- it { expect(manual.reload).to be_pending }
+ context 'when manual action was played' do
+ before do
+ find('.js-pipeline-dropdown-manual-actions').click
+ click_link('Manual build')
+ end
+
+ it 'enqueues manual action job' do
+ expect(manual.reload).to be_pending
+ end
end
end
@@ -131,7 +148,10 @@ describe "Pipelines" do
before { visit namespace_project_pipelines_path(project.namespace, project) }
it { expect(page).to have_selector('.build-artifacts') }
- it { expect(page).to have_link(with_artifacts.name) }
+ it do
+ find('.js-pipeline-dropdown-download').click
+ expect(page).to have_link(with_artifacts.name)
+ end
end
context 'with artifacts expired' do
@@ -150,6 +170,42 @@ describe "Pipelines" do
it { expect(page).not_to have_selector('.build-artifacts') }
end
end
+
+ context 'mini pipleine graph' do
+ let!(:build) do
+ create(:ci_build, pipeline: pipeline, stage: 'build', name: 'build')
+ end
+
+ before do
+ visit namespace_project_pipelines_path(project.namespace, project)
+ end
+
+ it 'should render a mini pipeline graph' do
+ endpoint = stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: build.name)
+
+ expect(page).to have_selector('.mini-pipeline-graph')
+ expect(page).to have_selector(".js-builds-dropdown-button[data-stage-endpoint='#{endpoint}']")
+ end
+
+ context 'when clicking a graph stage' do
+ it 'should open a dropdown' do
+ find('.js-builds-dropdown-button').trigger('click')
+
+ wait_for_ajax
+
+ expect(page).to have_link build.name
+ end
+
+ it 'should be possible to retry the failed build' do
+ find('.js-builds-dropdown-button').trigger('click')
+
+ wait_for_ajax
+
+ find('a.ci-action-icon-container').trigger('click')
+ expect(page).not_to have_content('Cancel running')
+ end
+ end
+ end
end
describe 'POST /:project/pipelines' do
diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb
index f474e7e891b..274d50e7ce4 100644
--- a/spec/features/projects/services/mattermost_slash_command_spec.rb
+++ b/spec/features/projects/services/mattermost_slash_command_spec.rb
@@ -4,29 +4,26 @@ feature 'Setup Mattermost slash commands', feature: true do
include WaitForAjax
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:service) { project.create_mattermost_slash_commands_service }
+ let(:mattermost_enabled) { true }
before do
+ Settings.mattermost['enabled'] = mattermost_enabled
project.team << [user, :master]
login_as(user)
+ visit edit_namespace_project_service_path(project.namespace, project, service)
end
- describe 'user visites the mattermost slash command config page', js: true do
+ describe 'user visits the mattermost slash command config page', js: true do
it 'shows a help message' do
- visit edit_namespace_project_service_path(project.namespace, project, service)
-
wait_for_ajax
expect(page).to have_content("This service allows GitLab users to perform common")
end
- end
-
- describe 'saving a token' do
- let(:token) { ('a'..'z').to_a.join }
it 'shows the token after saving' do
- visit edit_namespace_project_service_path(project.namespace, project, service)
+ token = ('a'..'z').to_a.join
fill_in 'service_token', with: token
click_on 'Save'
@@ -35,14 +32,21 @@ feature 'Setup Mattermost slash commands', feature: true do
expect(value).to eq(token)
end
- end
- describe 'the trigger url' do
- it 'shows the correct url' do
- visit edit_namespace_project_service_path(project.namespace, project, service)
+ describe 'mattermost service is enabled' do
+ it 'shows the add to mattermost button' do
+ expect(page).to have_link 'Add to Mattermost'
+ end
+ end
+
+ describe 'mattermost service is not enabled' do
+ let(:mattermost_enabled) { false }
+
+ it 'shows the correct trigger url' do
+ value = find_field('request_url').value
- value = find_field('request_url').value
- expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger")
+ expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger")
+ end
end
end
end
diff --git a/spec/features/projects/services/slack_service_spec.rb b/spec/features/projects/services/slack_service_spec.rb
index 320ed13a01d..16541f51d98 100644
--- a/spec/features/projects/services/slack_service_spec.rb
+++ b/spec/features/projects/services/slack_service_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
feature 'Projects > Slack service > Setup events', feature: true do
let(:user) { create(:user) }
- let(:service) { SlackNotificationService.new }
- let(:project) { create(:project, slack_notification_service: service) }
+ let(:service) { SlackService.new }
+ let(:project) { create(:project, slack_service: service) }
background do
service.fields
diff --git a/spec/features/projects/services/slack_slash_command_spec.rb b/spec/features/projects/services/slack_slash_command_spec.rb
new file mode 100644
index 00000000000..32b32f7ae8e
--- /dev/null
+++ b/spec/features/projects/services/slack_slash_command_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+feature 'Slack slash commands', feature: true do
+ include WaitForAjax
+
+ given(:user) { create(:user) }
+ given(:project) { create(:project) }
+ given(:service) { project.create_slack_slash_commands_service }
+
+ background do
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ scenario 'user visits the slack slash command config page and shows a help message', js: true do
+ visit edit_namespace_project_service_path(project.namespace, project, service)
+
+ wait_for_ajax
+
+ expect(page).to have_content('This service allows GitLab users to perform common')
+ end
+
+ scenario 'shows the token after saving' do
+ visit edit_namespace_project_service_path(project.namespace, project, service)
+
+ fill_in 'service_token', with: 'token'
+ click_on 'Save'
+
+ value = find_field('service_token').value
+
+ expect(value).to eq('token')
+ end
+
+ scenario 'shows the correct trigger url' do
+ visit edit_namespace_project_service_path(project.namespace, project, service)
+
+ value = find_field('url').value
+ expect(value).to match("api/v3/projects/#{project.id}/services/slack_slash_commands/trigger")
+ end
+end
diff --git a/spec/helpers/import_helper_spec.rb b/spec/helpers/import_helper_spec.rb
index 187b891b927..10f293cddf5 100644
--- a/spec/helpers/import_helper_spec.rb
+++ b/spec/helpers/import_helper_spec.rb
@@ -25,24 +25,37 @@ describe ImportHelper do
end
end
- describe '#github_project_link' do
- context 'when provider does not specify a custom URL' do
- it 'uses default GitHub URL' do
- allow(Gitlab.config.omniauth).to receive(:providers).
+ describe '#provider_project_link' do
+ context 'when provider is "github"' do
+ context 'when provider does not specify a custom URL' do
+ it 'uses default GitHub URL' do
+ allow(Gitlab.config.omniauth).to receive(:providers).
and_return([Settingslogic.new('name' => 'github')])
- expect(helper.github_project_link('octocat/Hello-World')).
+ expect(helper.provider_project_link('github', 'octocat/Hello-World')).
to include('href="https://github.com/octocat/Hello-World"')
+ end
end
- end
- context 'when provider specify a custom URL' do
- it 'uses custom URL' do
- allow(Gitlab.config.omniauth).to receive(:providers).
+ context 'when provider specify a custom URL' do
+ it 'uses custom URL' do
+ allow(Gitlab.config.omniauth).to receive(:providers).
and_return([Settingslogic.new('name' => 'github', 'url' => 'https://github.company.com')])
- expect(helper.github_project_link('octocat/Hello-World')).
+ expect(helper.provider_project_link('github', 'octocat/Hello-World')).
to include('href="https://github.company.com/octocat/Hello-World"')
+ end
+ end
+ end
+
+ context 'when provider is "gitea"' do
+ before do
+ assign(:gitea_host_url, 'https://try.gitea.io/')
+ end
+
+ it 'uses given host' do
+ expect(helper.provider_project_link('gitea', 'octocat/Hello-World')).
+ to include('href="https://try.gitea.io/octocat/Hello-World"')
end
end
end
diff --git a/spec/javascripts/fixtures/issuable_filter.html.haml b/spec/javascripts/fixtures/issuable_filter.html.haml
new file mode 100644
index 00000000000..ae745b292e6
--- /dev/null
+++ b/spec/javascripts/fixtures/issuable_filter.html.haml
@@ -0,0 +1,8 @@
+%form.js-filter-form{action: '/user/project/issues?scope=all&state=closed'}
+ %input{id: 'utf8', name: 'utf8', value: '✓'}
+ %input{id: 'check_all_issues', name: 'check_all_issues'}
+ %input{id: 'search', name: 'search'}
+ %input{id: 'author_id', name: 'author_id'}
+ %input{id: 'assignee_id', name: 'assignee_id'}
+ %input{id: 'milestone_title', name: 'milestone_title'}
+ %input{id: 'label_name', name: 'label_name'}
diff --git a/spec/javascripts/fixtures/mini_dropdown_graph.html.haml b/spec/javascripts/fixtures/mini_dropdown_graph.html.haml
new file mode 100644
index 00000000000..e9bf7568e95
--- /dev/null
+++ b/spec/javascripts/fixtures/mini_dropdown_graph.html.haml
@@ -0,0 +1,8 @@
+%div.js-builds-dropdown-tests
+ %button.dropdown.js-builds-dropdown-button{'data-stage-endpoint' => 'foobar'}
+ Dropdown
+ %div.js-builds-dropdown-container
+ %div.js-builds-dropdown-list
+
+ %div.js-builds-dropdown-loading.builds-dropdown-loading.hidden
+ %span.fa.fa-spinner.fa-spin
diff --git a/spec/javascripts/issuable_spec.js.es6 b/spec/javascripts/issuable_spec.js.es6
new file mode 100644
index 00000000000..d61601ee4fb
--- /dev/null
+++ b/spec/javascripts/issuable_spec.js.es6
@@ -0,0 +1,81 @@
+/* global Issuable */
+/* global Turbolinks */
+
+//= require issuable
+//= require turbolinks
+
+(() => {
+ const BASE_URL = '/user/project/issues?scope=all&state=closed';
+ const DEFAULT_PARAMS = '&utf8=%E2%9C%93';
+
+ function updateForm(formValues, form) {
+ $.each(formValues, (id, value) => {
+ $(`#${id}`, form).val(value);
+ });
+ }
+
+ function resetForm(form) {
+ $('input[name!="utf8"]', form).each((index, input) => {
+ input.setAttribute('value', '');
+ });
+ }
+
+ describe('Issuable', () => {
+ fixture.preload('issuable_filter');
+
+ beforeEach(() => {
+ fixture.load('issuable_filter');
+ Issuable.init();
+ });
+
+ it('should be defined', () => {
+ expect(window.Issuable).toBeDefined();
+ });
+
+ describe('filtering', () => {
+ let $filtersForm;
+
+ beforeEach(() => {
+ $filtersForm = $('.js-filter-form');
+ fixture.load('issuable_filter');
+ resetForm($filtersForm);
+ });
+
+ it('should contain only the default parameters', () => {
+ spyOn(Turbolinks, 'visit');
+
+ Issuable.filterResults($filtersForm);
+
+ expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS);
+ });
+
+ it('should filter for the phrase "broken"', () => {
+ spyOn(Turbolinks, 'visit');
+
+ updateForm({ search: 'broken' }, $filtersForm);
+ Issuable.filterResults($filtersForm);
+ const params = `${DEFAULT_PARAMS}&search=broken`;
+
+ expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params);
+ });
+
+ it('should keep query parameters after modifying filter', () => {
+ spyOn(Turbolinks, 'visit');
+
+ // initial filter
+ updateForm({ milestone_title: 'v1.0' }, $filtersForm);
+
+ Issuable.filterResults($filtersForm);
+ let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`;
+ expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params);
+
+ // update filter
+ updateForm({ label_name: 'Frontend' }, $filtersForm);
+
+ Issuable.filterResults($filtersForm);
+ params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`;
+ expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params);
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6
new file mode 100644
index 00000000000..d1793e9308e
--- /dev/null
+++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6
@@ -0,0 +1,51 @@
+/* eslint-disable no-new */
+
+//= require flash
+//= require mini_pipeline_graph_dropdown
+
+(() => {
+ describe('Mini Pipeline Graph Dropdown', () => {
+ fixture.preload('mini_dropdown_graph');
+
+ beforeEach(() => {
+ fixture.load('mini_dropdown_graph');
+ });
+
+ describe('When is initialized', () => {
+ it('should initialize without errors when no options are given', () => {
+ const miniPipelineGraph = new window.gl.MiniPipelineGraph();
+
+ expect(miniPipelineGraph.dropdownListSelector).toEqual('.js-builds-dropdown-container');
+ });
+
+ it('should set the container as the given prop', () => {
+ const container = '.foo';
+
+ const miniPipelineGraph = new window.gl.MiniPipelineGraph({ container });
+
+ expect(miniPipelineGraph.container).toEqual(container);
+ });
+ });
+
+ describe('When dropdown is clicked', () => {
+ it('should call getBuildsList', () => {
+ const getBuildsListSpy = spyOn(gl.MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {});
+
+ new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' });
+
+ document.querySelector('.js-builds-dropdown-button').click();
+
+ expect(getBuildsListSpy).toHaveBeenCalled();
+ });
+
+ it('should make a request to the endpoint provided in the html', () => {
+ const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {});
+
+ new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' });
+
+ document.querySelector('.js-builds-dropdown-button').click();
+ expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar');
+ });
+ });
+ });
+})();
diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb
index 167397c736b..d9e4525cb28 100644
--- a/spec/lib/banzai/filter/external_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_link_filter_spec.rb
@@ -80,4 +80,18 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do
expect(filter(act).to_html).to eq(exp)
end
end
+
+ context 'for protocol-relative links' do
+ let(:doc) { filter %q(<p><a href="//google.com/">Google</a></p>) }
+
+ it 'adds rel="nofollow" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'nofollow'
+ end
+
+ it 'adds rel="noreferrer" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'noreferrer'
+ end
+ end
end
diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb
index bfc6818ac08..a0ec8884635 100644
--- a/spec/lib/gitlab/chat_commands/command_spec.rb
+++ b/spec/lib/gitlab/chat_commands/command_spec.rb
@@ -5,7 +5,9 @@ describe Gitlab::ChatCommands::Command, service: true do
let(:user) { create(:user) }
describe '#execute' do
- subject { described_class.new(project, user, params).execute }
+ subject do
+ described_class.new(project, user, params).execute
+ end
context 'when no command is available' do
let(:params) { { text: 'issue show 1' } }
@@ -74,7 +76,7 @@ describe Gitlab::ChatCommands::Command, service: true do
end
it 'returns action' do
- expect(subject[:text]).to include('Deployment from staging to production started')
+ expect(subject[:text]).to include('Deployment from staging to production started.')
expect(subject[:response_type]).to be(:in_channel)
end
@@ -91,4 +93,26 @@ describe Gitlab::ChatCommands::Command, service: true do
end
end
end
+
+ describe '#match_command' do
+ subject { described_class.new(project, user, params).match_command.first }
+
+ context 'IssueShow is triggered' do
+ let(:params) { { text: 'issue show 123' } }
+
+ it { is_expected.to eq(Gitlab::ChatCommands::IssueShow) }
+ end
+
+ context 'IssueCreate is triggered' do
+ let(:params) { { text: 'issue create my title' } }
+
+ it { is_expected.to eq(Gitlab::ChatCommands::IssueCreate) }
+ end
+
+ context 'IssueSearch is triggered' do
+ let(:params) { { text: 'issue search my query' } }
+
+ it { is_expected.to eq(Gitlab::ChatCommands::IssueSearch) }
+ end
+ end
end
diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb
index 444639acbaa..1f9c987be0b 100644
--- a/spec/lib/gitlab/git/rev_list_spec.rb
+++ b/spec/lib/gitlab/git/rev_list_spec.rb
@@ -26,6 +26,13 @@ describe Gitlab::Git::RevList, lib: true do
expect(rev_list).not_to be_valid
end
+
+ it "ignores nil values" do
+ env = { var => nil }
+ rev_list = described_class.new('oldrev', 'newrev', project: project, env: env)
+
+ expect(rev_list).to be_valid
+ end
end
end
end
diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb
index e829b936343..21f2a9e225b 100644
--- a/spec/lib/gitlab/github_import/client_spec.rb
+++ b/spec/lib/gitlab/github_import/client_spec.rb
@@ -45,20 +45,46 @@ describe Gitlab::GithubImport::Client, lib: true do
end
end
- context 'when provider does not specity an API endpoint' do
- it 'uses GitHub root API endpoint' do
- expect(client.api.api_endpoint).to eq 'https://api.github.com/'
+ describe '#api_endpoint' do
+ context 'when provider does not specity an API endpoint' do
+ it 'uses GitHub root API endpoint' do
+ expect(client.api.api_endpoint).to eq 'https://api.github.com/'
+ end
end
- end
- context 'when provider specify a custom API endpoint' do
- before do
- github_provider['args']['client_options']['site'] = 'https://github.company.com/'
+ context 'when provider specify a custom API endpoint' do
+ before do
+ github_provider['args']['client_options']['site'] = 'https://github.company.com/'
+ end
+
+ it 'uses the custom API endpoint' do
+ expect(OmniAuth::Strategies::GitHub).not_to receive(:default_options)
+ expect(client.api.api_endpoint).to eq 'https://github.company.com/'
+ end
+ end
+
+ context 'when given a host' do
+ subject(:client) { described_class.new(token, host: 'https://try.gitea.io/') }
+
+ it 'builds a endpoint with the given host and the default API version' do
+ expect(client.api.api_endpoint).to eq 'https://try.gitea.io/api/v3/'
+ end
end
- it 'uses the custom API endpoint' do
- expect(OmniAuth::Strategies::GitHub).not_to receive(:default_options)
- expect(client.api.api_endpoint).to eq 'https://github.company.com/'
+ context 'when given an API version' do
+ subject(:client) { described_class.new(token, api_version: 'v3') }
+
+ it 'does not use the API version without a host' do
+ expect(client.api.api_endpoint).to eq 'https://api.github.com/'
+ end
+ end
+
+ context 'when given a host and version' do
+ subject(:client) { described_class.new(token, host: 'https://try.gitea.io/', api_version: 'v3') }
+
+ it 'builds a endpoint with the given options' do
+ expect(client.api.api_endpoint).to eq 'https://try.gitea.io/api/v3/'
+ end
end
end
diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb
index 9e027839f59..72421832ffc 100644
--- a/spec/lib/gitlab/github_import/importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer_spec.rb
@@ -1,169 +1,251 @@
require 'spec_helper'
describe Gitlab::GithubImport::Importer, lib: true do
- describe '#execute' do
+ shared_examples 'Gitlab::GithubImport::Importer#execute' do
+ let(:expected_not_called) { [] }
+
before do
- allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new)
+ allow(project).to receive(:import_data).and_return(double.as_null_object)
end
- context 'when an error occurs' do
- let(:project) { create(:project, import_url: 'https://github.com/octocat/Hello-World.git', wiki_access_level: ProjectFeature::DISABLED) }
- let(:octocat) { double(id: 123456, login: 'octocat') }
- let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
- let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
- let(:repository) { double(id: 1, fork: false) }
- let(:source_sha) { create(:commit, project: project).id }
- let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) }
- let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id }
- let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) }
-
- let(:label1) do
- double(
- name: 'Bug',
- color: 'ff0000',
- url: 'https://api.github.com/repos/octocat/Hello-World/labels/bug'
- )
- end
+ it 'calls import methods' do
+ importer = described_class.new(project)
- let(:label2) do
- double(
- name: nil,
- color: 'ff0000',
- url: 'https://api.github.com/repos/octocat/Hello-World/labels/bug'
- )
- end
+ expected_called = [
+ :import_labels, :import_milestones, :import_pull_requests, :import_issues,
+ :import_wiki, :import_releases, :handle_errors
+ ]
- let(:milestone) do
- double(
- number: 1347,
- state: 'open',
- title: '1.0',
- description: 'Version 1.0',
- due_on: nil,
- created_at: created_at,
- updated_at: updated_at,
- closed_at: nil,
- url: 'https://api.github.com/repos/octocat/Hello-World/milestones/1'
- )
- end
+ expected_called -= expected_not_called
- let(:issue1) do
- double(
- number: 1347,
- milestone: nil,
- state: 'open',
- title: 'Found a bug',
- body: "I'm having a problem with this.",
- assignee: nil,
- user: octocat,
- comments: 0,
- pull_request: nil,
- created_at: created_at,
- updated_at: updated_at,
- closed_at: nil,
- url: 'https://api.github.com/repos/octocat/Hello-World/issues/1347',
- labels: [double(name: 'Label #1')],
- )
- end
+ aggregate_failures do
+ expected_called.each do |method_name|
+ expect(importer).to receive(method_name)
+ end
- let(:issue2) do
- double(
- number: 1348,
- milestone: nil,
- state: 'open',
- title: nil,
- body: "I'm having a problem with this.",
- assignee: nil,
- user: octocat,
- comments: 0,
- pull_request: nil,
- created_at: created_at,
- updated_at: updated_at,
- closed_at: nil,
- url: 'https://api.github.com/repos/octocat/Hello-World/issues/1348',
- labels: [double(name: 'Label #2')],
- )
- end
+ expect(importer).to receive(:import_comments).with(:issues)
+ expect(importer).to receive(:import_comments).with(:pull_requests)
- let(:pull_request) do
- double(
- number: 1347,
- milestone: nil,
- state: 'open',
- title: 'New feature',
- body: 'Please pull these awesome changes',
- head: source_branch,
- base: target_branch,
- assignee: nil,
- user: octocat,
- created_at: created_at,
- updated_at: updated_at,
- closed_at: nil,
- merged_at: nil,
- url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347',
- )
+ expected_not_called.each do |method_name|
+ expect(importer).not_to receive(method_name)
+ end
end
- let(:release1) do
- double(
- tag_name: 'v1.0.0',
- name: 'First release',
- body: 'Release v1.0.0',
- draft: false,
- created_at: created_at,
- updated_at: updated_at,
- url: 'https://api.github.com/repos/octocat/Hello-World/releases/1'
- )
- end
+ importer.execute
+ end
+ end
- let(:release2) do
- double(
- tag_name: 'v2.0.0',
- name: 'Second release',
- body: nil,
- draft: false,
- created_at: created_at,
- updated_at: updated_at,
- url: 'https://api.github.com/repos/octocat/Hello-World/releases/2'
- )
- end
+ shared_examples 'Gitlab::GithubImport::Importer#execute an error occurs' do
+ before do
+ allow(project).to receive(:import_data).and_return(double.as_null_object)
- before do
- allow(project).to receive(:import_data).and_return(double.as_null_object)
- allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound)
- allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label1, label2])
- allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone])
- allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2])
- allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request])
- allow_any_instance_of(Octokit::Client).to receive(:issues_comments).and_return([])
- allow_any_instance_of(Octokit::Client).to receive(:pull_requests_comments).and_return([])
- allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil }))
- allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2])
- allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error)
- end
+ allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new)
+
+ allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound)
+ allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error)
+
+ allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label1, label2])
+ allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone])
+ allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2])
+ allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request])
+ allow_any_instance_of(Octokit::Client).to receive(:issues_comments).and_return([])
+ allow_any_instance_of(Octokit::Client).to receive(:pull_requests_comments).and_return([])
+ allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil }))
+ allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2])
+ end
+ let(:octocat) { double(id: 123456, login: 'octocat') }
+ let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
+ let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
+ let(:label1) do
+ double(
+ name: 'Bug',
+ color: 'ff0000',
+ url: "#{api_root}/repos/octocat/Hello-World/labels/bug"
+ )
+ end
+
+ let(:label2) do
+ double(
+ name: nil,
+ color: 'ff0000',
+ url: "#{api_root}/repos/octocat/Hello-World/labels/bug"
+ )
+ end
+
+ let(:milestone) do
+ double(
+ id: 1347, # For Gitea
+ number: 1347,
+ state: 'open',
+ title: '1.0',
+ description: 'Version 1.0',
+ due_on: nil,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil,
+ url: "#{api_root}/repos/octocat/Hello-World/milestones/1"
+ )
+ end
- it 'returns true' do
- expect(described_class.new(project).execute).to eq true
+ let(:issue1) do
+ double(
+ number: 1347,
+ milestone: nil,
+ state: 'open',
+ title: 'Found a bug',
+ body: "I'm having a problem with this.",
+ assignee: nil,
+ user: octocat,
+ comments: 0,
+ pull_request: nil,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil,
+ url: "#{api_root}/repos/octocat/Hello-World/issues/1347",
+ labels: [double(name: 'Label #1')]
+ )
+ end
+
+ let(:issue2) do
+ double(
+ number: 1348,
+ milestone: nil,
+ state: 'open',
+ title: nil,
+ body: "I'm having a problem with this.",
+ assignee: nil,
+ user: octocat,
+ comments: 0,
+ pull_request: nil,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil,
+ url: "#{api_root}/repos/octocat/Hello-World/issues/1348",
+ labels: [double(name: 'Label #2')]
+ )
+ end
+
+ let(:repository) { double(id: 1, fork: false) }
+ let(:source_sha) { create(:commit, project: project).id }
+ let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) }
+ let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id }
+ let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) }
+ let(:pull_request) do
+ double(
+ number: 1347,
+ milestone: nil,
+ state: 'open',
+ title: 'New feature',
+ body: 'Please pull these awesome changes',
+ head: source_branch,
+ base: target_branch,
+ assignee: nil,
+ user: octocat,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil,
+ merged_at: nil,
+ url: "#{api_root}/repos/octocat/Hello-World/pulls/1347",
+ labels: [double(name: 'Label #2')]
+ )
+ end
+
+ let(:release1) do
+ double(
+ tag_name: 'v1.0.0',
+ name: 'First release',
+ body: 'Release v1.0.0',
+ draft: false,
+ created_at: created_at,
+ updated_at: updated_at,
+ url: "#{api_root}/repos/octocat/Hello-World/releases/1"
+ )
+ end
+
+ let(:release2) do
+ double(
+ tag_name: 'v2.0.0',
+ name: 'Second release',
+ body: nil,
+ draft: false,
+ created_at: created_at,
+ updated_at: updated_at,
+ url: "#{api_root}/repos/octocat/Hello-World/releases/2"
+ )
+ end
+
+ it 'returns true' do
+ expect(described_class.new(project).execute).to eq true
+ end
+
+ it 'does not raise an error' do
+ expect { described_class.new(project).execute }.not_to raise_error
+ end
+
+ it 'stores error messages' do
+ error = {
+ message: 'The remote data could not be fully imported.',
+ errors: [
+ { type: :label, url: "#{api_root}/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" },
+ { type: :issue, url: "#{api_root}/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank" },
+ { type: :wiki, errors: "Gitlab::Shell::Error" }
+ ]
+ }
+
+ unless project.gitea_import?
+ error[:errors] << { type: :release, url: "#{api_root}/repos/octocat/Hello-World/releases/2", errors: "Validation failed: Description can't be blank" }
end
- it 'does not raise an error' do
- expect { described_class.new(project).execute }.not_to raise_error
+ described_class.new(project).execute
+
+ expect(project.import_error).to eq error.to_json
+ end
+ end
+
+ let(:project) { create(:project, import_url: "#{repo_root}/octocat/Hello-World.git", wiki_access_level: ProjectFeature::DISABLED) }
+ let(:credentials) { { user: 'joe' } }
+
+ context 'when importing a GitHub project' do
+ let(:api_root) { 'https://api.github.com' }
+ let(:repo_root) { 'https://github.com' }
+
+ it_behaves_like 'Gitlab::GithubImport::Importer#execute'
+ it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs'
+
+ describe '#client' do
+ it 'instantiates a Client' do
+ allow(project).to receive(:import_data).and_return(double(credentials: credentials))
+ expect(Gitlab::GithubImport::Client).to receive(:new).with(
+ credentials[:user],
+ {}
+ )
+
+ described_class.new(project).client
end
+ end
+ end
- it 'stores error messages' do
- error = {
- message: 'The remote data could not be fully imported.',
- errors: [
- { type: :label, url: "https://api.github.com/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" },
- { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank" },
- { type: :wiki, errors: "Gitlab::Shell::Error" },
- { type: :release, url: 'https://api.github.com/repos/octocat/Hello-World/releases/2', errors: "Validation failed: Description can't be blank" }
- ]
- }
+ context 'when importing a Gitea project' do
+ let(:api_root) { 'https://try.gitea.io/api/v1' }
+ let(:repo_root) { 'https://try.gitea.io' }
+ before do
+ project.update(import_type: 'gitea', import_url: "#{repo_root}/foo/group/project.git")
+ end
- described_class.new(project).execute
+ it_behaves_like 'Gitlab::GithubImport::Importer#execute' do
+ let(:expected_not_called) { [:import_releases] }
+ end
+ it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs'
+
+ describe '#client' do
+ it 'instantiates a Client' do
+ allow(project).to receive(:import_data).and_return(double(credentials: credentials))
+ expect(Gitlab::GithubImport::Client).to receive(:new).with(
+ credentials[:user],
+ { host: "#{repo_root}:443/foo", api_version: 'v1' }
+ )
- expect(project.import_error).to eq error.to_json
+ described_class.new(project).client
end
end
end
diff --git a/spec/lib/gitlab/github_import/issuable_formatter_spec.rb b/spec/lib/gitlab/github_import/issuable_formatter_spec.rb
new file mode 100644
index 00000000000..6bc5f98ed2c
--- /dev/null
+++ b/spec/lib/gitlab/github_import/issuable_formatter_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::IssuableFormatter, lib: true do
+ let(:raw_data) do
+ double(number: 42)
+ end
+ let(:project) { double(import_type: 'github') }
+ let(:issuable_formatter) { described_class.new(project, raw_data) }
+
+ describe '#project_association' do
+ it { expect { issuable_formatter.project_association }.to raise_error(NotImplementedError) }
+ end
+
+ describe '#number' do
+ it { expect(issuable_formatter.number).to eq(42) }
+ end
+
+ describe '#find_condition' do
+ it { expect(issuable_formatter.find_condition).to eq({ iid: 42 }) }
+ end
+end
diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
index 95339e2f128..e31ed9c1fa0 100644
--- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
@@ -23,9 +23,9 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
}
end
- subject(:issue) { described_class.new(project, raw_data)}
+ subject(:issue) { described_class.new(project, raw_data) }
- describe '#attributes' do
+ shared_examples 'Gitlab::GithubImport::IssueFormatter#attributes' do
context 'when issue is open' do
let(:raw_data) { double(base_data.merge(state: 'open')) }
@@ -83,7 +83,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
end
context 'when it has a milestone' do
- let(:milestone) { double(number: 45) }
+ let(:milestone) { double(id: 42, number: 42) }
let(:raw_data) { double(base_data.merge(milestone: milestone)) }
it 'returns nil when milestone does not exist' do
@@ -91,7 +91,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
end
it 'returns milestone when it exists' do
- milestone = create(:milestone, project: project, iid: 45)
+ milestone = create(:milestone, project: project, iid: 42)
expect(issue.attributes.fetch(:milestone)).to eq milestone
end
@@ -118,6 +118,28 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
end
end
+ shared_examples 'Gitlab::GithubImport::IssueFormatter#number' do
+ let(:raw_data) { double(base_data.merge(number: 1347)) }
+
+ it 'returns issue number' do
+ expect(issue.number).to eq 1347
+ end
+ end
+
+ context 'when importing a GitHub project' do
+ it_behaves_like 'Gitlab::GithubImport::IssueFormatter#attributes'
+ it_behaves_like 'Gitlab::GithubImport::IssueFormatter#number'
+ end
+
+ context 'when importing a Gitea project' do
+ before do
+ project.update(import_type: 'gitea')
+ end
+
+ it_behaves_like 'Gitlab::GithubImport::IssueFormatter#attributes'
+ it_behaves_like 'Gitlab::GithubImport::IssueFormatter#number'
+ end
+
describe '#has_comments?' do
context 'when number of comments is greater than zero' do
let(:raw_data) { double(base_data.merge(comments: 1)) }
@@ -136,14 +158,6 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
end
end
- describe '#number' do
- let(:raw_data) { double(base_data.merge(number: 1347)) }
-
- it 'returns pull request number' do
- expect(issue.number).to eq 1347
- end
- end
-
describe '#pull_request?' do
context 'when mention a pull request' do
let(:raw_data) { double(base_data.merge(pull_request: double)) }
diff --git a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb b/spec/lib/gitlab/github_import/milestone_formatter_spec.rb
index 09337c99a07..6d38041c468 100644
--- a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/milestone_formatter_spec.rb
@@ -6,7 +6,6 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
let(:base_data) do
{
- number: 1347,
state: 'open',
title: '1.0',
description: 'Version 1.0',
@@ -16,12 +15,15 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do
closed_at: nil
}
end
+ let(:iid_attr) { :number }
- subject(:formatter) { described_class.new(project, raw_data)}
+ subject(:formatter) { described_class.new(project, raw_data) }
+
+ shared_examples 'Gitlab::GithubImport::MilestoneFormatter#attributes' do
+ let(:data) { base_data.merge(iid_attr => 1347) }
- describe '#attributes' do
context 'when milestone is open' do
- let(:raw_data) { double(base_data.merge(state: 'open')) }
+ let(:raw_data) { double(data.merge(state: 'open')) }
it 'returns formatted attributes' do
expected = {
@@ -40,7 +42,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do
end
context 'when milestone is closed' do
- let(:raw_data) { double(base_data.merge(state: 'closed')) }
+ let(:raw_data) { double(data.merge(state: 'closed')) }
it 'returns formatted attributes' do
expected = {
@@ -60,7 +62,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do
context 'when milestone has a due date' do
let(:due_date) { DateTime.strptime('2011-01-28T19:01:12Z') }
- let(:raw_data) { double(base_data.merge(due_on: due_date)) }
+ let(:raw_data) { double(data.merge(due_on: due_date)) }
it 'returns formatted attributes' do
expected = {
@@ -78,4 +80,17 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do
end
end
end
+
+ context 'when importing a GitHub project' do
+ it_behaves_like 'Gitlab::GithubImport::MilestoneFormatter#attributes'
+ end
+
+ context 'when importing a Gitea project' do
+ let(:iid_attr) { :id }
+ before do
+ project.update(import_type: 'gitea')
+ end
+
+ it_behaves_like 'Gitlab::GithubImport::MilestoneFormatter#attributes'
+ end
end
diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
index 302f0fc0623..2b3256edcb2 100644
--- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
@@ -32,9 +32,9 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
}
end
- subject(:pull_request) { described_class.new(project, raw_data)}
+ subject(:pull_request) { described_class.new(project, raw_data) }
- describe '#attributes' do
+ shared_examples 'Gitlab::GithubImport::PullRequestFormatter#attributes' do
context 'when pull request is open' do
let(:raw_data) { double(base_data.merge(state: 'open')) }
@@ -149,7 +149,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
context 'when it has a milestone' do
- let(:milestone) { double(number: 45) }
+ let(:milestone) { double(id: 42, number: 42) }
let(:raw_data) { double(base_data.merge(milestone: milestone)) }
it 'returns nil when milestone does not exist' do
@@ -157,22 +157,22 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
it 'returns milestone when it exists' do
- milestone = create(:milestone, project: project, iid: 45)
+ milestone = create(:milestone, project: project, iid: 42)
expect(pull_request.attributes.fetch(:milestone)).to eq milestone
end
end
end
- describe '#number' do
- let(:raw_data) { double(base_data.merge(number: 1347)) }
+ shared_examples 'Gitlab::GithubImport::PullRequestFormatter#number' do
+ let(:raw_data) { double(base_data) }
it 'returns pull request number' do
expect(pull_request.number).to eq 1347
end
end
- describe '#source_branch_name' do
+ shared_examples 'Gitlab::GithubImport::PullRequestFormatter#source_branch_name' do
context 'when source branch exists' do
let(:raw_data) { double(base_data) }
@@ -190,7 +190,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
end
- describe '#target_branch_name' do
+ shared_examples 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name' do
context 'when source branch exists' do
let(:raw_data) { double(base_data) }
@@ -208,6 +208,24 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
end
+ context 'when importing a GitHub project' do
+ it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#attributes'
+ it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#number'
+ it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#source_branch_name'
+ it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name'
+ end
+
+ context 'when importing a Gitea project' do
+ before do
+ project.update(import_type: 'gitea')
+ end
+
+ it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#attributes'
+ it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#number'
+ it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#source_branch_name'
+ it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name'
+ end
+
describe '#valid?' do
context 'when source, and target repos are not a fork' do
let(:raw_data) { double(base_data) }
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 9b49d6837c3..f420d71dee2 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -129,6 +129,7 @@ project:
- builds_email_service
- pipelines_email_service
- mattermost_slash_commands_service
+- slack_slash_commands_service
- irker_service
- pivotaltracker_service
- hipchat_service
@@ -136,8 +137,8 @@ project:
- assembla_service
- asana_service
- gemnasium_service
-- slack_notification_service
-- mattermost_notification_service
+- slack_service
+- mattermost_service
- buildkite_service
- bamboo_service
- teamcity_service
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 931d426c87f..2c0750c3377 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -15,6 +15,28 @@
"type": "ProjectLabel",
"priorities": [
]
+ },
+ {
+ "id": 3,
+ "title": "test3",
+ "color": "#428bca",
+ "group_id": 8,
+ "created_at": "2016-07-22T08:55:44.161Z",
+ "updated_at": "2016-07-22T08:55:44.161Z",
+ "template": false,
+ "description": "",
+ "project_id": null,
+ "type": "GroupLabel",
+ "priorities": [
+ {
+ "id": 1,
+ "project_id": 5,
+ "label_id": 1,
+ "priority": 1,
+ "created_at": "2016-10-18T09:35:43.338Z",
+ "updated_at": "2016-10-18T09:35:43.338Z"
+ }
+ ]
}
],
"issues": [
diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb
new file mode 100644
index 00000000000..8cea38e9ff8
--- /dev/null
+++ b/spec/lib/gitlab/import_sources_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe Gitlab::ImportSources do
+ describe '.options' do
+ it 'returns a hash' do
+ expected =
+ {
+ 'GitHub' => 'github',
+ 'Bitbucket' => 'bitbucket',
+ 'GitLab.com' => 'gitlab',
+ 'Google Code' => 'google_code',
+ 'FogBugz' => 'fogbugz',
+ 'Repo by URL' => 'git',
+ 'GitLab export' => 'gitlab_project',
+ 'Gitea' => 'gitea'
+ }
+
+ expect(described_class.options).to eq(expected)
+ end
+ end
+
+ describe '.values' do
+ it 'returns an array' do
+ expected =
+ [
+ 'github',
+ 'bitbucket',
+ 'gitlab',
+ 'google_code',
+ 'fogbugz',
+ 'git',
+ 'gitlab_project',
+ 'gitea'
+ ]
+
+ expect(described_class.values).to eq(expected)
+ end
+ end
+
+ describe '.importer_names' do
+ it 'returns an array of importer names' do
+ expected =
+ [
+ 'github',
+ 'bitbucket',
+ 'gitlab',
+ 'google_code',
+ 'fogbugz',
+ 'gitlab_project',
+ 'gitea'
+ ]
+
+ expect(described_class.importer_names).to eq(expected)
+ end
+ end
+
+ describe '.importer' do
+ import_sources = {
+ 'github' => Gitlab::GithubImport::Importer,
+ 'bitbucket' => Gitlab::BitbucketImport::Importer,
+ 'gitlab' => Gitlab::GitlabImport::Importer,
+ 'google_code' => Gitlab::GoogleCodeImport::Importer,
+ 'fogbugz' => Gitlab::FogbugzImport::Importer,
+ 'git' => nil,
+ 'gitlab_project' => Gitlab::ImportExport::Importer,
+ 'gitea' => Gitlab::GithubImport::Importer
+ }
+
+ import_sources.each do |name, klass|
+ it "returns #{klass} when given #{name}" do
+ expect(described_class.importer(name)).to eq(klass)
+ end
+ end
+ end
+
+ describe '.title' do
+ import_sources = {
+ 'github' => 'GitHub',
+ 'bitbucket' => 'Bitbucket',
+ 'gitlab' => 'GitLab.com',
+ 'google_code' => 'Google Code',
+ 'fogbugz' => 'FogBugz',
+ 'git' => 'Repo by URL',
+ 'gitlab_project' => 'GitLab export',
+ 'gitea' => 'Gitea'
+ }
+
+ import_sources.each do |name, title|
+ it "returns #{title} when given #{name}" do
+ expect(described_class.title(name)).to eq(title)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/kubernetes_spec.rb b/spec/lib/gitlab/kubernetes_spec.rb
new file mode 100644
index 00000000000..c9bd52a3b8f
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe Gitlab::Kubernetes do
+ include described_class
+
+ describe '#container_exec_url' do
+ let(:api_url) { 'https://example.com' }
+ let(:namespace) { 'default' }
+ let(:pod_name) { 'pod1' }
+ let(:container_name) { 'container1' }
+
+ subject(:result) { URI::parse(container_exec_url(api_url, namespace, pod_name, container_name)) }
+
+ it { expect(result.scheme).to eq('wss') }
+ it { expect(result.host).to eq('example.com') }
+ it { expect(result.path).to eq('/api/v1/namespaces/default/pods/pod1/exec') }
+ it { expect(result.query).to eq('container=container1&stderr=true&stdin=true&stdout=true&tty=true&command=sh&command=-c&command=bash+%7C%7C+sh') }
+
+ context 'with a HTTP API URL' do
+ let(:api_url) { 'http://example.com' }
+
+ it { expect(result.scheme).to eq('ws') }
+ end
+
+ context 'with a path prefix in the API URL' do
+ let(:api_url) { 'https://example.com/prefix/' }
+ it { expect(result.path).to eq('/prefix/api/v1/namespaces/default/pods/pod1/exec') }
+ end
+
+ context 'with arguments that need urlencoding' do
+ let(:namespace) { 'default namespace' }
+ let(:pod_name) { 'pod 1' }
+ let(:container_name) { 'container 1' }
+
+ it { expect(result.path).to eq('/api/v1/namespaces/default%20namespace/pods/pod%201/exec') }
+ it { expect(result.query).to match(/\Acontainer=container\+1&/) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
index bcaffd27909..7371b578a48 100644
--- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
@@ -33,7 +33,7 @@ describe Gitlab::Metrics::RackMiddleware do
end
it 'tags a transaction with the method and path of the route in the grape endpoint' do
- route = double(:route, route_method: "GET", route_path: "/:version/projects/:id/archive(.:format)")
+ route = double(:route, request_method: "GET", path: "/:version/projects/:id/archive(.:format)")
endpoint = double(:endpoint, route: route)
env['api.endpoint'] = endpoint
@@ -117,7 +117,7 @@ describe Gitlab::Metrics::RackMiddleware do
let(:transaction) { middleware.transaction_from_env(env) }
it 'tags a transaction with the method and path of the route in the grape endpount' do
- route = double(:route, route_method: "GET", route_path: "/:version/projects/:id/archive(.:format)")
+ route = double(:route, request_method: "GET", path: "/:version/projects/:id/archive(.:format)")
endpoint = double(:endpoint, route: route)
env['api.endpoint'] = endpoint
diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb
index ab1ab22795c..8d925460f01 100644
--- a/spec/lib/gitlab/middleware/multipart_spec.rb
+++ b/spec/lib/gitlab/middleware/multipart_spec.rb
@@ -12,7 +12,7 @@ describe Gitlab::Middleware::Multipart do
expect(app).to receive(:call) do |env|
file = Rack::Request.new(env).params['file']
- expect(file).to be_a(File)
+ expect(file).to be_a(::UploadedFile)
expect(file.path).to eq(tempfile.path)
end
@@ -39,7 +39,7 @@ describe Gitlab::Middleware::Multipart do
expect(app).to receive(:call) do |env|
file = Rack::Request.new(env).params['user']['avatar']
- expect(file).to be_a(File)
+ expect(file).to be_a(::UploadedFile)
expect(file.path).to eq(tempfile.path)
end
@@ -54,7 +54,7 @@ describe Gitlab::Middleware::Multipart do
expect(app).to receive(:call) do |env|
file = Rack::Request.new(env).params['project']['milestone']['themesong']
- expect(file).to be_a(File)
+ expect(file).to be_a(::UploadedFile)
expect(file.path).to eq(tempfile.path)
end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index b5b685da904..61da91dcbd3 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -37,6 +37,42 @@ describe Gitlab::Workhorse, lib: true do
end
end
+ describe '.terminal_websocket' do
+ def terminal(ca_pem: nil)
+ out = {
+ subprotocols: ['foo'],
+ url: 'wss://example.com/terminal.ws',
+ headers: { 'Authorization' => ['Token x'] }
+ }
+ out[:ca_pem] = ca_pem if ca_pem
+ out
+ end
+
+ def workhorse(ca_pem: nil)
+ out = {
+ 'Terminal' => {
+ 'Subprotocols' => ['foo'],
+ 'Url' => 'wss://example.com/terminal.ws',
+ 'Header' => { 'Authorization' => ['Token x'] }
+ }
+ }
+ out['Terminal']['CAPem'] = ca_pem if ca_pem
+ out
+ end
+
+ context 'without ca_pem' do
+ subject { Gitlab::Workhorse.terminal_websocket(terminal) }
+
+ it { is_expected.to eq(workhorse) }
+ end
+
+ context 'with ca_pem' do
+ subject { Gitlab::Workhorse.terminal_websocket(terminal(ca_pem: "foo")) }
+
+ it { is_expected.to eq(workhorse(ca_pem: "foo")) }
+ end
+ end
+
describe '.send_git_diff' do
let(:diff_refs) { double(base_sha: "base", head_sha: "head") }
subject { described_class.send_git_patch(repository, diff_refs) }
diff --git a/spec/lib/mattermost/client_spec.rb b/spec/lib/mattermost/client_spec.rb
new file mode 100644
index 00000000000..dc11a414717
--- /dev/null
+++ b/spec/lib/mattermost/client_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe Mattermost::Client do
+ let(:user) { build(:user) }
+
+ subject { described_class.new(user) }
+
+ context 'JSON parse error' do
+ before do
+ Struct.new("Request", :body, :success?)
+ end
+
+ it 'yields an error on malformed JSON' do
+ bad_json = Struct::Request.new("I'm not json", true)
+ expect { subject.send(:json_response, bad_json) }.to raise_error(Mattermost::ClientError)
+ end
+
+ it 'shows a client error if the request was unsuccessful' do
+ bad_request = Struct::Request.new("true", false)
+
+ expect { subject.send(:json_response, bad_request) }.to raise_error(Mattermost::ClientError)
+ end
+ end
+end
diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb
new file mode 100644
index 00000000000..5ccf1100898
--- /dev/null
+++ b/spec/lib/mattermost/command_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe Mattermost::Command do
+ let(:params) { { 'token' => 'token', team_id: 'abc' } }
+
+ before do
+ Mattermost::Session.base_uri('http://mattermost.example.com')
+
+ allow_any_instance_of(Mattermost::Client).to receive(:with_session).
+ and_yield(Mattermost::Session.new(nil))
+ end
+
+ describe '#create' do
+ let(:params) do
+ { team_id: 'abc',
+ trigger: 'gitlab'
+ }
+ end
+
+ subject { described_class.new(nil).create(params) }
+
+ context 'for valid trigger word' do
+ before do
+ stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create').
+ with(body: {
+ team_id: 'abc',
+ trigger: 'gitlab' }.to_json).
+ to_return(
+ status: 200,
+ headers: { 'Content-Type' => 'application/json' },
+ body: { token: 'token' }.to_json
+ )
+ end
+
+ it 'returns a token' do
+ is_expected.to eq('token')
+ end
+ end
+
+ context 'for error message' do
+ before do
+ stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create').
+ to_return(
+ status: 500,
+ headers: { 'Content-Type' => 'application/json' },
+ body: {
+ id: 'api.command.duplicate_trigger.app_error',
+ message: 'This trigger word is already in use. Please choose another word.',
+ detailed_error: '',
+ request_id: 'obc374man7bx5r3dbc1q5qhf3r',
+ status_code: 500
+ }.to_json
+ )
+ end
+
+ it 'raises an error with message' do
+ expect { subject }.to raise_error(Mattermost::Error, 'This trigger word is already in use. Please choose another word.')
+ end
+ end
+ end
+end
diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb
index 3c2eddbd221..74d12e37181 100644
--- a/spec/lib/mattermost/session_spec.rb
+++ b/spec/lib/mattermost/session_spec.rb
@@ -95,5 +95,29 @@ describe Mattermost::Session, type: :request do
end
end
end
+
+ context 'with lease' do
+ before do
+ allow(subject).to receive(:lease_try_obtain).and_return('aldkfjsldfk')
+ end
+
+ it 'tries to obtain a lease' do
+ expect(subject).to receive(:lease_try_obtain)
+ expect(Gitlab::ExclusiveLease).to receive(:cancel)
+
+ # Cannot setup a session, but we should still cancel the lease
+ expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
+ end
+ end
+
+ context 'without lease' do
+ before do
+ allow(subject).to receive(:lease_try_obtain).and_return(nil)
+ end
+
+ it 'returns a NoSessionError error' do
+ expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
+ end
+ end
end
end
diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb
new file mode 100644
index 00000000000..2d14be6bcc2
--- /dev/null
+++ b/spec/lib/mattermost/team_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe Mattermost::Team do
+ before do
+ Mattermost::Session.base_uri('http://mattermost.example.com')
+
+ allow_any_instance_of(Mattermost::Client).to receive(:with_session).
+ and_yield(Mattermost::Session.new(nil))
+ end
+
+ describe '#all' do
+ subject { described_class.new(nil).all }
+
+ context 'for valid request' do
+ let(:response) do
+ [{
+ "id" => "xiyro8huptfhdndadpz8r3wnbo",
+ "create_at" => 1482174222155,
+ "update_at" => 1482174222155,
+ "delete_at" => 0,
+ "display_name" => "chatops",
+ "name" => "chatops",
+ "email" => "admin@example.com",
+ "type" => "O",
+ "company_name" => "",
+ "allowed_domains" => "",
+ "invite_id" => "o4utakb9jtb7imctdfzbf9r5ro",
+ "allow_open_invite" => false }]
+ end
+
+ before do
+ stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all').
+ to_return(
+ status: 200,
+ headers: { 'Content-Type' => 'application/json' },
+ body: response.to_json
+ )
+ end
+
+ it 'returns a token' do
+ is_expected.to eq(response)
+ end
+ end
+
+ context 'for error message' do
+ before do
+ stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all').
+ to_return(
+ status: 500,
+ headers: { 'Content-Type' => 'application/json' },
+ body: {
+ id: 'api.team.list.app_error',
+ message: 'Cannot list teams.',
+ detailed_error: '',
+ request_id: 'obc374man7bx5r3dbc1q5qhf3r',
+ status_code: 500
+ }.to_json
+ )
+ end
+
+ it 'raises an error with message' do
+ expect { subject }.to raise_error(Mattermost::Error, 'Cannot list teams.')
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 52dd41065e9..dc377d15f15 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -175,6 +175,30 @@ describe Ci::Pipeline, models: true do
end
end
+ describe '#stage' do
+ subject { pipeline.stage('test') }
+
+ context 'with status in stage' do
+ before do
+ create(:commit_status, pipeline: pipeline, stage: 'test')
+ end
+
+ it { expect(subject).to be_a Ci::Stage }
+ it { expect(subject.name).to eq 'test' }
+ it { expect(subject.statuses).not_to be_empty }
+ end
+
+ context 'without status in stage' do
+ before do
+ create(:commit_status, pipeline: pipeline, stage: 'build')
+ end
+
+ it 'return stage object' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
describe 'state machine' do
let(:current) { Time.now.change(usec: 0) }
let(:build) { create_build('build1', 0) }
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index 8fff38f7cda..742bedb37e4 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -28,6 +28,19 @@ describe Ci::Stage, models: true do
end
end
+ describe '#statuses_count' do
+ before do
+ create_job(:ci_build)
+ create_job(:ci_build, stage: 'other stage')
+ end
+
+ subject { stage.statuses_count }
+
+ it "counts statuses only from current stage" do
+ is_expected.to eq(1)
+ end
+ end
+
describe '#builds' do
let!(:stage_build) { create_job(:ci_build) }
let!(:commit_status) { create_job(:commit_status) }
diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb
new file mode 100644
index 00000000000..a0765a264cf
--- /dev/null
+++ b/spec/models/concerns/reactive_caching_spec.rb
@@ -0,0 +1,145 @@
+require 'spec_helper'
+
+describe ReactiveCaching, caching: true do
+ include ReactiveCachingHelpers
+
+ class CacheTest
+ include ReactiveCaching
+
+ self.reactive_cache_key = ->(thing) { ["foo", thing.id] }
+
+ self.reactive_cache_lifetime = 5.minutes
+ self.reactive_cache_refresh_interval = 15.seconds
+
+ attr_reader :id
+
+ def initialize(id, &blk)
+ @id = id
+ @calculator = blk
+ end
+
+ def calculate_reactive_cache
+ @calculator.call
+ end
+
+ def result
+ with_reactive_cache do |data|
+ data / 2
+ end
+ end
+ end
+
+ let(:now) { Time.now.utc }
+
+ around(:each) do |example|
+ Timecop.freeze(now) { example.run }
+ end
+
+ let(:calculation) { -> { 2 + 2 } }
+ let(:cache_key) { "foo:666" }
+ let(:instance) { CacheTest.new(666, &calculation) }
+
+ describe '#with_reactive_cache' do
+ before { stub_reactive_cache }
+ subject(:go!) { instance.result }
+
+ context 'when cache is empty' do
+ it { is_expected.to be_nil }
+
+ it 'queues a background worker' do
+ expect(ReactiveCachingWorker).to receive(:perform_async).with(CacheTest, 666)
+
+ go!
+ end
+
+ it 'updates the cache lifespan' do
+ go!
+
+ expect(reactive_cache_alive?(instance)).to be_truthy
+ end
+ end
+
+ context 'when the cache is full' do
+ before { stub_reactive_cache(instance, 4) }
+
+ it { is_expected.to eq(2) }
+
+ context 'and expired' do
+ before { invalidate_reactive_cache(instance) }
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+
+ describe '#clear_reactive_cache!' do
+ before do
+ stub_reactive_cache(instance, 4)
+ instance.clear_reactive_cache!
+ end
+
+ it { expect(instance.result).to be_nil }
+ end
+
+ describe '#exclusively_update_reactive_cache!' do
+ subject(:go!) { instance.exclusively_update_reactive_cache! }
+
+ context 'when the lease is free and lifetime is not exceeded' do
+ before { stub_reactive_cache(instance, "preexisting") }
+
+ it 'takes and releases the lease' do
+ expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return("000000")
+ expect(Gitlab::ExclusiveLease).to receive(:cancel).with(cache_key, "000000")
+
+ go!
+ end
+
+ it 'caches the result of #calculate_reactive_cache' do
+ go!
+
+ expect(read_reactive_cache(instance)).to eq(calculation.call)
+ end
+
+ it "enqueues a repeat worker" do
+ expect_reactive_cache_update_queued(instance)
+
+ go!
+ end
+
+ context 'and #calculate_reactive_cache raises an exception' do
+ before { stub_reactive_cache(instance, "preexisting") }
+ let(:calculation) { -> { raise "foo"} }
+
+ it 'leaves the cache untouched' do
+ expect { go! }.to raise_error("foo")
+ expect(read_reactive_cache(instance)).to eq("preexisting")
+ end
+
+ it 'enqueues a repeat worker' do
+ expect_reactive_cache_update_queued(instance)
+
+ expect { go! }.to raise_error("foo")
+ end
+ end
+ end
+
+ context 'when lifetime is exceeded' do
+ it 'skips the calculation' do
+ expect(instance).to receive(:calculate_reactive_cache).never
+
+ go!
+ end
+ end
+
+ context 'when the lease is already taken' do
+ before do
+ expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(nil)
+ end
+
+ it 'skips the calculation' do
+ expect(instance).to receive(:calculate_reactive_cache).never
+
+ go!
+ end
+ end
+ end
+end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 97cbb093ed2..93eb402e060 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
describe Environment, models: true do
- subject(:environment) { create(:environment) }
+ let(:project) { create(:empty_project) }
+ subject(:environment) { create(:environment, project: project) }
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:deployments) }
@@ -31,6 +32,8 @@ describe Environment, models: true do
end
describe '#includes_commit?' do
+ let(:project) { create(:project) }
+
context 'without a last deployment' do
it "returns false" do
expect(environment.includes_commit?('HEAD')).to be false
@@ -38,9 +41,6 @@ describe Environment, models: true do
end
context 'with a last deployment' do
- let(:project) { create(:project) }
- let(:environment) { create(:environment, project: project) }
-
let!(:deployment) do
create(:deployment, environment: environment, sha: project.commit('master').id)
end
@@ -65,7 +65,6 @@ describe Environment, models: true do
describe '#first_deployment_for' do
let(:project) { create(:project) }
- let!(:environment) { create(:environment, project: project) }
let!(:deployment) { create(:deployment, environment: environment, ref: commit.parent.id) }
let!(:deployment1) { create(:deployment, environment: environment, ref: commit.id) }
let(:head_commit) { project.commit }
@@ -196,6 +195,57 @@ describe Environment, models: true do
end
end
+ describe '#has_terminals?' do
+ subject { environment.has_terminals? }
+
+ context 'when the enviroment is available' do
+ context 'with a deployment service' do
+ let(:project) { create(:kubernetes_project) }
+
+ context 'and a deployment' do
+ let!(:deployment) { create(:deployment, environment: environment) }
+ it { is_expected.to be_truthy }
+ end
+
+ context 'but no deployments' do
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ context 'without a deployment service' do
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ context 'when the environment is unavailable' do
+ let(:project) { create(:kubernetes_project) }
+ before { environment.stop }
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ describe '#terminals' do
+ let(:project) { create(:kubernetes_project) }
+ subject { environment.terminals }
+
+ context 'when the environment has terminals' do
+ before { allow(environment).to receive(:has_terminals?).and_return(true) }
+
+ it 'returns the terminals from the deployment service' do
+ expect(project.deployment_service).
+ to receive(:terminals).with(environment).
+ and_return(:fake_terminals)
+
+ is_expected.to eq(:fake_terminals)
+ end
+ end
+
+ context 'when the environment does not have terminals' do
+ before { allow(environment).to receive(:has_terminals?).and_return(false) }
+ it { is_expected.to eq(nil) }
+ end
+ end
+
describe '#slug' do
it "is automatically generated" do
expect(environment.slug).not_to be_nil
diff --git a/spec/models/project_authorization_spec.rb b/spec/models/project_authorization_spec.rb
new file mode 100644
index 00000000000..33ef67f97a7
--- /dev/null
+++ b/spec/models/project_authorization_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe ProjectAuthorization do
+ let(:user) { create(:user) }
+ let(:project1) { create(:empty_project) }
+ let(:project2) { create(:empty_project) }
+
+ describe '.insert_authorizations' do
+ it 'inserts the authorizations' do
+ described_class.
+ insert_authorizations([[user.id, project1.id, Gitlab::Access::MASTER]])
+
+ expect(user.project_authorizations.count).to eq(1)
+ end
+
+ it 'inserts rows in batches' do
+ described_class.insert_authorizations([
+ [user.id, project1.id, Gitlab::Access::MASTER],
+ [user.id, project2.id, Gitlab::Access::MASTER],
+ ], 1)
+
+ expect(user.project_authorizations.count).to eq(2)
+ end
+ end
+end
diff --git a/spec/models/project_services/chat_message/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb
index b71d153f814..50ad5013df9 100644
--- a/spec/models/project_services/chat_message/build_message_spec.rb
+++ b/spec/models/project_services/chat_message/build_message_spec.rb
@@ -10,7 +10,7 @@ describe ChatMessage::BuildMessage do
tag: false,
project_name: 'project_name',
- project_url: 'example.gitlab.com',
+ project_url: 'http://example.gitlab.com',
commit: {
status: status,
@@ -48,10 +48,10 @@ describe ChatMessage::BuildMessage do
end
def build_message(status_text = status)
- "<example.gitlab.com|project_name>:" \
- " Commit <example.gitlab.com/commit/" \
+ "<http://example.gitlab.com|project_name>:" \
+ " Commit <http://example.gitlab.com/commit/" \
"97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \
- " of <example.gitlab.com/commits/develop|develop> branch" \
+ " of <http://example.gitlab.com/commits/develop|develop> branch" \
" by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}"
end
end
diff --git a/spec/models/project_services/chat_message/issue_message_spec.rb b/spec/models/project_services/chat_message/issue_message_spec.rb
index ebe0ead4408..190ff4c535d 100644
--- a/spec/models/project_services/chat_message/issue_message_spec.rb
+++ b/spec/models/project_services/chat_message/issue_message_spec.rb
@@ -10,14 +10,14 @@ describe ChatMessage::IssueMessage, models: true do
username: 'test.user'
},
project_name: 'project_name',
- project_url: 'somewhere.com',
+ project_url: 'http://somewhere.com',
object_attributes: {
title: 'Issue title',
id: 10,
iid: 100,
assignee_id: 1,
- url: 'url',
+ url: 'http://url.com',
action: 'open',
state: 'opened',
description: 'issue description'
@@ -40,11 +40,11 @@ describe ChatMessage::IssueMessage, models: true do
context 'open' do
it 'returns a message regarding opening of issues' do
expect(subject.pretext).to eq(
- '<somewhere.com|[project_name>] Issue opened by test.user')
+ '[<http://somewhere.com|project_name>] Issue opened by test.user')
expect(subject.attachments).to eq([
{
title: "#100 Issue title",
- title_link: "url",
+ title_link: "http://url.com",
text: "issue description",
color: color,
}
@@ -60,7 +60,7 @@ describe ChatMessage::IssueMessage, models: true do
it 'returns a message regarding closing of issues' do
expect(subject.pretext). to eq(
- '<somewhere.com|[project_name>] Issue <url|#100 Issue title> closed by test.user')
+ '[<http://somewhere.com|project_name>] Issue <http://url.com|#100 Issue title> closed by test.user')
expect(subject.attachments).to be_empty
end
end
diff --git a/spec/models/project_services/chat_message/merge_message_spec.rb b/spec/models/project_services/chat_message/merge_message_spec.rb
index 07c414c6ca4..cc154112e90 100644
--- a/spec/models/project_services/chat_message/merge_message_spec.rb
+++ b/spec/models/project_services/chat_message/merge_message_spec.rb
@@ -10,14 +10,14 @@ describe ChatMessage::MergeMessage, models: true do
username: 'test.user'
},
project_name: 'project_name',
- project_url: 'somewhere.com',
+ project_url: 'http://somewhere.com',
object_attributes: {
title: "Issue title\nSecond line",
id: 10,
iid: 100,
assignee_id: 1,
- url: 'url',
+ url: 'http://url.com',
state: 'opened',
description: 'issue description',
source_branch: 'source_branch',
@@ -31,8 +31,8 @@ describe ChatMessage::MergeMessage, models: true do
context 'open' do
it 'returns a message regarding opening of merge requests' do
expect(subject.pretext).to eq(
- 'test.user opened <somewhere.com/merge_requests/100|merge request !100> '\
- 'in <somewhere.com|project_name>: *Issue title*')
+ 'test.user opened <http://somewhere.com/merge_requests/100|merge request !100> '\
+ 'in <http://somewhere.com|project_name>: *Issue title*')
expect(subject.attachments).to be_empty
end
end
@@ -43,8 +43,8 @@ describe ChatMessage::MergeMessage, models: true do
end
it 'returns a message regarding closing of merge requests' do
expect(subject.pretext).to eq(
- 'test.user closed <somewhere.com/merge_requests/100|merge request !100> '\
- 'in <somewhere.com|project_name>: *Issue title*')
+ 'test.user closed <http://somewhere.com/merge_requests/100|merge request !100> '\
+ 'in <http://somewhere.com|project_name>: *Issue title*')
expect(subject.attachments).to be_empty
end
end
diff --git a/spec/models/project_services/chat_message/note_message_spec.rb b/spec/models/project_services/chat_message/note_message_spec.rb
index 31936da40a2..da700a08e57 100644
--- a/spec/models/project_services/chat_message/note_message_spec.rb
+++ b/spec/models/project_services/chat_message/note_message_spec.rb
@@ -11,15 +11,15 @@ describe ChatMessage::NoteMessage, models: true do
avatar_url: 'http://fakeavatar'
},
project_name: 'project_name',
- project_url: 'somewhere.com',
+ project_url: 'http://somewhere.com',
repository: {
name: 'project_name',
- url: 'somewhere.com',
+ url: 'http://somewhere.com',
},
object_attributes: {
id: 10,
note: 'comment on a commit',
- url: 'url',
+ url: 'http://url.com',
noteable_type: 'Commit'
}
}
@@ -37,8 +37,8 @@ describe ChatMessage::NoteMessage, models: true do
it 'returns a message regarding notes on commits' do
message = described_class.new(@args)
- expect(message.pretext).to eq("test.user <url|commented on " \
- "commit 5f163b2b> in <somewhere.com|project_name>: " \
+ expect(message.pretext).to eq("test.user <http://url.com|commented on " \
+ "commit 5f163b2b> in <http://somewhere.com|project_name>: " \
"*Added a commit message*")
expected_attachments = [
{
@@ -63,8 +63,8 @@ describe ChatMessage::NoteMessage, models: true do
it 'returns a message regarding notes on a merge request' do
message = described_class.new(@args)
- expect(message.pretext).to eq("test.user <url|commented on " \
- "merge request !30> in <somewhere.com|project_name>: " \
+ expect(message.pretext).to eq("test.user <http://url.com|commented on " \
+ "merge request !30> in <http://somewhere.com|project_name>: " \
"*merge request title*")
expected_attachments = [
{
@@ -90,8 +90,8 @@ describe ChatMessage::NoteMessage, models: true do
it 'returns a message regarding notes on an issue' do
message = described_class.new(@args)
expect(message.pretext).to eq(
- "test.user <url|commented on " \
- "issue #20> in <somewhere.com|project_name>: " \
+ "test.user <http://url.com|commented on " \
+ "issue #20> in <http://somewhere.com|project_name>: " \
"*issue title*")
expected_attachments = [
{
@@ -115,8 +115,8 @@ describe ChatMessage::NoteMessage, models: true do
it 'returns a message regarding notes on a project snippet' do
message = described_class.new(@args)
- expect(message.pretext).to eq("test.user <url|commented on " \
- "snippet #5> in <somewhere.com|project_name>: " \
+ expect(message.pretext).to eq("test.user <http://url.com|commented on " \
+ "snippet #5> in <http://somewhere.com|project_name>: " \
"*snippet title*")
expected_attachments = [
{
diff --git a/spec/models/project_services/chat_message/pipeline_message_spec.rb b/spec/models/project_services/chat_message/pipeline_message_spec.rb
index eca71db07b6..bf2a9616455 100644
--- a/spec/models/project_services/chat_message/pipeline_message_spec.rb
+++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb
@@ -15,7 +15,7 @@ describe ChatMessage::PipelineMessage do
duration: duration
},
project: { path_with_namespace: 'project_name',
- web_url: 'example.gitlab.com' },
+ web_url: 'http://example.gitlab.com' },
user: user
}
end
@@ -59,9 +59,9 @@ describe ChatMessage::PipelineMessage do
end
def build_message(status_text = status, name = user[:name])
- "<example.gitlab.com|project_name>:" \
- " Pipeline <example.gitlab.com/pipelines/123|#123>" \
- " of <example.gitlab.com/commits/develop|develop> branch" \
+ "<http://example.gitlab.com|project_name>:" \
+ " Pipeline <http://example.gitlab.com/pipelines/123|#123>" \
+ " of <http://example.gitlab.com/commits/develop|develop> branch" \
" by #{name} #{status_text} in #{duration} #{'second'.pluralize(duration)}"
end
end
diff --git a/spec/models/project_services/chat_message/push_message_spec.rb b/spec/models/project_services/chat_message/push_message_spec.rb
index b781c4505db..24928873bad 100644
--- a/spec/models/project_services/chat_message/push_message_spec.rb
+++ b/spec/models/project_services/chat_message/push_message_spec.rb
@@ -10,7 +10,7 @@ describe ChatMessage::PushMessage, models: true do
project_name: 'project_name',
ref: 'refs/heads/master',
user_name: 'test.user',
- project_url: 'url'
+ project_url: 'http://url.com'
}
end
@@ -19,20 +19,20 @@ describe ChatMessage::PushMessage, models: true do
context 'push' do
before do
args[:commits] = [
- { message: 'message1', url: 'url1', id: 'abcdefghijkl', author: { name: 'author1' } },
- { message: 'message2', url: 'url2', id: '123456789012', author: { name: 'author2' } },
+ { message: 'message1', url: 'http://url1.com', id: 'abcdefghijkl', author: { name: 'author1' } },
+ { message: 'message2', url: 'http://url2.com', id: '123456789012', author: { name: 'author2' } },
]
end
it 'returns a message regarding pushes' do
expect(subject.pretext).to eq(
- 'test.user pushed to branch <url/commits/master|master> of '\
- '<url|project_name> (<url/compare/before...after|Compare changes>)'
+ 'test.user pushed to branch <http://url.com/commits/master|master> of '\
+ '<http://url.com|project_name> (<http://url.com/compare/before...after|Compare changes>)'
)
expect(subject.attachments).to eq([
{
- text: "<url1|abcdefgh>: message1 - author1\n"\
- "<url2|12345678>: message2 - author2",
+ text: "<http://url1.com|abcdefgh>: message1 - author1\n"\
+ "<http://url2.com|12345678>: message2 - author2",
color: color,
}
])
@@ -47,14 +47,14 @@ describe ChatMessage::PushMessage, models: true do
project_name: 'project_name',
ref: 'refs/tags/new_tag',
user_name: 'test.user',
- project_url: 'url'
+ project_url: 'http://url.com'
}
end
it 'returns a message regarding pushes' do
expect(subject.pretext).to eq('test.user pushed new tag ' \
- '<url/commits/new_tag|new_tag> to ' \
- '<url|project_name>')
+ '<http://url.com/commits/new_tag|new_tag> to ' \
+ '<http://url.com|project_name>')
expect(subject.attachments).to be_empty
end
end
@@ -66,8 +66,8 @@ describe ChatMessage::PushMessage, models: true do
it 'returns a message regarding a new branch' do
expect(subject.pretext).to eq(
- 'test.user pushed new branch <url/commits/master|master> to '\
- '<url|project_name>'
+ 'test.user pushed new branch <http://url.com/commits/master|master> to '\
+ '<http://url.com|project_name>'
)
expect(subject.attachments).to be_empty
end
@@ -80,7 +80,7 @@ describe ChatMessage::PushMessage, models: true do
it 'returns a message regarding a removed branch' do
expect(subject.pretext).to eq(
- 'test.user removed branch master from <url|project_name>'
+ 'test.user removed branch master from <http://url.com|project_name>'
)
expect(subject.attachments).to be_empty
end
diff --git a/spec/models/project_services/chat_message/wiki_page_message_spec.rb b/spec/models/project_services/chat_message/wiki_page_message_spec.rb
index 94c04dc0865..a2ad61e38e7 100644
--- a/spec/models/project_services/chat_message/wiki_page_message_spec.rb
+++ b/spec/models/project_services/chat_message/wiki_page_message_spec.rb
@@ -10,10 +10,10 @@ describe ChatMessage::WikiPageMessage, models: true do
username: 'test.user'
},
project_name: 'project_name',
- project_url: 'somewhere.com',
+ project_url: 'http://somewhere.com',
object_attributes: {
title: 'Wiki page title',
- url: 'url',
+ url: 'http://url.com',
content: 'Wiki page description'
}
}
@@ -25,7 +25,7 @@ describe ChatMessage::WikiPageMessage, models: true do
it 'returns a message that a new wiki page was created' do
expect(subject.pretext).to eq(
- 'test.user created <url|wiki page> in <somewhere.com|project_name>: '\
+ 'test.user created <http://url.com|wiki page> in <http://somewhere.com|project_name>: '\
'*Wiki page title*')
end
end
@@ -35,7 +35,7 @@ describe ChatMessage::WikiPageMessage, models: true do
it 'returns a message that a wiki page was updated' do
expect(subject.pretext).to eq(
- 'test.user edited <url|wiki page> in <somewhere.com|project_name>: '\
+ 'test.user edited <http://url.com|wiki page> in <http://somewhere.com|project_name>: '\
'*Wiki page title*')
end
end
diff --git a/spec/models/project_services/chat_service_spec.rb b/spec/models/project_services/chat_service_spec.rb
deleted file mode 100644
index c6a45a3e1be..00000000000
--- a/spec/models/project_services/chat_service_spec.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-require 'spec_helper'
-
-describe ChatService, models: true do
- describe "Associations" do
- it { is_expected.to have_many :chat_names }
- end
-
- describe '#valid_token?' do
- subject { described_class.new }
-
- it 'is false as it has no token' do
- expect(subject.valid_token?('wer')).to be_falsey
- end
- end
-end
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index 3603602e41d..4f3cd14e941 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -1,7 +1,29 @@
require 'spec_helper'
-describe KubernetesService, models: true do
- let(:project) { create(:empty_project) }
+describe KubernetesService, models: true, caching: true do
+ include KubernetesHelpers
+ include ReactiveCachingHelpers
+
+ let(:project) { create(:kubernetes_project) }
+ let(:service) { project.kubernetes_service }
+
+ # We use Kubeclient to interactive with the Kubernetes API. It will
+ # GET /api/v1 for a list of resources the API supports. This must be stubbed
+ # in addition to any other HTTP requests we expect it to perform.
+ let(:discovery_url) { service.api_url + '/api/v1' }
+ let(:discovery_response) { { body: kube_discovery_body.to_json } }
+
+ let(:pods_url) { service.api_url + "/api/v1/namespaces/#{service.namespace}/pods" }
+ let(:pods_response) { { body: kube_pods_body(kube_pod).to_json } }
+
+ def stub_kubeclient_discover
+ WebMock.stub_request(:get, discovery_url).to_return(discovery_response)
+ end
+
+ def stub_kubeclient_pods
+ stub_kubeclient_discover
+ WebMock.stub_request(:get, pods_url).to_return(pods_response)
+ end
describe "Associations" do
it { is_expected.to belong_to :project }
@@ -65,22 +87,15 @@ describe KubernetesService, models: true do
end
describe '#test' do
- let(:project) { create(:kubernetes_project) }
- let(:service) { project.kubernetes_service }
- let(:discovery_url) { service.api_url + '/api/v1' }
-
- # JSON response body from Kubernetes GET /api/v1 request
- let(:discovery_response) { { "kind" => "APIResourceList", "groupVersion" => "v1", "resources" => [] }.to_json }
+ before do
+ stub_kubeclient_discover
+ end
context 'with path prefix in api_url' do
let(:discovery_url) { 'https://kubernetes.example.com/prefix/api/v1' }
- before do
- service.api_url = 'https://kubernetes.example.com/prefix/'
- end
-
it 'tests with the prefix' do
- WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response)
+ service.api_url = 'https://kubernetes.example.com/prefix/'
expect(service.test[:success]).to be_truthy
expect(WebMock).to have_requested(:get, discovery_url).once
@@ -88,17 +103,12 @@ describe KubernetesService, models: true do
end
context 'with custom CA certificate' do
- let(:certificate) { "CA PEM DATA" }
- before do
- service.update_attributes!(ca_pem: certificate)
- end
-
it 'is added to the certificate store' do
- cert = double("certificate")
+ service.ca_pem = "CA PEM DATA"
- expect(OpenSSL::X509::Certificate).to receive(:new).with(certificate).and_return(cert)
+ cert = double("certificate")
+ expect(OpenSSL::X509::Certificate).to receive(:new).with(service.ca_pem).and_return(cert)
expect_any_instance_of(OpenSSL::X509::Store).to receive(:add_cert).with(cert)
- WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response)
expect(service.test[:success]).to be_truthy
expect(WebMock).to have_requested(:get, discovery_url).once
@@ -107,17 +117,15 @@ describe KubernetesService, models: true do
context 'success' do
it 'reads the discovery endpoint' do
- WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response)
-
expect(service.test[:success]).to be_truthy
expect(WebMock).to have_requested(:get, discovery_url).once
end
end
context 'failure' do
- it 'fails to read the discovery endpoint' do
- WebMock.stub_request(:get, discovery_url).to_return(status: 404)
+ let(:discovery_response) { { status: 404 } }
+ it 'fails to read the discovery endpoint' do
expect(service.test[:success]).to be_falsy
expect(WebMock).to have_requested(:get, discovery_url).once
end
@@ -156,4 +164,55 @@ describe KubernetesService, models: true do
)
end
end
+
+ describe '#terminals' do
+ let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") }
+ subject { service.terminals(environment) }
+
+ context 'with invalid pods' do
+ it 'returns no terminals' do
+ stub_reactive_cache(service, pods: [ { "bad" => "pod" } ])
+
+ is_expected.to be_empty
+ end
+ end
+
+ context 'with valid pods' do
+ let(:pod) { kube_pod(app: environment.slug) }
+ let(:terminals) { kube_terminals(service, pod) }
+
+ it 'returns terminals' do
+ stub_reactive_cache(service, pods: [ pod, pod, kube_pod(app: "should-be-filtered-out") ])
+
+ is_expected.to eq(terminals + terminals)
+ end
+ end
+ end
+
+ describe '#calculate_reactive_cache' do
+ before { stub_kubeclient_pods }
+ subject { service.calculate_reactive_cache }
+
+ context 'when service is inactive' do
+ before { service.active = false }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when kubernetes responds with valid pods' do
+ it { is_expected.to eq(pods: [kube_pod]) }
+ end
+
+ context 'when kubernetes responds with 500' do
+ let(:pods_response) { { status: 500 } }
+
+ it { expect { subject }.to raise_error(KubeException) }
+ end
+
+ context 'when kubernetes responds with 404' do
+ let(:pods_response) { { status: 404 } }
+
+ it { is_expected.to eq(pods: []) }
+ end
+ end
end
diff --git a/spec/models/project_services/mattermost_notification_service_spec.rb b/spec/models/project_services/mattermost_notification_service_spec.rb
deleted file mode 100644
index c01e64b4c8e..00000000000
--- a/spec/models/project_services/mattermost_notification_service_spec.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-require 'spec_helper'
-
-describe MattermostNotificationService, models: true do
- it_behaves_like "slack or mattermost"
-end
diff --git a/spec/models/project_services/mattermost_service_spec.rb b/spec/models/project_services/mattermost_service_spec.rb
new file mode 100644
index 00000000000..490d6aedffc
--- /dev/null
+++ b/spec/models/project_services/mattermost_service_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe MattermostService, models: true do
+ it_behaves_like "slack or mattermost notifications"
+end
diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
index 4a1037e950b..d6f4fbd7265 100644
--- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb
+++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
@@ -1,97 +1,122 @@
require 'spec_helper'
-describe MattermostSlashCommandsService, models: true do
- describe "Associations" do
- it { is_expected.to respond_to :token }
- end
+describe MattermostSlashCommandsService, :models do
+ it_behaves_like "chat slash commands service"
- describe '#valid_token?' do
- subject { described_class.new }
+ context 'Mattermost API' do
+ let(:project) { create(:empty_project) }
+ let(:service) { project.build_mattermost_slash_commands_service }
+ let(:user) { create(:user)}
- context 'when the token is empty' do
- it 'is false' do
- expect(subject.valid_token?('wer')).to be_falsey
- end
+ before do
+ Mattermost::Session.base_uri("http://mattermost.example.com")
+
+ allow_any_instance_of(Mattermost::Client).to receive(:with_session).
+ and_yield(Mattermost::Session.new(nil))
end
- context 'when there is a token' do
- before do
- subject.token = '123'
+ describe '#configure' do
+ subject do
+ service.configure(user, team_id: 'abc',
+ trigger: 'gitlab', url: 'http://trigger.url',
+ icon_url: 'http://icon.url/icon.png')
end
- it 'accepts equal tokens' do
- expect(subject.valid_token?('123')).to be_truthy
- end
- end
- end
+ context 'the requests succeeds' do
+ before do
+ stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create').
+ with(body: {
+ team_id: 'abc',
+ trigger: 'gitlab',
+ url: 'http://trigger.url',
+ icon_url: 'http://icon.url/icon.png',
+ auto_complete: true,
+ auto_complete_desc: "Perform common operations on: #{project.name_with_namespace}",
+ auto_complete_hint: '[help]',
+ description: "Perform common operations on: #{project.name_with_namespace}",
+ display_name: "GitLab / #{project.name_with_namespace}",
+ method: 'P',
+ user_name: 'GitLab' }.to_json).
+ to_return(
+ status: 200,
+ headers: { 'Content-Type' => 'application/json' },
+ body: { token: 'token' }.to_json
+ )
+ end
- describe '#trigger' do
- subject { described_class.new }
+ it 'saves the service' do
+ expect { subject }.to change { project.services.count }.by(1)
+ end
- context 'no token is passed' do
- let(:params) { Hash.new }
+ it 'saves the token' do
+ subject
- it 'returns nil' do
- expect(subject.trigger(params)).to be_nil
+ expect(service.reload.token).to eq('token')
+ end
end
- end
- context 'with a token passed' do
- let(:project) { create(:empty_project) }
- let(:params) { { token: 'token' } }
-
- before do
- allow(subject).to receive(:token).and_return('token')
- end
+ context 'an error is received' do
+ before do
+ stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create').
+ to_return(
+ status: 500,
+ headers: { 'Content-Type' => 'application/json' },
+ body: {
+ id: 'api.command.duplicate_trigger.app_error',
+ message: 'This trigger word is already in use. Please choose another word.',
+ detailed_error: '',
+ request_id: 'obc374man7bx5r3dbc1q5qhf3r',
+ status_code: 500
+ }.to_json
+ )
+ end
- context 'no user can be found' do
- context 'when no url can be generated' do
- it 'responds with the authorize url' do
- response = subject.trigger(params)
+ it 'shows error messages' do
+ succeeded, message = subject
- expect(response[:response_type]).to eq :ephemeral
- expect(response[:text]).to start_with ":sweat_smile: Couldn't identify you"
- end
+ expect(succeeded).to be(false)
+ expect(message).to eq('This trigger word is already in use. Please choose another word.')
end
+ end
+ end
- context 'when an auth url can be generated' do
- let(:params) do
- {
- team_domain: 'http://domain.tld',
- team_id: 'T3423423',
- user_id: 'U234234',
- user_name: 'mepmep',
- token: 'token'
- }
- end
-
- let(:service) do
- project.create_mattermost_slash_commands_service(
- properties: { token: 'token' }
- )
- end
+ describe '#list_teams' do
+ subject do
+ service.list_teams(user)
+ end
- it 'generates the url' do
- response = service.trigger(params)
+ context 'the requests succeeds' do
+ before do
+ stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all').
+ to_return(
+ status: 200,
+ headers: { 'Content-Type' => 'application/json' },
+ body: ['list'].to_json
+ )
+ end
- expect(response[:text]).to start_with(':wave: Hi there!')
- end
+ it 'returns a list of teams' do
+ expect(subject).not_to be_empty
end
end
- context 'when the user is authenticated' do
- let!(:chat_name) { create(:chat_name, service: service) }
- let(:service) do
- project.create_mattermost_slash_commands_service(
- properties: { token: 'token' }
- )
+ context 'an error is received' do
+ before do
+ stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all').
+ to_return(
+ status: 500,
+ headers: { 'Content-Type' => 'application/json' },
+ body: {
+ message: 'Failed to get team list.'
+ }.to_json
+ )
end
- let(:params) { { token: 'token', team_id: chat_name.team_id, user_id: chat_name.chat_id } }
- it 'triggers the command' do
- expect_any_instance_of(Gitlab::ChatCommands::Command).to receive(:execute)
+ it 'shows error messages' do
+ teams, message = subject
- service.trigger(params)
+ expect(teams).to be_empty
+ expect(message).to eq('Failed to get team list.')
end
end
end
diff --git a/spec/models/project_services/slack_notification_service_spec.rb b/spec/models/project_services/slack_notification_service_spec.rb
deleted file mode 100644
index 59ddddf7454..00000000000
--- a/spec/models/project_services/slack_notification_service_spec.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-require 'spec_helper'
-
-describe SlackNotificationService, models: true do
- it_behaves_like "slack or mattermost"
-end
diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb
new file mode 100644
index 00000000000..9a3ecc66d83
--- /dev/null
+++ b/spec/models/project_services/slack_service_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe SlackService, models: true do
+ it_behaves_like "slack or mattermost notifications"
+end
diff --git a/spec/models/project_services/slack_slash_commands_service.rb b/spec/models/project_services/slack_slash_commands_service.rb
new file mode 100644
index 00000000000..5775e439906
--- /dev/null
+++ b/spec/models/project_services/slack_slash_commands_service.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe SlackSlashCommandsService, :models do
+ it_behaves_like "chat slash commands service"
+
+ describe '#trigger' do
+ context 'when an auth url is generated' do
+ let(:project) { create(:empty_project) }
+ let(:params) do
+ {
+ team_domain: 'http://domain.tld',
+ team_id: 'T3423423',
+ user_id: 'U234234',
+ user_name: 'mepmep',
+ token: 'token'
+ }
+ end
+
+ let(:service) do
+ project.create_slack_slash_commands_service(
+ properties: { token: 'token' }
+ )
+ end
+
+ let(:authorize_url) do
+ 'http://authorize.example.com/'
+ end
+
+ before do
+ allow(service).to receive(:authorize_chat_name_url).and_return(authorize_url)
+ end
+
+ it 'uses slack compatible links' do
+ response = service.trigger(params)
+
+ expect(response[:text]).to include("<#{authorize_url}|connect your GitLab account>")
+ end
+ end
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index ed6b2c6a22b..88d5d14f855 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -20,10 +20,9 @@ describe Project, models: true do
it { is_expected.to have_many(:deploy_keys) }
it { is_expected.to have_many(:hooks).dependent(:destroy) }
it { is_expected.to have_many(:protected_branches).dependent(:destroy) }
- it { is_expected.to have_many(:chat_services) }
it { is_expected.to have_one(:forked_project_link).dependent(:destroy) }
- it { is_expected.to have_one(:slack_notification_service).dependent(:destroy) }
- it { is_expected.to have_one(:mattermost_notification_service).dependent(:destroy) }
+ it { is_expected.to have_one(:slack_service).dependent(:destroy) }
+ it { is_expected.to have_one(:mattermost_service).dependent(:destroy) }
it { is_expected.to have_one(:pushover_service).dependent(:destroy) }
it { is_expected.to have_one(:asana_service).dependent(:destroy) }
it { is_expected.to have_many(:boards).dependent(:destroy) }
@@ -37,6 +36,7 @@ describe Project, models: true do
it { is_expected.to have_one(:hipchat_service).dependent(:destroy) }
it { is_expected.to have_one(:flowdock_service).dependent(:destroy) }
it { is_expected.to have_one(:assembla_service).dependent(:destroy) }
+ it { is_expected.to have_one(:slack_slash_commands_service).dependent(:destroy) }
it { is_expected.to have_one(:mattermost_slash_commands_service).dependent(:destroy) }
it { is_expected.to have_one(:gemnasium_service).dependent(:destroy) }
it { is_expected.to have_one(:buildkite_service).dependent(:destroy) }
@@ -1458,6 +1458,18 @@ describe Project, models: true do
end
end
+ describe '#gitlab_project_import?' do
+ subject(:project) { build(:project, import_type: 'gitlab_project') }
+
+ it { expect(project.gitlab_project_import?).to be true }
+ end
+
+ describe '#gitea_import?' do
+ subject(:project) { build(:project, import_type: 'gitea') }
+
+ it { expect(project.gitea_import?).to be true }
+ end
+
describe '#lfs_enabled?' do
let(:project) { create(:project) }
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index 6f491fdf9a0..8481a9bef16 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Route, models: true do
- let!(:group) { create(:group) }
+ let!(:group) { create(:group, path: 'gitlab') }
let!(:route) { group.route }
describe 'relationships' do
@@ -17,13 +17,15 @@ describe Route, models: true do
describe '#rename_children' do
let!(:nested_group) { create(:group, path: "test", parent: group) }
let!(:deep_nested_group) { create(:group, path: "foo", parent: nested_group) }
+ let!(:similar_group) { create(:group, path: 'gitlab-org') }
- it "updates children routes with new path" do
- route.update_attributes(path: 'bar')
+ before { route.update_attributes(path: 'bar') }
+ it "updates children routes with new path" do
expect(described_class.exists?(path: 'bar')).to be_truthy
expect(described_class.exists?(path: 'bar/test')).to be_truthy
expect(described_class.exists?(path: 'bar/test/foo')).to be_truthy
+ expect(described_class.exists?(path: 'gitlab-org')).to be_truthy
end
end
end
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 2081f80ccc1..685da28c673 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -4,7 +4,14 @@ describe API::Files, api: true do
include ApiHelpers
let(:user) { create(:user) }
let!(:project) { create(:project, namespace: user.namespace ) }
+ let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } }
let(:file_path) { 'files/ruby/popen.rb' }
+ let(:params) do
+ {
+ file_path: file_path,
+ ref: 'master'
+ }
+ end
let(:author_email) { FFaker::Internet.email }
# I have to remove periods from the end of the name
@@ -24,36 +31,72 @@ describe API::Files, api: true do
before { project.team << [user, :developer] }
describe "GET /projects/:id/repository/files" do
- it "returns file info" do
- params = {
- file_path: file_path,
- ref: 'master',
- }
+ let(:route) { "/projects/#{project.id}/repository/files" }
- get api("/projects/#{project.id}/repository/files", user), params
+ shared_examples_for 'repository files' do
+ it "returns file info" do
+ get api(route, current_user), params
- expect(response).to have_http_status(200)
- expect(json_response['file_path']).to eq(file_path)
- expect(json_response['file_name']).to eq('popen.rb')
- expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
- expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n")
- end
+ expect(response).to have_http_status(200)
+ expect(json_response['file_path']).to eq(file_path)
+ expect(json_response['file_name']).to eq('popen.rb')
+ expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
+ expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n")
+ end
- it "returns a 400 bad request if no params given" do
- get api("/projects/#{project.id}/repository/files", user)
+ context 'when no params are given' do
+ it_behaves_like '400 response' do
+ let(:request) { get api(route, current_user) }
+ end
+ end
- expect(response).to have_http_status(400)
+ context 'when file_path does not exist' do
+ let(:params) do
+ {
+ file_path: 'app/models/application.rb',
+ ref: 'master',
+ }
+ end
+
+ it_behaves_like '404 response' do
+ let(:request) { get api(route, current_user), params }
+ let(:message) { '404 File Not Found' }
+ end
+ end
+
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, current_user), params }
+ end
+ end
end
- it "returns a 404 if such file does not exist" do
- params = {
- file_path: 'app/models/application.rb',
- ref: 'master',
- }
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository files' do
+ let(:project) { create(:project, :public) }
+ let(:current_user) { nil }
+ end
+ end
- get api("/projects/#{project.id}/repository/files", user), params
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route), params }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
+
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository files' do
+ let(:current_user) { user }
+ end
+ end
- expect(response).to have_http_status(404)
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, guest), params }
+ end
end
end
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index c90b69e8ebb..0b19fa38c55 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -7,204 +7,415 @@ describe API::Repositories, api: true do
include WorkhorseHelpers
let(:user) { create(:user) }
- let(:user2) { create(:user) }
+ let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } }
let!(:project) { create(:project, creator_id: user.id) }
let!(:master) { create(:project_member, :master, user: user, project: project) }
- let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
describe "GET /projects/:id/repository/tree" do
- context "authorized user" do
- before { project.team << [user2, :reporter] }
+ let(:route) { "/projects/#{project.id}/repository/tree" }
- it "returns project commits" do
- get api("/projects/#{project.id}/repository/tree", user)
+ shared_examples_for 'repository tree' do
+ it 'returns the repository tree' do
+ get api(route, current_user)
expect(response).to have_http_status(200)
+ first_commit = json_response.first
+
expect(json_response).to be_an Array
- expect(json_response.first['name']).to eq('bar')
- expect(json_response.first['type']).to eq('tree')
- expect(json_response.first['mode']).to eq('040000')
+ expect(first_commit['name']).to eq('bar')
+ expect(first_commit['type']).to eq('tree')
+ expect(first_commit['mode']).to eq('040000')
+ end
+
+ context 'when ref does not exist' do
+ it_behaves_like '404 response' do
+ let(:request) { get api("#{route}?ref_name=foo", current_user) }
+ let(:message) { '404 Tree Not Found' }
+ end
end
- it 'returns a 404 for unknown ref' do
- get api("/projects/#{project.id}/repository/tree?ref_name=foo", user)
- expect(response).to have_http_status(404)
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
- expect(json_response).to be_an Object
- json_response['message'] == '404 Tree Not Found'
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, current_user) }
+ end
+ end
+
+ context 'with recursive=1' do
+ it 'returns recursive project paths tree' do
+ get api("#{route}?recursive=1", current_user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response[4]['name']).to eq('html')
+ expect(json_response[4]['path']).to eq('files/html')
+ expect(json_response[4]['type']).to eq('tree')
+ expect(json_response[4]['mode']).to eq('040000')
+ end
+
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, current_user) }
+ end
+ end
+
+ context 'when ref does not exist' do
+ it_behaves_like '404 response' do
+ let(:request) { get api("#{route}?recursive=1&ref_name=foo", current_user) }
+ let(:message) { '404 Tree Not Found' }
+ end
+ end
end
end
- context "unauthorized user" do
- it "does not return project commits" do
- get api("/projects/#{project.id}/repository/tree")
- expect(response).to have_http_status(401)
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository tree' do
+ let(:project) { create(:project, :public) }
+ let(:current_user) { nil }
end
end
- end
- describe 'GET /projects/:id/repository/tree?recursive=1' do
- context 'authorized user' do
- before { project.team << [user2, :reporter] }
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
+
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository tree' do
+ let(:current_user) { user }
+ end
+ end
- it 'should return recursive project paths tree' do
- get api("/projects/#{project.id}/repository/tree?recursive=1", user)
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, guest) }
+ end
+ end
+ end
- expect(response.status).to eq(200)
+ {
+ 'blobs/:sha' => 'blobs/master',
+ 'commits/:sha/blob' => 'commits/master/blob'
+ }.each do |desc_path, example_path|
+ describe "GET /projects/:id/repository/#{desc_path}" do
+ let(:route) { "/projects/#{project.id}/repository/#{example_path}?filepath=README.md" }
+
+ shared_examples_for 'repository blob' do
+ it 'returns the repository blob' do
+ get api(route, current_user)
+
+ expect(response).to have_http_status(200)
+ end
+
+ context 'when sha does not exist' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route.sub('master', 'invalid_branch_name'), current_user) }
+ let(:message) { '404 Commit Not Found' }
+ end
+ end
+
+ context 'when filepath does not exist' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route.sub('README.md', 'README.invalid'), current_user) }
+ let(:message) { '404 File Not Found' }
+ end
+ end
+
+ context 'when no filepath is given' do
+ it_behaves_like '400 response' do
+ let(:request) { get api(route.sub('?filepath=README.md', ''), current_user) }
+ end
+ end
+
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, current_user) }
+ end
+ end
+ end
- expect(json_response).to be_an Array
- expect(json_response[4]['name']).to eq('html')
- expect(json_response[4]['path']).to eq('files/html')
- expect(json_response[4]['type']).to eq('tree')
- expect(json_response[4]['mode']).to eq('040000')
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository blob' do
+ let(:project) { create(:project, :public) }
+ let(:current_user) { nil }
+ end
end
- it 'returns a 404 for unknown ref' do
- get api("/projects/#{project.id}/repository/tree?ref_name=foo&recursive=1", user)
- expect(response).to have_http_status(404)
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
- expect(json_response).to be_an Object
- json_response['message'] == '404 Tree Not Found'
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository blob' do
+ let(:current_user) { user }
+ end
end
- end
- context "unauthorized user" do
- it "does not return project commits" do
- get api("/projects/#{project.id}/repository/tree?recursive=1")
- expect(response).to have_http_status(401)
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, guest) }
+ end
end
end
end
- describe "GET /projects/:id/repository/blobs/:sha" do
- it "gets the raw file contents" do
- get api("/projects/#{project.id}/repository/blobs/master?filepath=README.md", user)
- expect(response).to have_http_status(200)
+ describe "GET /projects/:id/repository/raw_blobs/:sha" do
+ let(:route) { "/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}" }
+
+ shared_examples_for 'repository raw blob' do
+ it 'returns the repository raw blob' do
+ get api(route, current_user)
+
+ expect(response).to have_http_status(200)
+ end
+
+ context 'when sha does not exist' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route.sub(sample_blob.oid, '123456'), current_user) }
+ let(:message) { '404 Blob Not Found' }
+ end
+ end
+
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, current_user) }
+ end
+ end
end
- it "returns 404 for invalid branch_name" do
- get api("/projects/#{project.id}/repository/blobs/invalid_branch_name?filepath=README.md", user)
- expect(response).to have_http_status(404)
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository raw blob' do
+ let(:project) { create(:project, :public) }
+ let(:current_user) { nil }
+ end
end
- it "returns 404 for invalid file" do
- get api("/projects/#{project.id}/repository/blobs/master?filepath=README.invalid", user)
- expect(response).to have_http_status(404)
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
end
- it "returns a 400 error if filepath is missing" do
- get api("/projects/#{project.id}/repository/blobs/master", user)
- expect(response).to have_http_status(400)
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository raw blob' do
+ let(:current_user) { user }
+ end
end
- end
- describe "GET /projects/:id/repository/commits/:sha/blob" do
- it "gets the raw file contents" do
- get api("/projects/#{project.id}/repository/commits/master/blob?filepath=README.md", user)
- expect(response).to have_http_status(200)
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, guest) }
+ end
end
end
- describe "GET /projects/:id/repository/raw_blobs/:sha" do
- it "gets the raw file contents" do
- get api("/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}", user)
- expect(response).to have_http_status(200)
- end
+ describe "GET /projects/:id/repository/archive(.:format)?:sha" do
+ let(:route) { "/projects/#{project.id}/repository/archive" }
+
+ shared_examples_for 'repository archive' do
+ it 'returns the repository archive' do
+ get api(route, current_user)
+
+ expect(response).to have_http_status(200)
- it 'returns a 404 for unknown blob' do
- get api("/projects/#{project.id}/repository/raw_blobs/123456", user)
- expect(response).to have_http_status(404)
+ repo_name = project.repository.name.gsub("\.git", "")
+ type, params = workhorse_send_data
- expect(json_response).to be_an Object
- json_response['message'] == '404 Blob Not Found'
+ expect(type).to eq('git-archive')
+ expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/)
+ end
+
+ it 'returns the repository archive archive.zip' do
+ get api("/projects/#{project.id}/repository/archive.zip", user)
+
+ expect(response).to have_http_status(200)
+
+ repo_name = project.repository.name.gsub("\.git", "")
+ type, params = workhorse_send_data
+
+ expect(type).to eq('git-archive')
+ expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/)
+ end
+
+ it 'returns the repository archive archive.tar.bz2' do
+ get api("/projects/#{project.id}/repository/archive.tar.bz2", user)
+
+ expect(response).to have_http_status(200)
+
+ repo_name = project.repository.name.gsub("\.git", "")
+ type, params = workhorse_send_data
+
+ expect(type).to eq('git-archive')
+ expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/)
+ end
+
+ context 'when sha does not exist' do
+ it_behaves_like '404 response' do
+ let(:request) { get api("#{route}?sha=xxx", current_user) }
+ let(:message) { '404 File Not Found' }
+ end
+ end
end
- end
- describe "GET /projects/:id/repository/archive(.:format)?:sha" do
- it "gets the archive" do
- get api("/projects/#{project.id}/repository/archive", user)
- repo_name = project.repository.name.gsub("\.git", "")
- expect(response).to have_http_status(200)
- type, params = workhorse_send_data
- expect(type).to eq('git-archive')
- expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/)
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository archive' do
+ let(:project) { create(:project, :public) }
+ let(:current_user) { nil }
+ end
end
- it "gets the archive.zip" do
- get api("/projects/#{project.id}/repository/archive.zip", user)
- repo_name = project.repository.name.gsub("\.git", "")
- expect(response).to have_http_status(200)
- type, params = workhorse_send_data
- expect(type).to eq('git-archive')
- expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/)
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
end
- it "gets the archive.tar.bz2" do
- get api("/projects/#{project.id}/repository/archive.tar.bz2", user)
- repo_name = project.repository.name.gsub("\.git", "")
- expect(response).to have_http_status(200)
- type, params = workhorse_send_data
- expect(type).to eq('git-archive')
- expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/)
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository archive' do
+ let(:current_user) { user }
+ end
end
- it "returns 404 for invalid sha" do
- get api("/projects/#{project.id}/repository/archive/?sha=xxx", user)
- expect(response).to have_http_status(404)
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, guest) }
+ end
end
end
describe 'GET /projects/:id/repository/compare' do
- it "compares branches" do
- get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'feature'
- expect(response).to have_http_status(200)
- expect(json_response['commits']).to be_present
- expect(json_response['diffs']).to be_present
+ let(:route) { "/projects/#{project.id}/repository/compare" }
+
+ shared_examples_for 'repository compare' do
+ it "compares branches" do
+ get api(route, current_user), from: 'master', to: 'feature'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['commits']).to be_present
+ expect(json_response['diffs']).to be_present
+ end
+
+ it "compares tags" do
+ get api(route, current_user), from: 'v1.0.0', to: 'v1.1.0'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['commits']).to be_present
+ expect(json_response['diffs']).to be_present
+ end
+
+ it "compares commits" do
+ get api(route, current_user), from: sample_commit.id, to: sample_commit.parent_id
+
+ expect(response).to have_http_status(200)
+ expect(json_response['commits']).to be_empty
+ expect(json_response['diffs']).to be_empty
+ expect(json_response['compare_same_ref']).to be_falsey
+ end
+
+ it "compares commits in reverse order" do
+ get api(route, current_user), from: sample_commit.parent_id, to: sample_commit.id
+
+ expect(response).to have_http_status(200)
+ expect(json_response['commits']).to be_present
+ expect(json_response['diffs']).to be_present
+ end
+
+ it "compares same refs" do
+ get api(route, current_user), from: 'master', to: 'master'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['commits']).to be_empty
+ expect(json_response['diffs']).to be_empty
+ expect(json_response['compare_same_ref']).to be_truthy
+ end
end
- it "compares tags" do
- get api("/projects/#{project.id}/repository/compare", user), from: 'v1.0.0', to: 'v1.1.0'
- expect(response).to have_http_status(200)
- expect(json_response['commits']).to be_present
- expect(json_response['diffs']).to be_present
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository compare' do
+ let(:project) { create(:project, :public) }
+ let(:current_user) { nil }
+ end
end
- it "compares commits" do
- get api("/projects/#{project.id}/repository/compare", user), from: sample_commit.id, to: sample_commit.parent_id
- expect(response).to have_http_status(200)
- expect(json_response['commits']).to be_empty
- expect(json_response['diffs']).to be_empty
- expect(json_response['compare_same_ref']).to be_falsey
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
end
- it "compares commits in reverse order" do
- get api("/projects/#{project.id}/repository/compare", user), from: sample_commit.parent_id, to: sample_commit.id
- expect(response).to have_http_status(200)
- expect(json_response['commits']).to be_present
- expect(json_response['diffs']).to be_present
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository compare' do
+ let(:current_user) { user }
+ end
end
- it "compares same refs" do
- get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'master'
- expect(response).to have_http_status(200)
- expect(json_response['commits']).to be_empty
- expect(json_response['diffs']).to be_empty
- expect(json_response['compare_same_ref']).to be_truthy
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, guest) }
+ end
end
end
describe 'GET /projects/:id/repository/contributors' do
- it 'returns valid data' do
- get api("/projects/#{project.id}/repository/contributors", user)
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- contributor = json_response.first
- expect(contributor['email']).to eq('tiagonbotelho@hotmail.com')
- expect(contributor['name']).to eq('tiagonbotelho')
- expect(contributor['commits']).to eq(1)
- expect(contributor['additions']).to eq(0)
- expect(contributor['deletions']).to eq(0)
+ let(:route) { "/projects/#{project.id}/repository/contributors" }
+
+ shared_examples_for 'repository contributors' do
+ it 'returns valid data' do
+ get api(route, current_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+
+ first_contributor = json_response.first
+
+ expect(first_contributor['email']).to eq('tiagonbotelho@hotmail.com')
+ expect(first_contributor['name']).to eq('tiagonbotelho')
+ expect(first_contributor['commits']).to eq(1)
+ expect(first_contributor['additions']).to eq(0)
+ expect(first_contributor['deletions']).to eq(0)
+ end
+ end
+
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository contributors' do
+ let(:project) { create(:project, :public) }
+ let(:current_user) { nil }
+ end
+ end
+
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
+
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository contributors' do
+ let(:current_user) { user }
+ end
+ end
+
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, guest) }
+ end
end
end
end
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index 80652129928..3b5dc98e4d5 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -37,6 +37,11 @@ describe Ci::API::Builds do
let(:user_agent) { 'Go-http-client/1.1' }
it { expect(response).to have_http_status(404) }
end
+
+ context "when runner doesn't have a User-Agent" do
+ let(:user_agent) { nil }
+ it { expect(response).to have_http_status(404) }
+ end
end
context 'when there is a pending build' do
@@ -249,7 +254,11 @@ describe Ci::API::Builds do
end
describe 'PATCH /builds/:id/trace.txt' do
- let(:build) { create(:ci_build, :pending, :trace, runner_id: runner.id) }
+ let(:build) do
+ attributes = { runner_id: runner.id, pipeline: pipeline }
+ create(:ci_build, :running, :trace, attributes)
+ end
+
let(:headers) { { Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token, 'Content-Type' => 'text/plain' } }
let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) }
let(:update_interval) { 10.seconds.to_i }
@@ -276,7 +285,6 @@ describe Ci::API::Builds do
end
before do
- build.run!
initial_patch_the_trace
end
@@ -329,6 +337,19 @@ describe Ci::API::Builds do
end
end
end
+
+ context 'when project for the build has been deleted' do
+ let(:build) do
+ attributes = { runner_id: runner.id, pipeline: pipeline }
+ create(:ci_build, :running, :trace, attributes) do |build|
+ build.project.update(pending_delete: true)
+ end
+ end
+
+ it 'responds with forbidden' do
+ expect(response.status).to eq(403)
+ end
+ end
end
context 'when Runner makes a force-patch' do
diff --git a/spec/routing/import_routing_spec.rb b/spec/routing/import_routing_spec.rb
new file mode 100644
index 00000000000..78ff9c6e6fd
--- /dev/null
+++ b/spec/routing/import_routing_spec.rb
@@ -0,0 +1,165 @@
+require 'spec_helper'
+
+# Shared examples for a resource inside a Project
+#
+# By default it tests all the default REST actions: index, create, new, edit,
+# show, update, and destroy. You can remove actions by customizing the
+# `actions` variable.
+#
+# It also expects a `controller` variable to be available which defines both
+# the path to the resource as well as the controller name.
+#
+# Examples
+#
+# # Default behavior
+# it_behaves_like 'RESTful project resources' do
+# let(:controller) { 'issues' }
+# end
+#
+# # Customizing actions
+# it_behaves_like 'RESTful project resources' do
+# let(:actions) { [:index] }
+# let(:controller) { 'issues' }
+# end
+shared_examples 'importer routing' do
+ let(:except_actions) { [] }
+
+ it 'to #create' do
+ expect(post("/import/#{provider}")).to route_to("import/#{provider}#create") unless except_actions.include?(:create)
+ end
+
+ it 'to #new' do
+ expect(get("/import/#{provider}/new")).to route_to("import/#{provider}#new") unless except_actions.include?(:new)
+ end
+
+ it 'to #status' do
+ expect(get("/import/#{provider}/status")).to route_to("import/#{provider}#status") unless except_actions.include?(:status)
+ end
+
+ it 'to #callback' do
+ expect(get("/import/#{provider}/callback")).to route_to("import/#{provider}#callback") unless except_actions.include?(:callback)
+ end
+
+ it 'to #jobs' do
+ expect(get("/import/#{provider}/jobs")).to route_to("import/#{provider}#jobs") unless except_actions.include?(:jobs)
+ end
+end
+
+# personal_access_token_import_github POST /import/github/personal_access_token(.:format) import/github#personal_access_token
+# status_import_github GET /import/github/status(.:format) import/github#status
+# callback_import_github GET /import/github/callback(.:format) import/github#callback
+# jobs_import_github GET /import/github/jobs(.:format) import/github#jobs
+# import_github POST /import/github(.:format) import/github#create
+# new_import_github GET /import/github/new(.:format) import/github#new
+describe Import::GithubController, 'routing' do
+ it_behaves_like 'importer routing' do
+ let(:provider) { 'github' }
+ end
+
+ it 'to #personal_access_token' do
+ expect(post('/import/github/personal_access_token')).to route_to('import/github#personal_access_token')
+ end
+end
+
+# personal_access_token_import_gitea POST /import/gitea/personal_access_token(.:format) import/gitea#personal_access_token
+# status_import_gitea GET /import/gitea/status(.:format) import/gitea#status
+# jobs_import_gitea GET /import/gitea/jobs(.:format) import/gitea#jobs
+# import_gitea POST /import/gitea(.:format) import/gitea#create
+# new_import_gitea GET /import/gitea/new(.:format) import/gitea#new
+describe Import::GiteaController, 'routing' do
+ it_behaves_like 'importer routing' do
+ let(:except_actions) { [:callback] }
+ let(:provider) { 'gitea' }
+ end
+
+ it 'to #personal_access_token' do
+ expect(post('/import/gitea/personal_access_token')).to route_to('import/gitea#personal_access_token')
+ end
+end
+
+# status_import_gitlab GET /import/gitlab/status(.:format) import/gitlab#status
+# callback_import_gitlab GET /import/gitlab/callback(.:format) import/gitlab#callback
+# jobs_import_gitlab GET /import/gitlab/jobs(.:format) import/gitlab#jobs
+# import_gitlab POST /import/gitlab(.:format) import/gitlab#create
+describe Import::GitlabController, 'routing' do
+ it_behaves_like 'importer routing' do
+ let(:except_actions) { [:new] }
+ let(:provider) { 'gitlab' }
+ end
+end
+
+# status_import_bitbucket GET /import/bitbucket/status(.:format) import/bitbucket#status
+# callback_import_bitbucket GET /import/bitbucket/callback(.:format) import/bitbucket#callback
+# jobs_import_bitbucket GET /import/bitbucket/jobs(.:format) import/bitbucket#jobs
+# import_bitbucket POST /import/bitbucket(.:format) import/bitbucket#create
+describe Import::BitbucketController, 'routing' do
+ it_behaves_like 'importer routing' do
+ let(:except_actions) { [:new] }
+ let(:provider) { 'bitbucket' }
+ end
+end
+
+# status_import_google_code GET /import/google_code/status(.:format) import/google_code#status
+# callback_import_google_code POST /import/google_code/callback(.:format) import/google_code#callback
+# jobs_import_google_code GET /import/google_code/jobs(.:format) import/google_code#jobs
+# new_user_map_import_google_code GET /import/google_code/user_map(.:format) import/google_code#new_user_map
+# create_user_map_import_google_code POST /import/google_code/user_map(.:format) import/google_code#create_user_map
+# import_google_code POST /import/google_code(.:format) import/google_code#create
+# new_import_google_code GET /import/google_code/new(.:format) import/google_code#new
+describe Import::GoogleCodeController, 'routing' do
+ it_behaves_like 'importer routing' do
+ let(:except_actions) { [:callback] }
+ let(:provider) { 'google_code' }
+ end
+
+ it 'to #callback' do
+ expect(post("/import/google_code/callback")).to route_to("import/google_code#callback")
+ end
+
+ it 'to #new_user_map' do
+ expect(get('/import/google_code/user_map')).to route_to('import/google_code#new_user_map')
+ end
+
+ it 'to #create_user_map' do
+ expect(post('/import/google_code/user_map')).to route_to('import/google_code#create_user_map')
+ end
+end
+
+# status_import_fogbugz GET /import/fogbugz/status(.:format) import/fogbugz#status
+# callback_import_fogbugz POST /import/fogbugz/callback(.:format) import/fogbugz#callback
+# jobs_import_fogbugz GET /import/fogbugz/jobs(.:format) import/fogbugz#jobs
+# new_user_map_import_fogbugz GET /import/fogbugz/user_map(.:format) import/fogbugz#new_user_map
+# create_user_map_import_fogbugz POST /import/fogbugz/user_map(.:format) import/fogbugz#create_user_map
+# import_fogbugz POST /import/fogbugz(.:format) import/fogbugz#create
+# new_import_fogbugz GET /import/fogbugz/new(.:format) import/fogbugz#new
+describe Import::FogbugzController, 'routing' do
+ it_behaves_like 'importer routing' do
+ let(:except_actions) { [:callback] }
+ let(:provider) { 'fogbugz' }
+ end
+
+ it 'to #callback' do
+ expect(post("/import/fogbugz/callback")).to route_to("import/fogbugz#callback")
+ end
+
+ it 'to #new_user_map' do
+ expect(get('/import/fogbugz/user_map')).to route_to('import/fogbugz#new_user_map')
+ end
+
+ it 'to #create_user_map' do
+ expect(post('/import/fogbugz/user_map')).to route_to('import/fogbugz#create_user_map')
+ end
+end
+
+# import_gitlab_project POST /import/gitlab_project(.:format) import/gitlab_projects#create
+# POST /import/gitlab_project(.:format) import/gitlab_projects#create
+# new_import_gitlab_project GET /import/gitlab_project/new(.:format) import/gitlab_projects#new
+describe Import::GitlabProjectsController, 'routing' do
+ it 'to #create' do
+ expect(post('/import/gitlab_project')).to route_to('import/gitlab_projects#create')
+ end
+
+ it 'to #new' do
+ expect(get('/import/gitlab_project/new')).to route_to('import/gitlab_projects#new')
+ end
+end
diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb
index 9c2331144a0..531180e48a1 100644
--- a/spec/services/groups/update_service_spec.rb
+++ b/spec/services/groups/update_service_spec.rb
@@ -1,15 +1,15 @@
require 'spec_helper'
describe Groups::UpdateService, services: true do
- let!(:user) { create(:user) }
- let!(:private_group) { create(:group, :private) }
- let!(:internal_group) { create(:group, :internal) }
- let!(:public_group) { create(:group, :public) }
+ let!(:user) { create(:user) }
+ let!(:private_group) { create(:group, :private) }
+ let!(:internal_group) { create(:group, :internal) }
+ let!(:public_group) { create(:group, :public) }
describe "#execute" do
context "project visibility_level validation" do
context "public group with public projects" do
- let!(:service) { described_class.new(public_group, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL ) }
+ let!(:service) { described_class.new(public_group, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL) }
before do
public_group.add_user(user, Gitlab::Access::MASTER)
@@ -23,7 +23,7 @@ describe Groups::UpdateService, services: true do
end
context "internal group with internal project" do
- let!(:service) { described_class.new(internal_group, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE ) }
+ let!(:service) { described_class.new(internal_group, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
before do
internal_group.add_user(user, Gitlab::Access::MASTER)
@@ -39,7 +39,7 @@ describe Groups::UpdateService, services: true do
end
context "unauthorized visibility_level validation" do
- let!(:service) { described_class.new(internal_group, user, visibility_level: 99 ) }
+ let!(:service) { described_class.new(internal_group, user, visibility_level: 99) }
before do
internal_group.add_user(user, Gitlab::Access::MASTER)
end
@@ -49,4 +49,41 @@ describe Groups::UpdateService, services: true do
expect(internal_group.errors.count).to eq(1)
end
end
+
+ context 'rename group' do
+ let!(:service) { described_class.new(internal_group, user, path: 'new_path') }
+
+ before do
+ internal_group.add_user(user, Gitlab::Access::MASTER)
+ create(:project, :internal, group: internal_group)
+ end
+
+ it 'returns true' do
+ expect(service.execute).to eq(true)
+ end
+
+ context 'error moving group' do
+ before do
+ allow(internal_group).to receive(:move_dir).and_raise(Gitlab::UpdatePathError)
+ end
+
+ it 'does not raise an error' do
+ expect { service.execute }.not_to raise_error
+ end
+
+ it 'returns false' do
+ expect(service.execute).to eq(false)
+ end
+
+ it 'has the right error' do
+ service.execute
+
+ expect(internal_group.errors.full_messages.first).to eq('Gitlab::UpdatePathError')
+ end
+
+ it "hasn't changed the path" do
+ expect { service.execute}.not_to change { internal_group.reload.path}
+ end
+ end
+ end
end
diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb
new file mode 100644
index 00000000000..72c8f7cd8ec
--- /dev/null
+++ b/spec/services/users/refresh_authorized_projects_service_spec.rb
@@ -0,0 +1,185 @@
+require 'spec_helper'
+
+describe Users::RefreshAuthorizedProjectsService do
+ let(:project) { create(:empty_project) }
+ let(:user) { project.namespace.owner }
+ let(:service) { described_class.new(user) }
+
+ def create_authorization(project, user, access_level = Gitlab::Access::MASTER)
+ ProjectAuthorization.
+ create!(project: project, user: user, access_level: access_level)
+ end
+
+ describe '#execute' do
+ before do
+ user.project_authorizations.delete_all
+ end
+
+ it 'updates the authorized projects of the user' do
+ project2 = create(:empty_project)
+ to_remove = create_authorization(project2, user)
+
+ expect(service).to receive(:update_with_lease).
+ with([to_remove.id], [[user.id, project.id, Gitlab::Access::MASTER]])
+
+ service.execute
+ end
+
+ it 'sets the access level of a project to the highest available level' do
+ to_remove = create_authorization(project, user, Gitlab::Access::DEVELOPER)
+
+ expect(service).to receive(:update_with_lease).
+ with([to_remove.id], [[user.id, project.id, Gitlab::Access::MASTER]])
+
+ service.execute
+ end
+
+ it 'returns a User' do
+ expect(service.execute).to be_an_instance_of(User)
+ end
+ end
+
+ describe '#update_with_lease', :redis do
+ it 'refreshes the authorizations using a lease' do
+ expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).
+ and_return('foo')
+
+ expect(Gitlab::ExclusiveLease).to receive(:cancel).
+ with(an_instance_of(String), 'foo')
+
+ expect(service).to receive(:update_authorizations).with([1], [])
+
+ service.update_with_lease([1])
+ end
+ end
+
+ describe '#update_authorizations' do
+ it 'does nothing when there are no rows to add and remove' do
+ expect(user).not_to receive(:remove_project_authorizations)
+ expect(ProjectAuthorization).not_to receive(:insert_authorizations)
+ expect(user).not_to receive(:set_authorized_projects_column)
+
+ service.update_authorizations([], [])
+ end
+
+ it 'removes authorizations that should be removed' do
+ authorization = create_authorization(project, user)
+
+ service.update_authorizations([authorization.id])
+
+ expect(user.project_authorizations).to be_empty
+ end
+
+ it 'inserts authorizations that should be added' do
+ service.update_authorizations([], [[user.id, project.id, Gitlab::Access::MASTER]])
+
+ authorizations = user.project_authorizations
+
+ expect(authorizations.length).to eq(1)
+ expect(authorizations[0].user_id).to eq(user.id)
+ expect(authorizations[0].project_id).to eq(project.id)
+ expect(authorizations[0].access_level).to eq(Gitlab::Access::MASTER)
+ end
+
+ it 'populates the authorized projects column' do
+ # make sure we start with a nil value no matter what the default in the
+ # factory may be.
+ user.update(authorized_projects_populated: nil)
+
+ service.update_authorizations([], [[user.id, project.id, Gitlab::Access::MASTER]])
+
+ expect(user.authorized_projects_populated).to eq(true)
+ end
+ end
+
+ describe '#fresh_access_levels_per_project' do
+ let(:hash) { service.fresh_access_levels_per_project }
+
+ it 'returns a Hash' do
+ expect(hash).to be_an_instance_of(Hash)
+ end
+
+ it 'sets the keys to the project IDs' do
+ expect(hash.keys).to eq([project.id])
+ end
+
+ it 'sets the values to the access levels' do
+ expect(hash.values).to eq([Gitlab::Access::MASTER])
+ end
+ end
+
+ describe '#current_authorizations_per_project' do
+ before { create_authorization(project, user) }
+
+ let(:hash) { service.current_authorizations_per_project }
+
+ it 'returns a Hash' do
+ expect(hash).to be_an_instance_of(Hash)
+ end
+
+ it 'sets the keys to the project IDs' do
+ expect(hash.keys).to eq([project.id])
+ end
+
+ it 'sets the values to the project authorization rows' do
+ expect(hash.values).to eq([ProjectAuthorization.first])
+ end
+ end
+
+ describe '#current_authorizations' do
+ context 'without authorizations' do
+ it 'returns an empty list' do
+ expect(service.current_authorizations.empty?).to eq(true)
+ end
+ end
+
+ context 'with an authorization' do
+ before { create_authorization(project, user) }
+
+ let(:row) { service.current_authorizations.take }
+
+ it 'returns the currently authorized projects' do
+ expect(service.current_authorizations.length).to eq(1)
+ end
+
+ it 'includes the row ID for every row' do
+ expect(row.id).to be_a_kind_of(Numeric)
+ end
+
+ it 'includes the project ID for every row' do
+ expect(row.project_id).to eq(project.id)
+ end
+
+ it 'includes the access level for every row' do
+ expect(row.access_level).to eq(Gitlab::Access::MASTER)
+ end
+ end
+ end
+
+ describe '#fresh_authorizations' do
+ it 'returns the new authorized projects' do
+ expect(service.fresh_authorizations.length).to eq(1)
+ end
+
+ it 'returns the highest access level' do
+ project.team.add_guest(user)
+
+ rows = service.fresh_authorizations.to_a
+
+ expect(rows.length).to eq(1)
+ expect(rows.first.access_level).to eq(Gitlab::Access::MASTER)
+ end
+
+ context 'every returned row' do
+ let(:row) { service.fresh_authorizations.take }
+
+ it 'includes the project ID' do
+ expect(row.project_id).to eq(project.id)
+ end
+
+ it 'includes the access level' do
+ expect(row.access_level).to eq(Gitlab::Access::MASTER)
+ end
+ end
+ end
+end
diff --git a/spec/support/api/repositories_shared_context.rb b/spec/support/api/repositories_shared_context.rb
new file mode 100644
index 00000000000..ea38fe4f5b8
--- /dev/null
+++ b/spec/support/api/repositories_shared_context.rb
@@ -0,0 +1,10 @@
+shared_context 'disabled repository' do
+ before do
+ project.project_feature.update_attributes!(
+ repository_access_level: ProjectFeature::DISABLED,
+ merge_requests_access_level: ProjectFeature::DISABLED,
+ builds_access_level: ProjectFeature::DISABLED
+ )
+ expect(project.feature_available?(:repository, current_user)).to be false
+ end
+end
diff --git a/spec/support/api/status_shared_examples.rb b/spec/support/api/status_shared_examples.rb
new file mode 100644
index 00000000000..3481749a7f0
--- /dev/null
+++ b/spec/support/api/status_shared_examples.rb
@@ -0,0 +1,42 @@
+# Specs for status checking.
+#
+# Requires an API request:
+# let(:request) { get api("/projects/#{project.id}/repository/branches", user) }
+shared_examples_for '400 response' do
+ before do
+ # Fires the request
+ request
+ end
+
+ it 'returns 400' do
+ expect(response).to have_http_status(400)
+ end
+end
+
+shared_examples_for '403 response' do
+ before do
+ # Fires the request
+ request
+ end
+
+ it 'returns 403' do
+ expect(response).to have_http_status(403)
+ end
+end
+
+shared_examples_for '404 response' do
+ let(:message) { nil }
+ before do
+ # Fires the request
+ request
+ end
+
+ it 'returns 404' do
+ expect(response).to have_http_status(404)
+ expect(json_response).to be_an Object
+
+ if message.present?
+ expect(json_response['message']).to eq(message)
+ end
+ end
+end
diff --git a/spec/support/chat_slash_commands_shared_examples.rb b/spec/support/chat_slash_commands_shared_examples.rb
new file mode 100644
index 00000000000..4dfa29849ee
--- /dev/null
+++ b/spec/support/chat_slash_commands_shared_examples.rb
@@ -0,0 +1,97 @@
+RSpec.shared_examples 'chat slash commands service' do
+ describe "Associations" do
+ it { is_expected.to respond_to :token }
+ it { is_expected.to have_many :chat_names }
+ end
+
+ describe '#valid_token?' do
+ subject { described_class.new }
+
+ context 'when the token is empty' do
+ it 'is false' do
+ expect(subject.valid_token?('wer')).to be_falsey
+ end
+ end
+
+ context 'when there is a token' do
+ before do
+ subject.token = '123'
+ end
+
+ it 'accepts equal tokens' do
+ expect(subject.valid_token?('123')).to be_truthy
+ end
+ end
+ end
+
+ describe '#trigger' do
+ subject { described_class.new }
+
+ context 'no token is passed' do
+ let(:params) { Hash.new }
+
+ it 'returns nil' do
+ expect(subject.trigger(params)).to be_nil
+ end
+ end
+
+ context 'with a token passed' do
+ let(:project) { create(:empty_project) }
+ let(:params) { { token: 'token' } }
+
+ before do
+ allow(subject).to receive(:token).and_return('token')
+ end
+
+ context 'no user can be found' do
+ context 'when no url can be generated' do
+ it 'responds with the authorize url' do
+ response = subject.trigger(params)
+
+ expect(response[:response_type]).to eq :ephemeral
+ expect(response[:text]).to start_with ":sweat_smile: Couldn't identify you"
+ end
+ end
+
+ context 'when an auth url can be generated' do
+ let(:params) do
+ {
+ team_domain: 'http://domain.tld',
+ team_id: 'T3423423',
+ user_id: 'U234234',
+ user_name: 'mepmep',
+ token: 'token'
+ }
+ end
+
+ let(:service) do
+ project.create_mattermost_slash_commands_service(
+ properties: { token: 'token' }
+ )
+ end
+
+ it 'generates the url' do
+ response = service.trigger(params)
+
+ expect(response[:text]).to start_with(':wave: Hi there!')
+ end
+ end
+ end
+
+ context 'when the user is authenticated' do
+ let!(:chat_name) { create(:chat_name, service: subject) }
+ let(:params) { { token: 'token', team_id: chat_name.team_id, user_id: chat_name.chat_id } }
+
+ subject do
+ described_class.create(project: project, properties: { token: 'token' })
+ end
+
+ it 'triggers the command' do
+ expect_any_instance_of(Gitlab::ChatCommands::Command).to receive(:execute)
+
+ subject.trigger(params)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/controllers/githubish_import_controller_shared_context.rb b/spec/support/controllers/githubish_import_controller_shared_context.rb
new file mode 100644
index 00000000000..e71994edec6
--- /dev/null
+++ b/spec/support/controllers/githubish_import_controller_shared_context.rb
@@ -0,0 +1,10 @@
+shared_context 'a GitHub-ish import controller' do
+ let(:user) { create(:user) }
+ let(:token) { "asdasd12345" }
+ let(:access_params) { { github_access_token: token } }
+
+ before do
+ sign_in(user)
+ allow(controller).to receive(:"#{provider}_import_enabled?").and_return(true)
+ end
+end
diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb
new file mode 100644
index 00000000000..d0fd2d52004
--- /dev/null
+++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb
@@ -0,0 +1,232 @@
+# Specifications for behavior common to all objects with an email attribute.
+# Takes a list of email-format attributes and requires:
+# - subject { "the object with a attribute= setter" }
+# Note: You have access to `email_value` which is the email address value
+# being currently tested).
+
+def assign_session_token(provider)
+ session[:"#{provider}_access_token"] = 'asdasd12345'
+end
+
+shared_examples 'a GitHub-ish import controller: POST personal_access_token' do
+ let(:status_import_url) { public_send("status_import_#{provider}_url") }
+
+ it "updates access token" do
+ token = 'asdfasdf9876'
+
+ allow_any_instance_of(Gitlab::GithubImport::Client).
+ to receive(:user).and_return(true)
+
+ post :personal_access_token, personal_access_token: token
+
+ expect(session[:"#{provider}_access_token"]).to eq(token)
+ expect(controller).to redirect_to(status_import_url)
+ end
+end
+
+shared_examples 'a GitHub-ish import controller: GET new' do
+ let(:status_import_url) { public_send("status_import_#{provider}_url") }
+
+ it "redirects to status if we already have a token" do
+ assign_session_token(provider)
+ allow(controller).to receive(:logged_in_with_provider?).and_return(false)
+
+ get :new
+
+ expect(controller).to redirect_to(status_import_url)
+ end
+
+ it "renders the :new page if no token is present in session" do
+ get :new
+
+ expect(response).to render_template(:new)
+ end
+end
+
+shared_examples 'a GitHub-ish import controller: GET status' do
+ let(:new_import_url) { public_send("new_import_#{provider}_url") }
+ let(:user) { create(:user) }
+ let(:repo) { OpenStruct.new(login: 'vim', full_name: 'asd/vim') }
+ let(:org) { OpenStruct.new(login: 'company') }
+ let(:org_repo) { OpenStruct.new(login: 'company', full_name: 'company/repo') }
+ let(:extra_assign_expectations) { {} }
+
+ before do
+ assign_session_token(provider)
+ end
+
+ it "assigns variables" do
+ project = create(:empty_project, import_type: provider, creator_id: user.id)
+ stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo])
+
+ get :status
+
+ expect(assigns(:already_added_projects)).to eq([project])
+ expect(assigns(:repos)).to eq([repo, org_repo])
+ extra_assign_expectations.each do |key, value|
+ expect(assigns(key)).to eq(value)
+ end
+ end
+
+ it "does not show already added project" do
+ project = create(:empty_project, import_type: provider, creator_id: user.id, import_source: 'asd/vim')
+ stub_client(repos: [repo], orgs: [])
+
+ get :status
+
+ expect(assigns(:already_added_projects)).to eq([project])
+ expect(assigns(:repos)).to eq([])
+ end
+
+ it "handles an invalid access token" do
+ allow_any_instance_of(Gitlab::GithubImport::Client).
+ to receive(:repos).and_raise(Octokit::Unauthorized)
+
+ get :status
+
+ expect(session[:"#{provider}_access_token"]).to be_nil
+ expect(controller).to redirect_to(new_import_url)
+ expect(flash[:alert]).to eq("Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account.")
+ end
+end
+
+shared_examples 'a GitHub-ish import controller: POST create' do
+ let(:user) { create(:user) }
+ let(:provider_username) { user.username }
+ let(:provider_user) { OpenStruct.new(login: provider_username) }
+ let(:provider_repo) do
+ OpenStruct.new(
+ name: 'vim',
+ full_name: "#{provider_username}/vim",
+ owner: OpenStruct.new(login: provider_username)
+ )
+ end
+
+ before do
+ stub_client(user: provider_user, repo: provider_repo)
+ assign_session_token(provider)
+ end
+
+ context "when the repository owner is the provider user" do
+ context "when the provider user and GitLab user's usernames match" do
+ it "takes the current user's namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
+ end
+
+ context "when the provider user and GitLab user's usernames don't match" do
+ let(:provider_username) { "someone_else" }
+
+ it "takes the current user's namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
+ end
+ end
+
+ context "when the repository owner is not the provider user" do
+ let(:other_username) { "someone_else" }
+
+ before do
+ provider_repo.owner = OpenStruct.new(login: other_username)
+ assign_session_token(provider)
+ end
+
+ context "when a namespace with the provider user's username already exists" do
+ let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) }
+
+ context "when the namespace is owned by the GitLab user" do
+ it "takes the existing namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(provider_repo, provider_repo.name, existing_namespace, user, access_params, type: provider).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
+ end
+
+ context "when the namespace is not owned by the GitLab user" do
+ before do
+ existing_namespace.owner = create(:user)
+ existing_namespace.save
+ end
+
+ it "creates a project using user's namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
+ end
+ end
+
+ context "when a namespace with the provider user's username doesn't exist" do
+ context "when current user can create namespaces" do
+ it "creates the namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).and_return(double(execute: true))
+
+ expect { post :create, target_namespace: provider_repo.name, format: :js }.to change(Namespace, :count).by(1)
+ end
+
+ it "takes the new namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(provider_repo, provider_repo.name, an_instance_of(Group), user, access_params, type: provider).
+ and_return(double(execute: true))
+
+ post :create, target_namespace: provider_repo.name, format: :js
+ end
+ end
+
+ context "when current user can't create namespaces" do
+ before do
+ user.update_attribute(:can_create_group, false)
+ end
+
+ it "doesn't create the namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).and_return(double(execute: true))
+
+ expect { post :create, format: :js }.not_to change(Namespace, :count)
+ end
+
+ it "takes the current user's namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
+ end
+ end
+
+ context 'user has chosen a namespace and name for the project' do
+ let(:test_namespace) { create(:namespace, name: 'test_namespace', owner: user) }
+ let(:test_name) { 'test_name' }
+
+ it 'takes the selected namespace and name' do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(provider_repo, test_name, test_namespace, user, access_params, type: provider).
+ and_return(double(execute: true))
+
+ post :create, { target_namespace: test_namespace.name, new_name: test_name, format: :js }
+ end
+
+ it 'takes the selected name and default namespace' do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider).
+ and_return(double(execute: true))
+
+ post :create, { new_name: test_name, format: :js }
+ end
+ end
+ end
+end
diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb
new file mode 100644
index 00000000000..6c4c246a68b
--- /dev/null
+++ b/spec/support/kubernetes_helpers.rb
@@ -0,0 +1,52 @@
+module KubernetesHelpers
+ include Gitlab::Kubernetes
+
+ def kube_discovery_body
+ { "kind" => "APIResourceList",
+ "resources" => [
+ { "name" => "pods", "namespaced" => true, "kind" => "Pod" },
+ ],
+ }
+ end
+
+ def kube_pods_body(*pods)
+ { "kind" => "PodList",
+ "items" => [ kube_pod ],
+ }
+ end
+
+ # This is a partial response, it will have many more elements in reality but
+ # these are the ones we care about at the moment
+ def kube_pod(app: "valid-pod-label")
+ { "metadata" => {
+ "name" => "kube-pod",
+ "creationTimestamp" => "2016-11-25T19:55:19Z",
+ "labels" => { "app" => app },
+ },
+ "spec" => {
+ "containers" => [
+ { "name" => "container-0" },
+ { "name" => "container-1" },
+ ],
+ },
+ "status" => { "phase" => "Running" },
+ }
+ end
+
+ def kube_terminals(service, pod)
+ pod_name = pod['metadata']['name']
+ containers = pod['spec']['containers']
+
+ containers.map do |container|
+ terminal = {
+ selectors: { pod: pod_name, container: container['name'] },
+ url: container_exec_url(service.api_url, service.namespace, pod_name, container['name']),
+ subprotocols: ['channel.k8s.io'],
+ headers: { 'Authorization' => ["Bearer #{service.token}"] },
+ created_at: DateTime.parse(pod['metadata']['creationTimestamp'])
+ }
+ terminal[:ca_pem] = service.ca_pem if service.ca_pem.present?
+ terminal
+ end
+ end
+end
diff --git a/spec/support/query_recorder.rb b/spec/support/query_recorder.rb
new file mode 100644
index 00000000000..e40d5ebd9a8
--- /dev/null
+++ b/spec/support/query_recorder.rb
@@ -0,0 +1,40 @@
+module ActiveRecord
+ class QueryRecorder
+ attr_reader :log
+
+ def initialize(&block)
+ @log = []
+ ActiveSupport::Notifications.subscribed(method(:callback), 'sql.active_record', &block)
+ end
+
+ def callback(name, start, finish, message_id, values)
+ return if %w(CACHE SCHEMA).include?(values[:name])
+ @log << values[:sql]
+ end
+
+ def count
+ @log.count
+ end
+
+ def log_message
+ @log.join("\n\n")
+ end
+ end
+end
+
+RSpec::Matchers.define :exceed_query_limit do |expected|
+ supports_block_expectations
+
+ match do |block|
+ query_count(&block) > expected
+ end
+
+ failure_message_when_negated do |actual|
+ "Expected a maximum of #{expected} queries, got #{@recorder.count}:\n\n#{@recorder.log_message}"
+ end
+
+ def query_count(&block)
+ @recorder = ActiveRecord::QueryRecorder.new(&block)
+ @recorder.count
+ end
+end
diff --git a/spec/support/reactive_caching_helpers.rb b/spec/support/reactive_caching_helpers.rb
new file mode 100644
index 00000000000..279db3c5748
--- /dev/null
+++ b/spec/support/reactive_caching_helpers.rb
@@ -0,0 +1,38 @@
+module ReactiveCachingHelpers
+ def reactive_cache_key(subject, *qualifiers)
+ ([subject.class.reactive_cache_key.call(subject)].flatten + qualifiers).join(':')
+ end
+
+ def stub_reactive_cache(subject = nil, data = nil)
+ allow(ReactiveCachingWorker).to receive(:perform_async)
+ allow(ReactiveCachingWorker).to receive(:perform_in)
+ write_reactive_cache(subject, data) if data
+ end
+
+ def read_reactive_cache(subject)
+ Rails.cache.read(reactive_cache_key(subject))
+ end
+
+ def write_reactive_cache(subject, data)
+ start_reactive_cache_lifetime(subject)
+ Rails.cache.write(reactive_cache_key(subject), data)
+ end
+
+ def reactive_cache_alive?(subject)
+ Rails.cache.read(reactive_cache_key(subject, 'alive'))
+ end
+
+ def invalidate_reactive_cache(subject)
+ Rails.cache.delete(reactive_cache_key(subject, 'alive'))
+ end
+
+ def start_reactive_cache_lifetime(subject)
+ Rails.cache.write(reactive_cache_key(subject, 'alive'), true)
+ end
+
+ def expect_reactive_cache_update_queued(subject)
+ expect(ReactiveCachingWorker).
+ to receive(:perform_in).
+ with(subject.class.reactive_cache_refresh_interval, subject.class, subject.id)
+ end
+end
diff --git a/spec/support/slack_mattermost_shared_examples.rb b/spec/support/slack_mattermost_notifications_shared_examples.rb
index 56d4965f74d..8582aea5fe5 100644
--- a/spec/support/slack_mattermost_shared_examples.rb
+++ b/spec/support/slack_mattermost_notifications_shared_examples.rb
@@ -1,6 +1,6 @@
Dir[Rails.root.join("app/models/project_services/chat_message/*.rb")].each { |f| require f }
-RSpec.shared_examples 'slack or mattermost' do
+RSpec.shared_examples 'slack or mattermost notifications' do
let(:chat_service) { described_class.new }
let(:webhook_url) { 'https://example.gitlab.com/' }
diff --git a/spec/views/projects/pipelines/_stage.html.haml_spec.rb b/spec/views/projects/pipelines/_stage.html.haml_spec.rb
new file mode 100644
index 00000000000..eb7f7ca4a1a
--- /dev/null
+++ b/spec/views/projects/pipelines/_stage.html.haml_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe 'projects/pipelines/_stage', :view do
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:stage) { build(:ci_stage, pipeline: pipeline) }
+
+ before do
+ assign :stage, stage
+
+ create(:ci_build, name: 'test:build',
+ stage: stage.name,
+ pipeline: pipeline)
+ end
+
+ it 'shows the builds in the stage' do
+ render
+
+ expect(rendered).to have_text 'test:build'
+ end
+end
diff --git a/spec/workers/authorized_projects_worker_spec.rb b/spec/workers/authorized_projects_worker_spec.rb
index 95e2458da35..b6591f272f6 100644
--- a/spec/workers/authorized_projects_worker_spec.rb
+++ b/spec/workers/authorized_projects_worker_spec.rb
@@ -7,27 +7,17 @@ describe AuthorizedProjectsWorker do
it "refreshes user's authorized projects" do
user = create(:user)
- expect(worker).to receive(:refresh).with(an_instance_of(User))
+ expect_any_instance_of(User).to receive(:refresh_authorized_projects)
worker.perform(user.id)
end
context "when the user is not found" do
it "does nothing" do
- expect(worker).not_to receive(:refresh)
+ expect_any_instance_of(User).not_to receive(:refresh_authorized_projects)
described_class.new.perform(-1)
end
end
end
-
- describe '#refresh', redis: true do
- it 'refreshes the authorized projects of the user' do
- user = create(:user)
-
- expect(user).to receive(:refresh_authorized_projects)
-
- worker.refresh(user)
- end
- end
end
diff --git a/spec/workers/reactive_caching_worker_spec.rb b/spec/workers/reactive_caching_worker_spec.rb
new file mode 100644
index 00000000000..5f4453c15d6
--- /dev/null
+++ b/spec/workers/reactive_caching_worker_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe ReactiveCachingWorker do
+ let(:project) { create(:kubernetes_project) }
+ let(:service) { project.deployment_service }
+ subject { described_class.new.perform("KubernetesService", service.id) }
+
+ describe '#perform' do
+ it 'calls #exclusively_update_reactive_cache!' do
+ expect_any_instance_of(KubernetesService).to receive(:exclusively_update_reactive_cache!)
+
+ subject
+ end
+ end
+end
diff --git a/vendor/assets/javascripts/katex.js b/vendor/assets/javascripts/katex.js
index beb31ca6a7e..6b59a3477a7 100644
--- a/vendor/assets/javascripts/katex.js
+++ b/vendor/assets/javascripts/katex.js
@@ -35,8 +35,10 @@
1. Clone KaTeX. Anything later than 4fb9445a9 (is merged into master) will do.
2. make (requires node)
- 3. sed -i 's,fonts/,,' build/katex.css
- 4. Copy build/katex.js, build/katex.css and fonts/* to gitlab.
+ 3. sed -e 's,fonts/,,' -e 's/url\(([^)]*)\)/url(font-path\1)/g' build/katex.css > build/katex.scss
+ 4. Copy build/katex.js to gitlab/vendor/assets/javascripts/katex.js,
+ build/katex.scss to gitlab/vendor/assets/stylesheets/katex.scss and
+ fonts/* to gitlab/vendor/assets/fonts/.
*/
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.katex = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
diff --git a/vendor/assets/javascripts/xterm/fit.js b/vendor/assets/javascripts/xterm/fit.js
new file mode 100644
index 00000000000..7e24fd9b36e
--- /dev/null
+++ b/vendor/assets/javascripts/xterm/fit.js
@@ -0,0 +1,86 @@
+/*
+ * Fit terminal columns and rows to the dimensions of its
+ * DOM element.
+ *
+ * Approach:
+ * - Rows: Truncate the division of the terminal parent element height
+ * by the terminal row height
+ *
+ * - Columns: Truncate the division of the terminal parent element width by
+ * the terminal character width (apply display: inline at the
+ * terminal row and truncate its width with the current number
+ * of columns)
+ */
+(function (fit) {
+ if (typeof exports === 'object' && typeof module === 'object') {
+ /*
+ * CommonJS environment
+ */
+ module.exports = fit(require('../../xterm'));
+ } else if (typeof define == 'function') {
+ /*
+ * Require.js is available
+ */
+ define(['../../xterm'], fit);
+ } else {
+ /*
+ * Plain browser environment
+ */
+ fit(window.Terminal);
+ }
+})(function (Xterm) {
+ /**
+ * This module provides methods for fitting a terminal's size to a parent container.
+ *
+ * @module xterm/addons/fit/fit
+ */
+ var exports = {};
+
+ exports.proposeGeometry = function (term) {
+ var parentElementStyle = window.getComputedStyle(term.element.parentElement),
+ parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')),
+ parentElementWidth = parseInt(parentElementStyle.getPropertyValue('width')),
+ elementStyle = window.getComputedStyle(term.element),
+ elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom')),
+ elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left')),
+ availableHeight = parentElementHeight - elementPaddingVer,
+ availableWidth = parentElementWidth - elementPaddingHor,
+ container = term.rowContainer,
+ subjectRow = term.rowContainer.firstElementChild,
+ contentBuffer = subjectRow.innerHTML,
+ characterHeight,
+ rows,
+ characterWidth,
+ cols,
+ geometry;
+
+ subjectRow.style.display = 'inline';
+ subjectRow.innerHTML = 'W'; // Common character for measuring width, although on monospace
+ characterWidth = subjectRow.getBoundingClientRect().width;
+ subjectRow.style.display = ''; // Revert style before calculating height, since they differ.
+ characterHeight = parseInt(subjectRow.offsetHeight);
+ subjectRow.innerHTML = contentBuffer;
+
+ rows = parseInt(availableHeight / characterHeight);
+ cols = parseInt(availableWidth / characterWidth) - 1;
+
+ geometry = {cols: cols, rows: rows};
+ return geometry;
+ };
+
+ exports.fit = function (term) {
+ var geometry = exports.proposeGeometry(term);
+
+ term.resize(geometry.cols, geometry.rows);
+ };
+
+ Xterm.prototype.proposeGeometry = function () {
+ return exports.proposeGeometry(this);
+ };
+
+ Xterm.prototype.fit = function () {
+ return exports.fit(this);
+ };
+
+ return exports;
+});
diff --git a/vendor/assets/javascripts/xterm/xterm.js b/vendor/assets/javascripts/xterm/xterm.js
new file mode 100644
index 00000000000..11ce3c73db9
--- /dev/null
+++ b/vendor/assets/javascripts/xterm/xterm.js
@@ -0,0 +1,2235 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Terminal = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+/**
+ * xterm.js: xterm, in the browser
+ * Copyright (c) 2014-2016, SourceLair Private Company (www.sourcelair.com (MIT License)
+ */
+
+/**
+ * Encapsulates the logic for handling compositionstart, compositionupdate and compositionend
+ * events, displaying the in-progress composition to the UI and forwarding the final composition
+ * to the handler.
+ * @param {HTMLTextAreaElement} textarea The textarea that xterm uses for input.
+ * @param {HTMLElement} compositionView The element to display the in-progress composition in.
+ * @param {Terminal} terminal The Terminal to forward the finished composition to.
+ */
+function CompositionHelper(textarea, compositionView, terminal) {
+ this.textarea = textarea;
+ this.compositionView = compositionView;
+ this.terminal = terminal;
+
+ // Whether input composition is currently happening, eg. via a mobile keyboard, speech input
+ // or IME. This variable determines whether the compositionText should be displayed on the UI.
+ this.isComposing = false;
+
+ // The input currently being composed, eg. via a mobile keyboard, speech input or IME.
+ this.compositionText = null;
+
+ // The position within the input textarea's value of the current composition.
+ this.compositionPosition = { start: null, end: null };
+
+ // Whether a composition is in the process of being sent, setting this to false will cancel
+ // any in-progress composition.
+ this.isSendingComposition = false;
+}
+
+/**
+ * Handles the compositionstart event, activating the composition view.
+ */
+CompositionHelper.prototype.compositionstart = function () {
+ this.isComposing = true;
+ this.compositionPosition.start = this.textarea.value.length;
+ this.compositionView.textContent = '';
+ this.compositionView.classList.add('active');
+};
+
+/**
+ * Handles the compositionupdate event, updating the composition view.
+ * @param {CompositionEvent} ev The event.
+ */
+CompositionHelper.prototype.compositionupdate = function (ev) {
+ this.compositionView.textContent = ev.data;
+ this.updateCompositionElements();
+ var self = this;
+ setTimeout(function () {
+ self.compositionPosition.end = self.textarea.value.length;
+ }, 0);
+};
+
+/**
+ * Handles the compositionend event, hiding the composition view and sending the composition to
+ * the handler.
+ */
+CompositionHelper.prototype.compositionend = function () {
+ this.finalizeComposition(true);
+};
+
+/**
+ * Handles the keydown event, routing any necessary events to the CompositionHelper functions.
+ * @return Whether the Terminal should continue processing the keydown event.
+ */
+CompositionHelper.prototype.keydown = function (ev) {
+ if (this.isComposing || this.isSendingComposition) {
+ if (ev.keyCode === 229) {
+ // Continue composing if the keyCode is the "composition character"
+ return false;
+ } else if (ev.keyCode === 16 || ev.keyCode === 17 || ev.keyCode === 18) {
+ // Continue composing if the keyCode is a modifier key
+ return false;
+ } else {
+ // Finish composition immediately. This is mainly here for the case where enter is
+ // pressed and the handler needs to be triggered before the command is executed.
+ this.finalizeComposition(false);
+ }
+ }
+
+ if (ev.keyCode === 229) {
+ // If the "composition character" is used but gets to this point it means a non-composition
+ // character (eg. numbers and punctuation) was pressed when the IME was active.
+ this.handleAnyTextareaChanges();
+ return false;
+ }
+
+ return true;
+};
+
+/**
+ * Finalizes the composition, resuming regular input actions. This is called when a composition
+ * is ending.
+ * @param {boolean} waitForPropogation Whether to wait for events to propogate before sending
+ * the input. This should be false if a non-composition keystroke is entered before the
+ * compositionend event is triggered, such as enter, so that the composition is send before
+ * the command is executed.
+ */
+CompositionHelper.prototype.finalizeComposition = function (waitForPropogation) {
+ this.compositionView.classList.remove('active');
+ this.isComposing = false;
+ this.clearTextareaPosition();
+
+ if (!waitForPropogation) {
+ // Cancel any delayed composition send requests and send the input immediately.
+ this.isSendingComposition = false;
+ var input = this.textarea.value.substring(this.compositionPosition.start, this.compositionPosition.end);
+ this.terminal.handler(input);
+ } else {
+ // Make a deep copy of the composition position here as a new compositionstart event may
+ // fire before the setTimeout executes.
+ var currentCompositionPosition = {
+ start: this.compositionPosition.start,
+ end: this.compositionPosition.end
+ };
+
+ // Since composition* events happen before the changes take place in the textarea on most
+ // browsers, use a setTimeout with 0ms time to allow the native compositionend event to
+ // complete. This ensures the correct character is retrieved, this solution was used
+ // because:
+ // - The compositionend event's data property is unreliable, at least on Chromium
+ // - The last compositionupdate event's data property does not always accurately describe
+ // the character, a counter example being Korean where an ending consonsant can move to
+ // the following character if the following input is a vowel.
+ var self = this;
+ this.isSendingComposition = true;
+ setTimeout(function () {
+ // Ensure that the input has not already been sent
+ if (self.isSendingComposition) {
+ self.isSendingComposition = false;
+ var input;
+ if (self.isComposing) {
+ // Use the end position to get the string if a new composition has started.
+ input = self.textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end);
+ } else {
+ // Don't use the end position here in order to pick up any characters after the
+ // composition has finished, for example when typing a non-composition character
+ // (eg. 2) after a composition character.
+ input = self.textarea.value.substring(currentCompositionPosition.start);
+ }
+ self.terminal.handler(input);
+ }
+ }, 0);
+ }
+};
+
+/**
+ * Apply any changes made to the textarea after the current event chain is allowed to complete.
+ * This should be called when not currently composing but a keydown event with the "composition
+ * character" (229) is triggered, in order to allow non-composition text to be entered when an
+ * IME is active.
+ */
+CompositionHelper.prototype.handleAnyTextareaChanges = function () {
+ var oldValue = this.textarea.value;
+ var self = this;
+ setTimeout(function () {
+ // Ignore if a composition has started since the timeout
+ if (!self.isComposing) {
+ var newValue = self.textarea.value;
+ var diff = newValue.replace(oldValue, '');
+ if (diff.length > 0) {
+ self.terminal.handler(diff);
+ }
+ }
+ }, 0);
+};
+
+/**
+ * Positions the composition view on top of the cursor and the textarea just below it (so the
+ * IME helper dialog is positioned correctly).
+ */
+CompositionHelper.prototype.updateCompositionElements = function (dontRecurse) {
+ if (!this.isComposing) {
+ return;
+ }
+ var cursor = this.terminal.element.querySelector('.terminal-cursor');
+ if (cursor) {
+ // Take .xterm-rows offsetTop into account as well in case it's positioned absolutely within
+ // the .xterm element.
+ var xtermRows = this.terminal.element.querySelector('.xterm-rows');
+ var cursorTop = xtermRows.offsetTop + cursor.offsetTop;
+
+ this.compositionView.style.left = cursor.offsetLeft + 'px';
+ this.compositionView.style.top = cursorTop + 'px';
+ this.compositionView.style.height = cursor.offsetHeight + 'px';
+ this.compositionView.style.lineHeight = cursor.offsetHeight + 'px';
+ // Sync the textarea to the exact position of the composition view so the IME knows where the
+ // text is.
+ var compositionViewBounds = this.compositionView.getBoundingClientRect();
+ this.textarea.style.left = cursor.offsetLeft + 'px';
+ this.textarea.style.top = cursorTop + 'px';
+ this.textarea.style.width = compositionViewBounds.width + 'px';
+ this.textarea.style.height = compositionViewBounds.height + 'px';
+ this.textarea.style.lineHeight = compositionViewBounds.height + 'px';
+ }
+ if (!dontRecurse) {
+ setTimeout(this.updateCompositionElements.bind(this, true), 0);
+ }
+};
+
+/**
+ * Clears the textarea's position so that the cursor does not blink on IE.
+ * @private
+ */
+CompositionHelper.prototype.clearTextareaPosition = function () {
+ this.textarea.style.left = '';
+ this.textarea.style.top = '';
+};
+
+exports.CompositionHelper = CompositionHelper;
+
+},{}],2:[function(_dereq_,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+/**
+ * xterm.js: xterm, in the browser
+ * Copyright (c) 2014-2016, SourceLair Private Company (www.sourcelair.com (MIT License)
+ */
+
+function EventEmitter() {
+ this._events = this._events || {};
+}
+
+EventEmitter.prototype.addListener = function (type, listener) {
+ this._events[type] = this._events[type] || [];
+ this._events[type].push(listener);
+};
+
+EventEmitter.prototype.on = EventEmitter.prototype.addListener;
+
+EventEmitter.prototype.removeListener = function (type, listener) {
+ if (!this._events[type]) return;
+
+ var obj = this._events[type],
+ i = obj.length;
+
+ while (i--) {
+ if (obj[i] === listener || obj[i].listener === listener) {
+ obj.splice(i, 1);
+ return;
+ }
+ }
+};
+
+EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
+
+EventEmitter.prototype.removeAllListeners = function (type) {
+ if (this._events[type]) delete this._events[type];
+};
+
+EventEmitter.prototype.once = function (type, listener) {
+ var self = this;
+ function on() {
+ var args = Array.prototype.slice.call(arguments);
+ this.removeListener(type, on);
+ return listener.apply(this, args);
+ }
+ on.listener = listener;
+ return this.on(type, on);
+};
+
+EventEmitter.prototype.emit = function (type) {
+ if (!this._events[type]) return;
+
+ var args = Array.prototype.slice.call(arguments, 1),
+ obj = this._events[type],
+ l = obj.length,
+ i = 0;
+
+ for (; i < l; i++) {
+ obj[i].apply(this, args);
+ }
+};
+
+EventEmitter.prototype.listeners = function (type) {
+ return this._events[type] = this._events[type] || [];
+};
+
+exports.EventEmitter = EventEmitter;
+
+},{}],3:[function(_dereq_,module,exports){
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+/**
+ * xterm.js: xterm, in the browser
+ * Copyright (c) 2014-2016, SourceLair Private Company (www.sourcelair.com (MIT License)
+ */
+
+/**
+ * Represents the viewport of a terminal, the visible area within the larger buffer of output.
+ * Logic for the virtual scroll bar is included in this object.
+ * @param {Terminal} terminal The Terminal object.
+ * @param {HTMLElement} viewportElement The DOM element acting as the viewport
+ * @param {HTMLElement} charMeasureElement A DOM element used to measure the character size of
+ * the terminal.
+ */
+function Viewport(terminal, viewportElement, scrollArea, charMeasureElement) {
+ this.terminal = terminal;
+ this.viewportElement = viewportElement;
+ this.scrollArea = scrollArea;
+ this.charMeasureElement = charMeasureElement;
+ this.currentRowHeight = 0;
+ this.lastRecordedBufferLength = 0;
+ this.lastRecordedViewportHeight = 0;
+
+ this.terminal.on('scroll', this.syncScrollArea.bind(this));
+ this.terminal.on('resize', this.syncScrollArea.bind(this));
+ this.viewportElement.addEventListener('scroll', this.onScroll.bind(this));
+
+ this.syncScrollArea();
+}
+
+/**
+ * Refreshes row height, setting line-height, viewport height and scroll area height if
+ * necessary.
+ * @param {number|undefined} charSize A character size measurement bounding rect object, if it
+ * doesn't exist it will be created.
+ */
+Viewport.prototype.refresh = function (charSize) {
+ var size = charSize || this.charMeasureElement.getBoundingClientRect();
+ if (size.height > 0) {
+ var rowHeightChanged = size.height !== this.currentRowHeight;
+ if (rowHeightChanged) {
+ this.currentRowHeight = size.height;
+ this.viewportElement.style.lineHeight = size.height + 'px';
+ this.terminal.rowContainer.style.lineHeight = size.height + 'px';
+ }
+ var viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows;
+ if (rowHeightChanged || viewportHeightChanged) {
+ this.lastRecordedViewportHeight = this.terminal.rows;
+ this.viewportElement.style.height = size.height * this.terminal.rows + 'px';
+ }
+ this.scrollArea.style.height = size.height * this.lastRecordedBufferLength + 'px';
+ }
+};
+
+/**
+ * Updates dimensions and synchronizes the scroll area if necessary.
+ */
+Viewport.prototype.syncScrollArea = function () {
+ if (this.lastRecordedBufferLength !== this.terminal.lines.length) {
+ // If buffer height changed
+ this.lastRecordedBufferLength = this.terminal.lines.length;
+ this.refresh();
+ } else if (this.lastRecordedViewportHeight !== this.terminal.rows) {
+ // If viewport height changed
+ this.refresh();
+ } else {
+ // If size has changed, refresh viewport
+ var size = this.charMeasureElement.getBoundingClientRect();
+ if (size.height !== this.currentRowHeight) {
+ this.refresh(size);
+ }
+ }
+
+ // Sync scrollTop
+ var scrollTop = this.terminal.ydisp * this.currentRowHeight;
+ if (this.viewportElement.scrollTop !== scrollTop) {
+ this.viewportElement.scrollTop = scrollTop;
+ }
+};
+
+/**
+ * Handles scroll events on the viewport, calculating the new viewport and requesting the
+ * terminal to scroll to it.
+ * @param {Event} ev The scroll event.
+ */
+Viewport.prototype.onScroll = function (ev) {
+ var newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight);
+ var diff = newRow - this.terminal.ydisp;
+ this.terminal.scrollDisp(diff, true);
+};
+
+/**
+ * Handles mouse wheel events by adjusting the viewport's scrollTop and delegating the actual
+ * scrolling to `onScroll`, this event needs to be attached manually by the consumer of
+ * `Viewport`.
+ * @param {WheelEvent} ev The mouse wheel event.
+ */
+Viewport.prototype.onWheel = function (ev) {
+ if (ev.deltaY === 0) {
+ // Do nothing if it's not a vertical scroll event
+ return;
+ }
+ // Fallback to WheelEvent.DOM_DELTA_PIXEL
+ var multiplier = 1;
+ if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) {
+ multiplier = this.currentRowHeight;
+ } else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
+ multiplier = this.currentRowHeight * this.terminal.rows;
+ }
+ this.viewportElement.scrollTop += ev.deltaY * multiplier;
+ // Prevent the page from scrolling when the terminal scrolls
+ ev.preventDefault();
+};
+
+exports.Viewport = Viewport;
+
+},{}],4:[function(_dereq_,module,exports){
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+/**
+ * xterm.js: xterm, in the browser
+ * Copyright (c) 2016, SourceLair Private Company <www.sourcelair.com> (MIT License)
+ */
+
+/**
+ * Clipboard handler module. This module contains methods for handling all
+ * clipboard-related events appropriately in the terminal.
+ * @module xterm/handlers/Clipboard
+ */
+
+/**
+ * Prepares text copied from terminal selection, to be saved in the clipboard by:
+ * 1. stripping all trailing white spaces
+ * 2. converting all non-breaking spaces to regular spaces
+ * @param {string} text The copied text that needs processing for storing in clipboard
+ * @returns {string}
+ */
+function prepareTextForClipboard(text) {
+ var space = String.fromCharCode(32),
+ nonBreakingSpace = String.fromCharCode(160),
+ allNonBreakingSpaces = new RegExp(nonBreakingSpace, 'g'),
+ processedText = text.split('\n').map(function (line) {
+ // Strip all trailing white spaces and convert all non-breaking spaces
+ // to regular spaces.
+ var processedLine = line.replace(/\s+$/g, '').replace(allNonBreakingSpaces, space);
+
+ return processedLine;
+ }).join('\n');
+
+ return processedText;
+}
+
+/**
+ * Binds copy functionality to the given terminal.
+ * @param {ClipboardEvent} ev The original copy event to be handled
+ */
+function copyHandler(ev, term) {
+ var copiedText = window.getSelection().toString(),
+ text = prepareTextForClipboard(copiedText);
+
+ if (term.browser.isMSIE) {
+ window.clipboardData.setData('Text', text);
+ } else {
+ ev.clipboardData.setData('text/plain', text);
+ }
+
+ ev.preventDefault(); // Prevent or the original text will be copied.
+}
+
+/**
+ * Redirect the clipboard's data to the terminal's input handler.
+ * @param {ClipboardEvent} ev The original paste event to be handled
+ * @param {Terminal} term The terminal on which to apply the handled paste event
+ */
+function pasteHandler(ev, term) {
+ ev.stopPropagation();
+
+ var dispatchPaste = function dispatchPaste(text) {
+ term.handler(text);
+ term.textarea.value = '';
+ return term.cancel(ev);
+ };
+
+ if (term.browser.isMSIE) {
+ if (window.clipboardData) {
+ var text = window.clipboardData.getData('Text');
+ dispatchPaste(text);
+ }
+ } else {
+ if (ev.clipboardData) {
+ var text = ev.clipboardData.getData('text/plain');
+ dispatchPaste(text);
+ }
+ }
+}
+
+/**
+ * Bind to right-click event and allow right-click copy and paste.
+ *
+ * **Logic**
+ * If text is selected and right-click happens on selected text, then
+ * do nothing to allow seamless copying.
+ * If no text is selected or right-click is outside of the selection
+ * area, then bring the terminal's input below the cursor, in order to
+ * trigger the event on the textarea and allow-right click paste, without
+ * caring about disappearing selection.
+ * @param {ClipboardEvent} ev The original paste event to be handled
+ * @param {Terminal} term The terminal on which to apply the handled paste event
+ */
+function rightClickHandler(ev, term) {
+ var s = document.getSelection(),
+ selectedText = prepareTextForClipboard(s.toString()),
+ clickIsOnSelection = false;
+
+ if (s.rangeCount) {
+ var r = s.getRangeAt(0),
+ cr = r.getClientRects(),
+ x = ev.clientX,
+ y = ev.clientY,
+ i,
+ rect;
+
+ for (i = 0; i < cr.length; i++) {
+ rect = cr[i];
+ clickIsOnSelection = x > rect.left && x < rect.right && y > rect.top && y < rect.bottom;
+
+ if (clickIsOnSelection) {
+ break;
+ }
+ }
+ // If we clicked on selection and selection is not a single space,
+ // then mark the right click as copy-only. We check for the single
+ // space selection, as this can happen when clicking on an &nbsp;
+ // and there is not much pointing in copying a single space.
+ if (selectedText.match(/^\s$/) || !selectedText.length) {
+ clickIsOnSelection = false;
+ }
+ }
+
+ // Bring textarea at the cursor position
+ if (!clickIsOnSelection) {
+ term.textarea.style.position = 'fixed';
+ term.textarea.style.width = '20px';
+ term.textarea.style.height = '20px';
+ term.textarea.style.left = x - 10 + 'px';
+ term.textarea.style.top = y - 10 + 'px';
+ term.textarea.style.zIndex = 1000;
+ term.textarea.focus();
+
+ // Reset the terminal textarea's styling
+ setTimeout(function () {
+ term.textarea.style.position = null;
+ term.textarea.style.width = null;
+ term.textarea.style.height = null;
+ term.textarea.style.left = null;
+ term.textarea.style.top = null;
+ term.textarea.style.zIndex = null;
+ }, 4);
+ }
+}
+
+exports.prepareTextForClipboard = prepareTextForClipboard;
+exports.copyHandler = copyHandler;
+exports.pasteHandler = pasteHandler;
+exports.rightClickHandler = rightClickHandler;
+
+},{}],5:[function(_dereq_,module,exports){
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.isMSWindows = exports.isIphone = exports.isIpad = exports.isMac = exports.isMSIE = exports.isFirefox = undefined;
+
+var _Generic = _dereq_('./Generic.js');
+
+var isNode = typeof navigator == 'undefined' ? true : false; /**
+ * xterm.js: xterm, in the browser
+ * Copyright (c) 2016, SourceLair Private Company <www.sourcelair.com> (MIT License)
+ */
+
+/**
+ * Browser utilities module. This module contains attributes and methods to help with
+ * identifying the current browser and platform.
+ * @module xterm/utils/Browser
+ */
+
+var userAgent = isNode ? 'node' : navigator.userAgent;
+var platform = isNode ? 'node' : navigator.platform;
+
+var isFirefox = exports.isFirefox = !!~userAgent.indexOf('Firefox');
+var isMSIE = exports.isMSIE = !!~userAgent.indexOf('MSIE') || !!~userAgent.indexOf('Trident');
+
+// Find the users platform. We use this to interpret the meta key
+// and ISO third level shifts.
+// http://stackoverflow.com/q/19877924/577598
+var isMac = exports.isMac = (0, _Generic.contains)(['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], platform);
+var isIpad = exports.isIpad = platform === 'iPad';
+var isIphone = exports.isIphone = platform === 'iPhone';
+var isMSWindows = exports.isMSWindows = (0, _Generic.contains)(['Windows', 'Win16', 'Win32', 'WinCE'], platform);
+
+},{"./Generic.js":6}],6:[function(_dereq_,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+/**
+ * xterm.js: xterm, in the browser
+ * Copyright (c) 2016, SourceLair Private Company <www.sourcelair.com> (MIT License)
+ */
+
+/**
+ * Generic utilities module. This module contains generic methods that can be helpful at
+ * different parts of the code base.
+ * @module xterm/utils/Generic
+ */
+
+/**
+ * Return if the given array contains the given element
+ * @param {Array} array The array to search for the given element.
+ * @param {Object} el The element to look for into the array
+ */
+var contains = exports.contains = function contains(arr, el) {
+ return arr.indexOf(el) >= 0;
+};
+
+},{}],7:[function(_dereq_,module,exports){
+'use strict';var _typeof=typeof Symbol==="function"&&typeof Symbol.iterator==="symbol"?function(obj){return typeof obj;}:function(obj){return obj&&typeof Symbol==="function"&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj;};/**
+ * xterm.js: xterm, in the browser
+ * Copyright (c) 2014-2014, SourceLair Private Company <www.sourcelair.com> (MIT License)
+ * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
+ * https://github.com/chjj/term.js
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * Originally forked from (with the author's permission):
+ * Fabrice Bellard's javascript vt100 for jslinux:
+ * http://bellard.org/jslinux/
+ * Copyright (c) 2011 Fabrice Bellard
+ * The original design remains. The terminal itself
+ * has been extended to include xterm CSI codes, among
+ * other features.
+ */var _CompositionHelper=_dereq_('./CompositionHelper.js');var _EventEmitter=_dereq_('./EventEmitter.js');var _Viewport=_dereq_('./Viewport.js');var _Clipboard=_dereq_('./handlers/Clipboard.js');var _Browser=_dereq_('./utils/Browser');var Browser=_interopRequireWildcard(_Browser);function _interopRequireWildcard(obj){if(obj&&obj.__esModule){return obj;}else{var newObj={};if(obj!=null){for(var key in obj){if(Object.prototype.hasOwnProperty.call(obj,key))newObj[key]=obj[key];}}newObj.default=obj;return newObj;}}/**
+ * Terminal Emulation References:
+ * http://vt100.net/
+ * http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt
+ * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
+ * http://invisible-island.net/vttest/
+ * http://www.inwap.com/pdp10/ansicode.txt
+ * http://linux.die.net/man/4/console_codes
+ * http://linux.die.net/man/7/urxvt
+ */// Let it work inside Node.js for automated testing purposes.
+var document=typeof window!='undefined'?window.document:null;/**
+ * States
+ */var normal=0,escaped=1,csi=2,osc=3,charset=4,dcs=5,ignore=6;/**
+ * Terminal
+ *//**
+ * Creates a new `Terminal` object.
+ *
+ * @param {object} options An object containing a set of options, the available options are:
+ * - `cursorBlink` (boolean): Whether the terminal cursor blinks
+ * - `cols` (number): The number of columns of the terminal (horizontal size)
+ * - `rows` (number): The number of rows of the terminal (vertical size)
+ *
+ * @public
+ * @class Xterm Xterm
+ * @alias module:xterm/src/xterm
+ */function Terminal(options){var self=this;if(!(this instanceof Terminal)){return new Terminal(arguments[0],arguments[1],arguments[2]);}self.browser=Browser;self.cancel=Terminal.cancel;_EventEmitter.EventEmitter.call(this);if(typeof options==='number'){options={cols:arguments[0],rows:arguments[1],handler:arguments[2]};}options=options||{};Object.keys(Terminal.defaults).forEach(function(key){if(options[key]==null){options[key]=Terminal.options[key];if(Terminal[key]!==Terminal.defaults[key]){options[key]=Terminal[key];}}self[key]=options[key];});if(options.colors.length===8){options.colors=options.colors.concat(Terminal._colors.slice(8));}else if(options.colors.length===16){options.colors=options.colors.concat(Terminal._colors.slice(16));}else if(options.colors.length===10){options.colors=options.colors.slice(0,-2).concat(Terminal._colors.slice(8,-2),options.colors.slice(-2));}else if(options.colors.length===18){options.colors=options.colors.concat(Terminal._colors.slice(16,-2),options.colors.slice(-2));}this.colors=options.colors;this.options=options;// this.context = options.context || window;
+// this.document = options.document || document;
+this.parent=options.body||options.parent||(document?document.getElementsByTagName('body')[0]:null);this.cols=options.cols||options.geometry[0];this.rows=options.rows||options.geometry[1];this.geometry=[this.cols,this.rows];if(options.handler){this.on('data',options.handler);}/**
+ * The scroll position of the y cursor, ie. ybase + y = the y position within the entire
+ * buffer
+ */this.ybase=0;/**
+ * The scroll position of the viewport
+ */this.ydisp=0;/**
+ * The cursor's x position after ybase
+ */this.x=0;/**
+ * The cursor's y position after ybase
+ */this.y=0;/**
+ * Used to debounce the refresh function
+ */this.isRefreshing=false;/**
+ * Whether there is a full terminal refresh queued
+ */this.cursorState=0;this.cursorHidden=false;this.convertEol;this.state=0;this.queue='';this.scrollTop=0;this.scrollBottom=this.rows-1;this.customKeydownHandler=null;// modes
+this.applicationKeypad=false;this.applicationCursor=false;this.originMode=false;this.insertMode=false;this.wraparoundMode=true;// defaults: xterm - true, vt100 - false
+this.normal=null;// charset
+this.charset=null;this.gcharset=null;this.glevel=0;this.charsets=[null];// mouse properties
+this.decLocator;this.x10Mouse;this.vt200Mouse;this.vt300Mouse;this.normalMouse;this.mouseEvents;this.sendFocus;this.utfMouse;this.sgrMouse;this.urxvtMouse;// misc
+this.element;this.children;this.refreshStart;this.refreshEnd;this.savedX;this.savedY;this.savedCols;// stream
+this.readable=true;this.writable=true;this.defAttr=0<<18|257<<9|256<<0;this.curAttr=this.defAttr;this.params=[];this.currentParam=0;this.prefix='';this.postfix='';// leftover surrogate high from previous write invocation
+this.surrogate_high='';/**
+ * An array of all lines in the entire buffer, including the prompt. The lines are array of
+ * characters which are 2-length arrays where [0] is an attribute and [1] is the character.
+ */this.lines=[];var i=this.rows;while(i--){this.lines.push(this.blankLine());}this.tabs;this.setupStops();// Store if user went browsing history in scrollback
+this.userScrolling=false;}inherits(Terminal,_EventEmitter.EventEmitter);/**
+ * back_color_erase feature for xterm.
+ */Terminal.prototype.eraseAttr=function(){// if (this.is('screen')) return this.defAttr;
+return this.defAttr&~0x1ff|this.curAttr&0x1ff;};/**
+ * Colors
+ */// Colors 0-15
+Terminal.tangoColors=[// dark:
+'#2e3436','#cc0000','#4e9a06','#c4a000','#3465a4','#75507b','#06989a','#d3d7cf',// bright:
+'#555753','#ef2929','#8ae234','#fce94f','#729fcf','#ad7fa8','#34e2e2','#eeeeec'];// Colors 0-15 + 16-255
+// Much thanks to TooTallNate for writing this.
+Terminal.colors=function(){var colors=Terminal.tangoColors.slice(),r=[0x00,0x5f,0x87,0xaf,0xd7,0xff],i;// 16-231
+i=0;for(;i<216;i++){out(r[i/36%6|0],r[i/6%6|0],r[i%6]);}// 232-255 (grey)
+i=0;for(;i<24;i++){r=8+i*10;out(r,r,r);}function out(r,g,b){colors.push('#'+hex(r)+hex(g)+hex(b));}function hex(c){c=c.toString(16);return c.length<2?'0'+c:c;}return colors;}();Terminal._colors=Terminal.colors.slice();Terminal.vcolors=function(){var out=[],colors=Terminal.colors,i=0,color;for(;i<256;i++){color=parseInt(colors[i].substring(1),16);out.push([color>>16&0xff,color>>8&0xff,color&0xff]);}return out;}();/**
+ * Options
+ */Terminal.defaults={colors:Terminal.colors,theme:'default',convertEol:false,termName:'xterm',geometry:[80,24],cursorBlink:false,visualBell:false,popOnBell:false,scrollback:1000,screenKeys:false,debug:false,cancelEvents:false// programFeatures: false,
+// focusKeys: false,
+};Terminal.options={};Terminal.focus=null;each(keys(Terminal.defaults),function(key){Terminal[key]=Terminal.defaults[key];Terminal.options[key]=Terminal.defaults[key];});/**
+ * Focus the terminal. Delegates focus handling to the terminal's DOM element.
+ */Terminal.prototype.focus=function(){return this.textarea.focus();};/**
+ * Retrieves an option's value from the terminal.
+ * @param {string} key The option key.
+ */Terminal.prototype.getOption=function(key,value){if(!(key in Terminal.defaults)){throw new Error('No option with key "'+key+'"');}if(typeof this.options[key]!=='undefined'){return this.options[key];}return this[key];};/**
+ * Sets an option on the terminal.
+ * @param {string} key The option key.
+ * @param {string} value The option value.
+ */Terminal.prototype.setOption=function(key,value){if(!(key in Terminal.defaults)){throw new Error('No option with key "'+key+'"');}this[key]=value;this.options[key]=value;};/**
+ * Binds the desired focus behavior on a given terminal object.
+ *
+ * @static
+ */Terminal.bindFocus=function(term){on(term.textarea,'focus',function(ev){if(term.sendFocus){term.send('\x1b[I');}term.element.classList.add('focus');term.showCursor();Terminal.focus=term;term.emit('focus',{terminal:term});});};/**
+ * Blur the terminal. Delegates blur handling to the terminal's DOM element.
+ */Terminal.prototype.blur=function(){return this.textarea.blur();};/**
+ * Binds the desired blur behavior on a given terminal object.
+ *
+ * @static
+ */Terminal.bindBlur=function(term){on(term.textarea,'blur',function(ev){term.refresh(term.y,term.y);if(term.sendFocus){term.send('\x1b[O');}term.element.classList.remove('focus');Terminal.focus=null;term.emit('blur',{terminal:term});});};/**
+ * Initialize default behavior
+ */Terminal.prototype.initGlobal=function(){var term=this;Terminal.bindKeys(this);Terminal.bindFocus(this);Terminal.bindBlur(this);// Bind clipboard functionality
+on(this.element,'copy',function(ev){_Clipboard.copyHandler.call(this,ev,term);});on(this.textarea,'paste',function(ev){_Clipboard.pasteHandler.call(this,ev,term);});function rightClickHandlerWrapper(ev){_Clipboard.rightClickHandler.call(this,ev,term);}if(term.browser.isFirefox){on(this.element,'mousedown',function(ev){if(ev.button==2){rightClickHandlerWrapper(ev);}});}else{on(this.element,'contextmenu',rightClickHandlerWrapper);}};/**
+ * Apply key handling to the terminal
+ */Terminal.bindKeys=function(term){on(term.element,'keydown',function(ev){if(document.activeElement!=this){return;}term.keyDown(ev);},true);on(term.element,'keypress',function(ev){if(document.activeElement!=this){return;}term.keyPress(ev);},true);on(term.element,'keyup',term.focus.bind(term));on(term.textarea,'keydown',function(ev){term.keyDown(ev);},true);on(term.textarea,'keypress',function(ev){term.keyPress(ev);// Truncate the textarea's value, since it is not needed
+this.value='';},true);on(term.textarea,'compositionstart',term.compositionHelper.compositionstart.bind(term.compositionHelper));on(term.textarea,'compositionupdate',term.compositionHelper.compositionupdate.bind(term.compositionHelper));on(term.textarea,'compositionend',term.compositionHelper.compositionend.bind(term.compositionHelper));term.on('refresh',term.compositionHelper.updateCompositionElements.bind(term.compositionHelper));};/**
+ * Insert the given row to the terminal or produce a new one
+ * if no row argument is passed. Return the inserted row.
+ * @param {HTMLElement} row (optional) The row to append to the terminal.
+ */Terminal.prototype.insertRow=function(row){if((typeof row==='undefined'?'undefined':_typeof(row))!='object'){row=document.createElement('div');}this.rowContainer.appendChild(row);this.children.push(row);return row;};/**
+ * Opens the terminal within an element.
+ *
+ * @param {HTMLElement} parent The element to create the terminal within.
+ */Terminal.prototype.open=function(parent){var self=this,i=0,div;this.parent=parent||this.parent;if(!this.parent){throw new Error('Terminal requires a parent element.');}// Grab global elements
+this.context=this.parent.ownerDocument.defaultView;this.document=this.parent.ownerDocument;this.body=this.document.getElementsByTagName('body')[0];//Create main element container
+this.element=this.document.createElement('div');this.element.classList.add('terminal');this.element.classList.add('xterm');this.element.classList.add('xterm-theme-'+this.theme);this.element.style.height;this.element.setAttribute('tabindex',0);this.viewportElement=document.createElement('div');this.viewportElement.classList.add('xterm-viewport');this.element.appendChild(this.viewportElement);this.viewportScrollArea=document.createElement('div');this.viewportScrollArea.classList.add('xterm-scroll-area');this.viewportElement.appendChild(this.viewportScrollArea);// Create the container that will hold the lines of the terminal and then
+// produce the lines the lines.
+this.rowContainer=document.createElement('div');this.rowContainer.classList.add('xterm-rows');this.element.appendChild(this.rowContainer);this.children=[];// Create the container that will hold helpers like the textarea for
+// capturing DOM Events. Then produce the helpers.
+this.helperContainer=document.createElement('div');this.helperContainer.classList.add('xterm-helpers');// TODO: This should probably be inserted once it's filled to prevent an additional layout
+this.element.appendChild(this.helperContainer);this.textarea=document.createElement('textarea');this.textarea.classList.add('xterm-helper-textarea');this.textarea.setAttribute('autocorrect','off');this.textarea.setAttribute('autocapitalize','off');this.textarea.setAttribute('spellcheck','false');this.textarea.tabIndex=0;this.textarea.addEventListener('focus',function(){self.emit('focus',{terminal:self});});this.textarea.addEventListener('blur',function(){self.emit('blur',{terminal:self});});this.helperContainer.appendChild(this.textarea);this.compositionView=document.createElement('div');this.compositionView.classList.add('composition-view');this.compositionHelper=new _CompositionHelper.CompositionHelper(this.textarea,this.compositionView,this);this.helperContainer.appendChild(this.compositionView);this.charMeasureElement=document.createElement('div');this.charMeasureElement.classList.add('xterm-char-measure-element');this.charMeasureElement.innerHTML='W';this.helperContainer.appendChild(this.charMeasureElement);for(;i<this.rows;i++){this.insertRow();}this.parent.appendChild(this.element);this.viewport=new _Viewport.Viewport(this,this.viewportElement,this.viewportScrollArea,this.charMeasureElement);// Draw the screen.
+this.refresh(0,this.rows-1);// Initialize global actions that
+// need to be taken on the document.
+this.initGlobal();// Ensure there is a Terminal.focus.
+this.focus();on(this.element,'click',function(){var selection=document.getSelection(),collapsed=selection.isCollapsed,isRange=typeof collapsed=='boolean'?!collapsed:selection.type=='Range';if(!isRange){self.focus();}});// Listen for mouse events and translate
+// them into terminal mouse protocols.
+this.bindMouse();// Figure out whether boldness affects
+// the character width of monospace fonts.
+if(Terminal.brokenBold==null){Terminal.brokenBold=isBoldBroken(this.document);}this.emit('open');};/**
+ * Attempts to load an add-on using CommonJS or RequireJS (whichever is available).
+ * @param {string} addon The name of the addon to load
+ * @static
+ */Terminal.loadAddon=function(addon,callback){if((typeof exports==='undefined'?'undefined':_typeof(exports))==='object'&&(typeof module==='undefined'?'undefined':_typeof(module))==='object'){// CommonJS
+return _dereq_('../addons/'+addon);}else if(typeof define=='function'){// RequireJS
+return _dereq_(['../addons/'+addon+'/'+addon],callback);}else{console.error('Cannot load a module without a CommonJS or RequireJS environment.');return false;}};/**
+ * XTerm mouse events
+ * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
+ * To better understand these
+ * the xterm code is very helpful:
+ * Relevant files:
+ * button.c, charproc.c, misc.c
+ * Relevant functions in xterm/button.c:
+ * BtnCode, EmitButtonCode, EditorButton, SendMousePosition
+ */Terminal.prototype.bindMouse=function(){var el=this.element,self=this,pressed=32;// mouseup, mousedown, wheel
+// left click: ^[[M 3<^[[M#3<
+// wheel up: ^[[M`3>
+function sendButton(ev){var button,pos;// get the xterm-style button
+button=getButton(ev);// get mouse coordinates
+pos=getCoords(ev);if(!pos)return;sendEvent(button,pos);switch(ev.overrideType||ev.type){case'mousedown':pressed=button;break;case'mouseup':// keep it at the left
+// button, just in case.
+pressed=32;break;case'wheel':// nothing. don't
+// interfere with
+// `pressed`.
+break;}}// motion example of a left click:
+// ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7<
+function sendMove(ev){var button=pressed,pos;pos=getCoords(ev);if(!pos)return;// buttons marked as motions
+// are incremented by 32
+button+=32;sendEvent(button,pos);}// encode button and
+// position to characters
+function encode(data,ch){if(!self.utfMouse){if(ch===255)return data.push(0);if(ch>127)ch=127;data.push(ch);}else{if(ch===2047)return data.push(0);if(ch<127){data.push(ch);}else{if(ch>2047)ch=2047;data.push(0xC0|ch>>6);data.push(0x80|ch&0x3F);}}}// send a mouse event:
+// regular/utf8: ^[[M Cb Cx Cy
+// urxvt: ^[[ Cb ; Cx ; Cy M
+// sgr: ^[[ Cb ; Cx ; Cy M/m
+// vt300: ^[[ 24(1/3/5)~ [ Cx , Cy ] \r
+// locator: CSI P e ; P b ; P r ; P c ; P p & w
+function sendEvent(button,pos){// self.emit('mouse', {
+// x: pos.x - 32,
+// y: pos.x - 32,
+// button: button
+// });
+if(self.vt300Mouse){// NOTE: Unstable.
+// http://www.vt100.net/docs/vt3xx-gp/chapter15.html
+button&=3;pos.x-=32;pos.y-=32;var data='\x1b[24';if(button===0)data+='1';else if(button===1)data+='3';else if(button===2)data+='5';else if(button===3)return;else data+='0';data+='~['+pos.x+','+pos.y+']\r';self.send(data);return;}if(self.decLocator){// NOTE: Unstable.
+button&=3;pos.x-=32;pos.y-=32;if(button===0)button=2;else if(button===1)button=4;else if(button===2)button=6;else if(button===3)button=3;self.send('\x1b['+button+';'+(button===3?4:0)+';'+pos.y+';'+pos.x+';'+(pos.page||0)+'&w');return;}if(self.urxvtMouse){pos.x-=32;pos.y-=32;pos.x++;pos.y++;self.send('\x1b['+button+';'+pos.x+';'+pos.y+'M');return;}if(self.sgrMouse){pos.x-=32;pos.y-=32;self.send('\x1b[<'+((button&3)===3?button&~3:button)+';'+pos.x+';'+pos.y+((button&3)===3?'m':'M'));return;}var data=[];encode(data,button);encode(data,pos.x);encode(data,pos.y);self.send('\x1b[M'+String.fromCharCode.apply(String,data));}function getButton(ev){var button,shift,meta,ctrl,mod;// two low bits:
+// 0 = left
+// 1 = middle
+// 2 = right
+// 3 = release
+// wheel up/down:
+// 1, and 2 - with 64 added
+switch(ev.overrideType||ev.type){case'mousedown':button=ev.button!=null?+ev.button:ev.which!=null?ev.which-1:null;if(self.browser.isMSIE){button=button===1?0:button===4?1:button;}break;case'mouseup':button=3;break;case'DOMMouseScroll':button=ev.detail<0?64:65;break;case'wheel':button=ev.wheelDeltaY>0?64:65;break;}// next three bits are the modifiers:
+// 4 = shift, 8 = meta, 16 = control
+shift=ev.shiftKey?4:0;meta=ev.metaKey?8:0;ctrl=ev.ctrlKey?16:0;mod=shift|meta|ctrl;// no mods
+if(self.vt200Mouse){// ctrl only
+mod&=ctrl;}else if(!self.normalMouse){mod=0;}// increment to SP
+button=32+(mod<<2)+button;return button;}// mouse coordinates measured in cols/rows
+function getCoords(ev){var x,y,w,h,el;// ignore browsers without pageX for now
+if(ev.pageX==null)return;x=ev.pageX;y=ev.pageY;el=self.element;// should probably check offsetParent
+// but this is more portable
+while(el&&el!==self.document.documentElement){x-=el.offsetLeft;y-=el.offsetTop;el='offsetParent'in el?el.offsetParent:el.parentNode;}// convert to cols/rows
+w=self.element.clientWidth;h=self.element.clientHeight;x=Math.ceil(x/w*self.cols);y=Math.ceil(y/h*self.rows);// be sure to avoid sending
+// bad positions to the program
+if(x<0)x=0;if(x>self.cols)x=self.cols;if(y<0)y=0;if(y>self.rows)y=self.rows;// xterm sends raw bytes and
+// starts at 32 (SP) for each.
+x+=32;y+=32;return{x:x,y:y,type:'wheel'};}on(el,'mousedown',function(ev){if(!self.mouseEvents)return;// send the button
+sendButton(ev);// ensure focus
+self.focus();// fix for odd bug
+//if (self.vt200Mouse && !self.normalMouse) {
+if(self.vt200Mouse){ev.overrideType='mouseup';sendButton(ev);return self.cancel(ev);}// bind events
+if(self.normalMouse)on(self.document,'mousemove',sendMove);// x10 compatibility mode can't send button releases
+if(!self.x10Mouse){on(self.document,'mouseup',function up(ev){sendButton(ev);if(self.normalMouse)off(self.document,'mousemove',sendMove);off(self.document,'mouseup',up);return self.cancel(ev);});}return self.cancel(ev);});//if (self.normalMouse) {
+// on(self.document, 'mousemove', sendMove);
+//}
+on(el,'wheel',function(ev){if(!self.mouseEvents)return;if(self.x10Mouse||self.vt300Mouse||self.decLocator)return;sendButton(ev);return self.cancel(ev);});// allow wheel scrolling in
+// the shell for example
+on(el,'wheel',function(ev){if(self.mouseEvents)return;self.viewport.onWheel(ev);return self.cancel(ev);});};/**
+ * Destroys the terminal.
+ */Terminal.prototype.destroy=function(){this.readable=false;this.writable=false;this._events={};this.handler=function(){};this.write=function(){};if(this.element.parentNode){this.element.parentNode.removeChild(this.element);}//this.emit('close');
+};/**
+ * Flags used to render terminal text properly
+ */Terminal.flags={BOLD:1,UNDERLINE:2,BLINK:4,INVERSE:8,INVISIBLE:16};/**
+ * Refreshes (re-renders) terminal content within two rows (inclusive)
+ *
+ * Rendering Engine:
+ *
+ * In the screen buffer, each character is stored as a an array with a character
+ * and a 32-bit integer:
+ * - First value: a utf-16 character.
+ * - Second value:
+ * - Next 9 bits: background color (0-511).
+ * - Next 9 bits: foreground color (0-511).
+ * - Next 14 bits: a mask for misc. flags:
+ * - 1=bold
+ * - 2=underline
+ * - 4=blink
+ * - 8=inverse
+ * - 16=invisible
+ *
+ * @param {number} start The row to start from (between 0 and terminal's height terminal - 1)
+ * @param {number} end The row to end at (between fromRow and terminal's height terminal - 1)
+ * @param {boolean} queue Whether the refresh should ran right now or be queued
+ */Terminal.prototype.refresh=function(start,end,queue){var self=this;// queue defaults to true
+queue=typeof queue=='undefined'?true:queue;/**
+ * The refresh queue allows refresh to execute only approximately 30 times a second. For
+ * commands that pass a significant amount of output to the write function, this prevents the
+ * terminal from maxing out the CPU and making the UI unresponsive. While commands can still
+ * run beyond what they do on the terminal, it is far better with a debounce in place as
+ * every single terminal manipulation does not need to be constructed in the DOM.
+ *
+ * A side-effect of this is that it makes ^C to interrupt a process seem more responsive.
+ */if(queue){// If refresh should be queued, order the refresh and return.
+if(this._refreshIsQueued){// If a refresh has already been queued, just order a full refresh next
+this._fullRefreshNext=true;}else{setTimeout(function(){self.refresh(start,end,false);},34);this._refreshIsQueued=true;}return;}// If refresh should be run right now (not be queued), release the lock
+this._refreshIsQueued=false;// If multiple refreshes were requested, make a full refresh.
+if(this._fullRefreshNext){start=0;end=this.rows-1;this._fullRefreshNext=false;// reset lock
+}var x,y,i,line,out,ch,ch_width,width,data,attr,bg,fg,flags,row,parent,focused=document.activeElement;// If this is a big refresh, remove the terminal rows from the DOM for faster calculations
+if(end-start>=this.rows/2){parent=this.element.parentNode;if(parent){this.element.removeChild(this.rowContainer);}}width=this.cols;y=start;if(end>=this.rows.length){this.log('`end` is too large. Most likely a bad CSR.');end=this.rows.length-1;}for(;y<=end;y++){row=y+this.ydisp;line=this.lines[row];out='';if(this.y===y-(this.ybase-this.ydisp)&&this.cursorState&&!this.cursorHidden){x=this.x;}else{x=-1;}attr=this.defAttr;i=0;for(;i<width;i++){data=line[i][0];ch=line[i][1];ch_width=line[i][2];if(!ch_width)continue;if(i===x)data=-1;if(data!==attr){if(attr!==this.defAttr){out+='</span>';}if(data!==this.defAttr){if(data===-1){out+='<span class="reverse-video terminal-cursor';if(this.cursorBlink){out+=' blinking';}out+='">';}else{var classNames=[];bg=data&0x1ff;fg=data>>9&0x1ff;flags=data>>18;if(flags&Terminal.flags.BOLD){if(!Terminal.brokenBold){classNames.push('xterm-bold');}// See: XTerm*boldColors
+if(fg<8)fg+=8;}if(flags&Terminal.flags.UNDERLINE){classNames.push('xterm-underline');}if(flags&Terminal.flags.BLINK){classNames.push('xterm-blink');}// If inverse flag is on, then swap the foreground and background variables.
+if(flags&Terminal.flags.INVERSE){/* One-line variable swap in JavaScript: http://stackoverflow.com/a/16201730 */bg=[fg,fg=bg][0];// Should inverse just be before the
+// above boldColors effect instead?
+if(flags&1&&fg<8)fg+=8;}if(flags&Terminal.flags.INVISIBLE){classNames.push('xterm-hidden');}/**
+ * Weird situation: Invert flag used black foreground and white background results
+ * in invalid background color, positioned at the 256 index of the 256 terminal
+ * color map. Pin the colors manually in such a case.
+ *
+ * Source: https://github.com/sourcelair/xterm.js/issues/57
+ */if(flags&Terminal.flags.INVERSE){if(bg==257){bg=15;}if(fg==256){fg=0;}}if(bg<256){classNames.push('xterm-bg-color-'+bg);}if(fg<256){classNames.push('xterm-color-'+fg);}out+='<span';if(classNames.length){out+=' class="'+classNames.join(' ')+'"';}out+='>';}}}switch(ch){case'&':out+='&amp;';break;case'<':out+='&lt;';break;case'>':out+='&gt;';break;default:if(ch<=' '){out+='&nbsp;';}else{out+=ch;}break;}attr=data;}if(attr!==this.defAttr){out+='</span>';}this.children[y].innerHTML=out;}if(parent){this.element.appendChild(this.rowContainer);}this.emit('refresh',{element:this.element,start:start,end:end});};/**
+ * Display the cursor element
+ */Terminal.prototype.showCursor=function(){if(!this.cursorState){this.cursorState=1;this.refresh(this.y,this.y);}};/**
+ * Scroll the terminal
+ */Terminal.prototype.scroll=function(){var row;if(++this.ybase===this.scrollback){this.ybase=this.ybase/2|0;this.lines=this.lines.slice(-(this.ybase+this.rows)+1);}if(!this.userScrolling){this.ydisp=this.ybase;}// last line
+row=this.ybase+this.rows-1;// subtract the bottom scroll region
+row-=this.rows-1-this.scrollBottom;if(row===this.lines.length){// potential optimization:
+// pushing is faster than splicing
+// when they amount to the same
+// behavior.
+this.lines.push(this.blankLine());}else{// add our new line
+this.lines.splice(row,0,this.blankLine());}if(this.scrollTop!==0){if(this.ybase!==0){this.ybase--;if(!this.userScrolling){this.ydisp=this.ybase;}}this.lines.splice(this.ybase+this.scrollTop,1);}// this.maxRange();
+this.updateRange(this.scrollTop);this.updateRange(this.scrollBottom);this.emit('scroll',this.ydisp);};/**
+ * Scroll the display of the terminal
+ * @param {number} disp The number of lines to scroll down (negatives scroll up).
+ * @param {boolean} suppressScrollEvent Don't emit the scroll event as scrollDisp. This is used
+ * to avoid unwanted events being handled by the veiwport when the event was triggered from the
+ * viewport originally.
+ */Terminal.prototype.scrollDisp=function(disp,suppressScrollEvent){if(disp<0){this.userScrolling=true;}else if(disp+this.ydisp>=this.ybase){this.userScrolling=false;}this.ydisp+=disp;if(this.ydisp>this.ybase){this.ydisp=this.ybase;}else if(this.ydisp<0){this.ydisp=0;}if(!suppressScrollEvent){this.emit('scroll',this.ydisp);}this.refresh(0,this.rows-1);};/**
+ * Scroll the display of the terminal by a number of pages.
+ * @param {number} pageCount The number of pages to scroll (negative scrolls up).
+ */Terminal.prototype.scrollPages=function(pageCount){this.scrollDisp(pageCount*(this.rows-1));};/**
+ * Scrolls the display of the terminal to the top.
+ */Terminal.prototype.scrollToTop=function(){this.scrollDisp(-this.ydisp);};/**
+ * Scrolls the display of the terminal to the bottom.
+ */Terminal.prototype.scrollToBottom=function(){this.scrollDisp(this.ybase-this.ydisp);};/**
+ * Writes text to the terminal.
+ * @param {string} text The text to write to the terminal.
+ */Terminal.prototype.write=function(data){var l=data.length,i=0,j,cs,ch,code,low,ch_width,row;this.refreshStart=this.y;this.refreshEnd=this.y;// apply leftover surrogate high from last write
+if(this.surrogate_high){data=this.surrogate_high+data;this.surrogate_high='';}for(;i<l;i++){ch=data[i];// FIXME: higher chars than 0xa0 are not allowed in escape sequences
+// --> maybe move to default
+code=data.charCodeAt(i);if(0xD800<=code&&code<=0xDBFF){// we got a surrogate high
+// get surrogate low (next 2 bytes)
+low=data.charCodeAt(i+1);if(isNaN(low)){// end of data stream, save surrogate high
+this.surrogate_high=ch;continue;}code=(code-0xD800)*0x400+(low-0xDC00)+0x10000;ch+=data.charAt(i+1);}// surrogate low - already handled above
+if(0xDC00<=code&&code<=0xDFFF)continue;switch(this.state){case normal:switch(ch){case'\x07':this.bell();break;// '\n', '\v', '\f'
+case'\n':case'\x0b':case'\x0c':if(this.convertEol){this.x=0;}this.y++;if(this.y>this.scrollBottom){this.y--;this.scroll();}break;// '\r'
+case'\r':this.x=0;break;// '\b'
+case'\x08':if(this.x>0){this.x--;}break;// '\t'
+case'\t':this.x=this.nextStop();break;// shift out
+case'\x0e':this.setgLevel(1);break;// shift in
+case'\x0f':this.setgLevel(0);break;// '\e'
+case'\x1b':this.state=escaped;break;default:// ' '
+// calculate print space
+// expensive call, therefore we save width in line buffer
+ch_width=wcwidth(code);if(ch>=' '){if(this.charset&&this.charset[ch]){ch=this.charset[ch];}row=this.y+this.ybase;// insert combining char in last cell
+// FIXME: needs handling after cursor jumps
+if(!ch_width&&this.x){// dont overflow left
+if(this.lines[row][this.x-1]){if(!this.lines[row][this.x-1][2]){// found empty cell after fullwidth, need to go 2 cells back
+if(this.lines[row][this.x-2])this.lines[row][this.x-2][1]+=ch;}else{this.lines[row][this.x-1][1]+=ch;}this.updateRange(this.y);}break;}// goto next line if ch would overflow
+// TODO: needs a global min terminal width of 2
+if(this.x+ch_width-1>=this.cols){// autowrap - DECAWM
+if(this.wraparoundMode){this.x=0;this.y++;if(this.y>this.scrollBottom){this.y--;this.scroll();}}else{this.x=this.cols-1;if(ch_width===2)// FIXME: check for xterm behavior
+continue;}}row=this.y+this.ybase;// insert mode: move characters to right
+if(this.insertMode){// do this twice for a fullwidth char
+for(var moves=0;moves<ch_width;++moves){// remove last cell, if it's width is 0
+// we have to adjust the second last cell as well
+var removed=this.lines[this.y+this.ybase].pop();if(removed[2]===0&&this.lines[row][this.cols-2]&&this.lines[row][this.cols-2][2]===2)this.lines[row][this.cols-2]=[this.curAttr,' ',1];// insert empty cell at cursor
+this.lines[row].splice(this.x,0,[this.curAttr,' ',1]);}}this.lines[row][this.x]=[this.curAttr,ch,ch_width];this.x++;this.updateRange(this.y);// fullwidth char - set next cell width to zero and advance cursor
+if(ch_width===2){this.lines[row][this.x]=[this.curAttr,'',0];this.x++;}}break;}break;case escaped:switch(ch){// ESC [ Control Sequence Introducer ( CSI is 0x9b).
+case'[':this.params=[];this.currentParam=0;this.state=csi;break;// ESC ] Operating System Command ( OSC is 0x9d).
+case']':this.params=[];this.currentParam=0;this.state=osc;break;// ESC P Device Control String ( DCS is 0x90).
+case'P':this.params=[];this.currentParam=0;this.state=dcs;break;// ESC _ Application Program Command ( APC is 0x9f).
+case'_':this.state=ignore;break;// ESC ^ Privacy Message ( PM is 0x9e).
+case'^':this.state=ignore;break;// ESC c Full Reset (RIS).
+case'c':this.reset();break;// ESC E Next Line ( NEL is 0x85).
+// ESC D Index ( IND is 0x84).
+case'E':this.x=0;;case'D':this.index();break;// ESC M Reverse Index ( RI is 0x8d).
+case'M':this.reverseIndex();break;// ESC % Select default/utf-8 character set.
+// @ = default, G = utf-8
+case'%'://this.charset = null;
+this.setgLevel(0);this.setgCharset(0,Terminal.charsets.US);this.state=normal;i++;break;// ESC (,),*,+,-,. Designate G0-G2 Character Set.
+case'(':// <-- this seems to get all the attention
+case')':case'*':case'+':case'-':case'.':switch(ch){case'(':this.gcharset=0;break;case')':this.gcharset=1;break;case'*':this.gcharset=2;break;case'+':this.gcharset=3;break;case'-':this.gcharset=1;break;case'.':this.gcharset=2;break;}this.state=charset;break;// Designate G3 Character Set (VT300).
+// A = ISO Latin-1 Supplemental.
+// Not implemented.
+case'/':this.gcharset=3;this.state=charset;i--;break;// ESC N
+// Single Shift Select of G2 Character Set
+// ( SS2 is 0x8e). This affects next character only.
+case'N':break;// ESC O
+// Single Shift Select of G3 Character Set
+// ( SS3 is 0x8f). This affects next character only.
+case'O':break;// ESC n
+// Invoke the G2 Character Set as GL (LS2).
+case'n':this.setgLevel(2);break;// ESC o
+// Invoke the G3 Character Set as GL (LS3).
+case'o':this.setgLevel(3);break;// ESC |
+// Invoke the G3 Character Set as GR (LS3R).
+case'|':this.setgLevel(3);break;// ESC }
+// Invoke the G2 Character Set as GR (LS2R).
+case'}':this.setgLevel(2);break;// ESC ~
+// Invoke the G1 Character Set as GR (LS1R).
+case'~':this.setgLevel(1);break;// ESC 7 Save Cursor (DECSC).
+case'7':this.saveCursor();this.state=normal;break;// ESC 8 Restore Cursor (DECRC).
+case'8':this.restoreCursor();this.state=normal;break;// ESC # 3 DEC line height/width
+case'#':this.state=normal;i++;break;// ESC H Tab Set (HTS is 0x88).
+case'H':this.tabSet();break;// ESC = Application Keypad (DECKPAM).
+case'=':this.log('Serial port requested application keypad.');this.applicationKeypad=true;this.viewport.syncScrollArea();this.state=normal;break;// ESC > Normal Keypad (DECKPNM).
+case'>':this.log('Switching back to normal keypad.');this.applicationKeypad=false;this.viewport.syncScrollArea();this.state=normal;break;default:this.state=normal;this.error('Unknown ESC control: %s.',ch);break;}break;case charset:switch(ch){case'0':// DEC Special Character and Line Drawing Set.
+cs=Terminal.charsets.SCLD;break;case'A':// UK
+cs=Terminal.charsets.UK;break;case'B':// United States (USASCII).
+cs=Terminal.charsets.US;break;case'4':// Dutch
+cs=Terminal.charsets.Dutch;break;case'C':// Finnish
+case'5':cs=Terminal.charsets.Finnish;break;case'R':// French
+cs=Terminal.charsets.French;break;case'Q':// FrenchCanadian
+cs=Terminal.charsets.FrenchCanadian;break;case'K':// German
+cs=Terminal.charsets.German;break;case'Y':// Italian
+cs=Terminal.charsets.Italian;break;case'E':// NorwegianDanish
+case'6':cs=Terminal.charsets.NorwegianDanish;break;case'Z':// Spanish
+cs=Terminal.charsets.Spanish;break;case'H':// Swedish
+case'7':cs=Terminal.charsets.Swedish;break;case'=':// Swiss
+cs=Terminal.charsets.Swiss;break;case'/':// ISOLatin (actually /A)
+cs=Terminal.charsets.ISOLatin;i++;break;default:// Default
+cs=Terminal.charsets.US;break;}this.setgCharset(this.gcharset,cs);this.gcharset=null;this.state=normal;break;case osc:// OSC Ps ; Pt ST
+// OSC Ps ; Pt BEL
+// Set Text Parameters.
+if(ch==='\x1b'||ch==='\x07'){if(ch==='\x1b')i++;this.params.push(this.currentParam);switch(this.params[0]){case 0:case 1:case 2:if(this.params[1]){this.title=this.params[1];this.handleTitle(this.title);}break;case 3:// set X property
+break;case 4:case 5:// change dynamic colors
+break;case 10:case 11:case 12:case 13:case 14:case 15:case 16:case 17:case 18:case 19:// change dynamic ui colors
+break;case 46:// change log file
+break;case 50:// dynamic font
+break;case 51:// emacs shell
+break;case 52:// manipulate selection data
+break;case 104:case 105:case 110:case 111:case 112:case 113:case 114:case 115:case 116:case 117:case 118:// reset colors
+break;}this.params=[];this.currentParam=0;this.state=normal;}else{if(!this.params.length){if(ch>='0'&&ch<='9'){this.currentParam=this.currentParam*10+ch.charCodeAt(0)-48;}else if(ch===';'){this.params.push(this.currentParam);this.currentParam='';}}else{this.currentParam+=ch;}}break;case csi:// '?', '>', '!'
+if(ch==='?'||ch==='>'||ch==='!'){this.prefix=ch;break;}// 0 - 9
+if(ch>='0'&&ch<='9'){this.currentParam=this.currentParam*10+ch.charCodeAt(0)-48;break;}// '$', '"', ' ', '\''
+if(ch==='$'||ch==='"'||ch===' '||ch==='\''){this.postfix=ch;break;}this.params.push(this.currentParam);this.currentParam=0;// ';'
+if(ch===';')break;this.state=normal;switch(ch){// CSI Ps A
+// Cursor Up Ps Times (default = 1) (CUU).
+case'A':this.cursorUp(this.params);break;// CSI Ps B
+// Cursor Down Ps Times (default = 1) (CUD).
+case'B':this.cursorDown(this.params);break;// CSI Ps C
+// Cursor Forward Ps Times (default = 1) (CUF).
+case'C':this.cursorForward(this.params);break;// CSI Ps D
+// Cursor Backward Ps Times (default = 1) (CUB).
+case'D':this.cursorBackward(this.params);break;// CSI Ps ; Ps H
+// Cursor Position [row;column] (default = [1,1]) (CUP).
+case'H':this.cursorPos(this.params);break;// CSI Ps J Erase in Display (ED).
+case'J':this.eraseInDisplay(this.params);break;// CSI Ps K Erase in Line (EL).
+case'K':this.eraseInLine(this.params);break;// CSI Pm m Character Attributes (SGR).
+case'm':if(!this.prefix){this.charAttributes(this.params);}break;// CSI Ps n Device Status Report (DSR).
+case'n':if(!this.prefix){this.deviceStatus(this.params);}break;/**
+ * Additions
+ */// CSI Ps @
+// Insert Ps (Blank) Character(s) (default = 1) (ICH).
+case'@':this.insertChars(this.params);break;// CSI Ps E
+// Cursor Next Line Ps Times (default = 1) (CNL).
+case'E':this.cursorNextLine(this.params);break;// CSI Ps F
+// Cursor Preceding Line Ps Times (default = 1) (CNL).
+case'F':this.cursorPrecedingLine(this.params);break;// CSI Ps G
+// Cursor Character Absolute [column] (default = [row,1]) (CHA).
+case'G':this.cursorCharAbsolute(this.params);break;// CSI Ps L
+// Insert Ps Line(s) (default = 1) (IL).
+case'L':this.insertLines(this.params);break;// CSI Ps M
+// Delete Ps Line(s) (default = 1) (DL).
+case'M':this.deleteLines(this.params);break;// CSI Ps P
+// Delete Ps Character(s) (default = 1) (DCH).
+case'P':this.deleteChars(this.params);break;// CSI Ps X
+// Erase Ps Character(s) (default = 1) (ECH).
+case'X':this.eraseChars(this.params);break;// CSI Pm ` Character Position Absolute
+// [column] (default = [row,1]) (HPA).
+case'`':this.charPosAbsolute(this.params);break;// 141 61 a * HPR -
+// Horizontal Position Relative
+case'a':this.HPositionRelative(this.params);break;// CSI P s c
+// Send Device Attributes (Primary DA).
+// CSI > P s c
+// Send Device Attributes (Secondary DA)
+case'c':this.sendDeviceAttributes(this.params);break;// CSI Pm d
+// Line Position Absolute [row] (default = [1,column]) (VPA).
+case'd':this.linePosAbsolute(this.params);break;// 145 65 e * VPR - Vertical Position Relative
+case'e':this.VPositionRelative(this.params);break;// CSI Ps ; Ps f
+// Horizontal and Vertical Position [row;column] (default =
+// [1,1]) (HVP).
+case'f':this.HVPosition(this.params);break;// CSI Pm h Set Mode (SM).
+// CSI ? Pm h - mouse escape codes, cursor escape codes
+case'h':this.setMode(this.params);break;// CSI Pm l Reset Mode (RM).
+// CSI ? Pm l
+case'l':this.resetMode(this.params);break;// CSI Ps ; Ps r
+// Set Scrolling Region [top;bottom] (default = full size of win-
+// dow) (DECSTBM).
+// CSI ? Pm r
+case'r':this.setScrollRegion(this.params);break;// CSI s
+// Save cursor (ANSI.SYS).
+case's':this.saveCursor(this.params);break;// CSI u
+// Restore cursor (ANSI.SYS).
+case'u':this.restoreCursor(this.params);break;/**
+ * Lesser Used
+ */// CSI Ps I
+// Cursor Forward Tabulation Ps tab stops (default = 1) (CHT).
+case'I':this.cursorForwardTab(this.params);break;// CSI Ps S Scroll up Ps lines (default = 1) (SU).
+case'S':this.scrollUp(this.params);break;// CSI Ps T Scroll down Ps lines (default = 1) (SD).
+// CSI Ps ; Ps ; Ps ; Ps ; Ps T
+// CSI > Ps; Ps T
+case'T':// if (this.prefix === '>') {
+// this.resetTitleModes(this.params);
+// break;
+// }
+// if (this.params.length > 2) {
+// this.initMouseTracking(this.params);
+// break;
+// }
+if(this.params.length<2&&!this.prefix){this.scrollDown(this.params);}break;// CSI Ps Z
+// Cursor Backward Tabulation Ps tab stops (default = 1) (CBT).
+case'Z':this.cursorBackwardTab(this.params);break;// CSI Ps b Repeat the preceding graphic character Ps times (REP).
+case'b':this.repeatPrecedingCharacter(this.params);break;// CSI Ps g Tab Clear (TBC).
+case'g':this.tabClear(this.params);break;// CSI Pm i Media Copy (MC).
+// CSI ? Pm i
+// case 'i':
+// this.mediaCopy(this.params);
+// break;
+// CSI Pm m Character Attributes (SGR).
+// CSI > Ps; Ps m
+// case 'm': // duplicate
+// if (this.prefix === '>') {
+// this.setResources(this.params);
+// } else {
+// this.charAttributes(this.params);
+// }
+// break;
+// CSI Ps n Device Status Report (DSR).
+// CSI > Ps n
+// case 'n': // duplicate
+// if (this.prefix === '>') {
+// this.disableModifiers(this.params);
+// } else {
+// this.deviceStatus(this.params);
+// }
+// break;
+// CSI > Ps p Set pointer mode.
+// CSI ! p Soft terminal reset (DECSTR).
+// CSI Ps$ p
+// Request ANSI mode (DECRQM).
+// CSI ? Ps$ p
+// Request DEC private mode (DECRQM).
+// CSI Ps ; Ps " p
+case'p':switch(this.prefix){// case '>':
+// this.setPointerMode(this.params);
+// break;
+case'!':this.softReset(this.params);break;// case '?':
+// if (this.postfix === '$') {
+// this.requestPrivateMode(this.params);
+// }
+// break;
+// default:
+// if (this.postfix === '"') {
+// this.setConformanceLevel(this.params);
+// } else if (this.postfix === '$') {
+// this.requestAnsiMode(this.params);
+// }
+// break;
+}break;// CSI Ps q Load LEDs (DECLL).
+// CSI Ps SP q
+// CSI Ps " q
+// case 'q':
+// if (this.postfix === ' ') {
+// this.setCursorStyle(this.params);
+// break;
+// }
+// if (this.postfix === '"') {
+// this.setCharProtectionAttr(this.params);
+// break;
+// }
+// this.loadLEDs(this.params);
+// break;
+// CSI Ps ; Ps r
+// Set Scrolling Region [top;bottom] (default = full size of win-
+// dow) (DECSTBM).
+// CSI ? Pm r
+// CSI Pt; Pl; Pb; Pr; Ps$ r
+// case 'r': // duplicate
+// if (this.prefix === '?') {
+// this.restorePrivateValues(this.params);
+// } else if (this.postfix === '$') {
+// this.setAttrInRectangle(this.params);
+// } else {
+// this.setScrollRegion(this.params);
+// }
+// break;
+// CSI s Save cursor (ANSI.SYS).
+// CSI ? Pm s
+// case 's': // duplicate
+// if (this.prefix === '?') {
+// this.savePrivateValues(this.params);
+// } else {
+// this.saveCursor(this.params);
+// }
+// break;
+// CSI Ps ; Ps ; Ps t
+// CSI Pt; Pl; Pb; Pr; Ps$ t
+// CSI > Ps; Ps t
+// CSI Ps SP t
+// case 't':
+// if (this.postfix === '$') {
+// this.reverseAttrInRectangle(this.params);
+// } else if (this.postfix === ' ') {
+// this.setWarningBellVolume(this.params);
+// } else {
+// if (this.prefix === '>') {
+// this.setTitleModeFeature(this.params);
+// } else {
+// this.manipulateWindow(this.params);
+// }
+// }
+// break;
+// CSI u Restore cursor (ANSI.SYS).
+// CSI Ps SP u
+// case 'u': // duplicate
+// if (this.postfix === ' ') {
+// this.setMarginBellVolume(this.params);
+// } else {
+// this.restoreCursor(this.params);
+// }
+// break;
+// CSI Pt; Pl; Pb; Pr; Pp; Pt; Pl; Pp$ v
+// case 'v':
+// if (this.postfix === '$') {
+// this.copyRectagle(this.params);
+// }
+// break;
+// CSI Pt ; Pl ; Pb ; Pr ' w
+// case 'w':
+// if (this.postfix === '\'') {
+// this.enableFilterRectangle(this.params);
+// }
+// break;
+// CSI Ps x Request Terminal Parameters (DECREQTPARM).
+// CSI Ps x Select Attribute Change Extent (DECSACE).
+// CSI Pc; Pt; Pl; Pb; Pr$ x
+// case 'x':
+// if (this.postfix === '$') {
+// this.fillRectangle(this.params);
+// } else {
+// this.requestParameters(this.params);
+// //this.__(this.params);
+// }
+// break;
+// CSI Ps ; Pu ' z
+// CSI Pt; Pl; Pb; Pr$ z
+// case 'z':
+// if (this.postfix === '\'') {
+// this.enableLocatorReporting(this.params);
+// } else if (this.postfix === '$') {
+// this.eraseRectangle(this.params);
+// }
+// break;
+// CSI Pm ' {
+// CSI Pt; Pl; Pb; Pr$ {
+// case '{':
+// if (this.postfix === '\'') {
+// this.setLocatorEvents(this.params);
+// } else if (this.postfix === '$') {
+// this.selectiveEraseRectangle(this.params);
+// }
+// break;
+// CSI Ps ' |
+// case '|':
+// if (this.postfix === '\'') {
+// this.requestLocatorPosition(this.params);
+// }
+// break;
+// CSI P m SP }
+// Insert P s Column(s) (default = 1) (DECIC), VT420 and up.
+// case '}':
+// if (this.postfix === ' ') {
+// this.insertColumns(this.params);
+// }
+// break;
+// CSI P m SP ~
+// Delete P s Column(s) (default = 1) (DECDC), VT420 and up
+// case '~':
+// if (this.postfix === ' ') {
+// this.deleteColumns(this.params);
+// }
+// break;
+default:this.error('Unknown CSI code: %s.',ch);break;}this.prefix='';this.postfix='';break;case dcs:if(ch==='\x1b'||ch==='\x07'){if(ch==='\x1b')i++;switch(this.prefix){// User-Defined Keys (DECUDK).
+case'':break;// Request Status String (DECRQSS).
+// test: echo -e '\eP$q"p\e\\'
+case'$q':var pt=this.currentParam,valid=false;switch(pt){// DECSCA
+case'"q':pt='0"q';break;// DECSCL
+case'"p':pt='61"p';break;// DECSTBM
+case'r':pt=''+(this.scrollTop+1)+';'+(this.scrollBottom+1)+'r';break;// SGR
+case'm':pt='0m';break;default:this.error('Unknown DCS Pt: %s.',pt);pt='';break;}this.send('\x1bP'+ +valid+'$r'+pt+'\x1b\\');break;// Set Termcap/Terminfo Data (xterm, experimental).
+case'+p':break;// Request Termcap/Terminfo String (xterm, experimental)
+// Regular xterm does not even respond to this sequence.
+// This can cause a small glitch in vim.
+// test: echo -ne '\eP+q6b64\e\\'
+case'+q':var pt=this.currentParam,valid=false;this.send('\x1bP'+ +valid+'+r'+pt+'\x1b\\');break;default:this.error('Unknown DCS prefix: %s.',this.prefix);break;}this.currentParam=0;this.prefix='';this.state=normal;}else if(!this.currentParam){if(!this.prefix&&ch!=='$'&&ch!=='+'){this.currentParam=ch;}else if(this.prefix.length===2){this.currentParam=ch;}else{this.prefix+=ch;}}else{this.currentParam+=ch;}break;case ignore:// For PM and APC.
+if(ch==='\x1b'||ch==='\x07'){if(ch==='\x1b')i++;this.state=normal;}break;}}this.updateRange(this.y);this.refresh(this.refreshStart,this.refreshEnd);};/**
+ * Writes text to the terminal, followed by a break line character (\n).
+ * @param {string} text The text to write to the terminal.
+ */Terminal.prototype.writeln=function(data){this.write(data+'\r\n');};/**
+ * Attaches a custom keydown handler which is run before keys are processed, giving consumers of
+ * xterm.js ultimate control as to what keys should be processed by the terminal and what keys
+ * should not.
+ * @param {function} customKeydownHandler The custom KeyboardEvent handler to attach. This is a
+ * function that takes a KeyboardEvent, allowing consumers to stop propogation and/or prevent
+ * the default action. The function returns whether the event should be processed by xterm.js.
+ */Terminal.prototype.attachCustomKeydownHandler=function(customKeydownHandler){this.customKeydownHandler=customKeydownHandler;};/**
+ * Handle a keydown event
+ * Key Resources:
+ * - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent
+ * @param {KeyboardEvent} ev The keydown event to be handled.
+ */Terminal.prototype.keyDown=function(ev){// Scroll down to prompt, whenever the user presses a key.
+if(this.ybase!==this.ydisp){this.scrollToBottom();}if(this.customKeydownHandler&&this.customKeydownHandler(ev)===false){return false;}if(!this.compositionHelper.keydown.bind(this.compositionHelper)(ev)){return false;}var self=this;var result=this.evaluateKeyEscapeSequence(ev);if(result.scrollDisp){this.scrollDisp(result.scrollDisp);return this.cancel(ev,true);}if(isThirdLevelShift(this,ev)){return true;}if(result.cancel){// The event is canceled at the end already, is this necessary?
+this.cancel(ev,true);}if(!result.key){return true;}this.emit('keydown',ev);this.emit('key',result.key,ev);this.showCursor();this.handler(result.key);return this.cancel(ev,true);};/**
+ * Returns an object that determines how a KeyboardEvent should be handled. The key of the
+ * returned value is the new key code to pass to the PTY.
+ *
+ * Reference: http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
+ * @param {KeyboardEvent} ev The keyboard event to be translated to key escape sequence.
+ */Terminal.prototype.evaluateKeyEscapeSequence=function(ev){var result={// Whether to cancel event propogation (NOTE: this may not be needed since the event is
+// canceled at the end of keyDown
+cancel:false,// The new key even to emit
+key:undefined,// The number of characters to scroll, if this is defined it will cancel the event
+scrollDisp:undefined};var modifiers=ev.shiftKey<<0|ev.altKey<<1|ev.ctrlKey<<2|ev.metaKey<<3;switch(ev.keyCode){case 8:// backspace
+if(ev.shiftKey){result.key='\x08';// ^H
+break;}result.key='\x7f';// ^?
+break;case 9:// tab
+if(ev.shiftKey){result.key='\x1b[Z';break;}result.key='\t';result.cancel=true;break;case 13:// return/enter
+result.key='\r';result.cancel=true;break;case 27:// escape
+result.key='\x1b';result.cancel=true;break;case 37:// left-arrow
+if(modifiers){result.key='\x1b[1;'+(modifiers+1)+'D';// HACK: Make Alt + left-arrow behave like Ctrl + left-arrow: move one word backwards
+// http://unix.stackexchange.com/a/108106
+if(result.key=='\x1b[1;3D'){result.key='\x1b[1;5D';}}else if(this.applicationCursor){result.key='\x1bOD';}else{result.key='\x1b[D';}break;case 39:// right-arrow
+if(modifiers){result.key='\x1b[1;'+(modifiers+1)+'C';// HACK: Make Alt + right-arrow behave like Ctrl + right-arrow: move one word forward
+// http://unix.stackexchange.com/a/108106
+if(result.key=='\x1b[1;3C'){result.key='\x1b[1;5C';}}else if(this.applicationCursor){result.key='\x1bOC';}else{result.key='\x1b[C';}break;case 38:// up-arrow
+if(modifiers){result.key='\x1b[1;'+(modifiers+1)+'A';// HACK: Make Alt + up-arrow behave like Ctrl + up-arrow
+// http://unix.stackexchange.com/a/108106
+if(result.key=='\x1b[1;3A'){result.key='\x1b[1;5A';}}else if(this.applicationCursor){result.key='\x1bOA';}else{result.key='\x1b[A';}break;case 40:// down-arrow
+if(modifiers){result.key='\x1b[1;'+(modifiers+1)+'B';// HACK: Make Alt + down-arrow behave like Ctrl + down-arrow
+// http://unix.stackexchange.com/a/108106
+if(result.key=='\x1b[1;3B'){result.key='\x1b[1;5B';}}else if(this.applicationCursor){result.key='\x1bOB';}else{result.key='\x1b[B';}break;case 45:// insert
+if(!ev.shiftKey&&!ev.ctrlKey){// <Ctrl> or <Shift> + <Insert> are used to
+// copy-paste on some systems.
+result.key='\x1b[2~';}break;case 46:// delete
+if(modifiers){result.key='\x1b[3;'+(modifiers+1)+'~';}else{result.key='\x1b[3~';}break;case 36:// home
+if(modifiers)result.key='\x1b[1;'+(modifiers+1)+'H';else if(this.applicationCursor)result.key='\x1bOH';else result.key='\x1b[H';break;case 35:// end
+if(modifiers)result.key='\x1b[1;'+(modifiers+1)+'F';else if(this.applicationCursor)result.key='\x1bOF';else result.key='\x1b[F';break;case 33:// page up
+if(ev.shiftKey){result.scrollDisp=-(this.rows-1);}else{result.key='\x1b[5~';}break;case 34:// page down
+if(ev.shiftKey){result.scrollDisp=this.rows-1;}else{result.key='\x1b[6~';}break;case 112:// F1-F12
+if(modifiers){result.key='\x1b[1;'+(modifiers+1)+'P';}else{result.key='\x1bOP';}break;case 113:if(modifiers){result.key='\x1b[1;'+(modifiers+1)+'Q';}else{result.key='\x1bOQ';}break;case 114:if(modifiers){result.key='\x1b[1;'+(modifiers+1)+'R';}else{result.key='\x1bOR';}break;case 115:if(modifiers){result.key='\x1b[1;'+(modifiers+1)+'S';}else{result.key='\x1bOS';}break;case 116:if(modifiers){result.key='\x1b[15;'+(modifiers+1)+'~';}else{result.key='\x1b[15~';}break;case 117:if(modifiers){result.key='\x1b[17;'+(modifiers+1)+'~';}else{result.key='\x1b[17~';}break;case 118:if(modifiers){result.key='\x1b[18;'+(modifiers+1)+'~';}else{result.key='\x1b[18~';}break;case 119:if(modifiers){result.key='\x1b[19;'+(modifiers+1)+'~';}else{result.key='\x1b[19~';}break;case 120:if(modifiers){result.key='\x1b[20;'+(modifiers+1)+'~';}else{result.key='\x1b[20~';}break;case 121:if(modifiers){result.key='\x1b[21;'+(modifiers+1)+'~';}else{result.key='\x1b[21~';}break;case 122:if(modifiers){result.key='\x1b[23;'+(modifiers+1)+'~';}else{result.key='\x1b[23~';}break;case 123:if(modifiers){result.key='\x1b[24;'+(modifiers+1)+'~';}else{result.key='\x1b[24~';}break;default:// a-z and space
+if(ev.ctrlKey&&!ev.shiftKey&&!ev.altKey&&!ev.metaKey){if(ev.keyCode>=65&&ev.keyCode<=90){result.key=String.fromCharCode(ev.keyCode-64);}else if(ev.keyCode===32){// NUL
+result.key=String.fromCharCode(0);}else if(ev.keyCode>=51&&ev.keyCode<=55){// escape, file sep, group sep, record sep, unit sep
+result.key=String.fromCharCode(ev.keyCode-51+27);}else if(ev.keyCode===56){// delete
+result.key=String.fromCharCode(127);}else if(ev.keyCode===219){// ^[ - escape
+result.key=String.fromCharCode(27);}else if(ev.keyCode===221){// ^] - group sep
+result.key=String.fromCharCode(29);}}else if(!this.browser.isMac&&ev.altKey&&!ev.ctrlKey&&!ev.metaKey){// On Mac this is a third level shift. Use <Esc> instead.
+if(ev.keyCode>=65&&ev.keyCode<=90){result.key='\x1b'+String.fromCharCode(ev.keyCode+32);}else if(ev.keyCode===192){result.key='\x1b`';}else if(ev.keyCode>=48&&ev.keyCode<=57){result.key='\x1b'+(ev.keyCode-48);}}break;}return result;};/**
+ * Set the G level of the terminal
+ * @param g
+ */Terminal.prototype.setgLevel=function(g){this.glevel=g;this.charset=this.charsets[g];};/**
+ * Set the charset for the given G level of the terminal
+ * @param g
+ * @param charset
+ */Terminal.prototype.setgCharset=function(g,charset){this.charsets[g]=charset;if(this.glevel===g){this.charset=charset;}};/**
+ * Handle a keypress event.
+ * Key Resources:
+ * - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent
+ * @param {KeyboardEvent} ev The keypress event to be handled.
+ */Terminal.prototype.keyPress=function(ev){var key;this.cancel(ev);if(ev.charCode){key=ev.charCode;}else if(ev.which==null){key=ev.keyCode;}else if(ev.which!==0&&ev.charCode!==0){key=ev.which;}else{return false;}if(!key||(ev.altKey||ev.ctrlKey||ev.metaKey)&&!isThirdLevelShift(this,ev)){return false;}key=String.fromCharCode(key);this.emit('keypress',key,ev);this.emit('key',key,ev);this.showCursor();this.handler(key);return false;};/**
+ * Send data for handling to the terminal
+ * @param {string} data
+ */Terminal.prototype.send=function(data){var self=this;if(!this.queue){setTimeout(function(){self.handler(self.queue);self.queue='';},1);}this.queue+=data;};/**
+ * Ring the bell.
+ * Note: We could do sweet things with webaudio here
+ */Terminal.prototype.bell=function(){if(!this.visualBell)return;var self=this;this.element.style.borderColor='white';setTimeout(function(){self.element.style.borderColor='';},10);if(this.popOnBell)this.focus();};/**
+ * Log the current state to the console.
+ */Terminal.prototype.log=function(){if(!this.debug)return;if(!this.context.console||!this.context.console.log)return;var args=Array.prototype.slice.call(arguments);this.context.console.log.apply(this.context.console,args);};/**
+ * Log the current state as error to the console.
+ */Terminal.prototype.error=function(){if(!this.debug)return;if(!this.context.console||!this.context.console.error)return;var args=Array.prototype.slice.call(arguments);this.context.console.error.apply(this.context.console,args);};/**
+ * Resizes the terminal.
+ *
+ * @param {number} x The number of columns to resize to.
+ * @param {number} y The number of rows to resize to.
+ */Terminal.prototype.resize=function(x,y){var line,el,i,j,ch,addToY;if(x===this.cols&&y===this.rows){return;}if(x<1)x=1;if(y<1)y=1;// resize cols
+j=this.cols;if(j<x){ch=[this.defAttr,' ',1];// does xterm use the default attr?
+i=this.lines.length;while(i--){while(this.lines[i].length<x){this.lines[i].push(ch);}}}else{// (j > x)
+i=this.lines.length;while(i--){while(this.lines[i].length>x){this.lines[i].pop();}}}this.setupStops(j);this.cols=x;// resize rows
+j=this.rows;addToY=0;if(j<y){el=this.element;while(j++<y){// y is rows, not this.y
+if(this.lines.length<y+this.ybase){if(this.ybase>0&&this.lines.length<=this.ybase+this.y+addToY+1){// There is room above the buffer and there are no empty elements below the line,
+// scroll up
+this.ybase--;addToY++;if(this.ydisp>0){// Viewport is at the top of the buffer, must increase downwards
+this.ydisp--;}}else{// Add a blank line if there is no buffer left at the top to scroll to, or if there
+// are blank lines after the cursor
+this.lines.push(this.blankLine());}}if(this.children.length<y){this.insertRow();}}}else{// (j > y)
+while(j-->y){if(this.lines.length>y+this.ybase){if(this.lines.length>this.ybase+this.y+1){// The line is a blank line below the cursor, remove it
+this.lines.pop();}else{// The line is the cursor, scroll down
+this.ybase++;this.ydisp++;}}if(this.children.length>y){el=this.children.shift();if(!el)continue;el.parentNode.removeChild(el);}}}this.rows=y;// Make sure that the cursor stays on screen
+if(this.y>=y){this.y=y-1;}if(addToY){this.y+=addToY;}if(this.x>=x){this.x=x-1;}this.scrollTop=0;this.scrollBottom=y-1;this.refresh(0,this.rows-1);this.normal=null;this.geometry=[this.cols,this.rows];this.emit('resize',{terminal:this,cols:x,rows:y});};/**
+ * Updates the range of rows to refresh
+ * @param {number} y The number of rows to refresh next.
+ */Terminal.prototype.updateRange=function(y){if(y<this.refreshStart)this.refreshStart=y;if(y>this.refreshEnd)this.refreshEnd=y;// if (y > this.refreshEnd) {
+// this.refreshEnd = y;
+// if (y > this.rows - 1) {
+// this.refreshEnd = this.rows - 1;
+// }
+// }
+};/**
+ * Set the range of refreshing to the maximum value
+ */Terminal.prototype.maxRange=function(){this.refreshStart=0;this.refreshEnd=this.rows-1;};/**
+ * Setup the tab stops.
+ * @param {number} i
+ */Terminal.prototype.setupStops=function(i){if(i!=null){if(!this.tabs[i]){i=this.prevStop(i);}}else{this.tabs={};i=0;}for(;i<this.cols;i+=8){this.tabs[i]=true;}};/**
+ * Move the cursor to the previous tab stop from the given position (default is current).
+ * @param {number} x The position to move the cursor to the previous tab stop.
+ */Terminal.prototype.prevStop=function(x){if(x==null)x=this.x;while(!this.tabs[--x]&&x>0){}return x>=this.cols?this.cols-1:x<0?0:x;};/**
+ * Move the cursor one tab stop forward from the given position (default is current).
+ * @param {number} x The position to move the cursor one tab stop forward.
+ */Terminal.prototype.nextStop=function(x){if(x==null)x=this.x;while(!this.tabs[++x]&&x<this.cols){}return x>=this.cols?this.cols-1:x<0?0:x;};/**
+ * Erase in the identified line everything from "x" to the end of the line (right).
+ * @param {number} x The column from which to start erasing to the end of the line.
+ * @param {number} y The line in which to operate.
+ */Terminal.prototype.eraseRight=function(x,y){var line=this.lines[this.ybase+y],ch=[this.eraseAttr(),' ',1];// xterm
+for(;x<this.cols;x++){line[x]=ch;}this.updateRange(y);};/**
+ * Erase in the identified line everything from "x" to the start of the line (left).
+ * @param {number} x The column from which to start erasing to the start of the line.
+ * @param {number} y The line in which to operate.
+ */Terminal.prototype.eraseLeft=function(x,y){var line=this.lines[this.ybase+y],ch=[this.eraseAttr(),' ',1];// xterm
+x++;while(x--){line[x]=ch;}this.updateRange(y);};/**
+ * Clears the entire buffer, making the prompt line the new first line.
+ */Terminal.prototype.clear=function(){if(this.ybase===0&&this.y===0){// Don't clear if it's already clear
+return;}this.lines=[this.lines[this.ybase+this.y]];this.ydisp=0;this.ybase=0;this.y=0;for(var i=1;i<this.rows;i++){this.lines.push(this.blankLine());}this.refresh(0,this.rows-1);this.emit('scroll',this.ydisp);};/**
+ * Erase all content in the given line
+ * @param {number} y The line to erase all of its contents.
+ */Terminal.prototype.eraseLine=function(y){this.eraseRight(0,y);};/**
+ * Return the data array of a blank line/
+ * @param {number} cur First bunch of data for each "blank" character.
+ */Terminal.prototype.blankLine=function(cur){var attr=cur?this.eraseAttr():this.defAttr;var ch=[attr,' ',1]// width defaults to 1 halfwidth character
+,line=[],i=0;for(;i<this.cols;i++){line[i]=ch;}return line;};/**
+ * If cur return the back color xterm feature attribute. Else return defAttr.
+ * @param {object} cur
+ */Terminal.prototype.ch=function(cur){return cur?[this.eraseAttr(),' ',1]:[this.defAttr,' ',1];};/**
+ * Evaluate if the current erminal is the given argument.
+ * @param {object} term The terminal to evaluate
+ */Terminal.prototype.is=function(term){var name=this.termName;return(name+'').indexOf(term)===0;};/**
+ * Emit the 'data' event and populate the given data.
+ * @param {string} data The data to populate in the event.
+ */Terminal.prototype.handler=function(data){this.emit('data',data);};/**
+ * Emit the 'title' event and populate the given title.
+ * @param {string} title The title to populate in the event.
+ */Terminal.prototype.handleTitle=function(title){this.emit('title',title);};/**
+ * ESC
+ *//**
+ * ESC D Index (IND is 0x84).
+ */Terminal.prototype.index=function(){this.y++;if(this.y>this.scrollBottom){this.y--;this.scroll();}this.state=normal;};/**
+ * ESC M Reverse Index (RI is 0x8d).
+ */Terminal.prototype.reverseIndex=function(){var j;this.y--;if(this.y<this.scrollTop){this.y++;// possibly move the code below to term.reverseScroll();
+// test: echo -ne '\e[1;1H\e[44m\eM\e[0m'
+// blankLine(true) is xterm/linux behavior
+this.lines.splice(this.y+this.ybase,0,this.blankLine(true));j=this.rows-1-this.scrollBottom;this.lines.splice(this.rows-1+this.ybase-j+1,1);// this.maxRange();
+this.updateRange(this.scrollTop);this.updateRange(this.scrollBottom);}this.state=normal;};/**
+ * ESC c Full Reset (RIS).
+ */Terminal.prototype.reset=function(){this.options.rows=this.rows;this.options.cols=this.cols;var customKeydownHandler=this.customKeydownHandler;Terminal.call(this,this.options);this.customKeydownHandler=customKeydownHandler;this.refresh(0,this.rows-1);this.viewport.syncScrollArea();};/**
+ * ESC H Tab Set (HTS is 0x88).
+ */Terminal.prototype.tabSet=function(){this.tabs[this.x]=true;this.state=normal;};/**
+ * CSI
+ *//**
+ * CSI Ps A
+ * Cursor Up Ps Times (default = 1) (CUU).
+ */Terminal.prototype.cursorUp=function(params){var param=params[0];if(param<1)param=1;this.y-=param;if(this.y<0)this.y=0;};/**
+ * CSI Ps B
+ * Cursor Down Ps Times (default = 1) (CUD).
+ */Terminal.prototype.cursorDown=function(params){var param=params[0];if(param<1)param=1;this.y+=param;if(this.y>=this.rows){this.y=this.rows-1;}};/**
+ * CSI Ps C
+ * Cursor Forward Ps Times (default = 1) (CUF).
+ */Terminal.prototype.cursorForward=function(params){var param=params[0];if(param<1)param=1;this.x+=param;if(this.x>=this.cols){this.x=this.cols-1;}};/**
+ * CSI Ps D
+ * Cursor Backward Ps Times (default = 1) (CUB).
+ */Terminal.prototype.cursorBackward=function(params){var param=params[0];if(param<1)param=1;this.x-=param;if(this.x<0)this.x=0;};/**
+ * CSI Ps ; Ps H
+ * Cursor Position [row;column] (default = [1,1]) (CUP).
+ */Terminal.prototype.cursorPos=function(params){var row,col;row=params[0]-1;if(params.length>=2){col=params[1]-1;}else{col=0;}if(row<0){row=0;}else if(row>=this.rows){row=this.rows-1;}if(col<0){col=0;}else if(col>=this.cols){col=this.cols-1;}this.x=col;this.y=row;};/**
+ * CSI Ps J Erase in Display (ED).
+ * Ps = 0 -> Erase Below (default).
+ * Ps = 1 -> Erase Above.
+ * Ps = 2 -> Erase All.
+ * Ps = 3 -> Erase Saved Lines (xterm).
+ * CSI ? Ps J
+ * Erase in Display (DECSED).
+ * Ps = 0 -> Selective Erase Below (default).
+ * Ps = 1 -> Selective Erase Above.
+ * Ps = 2 -> Selective Erase All.
+ */Terminal.prototype.eraseInDisplay=function(params){var j;switch(params[0]){case 0:this.eraseRight(this.x,this.y);j=this.y+1;for(;j<this.rows;j++){this.eraseLine(j);}break;case 1:this.eraseLeft(this.x,this.y);j=this.y;while(j--){this.eraseLine(j);}break;case 2:j=this.rows;while(j--){this.eraseLine(j);}break;case 3:;// no saved lines
+break;}};/**
+ * CSI Ps K Erase in Line (EL).
+ * Ps = 0 -> Erase to Right (default).
+ * Ps = 1 -> Erase to Left.
+ * Ps = 2 -> Erase All.
+ * CSI ? Ps K
+ * Erase in Line (DECSEL).
+ * Ps = 0 -> Selective Erase to Right (default).
+ * Ps = 1 -> Selective Erase to Left.
+ * Ps = 2 -> Selective Erase All.
+ */Terminal.prototype.eraseInLine=function(params){switch(params[0]){case 0:this.eraseRight(this.x,this.y);break;case 1:this.eraseLeft(this.x,this.y);break;case 2:this.eraseLine(this.y);break;}};/**
+ * CSI Pm m Character Attributes (SGR).
+ * Ps = 0 -> Normal (default).
+ * Ps = 1 -> Bold.
+ * Ps = 4 -> Underlined.
+ * Ps = 5 -> Blink (appears as Bold).
+ * Ps = 7 -> Inverse.
+ * Ps = 8 -> Invisible, i.e., hidden (VT300).
+ * Ps = 2 2 -> Normal (neither bold nor faint).
+ * Ps = 2 4 -> Not underlined.
+ * Ps = 2 5 -> Steady (not blinking).
+ * Ps = 2 7 -> Positive (not inverse).
+ * Ps = 2 8 -> Visible, i.e., not hidden (VT300).
+ * Ps = 3 0 -> Set foreground color to Black.
+ * Ps = 3 1 -> Set foreground color to Red.
+ * Ps = 3 2 -> Set foreground color to Green.
+ * Ps = 3 3 -> Set foreground color to Yellow.
+ * Ps = 3 4 -> Set foreground color to Blue.
+ * Ps = 3 5 -> Set foreground color to Magenta.
+ * Ps = 3 6 -> Set foreground color to Cyan.
+ * Ps = 3 7 -> Set foreground color to White.
+ * Ps = 3 9 -> Set foreground color to default (original).
+ * Ps = 4 0 -> Set background color to Black.
+ * Ps = 4 1 -> Set background color to Red.
+ * Ps = 4 2 -> Set background color to Green.
+ * Ps = 4 3 -> Set background color to Yellow.
+ * Ps = 4 4 -> Set background color to Blue.
+ * Ps = 4 5 -> Set background color to Magenta.
+ * Ps = 4 6 -> Set background color to Cyan.
+ * Ps = 4 7 -> Set background color to White.
+ * Ps = 4 9 -> Set background color to default (original).
+ *
+ * If 16-color support is compiled, the following apply. Assume
+ * that xterm's resources are set so that the ISO color codes are
+ * the first 8 of a set of 16. Then the aixterm colors are the
+ * bright versions of the ISO colors:
+ * Ps = 9 0 -> Set foreground color to Black.
+ * Ps = 9 1 -> Set foreground color to Red.
+ * Ps = 9 2 -> Set foreground color to Green.
+ * Ps = 9 3 -> Set foreground color to Yellow.
+ * Ps = 9 4 -> Set foreground color to Blue.
+ * Ps = 9 5 -> Set foreground color to Magenta.
+ * Ps = 9 6 -> Set foreground color to Cyan.
+ * Ps = 9 7 -> Set foreground color to White.
+ * Ps = 1 0 0 -> Set background color to Black.
+ * Ps = 1 0 1 -> Set background color to Red.
+ * Ps = 1 0 2 -> Set background color to Green.
+ * Ps = 1 0 3 -> Set background color to Yellow.
+ * Ps = 1 0 4 -> Set background color to Blue.
+ * Ps = 1 0 5 -> Set background color to Magenta.
+ * Ps = 1 0 6 -> Set background color to Cyan.
+ * Ps = 1 0 7 -> Set background color to White.
+ *
+ * If xterm is compiled with the 16-color support disabled, it
+ * supports the following, from rxvt:
+ * Ps = 1 0 0 -> Set foreground and background color to
+ * default.
+ *
+ * If 88- or 256-color support is compiled, the following apply.
+ * Ps = 3 8 ; 5 ; Ps -> Set foreground color to the second
+ * Ps.
+ * Ps = 4 8 ; 5 ; Ps -> Set background color to the second
+ * Ps.
+ */Terminal.prototype.charAttributes=function(params){// Optimize a single SGR0.
+if(params.length===1&&params[0]===0){this.curAttr=this.defAttr;return;}var l=params.length,i=0,flags=this.curAttr>>18,fg=this.curAttr>>9&0x1ff,bg=this.curAttr&0x1ff,p;for(;i<l;i++){p=params[i];if(p>=30&&p<=37){// fg color 8
+fg=p-30;}else if(p>=40&&p<=47){// bg color 8
+bg=p-40;}else if(p>=90&&p<=97){// fg color 16
+p+=8;fg=p-90;}else if(p>=100&&p<=107){// bg color 16
+p+=8;bg=p-100;}else if(p===0){// default
+flags=this.defAttr>>18;fg=this.defAttr>>9&0x1ff;bg=this.defAttr&0x1ff;// flags = 0;
+// fg = 0x1ff;
+// bg = 0x1ff;
+}else if(p===1){// bold text
+flags|=1;}else if(p===4){// underlined text
+flags|=2;}else if(p===5){// blink
+flags|=4;}else if(p===7){// inverse and positive
+// test with: echo -e '\e[31m\e[42mhello\e[7mworld\e[27mhi\e[m'
+flags|=8;}else if(p===8){// invisible
+flags|=16;}else if(p===22){// not bold
+flags&=~1;}else if(p===24){// not underlined
+flags&=~2;}else if(p===25){// not blink
+flags&=~4;}else if(p===27){// not inverse
+flags&=~8;}else if(p===28){// not invisible
+flags&=~16;}else if(p===39){// reset fg
+fg=this.defAttr>>9&0x1ff;}else if(p===49){// reset bg
+bg=this.defAttr&0x1ff;}else if(p===38){// fg color 256
+if(params[i+1]===2){i+=2;fg=matchColor(params[i]&0xff,params[i+1]&0xff,params[i+2]&0xff);if(fg===-1)fg=0x1ff;i+=2;}else if(params[i+1]===5){i+=2;p=params[i]&0xff;fg=p;}}else if(p===48){// bg color 256
+if(params[i+1]===2){i+=2;bg=matchColor(params[i]&0xff,params[i+1]&0xff,params[i+2]&0xff);if(bg===-1)bg=0x1ff;i+=2;}else if(params[i+1]===5){i+=2;p=params[i]&0xff;bg=p;}}else if(p===100){// reset fg/bg
+fg=this.defAttr>>9&0x1ff;bg=this.defAttr&0x1ff;}else{this.error('Unknown SGR attribute: %d.',p);}}this.curAttr=flags<<18|fg<<9|bg;};/**
+ * CSI Ps n Device Status Report (DSR).
+ * Ps = 5 -> Status Report. Result (``OK'') is
+ * CSI 0 n
+ * Ps = 6 -> Report Cursor Position (CPR) [row;column].
+ * Result is
+ * CSI r ; c R
+ * CSI ? Ps n
+ * Device Status Report (DSR, DEC-specific).
+ * Ps = 6 -> Report Cursor Position (CPR) [row;column] as CSI
+ * ? r ; c R (assumes page is zero).
+ * Ps = 1 5 -> Report Printer status as CSI ? 1 0 n (ready).
+ * or CSI ? 1 1 n (not ready).
+ * Ps = 2 5 -> Report UDK status as CSI ? 2 0 n (unlocked)
+ * or CSI ? 2 1 n (locked).
+ * Ps = 2 6 -> Report Keyboard status as
+ * CSI ? 2 7 ; 1 ; 0 ; 0 n (North American).
+ * The last two parameters apply to VT400 & up, and denote key-
+ * board ready and LK01 respectively.
+ * Ps = 5 3 -> Report Locator status as
+ * CSI ? 5 3 n Locator available, if compiled-in, or
+ * CSI ? 5 0 n No Locator, if not.
+ */Terminal.prototype.deviceStatus=function(params){if(!this.prefix){switch(params[0]){case 5:// status report
+this.send('\x1b[0n');break;case 6:// cursor position
+this.send('\x1b['+(this.y+1)+';'+(this.x+1)+'R');break;}}else if(this.prefix==='?'){// modern xterm doesnt seem to
+// respond to any of these except ?6, 6, and 5
+switch(params[0]){case 6:// cursor position
+this.send('\x1b[?'+(this.y+1)+';'+(this.x+1)+'R');break;case 15:// no printer
+// this.send('\x1b[?11n');
+break;case 25:// dont support user defined keys
+// this.send('\x1b[?21n');
+break;case 26:// north american keyboard
+// this.send('\x1b[?27;1;0;0n');
+break;case 53:// no dec locator/mouse
+// this.send('\x1b[?50n');
+break;}}};/**
+ * Additions
+ *//**
+ * CSI Ps @
+ * Insert Ps (Blank) Character(s) (default = 1) (ICH).
+ */Terminal.prototype.insertChars=function(params){var param,row,j,ch;param=params[0];if(param<1)param=1;row=this.y+this.ybase;j=this.x;ch=[this.eraseAttr(),' ',1];// xterm
+while(param--&&j<this.cols){this.lines[row].splice(j++,0,ch);this.lines[row].pop();}};/**
+ * CSI Ps E
+ * Cursor Next Line Ps Times (default = 1) (CNL).
+ * same as CSI Ps B ?
+ */Terminal.prototype.cursorNextLine=function(params){var param=params[0];if(param<1)param=1;this.y+=param;if(this.y>=this.rows){this.y=this.rows-1;}this.x=0;};/**
+ * CSI Ps F
+ * Cursor Preceding Line Ps Times (default = 1) (CNL).
+ * reuse CSI Ps A ?
+ */Terminal.prototype.cursorPrecedingLine=function(params){var param=params[0];if(param<1)param=1;this.y-=param;if(this.y<0)this.y=0;this.x=0;};/**
+ * CSI Ps G
+ * Cursor Character Absolute [column] (default = [row,1]) (CHA).
+ */Terminal.prototype.cursorCharAbsolute=function(params){var param=params[0];if(param<1)param=1;this.x=param-1;};/**
+ * CSI Ps L
+ * Insert Ps Line(s) (default = 1) (IL).
+ */Terminal.prototype.insertLines=function(params){var param,row,j;param=params[0];if(param<1)param=1;row=this.y+this.ybase;j=this.rows-1-this.scrollBottom;j=this.rows-1+this.ybase-j+1;while(param--){// test: echo -e '\e[44m\e[1L\e[0m'
+// blankLine(true) - xterm/linux behavior
+this.lines.splice(row,0,this.blankLine(true));this.lines.splice(j,1);}// this.maxRange();
+this.updateRange(this.y);this.updateRange(this.scrollBottom);};/**
+ * CSI Ps M
+ * Delete Ps Line(s) (default = 1) (DL).
+ */Terminal.prototype.deleteLines=function(params){var param,row,j;param=params[0];if(param<1)param=1;row=this.y+this.ybase;j=this.rows-1-this.scrollBottom;j=this.rows-1+this.ybase-j;while(param--){// test: echo -e '\e[44m\e[1M\e[0m'
+// blankLine(true) - xterm/linux behavior
+this.lines.splice(j+1,0,this.blankLine(true));this.lines.splice(row,1);}// this.maxRange();
+this.updateRange(this.y);this.updateRange(this.scrollBottom);};/**
+ * CSI Ps P
+ * Delete Ps Character(s) (default = 1) (DCH).
+ */Terminal.prototype.deleteChars=function(params){var param,row,ch;param=params[0];if(param<1)param=1;row=this.y+this.ybase;ch=[this.eraseAttr(),' ',1];// xterm
+while(param--){this.lines[row].splice(this.x,1);this.lines[row].push(ch);}};/**
+ * CSI Ps X
+ * Erase Ps Character(s) (default = 1) (ECH).
+ */Terminal.prototype.eraseChars=function(params){var param,row,j,ch;param=params[0];if(param<1)param=1;row=this.y+this.ybase;j=this.x;ch=[this.eraseAttr(),' ',1];// xterm
+while(param--&&j<this.cols){this.lines[row][j++]=ch;}};/**
+ * CSI Pm ` Character Position Absolute
+ * [column] (default = [row,1]) (HPA).
+ */Terminal.prototype.charPosAbsolute=function(params){var param=params[0];if(param<1)param=1;this.x=param-1;if(this.x>=this.cols){this.x=this.cols-1;}};/**
+ * 141 61 a * HPR -
+ * Horizontal Position Relative
+ * reuse CSI Ps C ?
+ */Terminal.prototype.HPositionRelative=function(params){var param=params[0];if(param<1)param=1;this.x+=param;if(this.x>=this.cols){this.x=this.cols-1;}};/**
+ * CSI Ps c Send Device Attributes (Primary DA).
+ * Ps = 0 or omitted -> request attributes from terminal. The
+ * response depends on the decTerminalID resource setting.
+ * -> CSI ? 1 ; 2 c (``VT100 with Advanced Video Option'')
+ * -> CSI ? 1 ; 0 c (``VT101 with No Options'')
+ * -> CSI ? 6 c (``VT102'')
+ * -> CSI ? 6 0 ; 1 ; 2 ; 6 ; 8 ; 9 ; 1 5 ; c (``VT220'')
+ * The VT100-style response parameters do not mean anything by
+ * themselves. VT220 parameters do, telling the host what fea-
+ * tures the terminal supports:
+ * Ps = 1 -> 132-columns.
+ * Ps = 2 -> Printer.
+ * Ps = 6 -> Selective erase.
+ * Ps = 8 -> User-defined keys.
+ * Ps = 9 -> National replacement character sets.
+ * Ps = 1 5 -> Technical characters.
+ * Ps = 2 2 -> ANSI color, e.g., VT525.
+ * Ps = 2 9 -> ANSI text locator (i.e., DEC Locator mode).
+ * CSI > Ps c
+ * Send Device Attributes (Secondary DA).
+ * Ps = 0 or omitted -> request the terminal's identification
+ * code. The response depends on the decTerminalID resource set-
+ * ting. It should apply only to VT220 and up, but xterm extends
+ * this to VT100.
+ * -> CSI > Pp ; Pv ; Pc c
+ * where Pp denotes the terminal type
+ * Pp = 0 -> ``VT100''.
+ * Pp = 1 -> ``VT220''.
+ * and Pv is the firmware version (for xterm, this was originally
+ * the XFree86 patch number, starting with 95). In a DEC termi-
+ * nal, Pc indicates the ROM cartridge registration number and is
+ * always zero.
+ * More information:
+ * xterm/charproc.c - line 2012, for more information.
+ * vim responds with ^[[?0c or ^[[?1c after the terminal's response (?)
+ */Terminal.prototype.sendDeviceAttributes=function(params){if(params[0]>0)return;if(!this.prefix){if(this.is('xterm')||this.is('rxvt-unicode')||this.is('screen')){this.send('\x1b[?1;2c');}else if(this.is('linux')){this.send('\x1b[?6c');}}else if(this.prefix==='>'){// xterm and urxvt
+// seem to spit this
+// out around ~370 times (?).
+if(this.is('xterm')){this.send('\x1b[>0;276;0c');}else if(this.is('rxvt-unicode')){this.send('\x1b[>85;95;0c');}else if(this.is('linux')){// not supported by linux console.
+// linux console echoes parameters.
+this.send(params[0]+'c');}else if(this.is('screen')){this.send('\x1b[>83;40003;0c');}}};/**
+ * CSI Pm d
+ * Line Position Absolute [row] (default = [1,column]) (VPA).
+ */Terminal.prototype.linePosAbsolute=function(params){var param=params[0];if(param<1)param=1;this.y=param-1;if(this.y>=this.rows){this.y=this.rows-1;}};/**
+ * 145 65 e * VPR - Vertical Position Relative
+ * reuse CSI Ps B ?
+ */Terminal.prototype.VPositionRelative=function(params){var param=params[0];if(param<1)param=1;this.y+=param;if(this.y>=this.rows){this.y=this.rows-1;}};/**
+ * CSI Ps ; Ps f
+ * Horizontal and Vertical Position [row;column] (default =
+ * [1,1]) (HVP).
+ */Terminal.prototype.HVPosition=function(params){if(params[0]<1)params[0]=1;if(params[1]<1)params[1]=1;this.y=params[0]-1;if(this.y>=this.rows){this.y=this.rows-1;}this.x=params[1]-1;if(this.x>=this.cols){this.x=this.cols-1;}};/**
+ * CSI Pm h Set Mode (SM).
+ * Ps = 2 -> Keyboard Action Mode (AM).
+ * Ps = 4 -> Insert Mode (IRM).
+ * Ps = 1 2 -> Send/receive (SRM).
+ * Ps = 2 0 -> Automatic Newline (LNM).
+ * CSI ? Pm h
+ * DEC Private Mode Set (DECSET).
+ * Ps = 1 -> Application Cursor Keys (DECCKM).
+ * Ps = 2 -> Designate USASCII for character sets G0-G3
+ * (DECANM), and set VT100 mode.
+ * Ps = 3 -> 132 Column Mode (DECCOLM).
+ * Ps = 4 -> Smooth (Slow) Scroll (DECSCLM).
+ * Ps = 5 -> Reverse Video (DECSCNM).
+ * Ps = 6 -> Origin Mode (DECOM).
+ * Ps = 7 -> Wraparound Mode (DECAWM).
+ * Ps = 8 -> Auto-repeat Keys (DECARM).
+ * Ps = 9 -> Send Mouse X & Y on button press. See the sec-
+ * tion Mouse Tracking.
+ * Ps = 1 0 -> Show toolbar (rxvt).
+ * Ps = 1 2 -> Start Blinking Cursor (att610).
+ * Ps = 1 8 -> Print form feed (DECPFF).
+ * Ps = 1 9 -> Set print extent to full screen (DECPEX).
+ * Ps = 2 5 -> Show Cursor (DECTCEM).
+ * Ps = 3 0 -> Show scrollbar (rxvt).
+ * Ps = 3 5 -> Enable font-shifting functions (rxvt).
+ * Ps = 3 8 -> Enter Tektronix Mode (DECTEK).
+ * Ps = 4 0 -> Allow 80 -> 132 Mode.
+ * Ps = 4 1 -> more(1) fix (see curses resource).
+ * Ps = 4 2 -> Enable Nation Replacement Character sets (DECN-
+ * RCM).
+ * Ps = 4 4 -> Turn On Margin Bell.
+ * Ps = 4 5 -> Reverse-wraparound Mode.
+ * Ps = 4 6 -> Start Logging. This is normally disabled by a
+ * compile-time option.
+ * Ps = 4 7 -> Use Alternate Screen Buffer. (This may be dis-
+ * abled by the titeInhibit resource).
+ * Ps = 6 6 -> Application keypad (DECNKM).
+ * Ps = 6 7 -> Backarrow key sends backspace (DECBKM).
+ * Ps = 1 0 0 0 -> Send Mouse X & Y on button press and
+ * release. See the section Mouse Tracking.
+ * Ps = 1 0 0 1 -> Use Hilite Mouse Tracking.
+ * Ps = 1 0 0 2 -> Use Cell Motion Mouse Tracking.
+ * Ps = 1 0 0 3 -> Use All Motion Mouse Tracking.
+ * Ps = 1 0 0 4 -> Send FocusIn/FocusOut events.
+ * Ps = 1 0 0 5 -> Enable Extended Mouse Mode.
+ * Ps = 1 0 1 0 -> Scroll to bottom on tty output (rxvt).
+ * Ps = 1 0 1 1 -> Scroll to bottom on key press (rxvt).
+ * Ps = 1 0 3 4 -> Interpret "meta" key, sets eighth bit.
+ * (enables the eightBitInput resource).
+ * Ps = 1 0 3 5 -> Enable special modifiers for Alt and Num-
+ * Lock keys. (This enables the numLock resource).
+ * Ps = 1 0 3 6 -> Send ESC when Meta modifies a key. (This
+ * enables the metaSendsEscape resource).
+ * Ps = 1 0 3 7 -> Send DEL from the editing-keypad Delete
+ * key.
+ * Ps = 1 0 3 9 -> Send ESC when Alt modifies a key. (This
+ * enables the altSendsEscape resource).
+ * Ps = 1 0 4 0 -> Keep selection even if not highlighted.
+ * (This enables the keepSelection resource).
+ * Ps = 1 0 4 1 -> Use the CLIPBOARD selection. (This enables
+ * the selectToClipboard resource).
+ * Ps = 1 0 4 2 -> Enable Urgency window manager hint when
+ * Control-G is received. (This enables the bellIsUrgent
+ * resource).
+ * Ps = 1 0 4 3 -> Enable raising of the window when Control-G
+ * is received. (enables the popOnBell resource).
+ * Ps = 1 0 4 7 -> Use Alternate Screen Buffer. (This may be
+ * disabled by the titeInhibit resource).
+ * Ps = 1 0 4 8 -> Save cursor as in DECSC. (This may be dis-
+ * abled by the titeInhibit resource).
+ * Ps = 1 0 4 9 -> Save cursor as in DECSC and use Alternate
+ * Screen Buffer, clearing it first. (This may be disabled by
+ * the titeInhibit resource). This combines the effects of the 1
+ * 0 4 7 and 1 0 4 8 modes. Use this with terminfo-based
+ * applications rather than the 4 7 mode.
+ * Ps = 1 0 5 0 -> Set terminfo/termcap function-key mode.
+ * Ps = 1 0 5 1 -> Set Sun function-key mode.
+ * Ps = 1 0 5 2 -> Set HP function-key mode.
+ * Ps = 1 0 5 3 -> Set SCO function-key mode.
+ * Ps = 1 0 6 0 -> Set legacy keyboard emulation (X11R6).
+ * Ps = 1 0 6 1 -> Set VT220 keyboard emulation.
+ * Ps = 2 0 0 4 -> Set bracketed paste mode.
+ * Modes:
+ * http: *vt100.net/docs/vt220-rm/chapter4.html
+ */Terminal.prototype.setMode=function(params){if((typeof params==='undefined'?'undefined':_typeof(params))==='object'){var l=params.length,i=0;for(;i<l;i++){this.setMode(params[i]);}return;}if(!this.prefix){switch(params){case 4:this.insertMode=true;break;case 20://this.convertEol = true;
+break;}}else if(this.prefix==='?'){switch(params){case 1:this.applicationCursor=true;break;case 2:this.setgCharset(0,Terminal.charsets.US);this.setgCharset(1,Terminal.charsets.US);this.setgCharset(2,Terminal.charsets.US);this.setgCharset(3,Terminal.charsets.US);// set VT100 mode here
+break;case 3:// 132 col mode
+this.savedCols=this.cols;this.resize(132,this.rows);break;case 6:this.originMode=true;break;case 7:this.wraparoundMode=true;break;case 12:// this.cursorBlink = true;
+break;case 66:this.log('Serial port requested application keypad.');this.applicationKeypad=true;this.viewport.syncScrollArea();break;case 9:// X10 Mouse
+// no release, no motion, no wheel, no modifiers.
+case 1000:// vt200 mouse
+// no motion.
+// no modifiers, except control on the wheel.
+case 1002:// button event mouse
+case 1003:// any event mouse
+// any event - sends motion events,
+// even if there is no button held down.
+this.x10Mouse=params===9;this.vt200Mouse=params===1000;this.normalMouse=params>1000;this.mouseEvents=true;this.element.style.cursor='default';this.log('Binding to mouse events.');break;case 1004:// send focusin/focusout events
+// focusin: ^[[I
+// focusout: ^[[O
+this.sendFocus=true;break;case 1005:// utf8 ext mode mouse
+this.utfMouse=true;// for wide terminals
+// simply encodes large values as utf8 characters
+break;case 1006:// sgr ext mode mouse
+this.sgrMouse=true;// for wide terminals
+// does not add 32 to fields
+// press: ^[[<b;x;yM
+// release: ^[[<b;x;ym
+break;case 1015:// urxvt ext mode mouse
+this.urxvtMouse=true;// for wide terminals
+// numbers for fields
+// press: ^[[b;x;yM
+// motion: ^[[b;x;yT
+break;case 25:// show cursor
+this.cursorHidden=false;break;case 1049:// alt screen buffer cursor
+//this.saveCursor();
+;// FALL-THROUGH
+case 47:// alt screen buffer
+case 1047:// alt screen buffer
+if(!this.normal){var normal={lines:this.lines,ybase:this.ybase,ydisp:this.ydisp,x:this.x,y:this.y,scrollTop:this.scrollTop,scrollBottom:this.scrollBottom,tabs:this.tabs// XXX save charset(s) here?
+// charset: this.charset,
+// glevel: this.glevel,
+// charsets: this.charsets
+};this.reset();this.normal=normal;this.showCursor();}break;}}};/**
+ * CSI Pm l Reset Mode (RM).
+ * Ps = 2 -> Keyboard Action Mode (AM).
+ * Ps = 4 -> Replace Mode (IRM).
+ * Ps = 1 2 -> Send/receive (SRM).
+ * Ps = 2 0 -> Normal Linefeed (LNM).
+ * CSI ? Pm l
+ * DEC Private Mode Reset (DECRST).
+ * Ps = 1 -> Normal Cursor Keys (DECCKM).
+ * Ps = 2 -> Designate VT52 mode (DECANM).
+ * Ps = 3 -> 80 Column Mode (DECCOLM).
+ * Ps = 4 -> Jump (Fast) Scroll (DECSCLM).
+ * Ps = 5 -> Normal Video (DECSCNM).
+ * Ps = 6 -> Normal Cursor Mode (DECOM).
+ * Ps = 7 -> No Wraparound Mode (DECAWM).
+ * Ps = 8 -> No Auto-repeat Keys (DECARM).
+ * Ps = 9 -> Don't send Mouse X & Y on button press.
+ * Ps = 1 0 -> Hide toolbar (rxvt).
+ * Ps = 1 2 -> Stop Blinking Cursor (att610).
+ * Ps = 1 8 -> Don't print form feed (DECPFF).
+ * Ps = 1 9 -> Limit print to scrolling region (DECPEX).
+ * Ps = 2 5 -> Hide Cursor (DECTCEM).
+ * Ps = 3 0 -> Don't show scrollbar (rxvt).
+ * Ps = 3 5 -> Disable font-shifting functions (rxvt).
+ * Ps = 4 0 -> Disallow 80 -> 132 Mode.
+ * Ps = 4 1 -> No more(1) fix (see curses resource).
+ * Ps = 4 2 -> Disable Nation Replacement Character sets (DEC-
+ * NRCM).
+ * Ps = 4 4 -> Turn Off Margin Bell.
+ * Ps = 4 5 -> No Reverse-wraparound Mode.
+ * Ps = 4 6 -> Stop Logging. (This is normally disabled by a
+ * compile-time option).
+ * Ps = 4 7 -> Use Normal Screen Buffer.
+ * Ps = 6 6 -> Numeric keypad (DECNKM).
+ * Ps = 6 7 -> Backarrow key sends delete (DECBKM).
+ * Ps = 1 0 0 0 -> Don't send Mouse X & Y on button press and
+ * release. See the section Mouse Tracking.
+ * Ps = 1 0 0 1 -> Don't use Hilite Mouse Tracking.
+ * Ps = 1 0 0 2 -> Don't use Cell Motion Mouse Tracking.
+ * Ps = 1 0 0 3 -> Don't use All Motion Mouse Tracking.
+ * Ps = 1 0 0 4 -> Don't send FocusIn/FocusOut events.
+ * Ps = 1 0 0 5 -> Disable Extended Mouse Mode.
+ * Ps = 1 0 1 0 -> Don't scroll to bottom on tty output
+ * (rxvt).
+ * Ps = 1 0 1 1 -> Don't scroll to bottom on key press (rxvt).
+ * Ps = 1 0 3 4 -> Don't interpret "meta" key. (This disables
+ * the eightBitInput resource).
+ * Ps = 1 0 3 5 -> Disable special modifiers for Alt and Num-
+ * Lock keys. (This disables the numLock resource).
+ * Ps = 1 0 3 6 -> Don't send ESC when Meta modifies a key.
+ * (This disables the metaSendsEscape resource).
+ * Ps = 1 0 3 7 -> Send VT220 Remove from the editing-keypad
+ * Delete key.
+ * Ps = 1 0 3 9 -> Don't send ESC when Alt modifies a key.
+ * (This disables the altSendsEscape resource).
+ * Ps = 1 0 4 0 -> Do not keep selection when not highlighted.
+ * (This disables the keepSelection resource).
+ * Ps = 1 0 4 1 -> Use the PRIMARY selection. (This disables
+ * the selectToClipboard resource).
+ * Ps = 1 0 4 2 -> Disable Urgency window manager hint when
+ * Control-G is received. (This disables the bellIsUrgent
+ * resource).
+ * Ps = 1 0 4 3 -> Disable raising of the window when Control-
+ * G is received. (This disables the popOnBell resource).
+ * Ps = 1 0 4 7 -> Use Normal Screen Buffer, clearing screen
+ * first if in the Alternate Screen. (This may be disabled by
+ * the titeInhibit resource).
+ * Ps = 1 0 4 8 -> Restore cursor as in DECRC. (This may be
+ * disabled by the titeInhibit resource).
+ * Ps = 1 0 4 9 -> Use Normal Screen Buffer and restore cursor
+ * as in DECRC. (This may be disabled by the titeInhibit
+ * resource). This combines the effects of the 1 0 4 7 and 1 0
+ * 4 8 modes. Use this with terminfo-based applications rather
+ * than the 4 7 mode.
+ * Ps = 1 0 5 0 -> Reset terminfo/termcap function-key mode.
+ * Ps = 1 0 5 1 -> Reset Sun function-key mode.
+ * Ps = 1 0 5 2 -> Reset HP function-key mode.
+ * Ps = 1 0 5 3 -> Reset SCO function-key mode.
+ * Ps = 1 0 6 0 -> Reset legacy keyboard emulation (X11R6).
+ * Ps = 1 0 6 1 -> Reset keyboard emulation to Sun/PC style.
+ * Ps = 2 0 0 4 -> Reset bracketed paste mode.
+ */Terminal.prototype.resetMode=function(params){if((typeof params==='undefined'?'undefined':_typeof(params))==='object'){var l=params.length,i=0;for(;i<l;i++){this.resetMode(params[i]);}return;}if(!this.prefix){switch(params){case 4:this.insertMode=false;break;case 20://this.convertEol = false;
+break;}}else if(this.prefix==='?'){switch(params){case 1:this.applicationCursor=false;break;case 3:if(this.cols===132&&this.savedCols){this.resize(this.savedCols,this.rows);}delete this.savedCols;break;case 6:this.originMode=false;break;case 7:this.wraparoundMode=false;break;case 12:// this.cursorBlink = false;
+break;case 66:this.log('Switching back to normal keypad.');this.applicationKeypad=false;this.viewport.syncScrollArea();break;case 9:// X10 Mouse
+case 1000:// vt200 mouse
+case 1002:// button event mouse
+case 1003:// any event mouse
+this.x10Mouse=false;this.vt200Mouse=false;this.normalMouse=false;this.mouseEvents=false;this.element.style.cursor='';break;case 1004:// send focusin/focusout events
+this.sendFocus=false;break;case 1005:// utf8 ext mode mouse
+this.utfMouse=false;break;case 1006:// sgr ext mode mouse
+this.sgrMouse=false;break;case 1015:// urxvt ext mode mouse
+this.urxvtMouse=false;break;case 25:// hide cursor
+this.cursorHidden=true;break;case 1049:// alt screen buffer cursor
+;// FALL-THROUGH
+case 47:// normal screen buffer
+case 1047:// normal screen buffer - clearing it first
+if(this.normal){this.lines=this.normal.lines;this.ybase=this.normal.ybase;this.ydisp=this.normal.ydisp;this.x=this.normal.x;this.y=this.normal.y;this.scrollTop=this.normal.scrollTop;this.scrollBottom=this.normal.scrollBottom;this.tabs=this.normal.tabs;this.normal=null;// if (params === 1049) {
+// this.x = this.savedX;
+// this.y = this.savedY;
+// }
+this.refresh(0,this.rows-1);this.showCursor();}break;}}};/**
+ * CSI Ps ; Ps r
+ * Set Scrolling Region [top;bottom] (default = full size of win-
+ * dow) (DECSTBM).
+ * CSI ? Pm r
+ */Terminal.prototype.setScrollRegion=function(params){if(this.prefix)return;this.scrollTop=(params[0]||1)-1;this.scrollBottom=(params[1]||this.rows)-1;this.x=0;this.y=0;};/**
+ * CSI s
+ * Save cursor (ANSI.SYS).
+ */Terminal.prototype.saveCursor=function(params){this.savedX=this.x;this.savedY=this.y;};/**
+ * CSI u
+ * Restore cursor (ANSI.SYS).
+ */Terminal.prototype.restoreCursor=function(params){this.x=this.savedX||0;this.y=this.savedY||0;};/**
+ * Lesser Used
+ *//**
+ * CSI Ps I
+ * Cursor Forward Tabulation Ps tab stops (default = 1) (CHT).
+ */Terminal.prototype.cursorForwardTab=function(params){var param=params[0]||1;while(param--){this.x=this.nextStop();}};/**
+ * CSI Ps S Scroll up Ps lines (default = 1) (SU).
+ */Terminal.prototype.scrollUp=function(params){var param=params[0]||1;while(param--){this.lines.splice(this.ybase+this.scrollTop,1);this.lines.splice(this.ybase+this.scrollBottom,0,this.blankLine());}// this.maxRange();
+this.updateRange(this.scrollTop);this.updateRange(this.scrollBottom);};/**
+ * CSI Ps T Scroll down Ps lines (default = 1) (SD).
+ */Terminal.prototype.scrollDown=function(params){var param=params[0]||1;while(param--){this.lines.splice(this.ybase+this.scrollBottom,1);this.lines.splice(this.ybase+this.scrollTop,0,this.blankLine());}// this.maxRange();
+this.updateRange(this.scrollTop);this.updateRange(this.scrollBottom);};/**
+ * CSI Ps ; Ps ; Ps ; Ps ; Ps T
+ * Initiate highlight mouse tracking. Parameters are
+ * [func;startx;starty;firstrow;lastrow]. See the section Mouse
+ * Tracking.
+ */Terminal.prototype.initMouseTracking=function(params){// Relevant: DECSET 1001
+};/**
+ * CSI > Ps; Ps T
+ * Reset one or more features of the title modes to the default
+ * value. Normally, "reset" disables the feature. It is possi-
+ * ble to disable the ability to reset features by compiling a
+ * different default for the title modes into xterm.
+ * Ps = 0 -> Do not set window/icon labels using hexadecimal.
+ * Ps = 1 -> Do not query window/icon labels using hexadeci-
+ * mal.
+ * Ps = 2 -> Do not set window/icon labels using UTF-8.
+ * Ps = 3 -> Do not query window/icon labels using UTF-8.
+ * (See discussion of "Title Modes").
+ */Terminal.prototype.resetTitleModes=function(params){;};/**
+ * CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1) (CBT).
+ */Terminal.prototype.cursorBackwardTab=function(params){var param=params[0]||1;while(param--){this.x=this.prevStop();}};/**
+ * CSI Ps b Repeat the preceding graphic character Ps times (REP).
+ */Terminal.prototype.repeatPrecedingCharacter=function(params){var param=params[0]||1,line=this.lines[this.ybase+this.y],ch=line[this.x-1]||[this.defAttr,' ',1];while(param--){line[this.x++]=ch;}};/**
+ * CSI Ps g Tab Clear (TBC).
+ * Ps = 0 -> Clear Current Column (default).
+ * Ps = 3 -> Clear All.
+ * Potentially:
+ * Ps = 2 -> Clear Stops on Line.
+ * http://vt100.net/annarbor/aaa-ug/section6.html
+ */Terminal.prototype.tabClear=function(params){var param=params[0];if(param<=0){delete this.tabs[this.x];}else if(param===3){this.tabs={};}};/**
+ * CSI Pm i Media Copy (MC).
+ * Ps = 0 -> Print screen (default).
+ * Ps = 4 -> Turn off printer controller mode.
+ * Ps = 5 -> Turn on printer controller mode.
+ * CSI ? Pm i
+ * Media Copy (MC, DEC-specific).
+ * Ps = 1 -> Print line containing cursor.
+ * Ps = 4 -> Turn off autoprint mode.
+ * Ps = 5 -> Turn on autoprint mode.
+ * Ps = 1 0 -> Print composed display, ignores DECPEX.
+ * Ps = 1 1 -> Print all pages.
+ */Terminal.prototype.mediaCopy=function(params){;};/**
+ * CSI > Ps; Ps m
+ * Set or reset resource-values used by xterm to decide whether
+ * to construct escape sequences holding information about the
+ * modifiers pressed with a given key. The first parameter iden-
+ * tifies the resource to set/reset. The second parameter is the
+ * value to assign to the resource. If the second parameter is
+ * omitted, the resource is reset to its initial value.
+ * Ps = 1 -> modifyCursorKeys.
+ * Ps = 2 -> modifyFunctionKeys.
+ * Ps = 4 -> modifyOtherKeys.
+ * If no parameters are given, all resources are reset to their
+ * initial values.
+ */Terminal.prototype.setResources=function(params){;};/**
+ * CSI > Ps n
+ * Disable modifiers which may be enabled via the CSI > Ps; Ps m
+ * sequence. This corresponds to a resource value of "-1", which
+ * cannot be set with the other sequence. The parameter identi-
+ * fies the resource to be disabled:
+ * Ps = 1 -> modifyCursorKeys.
+ * Ps = 2 -> modifyFunctionKeys.
+ * Ps = 4 -> modifyOtherKeys.
+ * If the parameter is omitted, modifyFunctionKeys is disabled.
+ * When modifyFunctionKeys is disabled, xterm uses the modifier
+ * keys to make an extended sequence of functions rather than
+ * adding a parameter to each function key to denote the modi-
+ * fiers.
+ */Terminal.prototype.disableModifiers=function(params){;};/**
+ * CSI > Ps p
+ * Set resource value pointerMode. This is used by xterm to
+ * decide whether to hide the pointer cursor as the user types.
+ * Valid values for the parameter:
+ * Ps = 0 -> never hide the pointer.
+ * Ps = 1 -> hide if the mouse tracking mode is not enabled.
+ * Ps = 2 -> always hide the pointer. If no parameter is
+ * given, xterm uses the default, which is 1 .
+ */Terminal.prototype.setPointerMode=function(params){;};/**
+ * CSI ! p Soft terminal reset (DECSTR).
+ * http://vt100.net/docs/vt220-rm/table4-10.html
+ */Terminal.prototype.softReset=function(params){this.cursorHidden=false;this.insertMode=false;this.originMode=false;this.wraparoundMode=false;// autowrap
+this.applicationKeypad=false;// ?
+this.viewport.syncScrollArea();this.applicationCursor=false;this.scrollTop=0;this.scrollBottom=this.rows-1;this.curAttr=this.defAttr;this.x=this.y=0;// ?
+this.charset=null;this.glevel=0;// ??
+this.charsets=[null];// ??
+};/**
+ * CSI Ps$ p
+ * Request ANSI mode (DECRQM). For VT300 and up, reply is
+ * CSI Ps; Pm$ y
+ * where Ps is the mode number as in RM, and Pm is the mode
+ * value:
+ * 0 - not recognized
+ * 1 - set
+ * 2 - reset
+ * 3 - permanently set
+ * 4 - permanently reset
+ */Terminal.prototype.requestAnsiMode=function(params){;};/**
+ * CSI ? Ps$ p
+ * Request DEC private mode (DECRQM). For VT300 and up, reply is
+ * CSI ? Ps; Pm$ p
+ * where Ps is the mode number as in DECSET, Pm is the mode value
+ * as in the ANSI DECRQM.
+ */Terminal.prototype.requestPrivateMode=function(params){;};/**
+ * CSI Ps ; Ps " p
+ * Set conformance level (DECSCL). Valid values for the first
+ * parameter:
+ * Ps = 6 1 -> VT100.
+ * Ps = 6 2 -> VT200.
+ * Ps = 6 3 -> VT300.
+ * Valid values for the second parameter:
+ * Ps = 0 -> 8-bit controls.
+ * Ps = 1 -> 7-bit controls (always set for VT100).
+ * Ps = 2 -> 8-bit controls.
+ */Terminal.prototype.setConformanceLevel=function(params){;};/**
+ * CSI Ps q Load LEDs (DECLL).
+ * Ps = 0 -> Clear all LEDS (default).
+ * Ps = 1 -> Light Num Lock.
+ * Ps = 2 -> Light Caps Lock.
+ * Ps = 3 -> Light Scroll Lock.
+ * Ps = 2 1 -> Extinguish Num Lock.
+ * Ps = 2 2 -> Extinguish Caps Lock.
+ * Ps = 2 3 -> Extinguish Scroll Lock.
+ */Terminal.prototype.loadLEDs=function(params){;};/**
+ * CSI Ps SP q
+ * Set cursor style (DECSCUSR, VT520).
+ * Ps = 0 -> blinking block.
+ * Ps = 1 -> blinking block (default).
+ * Ps = 2 -> steady block.
+ * Ps = 3 -> blinking underline.
+ * Ps = 4 -> steady underline.
+ */Terminal.prototype.setCursorStyle=function(params){;};/**
+ * CSI Ps " q
+ * Select character protection attribute (DECSCA). Valid values
+ * for the parameter:
+ * Ps = 0 -> DECSED and DECSEL can erase (default).
+ * Ps = 1 -> DECSED and DECSEL cannot erase.
+ * Ps = 2 -> DECSED and DECSEL can erase.
+ */Terminal.prototype.setCharProtectionAttr=function(params){;};/**
+ * CSI ? Pm r
+ * Restore DEC Private Mode Values. The value of Ps previously
+ * saved is restored. Ps values are the same as for DECSET.
+ */Terminal.prototype.restorePrivateValues=function(params){;};/**
+ * CSI Pt; Pl; Pb; Pr; Ps$ r
+ * Change Attributes in Rectangular Area (DECCARA), VT400 and up.
+ * Pt; Pl; Pb; Pr denotes the rectangle.
+ * Ps denotes the SGR attributes to change: 0, 1, 4, 5, 7.
+ * NOTE: xterm doesn't enable this code by default.
+ */Terminal.prototype.setAttrInRectangle=function(params){var t=params[0],l=params[1],b=params[2],r=params[3],attr=params[4];var line,i;for(;t<b+1;t++){line=this.lines[this.ybase+t];for(i=l;i<r;i++){line[i]=[attr,line[i][1]];}}// this.maxRange();
+this.updateRange(params[0]);this.updateRange(params[2]);};/**
+ * CSI Pc; Pt; Pl; Pb; Pr$ x
+ * Fill Rectangular Area (DECFRA), VT420 and up.
+ * Pc is the character to use.
+ * Pt; Pl; Pb; Pr denotes the rectangle.
+ * NOTE: xterm doesn't enable this code by default.
+ */Terminal.prototype.fillRectangle=function(params){var ch=params[0],t=params[1],l=params[2],b=params[3],r=params[4];var line,i;for(;t<b+1;t++){line=this.lines[this.ybase+t];for(i=l;i<r;i++){line[i]=[line[i][0],String.fromCharCode(ch)];}}// this.maxRange();
+this.updateRange(params[1]);this.updateRange(params[3]);};/**
+ * CSI Ps ; Pu ' z
+ * Enable Locator Reporting (DECELR).
+ * Valid values for the first parameter:
+ * Ps = 0 -> Locator disabled (default).
+ * Ps = 1 -> Locator enabled.
+ * Ps = 2 -> Locator enabled for one report, then disabled.
+ * The second parameter specifies the coordinate unit for locator
+ * reports.
+ * Valid values for the second parameter:
+ * Pu = 0 <- or omitted -> default to character cells.
+ * Pu = 1 <- device physical pixels.
+ * Pu = 2 <- character cells.
+ */Terminal.prototype.enableLocatorReporting=function(params){var val=params[0]>0;//this.mouseEvents = val;
+//this.decLocator = val;
+};/**
+ * CSI Pt; Pl; Pb; Pr$ z
+ * Erase Rectangular Area (DECERA), VT400 and up.
+ * Pt; Pl; Pb; Pr denotes the rectangle.
+ * NOTE: xterm doesn't enable this code by default.
+ */Terminal.prototype.eraseRectangle=function(params){var t=params[0],l=params[1],b=params[2],r=params[3];var line,i,ch;ch=[this.eraseAttr(),' ',1];// xterm?
+for(;t<b+1;t++){line=this.lines[this.ybase+t];for(i=l;i<r;i++){line[i]=ch;}}// this.maxRange();
+this.updateRange(params[0]);this.updateRange(params[2]);};/**
+ * CSI P m SP }
+ * Insert P s Column(s) (default = 1) (DECIC), VT420 and up.
+ * NOTE: xterm doesn't enable this code by default.
+ */Terminal.prototype.insertColumns=function(){var param=params[0],l=this.ybase+this.rows,ch=[this.eraseAttr(),' ',1]// xterm?
+,i;while(param--){for(i=this.ybase;i<l;i++){this.lines[i].splice(this.x+1,0,ch);this.lines[i].pop();}}this.maxRange();};/**
+ * CSI P m SP ~
+ * Delete P s Column(s) (default = 1) (DECDC), VT420 and up
+ * NOTE: xterm doesn't enable this code by default.
+ */Terminal.prototype.deleteColumns=function(){var param=params[0],l=this.ybase+this.rows,ch=[this.eraseAttr(),' ',1]// xterm?
+,i;while(param--){for(i=this.ybase;i<l;i++){this.lines[i].splice(this.x,1);this.lines[i].push(ch);}}this.maxRange();};/**
+ * Character Sets
+ */Terminal.charsets={};// DEC Special Character and Line Drawing Set.
+// http://vt100.net/docs/vt102-ug/table5-13.html
+// A lot of curses apps use this if they see TERM=xterm.
+// testing: echo -e '\e(0a\e(B'
+// The xterm output sometimes seems to conflict with the
+// reference above. xterm seems in line with the reference
+// when running vttest however.
+// The table below now uses xterm's output from vttest.
+Terminal.charsets.SCLD={// (0
+'`':'\u25C6',// '◆'
+'a':'\u2592',// '▒'
+'b':'\t',// '\t'
+'c':'\f',// '\f'
+'d':'\r',// '\r'
+'e':'\n',// '\n'
+'f':'\xB0',// '°'
+'g':'\xB1',// '±'
+'h':'\u2424',// '\u2424' (NL)
+'i':'\x0B',// '\v'
+'j':'\u2518',// '┘'
+'k':'\u2510',// '┐'
+'l':'\u250C',// '┌'
+'m':'\u2514',// '└'
+'n':'\u253C',// '┼'
+'o':'\u23BA',// '⎺'
+'p':'\u23BB',// '⎻'
+'q':'\u2500',// '─'
+'r':'\u23BC',// '⎼'
+'s':'\u23BD',// '⎽'
+'t':'\u251C',// '├'
+'u':'\u2524',// '┤'
+'v':'\u2534',// '┴'
+'w':'\u252C',// '┬'
+'x':'\u2502',// '│'
+'y':'\u2264',// '≤'
+'z':'\u2265',// '≥'
+'{':'\u03C0',// 'π'
+'|':'\u2260',// '≠'
+'}':'\xA3',// '£'
+'~':'\xB7'// '·'
+};Terminal.charsets.UK=null;// (A
+Terminal.charsets.US=null;// (B (USASCII)
+Terminal.charsets.Dutch=null;// (4
+Terminal.charsets.Finnish=null;// (C or (5
+Terminal.charsets.French=null;// (R
+Terminal.charsets.FrenchCanadian=null;// (Q
+Terminal.charsets.German=null;// (K
+Terminal.charsets.Italian=null;// (Y
+Terminal.charsets.NorwegianDanish=null;// (E or (6
+Terminal.charsets.Spanish=null;// (Z
+Terminal.charsets.Swedish=null;// (H or (7
+Terminal.charsets.Swiss=null;// (=
+Terminal.charsets.ISOLatin=null;// /A
+/**
+ * Helpers
+ */function on(el,type,handler,capture){if(!Array.isArray(el)){el=[el];}el.forEach(function(element){element.addEventListener(type,handler,capture||false);});}function off(el,type,handler,capture){el.removeEventListener(type,handler,capture||false);}function cancel(ev,force){if(!this.cancelEvents&&!force){return;}ev.preventDefault();ev.stopPropagation();return false;}function inherits(child,parent){function f(){this.constructor=child;}f.prototype=parent.prototype;child.prototype=new f();}// if bold is broken, we can't
+// use it in the terminal.
+function isBoldBroken(document){var body=document.getElementsByTagName('body')[0];var el=document.createElement('span');el.innerHTML='hello world';body.appendChild(el);var w1=el.scrollWidth;el.style.fontWeight='bold';var w2=el.scrollWidth;body.removeChild(el);return w1!==w2;}function indexOf(obj,el){var i=obj.length;while(i--){if(obj[i]===el)return i;}return-1;}function isThirdLevelShift(term,ev){var thirdLevelKey=term.browser.isMac&&ev.altKey&&!ev.ctrlKey&&!ev.metaKey||term.browser.isMSWindows&&ev.altKey&&ev.ctrlKey&&!ev.metaKey;if(ev.type=='keypress'){return thirdLevelKey;}// Don't invoke for arrows, pageDown, home, backspace, etc. (on non-keypress events)
+return thirdLevelKey&&(!ev.keyCode||ev.keyCode>47);}function matchColor(r1,g1,b1){var hash=r1<<16|g1<<8|b1;if(matchColor._cache[hash]!=null){return matchColor._cache[hash];}var ldiff=Infinity,li=-1,i=0,c,r2,g2,b2,diff;for(;i<Terminal.vcolors.length;i++){c=Terminal.vcolors[i];r2=c[0];g2=c[1];b2=c[2];diff=matchColor.distance(r1,g1,b1,r2,g2,b2);if(diff===0){li=i;break;}if(diff<ldiff){ldiff=diff;li=i;}}return matchColor._cache[hash]=li;}matchColor._cache={};// http://stackoverflow.com/questions/1633828
+matchColor.distance=function(r1,g1,b1,r2,g2,b2){return Math.pow(30*(r1-r2),2)+Math.pow(59*(g1-g2),2)+Math.pow(11*(b1-b2),2);};function each(obj,iter,con){if(obj.forEach)return obj.forEach(iter,con);for(var i=0;i<obj.length;i++){iter.call(con,obj[i],i,obj);}}function keys(obj){if(Object.keys)return Object.keys(obj);var key,keys=[];for(key in obj){if(Object.prototype.hasOwnProperty.call(obj,key)){keys.push(key);}}return keys;}var wcwidth=function(opts){// extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c
+// combining characters
+var COMBINING=[[0x0300,0x036F],[0x0483,0x0486],[0x0488,0x0489],[0x0591,0x05BD],[0x05BF,0x05BF],[0x05C1,0x05C2],[0x05C4,0x05C5],[0x05C7,0x05C7],[0x0600,0x0603],[0x0610,0x0615],[0x064B,0x065E],[0x0670,0x0670],[0x06D6,0x06E4],[0x06E7,0x06E8],[0x06EA,0x06ED],[0x070F,0x070F],[0x0711,0x0711],[0x0730,0x074A],[0x07A6,0x07B0],[0x07EB,0x07F3],[0x0901,0x0902],[0x093C,0x093C],[0x0941,0x0948],[0x094D,0x094D],[0x0951,0x0954],[0x0962,0x0963],[0x0981,0x0981],[0x09BC,0x09BC],[0x09C1,0x09C4],[0x09CD,0x09CD],[0x09E2,0x09E3],[0x0A01,0x0A02],[0x0A3C,0x0A3C],[0x0A41,0x0A42],[0x0A47,0x0A48],[0x0A4B,0x0A4D],[0x0A70,0x0A71],[0x0A81,0x0A82],[0x0ABC,0x0ABC],[0x0AC1,0x0AC5],[0x0AC7,0x0AC8],[0x0ACD,0x0ACD],[0x0AE2,0x0AE3],[0x0B01,0x0B01],[0x0B3C,0x0B3C],[0x0B3F,0x0B3F],[0x0B41,0x0B43],[0x0B4D,0x0B4D],[0x0B56,0x0B56],[0x0B82,0x0B82],[0x0BC0,0x0BC0],[0x0BCD,0x0BCD],[0x0C3E,0x0C40],[0x0C46,0x0C48],[0x0C4A,0x0C4D],[0x0C55,0x0C56],[0x0CBC,0x0CBC],[0x0CBF,0x0CBF],[0x0CC6,0x0CC6],[0x0CCC,0x0CCD],[0x0CE2,0x0CE3],[0x0D41,0x0D43],[0x0D4D,0x0D4D],[0x0DCA,0x0DCA],[0x0DD2,0x0DD4],[0x0DD6,0x0DD6],[0x0E31,0x0E31],[0x0E34,0x0E3A],[0x0E47,0x0E4E],[0x0EB1,0x0EB1],[0x0EB4,0x0EB9],[0x0EBB,0x0EBC],[0x0EC8,0x0ECD],[0x0F18,0x0F19],[0x0F35,0x0F35],[0x0F37,0x0F37],[0x0F39,0x0F39],[0x0F71,0x0F7E],[0x0F80,0x0F84],[0x0F86,0x0F87],[0x0F90,0x0F97],[0x0F99,0x0FBC],[0x0FC6,0x0FC6],[0x102D,0x1030],[0x1032,0x1032],[0x1036,0x1037],[0x1039,0x1039],[0x1058,0x1059],[0x1160,0x11FF],[0x135F,0x135F],[0x1712,0x1714],[0x1732,0x1734],[0x1752,0x1753],[0x1772,0x1773],[0x17B4,0x17B5],[0x17B7,0x17BD],[0x17C6,0x17C6],[0x17C9,0x17D3],[0x17DD,0x17DD],[0x180B,0x180D],[0x18A9,0x18A9],[0x1920,0x1922],[0x1927,0x1928],[0x1932,0x1932],[0x1939,0x193B],[0x1A17,0x1A18],[0x1B00,0x1B03],[0x1B34,0x1B34],[0x1B36,0x1B3A],[0x1B3C,0x1B3C],[0x1B42,0x1B42],[0x1B6B,0x1B73],[0x1DC0,0x1DCA],[0x1DFE,0x1DFF],[0x200B,0x200F],[0x202A,0x202E],[0x2060,0x2063],[0x206A,0x206F],[0x20D0,0x20EF],[0x302A,0x302F],[0x3099,0x309A],[0xA806,0xA806],[0xA80B,0xA80B],[0xA825,0xA826],[0xFB1E,0xFB1E],[0xFE00,0xFE0F],[0xFE20,0xFE23],[0xFEFF,0xFEFF],[0xFFF9,0xFFFB],[0x10A01,0x10A03],[0x10A05,0x10A06],[0x10A0C,0x10A0F],[0x10A38,0x10A3A],[0x10A3F,0x10A3F],[0x1D167,0x1D169],[0x1D173,0x1D182],[0x1D185,0x1D18B],[0x1D1AA,0x1D1AD],[0x1D242,0x1D244],[0xE0001,0xE0001],[0xE0020,0xE007F],[0xE0100,0xE01EF]];// binary search
+function bisearch(ucs){var min=0;var max=COMBINING.length-1;var mid;if(ucs<COMBINING[0][0]||ucs>COMBINING[max][1])return false;while(max>=min){mid=Math.floor((min+max)/2);if(ucs>COMBINING[mid][1])min=mid+1;else if(ucs<COMBINING[mid][0])max=mid-1;else return true;}return false;}function wcwidth(ucs){// test for 8-bit control characters
+if(ucs===0)return opts.nul;if(ucs<32||ucs>=0x7f&&ucs<0xa0)return opts.control;// binary search in table of non-spacing characters
+if(bisearch(ucs))return 0;// if we arrive here, ucs is not a combining or C0/C1 control character
+return 1+(ucs>=0x1100&&(ucs<=0x115f||// Hangul Jamo init. consonants
+ucs==0x2329||ucs==0x232a||ucs>=0x2e80&&ucs<=0xa4cf&&ucs!=0x303f||// CJK..Yi
+ucs>=0xac00&&ucs<=0xd7a3||// Hangul Syllables
+ucs>=0xf900&&ucs<=0xfaff||// CJK Compat Ideographs
+ucs>=0xfe10&&ucs<=0xfe19||// Vertical forms
+ucs>=0xfe30&&ucs<=0xfe6f||// CJK Compat Forms
+ucs>=0xff00&&ucs<=0xff60||// Fullwidth Forms
+ucs>=0xffe0&&ucs<=0xffe6||ucs>=0x20000&&ucs<=0x2fffd||ucs>=0x30000&&ucs<=0x3fffd));}return wcwidth;}({nul:0,control:0});// configurable options
+/**
+ * Expose
+ */Terminal.EventEmitter=_EventEmitter.EventEmitter;Terminal.CompositionHelper=_CompositionHelper.CompositionHelper;Terminal.Viewport=_Viewport.Viewport;Terminal.inherits=inherits;/**
+ * Adds an event listener to the terminal.
+ *
+ * @param {string} event The name of the event. TODO: Document all event types
+ * @param {function} callback The function to call when the event is triggered.
+ */Terminal.on=on;Terminal.off=off;Terminal.cancel=cancel;module.exports=Terminal;
+
+},{"./CompositionHelper.js":1,"./EventEmitter.js":2,"./Viewport.js":3,"./handlers/Clipboard.js":4,"./utils/Browser":5}]},{},[7])(7)
+});
+//# sourceMappingURL=xterm.js.map
diff --git a/vendor/assets/stylesheets/katex.css b/vendor/assets/stylesheets/katex.scss
index 3e62df2329c..9dd8a30bf51 100644
--- a/vendor/assets/stylesheets/katex.css
+++ b/vendor/assets/stylesheets/katex.scss
@@ -35,119 +35,121 @@ SOFTWARE.
1. Clone KaTeX. Anything later than 4fb9445a9 (is merged into master) will do.
2. make (requires node)
- 3. sed -i 's,fonts/,,' build/katex.css
- 4. Copy build/katex.js, build/katex.css and fonts/* to gitlab.
+ 3. sed -e 's,fonts/,,' -e 's/url\(([^)]*)\)/url(font-path\1)/g' build/katex.css > build/katex.scss
+ 4. Copy build/katex.js to gitlab/vendor/assets/javascripts/katex.js,
+ build/katex.scss to gitlab/vendor/assets/stylesheets/katex.scss and
+ fonts/* to gitlab/vendor/assets/fonts/.
*/
@font-face {
font-family: 'KaTeX_AMS';
- src: url('KaTeX_AMS-Regular.eot');
- src: url('KaTeX_AMS-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_AMS-Regular.woff2') format('woff2'), url('KaTeX_AMS-Regular.woff') format('woff'), url('KaTeX_AMS-Regular.ttf') format('truetype');
+ src: url(font-path('KaTeX_AMS-Regular.eot'));
+ src: url(font-path('KaTeX_AMS-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_AMS-Regular.woff2')) format('woff2'), url(font-path('KaTeX_AMS-Regular.woff')) format('woff'), url(font-path('KaTeX_AMS-Regular.ttf')) format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Caligraphic';
- src: url('KaTeX_Caligraphic-Bold.eot');
- src: url('KaTeX_Caligraphic-Bold.eot#iefix') format('embedded-opentype'), url('KaTeX_Caligraphic-Bold.woff2') format('woff2'), url('KaTeX_Caligraphic-Bold.woff') format('woff'), url('KaTeX_Caligraphic-Bold.ttf') format('truetype');
+ src: url(font-path('KaTeX_Caligraphic-Bold.eot'));
+ src: url(font-path('KaTeX_Caligraphic-Bold.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Caligraphic-Bold.woff2')) format('woff2'), url(font-path('KaTeX_Caligraphic-Bold.woff')) format('woff'), url(font-path('KaTeX_Caligraphic-Bold.ttf')) format('truetype');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Caligraphic';
- src: url('KaTeX_Caligraphic-Regular.eot');
- src: url('KaTeX_Caligraphic-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Caligraphic-Regular.woff2') format('woff2'), url('KaTeX_Caligraphic-Regular.woff') format('woff'), url('KaTeX_Caligraphic-Regular.ttf') format('truetype');
+ src: url(font-path('KaTeX_Caligraphic-Regular.eot'));
+ src: url(font-path('KaTeX_Caligraphic-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Caligraphic-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Caligraphic-Regular.woff')) format('woff'), url(font-path('KaTeX_Caligraphic-Regular.ttf')) format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Fraktur';
- src: url('KaTeX_Fraktur-Bold.eot');
- src: url('KaTeX_Fraktur-Bold.eot#iefix') format('embedded-opentype'), url('KaTeX_Fraktur-Bold.woff2') format('woff2'), url('KaTeX_Fraktur-Bold.woff') format('woff'), url('KaTeX_Fraktur-Bold.ttf') format('truetype');
+ src: url(font-path('KaTeX_Fraktur-Bold.eot'));
+ src: url(font-path('KaTeX_Fraktur-Bold.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Fraktur-Bold.woff2')) format('woff2'), url(font-path('KaTeX_Fraktur-Bold.woff')) format('woff'), url(font-path('KaTeX_Fraktur-Bold.ttf')) format('truetype');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Fraktur';
- src: url('KaTeX_Fraktur-Regular.eot');
- src: url('KaTeX_Fraktur-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Fraktur-Regular.woff2') format('woff2'), url('KaTeX_Fraktur-Regular.woff') format('woff'), url('KaTeX_Fraktur-Regular.ttf') format('truetype');
+ src: url(font-path('KaTeX_Fraktur-Regular.eot'));
+ src: url(font-path('KaTeX_Fraktur-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Fraktur-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Fraktur-Regular.woff')) format('woff'), url(font-path('KaTeX_Fraktur-Regular.ttf')) format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Main';
- src: url('KaTeX_Main-Bold.eot');
- src: url('KaTeX_Main-Bold.eot#iefix') format('embedded-opentype'), url('KaTeX_Main-Bold.woff2') format('woff2'), url('KaTeX_Main-Bold.woff') format('woff'), url('KaTeX_Main-Bold.ttf') format('truetype');
+ src: url(font-path('KaTeX_Main-Bold.eot'));
+ src: url(font-path('KaTeX_Main-Bold.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Main-Bold.woff2')) format('woff2'), url(font-path('KaTeX_Main-Bold.woff')) format('woff'), url(font-path('KaTeX_Main-Bold.ttf')) format('truetype');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Main';
- src: url('KaTeX_Main-Italic.eot');
- src: url('KaTeX_Main-Italic.eot#iefix') format('embedded-opentype'), url('KaTeX_Main-Italic.woff2') format('woff2'), url('KaTeX_Main-Italic.woff') format('woff'), url('KaTeX_Main-Italic.ttf') format('truetype');
+ src: url(font-path('KaTeX_Main-Italic.eot'));
+ src: url(font-path('KaTeX_Main-Italic.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Main-Italic.woff2')) format('woff2'), url(font-path('KaTeX_Main-Italic.woff')) format('woff'), url(font-path('KaTeX_Main-Italic.ttf')) format('truetype');
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: 'KaTeX_Main';
- src: url('KaTeX_Main-Regular.eot');
- src: url('KaTeX_Main-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Main-Regular.woff2') format('woff2'), url('KaTeX_Main-Regular.woff') format('woff'), url('KaTeX_Main-Regular.ttf') format('truetype');
+ src: url(font-path('KaTeX_Main-Regular.eot'));
+ src: url(font-path('KaTeX_Main-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Main-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Main-Regular.woff')) format('woff'), url(font-path('KaTeX_Main-Regular.ttf')) format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Math';
- src: url('KaTeX_Math-Italic.eot');
- src: url('KaTeX_Math-Italic.eot#iefix') format('embedded-opentype'), url('KaTeX_Math-Italic.woff2') format('woff2'), url('KaTeX_Math-Italic.woff') format('woff'), url('KaTeX_Math-Italic.ttf') format('truetype');
+ src: url(font-path('KaTeX_Math-Italic.eot'));
+ src: url(font-path('KaTeX_Math-Italic.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Math-Italic.woff2')) format('woff2'), url(font-path('KaTeX_Math-Italic.woff')) format('woff'), url(font-path('KaTeX_Math-Italic.ttf')) format('truetype');
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: 'KaTeX_SansSerif';
- src: url('KaTeX_SansSerif-Regular.eot');
- src: url('KaTeX_SansSerif-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_SansSerif-Regular.woff2') format('woff2'), url('KaTeX_SansSerif-Regular.woff') format('woff'), url('KaTeX_SansSerif-Regular.ttf') format('truetype');
+ src: url(font-path('KaTeX_SansSerif-Regular.eot'));
+ src: url(font-path('KaTeX_SansSerif-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_SansSerif-Regular.woff2')) format('woff2'), url(font-path('KaTeX_SansSerif-Regular.woff')) format('woff'), url(font-path('KaTeX_SansSerif-Regular.ttf')) format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Script';
- src: url('KaTeX_Script-Regular.eot');
- src: url('KaTeX_Script-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Script-Regular.woff2') format('woff2'), url('KaTeX_Script-Regular.woff') format('woff'), url('KaTeX_Script-Regular.ttf') format('truetype');
+ src: url(font-path('KaTeX_Script-Regular.eot'));
+ src: url(font-path('KaTeX_Script-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Script-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Script-Regular.woff')) format('woff'), url(font-path('KaTeX_Script-Regular.ttf')) format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Size1';
- src: url('KaTeX_Size1-Regular.eot');
- src: url('KaTeX_Size1-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size1-Regular.woff2') format('woff2'), url('KaTeX_Size1-Regular.woff') format('woff'), url('KaTeX_Size1-Regular.ttf') format('truetype');
+ src: url(font-path('KaTeX_Size1-Regular.eot'));
+ src: url(font-path('KaTeX_Size1-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Size1-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Size1-Regular.woff')) format('woff'), url(font-path('KaTeX_Size1-Regular.ttf')) format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Size2';
- src: url('KaTeX_Size2-Regular.eot');
- src: url('KaTeX_Size2-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size2-Regular.woff2') format('woff2'), url('KaTeX_Size2-Regular.woff') format('woff'), url('KaTeX_Size2-Regular.ttf') format('truetype');
+ src: url(font-path('KaTeX_Size2-Regular.eot'));
+ src: url(font-path('KaTeX_Size2-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Size2-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Size2-Regular.woff')) format('woff'), url(font-path('KaTeX_Size2-Regular.ttf')) format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Size3';
- src: url('KaTeX_Size3-Regular.eot');
- src: url('KaTeX_Size3-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size3-Regular.woff2') format('woff2'), url('KaTeX_Size3-Regular.woff') format('woff'), url('KaTeX_Size3-Regular.ttf') format('truetype');
+ src: url(font-path('KaTeX_Size3-Regular.eot'));
+ src: url(font-path('KaTeX_Size3-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Size3-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Size3-Regular.woff')) format('woff'), url(font-path('KaTeX_Size3-Regular.ttf')) format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Size4';
- src: url('KaTeX_Size4-Regular.eot');
- src: url('KaTeX_Size4-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size4-Regular.woff2') format('woff2'), url('KaTeX_Size4-Regular.woff') format('woff'), url('KaTeX_Size4-Regular.ttf') format('truetype');
+ src: url(font-path('KaTeX_Size4-Regular.eot'));
+ src: url(font-path('KaTeX_Size4-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Size4-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Size4-Regular.woff')) format('woff'), url(font-path('KaTeX_Size4-Regular.ttf')) format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Typewriter';
- src: url('KaTeX_Typewriter-Regular.eot');
- src: url('KaTeX_Typewriter-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Typewriter-Regular.woff2') format('woff2'), url('KaTeX_Typewriter-Regular.woff') format('woff'), url('KaTeX_Typewriter-Regular.ttf') format('truetype');
+ src: url(font-path('KaTeX_Typewriter-Regular.eot'));
+ src: url(font-path('KaTeX_Typewriter-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Typewriter-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Typewriter-Regular.woff')) format('woff'), url(font-path('KaTeX_Typewriter-Regular.ttf')) format('truetype');
font-weight: normal;
font-style: normal;
}
diff --git a/vendor/assets/stylesheets/xterm/xterm.css b/vendor/assets/stylesheets/xterm/xterm.css
new file mode 100644
index 00000000000..b30d7b493f1
--- /dev/null
+++ b/vendor/assets/stylesheets/xterm/xterm.css
@@ -0,0 +1,2206 @@
+/**
+ * xterm.js: xterm, in the browser
+ * Copyright (c) 2014-2016, SourceLair Private Company (www.sourcelair.com (MIT License)
+ * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
+ * https://github.com/chjj/term.js
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * Originally forked from (with the author's permission):
+ * Fabrice Bellard's javascript vt100 for jslinux:
+ * http://bellard.org/jslinux/
+ * Copyright (c) 2011 Fabrice Bellard
+ * The original design remains. The terminal itself
+ * has been extended to include xterm CSI codes, among
+ * other features.
+ */
+
+/*
+ * Default style for xterm.js
+ */
+
+.terminal {
+ background-color: #000;
+ color: #fff;
+ font-family: courier-new, courier, monospace;
+ font-feature-settings: "liga" 0;
+ position: relative;
+}
+
+.terminal.focus,
+.terminal:focus {
+ outline: none;
+}
+
+.terminal .xterm-helpers {
+ position: absolute;
+ top: 0;
+}
+
+.terminal .xterm-helper-textarea {
+ /*
+ * HACK: to fix IE's blinking cursor
+ * Move textarea out of the screen to the far left, so that the cursor is not visible.
+ */
+ position: absolute;
+ opacity: 0;
+ left: -9999em;
+ top: -9999em;
+ width: 0;
+ height: 0;
+ z-index: -10;
+ /** Prevent wrapping so the IME appears against the textarea at the correct position */
+ white-space: nowrap;
+ overflow: hidden;
+ resize: none;
+}
+
+.terminal .terminal-cursor {
+ background-color: #fff;
+ color: #000;
+}
+
+.terminal:not(.focus) .terminal-cursor {
+ outline: 1px solid #fff;
+ outline-offset: -1px;
+ background-color: transparent;
+}
+
+.terminal.focus .terminal-cursor.blinking {
+ animation: blink-cursor 1.2s infinite step-end;
+}
+
+@keyframes blink-cursor {
+ 0% {
+ background-color: #fff;
+ color: #000;
+ }
+ 50% {
+ background-color: transparent;
+ color: #FFF;
+ }
+}
+
+.terminal .composition-view {
+ background: #000;
+ color: #FFF;
+ display: none;
+ position: absolute;
+ white-space: nowrap;
+ z-index: 1;
+}
+
+.terminal .composition-view.active {
+ display: block;
+}
+
+.terminal .xterm-viewport {
+ /* On OS X this is required in order for the scroll bar to appear fully opaque */
+ background-color: #000;
+ overflow-y: scroll;
+}
+
+.terminal .xterm-rows {
+ position: absolute;
+ left: 0;
+ top: 0;
+}
+
+.terminal .xterm-rows > div {
+ /* Lines containing spans and text nodes ocassionally wrap despite being the same width (#327) */
+ white-space: nowrap;
+}
+
+.terminal .xterm-scroll-area {
+ visibility: hidden;
+}
+
+.terminal .xterm-char-measure-element {
+ display: inline-block;
+ visibility: hidden;
+ position: absolute;
+ left: -9999em;
+}
+
+/*
+ * Determine default colors for xterm.js
+ */
+.terminal .xterm-bold {
+ font-weight: bold;
+}
+
+.terminal .xterm-underline {
+ text-decoration: underline;
+}
+
+.terminal .xterm-blink {
+ text-decoration: blink;
+}
+
+.terminal .xterm-hidden {
+ visibility: hidden;
+}
+
+.terminal .xterm-color-0 {
+ color: #2e3436;
+}
+
+.terminal .xterm-bg-color-0 {
+ background-color: #2e3436;
+}
+
+.terminal .xterm-color-1 {
+ color: #cc0000;
+}
+
+.terminal .xterm-bg-color-1 {
+ background-color: #cc0000;
+}
+
+.terminal .xterm-color-2 {
+ color: #4e9a06;
+}
+
+.terminal .xterm-bg-color-2 {
+ background-color: #4e9a06;
+}
+
+.terminal .xterm-color-3 {
+ color: #c4a000;
+}
+
+.terminal .xterm-bg-color-3 {
+ background-color: #c4a000;
+}
+
+.terminal .xterm-color-4 {
+ color: #3465a4;
+}
+
+.terminal .xterm-bg-color-4 {
+ background-color: #3465a4;
+}
+
+.terminal .xterm-color-5 {
+ color: #75507b;
+}
+
+.terminal .xterm-bg-color-5 {
+ background-color: #75507b;
+}
+
+.terminal .xterm-color-6 {
+ color: #06989a;
+}
+
+.terminal .xterm-bg-color-6 {
+ background-color: #06989a;
+}
+
+.terminal .xterm-color-7 {
+ color: #d3d7cf;
+}
+
+.terminal .xterm-bg-color-7 {
+ background-color: #d3d7cf;
+}
+
+.terminal .xterm-color-8 {
+ color: #555753;
+}
+
+.terminal .xterm-bg-color-8 {
+ background-color: #555753;
+}
+
+.terminal .xterm-color-9 {
+ color: #ef2929;
+}
+
+.terminal .xterm-bg-color-9 {
+ background-color: #ef2929;
+}
+
+.terminal .xterm-color-10 {
+ color: #8ae234;
+}
+
+.terminal .xterm-bg-color-10 {
+ background-color: #8ae234;
+}
+
+.terminal .xterm-color-11 {
+ color: #fce94f;
+}
+
+.terminal .xterm-bg-color-11 {
+ background-color: #fce94f;
+}
+
+.terminal .xterm-color-12 {
+ color: #729fcf;
+}
+
+.terminal .xterm-bg-color-12 {
+ background-color: #729fcf;
+}
+
+.terminal .xterm-color-13 {
+ color: #ad7fa8;
+}
+
+.terminal .xterm-bg-color-13 {
+ background-color: #ad7fa8;
+}
+
+.terminal .xterm-color-14 {
+ color: #34e2e2;
+}
+
+.terminal .xterm-bg-color-14 {
+ background-color: #34e2e2;
+}
+
+.terminal .xterm-color-15 {
+ color: #eeeeec;
+}
+
+.terminal .xterm-bg-color-15 {
+ background-color: #eeeeec;
+}
+
+.terminal .xterm-color-16 {
+ color: #000000;
+}
+
+.terminal .xterm-bg-color-16 {
+ background-color: #000000;
+}
+
+.terminal .xterm-color-17 {
+ color: #00005f;
+}
+
+.terminal .xterm-bg-color-17 {
+ background-color: #00005f;
+}
+
+.terminal .xterm-color-18 {
+ color: #000087;
+}
+
+.terminal .xterm-bg-color-18 {
+ background-color: #000087;
+}
+
+.terminal .xterm-color-19 {
+ color: #0000af;
+}
+
+.terminal .xterm-bg-color-19 {
+ background-color: #0000af;
+}
+
+.terminal .xterm-color-20 {
+ color: #0000d7;
+}
+
+.terminal .xterm-bg-color-20 {
+ background-color: #0000d7;
+}
+
+.terminal .xterm-color-21 {
+ color: #0000ff;
+}
+
+.terminal .xterm-bg-color-21 {
+ background-color: #0000ff;
+}
+
+.terminal .xterm-color-22 {
+ color: #005f00;
+}
+
+.terminal .xterm-bg-color-22 {
+ background-color: #005f00;
+}
+
+.terminal .xterm-color-23 {
+ color: #005f5f;
+}
+
+.terminal .xterm-bg-color-23 {
+ background-color: #005f5f;
+}
+
+.terminal .xterm-color-24 {
+ color: #005f87;
+}
+
+.terminal .xterm-bg-color-24 {
+ background-color: #005f87;
+}
+
+.terminal .xterm-color-25 {
+ color: #005faf;
+}
+
+.terminal .xterm-bg-color-25 {
+ background-color: #005faf;
+}
+
+.terminal .xterm-color-26 {
+ color: #005fd7;
+}
+
+.terminal .xterm-bg-color-26 {
+ background-color: #005fd7;
+}
+
+.terminal .xterm-color-27 {
+ color: #005fff;
+}
+
+.terminal .xterm-bg-color-27 {
+ background-color: #005fff;
+}
+
+.terminal .xterm-color-28 {
+ color: #008700;
+}
+
+.terminal .xterm-bg-color-28 {
+ background-color: #008700;
+}
+
+.terminal .xterm-color-29 {
+ color: #00875f;
+}
+
+.terminal .xterm-bg-color-29 {
+ background-color: #00875f;
+}
+
+.terminal .xterm-color-30 {
+ color: #008787;
+}
+
+.terminal .xterm-bg-color-30 {
+ background-color: #008787;
+}
+
+.terminal .xterm-color-31 {
+ color: #0087af;
+}
+
+.terminal .xterm-bg-color-31 {
+ background-color: #0087af;
+}
+
+.terminal .xterm-color-32 {
+ color: #0087d7;
+}
+
+.terminal .xterm-bg-color-32 {
+ background-color: #0087d7;
+}
+
+.terminal .xterm-color-33 {
+ color: #0087ff;
+}
+
+.terminal .xterm-bg-color-33 {
+ background-color: #0087ff;
+}
+
+.terminal .xterm-color-34 {
+ color: #00af00;
+}
+
+.terminal .xterm-bg-color-34 {
+ background-color: #00af00;
+}
+
+.terminal .xterm-color-35 {
+ color: #00af5f;
+}
+
+.terminal .xterm-bg-color-35 {
+ background-color: #00af5f;
+}
+
+.terminal .xterm-color-36 {
+ color: #00af87;
+}
+
+.terminal .xterm-bg-color-36 {
+ background-color: #00af87;
+}
+
+.terminal .xterm-color-37 {
+ color: #00afaf;
+}
+
+.terminal .xterm-bg-color-37 {
+ background-color: #00afaf;
+}
+
+.terminal .xterm-color-38 {
+ color: #00afd7;
+}
+
+.terminal .xterm-bg-color-38 {
+ background-color: #00afd7;
+}
+
+.terminal .xterm-color-39 {
+ color: #00afff;
+}
+
+.terminal .xterm-bg-color-39 {
+ background-color: #00afff;
+}
+
+.terminal .xterm-color-40 {
+ color: #00d700;
+}
+
+.terminal .xterm-bg-color-40 {
+ background-color: #00d700;
+}
+
+.terminal .xterm-color-41 {
+ color: #00d75f;
+}
+
+.terminal .xterm-bg-color-41 {
+ background-color: #00d75f;
+}
+
+.terminal .xterm-color-42 {
+ color: #00d787;
+}
+
+.terminal .xterm-bg-color-42 {
+ background-color: #00d787;
+}
+
+.terminal .xterm-color-43 {
+ color: #00d7af;
+}
+
+.terminal .xterm-bg-color-43 {
+ background-color: #00d7af;
+}
+
+.terminal .xterm-color-44 {
+ color: #00d7d7;
+}
+
+.terminal .xterm-bg-color-44 {
+ background-color: #00d7d7;
+}
+
+.terminal .xterm-color-45 {
+ color: #00d7ff;
+}
+
+.terminal .xterm-bg-color-45 {
+ background-color: #00d7ff;
+}
+
+.terminal .xterm-color-46 {
+ color: #00ff00;
+}
+
+.terminal .xterm-bg-color-46 {
+ background-color: #00ff00;
+}
+
+.terminal .xterm-color-47 {
+ color: #00ff5f;
+}
+
+.terminal .xterm-bg-color-47 {
+ background-color: #00ff5f;
+}
+
+.terminal .xterm-color-48 {
+ color: #00ff87;
+}
+
+.terminal .xterm-bg-color-48 {
+ background-color: #00ff87;
+}
+
+.terminal .xterm-color-49 {
+ color: #00ffaf;
+}
+
+.terminal .xterm-bg-color-49 {
+ background-color: #00ffaf;
+}
+
+.terminal .xterm-color-50 {
+ color: #00ffd7;
+}
+
+.terminal .xterm-bg-color-50 {
+ background-color: #00ffd7;
+}
+
+.terminal .xterm-color-51 {
+ color: #00ffff;
+}
+
+.terminal .xterm-bg-color-51 {
+ background-color: #00ffff;
+}
+
+.terminal .xterm-color-52 {
+ color: #5f0000;
+}
+
+.terminal .xterm-bg-color-52 {
+ background-color: #5f0000;
+}
+
+.terminal .xterm-color-53 {
+ color: #5f005f;
+}
+
+.terminal .xterm-bg-color-53 {
+ background-color: #5f005f;
+}
+
+.terminal .xterm-color-54 {
+ color: #5f0087;
+}
+
+.terminal .xterm-bg-color-54 {
+ background-color: #5f0087;
+}
+
+.terminal .xterm-color-55 {
+ color: #5f00af;
+}
+
+.terminal .xterm-bg-color-55 {
+ background-color: #5f00af;
+}
+
+.terminal .xterm-color-56 {
+ color: #5f00d7;
+}
+
+.terminal .xterm-bg-color-56 {
+ background-color: #5f00d7;
+}
+
+.terminal .xterm-color-57 {
+ color: #5f00ff;
+}
+
+.terminal .xterm-bg-color-57 {
+ background-color: #5f00ff;
+}
+
+.terminal .xterm-color-58 {
+ color: #5f5f00;
+}
+
+.terminal .xterm-bg-color-58 {
+ background-color: #5f5f00;
+}
+
+.terminal .xterm-color-59 {
+ color: #5f5f5f;
+}
+
+.terminal .xterm-bg-color-59 {
+ background-color: #5f5f5f;
+}
+
+.terminal .xterm-color-60 {
+ color: #5f5f87;
+}
+
+.terminal .xterm-bg-color-60 {
+ background-color: #5f5f87;
+}
+
+.terminal .xterm-color-61 {
+ color: #5f5faf;
+}
+
+.terminal .xterm-bg-color-61 {
+ background-color: #5f5faf;
+}
+
+.terminal .xterm-color-62 {
+ color: #5f5fd7;
+}
+
+.terminal .xterm-bg-color-62 {
+ background-color: #5f5fd7;
+}
+
+.terminal .xterm-color-63 {
+ color: #5f5fff;
+}
+
+.terminal .xterm-bg-color-63 {
+ background-color: #5f5fff;
+}
+
+.terminal .xterm-color-64 {
+ color: #5f8700;
+}
+
+.terminal .xterm-bg-color-64 {
+ background-color: #5f8700;
+}
+
+.terminal .xterm-color-65 {
+ color: #5f875f;
+}
+
+.terminal .xterm-bg-color-65 {
+ background-color: #5f875f;
+}
+
+.terminal .xterm-color-66 {
+ color: #5f8787;
+}
+
+.terminal .xterm-bg-color-66 {
+ background-color: #5f8787;
+}
+
+.terminal .xterm-color-67 {
+ color: #5f87af;
+}
+
+.terminal .xterm-bg-color-67 {
+ background-color: #5f87af;
+}
+
+.terminal .xterm-color-68 {
+ color: #5f87d7;
+}
+
+.terminal .xterm-bg-color-68 {
+ background-color: #5f87d7;
+}
+
+.terminal .xterm-color-69 {
+ color: #5f87ff;
+}
+
+.terminal .xterm-bg-color-69 {
+ background-color: #5f87ff;
+}
+
+.terminal .xterm-color-70 {
+ color: #5faf00;
+}
+
+.terminal .xterm-bg-color-70 {
+ background-color: #5faf00;
+}
+
+.terminal .xterm-color-71 {
+ color: #5faf5f;
+}
+
+.terminal .xterm-bg-color-71 {
+ background-color: #5faf5f;
+}
+
+.terminal .xterm-color-72 {
+ color: #5faf87;
+}
+
+.terminal .xterm-bg-color-72 {
+ background-color: #5faf87;
+}
+
+.terminal .xterm-color-73 {
+ color: #5fafaf;
+}
+
+.terminal .xterm-bg-color-73 {
+ background-color: #5fafaf;
+}
+
+.terminal .xterm-color-74 {
+ color: #5fafd7;
+}
+
+.terminal .xterm-bg-color-74 {
+ background-color: #5fafd7;
+}
+
+.terminal .xterm-color-75 {
+ color: #5fafff;
+}
+
+.terminal .xterm-bg-color-75 {
+ background-color: #5fafff;
+}
+
+.terminal .xterm-color-76 {
+ color: #5fd700;
+}
+
+.terminal .xterm-bg-color-76 {
+ background-color: #5fd700;
+}
+
+.terminal .xterm-color-77 {
+ color: #5fd75f;
+}
+
+.terminal .xterm-bg-color-77 {
+ background-color: #5fd75f;
+}
+
+.terminal .xterm-color-78 {
+ color: #5fd787;
+}
+
+.terminal .xterm-bg-color-78 {
+ background-color: #5fd787;
+}
+
+.terminal .xterm-color-79 {
+ color: #5fd7af;
+}
+
+.terminal .xterm-bg-color-79 {
+ background-color: #5fd7af;
+}
+
+.terminal .xterm-color-80 {
+ color: #5fd7d7;
+}
+
+.terminal .xterm-bg-color-80 {
+ background-color: #5fd7d7;
+}
+
+.terminal .xterm-color-81 {
+ color: #5fd7ff;
+}
+
+.terminal .xterm-bg-color-81 {
+ background-color: #5fd7ff;
+}
+
+.terminal .xterm-color-82 {
+ color: #5fff00;
+}
+
+.terminal .xterm-bg-color-82 {
+ background-color: #5fff00;
+}
+
+.terminal .xterm-color-83 {
+ color: #5fff5f;
+}
+
+.terminal .xterm-bg-color-83 {
+ background-color: #5fff5f;
+}
+
+.terminal .xterm-color-84 {
+ color: #5fff87;
+}
+
+.terminal .xterm-bg-color-84 {
+ background-color: #5fff87;
+}
+
+.terminal .xterm-color-85 {
+ color: #5fffaf;
+}
+
+.terminal .xterm-bg-color-85 {
+ background-color: #5fffaf;
+}
+
+.terminal .xterm-color-86 {
+ color: #5fffd7;
+}
+
+.terminal .xterm-bg-color-86 {
+ background-color: #5fffd7;
+}
+
+.terminal .xterm-color-87 {
+ color: #5fffff;
+}
+
+.terminal .xterm-bg-color-87 {
+ background-color: #5fffff;
+}
+
+.terminal .xterm-color-88 {
+ color: #870000;
+}
+
+.terminal .xterm-bg-color-88 {
+ background-color: #870000;
+}
+
+.terminal .xterm-color-89 {
+ color: #87005f;
+}
+
+.terminal .xterm-bg-color-89 {
+ background-color: #87005f;
+}
+
+.terminal .xterm-color-90 {
+ color: #870087;
+}
+
+.terminal .xterm-bg-color-90 {
+ background-color: #870087;
+}
+
+.terminal .xterm-color-91 {
+ color: #8700af;
+}
+
+.terminal .xterm-bg-color-91 {
+ background-color: #8700af;
+}
+
+.terminal .xterm-color-92 {
+ color: #8700d7;
+}
+
+.terminal .xterm-bg-color-92 {
+ background-color: #8700d7;
+}
+
+.terminal .xterm-color-93 {
+ color: #8700ff;
+}
+
+.terminal .xterm-bg-color-93 {
+ background-color: #8700ff;
+}
+
+.terminal .xterm-color-94 {
+ color: #875f00;
+}
+
+.terminal .xterm-bg-color-94 {
+ background-color: #875f00;
+}
+
+.terminal .xterm-color-95 {
+ color: #875f5f;
+}
+
+.terminal .xterm-bg-color-95 {
+ background-color: #875f5f;
+}
+
+.terminal .xterm-color-96 {
+ color: #875f87;
+}
+
+.terminal .xterm-bg-color-96 {
+ background-color: #875f87;
+}
+
+.terminal .xterm-color-97 {
+ color: #875faf;
+}
+
+.terminal .xterm-bg-color-97 {
+ background-color: #875faf;
+}
+
+.terminal .xterm-color-98 {
+ color: #875fd7;
+}
+
+.terminal .xterm-bg-color-98 {
+ background-color: #875fd7;
+}
+
+.terminal .xterm-color-99 {
+ color: #875fff;
+}
+
+.terminal .xterm-bg-color-99 {
+ background-color: #875fff;
+}
+
+.terminal .xterm-color-100 {
+ color: #878700;
+}
+
+.terminal .xterm-bg-color-100 {
+ background-color: #878700;
+}
+
+.terminal .xterm-color-101 {
+ color: #87875f;
+}
+
+.terminal .xterm-bg-color-101 {
+ background-color: #87875f;
+}
+
+.terminal .xterm-color-102 {
+ color: #878787;
+}
+
+.terminal .xterm-bg-color-102 {
+ background-color: #878787;
+}
+
+.terminal .xterm-color-103 {
+ color: #8787af;
+}
+
+.terminal .xterm-bg-color-103 {
+ background-color: #8787af;
+}
+
+.terminal .xterm-color-104 {
+ color: #8787d7;
+}
+
+.terminal .xterm-bg-color-104 {
+ background-color: #8787d7;
+}
+
+.terminal .xterm-color-105 {
+ color: #8787ff;
+}
+
+.terminal .xterm-bg-color-105 {
+ background-color: #8787ff;
+}
+
+.terminal .xterm-color-106 {
+ color: #87af00;
+}
+
+.terminal .xterm-bg-color-106 {
+ background-color: #87af00;
+}
+
+.terminal .xterm-color-107 {
+ color: #87af5f;
+}
+
+.terminal .xterm-bg-color-107 {
+ background-color: #87af5f;
+}
+
+.terminal .xterm-color-108 {
+ color: #87af87;
+}
+
+.terminal .xterm-bg-color-108 {
+ background-color: #87af87;
+}
+
+.terminal .xterm-color-109 {
+ color: #87afaf;
+}
+
+.terminal .xterm-bg-color-109 {
+ background-color: #87afaf;
+}
+
+.terminal .xterm-color-110 {
+ color: #87afd7;
+}
+
+.terminal .xterm-bg-color-110 {
+ background-color: #87afd7;
+}
+
+.terminal .xterm-color-111 {
+ color: #87afff;
+}
+
+.terminal .xterm-bg-color-111 {
+ background-color: #87afff;
+}
+
+.terminal .xterm-color-112 {
+ color: #87d700;
+}
+
+.terminal .xterm-bg-color-112 {
+ background-color: #87d700;
+}
+
+.terminal .xterm-color-113 {
+ color: #87d75f;
+}
+
+.terminal .xterm-bg-color-113 {
+ background-color: #87d75f;
+}
+
+.terminal .xterm-color-114 {
+ color: #87d787;
+}
+
+.terminal .xterm-bg-color-114 {
+ background-color: #87d787;
+}
+
+.terminal .xterm-color-115 {
+ color: #87d7af;
+}
+
+.terminal .xterm-bg-color-115 {
+ background-color: #87d7af;
+}
+
+.terminal .xterm-color-116 {
+ color: #87d7d7;
+}
+
+.terminal .xterm-bg-color-116 {
+ background-color: #87d7d7;
+}
+
+.terminal .xterm-color-117 {
+ color: #87d7ff;
+}
+
+.terminal .xterm-bg-color-117 {
+ background-color: #87d7ff;
+}
+
+.terminal .xterm-color-118 {
+ color: #87ff00;
+}
+
+.terminal .xterm-bg-color-118 {
+ background-color: #87ff00;
+}
+
+.terminal .xterm-color-119 {
+ color: #87ff5f;
+}
+
+.terminal .xterm-bg-color-119 {
+ background-color: #87ff5f;
+}
+
+.terminal .xterm-color-120 {
+ color: #87ff87;
+}
+
+.terminal .xterm-bg-color-120 {
+ background-color: #87ff87;
+}
+
+.terminal .xterm-color-121 {
+ color: #87ffaf;
+}
+
+.terminal .xterm-bg-color-121 {
+ background-color: #87ffaf;
+}
+
+.terminal .xterm-color-122 {
+ color: #87ffd7;
+}
+
+.terminal .xterm-bg-color-122 {
+ background-color: #87ffd7;
+}
+
+.terminal .xterm-color-123 {
+ color: #87ffff;
+}
+
+.terminal .xterm-bg-color-123 {
+ background-color: #87ffff;
+}
+
+.terminal .xterm-color-124 {
+ color: #af0000;
+}
+
+.terminal .xterm-bg-color-124 {
+ background-color: #af0000;
+}
+
+.terminal .xterm-color-125 {
+ color: #af005f;
+}
+
+.terminal .xterm-bg-color-125 {
+ background-color: #af005f;
+}
+
+.terminal .xterm-color-126 {
+ color: #af0087;
+}
+
+.terminal .xterm-bg-color-126 {
+ background-color: #af0087;
+}
+
+.terminal .xterm-color-127 {
+ color: #af00af;
+}
+
+.terminal .xterm-bg-color-127 {
+ background-color: #af00af;
+}
+
+.terminal .xterm-color-128 {
+ color: #af00d7;
+}
+
+.terminal .xterm-bg-color-128 {
+ background-color: #af00d7;
+}
+
+.terminal .xterm-color-129 {
+ color: #af00ff;
+}
+
+.terminal .xterm-bg-color-129 {
+ background-color: #af00ff;
+}
+
+.terminal .xterm-color-130 {
+ color: #af5f00;
+}
+
+.terminal .xterm-bg-color-130 {
+ background-color: #af5f00;
+}
+
+.terminal .xterm-color-131 {
+ color: #af5f5f;
+}
+
+.terminal .xterm-bg-color-131 {
+ background-color: #af5f5f;
+}
+
+.terminal .xterm-color-132 {
+ color: #af5f87;
+}
+
+.terminal .xterm-bg-color-132 {
+ background-color: #af5f87;
+}
+
+.terminal .xterm-color-133 {
+ color: #af5faf;
+}
+
+.terminal .xterm-bg-color-133 {
+ background-color: #af5faf;
+}
+
+.terminal .xterm-color-134 {
+ color: #af5fd7;
+}
+
+.terminal .xterm-bg-color-134 {
+ background-color: #af5fd7;
+}
+
+.terminal .xterm-color-135 {
+ color: #af5fff;
+}
+
+.terminal .xterm-bg-color-135 {
+ background-color: #af5fff;
+}
+
+.terminal .xterm-color-136 {
+ color: #af8700;
+}
+
+.terminal .xterm-bg-color-136 {
+ background-color: #af8700;
+}
+
+.terminal .xterm-color-137 {
+ color: #af875f;
+}
+
+.terminal .xterm-bg-color-137 {
+ background-color: #af875f;
+}
+
+.terminal .xterm-color-138 {
+ color: #af8787;
+}
+
+.terminal .xterm-bg-color-138 {
+ background-color: #af8787;
+}
+
+.terminal .xterm-color-139 {
+ color: #af87af;
+}
+
+.terminal .xterm-bg-color-139 {
+ background-color: #af87af;
+}
+
+.terminal .xterm-color-140 {
+ color: #af87d7;
+}
+
+.terminal .xterm-bg-color-140 {
+ background-color: #af87d7;
+}
+
+.terminal .xterm-color-141 {
+ color: #af87ff;
+}
+
+.terminal .xterm-bg-color-141 {
+ background-color: #af87ff;
+}
+
+.terminal .xterm-color-142 {
+ color: #afaf00;
+}
+
+.terminal .xterm-bg-color-142 {
+ background-color: #afaf00;
+}
+
+.terminal .xterm-color-143 {
+ color: #afaf5f;
+}
+
+.terminal .xterm-bg-color-143 {
+ background-color: #afaf5f;
+}
+
+.terminal .xterm-color-144 {
+ color: #afaf87;
+}
+
+.terminal .xterm-bg-color-144 {
+ background-color: #afaf87;
+}
+
+.terminal .xterm-color-145 {
+ color: #afafaf;
+}
+
+.terminal .xterm-bg-color-145 {
+ background-color: #afafaf;
+}
+
+.terminal .xterm-color-146 {
+ color: #afafd7;
+}
+
+.terminal .xterm-bg-color-146 {
+ background-color: #afafd7;
+}
+
+.terminal .xterm-color-147 {
+ color: #afafff;
+}
+
+.terminal .xterm-bg-color-147 {
+ background-color: #afafff;
+}
+
+.terminal .xterm-color-148 {
+ color: #afd700;
+}
+
+.terminal .xterm-bg-color-148 {
+ background-color: #afd700;
+}
+
+.terminal .xterm-color-149 {
+ color: #afd75f;
+}
+
+.terminal .xterm-bg-color-149 {
+ background-color: #afd75f;
+}
+
+.terminal .xterm-color-150 {
+ color: #afd787;
+}
+
+.terminal .xterm-bg-color-150 {
+ background-color: #afd787;
+}
+
+.terminal .xterm-color-151 {
+ color: #afd7af;
+}
+
+.terminal .xterm-bg-color-151 {
+ background-color: #afd7af;
+}
+
+.terminal .xterm-color-152 {
+ color: #afd7d7;
+}
+
+.terminal .xterm-bg-color-152 {
+ background-color: #afd7d7;
+}
+
+.terminal .xterm-color-153 {
+ color: #afd7ff;
+}
+
+.terminal .xterm-bg-color-153 {
+ background-color: #afd7ff;
+}
+
+.terminal .xterm-color-154 {
+ color: #afff00;
+}
+
+.terminal .xterm-bg-color-154 {
+ background-color: #afff00;
+}
+
+.terminal .xterm-color-155 {
+ color: #afff5f;
+}
+
+.terminal .xterm-bg-color-155 {
+ background-color: #afff5f;
+}
+
+.terminal .xterm-color-156 {
+ color: #afff87;
+}
+
+.terminal .xterm-bg-color-156 {
+ background-color: #afff87;
+}
+
+.terminal .xterm-color-157 {
+ color: #afffaf;
+}
+
+.terminal .xterm-bg-color-157 {
+ background-color: #afffaf;
+}
+
+.terminal .xterm-color-158 {
+ color: #afffd7;
+}
+
+.terminal .xterm-bg-color-158 {
+ background-color: #afffd7;
+}
+
+.terminal .xterm-color-159 {
+ color: #afffff;
+}
+
+.terminal .xterm-bg-color-159 {
+ background-color: #afffff;
+}
+
+.terminal .xterm-color-160 {
+ color: #d70000;
+}
+
+.terminal .xterm-bg-color-160 {
+ background-color: #d70000;
+}
+
+.terminal .xterm-color-161 {
+ color: #d7005f;
+}
+
+.terminal .xterm-bg-color-161 {
+ background-color: #d7005f;
+}
+
+.terminal .xterm-color-162 {
+ color: #d70087;
+}
+
+.terminal .xterm-bg-color-162 {
+ background-color: #d70087;
+}
+
+.terminal .xterm-color-163 {
+ color: #d700af;
+}
+
+.terminal .xterm-bg-color-163 {
+ background-color: #d700af;
+}
+
+.terminal .xterm-color-164 {
+ color: #d700d7;
+}
+
+.terminal .xterm-bg-color-164 {
+ background-color: #d700d7;
+}
+
+.terminal .xterm-color-165 {
+ color: #d700ff;
+}
+
+.terminal .xterm-bg-color-165 {
+ background-color: #d700ff;
+}
+
+.terminal .xterm-color-166 {
+ color: #d75f00;
+}
+
+.terminal .xterm-bg-color-166 {
+ background-color: #d75f00;
+}
+
+.terminal .xterm-color-167 {
+ color: #d75f5f;
+}
+
+.terminal .xterm-bg-color-167 {
+ background-color: #d75f5f;
+}
+
+.terminal .xterm-color-168 {
+ color: #d75f87;
+}
+
+.terminal .xterm-bg-color-168 {
+ background-color: #d75f87;
+}
+
+.terminal .xterm-color-169 {
+ color: #d75faf;
+}
+
+.terminal .xterm-bg-color-169 {
+ background-color: #d75faf;
+}
+
+.terminal .xterm-color-170 {
+ color: #d75fd7;
+}
+
+.terminal .xterm-bg-color-170 {
+ background-color: #d75fd7;
+}
+
+.terminal .xterm-color-171 {
+ color: #d75fff;
+}
+
+.terminal .xterm-bg-color-171 {
+ background-color: #d75fff;
+}
+
+.terminal .xterm-color-172 {
+ color: #d78700;
+}
+
+.terminal .xterm-bg-color-172 {
+ background-color: #d78700;
+}
+
+.terminal .xterm-color-173 {
+ color: #d7875f;
+}
+
+.terminal .xterm-bg-color-173 {
+ background-color: #d7875f;
+}
+
+.terminal .xterm-color-174 {
+ color: #d78787;
+}
+
+.terminal .xterm-bg-color-174 {
+ background-color: #d78787;
+}
+
+.terminal .xterm-color-175 {
+ color: #d787af;
+}
+
+.terminal .xterm-bg-color-175 {
+ background-color: #d787af;
+}
+
+.terminal .xterm-color-176 {
+ color: #d787d7;
+}
+
+.terminal .xterm-bg-color-176 {
+ background-color: #d787d7;
+}
+
+.terminal .xterm-color-177 {
+ color: #d787ff;
+}
+
+.terminal .xterm-bg-color-177 {
+ background-color: #d787ff;
+}
+
+.terminal .xterm-color-178 {
+ color: #d7af00;
+}
+
+.terminal .xterm-bg-color-178 {
+ background-color: #d7af00;
+}
+
+.terminal .xterm-color-179 {
+ color: #d7af5f;
+}
+
+.terminal .xterm-bg-color-179 {
+ background-color: #d7af5f;
+}
+
+.terminal .xterm-color-180 {
+ color: #d7af87;
+}
+
+.terminal .xterm-bg-color-180 {
+ background-color: #d7af87;
+}
+
+.terminal .xterm-color-181 {
+ color: #d7afaf;
+}
+
+.terminal .xterm-bg-color-181 {
+ background-color: #d7afaf;
+}
+
+.terminal .xterm-color-182 {
+ color: #d7afd7;
+}
+
+.terminal .xterm-bg-color-182 {
+ background-color: #d7afd7;
+}
+
+.terminal .xterm-color-183 {
+ color: #d7afff;
+}
+
+.terminal .xterm-bg-color-183 {
+ background-color: #d7afff;
+}
+
+.terminal .xterm-color-184 {
+ color: #d7d700;
+}
+
+.terminal .xterm-bg-color-184 {
+ background-color: #d7d700;
+}
+
+.terminal .xterm-color-185 {
+ color: #d7d75f;
+}
+
+.terminal .xterm-bg-color-185 {
+ background-color: #d7d75f;
+}
+
+.terminal .xterm-color-186 {
+ color: #d7d787;
+}
+
+.terminal .xterm-bg-color-186 {
+ background-color: #d7d787;
+}
+
+.terminal .xterm-color-187 {
+ color: #d7d7af;
+}
+
+.terminal .xterm-bg-color-187 {
+ background-color: #d7d7af;
+}
+
+.terminal .xterm-color-188 {
+ color: #d7d7d7;
+}
+
+.terminal .xterm-bg-color-188 {
+ background-color: #d7d7d7;
+}
+
+.terminal .xterm-color-189 {
+ color: #d7d7ff;
+}
+
+.terminal .xterm-bg-color-189 {
+ background-color: #d7d7ff;
+}
+
+.terminal .xterm-color-190 {
+ color: #d7ff00;
+}
+
+.terminal .xterm-bg-color-190 {
+ background-color: #d7ff00;
+}
+
+.terminal .xterm-color-191 {
+ color: #d7ff5f;
+}
+
+.terminal .xterm-bg-color-191 {
+ background-color: #d7ff5f;
+}
+
+.terminal .xterm-color-192 {
+ color: #d7ff87;
+}
+
+.terminal .xterm-bg-color-192 {
+ background-color: #d7ff87;
+}
+
+.terminal .xterm-color-193 {
+ color: #d7ffaf;
+}
+
+.terminal .xterm-bg-color-193 {
+ background-color: #d7ffaf;
+}
+
+.terminal .xterm-color-194 {
+ color: #d7ffd7;
+}
+
+.terminal .xterm-bg-color-194 {
+ background-color: #d7ffd7;
+}
+
+.terminal .xterm-color-195 {
+ color: #d7ffff;
+}
+
+.terminal .xterm-bg-color-195 {
+ background-color: #d7ffff;
+}
+
+.terminal .xterm-color-196 {
+ color: #ff0000;
+}
+
+.terminal .xterm-bg-color-196 {
+ background-color: #ff0000;
+}
+
+.terminal .xterm-color-197 {
+ color: #ff005f;
+}
+
+.terminal .xterm-bg-color-197 {
+ background-color: #ff005f;
+}
+
+.terminal .xterm-color-198 {
+ color: #ff0087;
+}
+
+.terminal .xterm-bg-color-198 {
+ background-color: #ff0087;
+}
+
+.terminal .xterm-color-199 {
+ color: #ff00af;
+}
+
+.terminal .xterm-bg-color-199 {
+ background-color: #ff00af;
+}
+
+.terminal .xterm-color-200 {
+ color: #ff00d7;
+}
+
+.terminal .xterm-bg-color-200 {
+ background-color: #ff00d7;
+}
+
+.terminal .xterm-color-201 {
+ color: #ff00ff;
+}
+
+.terminal .xterm-bg-color-201 {
+ background-color: #ff00ff;
+}
+
+.terminal .xterm-color-202 {
+ color: #ff5f00;
+}
+
+.terminal .xterm-bg-color-202 {
+ background-color: #ff5f00;
+}
+
+.terminal .xterm-color-203 {
+ color: #ff5f5f;
+}
+
+.terminal .xterm-bg-color-203 {
+ background-color: #ff5f5f;
+}
+
+.terminal .xterm-color-204 {
+ color: #ff5f87;
+}
+
+.terminal .xterm-bg-color-204 {
+ background-color: #ff5f87;
+}
+
+.terminal .xterm-color-205 {
+ color: #ff5faf;
+}
+
+.terminal .xterm-bg-color-205 {
+ background-color: #ff5faf;
+}
+
+.terminal .xterm-color-206 {
+ color: #ff5fd7;
+}
+
+.terminal .xterm-bg-color-206 {
+ background-color: #ff5fd7;
+}
+
+.terminal .xterm-color-207 {
+ color: #ff5fff;
+}
+
+.terminal .xterm-bg-color-207 {
+ background-color: #ff5fff;
+}
+
+.terminal .xterm-color-208 {
+ color: #ff8700;
+}
+
+.terminal .xterm-bg-color-208 {
+ background-color: #ff8700;
+}
+
+.terminal .xterm-color-209 {
+ color: #ff875f;
+}
+
+.terminal .xterm-bg-color-209 {
+ background-color: #ff875f;
+}
+
+.terminal .xterm-color-210 {
+ color: #ff8787;
+}
+
+.terminal .xterm-bg-color-210 {
+ background-color: #ff8787;
+}
+
+.terminal .xterm-color-211 {
+ color: #ff87af;
+}
+
+.terminal .xterm-bg-color-211 {
+ background-color: #ff87af;
+}
+
+.terminal .xterm-color-212 {
+ color: #ff87d7;
+}
+
+.terminal .xterm-bg-color-212 {
+ background-color: #ff87d7;
+}
+
+.terminal .xterm-color-213 {
+ color: #ff87ff;
+}
+
+.terminal .xterm-bg-color-213 {
+ background-color: #ff87ff;
+}
+
+.terminal .xterm-color-214 {
+ color: #ffaf00;
+}
+
+.terminal .xterm-bg-color-214 {
+ background-color: #ffaf00;
+}
+
+.terminal .xterm-color-215 {
+ color: #ffaf5f;
+}
+
+.terminal .xterm-bg-color-215 {
+ background-color: #ffaf5f;
+}
+
+.terminal .xterm-color-216 {
+ color: #ffaf87;
+}
+
+.terminal .xterm-bg-color-216 {
+ background-color: #ffaf87;
+}
+
+.terminal .xterm-color-217 {
+ color: #ffafaf;
+}
+
+.terminal .xterm-bg-color-217 {
+ background-color: #ffafaf;
+}
+
+.terminal .xterm-color-218 {
+ color: #ffafd7;
+}
+
+.terminal .xterm-bg-color-218 {
+ background-color: #ffafd7;
+}
+
+.terminal .xterm-color-219 {
+ color: #ffafff;
+}
+
+.terminal .xterm-bg-color-219 {
+ background-color: #ffafff;
+}
+
+.terminal .xterm-color-220 {
+ color: #ffd700;
+}
+
+.terminal .xterm-bg-color-220 {
+ background-color: #ffd700;
+}
+
+.terminal .xterm-color-221 {
+ color: #ffd75f;
+}
+
+.terminal .xterm-bg-color-221 {
+ background-color: #ffd75f;
+}
+
+.terminal .xterm-color-222 {
+ color: #ffd787;
+}
+
+.terminal .xterm-bg-color-222 {
+ background-color: #ffd787;
+}
+
+.terminal .xterm-color-223 {
+ color: #ffd7af;
+}
+
+.terminal .xterm-bg-color-223 {
+ background-color: #ffd7af;
+}
+
+.terminal .xterm-color-224 {
+ color: #ffd7d7;
+}
+
+.terminal .xterm-bg-color-224 {
+ background-color: #ffd7d7;
+}
+
+.terminal .xterm-color-225 {
+ color: #ffd7ff;
+}
+
+.terminal .xterm-bg-color-225 {
+ background-color: #ffd7ff;
+}
+
+.terminal .xterm-color-226 {
+ color: #ffff00;
+}
+
+.terminal .xterm-bg-color-226 {
+ background-color: #ffff00;
+}
+
+.terminal .xterm-color-227 {
+ color: #ffff5f;
+}
+
+.terminal .xterm-bg-color-227 {
+ background-color: #ffff5f;
+}
+
+.terminal .xterm-color-228 {
+ color: #ffff87;
+}
+
+.terminal .xterm-bg-color-228 {
+ background-color: #ffff87;
+}
+
+.terminal .xterm-color-229 {
+ color: #ffffaf;
+}
+
+.terminal .xterm-bg-color-229 {
+ background-color: #ffffaf;
+}
+
+.terminal .xterm-color-230 {
+ color: #ffffd7;
+}
+
+.terminal .xterm-bg-color-230 {
+ background-color: #ffffd7;
+}
+
+.terminal .xterm-color-231 {
+ color: #ffffff;
+}
+
+.terminal .xterm-bg-color-231 {
+ background-color: #ffffff;
+}
+
+.terminal .xterm-color-232 {
+ color: #080808;
+}
+
+.terminal .xterm-bg-color-232 {
+ background-color: #080808;
+}
+
+.terminal .xterm-color-233 {
+ color: #121212;
+}
+
+.terminal .xterm-bg-color-233 {
+ background-color: #121212;
+}
+
+.terminal .xterm-color-234 {
+ color: #1c1c1c;
+}
+
+.terminal .xterm-bg-color-234 {
+ background-color: #1c1c1c;
+}
+
+.terminal .xterm-color-235 {
+ color: #262626;
+}
+
+.terminal .xterm-bg-color-235 {
+ background-color: #262626;
+}
+
+.terminal .xterm-color-236 {
+ color: #303030;
+}
+
+.terminal .xterm-bg-color-236 {
+ background-color: #303030;
+}
+
+.terminal .xterm-color-237 {
+ color: #3a3a3a;
+}
+
+.terminal .xterm-bg-color-237 {
+ background-color: #3a3a3a;
+}
+
+.terminal .xterm-color-238 {
+ color: #444444;
+}
+
+.terminal .xterm-bg-color-238 {
+ background-color: #444444;
+}
+
+.terminal .xterm-color-239 {
+ color: #4e4e4e;
+}
+
+.terminal .xterm-bg-color-239 {
+ background-color: #4e4e4e;
+}
+
+.terminal .xterm-color-240 {
+ color: #585858;
+}
+
+.terminal .xterm-bg-color-240 {
+ background-color: #585858;
+}
+
+.terminal .xterm-color-241 {
+ color: #626262;
+}
+
+.terminal .xterm-bg-color-241 {
+ background-color: #626262;
+}
+
+.terminal .xterm-color-242 {
+ color: #6c6c6c;
+}
+
+.terminal .xterm-bg-color-242 {
+ background-color: #6c6c6c;
+}
+
+.terminal .xterm-color-243 {
+ color: #767676;
+}
+
+.terminal .xterm-bg-color-243 {
+ background-color: #767676;
+}
+
+.terminal .xterm-color-244 {
+ color: #808080;
+}
+
+.terminal .xterm-bg-color-244 {
+ background-color: #808080;
+}
+
+.terminal .xterm-color-245 {
+ color: #8a8a8a;
+}
+
+.terminal .xterm-bg-color-245 {
+ background-color: #8a8a8a;
+}
+
+.terminal .xterm-color-246 {
+ color: #949494;
+}
+
+.terminal .xterm-bg-color-246 {
+ background-color: #949494;
+}
+
+.terminal .xterm-color-247 {
+ color: #9e9e9e;
+}
+
+.terminal .xterm-bg-color-247 {
+ background-color: #9e9e9e;
+}
+
+.terminal .xterm-color-248 {
+ color: #a8a8a8;
+}
+
+.terminal .xterm-bg-color-248 {
+ background-color: #a8a8a8;
+}
+
+.terminal .xterm-color-249 {
+ color: #b2b2b2;
+}
+
+.terminal .xterm-bg-color-249 {
+ background-color: #b2b2b2;
+}
+
+.terminal .xterm-color-250 {
+ color: #bcbcbc;
+}
+
+.terminal .xterm-bg-color-250 {
+ background-color: #bcbcbc;
+}
+
+.terminal .xterm-color-251 {
+ color: #c6c6c6;
+}
+
+.terminal .xterm-bg-color-251 {
+ background-color: #c6c6c6;
+}
+
+.terminal .xterm-color-252 {
+ color: #d0d0d0;
+}
+
+.terminal .xterm-bg-color-252 {
+ background-color: #d0d0d0;
+}
+
+.terminal .xterm-color-253 {
+ color: #dadada;
+}
+
+.terminal .xterm-bg-color-253 {
+ background-color: #dadada;
+}
+
+.terminal .xterm-color-254 {
+ color: #e4e4e4;
+}
+
+.terminal .xterm-bg-color-254 {
+ background-color: #e4e4e4;
+}
+
+.terminal .xterm-color-255 {
+ color: #eeeeee;
+}
+
+.terminal .xterm-bg-color-255 {
+ background-color: #eeeeee;
+}
diff --git a/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml
new file mode 100644
index 00000000000..e384b585ae0
--- /dev/null
+++ b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml
@@ -0,0 +1,74 @@
+image: registry.gitlab.com/gitlab-examples/openshift-deploy
+
+variables:
+ # Application deployment domain
+ KUBE_DOMAIN: domain.example.com
+
+stages:
+ - build
+ - test
+ - review
+ - staging
+ - production
+
+build:
+ stage: build
+ script:
+ - command build
+ only:
+ - branches
+
+production:
+ stage: production
+ variables:
+ CI_ENVIRONMENT_URL: http://production.$KUBE_DOMAIN
+ script:
+ - command deploy
+ environment:
+ name: production
+ url: http://production.$KUBE_DOMAIN
+ when: manual
+ only:
+ - master
+
+staging:
+ stage: staging
+ variables:
+ CI_ENVIRONMENT_URL: http://staging.$KUBE_DOMAIN
+ script:
+ - command deploy
+ environment:
+ name: staging
+ url: http://staging.$KUBE_DOMAIN
+ only:
+ - master
+
+review:
+ stage: review
+ variables:
+ CI_ENVIRONMENT_URL: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
+ script:
+ - command deploy
+ environment:
+ name: review/$CI_BUILD_REF_NAME
+ url: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
+ on_stop: stop_review
+ only:
+ - branches
+ except:
+ - master
+
+stop_review:
+ stage: review
+ variables:
+ GIT_STRATEGY: none
+ script:
+ - command destroy
+ environment:
+ name: review/$CI_BUILD_REF_NAME
+ action: stop
+ when: manual
+ only:
+ - branches
+ except:
+ - master