summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLin Jen-Shin <godfat@godfat.org>2016-12-20 20:54:40 +0800
committerLin Jen-Shin <godfat@godfat.org>2016-12-20 20:54:40 +0800
commit0f0738e78867f6822dd15cb26da1f17628acde77 (patch)
tree0813c71b448d149afad6802e25c56a219efb0523
parenteb839b9af51d411a6a35786a1c1c58954da1a650 (diff)
parentad1a1d976c877eca16858368db0c5b3ef800db8b (diff)
downloadgitlab-ce-0f0738e78867f6822dd15cb26da1f17628acde77.tar.gz
Merge remote-tracking branch 'upstream/master' into feature/1376-allow-write-access-deploy-keys
* upstream/master: (538 commits) Reject blank environment vcariables in Gitlab::Git::RevList Add online terminal documentation Add changelog entry Add terminal UI and controller actions Fix specs Even out padding on plus button in breadcrumb menu Update font size of detail page header to 14px Update CHANGELOG.md for 8.13.10 Update CHANGELOG.md for 8.14.5 Fix Route#rename_children behavior Remove inline-block styling from status Add terminals to the Kubernetes deployment service Add a ReactiveCaching concern for use in the KubernetesService Add xterm.js 2.1.0 and a wrapper class to the asset pipeline Remove unnecessary hidden svg elements for icons. Fix consistent typo in environment.js Use a block to insert extra check for authenticate_build! Align milestone column header with count number Add Wiki import to BB importer Make CI badge hitboxes better match container ...
-rw-r--r--.eslintignore1
-rw-r--r--.gitlab-ci.yml1
-rw-r--r--.rubocop.yml3
-rw-r--r--CHANGELOG.md22
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile10
-rw-r--r--Gemfile.lock54
-rw-r--r--app/assets/javascripts/abuse_reports.js.es63
-rw-r--r--app/assets/javascripts/admin.js4
-rw-r--r--app/assets/javascripts/api.js11
-rw-r--r--app/assets/javascripts/application.js9
-rw-r--r--app/assets/javascripts/awards_handler.js4
-rw-r--r--app/assets/javascripts/behaviors/autosize.js3
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js3
-rw-r--r--app/assets/javascripts/blob/blob_ci_yaml.js.es64
-rw-r--r--app/assets/javascripts/blob/blob_dockerfile_selector.js.es618
-rw-r--r--app/assets/javascripts/blob/blob_dockerfile_selectors.js.es627
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js4
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selector.js3
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selectors.js4
-rw-r--r--app/assets/javascripts/blob/blob_license_selector.js3
-rw-r--r--app/assets/javascripts/blob/blob_license_selectors.js.es64
-rw-r--r--app/assets/javascripts/blob/template_selector.js.es63
-rw-r--r--app/assets/javascripts/blob_edit/blob_edit_bundle.js5
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js10
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js.es65
-rw-r--r--app/assets/javascripts/boards/components/board.js.es65
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.js.es65
-rw-r--r--app/assets/javascripts/boards/components/board_card.js.es64
-rw-r--r--app/assets/javascripts/boards/components/board_delete.js.es64
-rw-r--r--app/assets/javascripts/boards/components/board_list.js.es65
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.js.es65
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js.es68
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js.es611
-rw-r--r--app/assets/javascripts/boards/filters/due_date_filters.js.es63
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js.es64
-rw-r--r--app/assets/javascripts/boards/models/issue.js.es67
-rw-r--r--app/assets/javascripts/boards/models/label.js.es63
-rw-r--r--app/assets/javascripts/boards/models/list.js.es65
-rw-r--r--app/assets/javascripts/boards/models/milestone.js.es65
-rw-r--r--app/assets/javascripts/boards/models/user.js.es65
-rw-r--r--app/assets/javascripts/boards/services/board_service.js.es64
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js.es65
-rw-r--r--app/assets/javascripts/boards/vue_resource_interceptor.js.es64
-rw-r--r--app/assets/javascripts/breakpoints.js6
-rw-r--r--app/assets/javascripts/build.js6
-rw-r--r--app/assets/javascripts/build_variables.js.es63
-rw-r--r--app/assets/javascripts/commit.js4
-rw-r--r--app/assets/javascripts/commit/file.js6
-rw-r--r--app/assets/javascripts/commit/image_file.js2
-rw-r--r--app/assets/javascripts/commits.js8
-rw-r--r--app/assets/javascripts/compare_autocomplete.js.es63
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js7
-rw-r--r--app/assets/javascripts/create_label.js.es64
-rw-r--r--app/assets/javascripts/diff.js.es67
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es65
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es68
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js.es67
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_count.js.es66
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es66
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js.es65
-rw-r--r--app/assets/javascripts/diff_notes/mixins/discussion.js.es63
-rw-r--r--app/assets/javascripts/diff_notes/models/discussion.js.es67
-rw-r--r--app/assets/javascripts/diff_notes/models/note.js.es65
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js.es66
-rw-r--r--app/assets/javascripts/diff_notes/stores/comments.js.es65
-rw-r--r--app/assets/javascripts/dispatcher.js.es648
-rw-r--r--app/assets/javascripts/dropzone_input.js3
-rw-r--r--app/assets/javascripts/due_date_select.js.es63
-rw-r--r--app/assets/javascripts/environments/components/environment.js.es616
-rw-r--r--app/assets/javascripts/environments/components/environment_item.js.es618
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.js.es627
-rw-r--r--app/assets/javascripts/extensions/object.js.es626
-rw-r--r--app/assets/javascripts/files_comment_button.js4
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es6244
-rw-r--r--app/assets/javascripts/gl_dropdown.js37
-rw-r--r--app/assets/javascripts/gl_field_errors.js.es62
-rw-r--r--app/assets/javascripts/gl_form.js8
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors.js7
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_graph.js4
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_util.js11
-rw-r--r--app/assets/javascripts/group_label_subscription.js.es63
-rw-r--r--app/assets/javascripts/groups_select.js6
-rw-r--r--app/assets/javascripts/importer_status.js7
-rw-r--r--app/assets/javascripts/issuable.js.es69
-rw-r--r--app/assets/javascripts/issuable_context.js4
-rw-r--r--app/assets/javascripts/issuable_form.js9
-rw-r--r--app/assets/javascripts/issue.js3
-rw-r--r--app/assets/javascripts/issues_bulk_assignment.js.es697
-rw-r--r--app/assets/javascripts/label_manager.js.es65
-rw-r--r--app/assets/javascripts/labels_select.js151
-rw-r--r--app/assets/javascripts/lib/ace.js1
-rw-r--r--app/assets/javascripts/lib/utils/notify.js3
-rw-r--r--app/assets/javascripts/line_highlighter.js3
-rw-r--r--app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es66
-rw-r--r--app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es64
-rw-r--r--app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es66
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es63
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es65
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es65
-rw-r--r--app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es63
-rw-r--r--app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es63
-rw-r--r--app/assets/javascripts/merge_request.js24
-rw-r--r--app/assets/javascripts/merge_request_widget.js.es623
-rw-r--r--app/assets/javascripts/merged_buttons.js3
-rw-r--r--app/assets/javascripts/milestone.js4
-rw-r--r--app/assets/javascripts/milestone_select.js6
-rw-r--r--app/assets/javascripts/namespace_select.js10
-rw-r--r--app/assets/javascripts/network/branch_graph.js4
-rw-r--r--app/assets/javascripts/network/network.js4
-rw-r--r--app/assets/javascripts/network/network_bundle.js5
-rw-r--r--app/assets/javascripts/notes.js14
-rw-r--r--app/assets/javascripts/notifications_dropdown.js6
-rw-r--r--app/assets/javascripts/pipelines.js.es614
-rw-r--r--app/assets/javascripts/preview_markdown.js9
-rw-r--r--app/assets/javascripts/profile/gl_crop.js.es63
-rw-r--r--app/assets/javascripts/profile/profile.js.es64
-rw-r--r--app/assets/javascripts/project.js6
-rw-r--r--app/assets/javascripts/project_find_file.js4
-rw-r--r--app/assets/javascripts/project_import.js4
-rw-r--r--app/assets/javascripts/project_label_subscription.js.es63
-rw-r--r--app/assets/javascripts/project_select.js4
-rw-r--r--app/assets/javascripts/projects_list.js7
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es63
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js.es64
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es63
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js.es66
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es63
-rw-r--r--app/assets/javascripts/protected_branches/protected_branches_bundle.js1
-rw-r--r--app/assets/javascripts/render_gfm.js16
-rw-r--r--app/assets/javascripts/render_math.js55
-rw-r--r--app/assets/javascripts/right_sidebar.js4
-rw-r--r--app/assets/javascripts/search.js4
-rw-r--r--app/assets/javascripts/search_autocomplete.js.es63
-rw-r--r--app/assets/javascripts/shortcuts.js6
-rw-r--r--app/assets/javascripts/shortcuts_blob.js4
-rw-r--r--app/assets/javascripts/shortcuts_dashboard_navigation.js4
-rw-r--r--app/assets/javascripts/shortcuts_find_file.js4
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js6
-rw-r--r--app/assets/javascripts/shortcuts_navigation.js4
-rw-r--r--app/assets/javascripts/shortcuts_network.js4
-rw-r--r--app/assets/javascripts/sidebar.js.es64
-rw-r--r--app/assets/javascripts/single_file_diff.js7
-rw-r--r--app/assets/javascripts/snippet/snippet_bundle.js4
-rw-r--r--app/assets/javascripts/snippets_list.js.es63
-rw-r--r--app/assets/javascripts/star.js4
-rw-r--r--app/assets/javascripts/syntax_highlight.js9
-rw-r--r--app/assets/javascripts/templates/issuable_template_selector.js.es64
-rw-r--r--app/assets/javascripts/templates/issuable_template_selectors.js.es63
-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/todos.js.es67
-rw-r--r--app/assets/javascripts/u2f/authenticate.js6
-rw-r--r--app/assets/javascripts/u2f/error.js4
-rw-r--r--app/assets/javascripts/u2f/register.js6
-rw-r--r--app/assets/javascripts/user.js.es64
-rw-r--r--app/assets/javascripts/user_tabs.js.es63
-rw-r--r--app/assets/javascripts/username_validator.js.es65
-rw-r--r--app/assets/javascripts/users/calendar.js5
-rw-r--r--app/assets/javascripts/users_select.js6
-rw-r--r--app/assets/javascripts/zen_mode.js5
-rw-r--r--app/assets/stylesheets/framework.scss3
-rw-r--r--app/assets/stylesheets/framework/awards.scss6
-rw-r--r--app/assets/stylesheets/framework/blocks.scss4
-rw-r--r--app/assets/stylesheets/framework/buttons.scss29
-rw-r--r--app/assets/stylesheets/framework/callout.scss2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss31
-rw-r--r--app/assets/stylesheets/framework/emojis.scss (renamed from app/assets/stylesheets/pages/emojis.scss)2
-rw-r--r--app/assets/stylesheets/framework/files.scss2
-rw-r--r--app/assets/stylesheets/framework/forms.scss8
-rw-r--r--app/assets/stylesheets/framework/header.scss18
-rw-r--r--app/assets/stylesheets/framework/icons.scss (renamed from app/assets/stylesheets/pages/icons.scss)8
-rw-r--r--app/assets/stylesheets/framework/images.scss2
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss2
-rw-r--r--app/assets/stylesheets/framework/layout.scss39
-rw-r--r--app/assets/stylesheets/framework/lists.scss12
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss4
-rw-r--r--app/assets/stylesheets/framework/mobile.scss2
-rw-r--r--app/assets/stylesheets/framework/nav.scss22
-rw-r--r--app/assets/stylesheets/framework/page-header.scss3
-rw-r--r--app/assets/stylesheets/framework/panels.scss14
-rw-r--r--app/assets/stylesheets/framework/selects.scss4
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss2
-rw-r--r--app/assets/stylesheets/framework/snippets.scss (renamed from app/assets/stylesheets/pages/snippets.scss)26
-rw-r--r--app/assets/stylesheets/framework/tables.scss4
-rw-r--r--app/assets/stylesheets/framework/timeline.scss2
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss2
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss10
-rw-r--r--app/assets/stylesheets/framework/variables.scss73
-rw-r--r--app/assets/stylesheets/framework/wells.scss4
-rw-r--r--app/assets/stylesheets/framework/zen.scss4
-rw-r--r--app/assets/stylesheets/highlight/white.scss6
-rw-r--r--app/assets/stylesheets/mailers/highlighted_diff_email.scss6
-rw-r--r--app/assets/stylesheets/pages/boards.scss4
-rw-r--r--app/assets/stylesheets/pages/builds.scss8
-rw-r--r--app/assets/stylesheets/pages/commits.scss8
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss1
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss1
-rw-r--r--app/assets/stylesheets/pages/diff.scss8
-rw-r--r--app/assets/stylesheets/pages/editor.scss59
-rw-r--r--app/assets/stylesheets/pages/environments.scss30
-rw-r--r--app/assets/stylesheets/pages/events.scss2
-rw-r--r--app/assets/stylesheets/pages/explore.scss8
-rw-r--r--app/assets/stylesheets/pages/groups.scss6
-rw-r--r--app/assets/stylesheets/pages/help.scss2
-rw-r--r--app/assets/stylesheets/pages/issuable.scss61
-rw-r--r--app/assets/stylesheets/pages/issues.scss8
-rw-r--r--app/assets/stylesheets/pages/labels.scss7
-rw-r--r--app/assets/stylesheets/pages/members.scss31
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss6
-rw-r--r--app/assets/stylesheets/pages/milestone.scss2
-rw-r--r--app/assets/stylesheets/pages/note_form.scss4
-rw-r--r--app/assets/stylesheets/pages/notes.scss24
-rw-r--r--app/assets/stylesheets/pages/notifications.scss14
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss607
-rw-r--r--app/assets/stylesheets/pages/profile.scss20
-rw-r--r--app/assets/stylesheets/pages/projects.scss33
-rw-r--r--app/assets/stylesheets/pages/search.scss6
-rw-r--r--app/assets/stylesheets/pages/settings.scss2
-rw-r--r--app/assets/stylesheets/pages/status.scss36
-rw-r--r--app/assets/stylesheets/pages/tree.scss13
-rw-r--r--app/assets/stylesheets/pages/xterm.scss2
-rw-r--r--app/controllers/admin/applications_controller.rb5
-rw-r--r--app/controllers/application_controller.rb8
-rw-r--r--app/controllers/concerns/oauth_applications.rb19
-rw-r--r--app/controllers/concerns/service_params.rb2
-rw-r--r--app/controllers/groups/group_members_controller.rb12
-rw-r--r--app/controllers/import/bitbucket_controller.rb83
-rw-r--r--app/controllers/import/gitea_controller.rb45
-rw-r--r--app/controllers/import/github_controller.rb95
-rw-r--r--app/controllers/jwt_controller.rb2
-rw-r--r--app/controllers/oauth/applications_controller.rb2
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb12
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb8
-rw-r--r--app/controllers/projects/autocomplete_sources_controller.rb48
-rw-r--r--app/controllers/projects/blame_controller.rb3
-rw-r--r--app/controllers/projects/environments_controller.rb33
-rw-r--r--app/controllers/projects/notes_controller.rb2
-rw-r--r--app/controllers/projects/project_members_controller.rb9
-rw-r--r--app/controllers/projects/releases_controller.rb9
-rw-r--r--app/controllers/projects/snippets_controller.rb8
-rw-r--r--app/controllers/projects_controller.rb33
-rw-r--r--app/controllers/sessions_controller.rb2
-rw-r--r--app/finders/issues_finder.rb18
-rw-r--r--app/finders/notes_finder.rb113
-rw-r--r--app/finders/snippets_finder.rb42
-rw-r--r--app/helpers/blob_helper.rb4
-rw-r--r--app/helpers/button_helper.rb2
-rw-r--r--app/helpers/ci_status_helper.rb20
-rw-r--r--app/helpers/environment_helper.rb6
-rw-r--r--app/helpers/form_helper.rb12
-rw-r--r--app/helpers/groups_helper.rb11
-rw-r--r--app/helpers/import_helper.rb10
-rw-r--r--app/helpers/members_helper.rb8
-rw-r--r--app/helpers/merge_requests_helper.rb4
-rw-r--r--app/helpers/nav_helper.rb18
-rw-r--r--app/helpers/projects_helper.rb4
-rw-r--r--app/helpers/snippets_helper.rb11
-rw-r--r--app/helpers/sorting_helper.rb59
-rw-r--r--app/helpers/tab_helper.rb6
-rw-r--r--app/models/ci/build.rb52
-rw-r--r--app/models/ci/pipeline.rb24
-rw-r--r--app/models/ci/stage.rb6
-rw-r--r--app/models/commit.rb12
-rw-r--r--app/models/commit_status.rb6
-rw-r--r--app/models/concerns/milestoneish.rb2
-rw-r--r--app/models/concerns/reactive_caching.rb114
-rw-r--r--app/models/concerns/routable.rb7
-rw-r--r--app/models/concerns/token_authenticatable.rb4
-rw-r--r--app/models/environment.rb65
-rw-r--r--app/models/group.rb2
-rw-r--r--app/models/issue.rb55
-rw-r--r--app/models/member.rb37
-rw-r--r--app/models/merge_request.rb40
-rw-r--r--app/models/namespace.rb16
-rw-r--r--app/models/network/graph.rb4
-rw-r--r--app/models/note.rb17
-rw-r--r--app/models/personal_access_token.rb2
-rw-r--r--app/models/project.rb24
-rw-r--r--app/models/project_services/buildkite_service.rb3
-rw-r--r--app/models/project_services/chat_message/base_message.rb (renamed from app/models/project_services/slack_service/base_message.rb)2
-rw-r--r--app/models/project_services/chat_message/build_message.rb (renamed from app/models/project_services/slack_service/build_message.rb)2
-rw-r--r--app/models/project_services/chat_message/issue_message.rb (renamed from app/models/project_services/slack_service/issue_message.rb)2
-rw-r--r--app/models/project_services/chat_message/merge_message.rb (renamed from app/models/project_services/slack_service/merge_message.rb)2
-rw-r--r--app/models/project_services/chat_message/note_message.rb (renamed from app/models/project_services/slack_service/note_message.rb)2
-rw-r--r--app/models/project_services/chat_message/pipeline_message.rb (renamed from app/models/project_services/slack_service/pipeline_message.rb)4
-rw-r--r--app/models/project_services/chat_message/push_message.rb (renamed from app/models/project_services/slack_service/push_message.rb)2
-rw-r--r--app/models/project_services/chat_message/wiki_page_message.rb (renamed from app/models/project_services/slack_service/wiki_page_message.rb)2
-rw-r--r--app/models/project_services/chat_notification_service.rb (renamed from app/models/project_services/slack_service.rb)73
-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.rb33
-rw-r--r--app/models/project_services/drone_ci_service.rb3
-rw-r--r--app/models/project_services/emails_on_push_service.rb18
-rw-r--r--app/models/project_services/hipchat_service.rb6
-rw-r--r--app/models/project_services/irker_service.rb3
-rw-r--r--app/models/project_services/issue_tracker_service.rb4
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--app/models/project_services/kubernetes_service.rb173
-rw-r--r--app/models/project_services/mattermost_notification_service.rb41
-rw-r--r--app/models/project_services/mattermost_slash_commands_service.rb30
-rw-r--r--app/models/project_services/slack_notification_service.rb40
-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.rb12
-rw-r--r--app/policies/group_member_policy.rb6
-rw-r--r--app/policies/group_policy.rb6
-rw-r--r--app/policies/note_policy.rb2
-rw-r--r--app/policies/project_policy.rb12
-rw-r--r--app/serializers/environment_entity.rb8
-rw-r--r--app/serializers/request_aware_entity.rb4
-rw-r--r--app/services/access_token_validation_service.rb32
-rw-r--r--app/services/ci/create_pipeline_builds_service.rb15
-rw-r--r--app/services/ci/image_for_build_service.rb8
-rw-r--r--app/services/commits/change_service.rb2
-rw-r--r--app/services/groups/update_service.rb2
-rw-r--r--app/services/issuable_base_service.rb7
-rw-r--r--app/services/issues/update_service.rb2
-rw-r--r--app/services/merge_requests/build_service.rb2
-rw-r--r--app/services/merge_requests/update_service.rb2
-rw-r--r--app/services/oauth2/access_token_validation_service.rb42
-rw-r--r--app/services/projects/import_service.rb16
-rw-r--r--app/services/projects/update_service.rb2
-rw-r--r--app/uploaders/artifact_uploader.rb10
-rw-r--r--app/uploaders/attachment_uploader.rb2
-rw-r--r--app/uploaders/avatar_uploader.rb2
-rw-r--r--app/uploaders/file_uploader.rb2
-rw-r--r--app/uploaders/gitlab_uploader.rb11
-rw-r--r--app/uploaders/lfs_object_uploader.rb10
-rw-r--r--app/views/admin/applications/_form.html.haml6
-rw-r--r--app/views/admin/applications/show.html.haml6
-rw-r--r--app/views/admin/dashboard/_head.html.haml2
-rw-r--r--app/views/admin/dashboard/index.html.haml2
-rw-r--r--app/views/admin/groups/_group.html.haml4
-rw-r--r--app/views/admin/groups/show.html.haml6
-rw-r--r--app/views/admin/logs/show.html.haml2
-rw-r--r--app/views/admin/projects/index.html.haml12
-rw-r--r--app/views/admin/runners/show.html.haml2
-rw-r--r--app/views/ci/status/_badge.html.haml11
-rw-r--r--app/views/ci/status/_graph_badge.html.haml20
-rw-r--r--app/views/dashboard/_activity_head.html.haml4
-rw-r--r--app/views/dashboard/_snippets_head.html.haml20
-rw-r--r--app/views/dashboard/snippets/index.html.haml42
-rw-r--r--app/views/dashboard/todos/index.html.haml10
-rw-r--r--app/views/doorkeeper/applications/_form.html.haml4
-rw-r--r--app/views/doorkeeper/applications/show.html.haml5
-rw-r--r--app/views/explore/_head.html.haml4
-rw-r--r--app/views/explore/snippets/index.html.haml10
-rw-r--r--app/views/groups/group_members/index.html.haml1
-rw-r--r--app/views/groups/issues.html.haml6
-rw-r--r--app/views/groups/merge_requests.html.haml5
-rw-r--r--app/views/import/_githubish_status.html.haml61
-rw-r--r--app/views/import/bitbucket/status.html.haml45
-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/_init_auto_complete.html.haml14
-rw-r--r--app/views/layouts/_page.html.haml7
-rw-r--r--app/views/profiles/personal_access_tokens/_form.html.haml21
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml17
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml2
-rw-r--r--app/views/projects/_home_panel.html.haml16
-rw-r--r--app/views/projects/blob/_editor.html.haml2
-rw-r--r--app/views/projects/builds/_header.html.haml2
-rw-r--r--app/views/projects/builds/show.html.haml3
-rw-r--r--app/views/projects/buttons/_download.html.haml2
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml2
-rw-r--r--app/views/projects/buttons/_koding.html.haml10
-rw-r--r--app/views/projects/ci/builds/_build.html.haml5
-rw-r--r--app/views/projects/ci/builds/_build_pipeline.html.haml13
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml5
-rw-r--r--app/views/projects/commit/_change.html.haml9
-rw-r--r--app/views/projects/commit/_commit_box.html.haml9
-rw-r--r--app/views/projects/commit/_pipeline.html.haml2
-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/generic_commit_statuses/_generic_commit_status.html.haml5
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml10
-rw-r--r--app/views/projects/issues/show.html.haml1
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml4
-rw-r--r--app/views/projects/merge_requests/_show.html.haml5
-rw-r--r--app/views/projects/merge_requests/show/_commits.html.haml4
-rw-r--r--app/views/projects/merge_requests/widget/_open.html.haml19
-rw-r--r--app/views/projects/merge_requests/widget/open/_accept.html.haml2
-rw-r--r--app/views/projects/new.html.haml5
-rw-r--r--app/views/projects/notes/_note.html.haml2
-rw-r--r--app/views/projects/pipelines/_info.html.haml2
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml2
-rw-r--r--app/views/projects/pipelines/index.html.haml8
-rw-r--r--app/views/projects/project_members/index.html.haml1
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_help.html.haml5
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml93
-rw-r--r--app/views/projects/show.html.haml17
-rw-r--r--app/views/projects/snippets/_actions.html.haml14
-rw-r--r--app/views/projects/snippets/index.html.haml20
-rw-r--r--app/views/projects/snippets/show.html.haml5
-rw-r--r--app/views/projects/stage/_graph.html.haml12
-rw-r--r--app/views/projects/stage/_in_stage_group.html.haml10
-rw-r--r--app/views/projects/tree/_tree_content.html.haml9
-rw-r--r--app/views/repository_check_mailer/notify.html.haml2
-rw-r--r--app/views/repository_check_mailer/notify.text.haml2
-rw-r--r--app/views/search/results/_snippet_title.html.haml2
-rw-r--r--app/views/shared/_commit_message_container.html.haml9
-rw-r--r--app/views/shared/_milestones_filter.html.haml6
-rw-r--r--app/views/shared/builds/_tabs.html.haml8
-rw-r--r--app/views/shared/groups/_group.html.haml2
-rw-r--r--app/views/shared/icons/_go_logo.svg.erb1
-rwxr-xr-x[-rw-r--r--]app/views/shared/icons/_icon_status_canceled.svg2
-rwxr-xr-x[-rw-r--r--]app/views/shared/icons/_icon_status_created.svg2
-rwxr-xr-x[-rw-r--r--]app/views/shared/icons/_icon_status_failed.svg2
-rwxr-xr-xapp/views/shared/icons/_icon_status_manual.svg1
-rwxr-xr-x[-rw-r--r--]app/views/shared/icons/_icon_status_pending.svg2
-rwxr-xr-x[-rw-r--r--]app/views/shared/icons/_icon_status_running.svg2
-rwxr-xr-x[-rw-r--r--]app/views/shared/icons/_icon_status_skipped.svg2
-rwxr-xr-x[-rw-r--r--]app/views/shared/icons/_icon_status_success.svg2
-rwxr-xr-x[-rw-r--r--]app/views/shared/icons/_icon_status_warning.svg2
-rw-r--r--app/views/shared/icons/_icon_terminal.svg1
-rw-r--r--app/views/shared/issuable/_nav.html.haml10
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/members/_access_request_buttons.html.haml29
-rw-r--r--app/views/shared/members/_sort_dropdown.html.haml9
-rw-r--r--app/views/shared/milestones/_issuables.html.haml8
-rw-r--r--app/views/shared/notifications/_button.html.haml2
-rw-r--r--app/views/shared/snippets/_header.html.haml12
-rw-r--r--app/views/shared/snippets/_snippet.html.haml31
-rw-r--r--app/views/shared/tokens/_scopes_form.html.haml9
-rw-r--r--app/views/shared/tokens/_scopes_list.html.haml13
-rw-r--r--app/views/snippets/_actions.html.haml14
-rw-r--r--app/views/snippets/_snippets.html.haml3
-rw-r--r--app/views/snippets/_snippets_scope_menu.html.haml31
-rw-r--r--app/views/snippets/show.html.haml4
-rw-r--r--app/workers/reactive_caching_worker.rb15
-rwxr-xr-xbin/changelog11
-rw-r--r--changelogs/unreleased/18435-autocomplete-is-not-performant.yml4
-rw-r--r--changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml4
-rw-r--r--changelogs/unreleased/20052-actions-table-vscroll.yml4
-rw-r--r--changelogs/unreleased/20492-access-token-scopes.yml4
-rw-r--r--changelogs/unreleased/22348-gitea-importer.yml4
-rw-r--r--changelogs/unreleased/22604-manual-actions.yml4
-rw-r--r--changelogs/unreleased/22849-ci-build-ref-slug.yml4
-rw-r--r--changelogs/unreleased/22864-add-environment-slug.yml4
-rw-r--r--changelogs/unreleased/22864-kubernetes-service.yml4
-rw-r--r--changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml5
-rw-r--r--changelogs/unreleased/23573-sort-functionality-for-project-member.yml4
-rw-r--r--changelogs/unreleased/24803-change-cursor-for-ca-stages.yml5
-rw-r--r--changelogs/unreleased/24824-dropdown-items-focus.yml4
-rw-r--r--changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml4
-rw-r--r--changelogs/unreleased/24927-custom-event-polyfill-test.yml4
-rw-r--r--changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml4
-rw-r--r--changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml4
-rw-r--r--changelogs/unreleased/25136-last-deployment-link.yml4
-rw-r--r--changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml4
-rw-r--r--changelogs/unreleased/25171-fix-mr-features-settings-hidden-when-builds-are-disabled.yml4
-rw-r--r--changelogs/unreleased/25207-text-overflow-env-table.yml4
-rw-r--r--changelogs/unreleased/25301-git-2-11-force-push-bug.yml4
-rw-r--r--changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml4
-rw-r--r--changelogs/unreleased/25482-fix-api-sudo.yml4
-rw-r--r--changelogs/unreleased/25483-broken-tabs.yml4
-rw-r--r--changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml4
-rw-r--r--changelogs/unreleased/25617-todos-filter-placeholder.yml4
-rw-r--r--changelogs/unreleased/25740-fix-new-branch-button-padding.yml4
-rw-r--r--changelogs/unreleased/25743-clean-up-css-for-project-alerts-and-flash-notifications.yml4
-rw-r--r--changelogs/unreleased/8003-katex-math.yml4
-rw-r--r--changelogs/unreleased/abuse_report-fixture.yml4
-rw-r--r--changelogs/unreleased/add_info_to_qr.yml4
-rw-r--r--changelogs/unreleased/allow-more-filenames.yml4
-rw-r--r--changelogs/unreleased/api-cherry-pick.yml4
-rw-r--r--changelogs/unreleased/api-simple-group-project.yml4
-rw-r--r--changelogs/unreleased/awards_handler.yml4
-rw-r--r--changelogs/unreleased/bitbucket-oauth2.yml4
-rw-r--r--changelogs/unreleased/change_development_build_fixtures.yml4
-rw-r--r--changelogs/unreleased/chomp-git-status-message.yml5
-rw-r--r--changelogs/unreleased/dockerfile-templates.yml4
-rw-r--r--changelogs/unreleased/dz-fix-route-rename.yml4
-rw-r--r--changelogs/unreleased/expose-deployment-variables.yml4
-rw-r--r--changelogs/unreleased/file-template-dropwdown-proper-position.yml4
-rw-r--r--changelogs/unreleased/fix-blame-500.yml4
-rw-r--r--changelogs/unreleased/fix-import-export-build-token.yml4
-rw-r--r--changelogs/unreleased/fix-import-export-ee-services.yml4
-rw-r--r--changelogs/unreleased/fix-import-export-mr-error.yml4
-rw-r--r--changelogs/unreleased/fix-milestone-summary.yml4
-rw-r--r--changelogs/unreleased/fix-slack-pipeline-message-by-api.yml4
-rw-r--r--changelogs/unreleased/fix-yaml-variables.yml4
-rw-r--r--changelogs/unreleased/gem-update-grape.yml4
-rw-r--r--changelogs/unreleased/gitlab-workhorse-multipart.yml4
-rw-r--r--changelogs/unreleased/group-members-in-project-members-view.yml4
-rw-r--r--changelogs/unreleased/issue_13270.yml4
-rw-r--r--changelogs/unreleased/issue_22269.yml4
-rw-r--r--changelogs/unreleased/issue_24020.yml4
-rw-r--r--changelogs/unreleased/issue_25030.yml4
-rw-r--r--changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml4
-rw-r--r--changelogs/unreleased/jej-note-search-uses-finder.yml4
-rw-r--r--changelogs/unreleased/ldap_maint_task.yml4
-rw-r--r--changelogs/unreleased/leave-project-btn.yml4
-rw-r--r--changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.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/pipeline-build-hitbox.yml4
-rw-r--r--changelogs/unreleased/process-commit-worker-migration-encoding.yml4
-rw-r--r--changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml4
-rw-r--r--changelogs/unreleased/rounded-labels-fixes.yml4
-rw-r--r--changelogs/unreleased/seed-runner-token.yml5
-rw-r--r--changelogs/unreleased/show-commit-status-from-latest-pipeline.yml4
-rw-r--r--changelogs/unreleased/timeago-perf-fix.yml4
-rw-r--r--changelogs/unreleased/unescape-relative-path.yml4
-rw-r--r--changelogs/unreleased/username-exists-root.yml4
-rw-r--r--changelogs/unreleased/zj-slack-slash-commands.yml4
-rw-r--r--config/application.rb8
-rw-r--r--config/gitlab.yml.example6
-rw-r--r--config/initializers/1_settings.rb11
-rw-r--r--config/initializers/doorkeeper.rb4
-rw-r--r--config/initializers/math_lexer.rb2
-rw-r--r--config/initializers/omniauth.rb6
-rw-r--r--config/initializers/public_key.rb2
-rw-r--r--config/initializers/sidekiq.rb2
-rw-r--r--config/initializers/workhorse_multipart.rb3
-rw-r--r--config/locales/doorkeeper.en.yml1
-rw-r--r--config/routes.rb1
-rw-r--r--config/routes/admin.rb23
-rw-r--r--config/routes/import.rb6
-rw-r--r--config/routes/project.rb15
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--db/fixtures/development/14_pipelines.rb65
-rw-r--r--db/fixtures/production/010_settings.rb16
-rw-r--r--db/migrate/20141006143943_move_slack_service_to_webhook.rb6
-rw-r--r--db/migrate/20160823083941_add_column_scopes_to_personal_access_tokens.rb19
-rw-r--r--db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb20
-rw-r--r--db/migrate/20161130095245_fill_routes_table.rb2
-rw-r--r--db/migrate/20161130101252_fill_projects_routes_table.rb22
-rw-r--r--db/migrate/20161202152031_remove_duplicates_from_routes.rb29
-rw-r--r--db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb36
-rw-r--r--db/migrate/20161206153751_add_path_index_to_namespace.rb20
-rw-r--r--db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb36
-rw-r--r--db/migrate/20161206153754_add_name_index_to_namespace.rb20
-rw-r--r--db/migrate/20161207231620_fixup_environment_name_uniqueness.rb53
-rw-r--r--db/migrate/20161207231621_create_environment_name_unique_index.rb18
-rw-r--r--db/migrate/20161207231626_add_environment_slug.rb60
-rw-r--r--db/migrate/20161209153400_add_unique_index_for_environment_slug.rb15
-rw-r--r--db/migrate/20161212142807_add_lower_path_index_to_routes.rb22
-rw-r--r--db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb14
-rw-r--r--db/post_migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb19
-rw-r--r--db/schema.rb21
-rw-r--r--doc/README.md3
-rw-r--r--doc/administration/auth/README.md3
-rw-r--r--doc/administration/auth/img/okta_admin_panel.pngbin0 -> 26164 bytes
-rw-r--r--doc/administration/auth/img/okta_saml_settings.pngbin0 -> 25470 bytes
-rw-r--r--doc/administration/auth/okta.md160
-rw-r--r--doc/administration/custom_hooks.md22
-rw-r--r--doc/administration/environment_variables.md23
-rw-r--r--doc/administration/high_availability/database.md6
-rw-r--r--doc/administration/high_availability/load_balancer.md26
-rw-r--r--doc/administration/high_availability/redis.md8
-rw-r--r--doc/administration/integration/terminal.md73
-rw-r--r--doc/administration/logs.md36
-rw-r--r--doc/administration/raketasks/check.md23
-rw-r--r--doc/administration/raketasks/ldap.md120
-rw-r--r--doc/api/commits.md39
-rw-r--r--doc/api/enviroments.md8
-rw-r--r--doc/api/groups.md17
-rw-r--r--doc/api/projects.md1
-rw-r--r--doc/api/services.md150
-rw-r--r--doc/ci/environments.md101
-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/ci/variables/README.md21
-rw-r--r--doc/ci/yaml/README.md48
-rw-r--r--doc/development/performance.md2
-rw-r--r--doc/development/ux_guide/basics.md16
-rw-r--r--doc/development/ux_guide/img/cursors-default.pngbin0 -> 567 bytes
-rw-r--r--doc/development/ux_guide/img/cursors-ibeam.pngbin0 -> 383 bytes
-rw-r--r--doc/development/ux_guide/img/cursors-move.pngbin0 -> 276 bytes
-rw-r--r--doc/development/ux_guide/img/cursors-panclosed.pngbin0 -> 483 bytes
-rw-r--r--doc/development/ux_guide/img/cursors-panopened.pngbin0 -> 622 bytes
-rw-r--r--doc/development/ux_guide/img/cursors-pointer.pngbin0 -> 574 bytes
-rw-r--r--doc/integration/bitbucket.md118
-rw-r--r--doc/integration/img/bitbucket_oauth_settings_page.pngbin30081 -> 28719 bytes
-rw-r--r--doc/project_services/img/kubernetes_configuration.pngbin0 -> 113827 bytes
-rw-r--r--doc/project_services/img/mattermost_configuration.pngbin0 -> 73502 bytes
-rw-r--r--doc/project_services/kubernetes.md63
-rw-r--r--doc/project_services/mattermost.md45
-rw-r--r--doc/project_services/mattermost_slash_commands.md18
-rw-r--r--doc/project_services/project_services.md4
-rw-r--r--doc/project_services/slack.md4
-rw-r--r--doc/raketasks/README.md3
-rw-r--r--doc/update/8.13-to-8.14.md2
-rw-r--r--doc/update/8.14-to-8.15.md2
-rw-r--r--doc/user/markdown.md37
-rw-r--r--doc/user/permissions.md1
-rw-r--r--doc/workflow/importing/README.md2
-rw-r--r--doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.pngbin30083 -> 0 bytes
-rw-r--r--doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.pngbin16502 -> 0 bytes
-rw-r--r--doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.pngbin46606 -> 0 bytes
-rw-r--r--doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.pngbin15288 -> 0 bytes
-rw-r--r--doc/workflow/importing/img/bitbucket_import_grant_access.pngbin0 -> 7248 bytes
-rw-r--r--doc/workflow/importing/img/bitbucket_import_new_project.pngbin0 -> 1316 bytes
-rw-r--r--doc/workflow/importing/img/bitbucket_import_select_project.pngbin0 -> 8688 bytes
-rw-r--r--doc/workflow/importing/img/import_projects_from_gitea_new_import.pngbin0 -> 15561 bytes
-rw-r--r--doc/workflow/importing/img/import_projects_from_github_new_project_page.pngbin11047 -> 0 bytes
-rw-r--r--doc/workflow/importing/img/import_projects_from_github_select_auth_method.pngbin17613 -> 17612 bytes
-rw-r--r--doc/workflow/importing/img/import_projects_from_new_project_page.pngbin0 -> 36821 bytes
-rw-r--r--doc/workflow/importing/import_projects_from_bitbucket.md62
-rw-r--r--doc/workflow/importing/import_projects_from_gitea.md80
-rw-r--r--doc/workflow/importing/import_projects_from_github.md10
-rw-r--r--features/admin/active_tab.feature54
-rw-r--r--features/admin/appearance.feature37
-rw-r--r--features/admin/applications.feature18
-rw-r--r--features/admin/broadcast_messages.feature33
-rw-r--r--features/admin/labels.feature38
-rw-r--r--features/admin/projects.feature47
-rw-r--r--features/admin/settings.feature19
-rw-r--r--features/project/service.feature8
-rw-r--r--features/steps/admin/active_tab.rb41
-rw-r--r--features/steps/admin/appearance.rb72
-rw-r--r--features/steps/admin/applications.rb55
-rw-r--r--features/steps/admin/broadcast_messages.rb66
-rw-r--r--features/steps/admin/labels.rb117
-rw-r--r--features/steps/admin/projects.rb104
-rw-r--r--features/steps/group/milestones.rb2
-rw-r--r--features/steps/project/services.rb8
-rw-r--r--features/steps/project/snippets.rb2
-rw-r--r--features/steps/shared/authentication.rb2
-rw-r--r--features/steps/shared/paths.rb14
-rw-r--r--lib/api/api.rb2
-rw-r--r--lib/api/api_guard.rb50
-rw-r--r--lib/api/commits.rb36
-rw-r--r--lib/api/entities.rb2
-rw-r--r--lib/api/environments.rb3
-rw-r--r--lib/api/groups.rb5
-rw-r--r--lib/api/helpers.rb135
-rw-r--r--lib/api/helpers/custom_validators.rb14
-rw-r--r--lib/api/helpers/internal_helpers.rb8
-rw-r--r--lib/api/internal.rb6
-rw-r--r--lib/api/merge_requests.rb4
-rw-r--r--lib/api/services.rb669
-rw-r--r--lib/api/templates.rb12
-rw-r--r--lib/api/users.rb7
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb2
-rw-r--r--lib/banzai/filter/math_filter.rb51
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb1
-rw-r--r--lib/bitbucket/client.rb58
-rw-r--r--lib/bitbucket/collection.rb21
-rw-r--r--lib/bitbucket/connection.rb69
-rw-r--r--lib/bitbucket/error/unauthorized.rb6
-rw-r--r--lib/bitbucket/page.rb34
-rw-r--r--lib/bitbucket/paginator.rb36
-rw-r--r--lib/bitbucket/representation/base.rb17
-rw-r--r--lib/bitbucket/representation/comment.rb27
-rw-r--r--lib/bitbucket/representation/issue.rb53
-rw-r--r--lib/bitbucket/representation/pull_request.rb65
-rw-r--r--lib/bitbucket/representation/pull_request_comment.rb39
-rw-r--r--lib/bitbucket/representation/repo.rb71
-rw-r--r--lib/bitbucket/representation/user.rb9
-rw-r--r--lib/ci/api/builds.rb19
-rw-r--r--lib/ci/api/helpers.rb15
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb2
-rw-r--r--lib/gitlab/allowable.rb7
-rw-r--r--lib/gitlab/asciidoc.rb27
-rw-r--r--lib/gitlab/auth.rb25
-rw-r--r--lib/gitlab/badge/build/status.rb4
-rw-r--r--lib/gitlab/bitbucket_import.rb6
-rw-r--r--lib/gitlab/bitbucket_import/client.rb142
-rw-r--r--lib/gitlab/bitbucket_import/importer.rb255
-rw-r--r--lib/gitlab/bitbucket_import/key_adder.rb24
-rw-r--r--lib/gitlab/bitbucket_import/key_deleter.rb23
-rw-r--r--lib/gitlab/bitbucket_import/project_creator.rb28
-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/issue_create.rb2
-rw-r--r--lib/gitlab/chat_commands/presenter.rb (renamed from lib/mattermost/presenter.rb)10
-rw-r--r--lib/gitlab/checks/change_access.rb5
-rw-r--r--lib/gitlab/checks/force_push.rb11
-rw-r--r--lib/gitlab/ci/status/build/cancelable.rb37
-rw-r--r--lib/gitlab/ci/status/build/common.rb19
-rw-r--r--lib/gitlab/ci/status/build/factory.rb18
-rw-r--r--lib/gitlab/ci/status/build/play.rb57
-rw-r--r--lib/gitlab/ci/status/build/retryable.rb37
-rw-r--r--lib/gitlab/ci/status/build/stop.rb53
-rw-r--r--lib/gitlab/ci/status/core.rb38
-rw-r--r--lib/gitlab/ci/status/extended.rb8
-rw-r--r--lib/gitlab/ci/status/factory.rb30
-rw-r--r--lib/gitlab/ci/status/pipeline/common.rb8
-rw-r--r--lib/gitlab/ci/status/pipeline/factory.rb8
-rw-r--r--lib/gitlab/ci/status/pipeline/success_with_warnings.rb6
-rw-r--r--lib/gitlab/ci/status/stage/common.rb10
-rw-r--r--lib/gitlab/ci/status/stage/factory.rb6
-rw-r--r--lib/gitlab/current_settings.rb2
-rw-r--r--lib/gitlab/email/reply_parser.rb2
-rw-r--r--lib/gitlab/gfm/uploads_rewriter.rb19
-rw-r--r--lib/gitlab/git/rev_list.rb42
-rw-r--r--lib/gitlab/git_access.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/gon_helper.rb2
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb2
-rw-r--r--lib/gitlab/import_export/relation_factory.rb11
-rw-r--r--lib/gitlab/import_sources.rb39
-rw-r--r--lib/gitlab/kubernetes.rb80
-rw-r--r--lib/gitlab/middleware/multipart.rb99
-rw-r--r--lib/gitlab/popen.rb4
-rw-r--r--lib/gitlab/project_search_results.rb2
-rw-r--r--lib/gitlab/regex.rb21
-rw-r--r--lib/gitlab/routing.rb6
-rw-r--r--lib/gitlab/serialize/ci/variables.rb27
-rw-r--r--lib/gitlab/sql/union.rb4
-rw-r--r--lib/gitlab/template/dockerfile_template.rb30
-rw-r--r--lib/gitlab/workhorse.rb19
-rw-r--r--lib/mattermost/session.rb115
-rw-r--r--lib/omniauth/strategies/bitbucket.rb41
-rw-r--r--lib/rouge/lexers/math.rb21
-rw-r--r--lib/support/nginx/gitlab7
-rw-r--r--lib/support/nginx/gitlab-ssl8
-rw-r--r--lib/tasks/gitlab/ldap.rake40
-rw-r--r--lib/tasks/migrate/setup_postgresql.rake4
-rw-r--r--package.json2
-rw-r--r--public/404.html9
-rw-r--r--public/422.html9
-rw-r--r--public/500.html9
-rw-r--r--public/502.html9
-rw-r--r--public/503.html9
-rw-r--r--spec/controllers/import/bitbucket_controller_spec.rb52
-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/profiles/personal_access_tokens_spec.rb49
-rw-r--r--spec/controllers/projects/blame_controller_spec.rb5
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb69
-rw-r--r--spec/controllers/projects/releases_controller_spec.rb55
-rw-r--r--spec/controllers/search_controller_spec.rb61
-rw-r--r--spec/db/production/settings.rb16
-rw-r--r--spec/factories/ci/builds.rb15
-rw-r--r--spec/factories/groups.rb4
-rw-r--r--spec/factories/notes.rb2
-rw-r--r--spec/factories/personal_access_tokens.rb1
-rw-r--r--spec/factories/projects.rb23
-rw-r--r--spec/features/admin/admin_active_tab_spec.rb90
-rw-r--r--spec/features/admin/admin_appearance_spec.rb76
-rw-r--r--spec/features/admin/admin_broadcast_messages_spec.rb51
-rw-r--r--spec/features/admin/admin_deploy_keys_spec.rb29
-rw-r--r--spec/features/admin/admin_groups_spec.rb2
-rw-r--r--spec/features/admin/admin_labels_spec.rb99
-rw-r--r--spec/features/admin/admin_manage_applications_spec.rb36
-rw-r--r--spec/features/admin/admin_projects_spec.rb105
-rw-r--r--spec/features/admin/admin_settings_spec.rb (renamed from features/steps/admin/settings.rb)61
-rw-r--r--spec/features/commits_spec.rb2
-rw-r--r--spec/features/dashboard/datetime_on_tooltips_spec.rb2
-rw-r--r--spec/features/environment_spec.rb32
-rw-r--r--spec/features/environments_spec.rb28
-rw-r--r--spec/features/groups/members/sorting_spec.rb98
-rw-r--r--spec/features/issues/bulk_assignment_labels_spec.rb42
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb71
-rw-r--r--spec/features/merge_requests/closes_issues_spec.rb55
-rw-r--r--spec/features/merge_requests/merge_commit_message_toggle_spec.rb74
-rw-r--r--spec/features/participants_autocomplete_spec.rb7
-rw-r--r--spec/features/profiles/personal_access_tokens_spec.rb25
-rw-r--r--spec/features/projects/builds_spec.rb37
-rw-r--r--spec/features/projects/files/creating_a_file_spec.rb44
-rw-r--r--spec/features/projects/files/dockerfile_dropdown_spec.rb30
-rw-r--r--spec/features/projects/gfm_autocomplete_load_spec.rb4
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin681774 -> 679415 bytes
-rw-r--r--spec/features/projects/members/sorting_spec.rb98
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb86
-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/features/security/admin_access_spec.rb2
-rw-r--r--spec/finders/issues_finder_spec.rb71
-rw-r--r--spec/finders/notes_finder_spec.rb200
-rw-r--r--spec/finders/snippets_finder_spec.rb25
-rw-r--r--spec/helpers/application_helper_spec.rb14
-rw-r--r--spec/helpers/groups_helper_spec.rb2
-rw-r--r--spec/helpers/import_helper_spec.rb33
-rw-r--r--spec/javascripts/abuse_reports_spec.js.es638
-rw-r--r--spec/javascripts/activities_spec.js.es63
-rw-r--r--spec/javascripts/awards_handler_spec.js13
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js2
-rw-r--r--spec/javascripts/boards/boards_store_spec.js.es69
-rw-r--r--spec/javascripts/boards/issue_spec.js.es65
-rw-r--r--spec/javascripts/boards/list_spec.js.es68
-rw-r--r--spec/javascripts/boards/mock_data.js.es63
-rw-r--r--spec/javascripts/dashboard_spec.js.es63
-rw-r--r--spec/javascripts/datetime_utility_spec.js.es62
-rw-r--r--spec/javascripts/diff_comments_store_spec.js.es65
-rw-r--r--spec/javascripts/extensions/object_spec.js.es625
-rw-r--r--spec/javascripts/fixtures/abuse_reports.html.haml16
-rw-r--r--spec/javascripts/fixtures/abuse_reports.rb27
-rw-r--r--spec/javascripts/fixtures/awards_handler.html.haml52
-rw-r--r--spec/javascripts/fixtures/pipeline_graph.html.haml14
-rw-r--r--spec/javascripts/gl_dropdown_spec.js.es64
-rw-r--r--spec/javascripts/gl_field_errors_spec.js.es63
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_graph_spec.js6
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_util_spec.js4
-rw-r--r--spec/javascripts/graphs/stat_graph_spec.js4
-rw-r--r--spec/javascripts/issue_spec.js5
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js.es65
-rw-r--r--spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es643
-rw-r--r--spec/javascripts/line_highlighter_spec.js3
-rw-r--r--spec/javascripts/merge_request_spec.js3
-rw-r--r--spec/javascripts/new_branch_spec.js3
-rw-r--r--spec/javascripts/notes_spec.js4
-rw-r--r--spec/javascripts/pipelines_spec.js.es625
-rw-r--r--spec/javascripts/project_title_spec.js20
-rw-r--r--spec/javascripts/right_sidebar_spec.js3
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js3
-rw-r--r--spec/javascripts/subbable_resource_spec.js.es63
-rw-r--r--spec/javascripts/u2f/authenticate_spec.js4
-rw-r--r--spec/javascripts/u2f/register_spec.js4
-rw-r--r--spec/javascripts/zen_mode_spec.js5
-rw-r--r--spec/lib/banzai/filter/math_filter_spec.rb120
-rw-r--r--spec/lib/bitbucket/collection_spec.rb24
-rw-r--r--spec/lib/bitbucket/connection_spec.rb35
-rw-r--r--spec/lib/bitbucket/page_spec.rb50
-rw-r--r--spec/lib/bitbucket/paginator_spec.rb21
-rw-r--r--spec/lib/bitbucket/representation/comment_spec.rb22
-rw-r--r--spec/lib/bitbucket/representation/issue_spec.rb47
-rw-r--r--spec/lib/bitbucket/representation/pull_request_comment_spec.rb34
-rw-r--r--spec/lib/bitbucket/representation/pull_request_spec.rb47
-rw-r--r--spec/lib/bitbucket/representation/repo_spec.rb49
-rw-r--r--spec/lib/bitbucket/representation/user_spec.rb11
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb24
-rw-r--r--spec/lib/gitlab/allowable_spec.rb27
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb4
-rw-r--r--spec/lib/gitlab/auth_spec.rb68
-rw-r--r--spec/lib/gitlab/badge/build/status_spec.rb4
-rw-r--r--spec/lib/gitlab/bitbucket_import/client_spec.rb67
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb62
-rw-r--r--spec/lib/gitlab/bitbucket_import/project_creator_spec.rb19
-rw-r--r--spec/lib/gitlab/chat_commands/command_spec.rb28
-rw-r--r--spec/lib/gitlab/checks/force_push_spec.rb19
-rw-r--r--spec/lib/gitlab/ci/status/build/cancelable_spec.rb94
-rw-r--r--spec/lib/gitlab/ci/status/build/common_spec.rb37
-rw-r--r--spec/lib/gitlab/ci/status/build/factory_spec.rb141
-rw-r--r--spec/lib/gitlab/ci/status/build/play_spec.rb86
-rw-r--r--spec/lib/gitlab/ci/status/build/retryable_spec.rb94
-rw-r--r--spec/lib/gitlab/ci/status/build/stop_spec.rb88
-rw-r--r--spec/lib/gitlab/ci/status/canceled_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/status/created_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/status/extended_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/factory_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/status/failed_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/status/pending_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/status/pipeline/common_spec.rb33
-rw-r--r--spec/lib/gitlab/ci/status/pipeline/factory_spec.rb9
-rw-r--r--spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/status/running_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/status/skipped_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/status/stage/common_spec.rb31
-rw-r--r--spec/lib/gitlab/ci/status/stage/factory_spec.rb15
-rw-r--r--spec/lib/gitlab/ci/status/success_spec.rb8
-rw-r--r--spec/lib/gitlab/gfm/reference_rewriter_spec.rb2
-rw-r--r--spec/lib/gitlab/git/rev_list_spec.rb60
-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/avatar_restorer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/project.json28
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb8
-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/middleware/multipart_spec.rb74
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb29
-rw-r--r--spec/lib/gitlab/regex_spec.rb16
-rw-r--r--spec/lib/gitlab/routing_spec.rb23
-rw-r--r--spec/lib/gitlab/serialize/ci/variables_spec.rb18
-rw-r--r--spec/lib/gitlab/sql/union_spec.rb22
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb36
-rw-r--r--spec/lib/mattermost/session_spec.rb99
-rw-r--r--spec/migrations/migrate_process_commit_worker_jobs_spec.rb27
-rw-r--r--spec/models/build_spec.rb177
-rw-r--r--spec/models/ci/pipeline_spec.rb79
-rw-r--r--spec/models/ci/stage_spec.rb4
-rw-r--r--spec/models/commit_spec.rb29
-rw-r--r--spec/models/commit_status_spec.rb9
-rw-r--r--spec/models/concerns/reactive_caching_spec.rb145
-rw-r--r--spec/models/concerns/routable_spec.rb14
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb7
-rw-r--r--spec/models/environment_spec.rb106
-rw-r--r--spec/models/generic_commit_status_spec.rb16
-rw-r--r--spec/models/group_spec.rb10
-rw-r--r--spec/models/issue_spec.rb20
-rw-r--r--spec/models/merge_request_spec.rb49
-rw-r--r--spec/models/milestone_spec.rb11
-rw-r--r--spec/models/namespace_spec.rb28
-rw-r--r--spec/models/note_spec.rb38
-rw-r--r--spec/models/project_services/chat_message/build_message_spec.rb (renamed from spec/models/project_services/slack_service/build_message_spec.rb)12
-rw-r--r--spec/models/project_services/chat_message/issue_message_spec.rb (renamed from spec/models/project_services/slack_service/issue_message_spec.rb)14
-rw-r--r--spec/models/project_services/chat_message/merge_message_spec.rb (renamed from spec/models/project_services/slack_service/merge_message_spec.rb)16
-rw-r--r--spec/models/project_services/chat_message/note_message_spec.rb (renamed from spec/models/project_services/slack_service/note_message_spec.rb)32
-rw-r--r--spec/models/project_services/chat_message/pipeline_message_spec.rb67
-rw-r--r--spec/models/project_services/chat_message/push_message_spec.rb (renamed from spec/models/project_services/slack_service/push_message_spec.rb)30
-rw-r--r--spec/models/project_services/chat_message/wiki_page_message_spec.rb (renamed from spec/models/project_services/slack_service/wiki_page_message_spec.rb)10
-rw-r--r--spec/models/project_services/chat_notification_service_spec.rb11
-rw-r--r--spec/models/project_services/chat_service_spec.rb15
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb218
-rw-r--r--spec/models/project_services/mattermost_notification_service_spec.rb5
-rw-r--r--spec/models/project_services/mattermost_slash_commands_service_spec.rb98
-rw-r--r--spec/models/project_services/slack_notification_service_spec.rb5
-rw-r--r--spec/models/project_services/slack_service/pipeline_message_spec.rb55
-rw-r--r--spec/models/project_services/slack_slash_commands_service.rb40
-rw-r--r--spec/models/project_spec.rb37
-rw-r--r--spec/models/route_spec.rb8
-rw-r--r--spec/models/user_spec.rb11
-rw-r--r--spec/policies/group_policy_spec.rb108
-rw-r--r--spec/requests/api/commits_spec.rb70
-rw-r--r--spec/requests/api/doorkeeper_access_spec.rb2
-rw-r--r--spec/requests/api/environments_spec.rb17
-rw-r--r--spec/requests/api/groups_spec.rb19
-rw-r--r--spec/requests/api/helpers_spec.rb (renamed from spec/requests/api/api_helpers_spec.rb)136
-rw-r--r--spec/requests/api/merge_requests_spec.rb16
-rw-r--r--spec/requests/api/projects_spec.rb2
-rw-r--r--spec/requests/api/services_spec.rb5
-rw-r--r--spec/requests/api/users_spec.rb27
-rw-r--r--spec/requests/ci/api/builds_spec.rb20
-rw-r--r--spec/requests/git_http_spec.rb2
-rw-r--r--spec/routing/admin_routing_spec.rb14
-rw-r--r--spec/routing/import_routing_spec.rb165
-rw-r--r--spec/routing/project_routing_spec.rb19
-rw-r--r--spec/serializers/analytics_build_entity_spec.rb8
-rw-r--r--spec/services/access_token_validation_service_spec.rb41
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb17
-rw-r--r--spec/services/issues/update_service_spec.rb5
-rw-r--r--spec/services/merge_requests/update_service_spec.rb5
-rw-r--r--spec/services/system_note_service_spec.rb2
-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/reactive_caching_helpers.rb38
-rw-r--r--spec/support/services/issuable_update_service_shared_examples.rb17
-rw-r--r--spec/support/services_shared_context.rb6
-rw-r--r--spec/support/slack_mattermost_notifications_shared_examples.rb (renamed from spec/models/project_services/slack_service_spec.rb)87
-rw-r--r--spec/support/upload_helpers.rb16
-rw-r--r--spec/tasks/gitlab/ldap_rake_spec.rb13
-rw-r--r--spec/uploaders/attachment_uploader_spec.rb18
-rw-r--r--spec/uploaders/avatar_uploader_spec.rb18
-rw-r--r--spec/uploaders/file_uploader_spec.rb12
-rw-r--r--spec/views/projects/commit/_commit_box.html.haml_spec.rb2
-rw-r--r--spec/views/projects/pipelines/show.html.haml_spec.rb2
-rw-r--r--spec/workers/reactive_caching_worker_spec.rb15
-rw-r--r--vendor/assets/fonts/KaTeX_AMS-Regular.eotbin0 -> 71656 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_AMS-Regular.ttfbin0 -> 71428 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_AMS-Regular.woffbin0 -> 40200 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_AMS-Regular.woff2bin0 -> 33188 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Caligraphic-Bold.eotbin0 -> 19836 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Caligraphic-Bold.ttfbin0 -> 19588 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Caligraphic-Bold.woffbin0 -> 12136 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff2bin0 -> 10604 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Caligraphic-Regular.eotbin0 -> 19220 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Caligraphic-Regular.ttfbin0 -> 18960 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Caligraphic-Regular.woffbin0 -> 11868 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff2bin0 -> 10396 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Fraktur-Bold.eotbin0 -> 36200 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Fraktur-Bold.ttfbin0 -> 35968 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Fraktur-Bold.woffbin0 -> 23388 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Fraktur-Bold.woff2bin0 -> 20476 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Fraktur-Regular.eotbin0 -> 34896 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Fraktur-Regular.ttfbin0 -> 34652 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Fraktur-Regular.woffbin0 -> 22844 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Fraktur-Regular.woff2bin0 -> 19868 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Main-Bold.eotbin0 -> 60688 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Main-Bold.ttfbin0 -> 60468 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Main-Bold.woffbin0 -> 35480 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Main-Bold.woff2bin0 -> 29492 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Main-Italic.eotbin0 -> 44132 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Main-Italic.ttfbin0 -> 43904 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Main-Italic.woffbin0 -> 24880 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Main-Italic.woff2bin0 -> 21032 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Main-Regular.eotbin0 -> 68228 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Main-Regular.ttfbin0 -> 67996 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Main-Regular.woffbin0 -> 37620 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Main-Regular.woff2bin0 -> 31220 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Math-BoldItalic.eotbin0 -> 39990 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Math-BoldItalic.ttfbin0 -> 39744 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Math-BoldItalic.woffbin0 -> 23192 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Math-BoldItalic.woff2bin0 -> 20036 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Math-Italic.eotbin0 -> 41676 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Math-Italic.ttfbin0 -> 41448 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Math-Italic.woffbin0 -> 23820 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Math-Italic.woff2bin0 -> 20432 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Math-Regular.eotbin0 -> 41536 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Math-Regular.ttfbin0 -> 41304 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Math-Regular.woffbin0 -> 23712 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Math-Regular.woff2bin0 -> 20344 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_SansSerif-Bold.eotbin0 -> 34204 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_SansSerif-Bold.ttfbin0 -> 33964 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_SansSerif-Bold.woffbin0 -> 19196 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_SansSerif-Bold.woff2bin0 -> 16020 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_SansSerif-Italic.eotbin0 -> 31320 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_SansSerif-Italic.ttfbin0 -> 31072 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_SansSerif-Italic.woffbin0 -> 18080 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_SansSerif-Italic.woff2bin0 -> 15152 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_SansSerif-Regular.eotbin0 -> 30212 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_SansSerif-Regular.ttfbin0 -> 29960 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_SansSerif-Regular.woffbin0 -> 16744 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_SansSerif-Regular.woff2bin0 -> 13908 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Script-Regular.eotbin0 -> 25104 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Script-Regular.ttfbin0 -> 24864 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Script-Regular.woffbin0 -> 13856 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Script-Regular.woff2bin0 -> 12276 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size1-Regular.eotbin0 -> 13408 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size1-Regular.ttfbin0 -> 13172 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size1-Regular.woffbin0 -> 6980 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size1-Regular.woff2bin0 -> 5820 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size2-Regular.eotbin0 -> 12648 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size2-Regular.ttfbin0 -> 12412 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size2-Regular.woffbin0 -> 6684 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size2-Regular.woff2bin0 -> 5560 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size3-Regular.eotbin0 -> 8596 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size3-Regular.ttfbin0 -> 8360 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size3-Regular.woffbin0 -> 4776 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size3-Regular.woff2bin0 -> 3856 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size4-Regular.eotbin0 -> 11520 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size4-Regular.ttfbin0 -> 11284 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size4-Regular.woffbin0 -> 6456 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size4-Regular.woff2bin0 -> 5172 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Typewriter-Regular.eotbin0 -> 35784 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Typewriter-Regular.ttfbin0 -> 35528 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Typewriter-Regular.woffbin0 -> 20712 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Typewriter-Regular.woff2bin0 -> 17344 bytes
-rw-r--r--vendor/assets/javascripts/katex.js8683
-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.css975
-rw-r--r--vendor/assets/stylesheets/xterm/xterm.css2206
-rw-r--r--vendor/dockerfile/HTTPdDockerfile3
1040 files changed, 29726 insertions, 5142 deletions
diff --git a/.eslintignore b/.eslintignore
index 93de4b10dfe..b4bfa5a1f7a 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,5 +1,6 @@
/coverage/
/coverage-javascript/
+/node_modules/
/public/
/tmp/
/vendor/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index e522d47d19d..b256e8a2a5f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -15,6 +15,7 @@ variables:
USE_BUNDLE_INSTALL: "true"
GIT_DEPTH: "20"
PHANTOMJS_VERSION: "2.1.1"
+ GET_SOURCES_ATTEMPTS: "3"
before_script:
- source ./scripts/prepare_build.sh
diff --git a/.rubocop.yml b/.rubocop.yml
index 13df3f99613..80eb4a5c19e 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -292,7 +292,8 @@ Style/MultilineMethodDefinitionBraceLayout:
# Checks indentation of binary operations that span more than one line.
Style/MultilineOperationIndentation:
- Enabled: false
+ Enabled: true
+ EnforcedStyle: indented
# Avoid multi-line `? :` (the ternary operator), use if/unless instead.
Style/MultilineTernaryOperator:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fb13db4dd1c..786b0128aac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,21 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 8.14.5 (2016-12-14)
+
+- Moved Leave Project and Leave Group buttons to access_request_buttons from the settings dropdown. !7600
+- fix display hook error message. !7775 (basyura)
+- Remove wrong '.builds-feature' class from the MR settings fieldset. !7930
+- Avoid escaping relative links in Markdown twice. !7940 (winniehell)
+- API: Memoize the current_user so that sudo can work properly. !8017
+- Displays milestone remaining days only when it's present.
+- Allow branch names with dots on API endpoint.
+- Issue#visible_to_user moved to IssuesFinder to prevent accidental use.
+- Shows group members in project members list.
+- Encode input when migrating ProcessCommitWorker jobs to prevent migration errors.
+- Fixed timeago re-rendering every timeago.
+- Fix missing Note access checks by moving Note#search to updated NoteFinder.
+
## 8.14.4 (2016-12-08)
- Fix diff view permalink highlighting. !7090
@@ -264,6 +279,13 @@ entry.
- Fix "Without projects" filter. !6611 (Ben Bodenmiller)
- Fix 404 when visit /projects page
+## 8.13.10 (2016-12-14)
+
+- API: Memoize the current_user so that sudo can work properly. !8017
+- Filter `authentication_token`, `incoming_email_token` and `runners_token` parameters.
+- Issue#visible_to_user moved to IssuesFinder to prevent accidental use.
+- Fix missing Note access checks by moving Note#search to updated NoteFinder.
+
## 8.13.9 (2016-12-08)
- Reenables /user API request to return private-token if user is admin and request is made with sudo. !7615
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index c4e41f94594..627a3f43a64 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-4.0.3
+4.1.1
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 524cb55242b..26aaba0e866 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-1.1.1
+1.2.0
diff --git a/Gemfile b/Gemfile
index f27d6363e3d..774dceff4f4 100644
--- a/Gemfile
+++ b/Gemfile
@@ -22,7 +22,6 @@ gem 'doorkeeper', '~> 4.2.0'
gem 'omniauth', '~> 1.3.1'
gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6'
-gem 'omniauth-bitbucket', '~> 0.0.2'
gem 'omniauth-cas3', '~> 1.1.2'
gem 'omniauth-facebook', '~> 4.0.0'
gem 'omniauth-github', '~> 1.1.1'
@@ -67,7 +66,7 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false
gem 'github-linguist', '~> 4.7.0', require: 'linguist'
# API
-gem 'grape', '~> 0.15.0'
+gem 'grape', '~> 0.18.0'
gem 'grape-entity', '~> 0.6.0'
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
@@ -132,7 +131,7 @@ gem 'after_commit_queue', '~> 1.3.0'
gem 'acts-as-taggable-on', '~> 4.0'
# Background jobs
-gem 'sidekiq', '~> 4.2'
+gem 'sidekiq', '~> 4.2.7'
gem 'sidekiq-cron', '~> 0.4.4'
gem 'redis-namespace', '~> 1.5.2'
gem 'sidekiq-limit_fetch', '~> 3.4'
@@ -170,7 +169,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'
@@ -178,6 +177,9 @@ gem 'asana', '~> 0.4.0'
# FogBugz integration
gem 'ruby-fogbugz', '~> 0.2.1'
+# Kubernetes integration
+gem 'kubeclient', '~> 2.2.0'
+
# d3
gem 'd3_rails', '~> 3.5.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index c464ff70587..8bc22479346 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -126,7 +126,7 @@ GEM
coffee-script-source (1.10.0)
colorize (0.7.7)
concurrent-ruby (1.0.2)
- connection_pool (2.2.0)
+ connection_pool (2.2.1)
crack (0.4.3)
safe_yaml (~> 1.0.0)
creole (0.5.0)
@@ -161,6 +161,8 @@ GEM
diff-lcs (1.2.5)
diffy (3.1.0)
docile (1.1.5)
+ domain_name (0.5.20161021)
+ unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.2.0)
railties (>= 4.2)
dropzonejs-rails (0.7.2)
@@ -282,15 +284,15 @@ GEM
json
multi_json
request_store (>= 1.0)
- grape (0.15.0)
+ grape (0.18.0)
activesupport
builder
hashie (>= 2.1.0)
multi_json (>= 1.3.2)
multi_xml (>= 0.5.2)
+ mustermann-grape (~> 0.4.0)
rack (>= 1.3.0)
rack-accept
- rack-mount
virtus (>= 1.0.0)
grape-entity (0.6.0)
activesupport
@@ -318,6 +320,15 @@ GEM
html2text (0.2.0)
nokogiri (~> 1.6)
htmlentities (4.3.4)
+ http (0.9.8)
+ addressable (~> 2.3)
+ http-cookie (~> 1.0)
+ http-form_data (~> 1.0.1)
+ http_parser.rb (~> 0.6.0)
+ http-cookie (1.0.3)
+ domain_name (~> 0.5)
+ http-form_data (1.0.1)
+ http_parser.rb (0.6.0)
httparty (0.13.7)
json (~> 1.8)
multi_xml (>= 0.5.2)
@@ -352,6 +363,10 @@ GEM
knapsack (1.11.0)
rake
timecop (>= 0.1.0)
+ kubeclient (2.2.0)
+ http (= 0.9.8)
+ recursive-open-struct (= 1.0.0)
+ rest-client
launchy (2.4.3)
addressable (~> 2.3)
letter_opener (1.4.1)
@@ -385,9 +400,14 @@ GEM
multi_json (1.12.1)
multi_xml (0.5.5)
multipart-post (2.0.0)
+ mustermann (0.4.0)
+ tool (~> 0.2)
+ mustermann-grape (0.4.0)
+ mustermann (= 0.4.0)
mysql2 (0.3.20)
net-ldap (0.12.1)
net-ssh (3.0.1)
+ netrc (0.11.0)
newrelic_rpm (3.16.0.318)
nokogiri (1.6.8)
mini_portile2 (~> 2.1.0)
@@ -412,10 +432,6 @@ GEM
jwt (~> 1.0)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.1)
- omniauth-bitbucket (0.0.2)
- multi_json (~> 1.7)
- omniauth (~> 1.1)
- omniauth-oauth (~> 1.0)
omniauth-cas3 (1.1.3)
addressable (~> 2.3)
nokogiri (~> 1.6.6)
@@ -489,14 +505,12 @@ GEM
pry-rails (0.3.4)
pry (>= 0.9.10)
pyu-ruby-sasl (0.0.3.3)
- rack (1.6.4)
+ rack (1.6.5)
rack-accept (0.4.5)
rack (>= 0.4)
rack-attack (4.4.1)
rack
rack-cors (0.4.0)
- rack-mount (0.8.3)
- rack (>= 1.0.0)
rack-oauth2 (1.2.3)
activesupport (>= 2.3)
attr_required (>= 0.0.5)
@@ -543,6 +557,7 @@ GEM
json (~> 1.4)
recaptcha (3.0.0)
json
+ recursive-open-struct (1.0.0)
redcarpet (3.3.3)
redis (3.2.2)
redis-actionpack (5.0.1)
@@ -568,6 +583,10 @@ GEM
listen (~> 3.0)
responders (2.3.0)
railties (>= 4.2.0, < 5.1)
+ rest-client (2.0.0)
+ http-cookie (>= 1.0.2, < 2.0)
+ mime-types (>= 1.16, < 4.0)
+ netrc (~> 0.8)
rinku (2.0.0)
rotp (2.1.2)
rouge (2.0.7)
@@ -648,10 +667,10 @@ GEM
rack
shoulda-matchers (2.8.0)
activesupport (>= 3.0.0)
- sidekiq (4.2.1)
+ sidekiq (4.2.7)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
- rack-protection (~> 1.5)
+ rack-protection (>= 1.5.0)
redis (~> 3.2, >= 3.2.1)
sidekiq-cron (0.4.4)
redis-namespace (>= 1.5.2)
@@ -664,7 +683,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
@@ -722,6 +741,7 @@ GEM
tilt (2.0.5)
timecop (0.8.1)
timfel-krb5-auth (0.8.3)
+ tool (0.2.3)
truncato (0.7.8)
htmlentities (~> 4.3.1)
nokogiri (~> 1.6.1)
@@ -840,7 +860,7 @@ DEPENDENCIES
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2)
gon (~> 6.1.0)
- grape (~> 0.15.0)
+ grape (~> 0.18.0)
grape-entity (~> 0.6.0)
haml_lint (~> 0.18.2)
hamlit (~> 2.6.1)
@@ -859,6 +879,7 @@ DEPENDENCIES
jwt
kaminari (~> 0.17.0)
knapsack (~> 1.11.0)
+ kubeclient (~> 2.2.0)
letter_opener_web (~> 1.3.0)
license_finder (~> 2.1.0)
licensee (~> 8.0.0)
@@ -877,7 +898,6 @@ DEPENDENCIES
omniauth (~> 1.3.1)
omniauth-auth0 (~> 1.4.1)
omniauth-azure-oauth2 (~> 0.0.6)
- omniauth-bitbucket (~> 0.0.2)
omniauth-cas3 (~> 1.1.2)
omniauth-facebook (~> 4.0.0)
omniauth-github (~> 1.1.1)
@@ -928,11 +948,11 @@ DEPENDENCIES
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0)
- sidekiq (~> 4.2)
+ sidekiq (~> 4.2.7)
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/javascripts/abuse_reports.js.es6 b/app/assets/javascripts/abuse_reports.js.es6
index 82e526ae0ef..8a260aae1b1 100644
--- a/app/assets/javascripts/abuse_reports.js.es6
+++ b/app/assets/javascripts/abuse_reports.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-param-reassign */
+
((global) => {
const MAX_MESSAGE_LENGTH = 500;
const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js
index 31852e4750c..5a7d823e84c 100644
--- a/app/assets/javascripts/admin.js
+++ b/app/assets/javascripts/admin.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, no-undef, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, padded-blocks, max-len */
+/* global Turbolinks */
+
(function() {
this.Admin = (function() {
function Admin() {
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 1c625e2f2b1..f60f27d1210 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -1,6 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, no-undef, comma-dangle, prefer-arrow-callback, indent, object-curly-spacing, quote-props, no-param-reassign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, comma-dangle, prefer-arrow-callback, indent, object-curly-spacing, quote-props, no-param-reassign, padded-blocks, max-len */
+
(function() {
- this.Api = {
+ var Api = {
groupsPath: "/api/:version/groups.json",
groupPath: "/api/:version/groups/:id.json",
namespacesPath: "/api/:version/namespaces.json",
@@ -10,6 +11,7 @@
licensePath: "/api/:version/templates/licenses/:key",
gitignorePath: "/api/:version/templates/gitignores/:key",
gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key",
+ dockerfilePath: "/api/:version/dockerfiles/:key",
issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
group: function(group_id, callback) {
var url = Api.buildUrl(Api.groupPath)
@@ -119,6 +121,10 @@
return callback(file);
});
},
+ dockerfileYml: function(key, callback) {
+ var url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
+ $.get(url, callback);
+ },
issueTemplate: function(namespacePath, projectPath, key, type, callback) {
var url = Api.buildUrl(Api.issuableTemplatePath)
.replace(':key', key)
@@ -140,4 +146,5 @@
}
};
+ window.Api = Api;
}).call(this);
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index b7c4673c8e3..043c6a11c4f 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -1,4 +1,11 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, no-undef, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len */
+/* global bp */
+/* global Cookies */
+/* global Flash */
+/* global ConfirmDangerModal */
+/* global AwardsHandler */
+/* global Aside */
+
// This is a manifest file that'll be compiled into including all the files listed below.
// Add new JavaScript code in separate files in this directory and they'll automatically
// be included in the compiled file accessible from http://example.com/assets/application.js
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index f4302e2e9f6..107a7978a87 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, spaced-comment, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, radix, keyword-spacing, space-before-blocks, brace-style, no-underscore-dangle, no-undef, no-plusplus, no-return-assign, camelcase, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, spaced-comment, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, radix, keyword-spacing, space-before-blocks, brace-style, no-underscore-dangle, no-plusplus, no-return-assign, camelcase, padded-blocks */
+/* global Cookies */
+
(function() {
this.AwardsHandler = (function() {
var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; //For separating lists produced by ruby's Array#toSentence
diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js
index a5d62f881fe..c62a4c5a456 100644
--- a/app/assets/javascripts/behaviors/autosize.js
+++ b/app/assets/javascripts/behaviors/autosize.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, no-undef, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, padded-blocks, max-len */
+/* global autosize */
/*= require jquery.ba-resize */
/*= require autosize */
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index 4edcaa15fe5..586f941a6e3 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-undef, prefer-arrow-callback, camelcase, max-len, consistent-return, quotes, object-shorthand, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, camelcase, consistent-return, quotes, object-shorthand, comma-dangle, padded-blocks, max-len */
+
// Quick Submit behavior
//
// When a child field of a form with a `js-quick-submit` class receives a
diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js.es6
index 37531aaec9b..57bd13eecf8 100644
--- a/app/assets/javascripts/blob/blob_ci_yaml.js.es6
+++ b/app/assets/javascripts/blob/blob_ci_yaml.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */
+/* global Api */
+
/*= require blob/template_selector */
((global) => {
diff --git a/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 b/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6
new file mode 100644
index 00000000000..bdf95017613
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6
@@ -0,0 +1,18 @@
+/* global Api */
+/*= require blob/template_selector */
+
+(() => {
+ const global = window.gl || (window.gl = {});
+
+ class BlobDockerfileSelector extends gl.TemplateSelector {
+ requestFile(query) {
+ return Api.dockerfileYml(query.name, this.requestFileSuccess.bind(this));
+ }
+
+ requestFileSuccess(file) {
+ return super.requestFileSuccess(file);
+ }
+ }
+
+ global.BlobDockerfileSelector = BlobDockerfileSelector;
+})();
diff --git a/app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6 b/app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6
new file mode 100644
index 00000000000..9cee79fa5d5
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6
@@ -0,0 +1,27 @@
+(() => {
+ const global = window.gl || (window.gl = {});
+
+ class BlobDockerfileSelectors {
+ constructor({ editor, $dropdowns } = {}) {
+ this.editor = editor;
+ this.$dropdowns = $dropdowns || $('.js-dockerfile-selector');
+ this.initSelectors();
+ }
+
+ initSelectors() {
+ const editor = this.editor;
+ this.$dropdowns.each((i, dropdown) => {
+ const $dropdown = $(dropdown);
+ return new gl.BlobDockerfileSelector({
+ editor,
+ pattern: /(Dockerfile)/,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-dockerfile-selector-wrap'),
+ dropdown: $dropdown,
+ });
+ });
+ }
+ }
+
+ global.BlobDockerfileSelectors = BlobDockerfileSelectors;
+})();
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index e0a2e8ac12e..eab686c45c3 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, camelcase, no-undef, object-shorthand, quotes, comma-dangle, prefer-arrow-callback, no-unused-vars, prefer-template, no-useless-escape, no-alert, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, camelcase, object-shorthand, quotes, comma-dangle, prefer-arrow-callback, no-unused-vars, prefer-template, no-useless-escape, no-alert, padded-blocks, max-len */
+/* global Dropzone */
+
(function() {
this.BlobFileDropzone = (function() {
function BlobFileDropzone(form, method) {
diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js
index 7e8f1062ab3..15563e429a0 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selector.js
+++ b/app/assets/javascripts/blob/blob_gitignore_selector.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, no-undef, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, padded-blocks */
+/* global Api */
/*= require blob/template_selector */
diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js
index 9a694daa010..d7f95093688 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selectors.js
+++ b/app/assets/javascripts/blob/blob_gitignore_selectors.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-cond-assign, no-sequences, no-undef, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-cond-assign, no-sequences, comma-dangle, padded-blocks, max-len */
+/* global BlobGitignoreSelector */
+
(function() {
this.BlobGitignoreSelectors = (function() {
function BlobGitignoreSelectors(opts) {
diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js
index 9a77fe35d55..d9c6f65a083 100644
--- a/app/assets/javascripts/blob/blob_license_selector.js
+++ b/app/assets/javascripts/blob/blob_license_selector.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, comma-dangle, no-undef, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, comma-dangle, padded-blocks */
+/* global Api */
/*= require blob/template_selector */
diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.es6 b/app/assets/javascripts/blob/blob_license_selectors.js.es6
index adeb8ba1318..268640681d4 100644
--- a/app/assets/javascripts/blob/blob_license_selectors.js.es6
+++ b/app/assets/javascripts/blob/blob_license_selectors.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable no-unused-vars, no-param-reassign, padded-blocks */
+/* global BlobLicenseSelector */
+
((global) => {
class BlobLicenseSelectors {
constructor({ $dropdowns, editor }) {
diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js.es6
index 0ff5c0fab05..7a1ee9998c8 100644
--- a/app/assets/javascripts/blob/template_selector.js.es6
+++ b/app/assets/javascripts/blob/template_selector.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable indent, comma-dangle, object-shorthand, func-names, space-before-function-paren, arrow-parens, no-unused-vars, class-methods-use-this, no-var, consistent-return, prefer-const, no-param-reassign, space-in-parens, max-len */
+
((global) => {
class TemplateSelector {
constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) {
diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
index b8eb0f60a8e..8c40e36a80a 100644
--- a/app/assets/javascripts/blob_edit/blob_edit_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
@@ -1,4 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-undef, no-new, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, padded-blocks, max-len */
+/* global EditBlob */
+/* global NewCommitForm */
+
/*= require_tree . */
(function() {
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 0c74aaaa852..fa43ff611cc 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -1,4 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, no-param-reassign, no-undef, quotes, prefer-template, no-new, comma-dangle, one-var, one-var-declaration-per-line, prefer-arrow-callback, no-else-return, no-unused-vars, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, no-param-reassign, quotes, prefer-template, no-new, comma-dangle, one-var, one-var-declaration-per-line, prefer-arrow-callback, no-else-return, no-unused-vars, padded-blocks, max-len */
+/* global ace */
+/* global BlobGitignoreSelectors */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
@@ -33,6 +36,9 @@
new gl.BlobCiYamlSelectors({
editor: this.editor
});
+ new gl.BlobDockerfileSelectors({
+ editor: this.editor
+ });
}
EditBlob.prototype.initModePanesAndLinks = function() {
@@ -57,7 +63,7 @@
content: this.editor.getValue()
}, function(response) {
currentPane.empty().append(response);
- return currentPane.syntaxHighlight();
+ return currentPane.renderGFM();
});
} else {
this.$toggleButton.show();
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
index 7ba918a05f8..ab2343c72fc 100644
--- a/app/assets/javascripts/boards/boards_bundle.js.es6
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable one-var, indent, quote-props, comma-dangle, space-before-function-paren */
+/* global Vue */
+/* global BoardService */
+
//= require vue
//= require vue-resource
//= require Sortable
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6
index 31de3b25284..d1fb0ec48e0 100644
--- a/app/assets/javascripts/boards/components/board.js.es6
+++ b/app/assets/javascripts/boards/components/board.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, space-before-function-paren, one-var, indent, radix */
+/* global Vue */
+/* global Sortable */
+
//= require ./board_blank_state
//= require ./board_delete
//= require ./board_list
diff --git a/app/assets/javascripts/boards/components/board_blank_state.js.es6 b/app/assets/javascripts/boards/components/board_blank_state.js.es6
index 691487b272a..0a47a22fad2 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.js.es6
+++ b/app/assets/javascripts/boards/components/board_blank_state.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, comma-dangle, semi */
+/* global Vue */
+/* global ListLabel */
+
(() => {
const Store = gl.issueBoards.BoardsStore;
diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6
index 2299dafd217..5fc50280811 100644
--- a/app/assets/javascripts/boards/components/board_card.js.es6
+++ b/app/assets/javascripts/boards/components/board_card.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, space-before-function-paren, dot-notation */
+/* global Vue */
+
(() => {
const Store = gl.issueBoards.BoardsStore;
diff --git a/app/assets/javascripts/boards/components/board_delete.js.es6 b/app/assets/javascripts/boards/components/board_delete.js.es6
index c45e1926c5c..861600424a5 100644
--- a/app/assets/javascripts/boards/components/board_delete.js.es6
+++ b/app/assets/javascripts/boards/components/board_delete.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, space-before-function-paren, no-alert */
+/* global Vue */
+
(() => {
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6
index 43ebeef39c4..6711930622b 100644
--- a/app/assets/javascripts/boards/components/board_list.js.es6
+++ b/app/assets/javascripts/boards/components/board_list.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, space-before-function-paren, max-len, no-plusplus */
+/* global Vue */
+/* global Sortable */
+
//= require ./board_card
//= require ./board_new_issue
diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6
index a7989a2ff4c..2386d3a613c 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.js.es6
+++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, no-unused-vars */
+/* global Vue */
+/* global ListIssue */
+
(() => {
const Store = gl.issueBoards.BoardsStore;
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6
index 1644a772737..02459722bbf 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js.es6
+++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6
@@ -1,4 +1,10 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, space-before-function-paren, no-new */
+/* global Vue */
+/* global IssuableContext */
+/* global MilestoneSelect */
+/* global LabelsSelect */
+/* global Sidebar */
+
(() => {
const Store = gl.issueBoards.BoardsStore;
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
index 10ce746deb5..3f5cf8420a8 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var, indent */
+
(() => {
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
@@ -45,10 +46,10 @@
return $li.append($a.prepend($labelColor));
},
- search: {
- fields: ['title']
- },
- filterable: true,
+ search: {
+ fields: ['title']
+ },
+ filterable: true,
selectable: true,
multiSelect: true,
clicked (label, $el, e) {
diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 b/app/assets/javascripts/boards/filters/due_date_filters.js.es6
index 9eceac4eddd..7e192e90fe6 100644
--- a/app/assets/javascripts/boards/filters/due_date_filters.js.es6
+++ b/app/assets/javascripts/boards/filters/due_date_filters.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* global Vue */
+
Vue.filter('due-date', (value) => {
const date = new Date(value);
return $.datepicker.formatDate('M d, yy', date);
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
index 5f99de39122..a5e62ed775d 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable no-unused-vars, no-mixed-operators, prefer-const, comma-dangle, semi */
+/* global DocumentTouch */
+
((w) => {
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6
index 21d735e8231..1199e022ff1 100644
--- a/app/assets/javascripts/boards/models/issue.js.es6
+++ b/app/assets/javascripts/boards/models/issue.js.es6
@@ -1,4 +1,9 @@
-/* eslint-disable */
+/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, space-in-parens, arrow-parens, comma-dangle, max-len */
+/* global Vue */
+/* global ListLabel */
+/* global ListMilestone */
+/* global ListUser */
+
class ListIssue {
constructor (obj) {
this.id = obj.iid;
diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js.es6
index 0910fe9a854..8f20a1bbec7 100644
--- a/app/assets/javascripts/boards/models/label.js.es6
+++ b/app/assets/javascripts/boards/models/label.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-unused-vars, space-before-function-paren */
+
class ListLabel {
constructor (obj) {
this.id = obj.id;
diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6
index 429bd27c3fb..a8d38c16485 100644
--- a/app/assets/javascripts/boards/models/list.js.es6
+++ b/app/assets/javascripts/boards/models/list.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-plusplus, prefer-const, space-in-parens, no-shadow, no-param-reassign, max-len, no-unused-vars */
+/* global ListIssue */
+/* global ListLabel */
+
class List {
constructor (obj) {
this.id = obj.id;
diff --git a/app/assets/javascripts/boards/models/milestone.js.es6 b/app/assets/javascripts/boards/models/milestone.js.es6
index a48969e19c9..9c173c1b70b 100644
--- a/app/assets/javascripts/boards/models/milestone.js.es6
+++ b/app/assets/javascripts/boards/models/milestone.js.es6
@@ -1,6 +1,7 @@
-/* eslint-disable */
+/* eslint-disable no-unused-vars */
+
class ListMilestone {
- constructor (obj) {
+ constructor(obj) {
this.id = obj.id;
this.title = obj.title;
}
diff --git a/app/assets/javascripts/boards/models/user.js.es6 b/app/assets/javascripts/boards/models/user.js.es6
index 583a973fc46..a8a3892e2ad 100644
--- a/app/assets/javascripts/boards/models/user.js.es6
+++ b/app/assets/javascripts/boards/models/user.js.es6
@@ -1,6 +1,7 @@
-/* eslint-disable */
+/* eslint-disable no-unused-vars */
+
class ListUser {
- constructor (user) {
+ constructor(user) {
this.id = user.id;
this.name = user.name;
this.username = user.username;
diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6
index f59a2ed7937..189a8703198 100644
--- a/app/assets/javascripts/boards/services/board_service.js.es6
+++ b/app/assets/javascripts/boards/services/board_service.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, prefer-const, no-extra-semi, max-len, no-unused-vars */
+/* global Vue */
+
class BoardService {
constructor (root, boardId) {
this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6
index bb2a4de8b18..e7a14ea5bca 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js.es6
+++ b/app/assets/javascripts/boards/stores/boards_store.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, space-before-function-paren, one-var, indent, space-in-parens, no-shadow, radix, dot-notation, semi, max-len */
+/* global Cookies */
+/* global List */
+
(() => {
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
index 80f137ca12e..3723a2039f9 100644
--- a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
+++ b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars, no-plusplus */
+/* global Vue */
+
Vue.http.interceptors.push((request, next) => {
Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js
index e7ceb602601..a7e72430141 100644
--- a/app/assets/javascripts/breakpoints.js
+++ b/app/assets/javascripts/breakpoints.js
@@ -1,6 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, quotes, no-shadow, prefer-arrow-callback, prefer-template, consistent-return, padded-blocks, no-return-assign, new-parens, no-param-reassign, no-undef, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, quotes, no-shadow, prefer-arrow-callback, prefer-template, consistent-return, padded-blocks, no-return-assign, new-parens, no-param-reassign, max-len */
+
(function() {
- this.Breakpoints = (function() {
+ var Breakpoints = (function() {
var BreakpointInstance, instance;
function Breakpoints() {}
@@ -68,4 +69,5 @@
};
})(this));
+ window.Breakpoints = Breakpoints;
}).call(this);
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 116a47b0907..824febe3fd3 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -1,4 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, no-undef, quotes, yoda, no-else-return, consistent-return, comma-dangle, semi, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, semi, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top, padded-blocks */
+/* global Breakpoints */
+/* global Turbolinks */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
@@ -193,6 +196,7 @@
};
Build.prototype.stepTrace = function(e) {
+ var $currentTarget;
e.preventDefault();
$currentTarget = $(e.currentTarget);
$.scrollTo($currentTarget.attr('href'), {
diff --git a/app/assets/javascripts/build_variables.js.es6 b/app/assets/javascripts/build_variables.js.es6
index 0ecd20bc11e..993424d422f 100644
--- a/app/assets/javascripts/build_variables.js.es6
+++ b/app/assets/javascripts/build_variables.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, prefer-arrow-callback, space-before-blocks, space-before-function-paren, comma-spacing, max-len */
+
$(function(){
$('.reveal-variables').off('click').on('click',function(){
$('.js-build').toggle().niceScroll();
diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js
index 67509ea7d91..67b33a4d7ee 100644
--- a/app/assets/javascripts/commit.js
+++ b/app/assets/javascripts/commit.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-undef, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, padded-blocks */
+/* global CommitFile */
+
(function() {
this.Commit = (function() {
function Commit() {
diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js
index 3f29826fa9b..27512312c7c 100644
--- a/app/assets/javascripts/commit/file.js
+++ b/app/assets/javascripts/commit/file.js
@@ -1,9 +1,11 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, no-undef, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, padded-blocks */
+/* global ImageFile */
+
(function() {
this.CommitFile = (function() {
function CommitFile(file) {
if ($('.image', file).length) {
- new ImageFile(file);
+ new gl.ImageFile(file);
}
}
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index 4c2ae595319..fd8910e916f 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -1,6 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, padded-blocks, max-len */
(function() {
- this.ImageFile = (function() {
+ gl.ImageFile = (function() {
var prepareFrames;
// Width where images must fits in, for 2-up this gets divided by 2
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index 3627aaf5080..24a6e4ff0e9 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-undef, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, padded-blocks, max-len, prefer-arrow-callback */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, padded-blocks, max-len, prefer-arrow-callback */
+/* global Pager */
+
(function() {
this.CommitsList = (function() {
function CommitsList() {}
@@ -6,8 +8,8 @@
CommitsList.timer = null;
CommitsList.init = function(limit) {
- $("body").on("click", ".day-commits-table li.commit", function(event) {
- if (event.target.nodeName !== "A") {
+ $("body").on("click", ".day-commits-table li.commit", function(e) {
+ if (e.target.nodeName !== "A") {
location.href = $(this).attr("url");
e.stopPropagation();
return false;
diff --git a/app/assets/javascripts/compare_autocomplete.js.es6 b/app/assets/javascripts/compare_autocomplete.js.es6
index bd980f87e72..45c974b2b68 100644
--- a/app/assets/javascripts/compare_autocomplete.js.es6
+++ b/app/assets/javascripts/compare_autocomplete.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, no-dupe-keys, wrap-iife, padded-blocks, max-len */
+
(function() {
this.CompareAutocomplete = (function() {
function CompareAutocomplete() {
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
index 1cc34e490c2..6a13f38588d 100644
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ b/app/assets/javascripts/copy_to_clipboard.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-undef, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, padded-blocks, max-len */
+/* global Clipboard */
/*= require clipboard */
@@ -6,7 +7,7 @@
var genericError, genericSuccess, showTooltip;
genericSuccess = function(e) {
- showTooltip(e.trigger, 'Copied!');
+ showTooltip(e.trigger, 'Copied');
// Clear the selection and blur the trigger so it loses its border
e.clearSelection();
return $(e.trigger).blur();
@@ -31,7 +32,7 @@
var originalTitle = $target.data('original-title');
$target
- .attr('title', 'Copied!')
+ .attr('title', 'Copied')
.tooltip('fixTitle')
.tooltip('show')
.attr('title', originalTitle)
diff --git a/app/assets/javascripts/create_label.js.es6 b/app/assets/javascripts/create_label.js.es6
index 744aa0afa03..947c129d5b5 100644
--- a/app/assets/javascripts/create_label.js.es6
+++ b/app/assets/javascripts/create_label.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-param-reassign, wrap-iife, max-len */
+/* global Api */
+
(function (w) {
class CreateLabelDropdown {
constructor ($el, namespacePath, projectPath) {
diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6
index ecf9d1de81c..9cf33e62958 100644
--- a/app/assets/javascripts/diff.js.es6
+++ b/app/assets/javascripts/diff.js.es6
@@ -5,8 +5,11 @@
class Diff {
constructor() {
- $('.files .diff-file').singleFileDiff();
- $('.files .diff-file').filesCommentButton();
+ const $diffFile = $('.files .diff-file');
+ $diffFile.singleFileDiff();
+ $diffFile.filesCommentButton();
+
+ $diffFile.each((index, file) => new gl.ImageFile(file));
if (this.diffViewType() === 'parallel') {
$('.content-wrapper .container-fluid').removeClass('container-limited');
diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
index 52e2846d279..c59d3996fab 100644
--- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, semi, max-len */
+/* global Vue */
+/* global CommentsStore */
+
(() => {
const CommentAndResolveBtn = Vue.extend({
props: {
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
index 983e554b9c1..f47867fc3b0 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
@@ -1,6 +1,10 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, indent, space-before-function-paren, no-plusplus, no-lonely-if, no-continue, brace-style, max-len, quotes, semi */
+/* global Vue */
+/* global DiscussionMixins */
+/* global CommentsStore */
+
(() => {
- JumpToDiscussion = Vue.extend({
+ const JumpToDiscussion = Vue.extend({
mixins: [DiscussionMixins],
props: {
discussionId: String
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
index 27af9fc96ad..88a19fc6e1d 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
@@ -1,4 +1,9 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, no-new, max-len */
+/* global Vue */
+/* global CommentsStore */
+/* global ResolveService */
+/* global Flash */
+
(() => {
const ResolveBtn = Vue.extend({
props: {
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
index 9522ccb49da..72cdae812bc 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, object-shorthand, func-names, no-param-reassign */
+/* global Vue */
+/* global DiscussionMixins */
+/* global CommentsStore */
+
((w) => {
w.ResolveCount = Vue.extend({
mixins: [DiscussionMixins],
diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
index b945a09fcbe..ee5f62b2d9e 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable object-shorthand, func-names, space-before-function-paren, comma-dangle, no-else-return, quotes, max-len */
+/* global Vue */
+/* global CommentsStore */
+/* global ResolveService */
+
(() => {
const ResolveDiscussionBtn = Vue.extend({
props: {
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
index bd4c20aed8b..840b5aa922e 100644
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, comma-dangle, new-cap, no-new */
+/* global Vue */
+/* global ResolveCount */
+
//= require vue
//= require vue-resource
//= require_directory ./models
diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
index 7a929017f36..a9ea18bf82b 100644
--- a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
+++ b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-plusplus, no-param-reassign, max-len */
+
((w) => {
w.DiscussionMixins = {
computed: {
diff --git a/app/assets/javascripts/diff_notes/models/discussion.js.es6 b/app/assets/javascripts/diff_notes/models/discussion.js.es6
index badcdccc840..efcd46680a7 100644
--- a/app/assets/javascripts/diff_notes/models/discussion.js.es6
+++ b/app/assets/javascripts/diff_notes/models/discussion.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, camelcase, guard-for-in, no-restricted-syntax, no-unused-vars, max-len */
+/* global Vue */
+/* global NoteModel */
+
class DiscussionModel {
constructor (discussionId) {
this.id = discussionId;
@@ -69,7 +72,7 @@ class DiscussionModel {
gl.utils.localTimeAgo($('.js-timeago', `${discussionSelector}`));
} else {
- $discussionHeadline.remove();
+ $discussionHeadline.remove();
}
}
diff --git a/app/assets/javascripts/diff_notes/models/note.js.es6 b/app/assets/javascripts/diff_notes/models/note.js.es6
index d0541b02632..e3bce1d2038 100644
--- a/app/assets/javascripts/diff_notes/models/note.js.es6
+++ b/app/assets/javascripts/diff_notes/models/note.js.es6
@@ -1,6 +1,7 @@
-/* eslint-disable */
+/* eslint-disable camelcase, no-unused-vars */
+
class NoteModel {
- constructor (discussionId, noteId, canResolve, resolved, resolved_by) {
+ constructor(discussionId, noteId, canResolve, resolved, resolved_by) {
this.discussionId = discussionId;
this.id = noteId;
this.canResolve = canResolve;
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6
index 86953ce7ffb..78c74985f78 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js.es6
+++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable class-methods-use-this, one-var, indent, camelcase, no-new, comma-dangle, semi, no-param-reassign, max-len */
+/* global Vue */
+/* global Flash */
+/* global CommentsStore */
+
((w) => {
class ResolveServiceClass {
constructor() {
diff --git a/app/assets/javascripts/diff_notes/stores/comments.js.es6 b/app/assets/javascripts/diff_notes/stores/comments.js.es6
index f42ca406bb1..1a7abbc6f75 100644
--- a/app/assets/javascripts/diff_notes/stores/comments.js.es6
+++ b/app/assets/javascripts/diff_notes/stores/comments.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable object-shorthand, func-names, camelcase, prefer-const, no-restricted-syntax, guard-for-in, comma-dangle, max-len, no-param-reassign */
+/* global Vue */
+/* global DiscussionModel */
+
((w) => {
w.CommentsStore = {
state: {},
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
index 413117c2226..1e259a16f06 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -1,4 +1,42 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len, padded-blocks */
+/* global UsernameValidator */
+/* global ActiveTabMemoizer */
+/* global ShortcutsNavigation */
+/* global Build */
+/* global Issuable */
+/* global Issue */
+/* global ShortcutsIssuable */
+/* global ZenMode */
+/* global Milestone */
+/* global GLForm */
+/* global IssuableForm */
+/* global LabelsSelect */
+/* global MilestoneSelect */
+/* global MergedButtons */
+/* global Commit */
+/* global NotificationsForm */
+/* global TreeView */
+/* global NotificationsDropdown */
+/* global UsersSelect */
+/* global GroupAvatar */
+/* global LineHighlighter */
+/* global ShortcutsBlob */
+/* global ProjectFork */
+/* global BuildArtifacts */
+/* global GroupsSelect */
+/* global Search */
+/* global Admin */
+/* global NamespaceSelects */
+/* global ShortcutsDashboardNavigation */
+/* global Project */
+/* global ProjectAvatar */
+/* global CompareAutocomplete */
+/* global ProjectNew */
+/* global Star */
+/* global ProjectShow */
+/* global Labels */
+/* global Shortcuts */
+
(function() {
var Dispatcher;
@@ -36,7 +74,9 @@
case 'projects:merge_requests:index':
case 'projects:issues:index':
Issuable.init();
- new gl.IssuableBulkActions();
+ new gl.IssuableBulkActions({
+ prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_',
+ });
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:issues:show':
@@ -106,10 +146,6 @@
new ZenMode();
new MergedButtons();
break;
- case 'projects:merge_requests:index':
- shortcut_handler = new ShortcutsNavigation();
- Issuable.init();
- break;
case 'dashboard:activity':
new gl.Activities();
break;
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index e1e76bca6ad..56cb39be642 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, no-undef, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, no-plusplus, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, no-plusplus, prefer-arrow-callback, padded-blocks */
+/* global Dropzone */
/*= require preview_markdown */
diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6
index e84f5ac9183..2b7d57d86c6 100644
--- a/app/assets/javascripts/due_date_select.js.es6
+++ b/app/assets/javascripts/due_date_select.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, prefer-const, padded-blocks, no-unused-vars, no-underscore-dangle, no-new, max-len, semi, no-sequences, no-unused-expressions, no-param-reassign */
+
(function(global) {
class DueDateSelect {
constructor({ $dropdown, $loading } = {}) {
diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6
index 1db29dd47fb..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() {
@@ -164,8 +165,7 @@
{{state.availableCounter}}
</span>
</a>
- </li>
- <li v-bind:class="{ 'active' : scope === 'stopped' }">
+ </li><li v-bind:class="{ 'active' : scope === 'stopped' }">
<a :href="projectStoppedEnvironmentsPath">
Stopped
<span class="badge js-stopped-environments-count">
@@ -231,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"
@@ -241,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 2e046a60146..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() {
@@ -449,7 +457,7 @@
</span>
</td>
- <td>
+ <td class="environments-build-cell">
<a v-if="shouldRenderBuildName"
class="build-link"
:href="model.last_deployment.deployable.build_path">
@@ -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/extensions/object.js.es6 b/app/assets/javascripts/extensions/object.js.es6
new file mode 100644
index 00000000000..70a2d765abd
--- /dev/null
+++ b/app/assets/javascripts/extensions/object.js.es6
@@ -0,0 +1,26 @@
+/* eslint-disable no-restricted-syntax */
+
+// Adapted from https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill
+if (typeof Object.assign !== 'function') {
+ Object.assign = function assign(target, ...args) {
+ if (target == null) { // TypeError if undefined or null
+ throw new TypeError('Cannot convert undefined or null to object');
+ }
+
+ const to = Object(target);
+
+ for (let index = 0; index < args.length; index += 1) {
+ const nextSource = args[index];
+
+ if (nextSource != null) { // Skip over if undefined or null
+ for (const nextKey in nextSource) {
+ // Avoid bugs when hasOwnProperty is shadowed
+ if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
+ to[nextKey] = nextSource[nextKey];
+ }
+ }
+ }
+ }
+ return to;
+ };
+}
diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
index 0122e847161..785f2869970 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, padded-blocks, consistent-return, no-undef, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, padded-blocks, consistent-return */
+/* global FilesCommentButton */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
index 6f9d6283071..17d03c87bf5 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -1,19 +1,29 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, padded-blocks, vars-on-top, indent, no-extra-semi, no-multi-spaces, semi, max-len */
+
// Creates the variables for setting up GFM auto-completion
(function() {
- if (window.GitLab == null) {
- window.GitLab = {};
+ if (window.gl == null) {
+ window.gl = {};
}
function sanitize(str) {
return str.replace(/<(?:.|\n)*?>/gm, '');
}
- GitLab.GfmAutoComplete = {
- dataLoading: false,
- dataLoaded: false,
+ window.gl.GfmAutoComplete = {
+ dataSources: {},
+ defaultLoadingData: ['loading'],
cachedData: {},
- dataSource: '',
+ isLoadingData: {},
+ atTypeMap: {
+ ':': 'emojis',
+ '@': 'members',
+ '#': 'issues',
+ '!': 'mergeRequests',
+ '~': 'labels',
+ '%': 'milestones',
+ '/': 'commands'
+ },
// Emoji
Emoji: {
template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>'
@@ -34,42 +44,45 @@
template: '<li>${title}</li>'
},
Loading: {
- template: '<li><i class="fa fa-refresh fa-spin"></i> Loading...</li>'
+ template: '<li style="pointer-events: none;"><i class="fa fa-refresh fa-spin"></i> Loading...</li>'
},
DefaultOptions: {
sorter: function(query, items, searchKey) {
- // Highlight first item only if at least one char was typed
- this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0;
- if ((items[0].name != null) && items[0].name === 'loading') {
+ if (gl.GfmAutoComplete.isLoading(items)) {
return items;
}
return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey);
},
filter: function(query, data, searchKey) {
- if (data[0] === 'loading') {
+ if (gl.GfmAutoComplete.isLoading(data)) {
+ gl.GfmAutoComplete.togglePreventSelection.call(this, true);
+ gl.GfmAutoComplete.fetchData(this.$inputor, this.at);
return data;
+ } else {
+ gl.GfmAutoComplete.togglePreventSelection.call(this, false);
+ return $.fn.atwho["default"].callbacks.filter(query, data, searchKey);
}
- return $.fn.atwho["default"].callbacks.filter(query, data, searchKey);
},
beforeInsert: function(value) {
- if (!GitLab.GfmAutoComplete.dataLoaded) {
- return this.at;
- } else {
- return value;
+ if (value && !this.setting.skipSpecialCharacterTest) {
+ var withoutAt = value.substring(1);
+ if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"';
}
+ return value;
},
matcher: function (flag, subtext) {
// The below is taken from At.js source
// Tweaked to commands to start without a space only if char before is a non-word character
// https://github.com/ichord/At.js
- var _a, _y, regexp, match;
+ var _a, _y, regexp, match, atSymbols;
+ atSymbols = Object.keys(this.app.controllers).join('|');
subtext = subtext.split(' ').pop();
flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
_a = decodeURI("%C3%80");
_y = decodeURI("%C3%BF");
- regexp = new RegExp("(?:\\B|\\W|\\s)" + flag + "(?!\\W)([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)|([^\\x00-\\xff]*)$", 'gi');
+ regexp = new RegExp("(?:\\B|\\W|\\s)" + flag + "(?![" + atSymbols + "])([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)$", 'gi');
match = regexp.exec(subtext);
@@ -80,67 +93,46 @@
}
}
},
- setup: _.debounce(function(input) {
+ setup: function(input) {
// Add GFM auto-completion to all input fields, that accept GFM input.
this.input = input || $('.js-gfm-input');
- // destroy previous instances
- this.destroyAtWho();
- // set up instances
- this.setupAtWho();
-
- if (this.dataSource && !this.dataLoading && !this.cachedData) {
- this.dataLoading = true;
- return this.fetchData(this.dataSource)
- .done((data) => {
- this.dataLoading = false;
- this.loadData(data);
- });
- };
-
- if (this.cachedData != null) {
- return this.loadData(this.cachedData);
- }
- }, 1000),
- setupAtWho: function() {
+ this.setupLifecycle();
+ },
+ setupLifecycle() {
+ this.input.each((i, input) => {
+ const $input = $(input);
+ $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
+ });
+ },
+ setupAtWho: function($input) {
// Emoji
- this.input.atwho({
+ $input.atwho({
at: ':',
- displayTpl: (function(_this) {
- return function(value) {
- if (value.path != null) {
- return _this.Emoji.template;
- } else {
- return _this.Loading.template;
- }
- };
- })(this),
+ displayTpl: function(value) {
+ return value.path != null ? this.Emoji.template : this.Loading.template;
+ }.bind(this),
insertTpl: ':${name}:',
- data: ['loading'],
startWithSpace: false,
+ skipSpecialCharacterTest: true,
+ data: this.defaultLoadingData,
callbacks: {
sorter: this.DefaultOptions.sorter,
- filter: this.DefaultOptions.filter,
beforeInsert: this.DefaultOptions.beforeInsert,
- matcher: this.DefaultOptions.matcher
+ filter: this.DefaultOptions.filter
}
});
// Team Members
- this.input.atwho({
+ $input.atwho({
at: '@',
- displayTpl: (function(_this) {
- return function(value) {
- if (value.username != null) {
- return _this.Members.template;
- } else {
- return _this.Loading.template;
- }
- };
- })(this),
+ displayTpl: function(value) {
+ return value.username != null ? this.Members.template : this.Loading.template;
+ }.bind(this),
insertTpl: '${atwho-at}${username}',
searchKey: 'search',
- data: ['loading'],
startWithSpace: false,
alwaysHighlightFirst: true,
+ skipSpecialCharacterTest: true,
+ data: this.defaultLoadingData,
callbacks: {
sorter: this.DefaultOptions.sorter,
filter: this.DefaultOptions.filter,
@@ -171,20 +163,14 @@
}
}
});
- this.input.atwho({
+ $input.atwho({
at: '#',
alias: 'issues',
searchKey: 'search',
- displayTpl: (function(_this) {
- return function(value) {
- if (value.title != null) {
- return _this.Issues.template;
- } else {
- return _this.Loading.template;
- }
- };
- })(this),
- data: ['loading'],
+ displayTpl: function(value) {
+ return value.title != null ? this.Issues.template : this.Loading.template;
+ }.bind(this),
+ data: this.defaultLoadingData,
insertTpl: '${atwho-at}${id}',
startWithSpace: false,
callbacks: {
@@ -206,25 +192,21 @@
}
}
});
- this.input.atwho({
+ $input.atwho({
at: '%',
alias: 'milestones',
searchKey: 'search',
- displayTpl: (function(_this) {
- return function(value) {
- if (value.title != null) {
- return _this.Milestones.template;
- } else {
- return _this.Loading.template;
- }
- };
- })(this),
- insertTpl: '${atwho-at}"${title}"',
- data: ['loading'],
+ insertTpl: '${atwho-at}${title}',
+ 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,
sorter: this.DefaultOptions.sorter,
+ beforeInsert: this.DefaultOptions.beforeInsert,
+ filter: this.DefaultOptions.filter,
beforeSave: function(milestones) {
return $.map(milestones, function(m) {
if (m.title == null) {
@@ -239,21 +221,15 @@
}
}
});
- this.input.atwho({
+ $input.atwho({
at: '!',
alias: 'mergerequests',
searchKey: 'search',
- displayTpl: (function(_this) {
- return function(value) {
- if (value.title != null) {
- return _this.Issues.template;
- } else {
- return _this.Loading.template;
- }
- };
- })(this),
- data: ['loading'],
startWithSpace: false,
+ displayTpl: function(value) {
+ return value.title != null ? this.Issues.template : this.Loading.template;
+ }.bind(this),
+ data: this.defaultLoadingData,
insertTpl: '${atwho-at}${id}',
callbacks: {
sorter: this.DefaultOptions.sorter,
@@ -274,17 +250,23 @@
}
}
});
- this.input.atwho({
+ $input.atwho({
at: '~',
alias: 'labels',
searchKey: 'search',
- displayTpl: this.Labels.template,
+ data: this.defaultLoadingData,
+ displayTpl: function(value) {
+ 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,
+ beforeInsert: this.DefaultOptions.beforeInsert,
+ filter: this.DefaultOptions.filter,
beforeSave: function(merges) {
+ if (gl.GfmAutoComplete.isLoading(merges)) return merges;
var sanitizeLabelTitle;
sanitizeLabelTitle = function(title) {
if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) {
@@ -295,7 +277,7 @@
};
return $.map(merges, function(m) {
return {
- title: sanitizeLabelTitle(m.title),
+ title: sanitize(m.title),
color: m.color,
search: "" + m.title
};
@@ -304,11 +286,14 @@
}
});
// We don't instantiate the slash commands autocomplete for note and issue/MR edit forms
- this.input.filter('[data-supports-slash-commands="true"]').atwho({
+ $input.filter('[data-supports-slash-commands="true"]').atwho({
at: '/',
alias: 'commands',
searchKey: 'search',
+ skipSpecialCharacterTest: true,
+ data: this.defaultLoadingData,
displayTpl: function(value) {
+ if (this.isLoading(value)) return this.Loading.template;
var tpl = '<li>/${name}';
if (value.aliases.length > 0) {
tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>';
@@ -321,7 +306,7 @@
}
tpl += '</li>';
return _.template(tpl)(value);
- },
+ }.bind(this),
insertTpl: function(value) {
var tpl = "/${name} ";
var reference_prefix = null;
@@ -339,6 +324,7 @@
filter: this.DefaultOptions.filter,
beforeInsert: this.DefaultOptions.beforeInsert,
beforeSave: function(commands) {
+ if (gl.GfmAutoComplete.isLoading(commands)) return commands;
return $.map(commands, function(c) {
var search = c.name;
if (c.aliases.length > 0) {
@@ -366,32 +352,40 @@
});
return;
},
- destroyAtWho: function() {
- return this.input.atwho('destroy');
- },
- fetchData: function(dataSource) {
- return $.getJSON(dataSource);
+ fetchData: function($input, at) {
+ if (this.isLoadingData[at]) return;
+ this.isLoadingData[at] = true;
+ if (this.cachedData[at]) {
+ this.loadData($input, at, this.cachedData[at]);
+ } else {
+ $.getJSON(this.dataSources[this.atTypeMap[at]], (data) => {
+ this.loadData($input, at, data);
+ }).fail(() => { this.isLoadingData[at] = false; });
+ }
},
- loadData: function(data) {
- this.cachedData = data;
- this.dataLoaded = true;
- // load members
- this.input.atwho('load', '@', data.members);
- // load issues
- this.input.atwho('load', 'issues', data.issues);
- // load milestones
- this.input.atwho('load', 'milestones', data.milestones);
- // load merge requests
- this.input.atwho('load', 'mergerequests', data.mergerequests);
- // load emojis
- this.input.atwho('load', ':', data.emojis);
- // load labels
- this.input.atwho('load', '~', data.labels);
- // load commands
- this.input.atwho('load', '/', data.commands);
+ loadData: function($input, at, data) {
+ this.isLoadingData[at] = false;
+ this.cachedData[at] = data;
+ $input.atwho('load', at, data);
// This trigger at.js again
// otherwise we would be stuck with loading until the user types
- return $(':focus').trigger('keyup');
+ return $input.trigger('keyup');
+ },
+ isLoading(data) {
+ if (!data) return false;
+ if (Array.isArray(data)) data = data[0];
+ return data === this.defaultLoadingData[0] || data.name === this.defaultLoadingData[0];
+ },
+ 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();
}
};
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 9a91018a8e4..bb516b3d2df 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -1,4 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, space-before-blocks, prefer-rest-params, max-len, vars-on-top, no-plusplus, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, semi, no-return-assign, no-else-return, camelcase, no-undef, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, padded-blocks, prefer-template, no-param-reassign, no-loop-func, no-extra-semi, keyword-spacing, no-mixed-operators, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, space-before-blocks, prefer-rest-params, max-len, vars-on-top, no-plusplus, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, semi, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, padded-blocks, prefer-template, no-param-reassign, no-loop-func, no-extra-semi, keyword-spacing, no-mixed-operators */
+/* global fuzzaldrinPlus */
+/* global Turbolinks */
+
(function() {
var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote,
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
@@ -20,7 +23,6 @@
this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
$inputContainer = this.input.parent();
$clearButton = $inputContainer.find('.js-dropdown-input-clear');
- this.indeterminateIds = [];
$clearButton.on('click', (function(_this) {
// Clear click
return function(e) {
@@ -188,7 +190,7 @@
})();
GitLabDropdown = (function() {
- var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, currentIndex;
+ var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex;
LOADING_CLASS = "is-loading";
@@ -341,16 +343,18 @@
selector = ".dropdown-page-one .dropdown-content a";
}
this.dropdown.on("click", selector, function(e) {
- var $el, selected;
+ var $el, selected, selectedObj, isMarking;
$el = $(this);
selected = self.rowClicked($el);
+ selectedObj = selected ? selected[0] : null;
+ isMarking = selected ? selected[1] : null;
if (self.options.clicked) {
- self.options.clicked(selected, $el, e);
+ self.options.clicked(selectedObj, $el, e, isMarking);
}
// Update label right after all modifications in dropdown has been done
if (self.options.toggleLabel) {
- self.updateLabel(selected, $el, self);
+ self.updateLabel(selectedObj, $el, self);
}
$el.trigger('blur');
@@ -441,12 +445,6 @@
this.resetRows();
this.addArrowKeyEvent();
- if (this.options.setIndeterminateIds) {
- this.options.setIndeterminateIds.call(this);
- }
- if (this.options.setActiveIds) {
- this.options.setActiveIds.call(this);
- }
// Makes indeterminate items effective
if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
this.parseData(this.fullData);
@@ -480,11 +478,6 @@
if (this.options.filterable) {
$input.blur().val("");
}
- // Triggering 'keyup' will re-render the dropdown which is not always required
- // specially if we want to keep the state of the dropdown needed for bulk-assignment
- if (!this.options.persistWhenHide) {
- $input.trigger("input");
- }
if (this.dropdown.find(".dropdown-toggle-page").length) {
$('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
}
@@ -617,7 +610,8 @@
};
GitLabDropdown.prototype.rowClicked = function(el) {
- var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value;
+ var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking;
+
fieldName = this.options.fieldName;
isInput = $(this.el).is('input');
if (this.renderedData) {
@@ -638,7 +632,7 @@
el.addClass(ACTIVE_CLASS);
}
- return selectedObject;
+ return [selectedObject];
}
field = [];
@@ -656,6 +650,7 @@
}
if (el.hasClass(ACTIVE_CLASS)) {
+ isMarking = false;
el.removeClass(ACTIVE_CLASS);
if (field && field.length) {
if (isInput) {
@@ -665,6 +660,7 @@
}
}
} else if (el.hasClass(INDETERMINATE_CLASS)) {
+ isMarking = true;
el.addClass(ACTIVE_CLASS);
el.removeClass(INDETERMINATE_CLASS);
if (field && field.length && value == null) {
@@ -674,6 +670,7 @@
this.addInput(fieldName, value, selectedObject);
}
} else {
+ isMarking = true;
if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) {
this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS);
if (!isInput) {
@@ -694,7 +691,7 @@
}
}
- return selectedObject;
+ return [selectedObject, isMarking];
};
GitLabDropdown.prototype.focusTextInput = function() {
diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6
index 6ce392d2a5b..63f9cafa8d0 100644
--- a/app/assets/javascripts/gl_field_errors.js.es6
+++ b/app/assets/javascripts/gl_field_errors.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign, padded-blocks */
//= require gl_field_error
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index db5d9e75b3a..7dc2d13e5d8 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -1,4 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-undef, no-new, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, padded-blocks, max-len */
+/* global GitLab */
+/* global DropzoneInput */
+/* global autosize */
+
(function() {
this.GLForm = (function() {
function GLForm(form) {
@@ -26,7 +30,7 @@
this.form.addClass('gfm-form');
// remove notify commit author checkbox for non-commit notes
gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button'));
- GitLab.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
+ gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
new DropzoneInput(this.form);
autosize(this.textarea);
// form and textarea event listeners
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js
index c3a132b3c75..2d08a7c6ac3 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors.js
@@ -1,4 +1,9 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, no-undef, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, padded-blocks */
+/* global ContributorsGraph */
+/* global ContributorsAuthorGraph */
+/* global ContributorsMasterGraph */
+/* global ContributorsStatGraphUtil */
+/* global d3 */
/*= require d3 */
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
index cb2448e8cc7..9c5e9381e52 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, padded-blocks, no-undef, newline-per-chained-call, no-else-return, max-len */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, padded-blocks, newline-per-chained-call, no-else-return */
+/* global d3 */
+/* global ContributorsGraph */
/*= require d3 */
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js
index 051ff98c774..1982f4af939 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js
@@ -2,7 +2,7 @@
(function() {
window.ContributorsStatGraphUtil = {
parse_log: function(log) {
- var by_author, by_email, data, entry, i, len, total;
+ var by_author, by_email, data, entry, i, len, total, normalized_email;
total = {};
by_author = {};
by_email = {};
@@ -11,7 +11,8 @@
if (total[entry.date] == null) {
this.add_date(entry.date, total);
}
- data = by_author[entry.author_name] || by_email[entry.author_email];
+ normalized_email = entry.author_email.toLowerCase();
+ data = by_author[entry.author_name] || by_email[normalized_email];
if (data == null) {
data = this.add_author(entry, by_author, by_email);
}
@@ -32,12 +33,14 @@
return collection[date].date = date;
},
add_author: function(author, by_author, by_email) {
- var data;
+ var data, normalized_email;
data = {};
data.author_name = author.author_name;
data.author_email = author.author_email;
+ normalized_email = author.author_email.toLowerCase();
by_author[author.author_name] = data;
- return by_email[author.author_email] = data;
+ by_email[normalized_email] = data;
+ return data;
},
store_data: function(entry, total, by_author) {
this.store_commits(total, by_author);
diff --git a/app/assets/javascripts/group_label_subscription.js.es6 b/app/assets/javascripts/group_label_subscription.js.es6
index eea6cd40859..8e10e424412 100644
--- a/app/assets/javascripts/group_label_subscription.js.es6
+++ b/app/assets/javascripts/group_label_subscription.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, padded-blocks, max-len */
+
(function(global) {
class GroupLabelSubscription {
constructor(container) {
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index 3dc6f05ca20..99700e7562a 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, no-undef, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, padded-blocks, max-len */
+/* global Api */
+
(function() {
var slice = [].slice;
@@ -14,7 +16,7 @@
multiple: $(select).hasClass('multiselect'),
minimumInputLength: 0,
query: function(query) {
- options = { all_available: all_available, skip_groups: skip_groups };
+ var options = { all_available: all_available, skip_groups: skip_groups };
return Api.groups(query.term, options, function(groups) {
var data;
data = {
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js
index 9425b6ed9d4..fa795be07ed 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -1,6 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, camelcase, no-var, one-var, one-var-declaration-per-line, prefer-template, quotes, object-shorthand, comma-dangle, no-unused-vars, prefer-arrow-callback, no-else-return, padded-blocks, vars-on-top, no-new, no-undef, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, camelcase, no-var, one-var, one-var-declaration-per-line, prefer-template, quotes, object-shorthand, comma-dangle, no-unused-vars, prefer-arrow-callback, no-else-return, padded-blocks, vars-on-top, no-new, max-len */
+
(function() {
- this.ImporterStatus = (function() {
+ window.ImporterStatus = (function() {
function ImporterStatus(jobs_url, import_url) {
this.jobs_url = jobs_url;
this.import_url = import_url;
@@ -75,7 +76,7 @@
var jobsImportPath = $('.js-importer-status').data('jobs-import-path');
var importPath = $('.js-importer-status').data('import-path');
- new ImporterStatus(jobsImportPath, importPath);
+ new window.ImporterStatus(jobsImportPath, importPath);
}
});
}).call(this);
diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6
index 46503c290ae..1c10a7445bb 100644
--- a/app/assets/javascripts/issuable.js.es6
+++ b/app/assets/javascripts/issuable.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* 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 */
+/* global Issuable */
+/* global Turbolinks */
+
(function() {
var issuable_created;
@@ -141,6 +144,9 @@
const $issuesOtherFilters = $('.issues-other-filters');
const $issuesBulkUpdate = $('.issues_bulk_update');
+ this.issuableBulkActions.willUpdateLabels = false;
+ this.issuableBulkActions.setOriginalDropdownData();
+
if ($checkedIssues.length > 0) {
let ids = $.map($checkedIssues, function(value) {
return $(value).data('id');
@@ -152,7 +158,6 @@
$updateIssuesIds.val([]);
$issuesBulkUpdate.hide();
$issuesOtherFilters.show();
- this.issuableBulkActions.willUpdateLabels = false;
}
return true;
},
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index 317818951fd..4aaad111082 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, no-undef, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, padded-blocks, max-len */
+/* global UsersSelect */
+
(function() {
this.IssuableContext = (function() {
function IssuableContext(currentUser) {
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 50fdbc89c7c..1c4086517fe 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -1,4 +1,9 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-undef, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, radix, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, radix, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, padded-blocks, max-len */
+/* global GitLab */
+/* global UsersSelect */
+/* global ZenMode */
+/* global Autosave */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
@@ -14,7 +19,7 @@
this.renderWipExplanation = bind(this.renderWipExplanation, this);
this.resetAutosave = bind(this.resetAutosave, this);
this.handleSubmit = bind(this.handleSubmit, this);
- GitLab.GfmAutoComplete.setup();
+ gl.GfmAutoComplete.setup();
new UsersSelect();
new ZenMode();
this.titleField = this.form.find("input[name*='[title]']");
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 8540b199aba..63b70d4be17 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-undef, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, padded-blocks, max-len */
+/* global Flash */
/*= require flash */
/*= require jquery.waitforimages */
diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js.es6
index 9697fb33566..52fd5d71b18 100644
--- a/app/assets/javascripts/issues_bulk_assignment.js.es6
+++ b/app/assets/javascripts/issues_bulk_assignment.js.es6
@@ -1,10 +1,14 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, radix, max-len, padded-blocks, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
+/* global Issuable */
+/* global Flash */
+
((global) => {
class IssuableBulkActions {
- constructor({ container, form, issues } = {}) {
- this.container = container || $('.content'),
+ constructor({ container, form, issues, prefixId } = {}) {
+ this.prefixId = prefixId || 'issue_';
this.form = form || this.getElement('.bulk-update');
+ this.$labelDropdown = this.form.find('.js-label-select');
this.issues = issues || this.getElement('.issues-list .issue');
this.form.data('bulkActions', this);
this.willUpdateLabels = false;
@@ -13,10 +17,6 @@
Issuable.initChecks();
}
- getElement(selector) {
- return this.container.find(selector);
- }
-
bindEvents() {
return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
}
@@ -70,10 +70,7 @@
getUnmarkedIndeterminedLabels() {
const result = [];
- const labelsToKeep = [];
-
- this.getElement('.labels-filter .is-indeterminate')
- .each((i, el) => labelsToKeep.push($(el).data('labelId')));
+ const labelsToKeep = this.$labelDropdown.data('indeterminate');
this.getLabelsFromSelection().forEach((id) => {
if (labelsToKeep.indexOf(id) === -1) {
@@ -103,45 +100,65 @@
}
};
if (this.willUpdateLabels) {
- this.getLabelsToApply().map(function(id) {
- return formData.update.add_label_ids.push(id);
- });
- this.getLabelsToRemove().map(function(id) {
- return formData.update.remove_label_ids.push(id);
- });
+ formData.update.add_label_ids = this.$labelDropdown.data('marked');
+ formData.update.remove_label_ids = this.$labelDropdown.data('unmarked');
}
return formData;
}
- getLabelsToApply() {
+ setOriginalDropdownData() {
+ const $labelSelect = $('.bulk-update .js-label-select');
+ $labelSelect.data('common', this.getOriginalCommonIds());
+ $labelSelect.data('marked', this.getOriginalMarkedIds());
+ $labelSelect.data('indeterminate', this.getOriginalIndeterminateIds());
+ }
+
+ // From issuable's initial bulk selection
+ getOriginalCommonIds() {
const labelIds = [];
- const $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]');
- $labels.each(function(k, label) {
- if (label) {
- return labelIds.push(parseInt($(label).val()));
- }
+
+ this.getElement('.selected_issue:checked').each((i, el) => {
+ labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
});
- return labelIds;
+ return _.intersection.apply(this, labelIds);
}
+ // From issuable's initial bulk selection
+ getOriginalMarkedIds() {
+ const labelIds = [];
+ this.getElement('.selected_issue:checked').each((i, el) => {
+ labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
+ });
+ return _.intersection.apply(this, labelIds);
+ }
- /**
- * Returns Label IDs that will be removed from issue selection
- * @return {Array} Array of labels IDs
- */
-
- getLabelsToRemove() {
- const result = [];
- const indeterminatedLabels = this.getUnmarkedIndeterminedLabels();
- const labelsToApply = this.getLabelsToApply();
- indeterminatedLabels.map(function(id) {
- // We need to exclude label IDs that will be applied
- // By not doing this will cause issues from selection to not add labels at all
- if (labelsToApply.indexOf(id) === -1) {
- return result.push(id);
- }
+ // From issuable's initial bulk selection
+ getOriginalIndeterminateIds() {
+ const uniqueIds = [];
+ const labelIds = [];
+ let issuableLabels = [];
+
+ // Collect unique label IDs for all checked issues
+ this.getElement('.selected_issue:checked').each((i, el) => {
+ issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels');
+ issuableLabels.forEach((labelId) => {
+ // Store unique IDs
+ if (uniqueIds.indexOf(labelId) === -1) {
+ uniqueIds.push(labelId);
+ }
+ });
+ // Store array of IDs per issuable
+ labelIds.push(issuableLabels);
});
- return result;
+ // Add uniqueIds to add it as argument for _.intersection
+ labelIds.unshift(uniqueIds);
+ // Return IDs that are present but not in all selected issueables
+ return _.difference(uniqueIds, _.intersection.apply(this, labelIds));
+ }
+
+ getElement(selector) {
+ this.scopeEl = this.scopeEl || $('.content');
+ return this.scopeEl.find(selector);
}
}
diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js.es6
index 175623e7448..33c5e35324d 100644
--- a/app/assets/javascripts/label_manager.js.es6
+++ b/app/assets/javascripts/label_manager.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, padded-blocks, max-len */
+/* global Flash */
+
((global) => {
class LabelManager {
@@ -104,4 +106,3 @@
gl.LabelManager = LabelManager;
})(window.gl || (window.gl = {}));
-
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index f334f35594d..ec2fc87bece 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -1,12 +1,16 @@
-/* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, no-undef, semi, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread, padded-blocks, max-len */
+/* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, semi, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread, padded-blocks */
+/* global Issuable */
+/* global ListLabel */
+
(function() {
this.LabelsSelect = (function() {
function LabelsSelect() {
var _this;
_this = this;
$('.js-label-select').each(function(i, dropdown) {
- var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove;
+ var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer;
$dropdown = $(dropdown);
+ $dropdownContainer = $dropdown.closest('.labels-filter');
$toggleText = $dropdown.find('.dropdown-toggle-text');
namespacePath = $dropdown.data('namespace-path');
projectPath = $dropdown.data('project-path');
@@ -122,7 +126,7 @@
});
});
};
- return $dropdown.glDropdown({
+ $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: function(term, callback) {
return $.ajax({
@@ -169,33 +173,40 @@
});
},
renderRow: function(label, instance) {
- var $a, $li, active, color, colorEl, indeterminate, removesAll, selectedClass, spacing;
+ var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue;
$li = $('<li>');
$a = $('<a href="#">');
selectedClass = [];
removesAll = label.id <= 0 || (label.id == null);
if ($dropdown.hasClass('js-filter-bulk-update')) {
- indeterminate = instance.indeterminateIds;
- active = instance.activeIds;
+ indeterminate = $dropdown.data('indeterminate') || [];
+ marked = $dropdown.data('marked') || [];
+
if (indeterminate.indexOf(label.id) !== -1) {
selectedClass.push('is-indeterminate');
}
- if (active.indexOf(label.id) !== -1) {
+
+ if (marked.indexOf(label.id) !== -1) {
// Remove is-indeterminate class if the item will be marked as active
i = selectedClass.indexOf('is-indeterminate');
if (i !== -1) {
selectedClass.splice(i, 1);
}
selectedClass.push('is-active');
- // Add input manually
- instance.addInput(this.fieldName, label.id);
}
- }
- if (this.id(label) && $form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + this.id(label).toString().replace(/'/g, '\\\'') + "']").length) {
- selectedClass.push('is-active');
- }
- if ($dropdown.hasClass('js-multiselect') && removesAll) {
- selectedClass.push('dropdown-clear-active');
+ } else {
+ if (this.id(label)) {
+ dropdownName = $dropdown.data('fieldName');
+ dropdownValue = this.id(label).toString().replace(/'/g, '\\\'');
+
+ if ($form.find("input[type='hidden'][name='" + dropdownName + "'][value='" + dropdownValue + "']").length) {
+ selectedClass.push('is-active');
+ }
+ }
+
+ if ($dropdown.hasClass('js-multiselect') && removesAll) {
+ selectedClass.push('dropdown-clear-active');
+ }
}
if (label.duplicate) {
spacing = 100 / label.color.length;
@@ -231,7 +242,6 @@
// Return generated html
return $li.html($a).prop('outerHTML');
},
- persistWhenHide: $dropdown.data('persistWhenHide'),
search: {
fields: ['title']
},
@@ -310,18 +320,15 @@
}
}
}
- if ($dropdown.hasClass('js-filter-bulk-update')) {
- // If we are persisting state we need the classes
- if (!this.options.persistWhenHide) {
- return $dropdown.parent().find('.is-active, .is-indeterminate').removeClass();
- }
- }
},
multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'),
- clicked: function(label, $el, e) {
+ clicked: function(label, $el, e, isMarking) {
var isIssueIndex, isMRIndex, page;
- _this.enableBulkLabelDropdown();
+
+ page = $('body').data('page');
+ isIssueIndex = page === 'projects:issues:index';
+ isMRIndex = page === 'projects:merge_requests:index';
if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
$dropdown.parent()
@@ -330,12 +337,11 @@
}
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
+ _this.enableBulkLabelDropdown();
+ _this.setDropdownData($dropdown, isMarking, this.id(label));
return;
}
- page = $('body').data('page');
- isIssueIndex = page === 'projects:issues:index';
- isMRIndex = page === 'projects:merge_requests:index';
if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
if (label.isAny) {
gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
@@ -397,17 +403,10 @@
}
}
},
- setIndeterminateIds: function() {
- if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
- return this.indeterminateIds = _this.getIndeterminateIds();
- }
- },
- setActiveIds: function() {
- if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
- return this.activeIds = _this.getActiveIds();
- }
- }
});
+
+ // Set dropdown data
+ _this.setOriginalDropdownData($dropdownContainer, $dropdown);
});
this.bindEvents();
}
@@ -420,34 +419,9 @@
if ($('.selected_issue:checked').length) {
return;
}
- // Remove inputs
- $('.issues_bulk_update .labels-filter input[type="hidden"]').remove();
- // Also restore button text
return $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label');
};
- LabelsSelect.prototype.getIndeterminateIds = function() {
- var label_ids;
- label_ids = [];
- $('.selected_issue:checked').each(function(i, el) {
- var issue_id;
- issue_id = $(el).data('id');
- return label_ids.push($("#issue_" + issue_id).data('labels'));
- });
- return _.flatten(label_ids);
- };
-
- LabelsSelect.prototype.getActiveIds = function() {
- var label_ids;
- label_ids = [];
- $('.selected_issue:checked').each(function(i, el) {
- var issue_id;
- issue_id = $(el).data('id');
- return label_ids.push($("#issue_" + issue_id).data('labels'));
- });
- return _.intersection.apply(_, label_ids);
- };
-
LabelsSelect.prototype.enableBulkLabelDropdown = function() {
var issuableBulkActions;
if ($('.selected_issue:checked').length) {
@@ -456,8 +430,59 @@
}
};
- return LabelsSelect;
+ LabelsSelect.prototype.setDropdownData = function($dropdown, isMarking, value) {
+ var i, markedIds, unmarkedIds, indeterminateIds;
+ var issuableBulkActions = $('.bulk-update').data('bulkActions');
+ markedIds = $dropdown.data('marked') || [];
+ unmarkedIds = $dropdown.data('unmarked') || [];
+ indeterminateIds = $dropdown.data('indeterminate') || [];
+
+ if (isMarking) {
+ markedIds.push(value);
+
+ i = indeterminateIds.indexOf(value);
+ if (i > -1) {
+ indeterminateIds.splice(i, 1);
+ }
+
+ i = unmarkedIds.indexOf(value);
+ if (i > -1) {
+ unmarkedIds.splice(i, 1);
+ }
+ } else {
+ // If marked item (not common) is unmarked
+ i = markedIds.indexOf(value);
+ if (i > -1) {
+ markedIds.splice(i, 1);
+ }
+
+ // If an indeterminate item is being unmarked
+ if (issuableBulkActions.getOriginalIndeterminateIds().indexOf(value) > -1) {
+ unmarkedIds.push(value);
+ }
+
+ // If a marked item is being unmarked
+ // (a marked item could also be a label that is present in all selection)
+ if (issuableBulkActions.getOriginalCommonIds().indexOf(value) > -1) {
+ unmarkedIds.push(value);
+ }
+ }
+
+ $dropdown.data('marked', markedIds);
+ $dropdown.data('unmarked', unmarkedIds);
+ $dropdown.data('indeterminate', indeterminateIds);
+ };
+
+ LabelsSelect.prototype.setOriginalDropdownData = function($container, $dropdown) {
+ var labels = [];
+ $container.find('[name="label_name[]"]').map(function() {
+ return labels.push(this.value);
+ });
+ $dropdown.data('marked', labels);
+ };
+
+ return LabelsSelect;
})();
}).call(this);
diff --git a/app/assets/javascripts/lib/ace.js b/app/assets/javascripts/lib/ace.js
index b1718e89d3d..4cdf99cae72 100644
--- a/app/assets/javascripts/lib/ace.js
+++ b/app/assets/javascripts/lib/ace.js
@@ -1,3 +1,2 @@
-/* eslint-disable */
/*= require ace-rails-ap */
/*= require ace/ext-searchbox */
diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js
index d0fe69260a5..3c9ad0e67c8 100644
--- a/app/assets/javascripts/lib/utils/notify.js
+++ b/app/assets/javascripts/lib/utils/notify.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, no-undef, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, max-len */
+
(function() {
(function(w) {
var notificationGranted, notifyMe, notifyPermissions;
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index b0f834705c3..9af89b79f84 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-param-reassign, no-undef, prefer-template, quotes, comma-dangle, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, spaced-comment, radix, no-else-return, max-len, no-plusplus, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-param-reassign, prefer-template, quotes, comma-dangle, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, spaced-comment, radix, no-else-return, max-len, no-plusplus, padded-blocks */
+
// LineHighlighter
//
// Handles single- and multi-line selection and highlight for blob views.
diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
index 9e4ffd07dbd..f95b079c972 100644
--- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
+++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, prefer-const, no-new, padded-blocks, no-param-reassign, semi, max-len */
+/* global Vue */
+/* global ace */
+/* global Flash */
+
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
index 23c4618af70..74544b7d0c7 100644
--- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
+++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */
+/* global Vue */
+
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
index 4ccbdcd6daa..78c00c31c16 100644
--- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
+++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */
+/* global Vue */
+
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
@@ -19,7 +21,7 @@
</td>
<td class="diff-line-num old_line" :class="lineCssClass(line)" v-if="!line.isHeader">{{line.lineNumber}}</td>
<td class="line_content parallel" :class="lineCssClass(line)" v-if="!line.isHeader" v-html="line.richText"></td>
- </template>
+ </template>
</tr>
</table>
`,
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
index 8a7519b0786..8df3170edac 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-param-reassign, comma-dangle, no-extra-semi, padded-blocks */
+
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
index f94e51e783c..53b44007510 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, object-shorthand, no-dupe-keys, no-param-reassign, no-plusplus, camelcase, prefer-const, no-nested-ternary, no-continue, semi, func-call-spacing, no-spaced-func, padded-blocks, max-len */
+/* global Cookies */
+/* global Vue */
+
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
index 815443fb54e..83520702f9b 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable new-cap, comma-dangle, no-new, semi */
+/* global Vue */
+/* global Flash */
+
//= require vue
//= require ./merge_conflict_store
//= require ./merge_conflict_service
diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
index c8de586aa21..e89b35d5407 100644
--- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
+++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-param-reassign, comma-dangle, padded-blocks */
+
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
index 88c3a20ce13..a4aca85d460 100644
--- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
+++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-param-reassign, quote-props, comma-dangle, padded-blocks */
+
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 88c3636be6c..244c2f6746c 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-undef, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, padded-blocks, max-len, prefer-arrow-callback */
+/* global MergeRequestTabs */
/*= require jquery.waitforimages */
/*= require task_list */
@@ -26,6 +27,7 @@
// Prevent duplicate event bindings
this.disableTaskList();
this.initMRBtnListeners();
+ this.initCommitMessageListeners();
if ($("a.btn-close").length) {
this.initTaskList();
}
@@ -107,6 +109,26 @@
// note so that we can re-use its form here
};
+ MergeRequest.prototype.initCommitMessageListeners = function() {
+ var textarea = $('textarea.js-commit-message');
+
+ $('a.js-with-description-link').on('click', function(e) {
+ e.preventDefault();
+
+ textarea.val(textarea.data('messageWithDescription'));
+ $('p.js-with-description-hint').hide();
+ $('p.js-without-description-hint').show();
+ });
+
+ $('a.js-without-description-link').on('click', function(e) {
+ e.preventDefault();
+
+ textarea.val(textarea.data('messageWithoutDescription'));
+ $('p.js-with-description-hint').show();
+ $('p.js-without-description-hint').hide();
+ });
+ };
+
return MergeRequest;
})();
diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6
index 7022aa1263b..e47047c4cca 100644
--- a/app/assets/javascripts/merge_request_widget.js.es6
+++ b/app/assets/javascripts/merge_request_widget.js.es6
@@ -1,5 +1,10 @@
-/* eslint-disable */
- ((global) => {
+/* eslint-disable max-len, no-var, func-names, space-before-function-paren, vars-on-top, no-plusplus, comma-dangle, no-return-assign, consistent-return, no-param-reassign, one-var, one-var-declaration-per-line, quotes, prefer-template, no-else-return, prefer-arrow-callback, no-unused-vars, no-underscore-dangle, no-shadow, no-mixed-operators, template-curly-spacing, camelcase, default-case, wrap-iife, semi, padded-blocks */
+/* global notify */
+/* global notifyPermissions */
+/* global merge_request_widget */
+/* global Turbolinks */
+
+((global) => {
var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
const DEPLOYMENT_TEMPLATE = `<div class="mr-widget-heading" id="<%- id %>">
@@ -27,7 +32,7 @@
</div>
</div>`;
- global.MergeRequestWidget = (function() {
+ global.MergeRequestWidget = (function() {
function MergeRequestWidget(opts) {
// Initialize MergeRequestWidget behavior
//
@@ -82,10 +87,10 @@
};
MergeRequestWidget.prototype.retrieveSuccessIcon = function() {
- const $ciSuccessIcon = $('.js-success-icon');
- this.$ciSuccessIcon = $ciSuccessIcon.html();
- $ciSuccessIcon.remove();
- }
+ const $ciSuccessIcon = $('.js-success-icon');
+ this.$ciSuccessIcon = $ciSuccessIcon.html();
+ $ciSuccessIcon.remove();
+ }
MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) {
if (deleteSourceBranch == null) {
@@ -204,7 +209,7 @@
const template = _.template(templateString)(environment)
this.$widgetBody.before(template);
}
- };
+ };
MergeRequestWidget.prototype.showCIStatus = function(state) {
var allowed_states;
@@ -246,4 +251,4 @@
})();
- })(window.gl || (window.gl = {}));
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js
index 15a12c3d985..9f8af46c715 100644
--- a/app/assets/javascripts/merged_buttons.js
+++ b/app/assets/javascripts/merged_buttons.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-undef, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, padded-blocks, max-len */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index db7561a3a75..42152362e60 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, no-undef, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, padded-blocks, max-len */
+/* global Flash */
+
(function() {
this.Milestone = (function() {
Milestone.updateIssue = function(li, issue_url, data) {
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 67796083790..28054b78249 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -1,4 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-undef, no-param-reassign, no-shadow, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow, padded-blocks */
+/* global Vue */
+/* global Issuable */
+/* global ListMilestone */
+
(function() {
this.MilestoneSelect = (function() {
function MilestoneSelect(currentProject) {
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
index 87c903ec576..6633f2c2709 100644
--- a/app/assets/javascripts/namespace_select.js
+++ b/app/assets/javascripts/namespace_select.js
@@ -1,8 +1,10 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, no-undef, prefer-arrow-callback, padded-blocks, no-param-reassign, no-cond-assign, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, padded-blocks, no-param-reassign, no-cond-assign, max-len */
+/* global Api */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
- this.NamespaceSelect = (function() {
+ window.NamespaceSelect = (function() {
function NamespaceSelect(opts) {
this.onSelectItem = bind(this.onSelectItem, this);
var fieldName, showAny;
@@ -64,7 +66,7 @@
})();
- this.NamespaceSelects = (function() {
+ window.NamespaceSelects = (function() {
function NamespaceSelects(opts) {
var ref;
if (opts == null) {
@@ -74,7 +76,7 @@
this.$dropdowns.each(function(i, dropdown) {
var $dropdown;
$dropdown = $(dropdown);
- return new NamespaceSelect({
+ return new window.NamespaceSelect({
dropdown: $dropdown
});
});
diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js
index e3dc599b90a..64b19a54893 100644
--- a/app/assets/javascripts/network/branch_graph.js
+++ b/app/assets/javascripts/network/branch_graph.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, new-cap, no-undef, no-plusplus, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, new-cap, no-plusplus, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len, padded-blocks */
+/* global Raphael */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js
index 5a8f723a27b..2367d2497b2 100644
--- a/app/assets/javascripts/network/network.js
+++ b/app/assets/javascripts/network/network.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, no-undef, quote-props, prefer-template, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, padded-blocks, max-len */
+/* global BranchGraph */
+
(function() {
this.Network = (function() {
function Network(opts) {
diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js
index 732d92845cb..17833d3419a 100644
--- a/app/assets/javascripts/network/network_bundle.js
+++ b/app/assets/javascripts/network/network_bundle.js
@@ -1,4 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, no-undef, comma-dangle, consistent-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, padded-blocks, max-len */
+/* global Network */
+/* global ShortcutsNetwork */
+
// This is a manifest file that'll be compiled into including all the files listed below.
// Add new JavaScript code in separate files in this directory and they'll automatically
// be included in the compiled file accessible from http://example.com/assets/application.js
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 0ca0e255595..a8b9a352870 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,4 +1,8 @@
-/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, no-undef, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, semi, indent, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape, radix, padded-blocks, max-len */
+/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, semi, indent, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape, radix, padded-blocks */
+/* global Flash */
+/* global GLForm */
+/* global Autosave */
+/* global ResolveService */
/*= require autosave */
/*= require autosize */
@@ -305,7 +309,7 @@
}
row = form.closest("tr");
note_html = $(note.html);
- note_html.syntaxHighlight();
+ note_html.renderGFM();
// is this the first note of discussion?
discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
if ((note.original_discussion_id != null) && discussionContainer.length === 0) {
@@ -322,7 +326,7 @@
discussionContainer.append(note_html);
// Init discussion on 'Discussion' page if it is merge request page
if ($('body').attr('data-page').indexOf('projects:merge_request') === 0) {
- $('ul.main-notes-list').append(note.discussion_html).syntaxHighlight();
+ $('ul.main-notes-list').append(note.discussion_html).renderGFM();
}
} else {
// append new note to all matching discussions
@@ -463,7 +467,7 @@
// Convert returned HTML to a jQuery object so we can modify it further
$html = $(note.html);
gl.utils.localTimeAgo($('.js-timeago', $html));
- $html.syntaxHighlight();
+ $html.renderGFM();
$html.find('.js-task-list-container').taskList('enable');
// Find the note's `li` element by ID and replace it with the updated HTML
$note_li = $('.note-row-' + note.id);
@@ -671,7 +675,7 @@
*/
Notes.prototype.addDiffNote = function(e) {
- var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, replyButton, row, rowCssToAdd, targetContent;
+ var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent;
e.preventDefault();
$link = $(e.currentTarget);
row = $link.closest("tr");
diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js
index b152d26733f..5d0d594073d 100644
--- a/app/assets/javascripts/notifications_dropdown.js
+++ b/app/assets/javascripts/notifications_dropdown.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, no-undef, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, padded-blocks, max-len */
+/* global Flash */
+
(function() {
this.NotificationsDropdown = (function() {
function NotificationsDropdown() {
@@ -17,7 +19,7 @@
});
$(document).off('ajax:success', '.notification-form').on('ajax:success', '.notification-form', function(e, data) {
if (data.saved) {
- return $(e.currentTarget).closest('.notification-dropdown').replaceWith(data.html);
+ return $(e.currentTarget).closest('.js-notification-dropdown').replaceWith(data.html);
} else {
return new Flash('Failed to save new settings', 'alert');
}
diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6
index 72c6c4a1fcd..0b09ad113a3 100644
--- a/app/assets/javascripts/pipelines.js.es6
+++ b/app/assets/javascripts/pipelines.js.es6
@@ -1,10 +1,11 @@
+/* eslint-disable no-new, guard-for-in, no-restricted-syntax, no-continue, padded-blocks, no-param-reassign, max-len */
+
//= require lib/utils/bootstrap_linked_tabs
-/* eslint-disable */
((global) => {
class Pipelines {
- constructor(options) {
+ constructor(options = {}) {
if (options.initTabs && options.tabsOptions) {
new global.LinkedTabs(options.tabsOptions);
@@ -14,9 +15,11 @@
}
addMarginToBuildColumns() {
- this.pipelineGraph = document.querySelector('.pipeline-graph');
- const secondChildBuildNodes = document.querySelector('.pipeline-graph').querySelectorAll('.build:nth-child(2)');
- for (buildNodeIndex in secondChildBuildNodes) {
+ this.pipelineGraph = document.querySelector('.js-pipeline-graph');
+
+ const secondChildBuildNodes = this.pipelineGraph.querySelectorAll('.build:nth-child(2)');
+
+ for (const buildNodeIndex in secondChildBuildNodes) {
const buildNode = secondChildBuildNodes[buildNodeIndex];
const firstChildBuildNode = buildNode.previousElementSibling;
if (!firstChildBuildNode || !firstChildBuildNode.matches('.build')) continue;
@@ -28,6 +31,7 @@
const columnBuilds = previousColumn.querySelectorAll('.build');
if (columnBuilds.length === 1) previousColumn.classList.add('no-margin');
}
+
this.pipelineGraph.classList.remove('hidden');
}
}
diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js
index 3723aa24942..1e261cd49c2 100644
--- a/app/assets/javascripts/preview_markdown.js
+++ b/app/assets/javascripts/preview_markdown.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, wrap-iife, no-else-return, consistent-return, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, no-undef, camelcase, prefer-arrow-callback, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, wrap-iife, no-else-return, consistent-return, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, camelcase, prefer-arrow-callback, max-len */
+
// MarkdownPreview
//
// Handles toggling the "Write" and "Preview" tab clicks, rendering the preview,
@@ -7,7 +8,7 @@
(function() {
var lastTextareaPreviewed, markdownPreview, previewButtonSelector, writeButtonSelector;
- this.MarkdownPreview = (function() {
+ window.MarkdownPreview = (function() {
function MarkdownPreview() {}
// Minimum number of users referenced before triggering a warning
@@ -27,7 +28,7 @@
return this.renderMarkdown(mdText, (function(_this) {
return function(response) {
preview.html(response.body);
- preview.syntaxHighlight();
+ preview.renderGFM();
return _this.renderReferencedUsers(response.references.users, form);
};
})(this));
@@ -83,7 +84,7 @@
})();
- markdownPreview = new MarkdownPreview();
+ markdownPreview = new window.MarkdownPreview();
previewButtonSelector = '.js-md-preview-button';
diff --git a/app/assets/javascripts/profile/gl_crop.js.es6 b/app/assets/javascripts/profile/gl_crop.js.es6
index 6da6c1d0295..b4b6da41f63 100644
--- a/app/assets/javascripts/profile/gl_crop.js.es6
+++ b/app/assets/javascripts/profile/gl_crop.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-useless-escape, max-len, padded-blocks, quotes, no-var, no-underscore-dangle, func-names, space-before-function-paren, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, no-plusplus, new-parens, semi */
+
((global) => {
// Matches everything but the file name
diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js.es6
index 3eb81808bd6..eb2fe3477a2 100644
--- a/app/assets/javascripts/profile/profile.js.es6
+++ b/app/assets/javascripts/profile/profile.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len, padded-blocks */
+/* global Flash */
+
((global) => {
class Profile {
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index 016d999d77e..fcf3a4af956 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -1,4 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-undef, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, semi, vars-on-top, indent, prefer-template, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, semi, vars-on-top, indent, prefer-template, padded-blocks, max-len */
+/* global Cookies */
+/* global Turbolinks */
+/* global ProjectSelect */
+
(function() {
this.Project = (function() {
function Project() {
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index 804306a3293..1bd232314d0 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, no-undef, object-shorthand, no-param-reassign, comma-dangle, no-plusplus, prefer-template, no-unused-vars, no-return-assign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, object-shorthand, no-param-reassign, comma-dangle, no-plusplus, prefer-template, no-unused-vars, no-return-assign, padded-blocks */
+/* global fuzzaldrinPlus */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js
index c99e55234cf..02dafcfb865 100644
--- a/app/assets/javascripts/project_import.js
+++ b/app/assets/javascripts/project_import.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-undef, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, padded-blocks, max-len */
+/* global Turbolinks */
+
(function() {
this.ProjectImport = (function() {
function ProjectImport() {
diff --git a/app/assets/javascripts/project_label_subscription.js.es6 b/app/assets/javascripts/project_label_subscription.js.es6
index 03a115cb35b..b8d6a198996 100644
--- a/app/assets/javascripts/project_label_subscription.js.es6
+++ b/app/assets/javascripts/project_label_subscription.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable wrap-iife, func-names, space-before-function-paren, object-shorthand, comma-dangle, one-var, one-var-declaration-per-line, no-restricted-syntax, prefer-const, max-len, no-param-reassign, padded-blocks */
+
(function(global) {
class ProjectLabelSubscription {
constructor(container) {
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index fe1f96872f3..650996700ba 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-undef, no-else-return, quotes, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, padded-blocks, max-len */
+/* global Api */
+
(function() {
this.ProjectSelect = (function() {
function ProjectSelect() {
diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js
index dbf530bed41..4548dc68fe1 100644
--- a/app/assets/javascripts/projects_list.js
+++ b/app/assets/javascripts/projects_list.js
@@ -1,6 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-var, one-var, one-var-declaration-per-line, no-undef, prefer-arrow-callback, consistent-return, no-unused-vars, camelcase, prefer-template, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-var, one-var, one-var-declaration-per-line, prefer-arrow-callback, consistent-return, no-unused-vars, camelcase, prefer-template, comma-dangle, padded-blocks, max-len */
+
(function() {
- this.ProjectsList = {
+ window.ProjectsList = {
init: function() {
$(".projects-list-filter").off('keyup');
this.initSearch();
@@ -9,7 +10,7 @@
initSearch: function() {
var debounceFilter, projectsListFilter;
projectsListFilter = $('.projects-list-filter');
- debounceFilter = _.debounce(ProjectsList.filterResults, 500);
+ debounceFilter = _.debounce(window.ProjectsList.filterResults, 500);
return projectsListFilter.on('keyup', function(e) {
if (projectsListFilter.val() !== '') {
return debounceFilter();
diff --git a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
index 2d60947a666..4aef1c84b56 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable arrow-parens, no-param-reassign, no-irregular-whitespace, object-shorthand, no-else-return, comma-dangle, semi, padded-blocks, max-len */
+
(global => {
global.gl = global.gl || {};
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
index c45c9d8ff22..f26fba979a4 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable no-new, arrow-parens, no-param-reassign, no-irregular-whitespace, comma-dangle, padded-blocks, semi, max-len */
+/* global ProtectedBranchDropdown */
+
(global => {
global.gl = global.gl || {};
diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
index e3f226e9a2a..08264ad3d2f 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, no-unused-vars */
+
class ProtectedBranchDropdown {
constructor(options) {
this.onSelect = options.onSelect;
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
index ac3142ffb07..4ff2fa5a80f 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable no-new, arrow-parens, no-param-reassign, no-irregular-whitespace, padded-blocks, comma-dangle, no-trailing-spaces, semi, max-len */
+/* global Flash */
+
(global => {
global.gl = global.gl || {};
@@ -33,7 +35,7 @@
const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`);
// Do not update if one dropdown has not selected any option
- if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return;
+ if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return;
$.ajax({
type: 'POST',
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
index 705378a364d..b6972ef2e16 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable arrow-parens, no-param-reassign, no-irregular-whitespace, no-new, comma-dangle, semi, padded-blocks, max-len */
+
(global => {
global.gl = global.gl || {};
diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
index 17e34163831..15b3affd469 100644
--- a/app/assets/javascripts/protected_branches/protected_branches_bundle.js
+++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
@@ -1,2 +1 @@
-/* eslint-disable */
/*= require_tree . */
diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js
new file mode 100644
index 00000000000..bbb2f186655
--- /dev/null
+++ b/app/assets/javascripts/render_gfm.js
@@ -0,0 +1,16 @@
+/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len */
+// Render Gitlab flavoured Markdown
+//
+// Delegates to syntax highlight and render math
+//
+(function() {
+ $.fn.renderGFM = function() {
+ this.find('.js-syntax-highlight').syntaxHighlight();
+ this.find('.js-render-math').renderMath();
+ };
+
+ $(document).on('ready page:load', function() {
+ return $('body').renderGFM();
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/render_math.js
new file mode 100644
index 00000000000..209e7a8661d
--- /dev/null
+++ b/app/assets/javascripts/render_math.js
@@ -0,0 +1,55 @@
+/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len, no-console */
+// Renders math using KaTeX in any element with the
+// `js-render-math` class
+//
+// ### Example Markup
+//
+// <code class="js-render-math"></div>
+//
+(function() {
+ // Only load once
+ var katexLoaded = false;
+
+ // Loop over all math elements and render math
+ var renderWithKaTeX = function (elements) {
+ elements.each(function () {
+ var mathNode = $('<span></span>');
+ var $this = $(this);
+
+ var display = $this.attr('data-math-style') === 'display';
+ try {
+ katex.render($this.text(), mathNode.get(0), { displayMode: display });
+ mathNode.insertAfter($this);
+ $this.remove();
+ } catch (err) {
+ // What can we do??
+ console.log(err.message);
+ }
+ });
+ };
+
+ $.fn.renderMath = function() {
+ var $this = this;
+ if ($this.length === 0) return;
+
+ if (katexLoaded) renderWithKaTeX($this);
+ else {
+ // Request CSS file so it is in the cache
+ $.get(gon.katex_css_url, function() {
+ var css = $('<link>',
+ { rel: 'stylesheet',
+ type: 'text/css',
+ href: gon.katex_css_url,
+ });
+ css.appendTo('head');
+
+ // Load KaTeX js
+ $.getScript(gon.katex_js_url, function() {
+ katexLoaded = true;
+ renderWithKaTeX($this); // Run KaTeX
+ });
+ });
+ }
+ };
+
+}).call(this);
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 440b5da756d..b1e844b7302 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-unused-vars, semi, consistent-return, one-var, one-var-declaration-per-line, no-undef, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-unused-vars, semi, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, padded-blocks, max-len */
+/* global Cookies */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js
index 1d208f1494c..4b6ebadeac7 100644
--- a/app/assets/javascripts/search.js
+++ b/app/assets/javascripts/search.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, no-undef, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, padded-blocks, max-len */
+/* global Api */
+
(function() {
this.Search = (function() {
function Search() {
diff --git a/app/assets/javascripts/search_autocomplete.js.es6 b/app/assets/javascripts/search_autocomplete.js.es6
index 5fa94556501..437f5dbbf7d 100644
--- a/app/assets/javascripts/search_autocomplete.js.es6
+++ b/app/assets/javascripts/search_autocomplete.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, no-plusplus, prefer-template, quotes, class-methods-use-this, no-unused-expressions, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, padded-blocks, no-extra-semi, indent, max-len */
+
((global) => {
const KEYCODE = {
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index fa2168723be..5ea00f408f4 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -1,4 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-undef, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-plusplus, no-else-return, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-plusplus, no-else-return, comma-dangle, padded-blocks, max-len */
+/* global Mousetrap */
+/* global Turbolinks */
+/* global findFileURL */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js
index 65305b8c22f..c26903038b4 100644
--- a/app/assets/javascripts/shortcuts_blob.js
+++ b/app/assets/javascripts/shortcuts_blob.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, consistent-return, padded-blocks, no-undef, max-len */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, consistent-return, padded-blocks */
+/* global Shortcuts */
+/* global Mousetrap */
/*= require shortcuts */
diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js
index 1b9a265ba39..4549742bbcb 100644
--- a/app/assets/javascripts/shortcuts_dashboard_navigation.js
+++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks, no-undef, max-len */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks */
+/* global Mousetrap */
+/* global Shortcuts */
/*= require shortcuts */
diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js
index 68cd6fad04e..3a81380eef0 100644
--- a/app/assets/javascripts/shortcuts_find_file.js
+++ b/app/assets/javascripts/shortcuts_find_file.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks, no-undef, max-len */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks */
+/* global Mousetrap */
+/* global ShortcutsNavigation */
/*= require shortcuts_navigation */
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index c4899f3566a..b892fbc3393 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -1,4 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators, no-undef, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators, padded-blocks */
+/* global Mousetrap */
+/* global Turbolinks */
+/* global ShortcutsNavigation */
+/* global sidebar */
/*= require mousetrap */
/*= require shortcuts_navigation */
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
index 7d4d6364c70..0776d0a9b67 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks, no-undef, max-len */
+/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks */
+/* global Mousetrap */
+/* global Shortcuts */
/*= require shortcuts */
diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js
index a4095d2c06b..ecc3fab84c3 100644
--- a/app/assets/javascripts/shortcuts_network.js
+++ b/app/assets/javascripts/shortcuts_network.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks, no-undef, max-len */
+/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks, max-len */
+/* global Mousetrap */
+/* global ShortcutsNavigation */
/*= require shortcuts_navigation */
diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6
index a23ca449c4b..9790a44972d 100644
--- a/app/assets/javascripts/sidebar.js.es6
+++ b/app/assets/javascripts/sidebar.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign, padded-blocks */
+/* global Cookies */
+
((global) => {
let singleton;
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 0d48e69cce9..ac8603ccd10 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -1,8 +1,9 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, padded-blocks, no-undef, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, padded-blocks, max-len */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
- this.SingleFileDiff = (function() {
+ window.SingleFileDiff = (function() {
var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER;
WRAPPER = '<div class="diff-content diff-wrap-lines"></div>';
@@ -93,7 +94,7 @@
$.fn.singleFileDiff = function(forceLoad, cb) {
return this.each(function() {
if (!$.data(this, 'singleFileDiff') || forceLoad) {
- return $.data(this, 'singleFileDiff', new SingleFileDiff(this, forceLoad, cb));
+ return $.data(this, 'singleFileDiff', new window.SingleFileDiff(this, forceLoad, cb));
}
});
};
diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js
index 2c8ecba7de4..18512d179b3 100644
--- a/app/assets/javascripts/snippet/snippet_bundle.js
+++ b/app/assets/javascripts/snippet/snippet_bundle.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, no-undef, quotes, semi, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, semi, padded-blocks, max-len */
+/* global ace */
+
/*= require_tree . */
(function() {
diff --git a/app/assets/javascripts/snippets_list.js.es6 b/app/assets/javascripts/snippets_list.js.es6
index c3afc3f2246..6f913326a3a 100644
--- a/app/assets/javascripts/snippets_list.js.es6
+++ b/app/assets/javascripts/snippets_list.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable arrow-parens, no-param-reassign, space-before-function-paren, func-names, no-var, semi, max-len */
+
(global => {
global.gl = global.gl || {};
diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js
index 32803fa790b..f1fc526bf2e 100644
--- a/app/assets/javascripts/star.js
+++ b/app/assets/javascripts/star.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, no-undef, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, padded-blocks, max-len */
+/* global Flash */
+
(function() {
this.Star = (function() {
function Star() {
diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js
index bd37d69165f..5d0fa62c50a 100644
--- a/app/assets/javascripts/syntax_highlight.js
+++ b/app/assets/javascripts/syntax_highlight.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, padded-blocks, max-len */
+
// Syntax Highlighter
//
// Applies a syntax highlighting color scheme CSS class to any element with the
@@ -9,8 +10,10 @@
// <div class="js-syntax-highlight"></div>
//
(function() {
+
$.fn.syntaxHighlight = function() {
var $children;
+
if ($(this).hasClass('js-syntax-highlight')) {
// Given the element itself, apply highlighting
return $(this).addClass(gon.user_color_scheme);
@@ -23,8 +26,4 @@
}
};
- $(document).on('ready page:load', function() {
- return $('.js-syntax-highlight').syntaxHighlight();
- });
-
}).call(this);
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6
index 93a3d67ee9f..d2b152045b4 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js.es6
+++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable prefer-const, comma-dangle, max-len, no-useless-return, object-curly-spacing, no-param-reassign, max-len */
+/* global Api */
+
/*= require ../blob/template_selector */
((global) => {
diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
index 0a3890e85fe..7310b9de074 100644
--- a/app/assets/javascripts/templates/issuable_template_selectors.js.es6
+++ b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-new, comma-dangle, class-methods-use-this, prefer-const, no-param-reassign */
+
((global) => {
class IssuableTemplateSelectors {
constructor({ $dropdowns, editor } = {}) {
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/todos.js.es6 b/app/assets/javascripts/todos.js.es6
index 213e80825b7..d8713600030 100644
--- a/app/assets/javascripts/todos.js.es6
+++ b/app/assets/javascripts/todos.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable padded-blocks, class-methods-use-this, no-new, func-names, prefer-template, no-unneeded-ternary, object-shorthand, space-before-function-paren, comma-dangle, quote-props, consistent-return, no-else-return, semi, no-param-reassign, max-len */
+/* global UsersSelect */
+/* global Turbolinks */
+
((global) => {
class Todos {
@@ -72,7 +75,7 @@
allDoneClicked(e) {
e.preventDefault();
e.stopImmediatePropagation();
- $target = $(e.currentTarget);
+ const $target = $(e.currentTarget);
$target.disable();
return $.ajax({
type: 'POST',
diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js
index 5d991542b51..d2aa3c7a841 100644
--- a/app/assets/javascripts/u2f/authenticate.js
+++ b/app/assets/javascripts/u2f/authenticate.js
@@ -1,4 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, prefer-arrow-callback, no-undef, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, prefer-arrow-callback, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */
+/* global u2f */
+/* global U2FError */
+/* global U2FUtil */
+
// Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
//
// State Flow #1: setup -> in_progress -> authenticated -> POST to server
diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js
index 4c70a6e9bb6..69f98c9c0ad 100644
--- a/app/assets/javascripts/u2f/error.js
+++ b/app/assets/javascripts/u2f/error.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-console, quotes, prefer-template, no-undef, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-console, quotes, prefer-template, padded-blocks, max-len */
+/* global u2f */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js
index 97d8993cac2..4f5d68f546b 100644
--- a/app/assets/javascripts/u2f/register.js
+++ b/app/assets/javascripts/u2f/register.js
@@ -1,4 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-undef, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */
+/* global u2f */
+/* global U2FError */
+/* global U2FUtil */
+
// Register U2F (universal 2nd factor) devices for users to authenticate with.
//
// State Flow #1: setup -> in_progress -> registered -> POST to server
diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js.es6
index 5e869e99fdb..0a2db7c05fe 100644
--- a/app/assets/javascripts/user.js.es6
+++ b/app/assets/javascripts/user.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable class-methods-use-this, comma-dangle, arrow-parens, no-param-reassign, semi */
+/* global Cookies */
+
((global) => {
global.User = class {
constructor({ action }) {
diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js.es6
index 5a625611987..b9c23b51b4d 100644
--- a/app/assets/javascripts/user_tabs.js.es6
+++ b/app/assets/javascripts/user_tabs.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable max-len, space-before-function-paren, no-underscore-dangle, array-bracket-spacing, consistent-return, comma-dangle, no-unused-vars, dot-notation, no-new, no-return-assign, camelcase, semi, no-param-reassign */
+
/*
UserTabs
diff --git a/app/assets/javascripts/username_validator.js.es6 b/app/assets/javascripts/username_validator.js.es6
index c4dde575c6e..137cefa3b8e 100644
--- a/app/assets/javascripts/username_validator.js.es6
+++ b/app/assets/javascripts/username_validator.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, consistent-return, class-methods-use-this, arrow-parens, no-param-reassign, max-len */
+
((global) => {
const debounceTimeoutDuration = 1000;
const invalidInputClass = 'gl-field-error-outline';
@@ -77,7 +78,7 @@
this.renderState();
return $.ajax({
type: 'GET',
- url: `/users/${username}/exists`,
+ url: `${gon.relative_url_root}/users/${username}/exists`,
dataType: 'json',
success: (res) => this.setAvailabilityState(res.exists)
});
diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js
index ba7f533c349..578be7c3590 100644
--- a/app/assets/javascripts/users/calendar.js
+++ b/app/assets/javascripts/users/calendar.js
@@ -1,4 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, vars-on-top, semi, keyword-spacing, no-plusplus, no-undef, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, vars-on-top, semi, keyword-spacing, no-plusplus, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, padded-blocks, max-len */
+/* global d3 */
+/* global dateFormat */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index c6e18fad832..d4b5e03aa35 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -1,4 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, no-undef, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-plusplus, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, keyword-spacing, no-param-reassign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-plusplus, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, keyword-spacing, no-param-reassign, padded-blocks */
+/* global Vue */
+/* global Issuable */
+/* global ListUser */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
slice = [].slice;
diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js
index 82eb761442a..e09b59dd5aa 100644
--- a/app/assets/javascripts/zen_mode.js
+++ b/app/assets/javascripts/zen_mode.js
@@ -1,4 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, no-undef, camelcase, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, padded-blocks, max-len */
+/* global Dropzone */
+/* global Mousetrap */
+
// Zen Mode (full screen) textarea
//
/*= provides zen_mode:enter */
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index c82a9a2b9e3..40bc0579393 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -44,3 +44,6 @@
@import "framework/awards.scss";
@import "framework/images.scss";
@import "framework/broadcast-messages";
+@import "framework/emojis.scss";
+@import "framework/icons.scss";
+@import "framework/snippets.scss";
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index dece5c3202b..9fc9bcebc44 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -12,8 +12,8 @@
z-index: 9;
width: 300px;
font-size: 14px;
- background-color: $award-emoji-menu-bg;
- border: 1px solid $award-emoji-menu-border;
+ background-color: $white-light;
+ border: 1px solid $border-white-light;
border-radius: $border-radius-base;
box-shadow: 0 6px 12px $award-emoji-menu-shadow;
pointer-events: none;
@@ -135,7 +135,7 @@
}
.award-control-icon {
- color: $award-emoji-new-btn-icon-color;
+ color: $border-gray-normal;
margin-top: 1px;
}
}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 95c02499271..9f02749f5ab 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -24,7 +24,7 @@
.row-content-block {
margin-top: 0;
margin-bottom: -$gl-padding;
- background-color: $background-color;
+ background-color: $gray-light;
padding: $gl-padding;
margin-bottom: 0;
border-top: 1px solid $white-dark;
@@ -118,7 +118,7 @@
.cover-block {
text-align: center;
- background: $background-color;
+ background: $gray-light;
padding-top: 44px;
position: relative;
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 1c7b2f4df7c..a11f1cd7735 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -88,11 +88,11 @@
}
@mixin btn-gray {
- @include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, $gl-gray-dark);
+ @include btn-color($gray-light, $border-gray-normal, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, $gl-gray-dark);
}
@mixin btn-white {
- @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-dark, $gl-text-color);
+ @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-gray-dark, $gl-text-color);
}
@mixin btn-with-margin {
@@ -230,12 +230,19 @@
}
}
+.btn-terminal {
+ svg {
+ height: 14px;
+ width: 18px;
+ }
+}
+
.btn-lg {
padding: 12px 20px;
}
.btn-transparent {
- color: $btn-transparent-color;
+ color: $gl-gray-light;
background-color: transparent;
border: 0;
@@ -289,7 +296,7 @@
.active {
box-shadow: $gl-btn-active-background;
- border: 1px solid $border-white-dark !important;
+ border: 1px solid $border-gray-dark !important;
background-color: $btn-active-gray-light !important;
}
}
@@ -309,8 +316,8 @@
text-align: left;
padding: 6px 16px;
border-color: $border-color;
- color: $btn-placeholder-gray;
- background-color: $background-color;
+ color: $gray-darkest;
+ background-color: $gray-light;
&:hover,
&:active,
@@ -318,8 +325,8 @@
cursor: text;
box-shadow: none;
border-color: $border-color;
- color: $btn-placeholder-gray;
- background-color: $background-color;
+ color: $gray-darkest;
+ background-color: $gray-light;
}
}
@@ -331,7 +338,7 @@
margin-left: 10px;
i {
- color: $gl-icon-color;
+ color: $gl-gray-light;
}
}
@@ -344,8 +351,8 @@
}
.btn-static {
- background-color: $background-color !important;
- border: 1px solid $border-gray-light;
+ background-color: $gray-light !important;
+ border: 1px solid $border-gray-normal;
cursor: default;
&:active {
diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss
index 2a100980aca..e0e46dd73af 100644
--- a/app/assets/stylesheets/framework/callout.scss
+++ b/app/assets/stylesheets/framework/callout.scss
@@ -11,7 +11,7 @@
padding: $gl-padding;
border-left: 3px solid $border-color;
color: $text-color;
- background: $background-color;
+ background: $gray-light;
}
.bs-callout h4 {
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index d5914b900e2..889366d6ddf 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -10,7 +10,7 @@
@mixin chevron-active {
.fa-chevron-down {
- color: $dropdown-toggle-hover-icon-color;
+ color: $gray-darkest;
}
}
@@ -28,14 +28,14 @@
.dropdown-toggle,
.dropdown-menu-toggle {
@include chevron-active;
- border-color: $dropdown-toggle-hover-border-color;
+ border-color: $gray-darkest;
}
}
.dropdown-toggle {
padding: 6px 8px 6px 10px;
- background-color: $dropdown-toggle-bg;
- color: $dropdown-toggle-color;
+ background-color: $white-light;
+ color: $gl-text-color;
font-size: 14px;
text-align: left;
border: 1px solid $border-color;
@@ -73,7 +73,7 @@
}
.fa {
- color: $dropdown-toggle-icon-color;
+ color: $gray-darkest;
}
.fa-chevron-down {
@@ -85,7 +85,7 @@
&:hover {
@include chevron-active;
- border-color: $dropdown-toggle-hover-border-color;
+ border-color: $gray-darkest;
}
&:focus:active {
@@ -98,7 +98,7 @@
@extend .dropdown-toggle;
padding-right: 20px;
position: relative;
- width: 160px;
+ width: 163px;
text-overflow: ellipsis;
overflow: hidden;
@@ -131,7 +131,7 @@
font-size: 14px;
font-weight: normal;
padding: 8px 0;
- background-color: $dropdown-bg;
+ background-color: $white-light;
border: 1px solid $dropdown-border-color;
border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color;
@@ -188,7 +188,6 @@
&.is-focused {
background-color: $dropdown-link-hover-bg;
text-decoration: none;
- outline: 0;
}
&.dropdown-menu-empty-link {
@@ -202,7 +201,7 @@
}
.icon-play {
- fill: $table-text-gray;
+ fill: $gl-gray-light;
margin-right: 6px;
height: 12px;
width: 11px;
@@ -210,7 +209,7 @@
}
.dropdown-header {
- color: $dropdown-header-color;
+ color: $gl-gray-light;
font-size: 13px;
line-height: 22px;
padding: 0 10px;
@@ -223,7 +222,7 @@
.unclickable {
cursor: not-allowed;
padding: 5px 8px;
- color: $dropdown-header-color;
+ color: $gl-gray-light;
}
}
@@ -602,14 +601,14 @@
th {
padding: 2px 0;
- color: $calendar-header-color;
+ color: $note-disabled-comment-color;
font-weight: normal;
text-transform: lowercase;
border-top: 1px solid $calendar-border-color;
}
.ui-datepicker-unselectable {
- background-color: $calendar-unselectable-bg;
+ background-color: $gray-light;
}
}
@@ -621,11 +620,11 @@
.dropdown-menu-inner-content {
display: block;
- color: $gl-placeholder-color;
+ color: $gl-gray-light;
}
.dropdown-toggle-text {
&.is-default {
- color: $gl-placeholder-color;
+ color: $gl-gray-light;
}
}
diff --git a/app/assets/stylesheets/pages/emojis.scss b/app/assets/stylesheets/framework/emojis.scss
index f17797b2381..7158de65143 100644
--- a/app/assets/stylesheets/pages/emojis.scss
+++ b/app/assets/stylesheets/framework/emojis.scss
@@ -1,4 +1,4 @@
-.emoji-0023-20E3 { background-position: 0 0px; }
+.emoji-0023-20E3 { background-position: 0 0; }
.emoji-002A-20E3 { background-position: -20px 0; }
.emoji-0030-20E3 { background-position: 0 -20px; }
.emoji-0031-20E3 { background-position: -20px -20px; }
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index ab0b81f77f7..88ed0a4a17e 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -19,7 +19,7 @@
.file-title {
position: relative;
- background-color: $background-color;
+ background-color: $gray-light;
border-bottom: 1px solid $border-color;
margin: 0;
text-align: left;
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 25a2b38baaa..8726a69867b 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -22,7 +22,7 @@ input[type='text'].danger {
margin-top: 0;
margin-bottom: -$gl-padding;
padding: $gl-padding;
- background-color: $background-color;
+ background-color: $gray-light;
border-top: 1px solid $border-color;
}
@@ -96,6 +96,10 @@ label {
code {
line-height: 1.8;
}
+
+ img {
+ margin-right: $gl-padding;
+ }
}
@media(max-width: $screen-xs-max) {
@@ -149,7 +153,7 @@ label {
}
.form-control::-webkit-input-placeholder {
- color: $gl-placeholder-color;
+ color: $gl-gray-light;
}
.input-group {
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index cc2286038c0..971940773f7 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -9,7 +9,7 @@ header {
&.navbar-empty {
height: $header-height;
background: $white-light;
- border-bottom: 1px solid $btn-gray-hover;
+ border-bottom: 1px solid $white-normal;
.center-logo {
margin: 8px 0;
@@ -27,7 +27,7 @@ header {
z-index: 100;
margin-bottom: 0;
height: $header-height;
- background-color: $background-color;
+ background-color: $gray-light;
border: none;
border-bottom: 1px solid $border-color;
@@ -45,7 +45,7 @@ header {
padding: 0;
.nav > li > a {
- color: $gl-icon-color;
+ color: $gl-gray-light;
font-size: 18px;
padding: 0;
margin: ($header-height - 28) / 2 0;
@@ -62,8 +62,8 @@ header {
&:hover,
&:focus,
&:active {
- background-color: $background-color;
- color: darken($gl-icon-color, 30%);
+ background-color: $gray-light;
+ color: darken($gl-gray-light, 30%);
.todos-pending-count {
background: darken($todo-alert-blue, 10%);
@@ -84,11 +84,11 @@ header {
padding: 6px 10px;
&:hover {
- background-color: $btn-gray-hover;
+ background-color: $white-normal;
}
&.active {
- color: $gl-icon-color;
+ color: $gl-gray-light;
}
}
}
@@ -100,10 +100,10 @@ header {
font-size: 18px;
padding: 6px 10px;
border: none;
- background-color: $background-color;
+ background-color: $gray-light;
&:hover {
- background-color: $btn-gray-hover;
+ background-color: $white-normal;
}
}
}
diff --git a/app/assets/stylesheets/pages/icons.scss b/app/assets/stylesheets/framework/icons.scss
index 226bd2ead31..8624a25c052 100644
--- a/app/assets/stylesheets/pages/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -49,3 +49,11 @@
fill: $gray-darkest;
}
}
+
+.ci-status-icon-manual {
+ color: $gl-text-color;
+
+ svg {
+ fill: $gl-text-color;
+ }
+}
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
index 878f44116ba..09a569ad415 100644
--- a/app/assets/stylesheets/framework/images.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -4,7 +4,7 @@
}
.appearance-light-logo-preview {
- background-color: $background-color;
+ background-color: $gray-light;
max-width: 72px;
padding: 10px;
margin-bottom: 10px;
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index 44834a84234..298913108ee 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -41,6 +41,6 @@
}
&.status-box-upcoming {
- background: $issue-box-upcoming-bg;
+ background: $gl-gray-light;
}
}
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index dfaf2f7f1d3..59fae61a44f 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -26,6 +26,45 @@ body {
.container-limited {
max-width: $fixed-layout-width;
+
+ &.limit-container-width {
+ max-width: $limited-layout-width;
+ }
+}
+
+.alert-wrapper {
+ margin-bottom: $gl-padding;
+
+ .alert {
+ margin-bottom: 0;
+ }
+
+ /* Stripe the background colors so that adjacent alert-warnings are distinct from one another */
+ .alert-warning {
+ transition: background-color 0.15s, border-color 0.15s;
+ background-color: lighten($gl-warning, 4%);
+ border-color: lighten($gl-warning, 4%);
+ }
+
+ .alert-warning + .alert-warning {
+ background-color: $gl-warning;
+ border-color: $gl-warning;
+ }
+
+ .alert-warning + .alert-warning + .alert-warning {
+ background-color: darken($gl-warning, 4%);
+ border-color: darken($gl-warning, 4%);
+ }
+
+ .alert-warning + .alert-warning + .alert-warning + .alert-warning {
+ background-color: darken($gl-warning, 8%);
+ border-color: darken($gl-warning, 8%);
+ }
+
+ .alert-warning:only-of-type {
+ background-color: $gl-warning;
+ border-color: $gl-warning;
+ }
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index ed4b60faf92..e96cd671e34 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -36,7 +36,7 @@
color: $list-warning-row-color;
}
- &.smoke { background-color: $background-color; }
+ &.smoke { background-color: $gray-light; }
&:not(.ui-sort-disabled):hover {
background: $row-hover;
@@ -46,7 +46,7 @@
border-bottom: none;
&.bottom {
- background: $background-color;
+ background: $gray-light;
}
}
@@ -59,7 +59,7 @@
p {
padding-top: 1px;
margin: 0;
- color: $gray-dark;
+ color: $white-normal;
img {
position: relative;
@@ -113,7 +113,7 @@ ul.content-list {
padding: 0;
li {
- border-color: $table-border-color;
+ border-color: $white-normal;
font-size: $list-font-size;
color: $list-text-color;
@@ -186,7 +186,7 @@ ul.content-list {
&.list-placeholder {
background-color: $gray-light;
- border: dotted 1px $gray-dark;
+ border: dotted 1px $white-normal;
margin: 1px 0;
min-height: 52px;
}
@@ -224,7 +224,7 @@ ul.content-list {
}
.label-default {
- color: $btn-transparent-color;
+ color: $gl-gray-light;
}
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 59a30d31ac7..e30d81d09f0 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -114,7 +114,7 @@
// Border around images in issue and MR comments.
img:not(.emoji) {
- border: 1px solid $table-border-gray;
+ border: 1px solid $white-normal;
padding: 5px;
margin: 5px 0;
// Ensure that image does not exceed viewport
@@ -135,7 +135,7 @@
.toolbar-btn {
float: left;
padding: 0 5px;
- color: $note-toolbar-color;
+ color: $gl-gray-light;
background: transparent;
border: 0;
outline: 0;
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index abfdd7a759d..7eb9962ba33 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -54,7 +54,7 @@
}
// Display Star and Fork buttons without counters on mobile.
- .project-action-buttons {
+ .project-repo-buttons {
display: block;
.count-buttons .btn {
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index ea77348633d..e4affbb1be1 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -51,7 +51,7 @@
margin-bottom: -1px;
font-size: 14px;
line-height: 28px;
- color: $note-toolbar-color;
+ color: $gl-gray-light;
border-bottom: 2px solid transparent;
&:hover,
@@ -80,17 +80,17 @@
.badge {
font-weight: normal;
background-color: $nav-badge-bg;
- color: $btn-transparent-color;
+ color: $gl-gray-light;
vertical-align: baseline;
}
}
&.sub-nav {
text-align: center;
- background-color: $dark-background-color;
+ background-color: $gray-normal;
.container-fluid {
- background-color: $dark-background-color;
+ background-color: $gray-normal;
margin-bottom: 0;
}
@@ -117,7 +117,7 @@
.top-area {
@include clearfix;
- border-bottom: 1px solid $btn-gray-hover;
+ border-bottom: 1px solid $white-normal;
.nav-text {
padding-top: 16px;
@@ -289,7 +289,7 @@
top: $header-height;
width: 100%;
z-index: 11;
- background: $background-color;
+ background: $gray-light;
border-bottom: 1px solid $border-color;
transition: padding $sidebar-transition-duration;
text-align: center;
@@ -317,7 +317,7 @@
.fa-caret-down {
margin-left: 5px;
- color: $gl-icon-color;
+ color: $gl-gray-light;
}
.dropdown {
@@ -352,7 +352,7 @@
}
.fade-right {
- @include fade(left, $background-color);
+ @include fade(left, $gray-light);
right: -5px;
.fa {
@@ -361,7 +361,7 @@
}
.fade-left {
- @include fade(right, $background-color);
+ @include fade(right, $gray-light);
left: -5px;
.fa {
@@ -372,7 +372,7 @@
&.sub-nav-scroll {
.fade-right {
- @include fade(left, $dark-background-color);
+ @include fade(left, $gray-normal);
right: 0;
.fa {
@@ -381,7 +381,7 @@
}
.fade-left {
- @include fade(right, $dark-background-color);
+ @include fade(right, $gray-normal);
left: 0;
.fa {
diff --git a/app/assets/stylesheets/framework/page-header.scss b/app/assets/stylesheets/framework/page-header.scss
index 85c1385d5d9..625bea96aaa 100644
--- a/app/assets/stylesheets/framework/page-header.scss
+++ b/app/assets/stylesheets/framework/page-header.scss
@@ -14,7 +14,7 @@
.header-action-buttons {
i {
- color: $gl-icon-color;
+ color: $gl-gray-light;
font-size: 13px;
margin-right: 3px;
}
@@ -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/selects.scss b/app/assets/stylesheets/framework/selects.scss
index fde1431b13e..9ab17e67d4c 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -39,7 +39,7 @@
}
&:hover {
- background-color: $gray-dark;
+ background-color: $white-normal;
border-color: $border-white-normal;
color: $gl-text-color;
}
@@ -108,7 +108,7 @@
border-color: $input-border;
color: $gl-text-color;
line-height: 15px;
- background-color: $background-color;
+ background-color: $gray-light;
background-image: none;
.select2-search-choice-close {
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 0aa609b8dd5..46a06cd7eab 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -23,7 +23,7 @@
.sidebar-wrapper {
z-index: 1000;
- background: $background-color;
+ background: $gray-light;
.nicescroll-rails-hr {
// TODO: Figure out why nicescroll doesn't hide horizontal bar
diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/framework/snippets.scss
index 857eb76131a..5f7e1b17cc7 100644
--- a/app/assets/stylesheets/pages/snippets.scss
+++ b/app/assets/stylesheets/framework/snippets.scss
@@ -1,3 +1,13 @@
+.snippet-row {
+ .title {
+ margin-bottom: 2px;
+ }
+
+ .snippet-filename {
+ padding: 0 2px;
+ }
+}
+
.snippet-form-holder .file-holder .file-title {
padding: 2px;
}
@@ -12,23 +22,19 @@
.snippet-file-content {
border-radius: 3px;
- margin-bottom: $gl-padding;
-
- .btn-clipboard {
- @extend .btn;
- }
}
-.project-snippets .awards {
- border-bottom: 1px solid $table-border-color;
- padding-bottom: $gl-padding;
+.snippet-header {
+ padding: $gl-padding 0;
}
.snippet-title {
font-size: 24px;
font-weight: 600;
- padding: $gl-padding;
- padding-left: 0;
+}
+
+.snippet-edited-ago {
+ color: $gray-darkest;
}
.snippet-actions {
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 5d0ca63ea08..6d9fa74a030 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -31,7 +31,7 @@ table {
}
th {
- background-color: $background-color;
+ background-color: $gray-light;
font-weight: normal;
border-bottom: none;
@@ -41,7 +41,7 @@ table {
}
td {
- border-color: $table-border-color;
+ border-color: $white-normal;
}
}
}
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 875cded8b4e..6078505807e 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -6,7 +6,7 @@
.timeline-entry {
padding: $gl-padding $gl-btn-padding 11px;
- border-color: $table-border-color;
+ border-color: $white-normal;
color: $gl-gray;
border-bottom: 1px solid $border-white-light;
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index 55bc325b858..718dbbfea27 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/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index c731a8f222f..876adf7f712 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -78,7 +78,7 @@ $pagination-active-bg: $white-light;
$pagination-active-border: $border-color;
$pagination-disabled-color: #cdcdcd;
-$pagination-disabled-bg: $background-color;
+$pagination-disabled-bg: $gray-light;
$pagination-disabled-border: $border-color;
@@ -117,8 +117,8 @@ $alert-border-radius: 0;
$panel-border-radius: 2px;
$panel-default-text: $text-color;
$panel-default-border: $border-color;
-$panel-default-heading-bg: $background-color;
-$panel-footer-bg: $background-color;
+$panel-default-heading-bg: $gray-light;
+$panel-footer-bg: $gray-light;
$panel-inner-border: $border-color;
//== Wells
@@ -153,8 +153,8 @@ $nav-link-padding: 13px $gl-padding;
//== Code
//
//##
-$pre-bg: $background-color !default;
+$pre-bg: $gray-light !default;
$pre-color: $gl-gray !default;
$pre-border-color: $border-color;
-$table-bg-accent: $background-color;
+$table-bg-accent: $gray-light;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 18716813c48..460c5d995be 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -17,16 +17,16 @@ $darken-dark-factor: 10%;
$darken-border-factor: 5%;
$white-light: #fff;
-$white-normal: darken($white-light, $darken-normal-factor);
-$white-dark: darken($white-light, $darken-dark-factor);
+$white-normal: #f0f0f0;
+$white-dark: #eaeaea;
$gray-lightest: #fdfdfd;
$gray-light: #fafafa;
$gray-lighter: #f9f9f9;
-$gray-normal: darken($gray-light, $darken-normal-factor);
+$gray-normal: #f5f5f5;
$gray-dark: darken($gray-light, $darken-dark-factor);
$gray-darker: #eee;
-$gray-darkest: #c9c9c9;
+$gray-darkest: #c4c4c4;
$green-light: #3cbd70;
$green-normal: darken($green-light, $darken-normal-factor);
@@ -55,11 +55,9 @@ $black-transparent: rgba(0, 0, 0, 0.3);
$border-white-light: darken($white-light, $darken-border-factor);
$border-white-normal: darken($white-normal, $darken-border-factor);
-$border-white-dark: darken($white-dark, $darken-border-factor);
-$border-gray-light: darken($gray-light, $darken-border-factor);
$border-gray-normal: darken($gray-normal, $darken-border-factor);
-$border-gray-dark: darken($gray-dark, $darken-border-factor);
+$border-gray-dark: darken($white-normal, $darken-border-factor);
$border-green-extra-light: #9adb84;
$border-green-light: darken($green-light, $darken-border-factor);
@@ -78,9 +76,6 @@ $border-red-light: darken($red-light, $darken-border-factor);
$border-red-normal: darken($red-normal, $darken-border-factor);
$border-red-dark: darken($red-dark, $darken-border-factor);
-$help-well-bg: $gray-light;
-$help-well-border: #e5e5e5;
-
$warning-message-bg: #fbf2d9;
$warning-message-color: #9e8e60;
$warning-message-border: #f0e2bb;
@@ -90,10 +85,6 @@ $warning-message-border: #f0e2bb;
*/
$border-color: #e5e5e5;
$focus-border-color: #3aabf0;
-$table-border-color: #f0f0f0;
-$background-color: $gray-light;
-$dark-background-color: #f5f5f5;
-$table-text-gray: #8f8f8f;
$well-expand-item: #e8f2f7;
$well-inner-border: #eef0f2;
$well-light-border: #f1f1f1;
@@ -113,12 +104,10 @@ $gl-text-orange: #d90;
$gl-link-color: #3777b0;
$gl-diff-text-color: #555;
$gl-dark-link-color: #333;
-$gl-placeholder-color: #8f8f8f;
-$gl-icon-color: $gl-placeholder-color;
+$gl-gray-light: #8f8f8f;
$gl-grayish-blue: #7f8fa4;
$gl-gray: $gl-text-color;
$gl-gray-dark: #313236;
-$gl-gray-light: $gl-placeholder-color;
$gl-header-color: #4c4e54;
/*
@@ -166,11 +155,11 @@ $row-hover-border: #b2d7ff;
$progress-color: #c0392b;
$header-height: 50px;
$fixed-layout-width: 1280px;
+$limited-layout-width: 990px;
+$gl-avatar-size: 40px;
$error-exclamation-point: #e62958;
$border-radius-default: 2px;
-$btn-transparent-color: #8f8f8f;
$settings-icon-size: 18px;
-$provider-btn-group-border: #e5e5e5;
$provider-btn-not-active-color: #4688f1;
$link-underline-blue: #4a8bee;
$active-item-blue: #4a8bee;
@@ -193,7 +182,6 @@ $count-arrow-border: #dce0e5;
$save-project-loader-color: #555;
$divergence-graph-bar-bg: #ccc;
$divergence-graph-separator-bg: #ccc;
-$issue-box-upcoming-bg: #8f8f8f;
/*
* Common component specific colors
@@ -246,8 +234,6 @@ $line-removed-dark: #fac5cd;
$line-number-old: #f9d7dc;
$line-number-new: #ddfbe6;
$line-number-select: #fbf2da;
-$match-line: $gray-light;
-$table-border-gray: #f0f0f0;
$line-target-blue: #f6faff;
$line-select-yellow: #fcf8e7;
$line-select-yellow-dark: #f0e2bd;
@@ -257,7 +243,6 @@ $file-mode-changed: #777;
$file-mode-changed: #777;
$diff-image-bg: #ddd;
$diff-image-info-color: grey;
-$diff-image-img-bg: #e5e5e5;
$diff-swipe-border: #999;
$diff-view-modes-color: grey;
$diff-view-modes-border: #c1c1c1;
@@ -272,14 +257,12 @@ $regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-San
* Dropdowns
*/
$dropdown-width: 300px;
-$dropdown-bg: #fff;
$dropdown-link-color: #555;
$dropdown-link-hover-bg: $row-hover;
$dropdown-empty-row-bg: rgba(#000, .04);
$dropdown-border-color: $border-color;
$dropdown-shadow-color: rgba(#000, .1);
$dropdown-divider-color: rgba(#000, .1);
-$dropdown-header-color: #959494;
$dropdown-title-btn-color: #bfbfbf;
$dropdown-input-color: #555;
$dropdown-input-fa-color: #c7c7c7;
@@ -287,31 +270,19 @@ $dropdown-input-focus-border: $focus-border-color;
$dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, .4);
$dropdown-loading-bg: rgba(#fff, .6);
$dropdown-chevron-size: 10px;
+$dropdown-toggle-active-border-color: darken($border-color, 14%);
-$dropdown-toggle-bg: #fff;
-$dropdown-toggle-color: #5c5c5c;
-$dropdown-toggle-border-color: #e5e5e5;
-$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 13%);
-$dropdown-toggle-active-border-color: darken($dropdown-toggle-border-color, 14%);
-$dropdown-toggle-icon-color: #c4c4c4;
-$dropdown-toggle-hover-icon-color: darken($dropdown-toggle-icon-color, 7%);
/*
* Buttons
*/
$btn-active-gray: #ececec;
$btn-active-gray-light: e4e7ed;
-$btn-placeholder-gray: #c7c7c7;
-$btn-white-active: #848484;
-$btn-gray-hover: #eee;
/*
* Award emoji
*/
-$award-emoji-menu-bg: #fff;
-$award-emoji-menu-border: #f1f2f4;
$award-emoji-menu-shadow: rgba(0,0,0,.175);
-$award-emoji-new-btn-icon-color: #dcdcdc;
/*
* Search Box
@@ -319,22 +290,15 @@ $award-emoji-new-btn-icon-color: #dcdcdc;
$search-input-border-color: rgba(#4688f1, .8);
$search-input-focus-shadow-color: $dropdown-input-focus-shadow;
$search-input-width: 220px;
-$location-badge-color: #aaa;
-$location-badge-bg: $dark-background-color;
$location-badge-active-bg: #4f91f8;
$location-icon-color: #e7e9ed;
-$location-icon-active-color: #807e7e;
/*
* Notes
*/
$notes-light-color: #8e8e8e;
-$notes-action-color: #c3c3c3;
$notes-role-color: #8e8e8e;
-$notes-role-border-color: #e4e4e4;
$note-disabled-comment-color: #b2b2b2;
-$note-form-border-color: #e5e5e5;
-$note-toolbar-color: #959494;
$note-targe3-outside: #fffff0;
$note-targe3-inside: #ffffd3;
$note-line2-border: #ddd;
@@ -344,15 +308,12 @@ $note-line2-border: #ddd;
* Zen
*/
$zen-control-color: #555;
-$zen-control-hover-color: #111;
/*
* Calendar
*/
-$calendar-header-color: #b8b8b8;
$calendar-hover-bg: #ecf3fe;
$calendar-border-color: rgba(#000, .1);
-$calendar-unselectable-bg: $gray-light;
$calendar-user-contrib-text: #959494;
/*
@@ -366,15 +327,8 @@ $cycle-analytics-light-gray: #bfbfbf;
$cycle-analytics-dismiss-icon-color: #b2b2b2;
/*
- * Personal Access Tokens
- */
-$personal-access-tokens-disabled-label-color: #bbb;
-
-/*
* CI
*/
-$ci-output-bg: #1d1f21;
-$ci-text-color: #c5c8c6;
$ci-skipped-color: #888;
/*
@@ -470,7 +424,6 @@ $help-shortcut-header-color: #333;
/*
* Issues
*/
-$issues-border: #e5e5e5;
$issues-today-bg: #f3fff2;
$issues-today-border: #e1e8d5;
@@ -486,6 +439,7 @@ $jq-ui-default-color: #777;
$label-gray-bg: #f8fafc;
$label-inverse-bg: #333;
$label-remove-border: rgba(0, 0, 0, .1);
+$label-border-radius: 100px;
/*
* Lint
@@ -521,7 +475,6 @@ $project-option-descr-color: #54565b;
$project-breadcrumb-color: #999;
$project-private-forks-notice-odd: #2aa056;
$project-network-controls-color: #888;
-$project-limit-message-bg: #f28d35;
/*
* Runners
@@ -571,3 +524,9 @@ $body-text-shadow: rgba(255,255,255,0.01);
*/
$ui-dev-kit-example-color: #bbb;
$ui-dev-kit-example-border: #ddd;
+
+/*
+Pipeline Graph
+*/
+$stage-hover-bg: #eaf3fc;
+$stage-hover-border: #d1e7fc;
diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss
index f2860dfe84d..f9c850ebdc8 100644
--- a/app/assets/stylesheets/framework/wells.scss
+++ b/app/assets/stylesheets/framework/wells.scss
@@ -1,5 +1,5 @@
.info-well {
- background: $background-color;
+ background: $gray-light;
color: $gl-gray;
border: 1px solid $border-color;
border-radius: $border-radius-default;
@@ -45,7 +45,7 @@
}
.light-well {
- background-color: $background-color;
+ background-color: $gray-light;
padding: 15px;
}
diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss
index e5c7d70d45a..84b639fabf5 100644
--- a/app/assets/stylesheets/framework/zen.scss
+++ b/app/assets/stylesheets/framework/zen.scss
@@ -40,7 +40,7 @@
}
.zen-control-full {
- color: $note-toolbar-color;
+ color: $gl-gray-light;
&:hover {
color: $gl-link-color;
@@ -57,6 +57,6 @@
font-size: 36px;
&:hover {
- color: $zen-control-hover-color;
+ color: $black;
}
}
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index 1adab3ffd94..54a5664a874 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -69,14 +69,14 @@ $white-gc-bg: #eaf2f5;
@mixin matchLine {
color: $black-transparent;
- background-color: $match-line;
+ background-color: $gray-light;
}
.code.white {
// Line numbers
.line-numbers,
.diff-line-num {
- background-color: $background-color;
+ background-color: $gray-light;
}
.diff-line-num,
@@ -87,7 +87,7 @@ $white-gc-bg: #eaf2f5;
// Code itself
pre.code,
.diff-line-num {
- border-color: $table-border-gray;
+ border-color: $white-normal;
}
&,
diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
index 024b4df6bd0..60ff72c703e 100644
--- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss
+++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
@@ -91,9 +91,9 @@ $highlighted-gc-bg: #eaf2f5;
padding: 0 5px;
text-align: right;
width: 35px;
- background-color: $background-color;
+ background-color: $gray-light;
color: $black-transparent;
- border-right: 1px solid $table-border-gray;
+ border-right: 1px solid $white-normal;
&.old {
background-color: $line-number-old;
@@ -130,7 +130,7 @@ $highlighted-gc-bg: #eaf2f5;
&.match {
color: $black-transparent;
- background-color: $match-line;
+ background-color: $gray-light;
}
}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 0d9cf679e7c..c735f104c20 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -98,7 +98,7 @@
.board-inner {
height: 100%;
font-size: $issue-boards-font-size;
- background: $background-color;
+ background: $gray-light;
border: 1px solid $border-color;
border-radius: $border-radius-default;
}
@@ -253,7 +253,7 @@
.board-list-count {
padding: 10px 0;
- color: $gl-placeholder-color;
+ color: $gl-gray-light;
font-size: 13px;
> .fa {
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index dcc13f6d74a..66f7e7f97c8 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -42,7 +42,7 @@
}
.environment-information {
- background-color: $background-color;
+ background-color: $gray-light;
border: 1px solid $border-color;
padding: 12px $gl-padding;
border-radius: $border-radius-default;
@@ -96,8 +96,8 @@
}
.build-trace {
- background: $ci-output-bg;
- color: $ci-text-color;
+ background: $black;
+ color: $gray-darkest;
white-space: pre;
overflow-x: auto;
font-size: 12px;
@@ -257,7 +257,7 @@
}
.build-light-text {
- color: $gl-placeholder-color;
+ color: $gl-gray-light;
}
.build-gutter-toggle {
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index c29b5fdea78..e76e1a73b25 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -7,7 +7,7 @@
.commit-header {
padding: 5px 10px;
- background-color: $background-color;
+ background-color: $gray-light;
border-top: 1px solid $gray-darker;
border-bottom: 1px solid $gray-darker;
font-size: 14px;
@@ -38,7 +38,7 @@
.text-expander {
display: inline-block;
background: $gray-light;
- color: $gl-placeholder-color;
+ color: $gl-gray-light;
padding: 0 5px;
cursor: pointer;
border: 1px solid $border-gray-dark;
@@ -117,7 +117,7 @@
.commit-row-description {
font-size: 14px;
- border-left: 1px solid $btn-gray-hover;
+ border-left: 1px solid $white-normal;
padding: 10px 15px;
margin: 10px 0;
background: $gray-light;
@@ -174,7 +174,7 @@
height: 14px;
width: 14px;
vertical-align: middle;
- fill: $table-text-gray;
+ fill: $gl-gray-light;
}
}
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index 57146e1fccd..5aef31724e1 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -232,6 +232,7 @@
&:hover:not(.active) {
background-color: $gray-lightest;
box-shadow: inset 2px 0 0 0 $border-color;
+ cursor: pointer;
}
&:first-child {
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/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 737f6e0f4be..f30795fd2c2 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -11,7 +11,7 @@
.diff-header {
position: relative;
- background: $background-color;
+ background: $gray-light;
border-bottom: 1px solid $border-color;
padding: 10px 16px;
color: $gl-diff-text-color;
@@ -38,7 +38,7 @@
cursor: pointer;
&:hover {
- background-color: $dark-background-color;
+ background-color: $gray-normal;
}
.diff-toggle-caret {
@@ -187,8 +187,8 @@
img {
border: 1px solid $white-light;
- background-image: linear-gradient(45deg, $diff-image-img-bg 25%, transparent 25%, transparent 75%, $diff-image-img-bg 75%, $diff-image-img-bg 100%),
- linear-gradient(45deg, $diff-image-img-bg 25%, transparent 25%, transparent 75%, $diff-image-img-bg 75%, $diff-image-img-bg 100%);
+ background-image: linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%),
+ linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%);
background-size: 10px 10px;
background-position: 0 0, 5px 5px;
max-width: 100%;
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 6cde9c592de..4af267403d8 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -10,7 +10,7 @@
}
.ace_gutter-cell {
- background-color: $background-color;
+ background-color: $gray-light;
}
.cancel-btn {
@@ -34,7 +34,7 @@
}
.editor-ref {
- background: $background-color;
+ background: $gray-light;
padding-right: $gl-padding;
border-right: 1px solid $border-color;
display: block;
@@ -51,8 +51,16 @@
.new-file-name {
display: inline-block;
- width: 450px;
+ max-width: 450px;
float: left;
+
+ @media(max-width: $screen-md-max) {
+ width: 280px;
+ }
+
+ @media(max-width: $screen-sm-max) {
+ width: 180px;
+ }
}
.file-buttons {
@@ -67,7 +75,8 @@
.soft-wrap-toggle,
.license-selector,
.gitignore-selector,
- .gitlab-ci-yml-selector {
+ .gitlab-ci-yml-selector,
+ .dockerfile-selector {
display: inline-block;
vertical-align: top;
font-family: $regular_font;
@@ -97,7 +106,8 @@
.gitignore-selector,
.license-selector,
- .gitlab-ci-yml-selector {
+ .gitlab-ci-yml-selector,
+ .dockerfile-selector {
.dropdown {
line-height: 21px;
}
@@ -114,3 +124,42 @@
}
}
}
+
+@media(max-width: $screen-xs-max){
+ .file-editor {
+ .file-title {
+ .pull-right {
+ height: auto;
+ }
+ }
+
+ .new-file-name {
+ max-width: none;
+ width: 100%;
+ margin-bottom: 3px;
+ }
+
+ .file-buttons {
+ display: block;
+ width: 100%;
+ margin-bottom: 10px;
+
+ .soft-wrap-toggle {
+ width: 100%;
+ margin: 3px 0;
+ }
+
+ .encoding-selector,
+ .license-selector,
+ .gitignore-selector,
+ .gitlab-ci-yml-selector {
+ display: block;
+ margin: 3px 0;
+
+ button {
+ width: 100%;
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index de3d2ba549f..3d60426de01 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -1,6 +1,8 @@
-.deployments-container {
- width: 100%;
- overflow: auto;
+@media (max-width: $screen-md-max) {
+ .deployments-container {
+ width: 100%;
+ overflow: auto;
+ }
}
.environments-list-loading {
@@ -28,19 +30,25 @@
display: table-cell;
}
+ .environments-name,
.environments-commit,
.environments-actions {
width: 20%;
}
- .environments-deploy,
- .environments-build,
.environments-date {
width: 10%;
}
- .environments-name {
- width: 30%;
+ .environments-deploy,
+ .environments-build {
+ width: 15%;
+ }
+
+ .environment-name,
+ .environments-build-cell,
+ .deployment-column {
+ word-break: break-all;
}
.deployment-column {
@@ -64,14 +72,14 @@
.external-url,
.dropdown-new {
- color: $table-text-gray;
+ color: $gl-gray-light;
}
.dropdown-menu {
.fa {
margin-right: 6px;
- color: $table-text-gray;
+ color: $gl-gray-light;
}
}
@@ -82,7 +90,7 @@
.stop-env-link,
.external-url {
- color: $table-text-gray;
+ color: $gl-gray-light;
.stop-env-icon {
font-size: 14px;
@@ -117,7 +125,7 @@
.badge {
font-weight: normal;
background-color: $gray-darker;
- color: $gl-placeholder-color;
+ color: $gl-gray-light;
vertical-align: baseline;
}
}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index dc67d411c71..98925c2d0cb 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -5,7 +5,7 @@
.event-item {
font-size: $gl-font-size;
padding: $gl-padding-top 0 $gl-padding-top ($gl-avatar-size + $gl-padding-top);
- border-bottom: 1px solid $table-border-color;
+ border-bottom: 1px solid $white-normal;
color: $list-text-color;
&.event-inline {
diff --git a/app/assets/stylesheets/pages/explore.scss b/app/assets/stylesheets/pages/explore.scss
deleted file mode 100644
index 9b92128624c..00000000000
--- a/app/assets/stylesheets/pages/explore.scss
+++ /dev/null
@@ -1,8 +0,0 @@
-.explore-title {
- text-align: center;
-
- h3 {
- font-weight: normal;
- font-size: 30px;
- }
-}
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index a9af7af59e2..16bff5f1e03 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -27,12 +27,6 @@
}
}
-.group-buttons {
- .notification-dropdown {
- display: inline-block;
- }
-}
-
.groups-header {
@media (min-width: $screen-sm-min) {
.nav-links {
diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss
index e2e644dc23b..dae8ccdef6c 100644
--- a/app/assets/stylesheets/pages/help.scss
+++ b/app/assets/stylesheets/pages/help.scss
@@ -60,7 +60,7 @@
// Border around images in the help pages.
img:not(.emoji) {
- border: 1px solid $table-border-gray;
+ border: 1px solid $white-normal;
padding: 5px;
margin: 5px;
max-height: calc(100vh - 100px);
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 90587b9425b..4fac0cfb0ba 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -1,3 +1,50 @@
+// Limit MR description for side-by-side diff view
+.limit-container-width {
+ .detail-page-header {
+ max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ .issuable-details {
+ .detail-page-description,
+ .mr-source-target,
+ .mr-state-widget,
+ .merge-manually {
+ max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ .merge-request-tabs-holder {
+ &.affix {
+ border-bottom: 1px solid $border-color;
+
+ .nav-links {
+ border: 0;
+ }
+ }
+
+ .container-fluid {
+ padding-left: 0;
+ padding-right: 0;
+ max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
+ margin-left: auto;
+ margin-right: auto;
+ }
+ }
+ }
+
+ .diffs {
+ .mr-version-controls,
+ .files-changed {
+ max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
+ margin-left: auto;
+ margin-right: auto;
+ }
+ }
+}
+
.issuable-details {
section {
.issuable-discussion {
@@ -7,9 +54,8 @@
// Border around images in issue and MR descriptions.
.description img:not(.emoji) {
- border: 1px solid $table-border-gray;
+ border: 1px solid $white-normal;
padding: 5px;
- margin: 5px;
max-height: calc(100vh - 100px);
}
}
@@ -30,6 +76,7 @@
.color-label {
padding: 6px 10px;
+ border-radius: $label-border-radius;
}
}
@@ -50,7 +97,7 @@
.block {
@include clearfix;
padding: $gl-padding 0;
- border-bottom: 1px solid $border-gray-light;
+ border-bottom: 1px solid $border-gray-normal;
// This prevents the mess when resizing the sidebar
// of elements repositioning themselves..
width: $gutter_inner_width;
@@ -168,7 +215,7 @@
}
.no-value {
- color: $gl-placeholder-color;
+ color: $gl-gray-light;
}
.sidebar-collapsed-icon {
@@ -177,7 +224,7 @@
.gutter-toggle {
margin-top: 7px;
- border-left: 1px solid $border-gray-light;
+ border-left: 1px solid $border-gray-normal;
}
.assignee .avatar {
@@ -215,7 +262,7 @@
}
.participants {
- border-bottom: 1px solid $border-gray-light;
+ border-bottom: 1px solid $border-gray-normal;
}
.hide-collapsed {
@@ -332,7 +379,7 @@
margin-left: 5px;
a {
- color: $gl-placeholder-color;
+ color: $gl-gray-light;
}
}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 3b47f99df2c..8734a3b1598 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -88,12 +88,12 @@ ul.related-merge-requests > li {
&.closed {
background: $gray-light;
- border-color: $issues-border;
+ border-color: $border-color;
}
&.merged {
background: $gray-light;
- border-color: $issues-border;
+ border-color: $border-color;
}
}
@@ -144,7 +144,7 @@ ul.related-merge-requests > li {
}
.btn {
- background-color: $background-color;
- border: 1px solid $border-gray-light;
+ background-color: $gray-light;
+ border: 1px solid $border-gray-normal;
}
}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index b1ccd644450..d129eb12a45 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -98,13 +98,14 @@
}
.label {
- padding: 9px;
+ padding: 8px 9px 9px;
font-size: 14px;
}
}
.color-label {
- padding: 3px 4px;
+ padding: 3px 7px;
+ border-radius: $label-border-radius;
}
.dropdown-labels-error {
@@ -200,6 +201,8 @@
.label-remove {
border-left: 1px solid $label-remove-border;
z-index: 3;
+ border-radius: $label-border-radius;
+ padding: 6px 10px 6px 9px;
}
.btn {
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index 5f3bbb40ba0..36ee5d17211 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -78,6 +78,21 @@
float: right;
}
+ .dropdown {
+ width: 100%;
+ margin-top: 5px;
+
+ .dropdown-menu-toggle {
+ vertical-align: middle;
+ width: 100%;
+ }
+
+ @media (min-width: $screen-sm-min) {
+ margin-top: 0;
+ width: 155px;
+ }
+ }
+
.form-control {
width: 100%;
padding-right: 35px;
@@ -85,12 +100,22 @@
@media (min-width: $screen-sm-min) {
width: 350px;
}
+
+ &.input-short {
+ @media (min-width: $screen-md-min) {
+ width: 170px;
+ }
+
+ @media (min-width: $screen-lg-min) {
+ width: 210px;
+ }
+ }
}
}
.member-search-btn {
position: absolute;
- right: 0;
+ right: 4px;
top: 0;
height: 35px;
padding-left: 10px;
@@ -99,4 +124,8 @@
background: transparent;
border: 0;
outline: 0;
+
+ @media (min-width: $screen-sm-min) {
+ right: 160px;
+ }
}
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
index 7a90713dd3f..5a9f199fb34 100644
--- a/app/assets/stylesheets/pages/merge_conflicts.scss
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -274,7 +274,7 @@ $colors: (
}
.discard-changes-alert {
- background-color: $background-color;
+ background-color: $gray-light;
text-align: right;
padding: $gl-padding-top $gl-padding;
color: $gl-text-color;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 6234779ac19..e779e65eca3 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -3,7 +3,7 @@
*
*/
.mr-state-widget {
- background: $background-color;
+ background: $gray-light;
color: $gl-gray;
border: 1px solid $border-color;
border-radius: 2px;
@@ -359,7 +359,7 @@
th {
background-color: $white-light;
- color: $gl-placeholder-color;
+ color: $gl-gray-light;
}
}
}
@@ -375,7 +375,7 @@
}
.mr-version-controls {
- background: $background-color;
+ background: $gray-light;
border-bottom: 1px solid $border-color;
color: $gl-text-color;
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index dfc6079bd15..77c523d7310 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -108,7 +108,7 @@
margin-top: 7px;
.issuable-number {
- color: $gl-placeholder-color;
+ color: $gl-gray-light;
margin-right: 5px;
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index c35d71f9e7b..074abec7692 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -62,7 +62,7 @@
.common-note-form {
.md-area {
padding: $gl-padding-top $gl-padding;
- border: 1px solid $note-form-border-color;
+ border: 1px solid $border-color;
border-radius: $border-radius-base;
transition: border-color ease-in-out 0.15s,
box-shadow ease-in-out 0.15s;
@@ -204,7 +204,7 @@
.comment-toolbar {
padding-top: $gl-padding-top;
- color: $note-toolbar-color;
+ color: $gl-gray-light;
border-top: 1px solid $border-color;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 10eb3d4203e..106c5d4d390 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -166,7 +166,7 @@ ul.notes {
.note {
display: block;
position: relative;
- border-bottom: 1px solid $table-border-gray;
+ border-bottom: 1px solid $white-normal;
&.note-discussion {
&.timeline-entry {
@@ -291,14 +291,14 @@ ul.notes {
font-family: $regular_font;
td {
- border: 1px solid $table-border-gray;
+ border: 1px solid $white-normal;
border-left: none;
&.notes_line {
vertical-align: middle;
text-align: center;
padding: 10px 0;
- background: $background-color;
+ background: $gray-light;
color: $text-color;
}
@@ -309,7 +309,7 @@ ul.notes {
}
&.notes_content {
- background-color: $background-color;
+ background-color: $gray-light;
border-width: 1px 0;
padding: 0;
vertical-align: top;
@@ -372,7 +372,7 @@ ul.notes {
.note-actions {
float: right;
margin-left: 10px;
- color: $notes-action-color;
+ color: $gray-darkest;
}
.note-actions {
@@ -383,10 +383,6 @@ ul.notes {
.note-action-button {
margin-left: 10px;
}
-
- @media (min-width: $screen-sm-min) {
- position: relative;
- }
}
.discussion-actions {
@@ -411,7 +407,7 @@ ul.notes {
}
.fa {
- color: $notes-action-color;
+ color: $gray-darkest;
position: relative;
font-size: 17px;
}
@@ -448,7 +444,7 @@ ul.notes {
color: $notes-role-color;
font-size: 12px;
line-height: 20px;
- border: 1px solid $notes-role-border-color;
+ border: 1px solid $border-color;
border-radius: $border-radius-base;
}
@@ -529,7 +525,7 @@ ul.notes {
.line-resolve-all {
display: inline-block;
padding: 5px 10px;
- background-color: $background-color;
+ background-color: $gray-light;
border: 1px solid $border-color;
border-radius: $border-radius-default;
@@ -573,10 +569,10 @@ ul.notes {
svg {
position: relative;
- color: $notes-action-color;
+ color: $gray-darkest;
path {
- fill: $notes-action-color;
+ fill: $gray-darkest;
}
}
}
diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss
index 7d61390a439..bdf07a99daf 100644
--- a/app/assets/stylesheets/pages/notifications.scss
+++ b/app/assets/stylesheets/pages/notifications.scss
@@ -10,19 +10,7 @@
position: relative;
top: 1px;
- > .fa {
+ .fa {
font-size: 18px;
}
}
-
-.ns-part {
- color: $gl-text-green;
-}
-
-.ns-watch {
- color: $gl-success;
-}
-
-.ns-mute {
- color: $gl-danger;
-}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 08062b85504..c9d54b4f3d3 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -37,12 +37,13 @@
}
}
-.content-list {
-
- &.pipelines,
- &.builds-content-list {
- width: 100%;
- overflow: auto;
+@media (max-width: $screen-md-max) {
+ .content-list {
+ &.pipelines,
+ &.builds-content-list {
+ width: 100%;
+ overflow: auto;
+ }
}
}
@@ -117,7 +118,7 @@
height: 14px;
width: 14px;
vertical-align: middle;
- fill: $table-text-gray;
+ fill: $gl-gray-light;
}
.fa {
@@ -200,7 +201,7 @@
.duration,
.finished-at {
- color: $table-text-gray;
+ color: $gl-gray-light;
margin: 4px 0;
.fa {
@@ -221,7 +222,7 @@
.btn {
margin: 0;
- color: $table-text-gray;
+ color: $gl-gray-light;
}
.cancel-retry-btns {
@@ -234,10 +235,10 @@
.dropdown-toggle,
.dropdown-menu {
- color: $table-text-gray;
+ color: $gl-gray-light;
.fa {
- color: $table-text-gray;
+ color: $gl-gray-light;
font-size: 14px;
}
@@ -287,69 +288,162 @@
}
// Pipeline visualization
+.pipeline-actions {
+ border-bottom: none;
+}
+
+.tab-pane {
+ &.pipelines {
+ .ci-table {
+ min-width: 900px;
+ }
+
+ .content-list.pipelines {
+ overflow: auto;
+ }
-.toggle-pipeline-btn {
- background-color: $gray-dark;
+ .stage {
+ max-width: 100px;
+ width: 100px;
+ }
+
+ .pipeline-actions {
+ min-width: initial;
+ }
+ }
- &.graph-collapsed {
- background-color: $white-light;
+ &.builds {
+ .ci-table {
+ tr {
+ height: 71px;
+ }
+ }
}
}
+// Pipeline graph
.pipeline-graph {
width: 100%;
- background-color: $background-color;
+ background-color: $gray-light;
padding: $gl-padding;
overflow: auto;
white-space: nowrap;
transition: max-height 0.3s, padding 0.3s;
- &.graph-collapsed {
- max-height: 0;
- padding: 0 16px;
- }
-}
-
-.pipeline-visualization {
- position: relative;
-
ul {
padding: 0;
}
-}
-.stage-column {
- display: inline-block;
- vertical-align: top;
+ a {
+ text-decoration: none;
+ color: $gl-text-color-light;
+ }
- &:not(:last-child) {
- margin-right: 44px;
+ svg {
+ vertical-align: middle;
+ margin-right: 3px;
}
- &.left-margin {
- &:not(:first-child) {
- margin-left: 44px;
+ .stage-column {
+ display: inline-block;
+ vertical-align: top;
- .left-connector {
- &::before {
- content: '';
- position: absolute;
- top: 48%;
- left: -48px;
- border-top: 2px solid $border-color;
- width: 48px;
- height: 1px;
+ &:not(:last-child) {
+ margin-right: 44px;
+ }
+
+ &.left-margin {
+ &:not(:first-child) {
+ margin-left: 44px;
+
+ .left-connector {
+ &::before {
+ content: '';
+ position: absolute;
+ top: 48%;
+ left: -48px;
+ border-top: 2px solid $border-color;
+ width: 48px;
+ height: 1px;
+ }
}
}
}
- }
- &.no-margin {
- margin: 0;
- }
+ &.no-margin {
+ margin: 0;
+ }
+
+ li {
+ list-style: none;
+ }
+
+ &:last-child {
+ .build {
+ // Remove right connecting horizontal line from first build in last stage
+ &:first-child {
+ &::after {
+ border: none;
+ }
+ }
+ // Remove right curved connectors from all builds in last stage
+ &:not(:first-child) {
+ &::after {
+ border: none;
+ }
+ }
+ // Remove opposite curve
+ .curve {
+ &::before {
+ display: none;
+ }
+ }
+ }
+ }
+
+ &:first-child {
+ .build {
+ // Remove left curved connectors from all builds in first stage
+ &:not(:first-child) {
+ &::before {
+ border: none;
+ }
+ }
+ // Remove opposite curve
+ .curve {
+ &::after {
+ display: none;
+ }
+ }
+ }
+ }
+
+ // Curve first child connecting lines in opposite direction
+ .curve {
+ display: none;
+
+ &::before,
+ &::after {
+ content: '';
+ width: 21px;
+ height: 25px;
+ position: absolute;
+ top: -31px;
+ border-top: 2px solid $border-color;
+ }
+
+ &::after {
+ left: -44px;
+ border-right: 2px solid $border-color;
+ border-radius: 0 20px;
+ }
- li {
- list-style: none;
+ &::before {
+ right: -44px;
+ border-left: 2px solid $border-color;
+ border-radius: 20px 0 0;
+ }
+ }
}
.stage-name {
@@ -362,176 +456,78 @@
}
.build {
- border: 1px solid $border-color;
- background-color: $white-light;
position: relative;
- padding: 7px 10px 8px;
- border-radius: 30px;
width: 186px;
margin-bottom: 10px;
+ white-space: normal;
+ color: $gl-text-color-light;
- &:hover {
- background-color: $gray-lighter;
- }
-
- &.playable {
-
- svg {
- height: 13px;
- width: 20px;
- position: relative;
- top: 1px;
+ > .build-content {
+ display: inline-block;
+ padding: 8px 10px 9px;
+ width: 100%;
+ border: 1px solid $border-color;
+ border-radius: 30px;
+ background-color: $white-light;
- path {
- fill: $layout-link-gray;
- }
+ &:hover {
+ background-color: $stage-hover-bg;
+ border: 1px solid $stage-hover-border;
+ color: $gl-text-color;
}
}
- .build-content {
- display: -ms-flexbox;
- display: -webkit-flex;
- display: flex;
- width: 164px;
-
- .ci-status-icon {
- svg {
- height: 20px;
- width: 20px;
- }
- }
+ > .ci-action-icon-container {
+ position: absolute;
+ right: 4px;
+ top: 5px;
+ }
- .tooltip {
- white-space: nowrap;
+ .ci-status-icon {
+ position: relative;
+ top: 1px;
+ }
- .tooltip-inner {
- overflow: hidden;
- text-overflow: ellipsis;
- }
- }
+ .ci-status-icon svg {
+ height: 20px;
+ width: 20px;
+ }
- .ci-status-text {
- width: 135px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- vertical-align: middle;
+ .arrow {
+ &::before,
+ &::after {
+ content: '';
display: inline-block;
- position: relative;
- top: -1px;
- }
-
- a {
- color: $gl-text-color-light;
- text-decoration: none;
- }
-
- .dropdown-menu-toggle {
- background-color: transparent;
- border: none;
- width: auto;
- padding: 0;
- color: $gl-text-color-light;
- flex-grow: 1;
-
- .ci-status-text {
- max-width: 112px;
- width: auto;
- }
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid;
+ top: 18px;
}
- .grouped-pipeline-dropdown {
- padding: 0;
- width: 186px;
- left: auto;
- right: -197px;
- top: -9px;
-
- ul {
- max-height: 245px;
- overflow: auto;
-
- li:first-child {
- padding-top: 8px;
- }
-
- li:last-child {
- padding-bottom: 8px;
- }
- }
-
- a {
- color: $gl-text-color;
- padding: 7px 8px 8px;
-
- &:hover {
- background-color: $blue-light-transparent;
- border-radius: 3px;
-
- .ci-status-text {
- text-decoration: none;
- }
- }
- }
-
- svg {
- width: 14px;
- height: 14px;
- }
-
- .ci-status-text {
- width: 112px;
- }
-
- .arrow {
- &::before,
- &::after {
- content: '';
- display: inline-block;
- position: absolute;
- width: 0;
- height: 0;
- border-color: transparent;
- border-style: solid;
- top: 18px;
- }
-
- &::before {
- left: -5px;
- margin-top: -6px;
- border-width: 7px 5px 7px 0;
- border-right-color: $border-color;
- }
-
- &::after {
- left: -4px;
- margin-top: -9px;
- border-width: 10px 7px 10px 0;
- border-right-color: $white-light;
- }
- }
+ &::before {
+ left: -5px;
+ margin-top: -6px;
+ border-width: 7px 5px 7px 0;
+ border-right-color: $border-color;
}
- .badge {
- background-color: $gray-darker;
- color: $gl-text-color-light;
- font-weight: normal;
- margin-left: $btn-xs-side-margin;
+ &::after {
+ left: -4px;
+ margin-top: -9px;
+ border-width: 10px 7px 10px 0;
+ border-right-color: $white-light;
}
}
- svg {
- vertical-align: middle;
- margin-right: 5px;
- }
-
// Connect first build in each stage with right horizontal line
&:first-child {
&::after {
content: '';
position: absolute;
top: 48%;
- right: -48px;
+ right: -49px;
border-top: 2px solid $border-color;
width: 48px;
height: 1px;
@@ -579,113 +575,174 @@
}
}
- &:last-child {
- .build {
- // Remove right connecting horizontal line from first build in last stage
- &:first-child {
- &::after {
- border: none;
- }
- }
- // Remove right curved connectors from all builds in last stage
- &:not(:first-child) {
- &::after {
- border: none;
- }
- }
- // Remove opposite curve
- .curve {
- &::before {
- display: none;
- }
- }
- }
+ .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;
}
- &:first-child {
- .build {
- // Remove left curved connectors from all builds in first stage
- &:not(:first-child) {
- &::before {
- border: none;
- }
- }
- // Remove opposite curve
- .curve {
- &::after {
- display: none;
- }
+ .dropdown-menu-toggle {
+ background-color: transparent;
+ border: none;
+ padding: 0;
+ color: $gl-text-color-light;
+ white-space: normal;
+ overflow: visible;
+
+ &:focus {
+ outline: none;
+ }
+
+ &:hover {
+ color: $gl-text-color;
+
+ .dropdown-counter-badge {
+ color: $gl-text-color;
}
}
}
- // Curve first child connecting lines in opposite direction
- .curve {
- display: none;
+ .dropdown-counter-badge {
+ float: right;
+ clear: right;
+ color: $border-color;
+ font-weight: 100;
+ font-size: 15px;
+ margin-right: 2px;
+ }
- &::before,
- &::after {
- content: '';
- width: 21px;
- height: 25px;
- position: absolute;
- top: -32px;
- border-top: 2px solid $border-color;
- }
+ .grouped-pipeline-dropdown {
+ padding: 0;
+ width: 191px;
+ left: auto;
+ right: -195px;
+ top: -4px;
+ box-shadow: 0 1px 5px $black-transparent;
+
+ a {
+ display: inline-block;
- &::after {
- left: -44px;
- border-right: 2px solid $border-color;
- border-radius: 0 20px;
+ &:hover {
+ background-color: $stage-hover-bg;
+ }
}
- &::before {
- right: -44px;
- border-left: 2px solid $border-color;
- border-radius: 20px 0 0;
+ ul {
+ max-height: 245px;
+ overflow: auto;
+ margin: 5px 0;
+
+ li {
+ margin: 0 5px;
+ padding-left: 0;
+ padding-bottom: 0;
+ margin-bottom: 0;
+ line-height: 1.2;
+ }
}
- }
-}
-.pipeline-actions {
- border-bottom: none;
-}
+ .dropdown-build {
+ color: $gl-text-color-light;
-.toggle-pipeline-btn {
+ .build-content {
+ width: 100%;
+ }
- .fa {
- color: $dropdown-header-color;
- }
-}
+ .ci-action-icon-container {
+ font-size: 11px;
+ position: absolute;
+ right: 4px;
-.tab-pane {
+ i {
+ width: 25px;
+ height: 25px;
+ font-size: 11px;
+ margin-top: 0;
- &.pipelines {
+ &::before {
+ top: 1px;
+ left: 1px;
+ }
+ }
+ }
- .ci-table {
- min-width: 900px;
- }
+ &:hover {
+ background-color: $stage-hover-bg;
+ border-radius: 3px;
+ color: $gl-text-color;
+ }
- .content-list.pipelines {
- overflow: auto;
+ .stage {
+ max-width: 100px;
+ width: 100px;
+ }
+
+ .ci-status-icon svg {
+ height: 18px;
+ width: 18px;
+ }
+
+ .ci-status-text {
+ max-width: 95px;
+ padding-bottom: 3px;
+ position: relative;
+ top: 3px;
+ }
}
+ }
+}
- .stage {
- max-width: 100px;
- width: 100px;
+// Action Icons
+.ci-action-icon-container .ci-action-icon-wrapper {
+ i {
+ color: $border-color;
+ border-radius: 100%;
+ border: 1px solid $border-color;
+ padding: 5px 6px;
+ font-size: 13px;
+ background: $white-light;
+ height: 30px;
+ width: 30px;
+
+ &::before {
+ position: relative;
+ top: 3px;
+ left: 3px;
}
- .pipeline-actions {
- min-width: initial;
+ &:hover {
+ color: $gl-text-color;
+ background-color: $stage-hover-bg;
+ border: 1px solid $stage-hover-bg;
}
}
- &.builds {
+ .ci-play-icon {
+ padding: 5px 5px 5px 7px;
+ }
+}
- .ci-table {
- tr {
- height: 71px;
- }
+.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/profile.scss b/app/assets/stylesheets/pages/profile.scss
index f8677f93fe0..8b1976bd925 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -60,8 +60,8 @@
.account-well {
padding: 10px;
- background-color: $help-well-bg;
- border: 1px solid $help-well-border;
+ background-color: $gray-light;
+ border: 1px solid $border-color;
border-radius: $border-radius-base;
ul {
@@ -136,7 +136,7 @@
.provider-btn-group {
display: inline-block;
margin-right: 10px;
- border: 1px solid $provider-btn-group-border;
+ border: 1px solid $border-color;
border-radius: 3px;
&:last-child {
@@ -147,7 +147,7 @@
.provider-btn-image {
display: inline-block;
padding: 5px 10px;
- border-right: 1px solid $provider-btn-group-border;
+ border-right: 1px solid $border-color;
> img {
width: 20px;
@@ -198,7 +198,7 @@
}
.personal-access-tokens-never-expires-label {
- color: $personal-access-tokens-disabled-label-color;
+ color: $note-disabled-comment-color;
}
.datepicker.personal-access-tokens-expires-at .ui-state-disabled span {
@@ -262,3 +262,13 @@ table.u2f-registrations {
border-right: solid 1px transparent;
}
}
+
+.oauth-application-show {
+ .scope-name {
+ font-weight: 600;
+ }
+
+ .scopes-list {
+ padding-left: 18px;
+ }
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 6e0f6b1cd81..a443b6a37b3 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -6,12 +6,6 @@
}
}
-.no-ssh-key-message,
-.project-limit-message {
- background-color: $project-limit-message-bg;
- margin-bottom: 0;
-}
-
.new_project,
.edit-project {
@@ -151,8 +145,6 @@
.project-repo-buttons,
.group-buttons {
- margin-top: 15px;
-
.btn {
@include btn-gray;
padding: 3px 10px;
@@ -181,11 +173,9 @@
}
}
- .download-button,
- .dropdown-toggle,
- .notification-dropdown,
- .project-dropdown {
- margin-left: 10px;
+ .project-action-button {
+ margin: 15px 5px 0;
+ vertical-align: top;
}
.notification-dropdown .dropdown-menu {
@@ -201,13 +191,15 @@
.count-buttons {
display: inline-block;
vertical-align: top;
+ margin-top: 15px;
}
.project-clone-holder {
display: inline-block;
+ margin: 15px 5px 0 0;
input {
- height: 29px;
+ height: 28px;
}
}
@@ -261,7 +253,7 @@
line-height: 13px;
padding: $gl-vert-padding $gl-padding;
letter-spacing: .4px;
- padding: 7px 14px;
+ padding: 6px 14px;
text-align: center;
vertical-align: middle;
touch-action: manipulation;
@@ -420,13 +412,13 @@ a.deploy-project-label {
width: 100px;
height: 100px;
background-color: $gray-light;
- border: 1px solid $gray-dark;
+ border: 1px solid $white-normal;
margin: 0 auto;
border-radius: 50%;
i {
font-size: 100px;
- color: $gray-dark;
+ color: $white-normal;
}
}
@@ -498,6 +490,7 @@ a.deploy-project-label {
.project-stats {
font-size: 0;
+ text-align: center;
border-bottom: 1px solid $border-color;
.nav {
@@ -536,7 +529,7 @@ a.deploy-project-label {
}
li.missing {
- border: 1px dashed $border-gray-light;
+ border: 1px dashed $border-gray-normal;
border-radius: $border-radius-default;
a {
@@ -591,7 +584,7 @@ pre.light-well {
@include basic-list;
.project-row {
- border-color: $table-border-color;
+ border-color: $white-normal;
.project-full-name {
@include str-truncated;
@@ -643,7 +636,7 @@ pre.light-well {
&.container-fluid {
padding-top: 12px;
padding-bottom: 12px;
- background-color: $background-color;
+ background-color: $gray-light;
border: 1px solid $border-color;
border-right-width: 0;
border-left-width: 0;
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 63d0a34e610..cedd4cb2987 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -51,9 +51,9 @@
border-radius: $border-radius-default;
font-size: 14px;
font-style: normal;
- color: $location-badge-color;
+ color: $note-disabled-comment-color;
display: inline-block;
- background-color: $location-badge-bg;
+ background-color: $gray-normal;
vertical-align: top;
cursor: default;
}
@@ -140,7 +140,7 @@
.search-input-wrap {
i {
- color: $location-icon-active-color;
+ color: $layout-link-gray;
}
}
}
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 51c926608f9..ddee2c95247 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -1,5 +1,5 @@
.settings-list-icon {
- color: $gl-placeholder-color;
+ color: $gl-gray-light;
font-size: $settings-icon-size;
line-height: 42px;
}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 5084b466722..a810ed32327 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -15,8 +15,7 @@
height: 13px;
width: 13px;
position: relative;
- top: 1px;
- margin-right: 3px;
+ top: 2px;
overflow: visible;
}
@@ -25,7 +24,7 @@
border-color: $gl-danger;
&:not(span):hover {
- background-color: rgba( $gl-danger, .07);
+ background-color: rgba($gl-danger, .07);
}
svg {
@@ -39,7 +38,7 @@
border-color: $gl-success;
&:not(span):hover {
- background-color: rgba( $gl-success, .07);
+ background-color: rgba($gl-success, .07);
}
svg {
@@ -52,7 +51,7 @@
border-color: $gl-info;
&:not(span):hover {
- background-color: rgba( $gl-info, .07);
+ background-color: rgba($gl-info, .07);
}
svg {
@@ -66,7 +65,7 @@
border-color: $gl-gray;
&:not(span):hover {
- background-color: rgba( $gl-gray, .07);
+ background-color: rgba($gl-gray, .07);
}
svg {
@@ -79,7 +78,7 @@
border-color: $gl-warning;
&:not(span):hover {
- background-color: rgba( $gl-warning, .07);
+ background-color: rgba($gl-warning, .07);
}
svg {
@@ -92,7 +91,7 @@
border-color: $blue-normal;
&:not(span):hover {
- background-color: rgba( $blue-normal, .07);
+ background-color: rgba($blue-normal, .07);
}
svg {
@@ -102,15 +101,28 @@
&.ci-created,
&.ci-skipped {
- color: $table-text-gray;
- border-color: $table-text-gray;
+ color: $gl-gray-light;
+ border-color: $gl-gray-light;
&:not(span):hover {
- background-color: rgba( $table-text-gray, .07);
+ background-color: rgba($gl-gray-light, .07);
}
svg {
- fill: $table-text-gray;
+ fill: $gl-gray-light;
+ }
+ }
+
+ &.ci-manual {
+ color: $gl-text-color;
+ border-color: $gl-text-color;
+
+ &:not(span):hover {
+ background-color: rgba($gl-text-color, .07);
+ }
+
+ svg {
+ fill: $gl-text-color;
}
}
}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 20ad63be045..cad4e149845 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -14,14 +14,15 @@
.add-to-tree {
vertical-align: top;
+ padding: 6px 10px;
}
.tree-table {
margin-bottom: 0;
tr {
- border-bottom: 1px solid $table-border-gray;
- border-top: 1px solid $table-border-gray;
+ border-bottom: 1px solid $white-normal;
+ border-top: 1px solid $white-normal;
td,
th {
@@ -39,7 +40,7 @@
.commit-history-link-spacer {
margin: 0 10px;
- color: $table-border-color;
+ color: $white-normal;
}
&:hover {
@@ -53,7 +54,7 @@
&.selected {
td {
- background: $gray-dark;
+ background: $white-normal;
border-top: 1px solid $border-gray-dark;
border-bottom: 1px solid $border-gray-dark;
}
@@ -134,7 +135,7 @@
.blob-commit-info {
list-style: none;
padding: $gl-padding;
- background: $background-color;
+ background: $gray-light;
border: 1px solid $border-color;
border-bottom: none;
margin: 0;
@@ -172,7 +173,7 @@
position: relative;
z-index: 2;
- .download-button {
+ .project-action-button {
margin-left: $btn-side-margin;
}
}
diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss
index 9f9d630978a..b085c56390d 100644
--- a/app/assets/stylesheets/pages/xterm.scss
+++ b/app/assets/stylesheets/pages/xterm.scss
@@ -18,7 +18,7 @@
$l-blue: #81a2be;
$l-magenta: #b294bb;
$l-cyan: #8abeb7;
- $l-white: $ci-text-color;
+ $l-white: $gray-darkest;
/*
* xterm colors
diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb
index 471d24934a0..62f62e99a97 100644
--- a/app/controllers/admin/applications_controller.rb
+++ b/app/controllers/admin/applications_controller.rb
@@ -1,5 +1,8 @@
class Admin::ApplicationsController < Admin::ApplicationController
+ include OauthApplications
+
before_action :set_application, only: [:show, :edit, :update, :destroy]
+ before_action :load_scopes, only: [:new, :edit]
def index
@applications = Doorkeeper::Application.where("owner_id IS NULL")
@@ -47,6 +50,6 @@ class Admin::ApplicationsController < Admin::ApplicationController
# Only allow a trusted parameter "white list" through.
def application_params
- params[:doorkeeper_application].permit(:name, :redirect_uri)
+ params[:doorkeeper_application].permit(:name, :redirect_uri, :scopes)
end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index bcc0b17bce2..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
@@ -262,7 +266,7 @@ class ApplicationController < ActionController::Base
end
def bitbucket_import_configured?
- Gitlab::OAuth::Provider.enabled?(:bitbucket) && Gitlab::BitbucketImport.public_key.present?
+ Gitlab::OAuth::Provider.enabled?(:bitbucket)
end
def google_code_import_enabled?
diff --git a/app/controllers/concerns/oauth_applications.rb b/app/controllers/concerns/oauth_applications.rb
new file mode 100644
index 00000000000..9849aa93fa6
--- /dev/null
+++ b/app/controllers/concerns/oauth_applications.rb
@@ -0,0 +1,19 @@
+module OauthApplications
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :prepare_scopes, only: [:create, :update]
+ end
+
+ def prepare_scopes
+ scopes = params.fetch(:doorkeeper_application, {}).fetch(:scopes, nil)
+
+ if scopes
+ params[:doorkeeper_application][:scopes] = scopes.join(' ')
+ end
+ end
+
+ def load_scopes
+ @scopes = Doorkeeper.configuration.scopes
+ end
+end
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index c33d7eecb9f..549a8526715 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -18,7 +18,7 @@ module ServiceParams
:add_pusher, :send_from_committer_email, :disable_diffs,
:external_wiki_url, :notify, :color,
:server_host, :server_port, :default_irc_uri, :enable_ssl_verification,
- :jira_issue_transition_id, :url, :project_key]
+ :jira_issue_transition_id, :url, :project_key, :ca_pem, :namespace]
# Parameters to ignore if no value is specified
FILTER_BLANK_PARAMS = [:password]
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 940a3ad20ba..4f273a8d4f0 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -1,20 +1,20 @@
class Groups::GroupMembersController < Groups::ApplicationController
include MembershipActions
+ include SortingHelper
# Authorize
before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access]
def index
+ @sort = params[:sort].presence || sort_value_name
@project = @group.projects.find(params[:project_id]) if params[:project_id]
+
@members = @group.group_members
@members = @members.non_invite unless can?(current_user, :admin_group, @group)
+ @members = @members.search(params[:search]) if params[:search].present?
+ @members = @members.sort(@sort)
+ @members = @members.page(params[:page]).per(50)
- if params[:search].present?
- users = @group.users.search(params[:search]).to_a
- @members = @members.where(user_id: users)
- end
-
- @members = @members.order('access_level DESC').page(params[:page]).per(50)
@requesters = AccessRequestsFinder.new(@group).execute(current_user)
@group_member = @group.group_members.new
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 6ea54744da8..8e42cdf415f 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -2,50 +2,57 @@ class Import::BitbucketController < Import::BaseController
before_action :verify_bitbucket_import_enabled
before_action :bitbucket_auth, except: :callback
- rescue_from OAuth::Error, with: :bitbucket_unauthorized
- rescue_from Gitlab::BitbucketImport::Client::Unauthorized, with: :bitbucket_unauthorized
+ rescue_from OAuth2::Error, with: :bitbucket_unauthorized
+ rescue_from Bitbucket::Error::Unauthorized, with: :bitbucket_unauthorized
def callback
- request_token = session.delete(:oauth_request_token)
- raise "Session expired!" if request_token.nil?
+ response = client.auth_code.get_token(params[:code], redirect_uri: callback_import_bitbucket_url)
- request_token.symbolize_keys!
-
- access_token = client.get_token(request_token, params[:oauth_verifier], callback_import_bitbucket_url)
-
- session[:bitbucket_access_token] = access_token.token
- session[:bitbucket_access_token_secret] = access_token.secret
+ session[:bitbucket_token] = response.token
+ session[:bitbucket_expires_at] = response.expires_at
+ session[:bitbucket_expires_in] = response.expires_in
+ session[:bitbucket_refresh_token] = response.refresh_token
redirect_to status_import_bitbucket_url
end
def status
- @repos = client.projects
- @incompatible_repos = client.incompatible_projects
+ bitbucket_client = Bitbucket::Client.new(credentials)
+ repos = bitbucket_client.repos
+
+ @repos, @incompatible_repos = repos.partition { |repo| repo.valid? }
- @already_added_projects = current_user.created_projects.where(import_type: "bitbucket")
+ @already_added_projects = current_user.created_projects.where(import_type: 'bitbucket')
already_added_projects_names = @already_added_projects.pluck(:import_source)
- @repos.to_a.reject!{ |repo| already_added_projects_names.include? "#{repo["owner"]}/#{repo["slug"]}" }
+ @repos.to_a.reject! { |repo| already_added_projects_names.include?(repo.full_name) }
end
def jobs
- jobs = current_user.created_projects.where(import_type: "bitbucket").to_json(only: [:id, :import_status])
- render json: jobs
+ render json: current_user.created_projects
+ .where(import_type: 'bitbucket')
+ .to_json(only: [:id, :import_status])
end
def create
+ bitbucket_client = Bitbucket::Client.new(credentials)
+
@repo_id = params[:repo_id].to_s
- repo = client.project(@repo_id.gsub('___', '/'))
- @project_name = repo['slug']
- @target_namespace = find_or_create_namespace(repo['owner'], client.user['user']['username'])
+ name = @repo_id.gsub('___', '/')
+ repo = bitbucket_client.repo(name)
+ @project_name = params[:new_name].presence || repo.name
- unless Gitlab::BitbucketImport::KeyAdder.new(repo, current_user, access_params).execute
- render 'deploy_key' and return
- end
+ repo_owner = repo.owner
+ repo_owner = current_user.username if repo_owner == bitbucket_client.user.username
+ @target_namespace = params[:new_namespace].presence || repo_owner
+
+ namespace = find_or_create_namespace(@target_namespace, current_user)
- if current_user.can?(:create_projects, @target_namespace)
- @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute
+ if current_user.can?(:create_projects, namespace)
+ # The token in a session can be expired, we need to get most recent one because
+ # Bitbucket::Connection class refreshes it.
+ session[:bitbucket_token] = bitbucket_client.connection.token
+ @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @project_name, namespace, current_user, credentials).execute
else
render 'unauthorized'
end
@@ -54,8 +61,15 @@ class Import::BitbucketController < Import::BaseController
private
def client
- @client ||= Gitlab::BitbucketImport::Client.new(session[:bitbucket_access_token],
- session[:bitbucket_access_token_secret])
+ @client ||= OAuth2::Client.new(provider.app_id, provider.app_secret, options)
+ end
+
+ def provider
+ Gitlab::OAuth::Provider.config_for('bitbucket')
+ end
+
+ def options
+ OmniAuth::Strategies::Bitbucket.default_options[:client_options].deep_symbolize_keys
end
def verify_bitbucket_import_enabled
@@ -63,26 +77,23 @@ class Import::BitbucketController < Import::BaseController
end
def bitbucket_auth
- if session[:bitbucket_access_token].blank?
- go_to_bitbucket_for_permissions
- end
+ go_to_bitbucket_for_permissions if session[:bitbucket_token].blank?
end
def go_to_bitbucket_for_permissions
- request_token = client.request_token(callback_import_bitbucket_url)
- session[:oauth_request_token] = request_token
-
- redirect_to client.authorize_url(request_token, callback_import_bitbucket_url)
+ redirect_to client.auth_code.authorize_url(redirect_uri: callback_import_bitbucket_url)
end
def bitbucket_unauthorized
go_to_bitbucket_for_permissions
end
- def access_params
+ def credentials
{
- bitbucket_access_token: session[:bitbucket_access_token],
- bitbucket_access_token_secret: session[:bitbucket_access_token_secret]
+ token: session[:bitbucket_token],
+ expires_at: session[:bitbucket_expires_at],
+ expires_in: session[:bitbucket_expires_in],
+ refresh_token: session[:bitbucket_refresh_token]
}
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/jwt_controller.rb b/app/controllers/jwt_controller.rb
index c736200a104..c2e4d62b50b 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -26,7 +26,7 @@ class JwtController < ApplicationController
@authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
render_unauthorized unless @authentication_result.success? &&
- (@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User))
+ (@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User))
end
rescue Gitlab::Auth::MissingPersonalTokenError
render_missing_personal_token
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index 0f54dfa4efc..2ae4785b12c 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -2,10 +2,12 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
include Gitlab::CurrentSettings
include Gitlab::GonHelper
include PageLayoutHelper
+ include OauthApplications
before_action :verify_user_oauth_applications_enabled
before_action :authenticate_user!
before_action :add_gon_variables
+ before_action :load_scopes, only: [:index, :create, :edit]
layout 'profile'
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index 508b82a9a6c..6e007f17913 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -1,8 +1,6 @@
class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
- before_action :load_personal_access_tokens, only: :index
-
def index
- @personal_access_token = current_user.personal_access_tokens.build
+ set_index_vars
end
def create
@@ -12,7 +10,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
flash[:personal_access_token] = @personal_access_token.token
redirect_to profile_personal_access_tokens_path, notice: "Your new personal access token has been created."
else
- load_personal_access_tokens
+ set_index_vars
render :index
end
end
@@ -32,10 +30,12 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
private
def personal_access_token_params
- params.require(:personal_access_token).permit(:name, :expires_at)
+ params.require(:personal_access_token).permit(:name, :expires_at, scopes: [])
end
- def load_personal_access_tokens
+ def set_index_vars
+ @personal_access_token ||= current_user.personal_access_tokens.build
+ @scopes = Gitlab::Auth::SCOPES
@active_personal_access_tokens = current_user.personal_access_tokens.active.order(:expires_at)
@inactive_personal_access_tokens = current_user.personal_access_tokens.inactive
end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index 9eb75bb3891..18044ca78e2 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -22,6 +22,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
end
@qr_code = build_qr_code
+ @account_string = account_string
setup_u2f_registration
end
@@ -78,11 +79,14 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
private
def build_qr_code
- issuer = "#{issuer_host} | #{current_user.email}"
- uri = current_user.otp_provisioning_uri(current_user.email, issuer: issuer)
+ uri = current_user.otp_provisioning_uri(account_string, issuer: issuer_host)
RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3)
end
+ def account_string
+ "#{issuer_host}:#{current_user.email}"
+ end
+
def issuer_host
Gitlab.config.gitlab.host
end
diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb
new file mode 100644
index 00000000000..d9dfa534669
--- /dev/null
+++ b/app/controllers/projects/autocomplete_sources_controller.rb
@@ -0,0 +1,48 @@
+class Projects::AutocompleteSourcesController < Projects::ApplicationController
+ before_action :load_autocomplete_service, except: [:emojis, :members]
+
+ def emojis
+ render json: Gitlab::AwardEmoji.urls
+ end
+
+ def members
+ render json: ::Projects::ParticipantsService.new(@project, current_user).execute(noteable)
+ end
+
+ def issues
+ render json: @autocomplete_service.issues
+ end
+
+ def merge_requests
+ render json: @autocomplete_service.merge_requests
+ end
+
+ def labels
+ render json: @autocomplete_service.labels
+ end
+
+ def milestones
+ render json: @autocomplete_service.milestones
+ end
+
+ def commands
+ render json: @autocomplete_service.commands(noteable, params[:type])
+ end
+
+ private
+
+ def load_autocomplete_service
+ @autocomplete_service = ::Projects::AutocompleteService.new(@project, current_user)
+ end
+
+ def noteable
+ case params[:type]
+ when 'Issue'
+ IssuesFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id])
+ when 'MergeRequest'
+ MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id])
+ when 'Commit'
+ @project.commit(params[:type_id])
+ end
+ end
+end
diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb
index f576d0be1fc..863a766a255 100644
--- a/app/controllers/projects/blame_controller.rb
+++ b/app/controllers/projects/blame_controller.rb
@@ -8,6 +8,9 @@ class Projects::BlameController < Projects::ApplicationController
def show
@blob = @repository.blob_at(@commit.id, @path)
+
+ return render_404 unless @blob
+
@blame_groups = Gitlab::Blame.new(@blob, @commit).groups
end
end
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/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 15ca080c696..b71509f2c9b 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -217,6 +217,6 @@ class Projects::NotesController < Projects::ApplicationController
end
def find_current_user_notes
- @notes = NotesFinder.new.execute(project, current_user, params)
+ @notes = NotesFinder.new(project, current_user, params).execute.inc_author
end
end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 53308948f62..3aec6f18e27 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -1,10 +1,12 @@
class Projects::ProjectMembersController < Projects::ApplicationController
include MembershipActions
+ include SortingHelper
# Authorize
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
def index
+ @sort = params[:sort].presence || sort_value_name
@group_links = @project.project_group_links
@project_members = @project.project_members
@@ -35,12 +37,13 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
end
- wheres = ["id IN (#{@project_members.select(:id).to_sql})"]
- wheres << "id IN (#{group_members.select(:id).to_sql})" if group_members
+ wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"]
+ wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members
@project_members = Member.
where(wheres.join(' OR ')).
- order(access_level: :desc).page(params[:page])
+ sort(@sort).
+ page(params[:page])
@requesters = AccessRequestsFinder.new(@project).execute(current_user)
diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb
index 0825a4311cb..2c097cb4d8d 100644
--- a/app/controllers/projects/releases_controller.rb
+++ b/app/controllers/projects/releases_controller.rb
@@ -10,7 +10,14 @@ class Projects::ReleasesController < Projects::ApplicationController
end
def update
- release.update_attributes(release_params)
+ # Release belongs to Tag which is not active record object,
+ # it exists only to save a description to each Tag.
+ # If description is empty we should destroy the existing record.
+ if release_params[:description].present?
+ release.update_attributes(release_params)
+ else
+ release.destroy
+ end
redirect_to namespace_project_tag_path(@project.namespace, @project, @tag.name)
end
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index e290a0eadda..0720be2e55d 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -19,10 +19,12 @@ class Projects::SnippetsController < Projects::ApplicationController
respond_to :html
def index
- @snippets = SnippetsFinder.new.execute(current_user, {
+ @snippets = SnippetsFinder.new.execute(
+ current_user,
filter: :by_project,
- project: @project
- })
+ project: @project,
+ scope: params[:scope]
+ )
@snippets = @snippets.page(params[:page])
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index a8a18b4fa16..d5ee503c44c 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -127,39 +127,6 @@ class ProjectsController < Projects::ApplicationController
redirect_to edit_project_path(@project), alert: ex.message
end
- def autocomplete_sources
- noteable =
- case params[:type]
- when 'Issue'
- IssuesFinder.new(current_user, project_id: @project.id).
- execute.find_by(iid: params[:type_id])
- when 'MergeRequest'
- MergeRequestsFinder.new(current_user, project_id: @project.id).
- execute.find_by(iid: params[:type_id])
- when 'Commit'
- @project.commit(params[:type_id])
- else
- nil
- end
-
- autocomplete = ::Projects::AutocompleteService.new(@project, current_user)
- participants = ::Projects::ParticipantsService.new(@project, current_user).execute(noteable)
-
- @suggestions = {
- emojis: Gitlab::AwardEmoji.urls,
- issues: autocomplete.issues,
- milestones: autocomplete.milestones,
- mergerequests: autocomplete.merge_requests,
- labels: autocomplete.labels,
- members: participants,
- commands: autocomplete.commands(noteable, params[:type])
- }
-
- respond_to do |format|
- format.json { render json: @suggestions }
- end
- end
-
def new_issue_address
return render_404 unless Gitlab::IncomingEmail.supports_issue_creation?
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 8c698695202..93a180b9036 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -114,7 +114,7 @@ class SessionsController < Devise::SessionsController
def valid_otp_attempt?(user)
user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
- user.invalidate_otp_backup_code!(user_params[:otp_attempt])
+ user.invalidate_otp_backup_code!(user_params[:otp_attempt])
end
def log_audit_event(user, options = {})
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index be00a219205..707eddd4d29 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -23,10 +23,26 @@ class IssuesFinder < IssuableFinder
private
def init_collection
- Issue.visible_to_user(current_user)
+ IssuesFinder.not_restricted_by_confidentiality(current_user)
end
def iid_pattern
@iid_pattern ||= %r{\A#{Regexp.escape(Issue.reference_prefix)}(?<iid>\d+)\z}
end
+
+ def self.not_restricted_by_confidentiality(user)
+ return Issue.where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank?
+
+ return Issue.all if user.admin?
+
+ Issue.where('
+ issues.confidential IS NULL
+ OR issues.confidential IS FALSE
+ OR (issues.confidential = TRUE
+ AND (issues.author_id = :user_id
+ OR issues.assignee_id = :user_id
+ OR issues.project_id IN(:project_ids)))',
+ user_id: user.id,
+ project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
+ end
end
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index 2484339e3a4..4bd8c83081a 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -1,27 +1,102 @@
class NotesFinder
FETCH_OVERLAP = 5.seconds
- def execute(project, current_user, params)
- target_type = params[:target_type]
- target_id = params[:target_id]
- # Default to 0 to remain compatible with old clients
- last_fetched_at = Time.at(params.fetch(:last_fetched_at, 0).to_i)
-
- notes =
- case target_type
- when "commit"
- project.notes.for_commit_id(target_id).non_diff_notes
- when "issue"
- IssuesFinder.new(current_user, project_id: project.id).find(target_id).notes.inc_author
- when "merge_request"
- MergeRequestsFinder.new(current_user, project_id: project.id).find(target_id).mr_and_commit_notes.inc_author
- when "snippet", "project_snippet"
- project.snippets.find(target_id).notes
+ # Used to filter Notes
+ # When used with target_type and target_id this returns notes specifically for the controller
+ #
+ # Arguments:
+ # current_user - which user check authorizations with
+ # project - which project to look for notes on
+ # params:
+ # target_type: string
+ # target_id: integer
+ # last_fetched_at: time
+ # search: string
+ #
+ def initialize(project, current_user, params = {})
+ @project = project
+ @current_user = current_user
+ @params = params
+ init_collection
+ end
+
+ def execute
+ @notes = since_fetch_at(@params[:last_fetched_at]) if @params[:last_fetched_at]
+ @notes
+ end
+
+ private
+
+ def init_collection
+ if @params[:target_id]
+ @notes = on_target(@params[:target_type], @params[:target_id])
+ else
+ @notes = notes_of_any_type
+ end
+ end
+
+ def notes_of_any_type
+ types = %w(commit issue merge_request snippet)
+ note_relations = types.map { |t| notes_for_type(t) }
+ note_relations.map!{ |notes| search(@params[:search], notes) } if @params[:search]
+ UnionFinder.new.find_union(note_relations, Note)
+ end
+
+ def noteables_for_type(noteable_type)
+ case noteable_type
+ when "issue"
+ IssuesFinder.new(@current_user, project_id: @project.id).execute
+ when "merge_request"
+ MergeRequestsFinder.new(@current_user, project_id: @project.id).execute
+ when "snippet", "project_snippet"
+ SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project)
+ else
+ raise 'invalid target_type'
+ end
+ end
+
+ def notes_for_type(noteable_type)
+ if noteable_type == "commit"
+ if Ability.allowed?(@current_user, :download_code, @project)
+ @project.notes.where(noteable_type: 'Commit')
+ else
+ Note.none
+ end
+ else
+ finder = noteables_for_type(noteable_type)
+ @project.notes.where(noteable_type: finder.base_class.name, noteable_id: finder.reorder(nil))
+ end
+ end
+
+ def on_target(target_type, target_id)
+ if target_type == "commit"
+ notes_for_type('commit').for_commit_id(target_id)
+ else
+ target = noteables_for_type(target_type).find(target_id)
+
+ if target.respond_to?(:related_notes)
+ target.related_notes
else
- raise 'invalid target_type'
+ target.notes
end
+ end
+ end
+
+ # Searches for notes matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ def search(query, notes_relation = @notes)
+ pattern = "%#{query}%"
+ notes_relation.where(Note.arel_table[:note].matches(pattern))
+ end
+
+ # Notes changed since last fetch
+ # Uses overlapping intervals to avoid worrying about race conditions
+ def since_fetch_at(fetch_time)
+ # Default to 0 to remain compatible with old clients
+ last_fetched_at = Time.at(@params.fetch(:last_fetched_at, 0).to_i)
- # Use overlapping intervals to avoid worrying about race conditions
- notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh
+ @notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh
end
end
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index 0586a923a74..da6e6e87a6f 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -11,7 +11,7 @@ class SnippetsFinder
when :by_user then
by_user(current_user, user, params[:scope])
when :by_project
- by_project(current_user, params[:project])
+ by_project(current_user, params[:project], params[:scope])
end
end
@@ -32,35 +32,35 @@ class SnippetsFinder
def by_user(current_user, user, scope)
snippets = user.snippets.fresh
- return snippets.are_public unless current_user
-
- if user == current_user
- case scope
- when 'are_internal' then
- snippets.are_internal
- when 'are_private' then
- snippets.are_private
- when 'are_public' then
- snippets.are_public
- else
- snippets
- end
+ if current_user
+ include_private = user == current_user
+ by_scope(snippets, scope, include_private)
else
- snippets.public_and_internal
+ snippets.are_public
end
end
- def by_project(current_user, project)
+ def by_project(current_user, project, scope)
snippets = project.snippets.fresh
if current_user
- if project.team.member?(current_user) || current_user.admin?
- snippets
- else
- snippets.public_and_internal
- end
+ include_private = project.team.member?(current_user) || current_user.admin?
+ by_scope(snippets, scope, include_private)
else
snippets.are_public
end
end
+
+ def by_scope(snippets, scope = nil, include_private = false)
+ case scope.to_s
+ when 'are_private'
+ include_private ? snippets.are_private : Snippet.none
+ when 'are_internal'
+ snippets.are_internal
+ when 'are_public'
+ snippets.are_public
+ else
+ include_private ? snippets : snippets.public_and_internal
+ end
+ end
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 07ff6fb9488..f31d4fb897d 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -191,6 +191,10 @@ module BlobHelper
@gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names
end
+ def dockerfile_names
+ @dockerfile_names ||= Gitlab::Template::DockerfileTemplate.dropdown_names
+ end
+
def blob_editor_paths
{
'relative-url-root' => Rails.application.config.relative_url_root,
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index dee3c78df47..4c7c16d694c 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -16,7 +16,7 @@ module ButtonHelper
# See http://clipboardjs.com/#usage
def clipboard_button(data = {})
css_class = data[:class] || 'btn-clipboard btn-transparent'
- title = data[:title] || 'Copy to Clipboard'
+ title = data[:title] || 'Copy to clipboard'
data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
content_tag :button,
icon('clipboard'),
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 8e19752a8a1..d9f5e01f0dc 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -4,25 +4,7 @@ module CiStatusHelper
builds_namespace_project_commit_path(project.namespace, project, pipeline.sha)
end
- def ci_status_with_icon(status, target = nil)
- content = ci_icon_for_status(status) + ci_text_for_status(status)
- klass = "ci-status ci-#{status}"
-
- if target
- link_to content, target, class: klass
- else
- content_tag :span, content, class: klass
- end
- end
-
- def ci_text_for_status(status)
- if detailed_status?(status)
- status.text
- else
- status
- end
- end
-
+ # Is used by Commit and Merge Request Widget
def ci_label_for_status(status)
if detailed_status?(status)
return status.label
diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb
index 27975b7ddb7..ff8550439d0 100644
--- a/app/helpers/environment_helper.rb
+++ b/app/helpers/environment_helper.rb
@@ -14,10 +14,12 @@ module EnvironmentHelper
end
end
- def deployment_link(deployment)
+ def deployment_link(deployment, text: nil)
return unless deployment
- link_to "##{deployment.iid}", [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable]
+ link_label = text ? text : "##{deployment.iid}"
+
+ link_to link_label, [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable]
end
def last_deployment_link_for_environment_build(project, build)
diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb
index 6a43be2cf3e..1182939f656 100644
--- a/app/helpers/form_helper.rb
+++ b/app/helpers/form_helper.rb
@@ -7,12 +7,12 @@ module FormHelper
content_tag(:div, class: 'alert alert-danger', id: 'error_explanation') do
content_tag(:h4, headline) <<
- content_tag(:ul) do
- model.errors.full_messages.
- map { |msg| content_tag(:li, msg) }.
- join.
- html_safe
- end
+ content_tag(:ul) do
+ model.errors.full_messages.
+ map { |msg| content_tag(:li, msg) }.
+ join.
+ html_safe
+ end
end
end
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index f6d4ea4659a..77dc9e7d538 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -12,11 +12,18 @@ module GroupsHelper
end
def group_title(group, name = nil, url = nil)
- full_title = link_to(simple_sanitize(group.name), group_path(group))
+ full_title = ''
+
+ group.parents.each do |parent|
+ full_title += link_to(simple_sanitize(parent.name), group_path(parent))
+ full_title += ' / '.html_safe
+ end
+
+ full_title += link_to(simple_sanitize(group.name), group_path(group))
full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url) if name
content_tag :span do
- full_title
+ full_title.html_safe
end
end
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/members_helper.rb b/app/helpers/members_helper.rb
index 877c77050be..41d471cc92f 100644
--- a/app/helpers/members_helper.rb
+++ b/app/helpers/members_helper.rb
@@ -36,4 +36,12 @@ module MembersHelper
"Are you sure you want to leave the " \
"\"#{member_source.human_name}\" #{member_source.class.to_s.humanize(capitalize: false)}?"
end
+
+ def filter_group_project_member_path(options = {})
+ options = params.slice(:search, :sort).merge(options)
+
+ path = request.path
+ path << "?#{options.to_param}"
+ path
+ end
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index a6659ea2fd1..20218775659 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -59,6 +59,10 @@ module MergeRequestsHelper
@mr_closes_issues ||= @merge_request.closes_issues
end
+ def mr_issues_mentioned_but_not_closing
+ @mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing
+ end
+
def mr_change_branches_path(merge_request)
new_namespace_project_merge_request_path(
@project.namespace, @project,
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index a3331dc80cb..e21178c7377 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -7,12 +7,12 @@ module NavHelper
def page_gutter_class
if current_path?('merge_requests#show') ||
- current_path?('merge_requests#diffs') ||
- current_path?('merge_requests#commits') ||
- current_path?('merge_requests#builds') ||
- current_path?('merge_requests#conflicts') ||
- current_path?('merge_requests#pipelines') ||
- current_path?('issues#show')
+ current_path?('merge_requests#diffs') ||
+ current_path?('merge_requests#commits') ||
+ current_path?('merge_requests#builds') ||
+ current_path?('merge_requests#conflicts') ||
+ current_path?('merge_requests#pipelines') ||
+ current_path?('issues#show')
if cookies[:collapsed_gutter] == 'true'
"page-gutter right-sidebar-collapsed"
else
@@ -21,9 +21,9 @@ module NavHelper
elsif current_path?('builds#show')
"page-gutter build-sidebar right-sidebar-expanded"
elsif current_path?('wikis#show') ||
- current_path?('wikis#edit') ||
- current_path?('wikis#history') ||
- current_path?('wikis#git_access')
+ current_path?('wikis#edit') ||
+ current_path?('wikis#history') ||
+ current_path?('wikis#git_access')
"page-gutter wiki-sidebar right-sidebar-expanded"
end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 3acbc5fea27..ebe763e6033 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -52,7 +52,7 @@ module ProjectsHelper
def project_title(project)
namespace_link =
if project.group
- link_to(simple_sanitize(project.group.name), group_path(project.group))
+ group_title(project.group)
else
owner = project.namespace.owner
link_to(simple_sanitize(owner.name), user_path(owner))
@@ -392,7 +392,7 @@ module ProjectsHelper
"success"
end
end
-
+
def readme_cache_key
sha = @project.commit.try(:sha) || 'nil'
[@project.path_with_namespace, sha, "readme"].join('-')
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index 7e33a562077..8c02b4061ca 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -8,6 +8,17 @@ module SnippetsHelper
end
end
+ # Return the path of a snippets index for a user or for a project
+ #
+ # @returns String, path to snippet index
+ def subject_snippets_path(subject = nil, opts = nil)
+ if subject.is_a?(Project)
+ namespace_project_snippets_path(subject.namespace, subject, opts)
+ else # assume subject === User
+ dashboard_snippets_path(opts)
+ end
+ end
+
# Get an array of line numbers surrounding a matching
# line, bounded by min/max.
#
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 8b138a8e69f..f03c4627050 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -25,7 +25,7 @@ module SortingHelper
sort_value_recently_updated => sort_title_recently_updated,
sort_value_oldest_updated => sort_title_oldest_updated,
sort_value_recently_created => sort_title_recently_created,
- sort_value_oldest_created => sort_title_oldest_created,
+ sort_value_oldest_created => sort_title_oldest_created
}
if current_controller?('admin/projects')
@@ -35,6 +35,19 @@ module SortingHelper
options
end
+ def member_sort_options_hash
+ {
+ sort_value_access_level_asc => sort_title_access_level_asc,
+ sort_value_access_level_desc => sort_title_access_level_desc,
+ sort_value_last_joined => sort_title_last_joined,
+ sort_value_oldest_joined => sort_title_oldest_joined,
+ sort_value_name => sort_title_name_asc,
+ sort_value_name_desc => sort_title_name_desc,
+ sort_value_recently_signin => sort_title_recently_signin,
+ sort_value_oldest_signin => sort_title_oldest_signin
+ }
+ end
+
def sort_title_priority
'Priority'
end
@@ -95,6 +108,50 @@ module SortingHelper
'Most popular'
end
+ def sort_title_last_joined
+ 'Last joined'
+ end
+
+ def sort_title_oldest_joined
+ 'Oldest joined'
+ end
+
+ def sort_title_access_level_asc
+ 'Access level, ascending'
+ end
+
+ def sort_title_access_level_desc
+ 'Access level, descending'
+ end
+
+ def sort_title_name_asc
+ 'Name, ascending'
+ end
+
+ def sort_title_name_desc
+ 'Name, descending'
+ end
+
+ def sort_value_last_joined
+ 'last_joined'
+ end
+
+ def sort_value_oldest_joined
+ 'oldest_joined'
+ end
+
+ def sort_value_access_level_asc
+ 'access_level_asc'
+ end
+
+ def sort_value_access_level_desc
+ 'access_level_desc'
+ end
+
+ def sort_value_name_desc
+ 'name_desc'
+ end
+
def sort_value_priority
'priority'
end
diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb
index 563ddd2a511..547f6258909 100644
--- a/app/helpers/tab_helper.rb
+++ b/app/helpers/tab_helper.rb
@@ -106,9 +106,9 @@ module TabHelper
def branches_tab_class
if current_controller?(:protected_branches) ||
- current_controller?(:branches) ||
- current_page?(namespace_project_repository_path(@project.namespace,
- @project))
+ current_controller?(:branches) ||
+ current_page?(namespace_project_repository_path(@project.namespace,
+ @project))
'active'
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 88c46076df6..cb76cdf5981 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -9,8 +9,16 @@ module Ci
has_many :deployments, as: :deployable
+ # The "environment" field for builds is a String, and is the unexpanded name
+ def persisted_environment
+ @persisted_environment ||= Environment.find_by(
+ name: expanded_environment_name,
+ project_id: gl_project_id
+ )
+ end
+
serialize :options
- serialize :yaml_variables
+ serialize :yaml_variables, Gitlab::Serialize::Ci::Variables
validates :coverage, numericality: true, allow_blank: true
validates_presence_of :ref
@@ -100,6 +108,12 @@ module Ci
end
end
+ def detailed_status(current_user)
+ Gitlab::Ci::Status::Build::Factory
+ .new(self, current_user)
+ .fabricate!
+ end
+
def manual?
self.when == 'manual'
end
@@ -123,8 +137,13 @@ module Ci
end
end
+ def cancelable?
+ active?
+ end
+
def retryable?
- project.builds_enabled? && commands.present? && complete?
+ project.builds_enabled? && commands.present? &&
+ (success? || failed? || canceled?)
end
def retried?
@@ -132,11 +151,11 @@ module Ci
end
def expanded_environment_name
- ExpandVariables.expand(environment, variables) if environment
+ ExpandVariables.expand(environment, simple_variables) if environment
end
def has_environment?
- self.environment.present?
+ environment.present?
end
def starts_environment?
@@ -148,7 +167,7 @@ module Ci
end
def environment_action
- self.options.fetch(:environment, {}).fetch(:action, 'start')
+ self.options.fetch(:environment, {}).fetch(:action, 'start') if self.options
end
def outdated_deployment?
@@ -184,12 +203,25 @@ module Ci
project.build_timeout
end
- def variables
+ # A slugified version of the build ref, suitable for inclusion in URLs and
+ # domain names. Rules:
+ #
+ # * Lowercased
+ # * Anything not matching [a-z0-9-] is replaced with a -
+ # * Maximum length is 63 bytes
+ def ref_slug
+ slugified = ref.to_s.downcase
+ slugified.gsub(/[^a-z0-9]/, '-')[0..62]
+ end
+
+ # Variables whose value does not depend on other variables
+ def simple_variables
variables = predefined_variables
variables += project.predefined_variables
variables += pipeline.predefined_variables
variables += runner.predefined_variables if runner
variables += project.container_registry_variables
+ variables += project.deployment_variables if has_environment?
variables += yaml_variables
variables += user_variables
variables += project.secret_variables
@@ -197,6 +229,13 @@ module Ci
variables
end
+ # All variables, including those dependent on other variables
+ def variables
+ variables = simple_variables
+ variables += persisted_environment.predefined_variables if persisted_environment.present?
+ variables
+ end
+
def merge_request
merge_requests = MergeRequest.includes(:merge_request_diff)
.where(source_branch: ref, source_project_id: pipeline.gl_project_id)
@@ -518,6 +557,7 @@ module Ci
{ key: 'CI_BUILD_REF', value: sha, public: true },
{ key: 'CI_BUILD_BEFORE_SHA', value: before_sha, public: true },
{ key: 'CI_BUILD_REF_NAME', value: ref, public: true },
+ { key: 'CI_BUILD_REF_SLUG', value: ref_slug, public: true },
{ key: 'CI_BUILD_NAME', value: name, public: true },
{ key: 'CI_BUILD_STAGE', value: stage, public: true },
{ key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index fda8228a1e9..48354cdbefb 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -88,8 +88,24 @@ module Ci
end
# ref can't be HEAD or SHA, can only be branch/tag name
+ scope :latest, ->(ref = nil) do
+ max_id = unscope(:select)
+ .select("max(#{quoted_table_name}.id)")
+ .group(:ref, :sha)
+
+ if ref
+ where(id: max_id, ref: ref)
+ else
+ where(id: max_id)
+ end
+ end
+
+ def self.latest_status(ref = nil)
+ latest(ref).status
+ end
+
def self.latest_successful_for(ref)
- where(ref: ref).order(id: :desc).success.first
+ success.latest(ref).first
end
def self.truncate_sha(sha)
@@ -336,8 +352,10 @@ module Ci
.select { |merge_request| merge_request.head_pipeline.try(:id) == self.id }
end
- def detailed_status
- Gitlab::Ci::Status::Pipeline::Factory.new(self).fabricate!
+ def detailed_status(current_user)
+ Gitlab::Ci::Status::Pipeline::Factory
+ .new(self, current_user)
+ .fabricate!
end
private
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index d2a37c0a827..7ef59445d77 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -22,8 +22,10 @@ module Ci
@status ||= statuses.latest.status
end
- def detailed_status
- Gitlab::Ci::Status::Stage::Factory.new(self).fabricate!
+ def detailed_status(current_user)
+ Gitlab::Ci::Status::Stage::Factory
+ .new(self, current_user)
+ .fabricate!
end
def statuses
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 1831cc7e175..69cfc47f5bf 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -228,13 +228,9 @@ class Commit
def status(ref = nil)
@statuses ||= {}
- if @statuses.key?(ref)
- @statuses[ref]
- elsif ref
- @statuses[ref] = pipelines.where(ref: ref).status
- else
- @statuses[ref] = pipelines.status
- end
+ return @statuses[ref] if @statuses.key?(ref)
+
+ @statuses[ref] = pipelines.latest_status(ref)
end
def revert_branch_name
@@ -270,7 +266,7 @@ class Commit
@merged_merge_request_hash ||= Hash.new do |hash, user|
hash[user] = merged_merge_request_no_cache(user)
end
-
+
@merged_merge_request_hash[current_user]
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index cf90475f4d4..31cd381dcd2 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -131,4 +131,10 @@ class CommitStatus < ActiveRecord::Base
def has_trace?
false
end
+
+ def detailed_status(current_user)
+ Gitlab::Ci::Status::Factory
+ .new(self, current_user)
+ .fabricate!
+ end
end
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index 875e9834487..4359f1d7b06 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -30,7 +30,7 @@ module Milestoneish
end
def issues_visible_to_user(user)
- issues.visible_to_user(user)
+ IssuesFinder.new(user).execute.where(id: issues)
end
def upcoming?
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/concerns/routable.rb b/app/models/concerns/routable.rb
index d36bb9da296..1108a64c59e 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -7,6 +7,7 @@ module Routable
has_one :route, as: :source, autosave: true, dependent: :destroy
validates_associated :route
+ validates :route, presence: true
before_validation :update_route_path, if: :full_path_changed?
end
@@ -28,17 +29,17 @@ module Routable
order_sql = "(CASE WHEN #{binary} routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)"
- where_paths_in([path]).reorder(order_sql).take
+ where_full_path_in([path]).reorder(order_sql).take
end
# Builds a relation to find multiple objects by their full paths.
#
# Usage:
#
- # Klass.where_paths_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee})
+ # Klass.where_full_path_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee})
#
# Returns an ActiveRecord::Relation.
- def where_paths_in(paths)
+ def where_full_path_in(paths)
wheres = []
cast_lower = Gitlab::Database.postgresql?
diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb
index 04d30f46210..1ca7f91dc03 100644
--- a/app/models/concerns/token_authenticatable.rb
+++ b/app/models/concerns/token_authenticatable.rb
@@ -39,6 +39,10 @@ module TokenAuthenticatable
current_token.blank? ? write_new_token(token_field) : current_token
end
+ define_method("set_#{token_field}") do |token|
+ write_attribute(token_field, token) if token
+ end
+
define_method("ensure_#{token_field}!") do
send("reset_#{token_field}!") if read_attribute(token_field).blank?
read_attribute(token_field)
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 96700143ddd..5cde94b3509 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -1,9 +1,15 @@
class Environment < ActiveRecord::Base
+ # Used to generate random suffixes for the slug
+ NUMBERS = '0'..'9'
+ SUFFIX_CHARS = ('a'..'z').to_a + NUMBERS.to_a
+
belongs_to :project, required: true, validate: true
has_many :deployments
before_validation :nullify_external_url
+ before_validation :generate_slug, if: ->(env) { env.slug.blank? }
+
before_save :set_environment_type
validates :name,
@@ -13,6 +19,13 @@ class Environment < ActiveRecord::Base
format: { with: Gitlab::Regex.environment_name_regex,
message: Gitlab::Regex.environment_name_regex_message }
+ validates :slug,
+ presence: true,
+ uniqueness: { scope: :project_id },
+ length: { maximum: 24 },
+ format: { with: Gitlab::Regex.environment_slug_regex,
+ message: Gitlab::Regex.environment_slug_regex_message }
+
validates :external_url,
uniqueness: { scope: :project_id },
length: { maximum: 255 },
@@ -37,6 +50,13 @@ class Environment < ActiveRecord::Base
state :stopped
end
+ def predefined_variables
+ [
+ { key: 'CI_ENVIRONMENT_NAME', value: name, public: true },
+ { key: 'CI_ENVIRONMENT_SLUG', value: slug, public: true },
+ ]
+ end
+
def recently_updated_on_branch?(ref)
ref.to_s == last_deployment.try(:ref)
end
@@ -107,4 +127,49 @@ class Environment < ActiveRecord::Base
action.expanded_environment_name == environment
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:
+ # * contains only lowercase letters (a-z), numbers (0-9), and '-'
+ # * begins with a letter
+ # * has a maximum length of 24 bytes (OpenShift limitation)
+ # * cannot end with `-`
+ def generate_slug
+ # Lowercase letters and numbers only
+ slugified = name.to_s.downcase.gsub(/[^a-z0-9]/, '-')
+
+ # Must start with a letter
+ slugified = "env-" + slugified if NUMBERS.cover?(slugified[0])
+
+ # Maximum length: 24 characters (OpenShift limitation)
+ slugified = slugified[0..23]
+
+ # Cannot end with a "-" character (Kubernetes label limitation)
+ slugified = slugified[0..-2] if slugified[-1] == "-"
+
+ # Add a random suffix, shortening the current string if necessary, if it
+ # has been slugified. This ensures uniqueness.
+ slugified = slugified[0..16] + "-" + random_suffix if slugified != name
+
+ self.slug = slugified
+ end
+
+ private
+
+ # Slugifying a name may remove the uniqueness guarantee afforded by it being
+ # based on name (which must be unique). To compensate, we add a random
+ # 6-byte suffix in those circumstances. This is not *guaranteed* uniqueness,
+ # but the chance of collisions is vanishingly small
+ def random_suffix
+ (0..5).map { SUFFIX_CHARS.sample }.join
+ end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 4248e1162d8..ac8a82c8c1e 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -83,7 +83,7 @@ class Group < Namespace
end
def human_name
- name
+ full_name
end
def visibility_level_field
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 7fe92051037..738c96e4db3 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -60,61 +60,6 @@ class Issue < ActiveRecord::Base
attributes
end
- class << self
- private
-
- # Returns the project that the current scope belongs to if any, nil otherwise.
- #
- # Examples:
- # - my_project.issues.without_due_date.owner_project => my_project
- # - Issue.all.owner_project => nil
- def owner_project
- # No owner if we're not being called from an association
- return unless all.respond_to?(:proxy_association)
-
- owner = all.proxy_association.owner
-
- # Check if the association is or belongs to a project
- if owner.is_a?(Project)
- owner
- else
- begin
- owner.association(:project).target
- rescue ActiveRecord::AssociationNotFoundError
- nil
- end
- end
- end
- end
-
- def self.visible_to_user(user)
- return where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank?
- return all if user.admin?
-
- # Check if we are scoped to a specific project's issues
- if owner_project
- if owner_project.team.member?(user, Gitlab::Access::REPORTER)
- # If the project is authorized for the user, they can see all issues in the project
- return all
- else
- # else only non confidential and authored/assigned to them
- return where('issues.confidential IS NULL OR issues.confidential IS FALSE
- OR issues.author_id = :user_id OR issues.assignee_id = :user_id',
- user_id: user.id)
- end
- end
-
- where('
- issues.confidential IS NULL
- OR issues.confidential IS FALSE
- OR (issues.confidential = TRUE
- AND (issues.author_id = :user_id
- OR issues.assignee_id = :user_id
- OR issues.project_id IN(:project_ids)))',
- user_id: user.id,
- project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
- end
-
def self.reference_prefix
'#'
end
diff --git a/app/models/member.rb b/app/models/member.rb
index 3b65587c66b..c585e0b450e 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -57,6 +57,11 @@ class Member < ActiveRecord::Base
scope :owners, -> { active.where(access_level: OWNER) }
scope :owners_and_masters, -> { active.where(access_level: [OWNER, MASTER]) }
+ scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) }
+ scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) }
+ scope :order_recent_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'DESC')) }
+ scope :order_oldest_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'ASC')) }
+
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
after_create :send_invite, if: :invite?, unless: :importing?
@@ -72,6 +77,34 @@ class Member < ActiveRecord::Base
default_value_for :notification_level, NotificationSetting.levels[:global]
class << self
+ def search(query)
+ joins(:user).merge(User.search(query))
+ end
+
+ def sort(method)
+ case method.to_s
+ when 'access_level_asc' then reorder(access_level: :asc)
+ when 'access_level_desc' then reorder(access_level: :desc)
+ when 'recent_sign_in' then order_recent_sign_in
+ when 'oldest_sign_in' then order_oldest_sign_in
+ when 'last_joined' then order_created_desc
+ when 'oldest_joined' then order_created_asc
+ else
+ order_by(method)
+ end
+ end
+
+ def left_join_users
+ users = User.arel_table
+ members = Member.arel_table
+
+ member_users = members.join(users, Arel::Nodes::OuterJoin).
+ on(members[:user_id].eq(users[:id])).
+ join_sources
+
+ joins(member_users)
+ end
+
def access_for_user_ids(user_ids)
where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h
end
@@ -89,8 +122,8 @@ class Member < ActiveRecord::Base
member =
if user.is_a?(User)
source.members.find_by(user_id: user.id) ||
- source.requesters.find_by(user_id: user.id) ||
- source.members.build(user_id: user.id)
+ source.requesters.find_by(user_id: user.id) ||
+ source.members.build(user_id: user.id)
else
source.members.build(invite_email: user)
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index ea3cf1cdaac..5ce20cc43d6 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -97,7 +97,7 @@ class MergeRequest < ActiveRecord::Base
validates :source_branch, presence: true
validates :target_project, presence: true
validates :target_branch, presence: true
- validates :merge_user, presence: true, if: :merge_when_build_succeeds?
+ validates :merge_user, presence: true, if: :merge_when_build_succeeds?, unless: :importing?
validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
validate :validate_fork, unless: :closed_without_fork?
@@ -452,7 +452,7 @@ class MergeRequest < ActiveRecord::Base
should_remove_source_branch? || force_remove_source_branch?
end
- def mr_and_commit_notes
+ def related_notes
# Fetch comments only from last 100 commits
commits_for_notes_limit = 100
commit_ids = commits.last(commits_for_notes_limit).map(&:id)
@@ -468,7 +468,7 @@ class MergeRequest < ActiveRecord::Base
end
def discussions
- @discussions ||= self.mr_and_commit_notes.
+ @discussions ||= self.related_notes.
inc_relations_for_view.
fresh.
discussions
@@ -568,6 +568,19 @@ class MergeRequest < ActiveRecord::Base
end
end
+ def issues_mentioned_but_not_closing(current_user = self.author)
+ return [] unless target_branch == project.default_branch
+
+ ext = Gitlab::ReferenceExtractor.new(project, current_user)
+ ext.analyze(description)
+
+ issues = ext.issues
+ closing_issues = Gitlab::ClosingIssueExtractor.new(project, current_user).
+ closed_by_message(description)
+
+ issues - closing_issues
+ end
+
def target_project_path
if target_project
target_project.path_with_namespace
@@ -612,13 +625,24 @@ class MergeRequest < ActiveRecord::Base
self.target_project.repository.branch_names.include?(self.target_branch)
end
- def merge_commit_message
- message = "Merge branch '#{source_branch}' into '#{target_branch}'\n\n"
- message << "#{title}\n\n"
- message << "#{description}\n\n" if description.present?
+ def merge_commit_message(include_description: false)
+ closes_issues_references = closes_issues.map do |issue|
+ issue.to_reference(target_project)
+ end
+
+ message = [
+ "Merge branch '#{source_branch}' into '#{target_branch}'",
+ title
+ ]
+
+ if !include_description && closes_issues_references.present?
+ message << "Closes #{closes_issues_references.to_sentence}"
+ end
+
+ message << "#{description}" if include_description && description.present?
message << "See merge request #{to_reference}"
- message
+ message.join("\n\n")
end
def reset_merge_when_build_succeeds
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 37374044551..fd42f2328d8 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -17,14 +17,13 @@ class Namespace < ActiveRecord::Base
validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
validates :name,
presence: true,
- uniqueness: true,
+ uniqueness: { scope: :parent_id },
length: { maximum: 255 },
namespace_name: true
validates :description, length: { maximum: 255 }
validates :path,
presence: true,
- uniqueness: { case_sensitive: false },
length: { maximum: 255 },
namespace: true
@@ -162,6 +161,19 @@ class Namespace < ActiveRecord::Base
end
end
+ def full_name
+ @full_name ||=
+ if parent
+ parent.full_name + ' / ' + name
+ else
+ name
+ end
+ end
+
+ def parents
+ @parents ||= parent ? parent.parents + [parent] : []
+ end
+
private
def repository_storage_paths
diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb
index 345041a6ad1..b524ca50ee8 100644
--- a/app/models/network/graph.rb
+++ b/app/models/network/graph.rb
@@ -161,8 +161,8 @@ module Network
def is_overlap?(range, overlap_space)
range.each do |i|
if i != range.first &&
- i != range.last &&
- @commits[i].spaces.include?(overlap_space)
+ i != range.last &&
+ @commits[i].spaces.include?(overlap_space)
return true
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 08bd08743ef..0c1b05dabf2 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -107,23 +107,6 @@ class Note < ActiveRecord::Base
Discussion.for_diff_notes(active_notes).
map { |d| [d.line_code, d] }.to_h
end
-
- # Searches for notes matching the given query.
- #
- # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
- #
- # query - The search query as a String.
- # as_user - Limit results to those viewable by a specific user
- #
- # Returns an ActiveRecord::Relation.
- def search(query, as_user: nil)
- table = arel_table
- pattern = "%#{query}%"
-
- Note.joins('LEFT JOIN issues ON issues.id = noteable_id').
- where(table[:note].matches(pattern)).
- merge(Issue.visible_to_user(as_user))
- end
end
def cross_reference?
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index c4b095e0c04..10a34c42fd8 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -2,6 +2,8 @@ class PersonalAccessToken < ActiveRecord::Base
include TokenAuthenticatable
add_authentication_token_field :token
+ serialize :scopes, Array
+
belongs_to :user
scope :active, -> { where(revoked: false).where("expires_at >= NOW() OR expires_at IS NULL") }
diff --git a/app/models/project.rb b/app/models/project.rb
index 77d740081c6..72d3da64f2d 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,7 +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 :slack_service, dependent: :destroy
+ has_one :mattermost_notification_service, dependent: :destroy
+ has_one :slack_slash_commands_service, dependent: :destroy
+ has_one :slack_notification_service, dependent: :destroy
has_one :buildkite_service, dependent: :destroy
has_one :bamboo_service, dependent: :destroy
has_one :teamcity_service, dependent: :destroy
@@ -106,6 +107,7 @@ class Project < ActiveRecord::Base
has_one :bugzilla_service, dependent: :destroy
has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project
has_one :external_wiki_service, dependent: :destroy
+ has_one :kubernetes_service, dependent: :destroy, inverse_of: :project
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
has_one :forked_from_project, through: :forked_project_link
@@ -531,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
@@ -742,6 +748,14 @@ class Project < ActiveRecord::Base
@ci_service ||= ci_services.reorder(nil).find_by(active: true)
end
+ def deployment_services
+ services.where(category: :deployment)
+ end
+
+ def deployment_service
+ @deployment_service ||= deployment_services.reorder(nil).find_by(active: true)
+ end
+
def jira_tracker?
issues_tracker.to_param == 'jira'
end
@@ -1220,6 +1234,12 @@ class Project < ActiveRecord::Base
end
end
+ def deployment_variables
+ return [] unless deployment_service
+
+ deployment_service.predefined_variables
+ end
+
def append_or_update_attribute(name, value)
old_values = public_send(name.to_s)
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
index 86a06321e21..fe6d7aabb22 100644
--- a/app/models/project_services/buildkite_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -3,7 +3,8 @@ require "addressable/uri"
class BuildkiteService < CiService
ENDPOINT = "https://buildkite.com"
- prop_accessor :project_url, :token, :enable_ssl_verification
+ prop_accessor :project_url, :token
+ boolean_accessor :enable_ssl_verification
validates :project_url, presence: true, url: true, if: :activated?
validates :token, presence: true, if: :activated?
diff --git a/app/models/project_services/slack_service/base_message.rb b/app/models/project_services/chat_message/base_message.rb
index f1182824687..a03605d01fb 100644
--- a/app/models/project_services/slack_service/base_message.rb
+++ b/app/models/project_services/chat_message/base_message.rb
@@ -1,6 +1,6 @@
require 'slack-notifier'
-class SlackService
+module ChatMessage
class BaseMessage
def initialize(params)
raise NotImplementedError
diff --git a/app/models/project_services/slack_service/build_message.rb b/app/models/project_services/chat_message/build_message.rb
index 0fca4267bad..53e35cb21bf 100644
--- a/app/models/project_services/slack_service/build_message.rb
+++ b/app/models/project_services/chat_message/build_message.rb
@@ -1,4 +1,4 @@
-class SlackService
+module ChatMessage
class BuildMessage < BaseMessage
attr_reader :sha
attr_reader :ref_type
diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb
index cd87a79d0c6..14fd64e5332 100644
--- a/app/models/project_services/slack_service/issue_message.rb
+++ b/app/models/project_services/chat_message/issue_message.rb
@@ -1,4 +1,4 @@
-class SlackService
+module ChatMessage
class IssueMessage < BaseMessage
attr_reader :user_name
attr_reader :title
diff --git a/app/models/project_services/slack_service/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb
index b7615c96068..ab5e8b24167 100644
--- a/app/models/project_services/slack_service/merge_message.rb
+++ b/app/models/project_services/chat_message/merge_message.rb
@@ -1,4 +1,4 @@
-class SlackService
+module ChatMessage
class MergeMessage < BaseMessage
attr_reader :user_name
attr_reader :project_name
diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/chat_message/note_message.rb
index 797c5937f09..ca1d7207034 100644
--- a/app/models/project_services/slack_service/note_message.rb
+++ b/app/models/project_services/chat_message/note_message.rb
@@ -1,4 +1,4 @@
-class SlackService
+module ChatMessage
class NoteMessage < BaseMessage
attr_reader :message
attr_reader :user_name
diff --git a/app/models/project_services/slack_service/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb
index f8d03c0e2fa..210027565a8 100644
--- a/app/models/project_services/slack_service/pipeline_message.rb
+++ b/app/models/project_services/chat_message/pipeline_message.rb
@@ -1,4 +1,4 @@
-class SlackService
+module ChatMessage
class PipelineMessage < BaseMessage
attr_reader :ref_type, :ref, :status, :project_name, :project_url,
:user_name, :duration, :pipeline_id
@@ -13,7 +13,7 @@ class SlackService
@project_name = data[:project][:path_with_namespace]
@project_url = data[:project][:web_url]
- @user_name = data[:user] && data[:user][:name]
+ @user_name = (data[:user] && data[:user][:name]) || 'API'
end
def pretext
diff --git a/app/models/project_services/slack_service/push_message.rb b/app/models/project_services/chat_message/push_message.rb
index b26f3e9ddce..2d73b71ec37 100644
--- a/app/models/project_services/slack_service/push_message.rb
+++ b/app/models/project_services/chat_message/push_message.rb
@@ -1,4 +1,4 @@
-class SlackService
+module ChatMessage
class PushMessage < BaseMessage
attr_reader :after
attr_reader :before
diff --git a/app/models/project_services/slack_service/wiki_page_message.rb b/app/models/project_services/chat_message/wiki_page_message.rb
index 160ca3ac115..134083e4504 100644
--- a/app/models/project_services/slack_service/wiki_page_message.rb
+++ b/app/models/project_services/chat_message/wiki_page_message.rb
@@ -1,4 +1,4 @@
-class SlackService
+module ChatMessage
class WikiPageMessage < BaseMessage
attr_reader :user_name
attr_reader :title
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/chat_notification_service.rb
index e1b937817f4..b0556987721 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -1,6 +1,13 @@
-class SlackService < Service
+# Base class for Chat notifications services
+# This class is not meant to be used directly, but only to inherit from.
+class ChatNotificationService < Service
+ include ChatMessage
+
+ default_value_for :category, 'chat'
+
prop_accessor :webhook, :username, :channel
boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines
+
validates :webhook, presence: true, url: true, if: :activated?
def initialize_properties
@@ -14,35 +21,8 @@ class SlackService < Service
end
end
- def title
- 'Slack'
- end
-
- def description
- 'A team communication tool for the 21st century'
- end
-
- def to_param
- 'slack'
- end
-
- def help
- 'This service sends notifications to your Slack channel.<br/>
- To setup this Service you need to create a new <b>"Incoming webhook"</b> in your Slack integration panel,
- and enter the Webhook URL below.'
- end
-
- def fields
- default_fields =
- [
- { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' },
- { type: 'text', name: 'username', placeholder: 'username' },
- { type: 'text', name: 'channel', placeholder: "#general" },
- { type: 'checkbox', name: 'notify_only_broken_builds' },
- { type: 'checkbox', name: 'notify_only_broken_pipelines' },
- ]
-
- default_fields + build_event_channels
+ def can_test?
+ valid?
end
def supported_events
@@ -67,21 +47,16 @@ class SlackService < Service
message = get_message(object_kind, data)
- if message
- opt = {}
-
- event_channel = get_channel_field(object_kind) || channel
+ return false unless message
- opt[:channel] = event_channel if event_channel
- opt[:username] = username if username
+ opt = {}
- notifier = Slack::Notifier.new(webhook, opt)
- notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
+ opt[:channel] = get_channel_field(object_kind).presence || channel || default_channel
+ opt[:username] = username if username
+ notifier = Slack::Notifier.new(webhook, opt)
+ notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
- true
- else
- false
- end
+ true
end
def event_channel_names
@@ -96,6 +71,10 @@ class SlackService < Service
fields.reject { |field| field[:name].end_with?('channel') }
end
+ def default_channel
+ raise NotImplementedError
+ end
+
private
def get_message(object_kind, data)
@@ -124,7 +103,7 @@ class SlackService < Service
def build_event_channels
supported_events.reduce([]) do |channels, event|
- channels << { type: 'text', name: event_channel_name(event), placeholder: "#general" }
+ channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel }
end
end
@@ -166,11 +145,3 @@ class SlackService < Service
end
end
end
-
-require "slack_service/issue_message"
-require "slack_service/push_message"
-require "slack_service/merge_message"
-require "slack_service/note_message"
-require "slack_service/build_message"
-require "slack_service/pipeline_message"
-require "slack_service/wiki_page_message"
diff --git a/app/models/project_services/chat_service.rb b/app/models/project_services/chat_service.rb
deleted file mode 100644
index d36beff5fa6..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 inherrit 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
new file mode 100644
index 00000000000..ab353a1abe6
--- /dev/null
+++ b/app/models/project_services/deployment_service.rb
@@ -0,0 +1,33 @@
+# Base class for deployment services
+#
+# These services integrate with a deployment solution like Kubernetes/OpenShift,
+# Mesosphere, etc, to provide additional features to environments.
+class DeploymentService < Service
+ default_value_for :category, 'deployment'
+
+ def supported_events
+ []
+ end
+
+ 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/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index 5e4dd101c53..adc78a427ee 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -1,5 +1,6 @@
class DroneCiService < CiService
- prop_accessor :drone_url, :token, :enable_ssl_verification
+ prop_accessor :drone_url, :token
+ boolean_accessor :enable_ssl_verification
validates :drone_url, presence: true, url: true, if: :activated?
validates :token, presence: true, if: :activated?
diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb
index e0083c43adb..79285cbd26d 100644
--- a/app/models/project_services/emails_on_push_service.rb
+++ b/app/models/project_services/emails_on_push_service.rb
@@ -1,6 +1,6 @@
class EmailsOnPushService < Service
- prop_accessor :send_from_committer_email
- prop_accessor :disable_diffs
+ boolean_accessor :send_from_committer_email
+ boolean_accessor :disable_diffs
prop_accessor :recipients
validates :recipients, presence: true, if: :activated?
@@ -24,20 +24,20 @@ class EmailsOnPushService < Service
return unless supported_events.include?(push_data[:object_kind])
EmailsOnPushWorker.perform_async(
- project_id,
- recipients,
- push_data,
- send_from_committer_email: send_from_committer_email?,
- disable_diffs: disable_diffs?
+ project_id,
+ recipients,
+ push_data,
+ send_from_committer_email: send_from_committer_email?,
+ disable_diffs: disable_diffs?
)
end
def send_from_committer_email?
- self.send_from_committer_email == "1"
+ Gitlab::Utils.to_boolean(self.send_from_committer_email)
end
def disable_diffs?
- self.disable_diffs == "1"
+ Gitlab::Utils.to_boolean(self.disable_diffs)
end
def fields
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 660a8ae3421..915f6fed74c 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -8,8 +8,8 @@ class HipchatService < Service
ul ol li dl dt dd
]
- prop_accessor :token, :room, :server, :notify, :color, :api_version
- boolean_accessor :notify_only_broken_builds
+ prop_accessor :token, :room, :server, :color, :api_version
+ boolean_accessor :notify_only_broken_builds, :notify
validates :token, presence: true, if: :activated?
def initialize_properties
@@ -75,7 +75,7 @@ class HipchatService < Service
end
def message_options(data = nil)
- { notify: notify.present? && notify == '1', color: message_color(data) }
+ { notify: notify.present? && Gitlab::Utils.to_boolean(notify), color: message_color(data) }
end
def create_message(data)
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index ce7d1c5d5b1..7355918feab 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -2,7 +2,8 @@ require 'uri'
class IrkerService < Service
prop_accessor :server_host, :server_port, :default_irc_uri
- prop_accessor :colorize_messages, :recipients, :channels
+ prop_accessor :recipients, :channels
+ boolean_accessor :colorize_messages
validates :recipients, presence: true, if: :activated?
before_validation :get_channels
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 207bb816ad1..bce2cdd5516 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -85,8 +85,8 @@ class IssueTrackerService < Service
def enabled_in_gitlab_config
Gitlab.config.issues_tracker &&
- Gitlab.config.issues_tracker.values.any? &&
- issues_tracker
+ Gitlab.config.issues_tracker.values.any? &&
+ issues_tracker
end
def issues_tracker
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 894315a8593..2d969d2fcb6 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -220,7 +220,7 @@ class JiraService < IssueTrackerService
entity_title = data[:entity][:title]
project_name = data[:project][:name]
- message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'"
+ message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title.chomp}'"
link_title = "GitLab: Mentioned on #{entity_name} - #{entity_title}"
link_props = build_remote_link_props(url: entity_url, title: link_title)
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
new file mode 100644
index 00000000000..085125ca9dc
--- /dev/null
+++ b/app/models/project_services/kubernetes_service.rb
@@ -0,0 +1,173 @@
+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
+
+ # Access to kubernetes is directly through the API
+ prop_accessor :api_url
+
+ # Bearer authentication
+ # TODO: user/password auth, client certificates
+ prop_accessor :token
+
+ # Provide a custom CA bundle for self-signed deployments
+ prop_accessor :ca_pem
+
+ with_options presence: true, if: :activated? do
+ validates :api_url, url: true
+ validates :token
+
+ validates :namespace,
+ format: {
+ with: Gitlab::Regex.kubernetes_namespace_regex,
+ message: Gitlab::Regex.kubernetes_namespace_regex_message,
+ },
+ length: 1..63
+ end
+
+ after_save :clear_reactive_cache!
+
+ def initialize_properties
+ if properties.nil?
+ self.properties = {}
+ self.namespace = project.path if project.present?
+ end
+ end
+
+ def title
+ 'Kubernetes'
+ end
+
+ def description
+ 'Kubernetes / Openshift integration'
+ end
+
+ def help
+ 'To enable terminal access to Kubernetes environments, label your ' \
+ 'deployments with `app=$CI_ENVIRONMENT_SLUG`'
+ end
+
+ def to_param
+ 'kubernetes'
+ end
+
+ def fields
+ [
+ { type: 'text',
+ name: 'namespace',
+ title: 'Kubernetes namespace',
+ placeholder: 'Kubernetes namespace',
+ },
+ { type: 'text',
+ name: 'api_url',
+ title: 'API URL',
+ placeholder: 'Kubernetes API URL, like https://kube.example.com/',
+ },
+ { type: 'text',
+ name: 'token',
+ title: 'Service token',
+ placeholder: 'Service token',
+ },
+ { type: 'textarea',
+ name: 'ca_pem',
+ title: 'Custom CA bundle',
+ placeholder: 'Certificate Authority bundle (PEM format)',
+ },
+ ]
+ end
+
+ # Check we can connect to the Kubernetes API
+ def test(*args)
+ kubeclient = build_kubeclient!
+
+ kubeclient.discover
+ { success: kubeclient.discovered, result: "Checked API discovery endpoint" }
+ rescue => err
+ { success: false, result: err }
+ end
+
+ def predefined_variables
+ variables = [
+ { key: 'KUBE_URL', value: api_url, public: true },
+ { key: 'KUBE_TOKEN', value: token, public: false },
+ { key: 'KUBE_NAMESPACE', value: namespace, public: true }
+ ]
+ variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } if ca_pem.present?
+ variables
+ end
+
+ # 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
+
+ # 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?
+
+ 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(
+ join_api_url(api_path),
+ api_version,
+ auth_options: kubeclient_auth_options,
+ ssl_options: kubeclient_ssl_options,
+ http_proxy_uri: ENV['http_proxy']
+ )
+ end
+
+ def kubeclient_ssl_options
+ opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER }
+
+ if ca_pem.present?
+ opts[:cert_store] = OpenSSL::X509::Store.new
+ opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem))
+ end
+
+ opts
+ end
+
+ 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_notification_service.rb
new file mode 100644
index 00000000000..de18c4b1f00
--- /dev/null
+++ b/app/models/project_services/mattermost_notification_service.rb
@@ -0,0 +1,41 @@
+class MattermostNotificationService < ChatNotificationService
+ def title
+ 'Mattermost notifications'
+ end
+
+ def description
+ 'Receive event notifications in Mattermost'
+ end
+
+ def to_param
+ 'mattermost_notification'
+ end
+
+ def help
+ 'This service sends notifications about projects events to Mattermost channels.<br />
+ To set up this service:
+ <ol>
+ <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation. </li>
+ <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event. </li>
+ <li>Paste the webhook <strong>URL</strong> into the field bellow. </li>
+ <li>Select events below to enable notifications. The channel and username are optional. </li>
+ </ol>'
+ end
+
+ def fields
+ default_fields + build_event_channels
+ end
+
+ def default_fields
+ [
+ { type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' },
+ { type: 'text', name: 'username', placeholder: 'username' },
+ { type: 'checkbox', name: 'notify_only_broken_builds' },
+ { type: 'checkbox', name: 'notify_only_broken_pipelines' },
+ ]
+ end
+
+ def default_channel
+ "#town-square"
+ end
+end
diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb
index 33431f41dc2..10740275669 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
@@ -18,32 +18,4 @@ class MattermostSlashCommandsService < ChatService
def to_param
'mattermost_slash_commands'
end
-
- def fields
- [
- { type: 'text', name: 'token', placeholder: '' }
- ]
- end
-
- def trigger(params)
- return nil unless valid_token?(params[:token])
-
- user = find_chat_user(params)
- unless user
- url = authorize_chat_name_url(params)
- return Mattermost::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
end
diff --git a/app/models/project_services/slack_notification_service.rb b/app/models/project_services/slack_notification_service.rb
new file mode 100644
index 00000000000..3cbf89efba4
--- /dev/null
+++ b/app/models/project_services/slack_notification_service.rb
@@ -0,0 +1,40 @@
+class SlackNotificationService < ChatNotificationService
+ def title
+ 'Slack notifications'
+ end
+
+ def description
+ 'Receive event notifications in Slack'
+ end
+
+ def to_param
+ 'slack_notification'
+ end
+
+ def help
+ 'This service sends notifications about projects events to Slack channels.<br />
+ To setup this service:
+ <ol>
+ <li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event. </li>
+ <li>Paste the <strong>Webhook URL</strong> into the field below. </li>
+ <li>Select events below to enable notifications. The channel and username are optional. </li>
+ </ol>'
+ end
+
+ def fields
+ default_fields + build_event_channels
+ end
+
+ def default_fields
+ [
+ { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' },
+ { type: 'text', name: 'username', placeholder: 'username' },
+ { type: 'checkbox', name: 'notify_only_broken_builds' },
+ { type: 'checkbox', name: 'notify_only_broken_pipelines' },
+ ]
+ end
+
+ def default_channel
+ "#general"
+ end
+end
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 0c36acfc1b7..8abd8e73e43 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -214,12 +214,15 @@ class Service < ActiveRecord::Base
hipchat
irker
jira
+ kubernetes
mattermost_slash_commands
+ mattermost_notification
pipelines_email
pivotaltracker
pushover
redmine
- slack
+ slack_slash_commands
+ slack_notification
teamcity
]
end
diff --git a/app/models/user.rb b/app/models/user.rb
index b9bb4a9e3f7..3a17c98eff6 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -178,6 +178,8 @@ class User < ActiveRecord::Base
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
+ scope :order_recent_sign_in, -> { reorder(last_sign_in_at: :desc) }
+ scope :order_oldest_sign_in, -> { reorder(last_sign_in_at: :asc) }
def self.with_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
@@ -205,8 +207,8 @@ class User < ActiveRecord::Base
def sort(method)
case method.to_s
- when 'recent_sign_in' then reorder(last_sign_in_at: :desc)
- when 'oldest_sign_in' then reorder(last_sign_in_at: :asc)
+ when 'recent_sign_in' then order_recent_sign_in
+ when 'oldest_sign_in' then order_oldest_sign_in
else
order_by(method)
end
@@ -304,10 +306,6 @@ class User < ActiveRecord::Base
personal_access_token.user if personal_access_token
end
- def by_username_or_id(name_or_id)
- find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i)
- end
-
# Returns a user for the given SSH key.
def find_by_ssh_key_id(key_id)
find_by(id: Key.unscoped.select(:user_id).where(id: key_id))
@@ -394,7 +392,7 @@ class User < ActiveRecord::Base
def namespace_uniq
# Return early if username already failed the first uniqueness validation
return if errors.key?(:username) &&
- errors[:username].include?('has already been taken')
+ errors[:username].include?('has already been taken')
existing_namespace = Namespace.by_path(username)
if existing_namespace && existing_namespace != namespace
diff --git a/app/policies/group_member_policy.rb b/app/policies/group_member_policy.rb
index 62335527654..5a3fe814b77 100644
--- a/app/policies/group_member_policy.rb
+++ b/app/policies/group_member_policy.rb
@@ -15,5 +15,11 @@ class GroupMemberPolicy < BasePolicy
elsif @user == target_user
can! :destroy_group_member
end
+
+ additional_rules!
+ end
+
+ def additional_rules!
+ # This is meant to be overriden in EE
end
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index b65fb68cd88..6f943feb2a7 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -33,6 +33,8 @@ class GroupPolicy < BasePolicy
if globally_viewable && @subject.request_access_enabled && !member
can! :request_access
end
+
+ additional_rules!(master)
end
def can_read_group?
@@ -43,4 +45,8 @@ class GroupPolicy < BasePolicy
GroupProjectsFinder.new(@subject).execute(@user).any?
end
+
+ def additional_rules!(master)
+ # This is meant to be overriden in EE
+ end
end
diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb
index 83847466ee2..5326061bd07 100644
--- a/app/policies/note_policy.rb
+++ b/app/policies/note_policy.rb
@@ -12,7 +12,7 @@ class NotePolicy < BasePolicy
end
if @subject.for_merge_request? &&
- @subject.noteable.author == @user
+ @subject.noteable.author == @user
can! :resolve_note
end
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index d5aadfce76a..b5db9c12622 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -3,7 +3,7 @@ class ProjectPolicy < BasePolicy
team_access!(user)
owner = project.owner == user ||
- (project.group && project.group.has_owner?(user))
+ (project.group && project.group.has_owner?(user))
owner_access! if user.admin? || owner
team_member_owner_access! if owner
@@ -13,7 +13,7 @@ class ProjectPolicy < BasePolicy
public_access!
if project.request_access_enabled &&
- !(owner || user.admin? || project.team.member?(user) || project_group_member?(user))
+ !(owner || user.admin? || project.team.member?(user) || project_group_member?(user))
can! :request_access
end
end
@@ -244,10 +244,10 @@ class ProjectPolicy < BasePolicy
def project_group_member?(user)
project.group &&
- (
- project.group.members.exists?(user_id: user.id) ||
- project.group.requesters.exists?(user_id: user.id)
- )
+ (
+ project.group.members.exists?(user_id: user.id) ||
+ project.group.requesters.exists?(user_id: user.id)
+ )
end
def named_abilities(name)
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/access_token_validation_service.rb b/app/services/access_token_validation_service.rb
new file mode 100644
index 00000000000..ddaaed90e5b
--- /dev/null
+++ b/app/services/access_token_validation_service.rb
@@ -0,0 +1,32 @@
+AccessTokenValidationService = Struct.new(:token) do
+ # Results:
+ VALID = :valid
+ EXPIRED = :expired
+ REVOKED = :revoked
+ INSUFFICIENT_SCOPE = :insufficient_scope
+
+ def validate(scopes: [])
+ if token.expired?
+ return EXPIRED
+
+ elsif token.revoked?
+ return REVOKED
+
+ elsif !self.include_any_scope?(scopes)
+ return INSUFFICIENT_SCOPE
+
+ else
+ return VALID
+ end
+ end
+
+ # True if the token's scope contains any of the passed scopes.
+ def include_any_scope?(scopes)
+ if scopes.blank?
+ true
+ else
+ # Check whether the token is allowed access to any of the required scopes.
+ Set.new(scopes).intersection(Set.new(token.scopes)).present?
+ end
+ end
+end
diff --git a/app/services/ci/create_pipeline_builds_service.rb b/app/services/ci/create_pipeline_builds_service.rb
index 005014fa1de..b7da3f8e7eb 100644
--- a/app/services/ci/create_pipeline_builds_service.rb
+++ b/app/services/ci/create_pipeline_builds_service.rb
@@ -10,18 +10,29 @@ module Ci
end
end
+ def project
+ pipeline.project
+ end
+
private
def create_build(build_attributes)
build_attributes = build_attributes.merge(
pipeline: pipeline,
- project: pipeline.project,
+ project: project,
ref: pipeline.ref,
tag: pipeline.tag,
user: current_user,
trigger_request: trigger_request
)
- pipeline.builds.create(build_attributes)
+ build = pipeline.builds.create(build_attributes)
+
+ # Create the environment before the build starts. This sets its slug and
+ # makes it available as an environment variable
+ project.environments.find_or_create_by(name: build.expanded_environment_name) if
+ build.has_environment?
+
+ build
end
def new_builds
diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb
index 75d847d5bee..240ddabec36 100644
--- a/app/services/ci/image_for_build_service.rb
+++ b/app/services/ci/image_for_build_service.rb
@@ -1,13 +1,13 @@
module Ci
class ImageForBuildService
def execute(project, opts)
- sha = opts[:sha] || ref_sha(project, opts[:ref])
-
+ ref = opts[:ref]
+ sha = opts[:sha] || ref_sha(project, ref)
pipelines = project.pipelines.where(sha: sha)
- pipelines = pipelines.where(ref: opts[:ref]) if opts[:ref]
- image_name = image_for_status(pipelines.status)
+ image_name = image_for_status(pipelines.latest_status(ref))
image_path = Rails.root.join('public/ci', image_name)
+
OpenStruct.new(path: image_path, name: image_name)
end
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index db5f2bf9b2e..4d410f66c55 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -35,7 +35,7 @@ module Commits
success
else
error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically.
- It may have already been #{action.to_s.dasherize}, or a more recent commit may have updated some of its content."
+ A #{action.to_s.dasherize} may have already been performed with this #{@commit.change_type_title(current_user)}, or a more recent commit may have updated some of its content."
raise ChangeError, error_msg
end
end
diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb
index 99ad12b1003..fff2273f402 100644
--- a/app/services/groups/update_service.rb
+++ b/app/services/groups/update_service.rb
@@ -5,7 +5,7 @@ module Groups
new_visibility = params[:visibility_level]
if new_visibility && new_visibility.to_i != group.visibility_level
unless can?(current_user, :change_visibility_level, group) &&
- Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
+ Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(group, new_visibility)
return group
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index b5f63cc5a1a..ab3d2a9a0cd 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -184,7 +184,8 @@ class IssuableBaseService < BaseService
old_labels = issuable.labels.to_a
old_mentioned_users = issuable.mentioned_users.to_a
- params[:label_ids] = process_label_ids(params, existing_label_ids: issuable.label_ids)
+ label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
+ params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids)
if params.present? && update_issuable(issuable, params)
# We do not touch as it will affect a update on updated_at field
@@ -201,6 +202,10 @@ class IssuableBaseService < BaseService
issuable
end
+ def labels_changing?(old_label_ids, new_label_ids)
+ old_label_ids.sort != new_label_ids.sort
+ end
+
def change_state(issuable)
case params.delete(:state_event)
when 'reopen'
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index a2111b3806b..78cbf94ec69 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -10,7 +10,7 @@ module Issues
end
if issue.previous_changes.include?('title') ||
- issue.previous_changes.include?('description')
+ issue.previous_changes.include?('description')
todo_service.update_issue(issue, current_user)
end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index bebfca7537b..6a7393a9921 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -42,7 +42,7 @@ module MergeRequests
end
if merge_request.source_project == merge_request.target_project &&
- merge_request.target_branch == merge_request.source_branch
+ merge_request.target_branch == merge_request.source_branch
messages << 'You must select different branches'
end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index fda0da19d87..ad16ef8c70f 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -25,7 +25,7 @@ module MergeRequests
end
if merge_request.previous_changes.include?('title') ||
- merge_request.previous_changes.include?('description')
+ merge_request.previous_changes.include?('description')
todo_service.update_merge_request(merge_request, current_user)
end
diff --git a/app/services/oauth2/access_token_validation_service.rb b/app/services/oauth2/access_token_validation_service.rb
deleted file mode 100644
index 264fdccde8f..00000000000
--- a/app/services/oauth2/access_token_validation_service.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-module Oauth2::AccessTokenValidationService
- # Results:
- VALID = :valid
- EXPIRED = :expired
- REVOKED = :revoked
- INSUFFICIENT_SCOPE = :insufficient_scope
-
- class << self
- def validate(token, scopes: [])
- if token.expired?
- return EXPIRED
-
- elsif token.revoked?
- return REVOKED
-
- elsif !self.sufficient_scope?(token, scopes)
- return INSUFFICIENT_SCOPE
-
- else
- return VALID
- end
- end
-
- protected
-
- # True if the token's scope is a superset of required scopes,
- # or the required scopes is empty.
- def sufficient_scope?(token, scopes)
- if scopes.blank?
- # if no any scopes required, the scopes of token is sufficient.
- return true
- else
- # If there are scopes required, then check whether
- # the set of authorized scopes is a superset of the set of required scopes
- required_scopes = Set.new(scopes)
- authorized_scopes = Set.new(token.scopes)
-
- return authorized_scopes >= required_scopes
- 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/projects/update_service.rb b/app/services/projects/update_service.rb
index 921ca6748d3..8a6af8d8ada 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -6,7 +6,7 @@ module Projects
if new_visibility && new_visibility.to_i != project.visibility_level
unless can?(current_user, :change_visibility_level, project) &&
- Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
+ Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(project, new_visibility)
return project
diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb
index b6c52ddac7a..86f317dcd18 100644
--- a/app/uploaders/artifact_uploader.rb
+++ b/app/uploaders/artifact_uploader.rb
@@ -1,4 +1,4 @@
-class ArtifactUploader < CarrierWave::Uploader::Base
+class ArtifactUploader < GitlabUploader
storage :file
attr_accessor :build, :field
@@ -38,12 +38,4 @@ class ArtifactUploader < CarrierWave::Uploader::Base
def exists?
file.try(:exists?)
end
-
- def move_to_cache
- true
- end
-
- def move_to_store
- true
- end
end
diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb
index fb3b5dfecd0..cfcb877cc3e 100644
--- a/app/uploaders/attachment_uploader.rb
+++ b/app/uploaders/attachment_uploader.rb
@@ -1,4 +1,4 @@
-class AttachmentUploader < CarrierWave::Uploader::Base
+class AttachmentUploader < GitlabUploader
include UploaderHelper
storage :file
diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb
index 38683fdf6d7..a1ecb7bc00b 100644
--- a/app/uploaders/avatar_uploader.rb
+++ b/app/uploaders/avatar_uploader.rb
@@ -1,4 +1,4 @@
-class AvatarUploader < CarrierWave::Uploader::Base
+class AvatarUploader < GitlabUploader
include UploaderHelper
storage :file
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 3ac6030c21c..47bef7cd1e4 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -1,4 +1,4 @@
-class FileUploader < CarrierWave::Uploader::Base
+class FileUploader < GitlabUploader
include UploaderHelper
MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}
diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb
new file mode 100644
index 00000000000..02d7c601d6c
--- /dev/null
+++ b/app/uploaders/gitlab_uploader.rb
@@ -0,0 +1,11 @@
+class GitlabUploader < CarrierWave::Uploader::Base
+ # Reduce disk IO
+ def move_to_cache
+ true
+ end
+
+ # Reduce disk IO
+ def move_to_store
+ true
+ end
+end
diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb
index 4f356dd663e..faab539b8e0 100644
--- a/app/uploaders/lfs_object_uploader.rb
+++ b/app/uploaders/lfs_object_uploader.rb
@@ -1,4 +1,4 @@
-class LfsObjectUploader < CarrierWave::Uploader::Base
+class LfsObjectUploader < GitlabUploader
storage :file
def store_dir
@@ -9,14 +9,6 @@ class LfsObjectUploader < CarrierWave::Uploader::Base
"#{Gitlab.config.lfs.storage_path}/tmp/cache"
end
- def move_to_cache
- true
- end
-
- def move_to_store
- true
- end
-
def exists?
file.try(:exists?)
end
diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml
index 4aacbb8cd77..c689b26d6e6 100644
--- a/app/views/admin/applications/_form.html.haml
+++ b/app/views/admin/applications/_form.html.haml
@@ -18,6 +18,12 @@
Use
%code= Doorkeeper.configuration.native_redirect_uri
for local tests
+
+ .form-group
+ = f.label :scopes, class: 'col-sm-2 control-label'
+ .col-sm-10
+ = render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes
+
.form-actions
= f.submit 'Submit', class: "btn btn-save wide"
= link_to "Cancel", admin_applications_path, class: "btn btn-default"
diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml
index 3eb9d61972b..14683cc66e9 100644
--- a/app/views/admin/applications/show.html.haml
+++ b/app/views/admin/applications/show.html.haml
@@ -2,8 +2,7 @@
%h3.page-title
Application: #{@application.name}
-
-.table-holder
+.table-holder.oauth-application-show
%table.table
%tr
%td
@@ -23,6 +22,9 @@
- @application.redirect_uri.split.each do |uri|
%div
%span.monospace= uri
+
+ = render "shared/tokens/scopes_list", token: @application
+
.form-actions
= link_to 'Edit', edit_admin_application_path(@application), class: 'btn btn-primary wide pull-left'
= render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10'
diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml
index ec40391a3e3..b5f96363230 100644
--- a/app/views/admin/dashboard/_head.html.haml
+++ b/app/views/admin/dashboard/_head.html.haml
@@ -8,7 +8,7 @@
%span
Overview
= nav_link(controller: [:admin, :projects]) do
- = link_to admin_namespaces_projects_path, title: 'Projects' do
+ = link_to admin_projects_path, title: 'Projects' do
%span
Projects
= nav_link(controller: :users) do
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index e51f4ac1d93..5238623e936 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -116,7 +116,7 @@
.light-well.well-centered
%h4 Projects
.data
- = link_to admin_namespaces_projects_path do
+ = link_to admin_projects_path do
%h1= number_with_delimiter(Project.cached_count)
%hr
= link_to('New Project', new_project_path, class: "btn btn-new")
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index 664bb417c6a..cf28f92853e 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -2,7 +2,7 @@
%li.group-row{ class: css_class }
.controls
- = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn'
+ = link_to 'Edit', admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'btn'
= link_to 'Delete', [:admin, group], data: { confirm: "Are you sure you want to remove #{group.name}?" }, method: :delete, class: 'btn btn-remove'
.stats
%span
@@ -20,7 +20,7 @@
= image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title
= link_to [:admin, group], class: 'group-name' do
- = group.name
+ = group.full_name
- if group.description.present?
.description
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 40871e32913..7b0175af214 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -1,8 +1,8 @@
- page_title @group.name, "Groups"
%h3.page-title
- Group: #{@group.name}
+ Group: #{@group.full_name}
- = link_to edit_admin_group_path(@group), class: "btn pull-right" do
+ = link_to admin_group_edit_path(@group), class: "btn pull-right" do
%i.fa.fa-pencil-square-o
Edit
%hr
@@ -88,7 +88,7 @@
Read more about project permissions
%strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
- = form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
+ = form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
%div
= users_select_tag(:user_ids, multiple: true, email_user: true, scope: :all)
%div.prepend-top-10
diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml
index 824edd171f3..0a954c20fcd 100644
--- a/app/views/admin/logs/show.html.haml
+++ b/app/views/admin/logs/show.html.haml
@@ -8,7 +8,7 @@
%div{ class: container_class }
%ul.nav-links.log-tabs
- loggers.each do |klass|
- %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }
+ %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }>
= link_to klass::file_name, "##{klass::file_name_noext}",
'data-toggle' => 'tab'
.row-content-block
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index b37b8d4fee7..8bc7dc7dd51 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -7,7 +7,7 @@
%div{ class: container_class }
.top-area
.prepend-top-default
- = form_tag admin_namespaces_projects_path, method: :get do |f|
+ = form_tag admin_projects_path, method: :get do |f|
.search-holder
.search-field-holder
= search_field_tag :name, params[:name], class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false, placeholder: 'Search by name'
@@ -41,19 +41,19 @@
= button_tag "Search", class: "btn btn-primary btn-search"
%ul.nav-links
- - opts = params[:visibility_level].present? ? {} : { page: admin_namespaces_projects_path }
+ - opts = params[:visibility_level].present? ? {} : { page: admin_projects_path }
= nav_link(opts) do
- = link_to admin_namespaces_projects_path do
+ = link_to admin_projects_path do
All
= nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s ? 'active' : '' }) do
- = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do
+ = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do
Private
= nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s ? 'active' : '' }) do
- = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do
+ = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do
Internal
= nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s ? 'active' : '' }) do
- = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do
+ = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do
Public
.nav-controls
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 73038164056..ca503e35623 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -91,7 +91,7 @@
%strong ##{build.id}
%td.status
- = ci_status_with_icon(build.status)
+ = render 'ci/status/badge', status: build.detailed_status(current_user)
%td.status
- if project
diff --git a/app/views/ci/status/_badge.html.haml b/app/views/ci/status/_badge.html.haml
new file mode 100644
index 00000000000..601fb7f0f3f
--- /dev/null
+++ b/app/views/ci/status/_badge.html.haml
@@ -0,0 +1,11 @@
+- status = local_assigns.fetch(:status)
+- css_classes = "ci-status ci-#{status.group}"
+
+- if status.has_details?
+ = link_to status.details_path, class: css_classes do
+ = custom_icon(status.icon)
+ = status.text
+- else
+ %span{ class: css_classes }
+ = custom_icon(status.icon)
+ = status.text
diff --git a/app/views/ci/status/_graph_badge.html.haml b/app/views/ci/status/_graph_badge.html.haml
new file mode 100644
index 00000000000..52b4d77d074
--- /dev/null
+++ b/app/views/ci/status/_graph_badge.html.haml
@@ -0,0 +1,20 @@
+-# Renders the graph node with both the status icon, status name and action icon
+
+- subject = local_assigns.fetch(:subject)
+- status = subject.detailed_status(current_user)
+- klass = "ci-status-icon ci-status-icon-#{status.group}"
+
+- if status.has_details?
+ = link_to status.details_path, class: 'build-content' do
+ %span{ class: klass }= custom_icon(status.icon)
+ .ci-status-text{ 'data-toggle' => 'tooltip', 'data-title' => "#{subject.name} - #{status.label}" }= subject.name
+- else
+ .build-content
+ %span{ class: klass }= custom_icon(status.icon)
+ .ci-status-text{ 'data-toggle' => 'tooltip', 'data-title' => "#{subject.name} - #{status.label}" }= 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
+ %i.ci-action-icon-wrapper
+ = icon(status.action_icon, class: status.action_class)
diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml
index b78e70ebc1e..02b94beee92 100644
--- a/app/views/dashboard/_activity_head.html.haml
+++ b/app/views/dashboard/_activity_head.html.haml
@@ -1,7 +1,7 @@
%ul.nav-links
- %li{ class: ("active" unless params[:filter]) }
+ %li{ class: ("active" unless params[:filter]) }>
= link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your Projects
- %li{ class: ("active" if params[:filter] == 'starred') }
+ %li{ class: ("active" if params[:filter] == 'starred') }>
= link_to activity_dashboard_path(filter: 'starred'), data: {placement: 'right'} do
Starred Projects
diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml
index b25e8ea1f0c..02e90bbfa55 100644
--- a/app/views/dashboard/_snippets_head.html.haml
+++ b/app/views/dashboard/_snippets_head.html.haml
@@ -1,7 +1,13 @@
-%ul.nav-links
- = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do
- = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
- Your Snippets
- = nav_link(page: explore_snippets_path) do
- = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do
- Explore Snippets
+.top-area
+ %ul.nav-links
+ = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do
+ = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
+ Your Snippets
+ = nav_link(page: explore_snippets_path) do
+ = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do
+ Explore Snippets
+
+ - if current_user
+ .nav-controls.hidden-xs
+ = link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do
+ New snippet
diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml
index b2af438ea57..85cbe0bf0e6 100644
--- a/app/views/dashboard/snippets/index.html.haml
+++ b/app/views/dashboard/snippets/index.html.haml
@@ -2,41 +2,11 @@
- header_title "Snippets", dashboard_snippets_path
= render 'dashboard/snippets_head'
+= render partial: 'snippets/snippets_scope_menu', locals: { include_private: true }
-.nav-block
- .controls.hidden-xs
- = link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do
- = icon('plus')
- New snippet
+.visible-xs
+ &nbsp;
+ = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do
+ New snippet
- .nav-links.snippet-scope-menu
- %li{ class: ("active" unless params[:scope]) }
- = link_to dashboard_snippets_path do
- All
- %span.badge
- = current_user.snippets.count
-
- %li{ class: ("active" if params[:scope] == "are_private") }
- = link_to dashboard_snippets_path(scope: 'are_private') do
- Private
- %span.badge
- = current_user.snippets.are_private.count
-
- %li{ class: ("active" if params[:scope] == "are_internal") }
- = link_to dashboard_snippets_path(scope: 'are_internal') do
- Internal
- %span.badge
- = current_user.snippets.are_internal.count
-
- %li{ class: ("active" if params[:scope] == "are_public") }
- = link_to dashboard_snippets_path(scope: 'are_public') do
- Public
- %span.badge
- = current_user.snippets.are_public.count
-
- .visible-xs
- = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do
- = icon('plus')
- New snippet
-
-= render 'snippets/snippets'
+= render partial: 'snippets/snippets', locals: { link_project: true }
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 62f52086be4..e13f404fee2 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -5,14 +5,14 @@
.top-area
%ul.nav-links
- todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending')
- %li{class: "todos-pending #{todo_pending_active}"}
+ %li{class: "todos-pending #{todo_pending_active}"}>
= link_to todos_filter_path(state: 'pending') do
%span
To do
%span.badge
= number_with_delimiter(todos_pending_count)
- todo_done_active = ('active' if params[:state] == 'done')
- %li{class: "todos-done #{todo_done_active}"}
+ %li{class: "todos-done #{todo_done_active}"}>
= link_to todos_filter_path(state: 'done') do
%span
Done
@@ -32,7 +32,7 @@
- if params[:project_id].present?
= hidden_field_tag(:project_id, params[:project_id])
= dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
- placeholder: 'Search projects', data: { data: todo_projects_options } })
+ placeholder: 'Search projects', data: { data: todo_projects_options, default_label: 'Project' } })
.filter-item.inline
- if params[:author_id].present?
= hidden_field_tag(:author_id, params[:author_id])
@@ -42,12 +42,12 @@
- if params[:type].present?
= hidden_field_tag(:type, params[:type])
= dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit',
- data: { data: todo_types_options } })
+ data: { data: todo_types_options, default_label: 'Type' } })
.filter-item.inline.actions-filter
- if params[:action_id].present?
= hidden_field_tag(:action_id, params[:action_id])
= dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit',
- data: { data: todo_actions_options }})
+ data: { data: todo_actions_options, default_label: 'Action' } })
.pull-right
.dropdown.inline.prepend-left-10
%button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'}
diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml
index 5c98265727a..b3313c7c985 100644
--- a/app/views/doorkeeper/applications/_form.html.haml
+++ b/app/views/doorkeeper/applications/_form.html.haml
@@ -17,5 +17,9 @@
%code= Doorkeeper.configuration.native_redirect_uri
for local tests
+ .form-group
+ = f.label :scopes, class: 'label-light'
+ = render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes
+
.prepend-top-default
= f.submit 'Save application', class: "btn btn-create"
diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml
index 47442b78d48..559de63d96d 100644
--- a/app/views/doorkeeper/applications/show.html.haml
+++ b/app/views/doorkeeper/applications/show.html.haml
@@ -2,7 +2,7 @@
%h3.page-title
Application: #{@application.name}
-.table-holder
+.table-holder.oauth-application-show
%table.table
%tr
%td
@@ -22,6 +22,9 @@
- @application.redirect_uri.split.each do |uri|
%div
%span.monospace= uri
+
+ = render "shared/tokens/scopes_list", token: @application
+
.form-actions
= link_to 'Edit', edit_oauth_application_path(@application), class: 'btn btn-primary wide pull-left'
= render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10'
diff --git a/app/views/explore/_head.html.haml b/app/views/explore/_head.html.haml
index d8a57560788..a3b0709e261 100644
--- a/app/views/explore/_head.html.haml
+++ b/app/views/explore/_head.html.haml
@@ -1,5 +1,5 @@
-.explore-title
- %h3
+.explore-title.text-center
+ %h2
Explore GitLab
%p.lead
Discover projects, groups and snippets. Share your projects with others
diff --git a/app/views/explore/snippets/index.html.haml b/app/views/explore/snippets/index.html.haml
index 7def9eacdc9..e5706d04736 100644
--- a/app/views/explore/snippets/index.html.haml
+++ b/app/views/explore/snippets/index.html.haml
@@ -6,12 +6,4 @@
- else
= render 'explore/head'
-.row-content-block
- - if current_user
- = link_to new_snippet_path, class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do
- New snippet
-
- .oneline
- Public snippets created by you and other users are listed here
-
-= render 'snippets/snippets'
+= render partial: 'snippets/snippets', locals: { link_project: true }
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index ebf9aca7700..bc5d3c797ac 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -21,6 +21,7 @@
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
%button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
= icon("search")
+ = render 'shared/members/sort_dropdown'
.panel.panel-default
.panel-heading
Users with access to
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 324a116a50e..b4aa4f24d9e 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -6,13 +6,13 @@
- if group_issues(@group).exists?
.top-area
= render 'shared/issuable/nav', type: :issues
- .nav-controls
- - if current_user
+ - if current_user
+ .nav-controls
= link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do
= icon('rss')
%span.icon-label
Subscribe
- = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
+ = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
= render 'shared/issuable/filter', type: :issues
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index e6953d94531..dbbdb583a24 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -2,8 +2,9 @@
.top-area
= render 'shared/issuable/nav', type: :merge_requests
- .nav-controls
- = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
+ - if current_user
+ .nav-controls
+ = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
= render 'shared/issuable/filter', type: :merge_requests
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/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index f8b4b107513..ac09b71ae89 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -1,5 +1,6 @@
-- page_title "Bitbucket import"
-- header_title "Projects", root_path
+- page_title 'Bitbucket import'
+- header_title 'Projects', root_path
+
%h3.page-title
%i.fa.fa-bitbucket
Import projects from Bitbucket
@@ -10,13 +11,13 @@
%hr
%p
- if @incompatible_repos.any?
- = button_tag class: "btn btn-import btn-success js-import-all" do
+ = button_tag class: 'btn btn-import btn-success js-import-all' do
Import all compatible projects
- = icon("spinner spin", class: "loading-icon")
+ = icon('spinner spin', class: 'loading-icon')
- else
- = button_tag class: "btn btn-success js-import-all" do
+ = button_tag class: 'btn btn-import btn-success js-import-all' do
Import all projects
- = icon("spinner spin", class: "loading-icon")
+ = icon('spinner spin', class: 'loading-icon')
.table-responsive
%table.table.import-jobs
@@ -32,7 +33,7 @@
- @already_added_projects.each do |project|
%tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
%td
- = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: "_blank"
+ = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: '_blank'
%td
= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
@@ -47,31 +48,41 @@
= project.human_import_status_name
- @repos.each do |repo|
- %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"}
+ %tr{id: "repo_#{repo.owner}___#{repo.slug}"}
%td
- = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank"
+ = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: "_blank"
%td.import-target
- = import_project_target(repo['owner'], repo['slug'])
+ %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, path: repo.owner) } : {}
+ = 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
+ = button_tag class: 'btn btn-import js-add-to-import' do
Import
- = icon("spinner spin", class: "loading-icon")
+ = icon('spinner spin', class: 'loading-icon')
- @incompatible_repos.each do |repo|
- %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"}
+ %tr{id: "repo_#{repo.owner}___#{repo.slug}"}
%td
- = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank"
+ = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank'
%td.import-target
%td.import-actions-job-status
- = label_tag "Incompatible Project", nil, class: "label label-danger"
+ = label_tag 'Incompatible Project', nil, class: 'label label-danger'
- if @incompatible_repos.any?
%p
One or more of your Bitbucket projects cannot be imported into GitLab
directly because they use Subversion or Mercurial for version control,
rather than Git. Please convert
- = link_to "them to Git,", "https://www.atlassian.com/git/tutorials/migrating-overview"
+ = link_to 'them to Git,', 'https://www.atlassian.com/git/tutorials/migrating-overview'
and go through the
- = link_to "import flow", status_import_bitbucket_path, "data-no-turbolink" => "true"
+ = link_to 'import flow', status_import_bitbucket_path, 'data-no-turbolink' => 'true'
again.
.js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_path}", import_path: "#{import_bitbucket_path}" } }
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/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml
index e138ebab018..3daa1e90a8c 100644
--- a/app/views/layouts/_init_auto_complete.html.haml
+++ b/app/views/layouts/_init_auto_complete.html.haml
@@ -3,6 +3,14 @@
- if project
:javascript
- GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: noteable_type, type_id: params[:id])}"
- GitLab.GfmAutoComplete.cachedData = undefined;
- GitLab.GfmAutoComplete.setup();
+ gl.GfmAutoComplete.dataSources = {
+ emojis: "#{emojis_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+ members: "#{members_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}",
+ issues: "#{issues_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+ mergeRequests: "#{merge_requests_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+ labels: "#{labels_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+ milestones: "#{milestones_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+ commands: "#{commands_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}"
+ };
+
+ gl.GfmAutoComplete.setup();
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index a9a0b149049..54d02ee8e4b 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -22,9 +22,10 @@
= render "layouts/nav/#{nav}"
.content-wrapper{ class: "#{layout_nav_class}" }
= yield :sub_nav
- = render "layouts/broadcast"
- = render "layouts/flash"
- = yield :flash_message
+ .alert-wrapper
+ = render "layouts/broadcast"
+ = render "layouts/flash"
+ = yield :flash_message
%div{ class: "#{(container_class unless @no_container)} #{@content_class}" }
.content{ id: "content-body" }
= yield
diff --git a/app/views/profiles/personal_access_tokens/_form.html.haml b/app/views/profiles/personal_access_tokens/_form.html.haml
new file mode 100644
index 00000000000..3f6efa33953
--- /dev/null
+++ b/app/views/profiles/personal_access_tokens/_form.html.haml
@@ -0,0 +1,21 @@
+- personal_access_token = local_assigns.fetch(:personal_access_token)
+- scopes = local_assigns.fetch(:scopes)
+
+= form_for [:profile, personal_access_token], method: :post, html: { class: 'js-requires-input' } do |f|
+
+ = form_errors(personal_access_token)
+
+ .form-group
+ = f.label :name, class: 'label-light'
+ = f.text_field :name, class: "form-control", required: true
+
+ .form-group
+ = f.label :expires_at, class: 'label-light'
+ = f.text_field :expires_at, class: "datepicker form-control"
+
+ .form-group
+ = f.label :scopes, class: 'label-light'
+ = render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: personal_access_token, scopes: scopes
+
+ .prepend-top-default
+ = f.submit 'Create Personal Access Token', class: "btn btn-create"
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index 05a2ea67aa2..bb4effeeeb1 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -28,21 +28,8 @@
Add a Personal Access Token
%p.profile-settings-content
Pick a name for the application, and we'll give you a unique token.
- = form_for [:profile, @personal_access_token],
- method: :post, html: { class: 'js-requires-input' } do |f|
- = form_errors(@personal_access_token)
-
- .form-group
- = f.label :name, class: 'label-light'
- = f.text_field :name, class: "form-control", required: true
-
- .form-group
- = f.label :expires_at, class: 'label-light'
- = f.text_field :expires_at, class: "datepicker form-control", required: false
-
- .prepend-top-default
- = f.submit 'Create Personal Access Token', class: "btn btn-create"
+ = render "form", personal_access_token: @personal_access_token, scopes: @scopes
%hr
@@ -56,6 +43,7 @@
%th Name
%th Created
%th Expires
+ %th Scopes
%th
%tbody
- @active_personal_access_tokens.each do |token|
@@ -67,6 +55,7 @@
= token.expires_at.to_date.to_s(:medium)
- else
%span.personal-access-tokens-never-expires-label Never
+ %td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>"
%td= link_to "Revoke", revoke_profile_personal_access_token_path(token), method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this token? This action cannot be undone." }
- else
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 03ac739ade5..558a1d56151 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -30,7 +30,7 @@
To add the entry manually, provide the following details to the application on your phone.
%p.prepend-top-0.append-bottom-0
Account:
- = current_user.email
+ = @account_string
%p.prepend-top-0.append-bottom-0
Key:
= current_user.otp_secret.scan(/.{4}/).join(' ')
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 5a04c3318cf..0d1f2b70018 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -18,11 +18,19 @@
= link_to project_path(forked_from_project) do
= forked_from_project.namespace.try(:name)
- .project-repo-buttons.project-action-buttons
+ .project-repo-buttons
.count-buttons
= render 'projects/buttons/star'
= render 'projects/buttons/fork'
- - if @project.feature_available?(:repository, current_user)
- .project-clone-holder
- = render "shared/clone_panel"
+ %span.hidden-xs
+ - if @project.feature_available?(:repository, current_user)
+ .project-clone-holder
+ = render "shared/clone_panel"
+
+ - if current_user && can?(current_user, :download_code, @project)
+ = render 'projects/buttons/download', project: @project, ref: @ref
+ = render 'projects/buttons/dropdown'
+ = render 'shared/notifications/button', notification_setting: @notification_setting
+ = render 'projects/buttons/koding'
+ = render 'shared/members/access_request_buttons', source: @project
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 4a6aa92e3f3..1d058daa094 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -21,6 +21,8 @@
= dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.hidden
= dropdown_tag("Choose a GitLab CI Yaml template", options: { toggle_class: 'btn js-gitlab-ci-yml-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
+ .dockerfile-selector.js-dockerfile-selector-wrap.hidden
+ = dropdown_tag("Choose a Dockerfile template", options: { toggle_class: 'btn js-dockerfile-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } )
= button_tag class: 'soft-wrap-toggle btn', type: 'button' do
%span.no-wrap
= custom_icon('icon_no_wrap')
diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml
index f6aa20c4579..057a720a54a 100644
--- a/app/views/projects/builds/_header.html.haml
+++ b/app/views/projects/builds/_header.html.haml
@@ -1,6 +1,6 @@
.content-block.build-header
.header-content
- = ci_status_with_icon(@build.status)
+ = render 'ci/status/badge', status: @build.detailed_status(current_user)
Build
%strong ##{@build.id}
in pipeline
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index 108674dbba6..cdeb81372ee 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -46,8 +46,7 @@
- else
This build is creating a deployment to #{environment_link_for_build(@build.project, @build)}
- if environment.try(:last_deployment)
- and will overwrite the
- = link_to 'latest deployment', deployment_link(environment.last_deployment)
+ and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')}
.prepend-top-default
- if @build.erased?
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 40bfa01a45a..324a7f8cd3f 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -1,5 +1,5 @@
- if !project.empty_repo? && can?(current_user, :download_code, project)
- .dropdown.inline.download-button
+ .project-action-button.dropdown.inline
%button.btn{ 'data-toggle' => 'dropdown' }
= icon('download')
= icon("caret-down")
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index d3ccebbe290..f5659be25f0 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -1,5 +1,5 @@
- if current_user
- .dropdown.inline
+ .project-action-button.dropdown.inline
%a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"}
= icon('plus')
= icon("caret-down")
diff --git a/app/views/projects/buttons/_koding.html.haml b/app/views/projects/buttons/_koding.html.haml
index fdc80d44253..5d9a776da89 100644
--- a/app/views/projects/buttons/_koding.html.haml
+++ b/app/views/projects/buttons/_koding.html.haml
@@ -1,7 +1,3 @@
-- if koding_enabled? && current_user && can_push_branch?(@project, @project.default_branch)
- - if @repository.koding_yml
- = link_to koding_project_url(@project), class: 'btn', target: '_blank' do
- Run in IDE (Koding)
- - else
- = link_to add_koding_stack_path(@project), class: 'btn' do
- Set Up Koding
+- if koding_enabled? && current_user && @repository.koding_yml && can_push_branch?(@project, @project.default_branch)
+ = link_to koding_project_url(@project), class: 'btn project-action-button inline', target: '_blank' do
+ Run in IDE (Koding)
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 18b3b04154f..f1cb0201032 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -9,10 +9,7 @@
%tr.build.commit{class: ('retried' if retried)}
%td.status
- - if can?(current_user, :read_build, build)
- = ci_status_with_icon(build.status, namespace_project_build_url(build.project.namespace, build.project, build))
- - else
- = ci_status_with_icon(build.status)
+ = render "ci/status/badge", status: build.detailed_status(current_user)
%td.branch-commit
- if can?(current_user, :read_build, build)
diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml
deleted file mode 100644
index 423a1282eb2..00000000000
--- a/app/views/projects/ci/builds/_build_pipeline.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-- is_playable = subject.playable? && can?(current_user, :update_build, @project)
-- if is_playable
- = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.pipeline-graph', placement: 'bottom' } do
- = ci_icon_for_status('play')
- .ci-status-text= subject.name
-- elsif can?(current_user, :read_build, @project)
- = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } do
- %span{class: "ci-status-icon ci-status-icon-#{subject.status}"}
- = ci_icon_for_status(subject.status)
- .ci-status-text= subject.name
-- else
- %span{class: "ci-status-icon ci-status-icon-#{subject.status}"}
- = ci_icon_for_status(subject.status)
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index b58dceb58c9..3f05a21990f 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -1,13 +1,10 @@
- status = pipeline.status
-- detailed_status = pipeline.detailed_status
- show_commit = local_assigns.fetch(:show_commit, true)
- show_branch = local_assigns.fetch(:show_branch, true)
%tr.commit
%td.commit-link
- = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "ci-status ci-#{detailed_status}" do
- = ci_icon_for_status(detailed_status)
- = ci_text_for_status(detailed_status)
+ = render 'ci/status/badge', status: pipeline.detailed_status(current_user)
%td
= link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index f6e3d5e76f5..782f558e8b0 100644
--- a/app/views/projects/commit/_change.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -13,7 +13,7 @@
%a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title== #{label} this #{commit.change_type_title(current_user)}
.modal-body
- = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-#{type}-form js-requires-input' do
+ = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do
.form-group.branch
= label_tag 'target_branch', target_label, class: 'control-label'
.col-sm-10
@@ -23,12 +23,11 @@
- if can?(current_user, :push_code, @project)
.js-create-merge-request-container
.checkbox
- - nonce = SecureRandom.hex
- = label_tag "create_merge_request-#{nonce}" do
- = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}"
+ = label_tag do
+ = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: nil
Start a <strong>new merge request</strong> with these changes
- else
- = hidden_field_tag 'create_merge_request', 1
+ = hidden_field_tag 'create_merge_request', 1, id: nil
.form-actions
= submit_tag label, class: 'btn btn-create'
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 65151ac3a56..c08ed8f6c16 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -1,13 +1,8 @@
.page-content-header
.header-main-content
- %strong Commit
- %strong.monospace.js-details-short= @commit.short_id
- = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do
- %span.text-expander
- \...
- %span.js-details-content.hide
- %strong.monospace.commit-hash-full= @commit.id
+ %strong
= clipboard_button(clipboard_text: @commit.id)
+ = @commit.short_id
%span.hidden-xs authored
#{time_ago_with_tooltip(@commit.authored_date)}
%span by
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index c7b5c1124b3..08d3443b3d0 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -24,7 +24,7 @@
in
= time_interval_in_words pipeline.duration
- .row-content-block.build-content.middle-block.hidden
+ .row-content-block.build-content.middle-block.js-pipeline-graph.hidden
= render "projects/pipelines/graph", pipeline: pipeline
- if pipeline.yaml_errors.present?
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/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
index 7f751d9ae2e..9f444f076c0 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -8,10 +8,7 @@
%tr.generic_commit_status{class: ('retried' if retried)}
%td.status
- - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url
- = ci_status_with_icon(generic_commit_status.status, generic_commit_status.target_url)
- - else
- = ci_status_with_icon(generic_commit_status.status)
+ = render 'ci/status/badge', status: generic_commit_status.detailed_status(current_user)
%td.generic_commit_status-link
- if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
deleted file mode 100644
index 7b82d913d29..00000000000
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } }
- - if subject.target_url
- = link_to subject.target_url do
- %span{class: "ci-status-icon ci-status-icon-#{subject.status}"}
- = ci_icon_for_status(subject.status)
- %span.ci-status-text= subject.name
- - else
- %span{class: "ci-status-icon ci-status-icon-#{subject.status}"}
- = ci_icon_for_status(subject.status)
- %span.ci-status-text= subject.name
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index bd629b5c519..981bf640a6b 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -1,3 +1,4 @@
+- @content_class = "limit-container-width"
- page_title "#{@issue.title} (#{@issue.to_reference})", "Issues"
- page_description @issue.description
- page_card_attributes @issue.card_attributes
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index fa189ae62d8..b3c43286a50 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -1,4 +1,4 @@
-%li{ class: mr_css_classes(merge_request) }
+%li{ id: dom_id(merge_request), class: mr_css_classes(merge_request), data: { labels: merge_request.label_ids, id: merge_request.id } }
- if @bulk_edit
.issue-check
= check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue"
@@ -39,7 +39,7 @@
= icon('thumbs-down')
= downvotes
- - note_count = merge_request.mr_and_commit_notes.user.count
+ - note_count = merge_request.related_notes.user.count
%li
= link_to merge_request_path(merge_request, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
= icon('comments')
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 896f10104fa..7725558518f 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -1,3 +1,4 @@
+- @content_class = "limit-container-width"
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
@@ -41,7 +42,7 @@
= render "projects/merge_requests/widget/show.html.haml"
- if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user)
- .light.prepend-top-default.append-bottom-default
+ .merge-manually.light.prepend-top-default.append-bottom-default
You can also accept this merge request manually using the
= succeed '.' do
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
@@ -53,7 +54,7 @@
%li.notes-tab
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do
Discussion
- %span.badge= @merge_request.mr_and_commit_notes.user.count
+ %span.badge= @merge_request.related_notes.user.count
- if @merge_request.source_project
%li.commits-tab
= link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do
diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml
index a0e12fb3f38..baa1ade5eee 100644
--- a/app/views/projects/merge_requests/show/_commits.html.haml
+++ b/app/views/projects/merge_requests/show/_commits.html.haml
@@ -1,6 +1,2 @@
-.content-block.oneline-block
- = icon("sort-amount-desc")
- Most recent commits displayed first
-
%ol#commits-list.list-unstyled
= render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch
diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml
index eee711dc5af..f4aa1609a1b 100644
--- a/app/views/projects/merge_requests/widget/_open.html.haml
+++ b/app/views/projects/merge_requests/widget/_open.html.haml
@@ -30,11 +30,18 @@
- elsif @merge_request.can_be_merged? || resolved_conflicts
= render 'projects/merge_requests/widget/open/accept'
- - if mr_closes_issues.present?
+ - if mr_closes_issues.present? || mr_issues_mentioned_but_not_closing.present?
.mr-widget-footer
%span
- %i.fa.fa-check
- Accepting this merge request will close #{"issue".pluralize(mr_closes_issues.size)}
- = succeed '.' do
- != markdown issues_sentence(mr_closes_issues), pipeline: :gfm, author: @merge_request.author
- = mr_assign_issues_link
+ = icon('check')
+ - if mr_closes_issues.present?
+ Accepting this merge request will close #{"issue".pluralize(mr_closes_issues.size)}
+ = succeed '.' do
+ != markdown issues_sentence(mr_closes_issues), pipeline: :gfm, author: @merge_request.author
+ = mr_assign_issues_link
+ - if mr_issues_mentioned_but_not_closing.present?
+ #{"Issue".pluralize(mr_issues_mentioned_but_not_closing.size)}
+ != markdown issues_sentence(mr_issues_mentioned_but_not_closing), pipeline: :gfm, author: @merge_request.author
+ #{mr_issues_mentioned_but_not_closing.size > 1 ? 'are' : 'is'} mentioned but will not closed.
+
+
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 435fe835fae..d6f7f23533c 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -41,6 +41,8 @@
Modify commit message
.js-toggle-content.hide.prepend-top-default
= render 'shared/commit_message_container', params: params,
+ message_with_description: @merge_request.merge_commit_message(include_description: true),
+ message_without_description: @merge_request.merge_commit_message,
text: @merge_request.merge_commit_message,
rows: 14, hint: true
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/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index ba8895438c5..778a32e6345 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -65,7 +65,7 @@
.note-text.md
= preserve do
= note.redacted_note_html
- = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
+ = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note_editable
= render 'projects/notes/edit_form', note: note
.note-awards
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 229bdfb0e8d..b00ba2d5307 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -1,6 +1,6 @@
.page-content-header
.header-main-content
- = ci_status_with_icon(@pipeline.detailed_status)
+ = render 'ci/status/badge', status: @pipeline.detailed_status(current_user)
%strong Pipeline ##{@commit.pipelines.last.id}
triggered #{time_ago_with_tooltip(@commit.authored_date)} by
= author_avatar(@commit, size: 24)
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 739e5930822..88af41aa835 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -12,7 +12,7 @@
.tab-content
#js-tab-pipeline.tab-pane
- .build-content.middle-block
+ .build-content.middle-block.js-pipeline-graph
= render "projects/pipelines/graph", pipeline: pipeline
#js-tab-builds.tab-pane
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index e1e787dbde4..030cd8ef78f 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -5,23 +5,23 @@
%div{ class: container_class }
.top-area
%ul.nav-links
- %li{class: ('active' if @scope.nil?)}
+ %li{class: ('active' if @scope.nil?)}>
= link_to project_pipelines_path(@project) do
All
%span.badge.js-totalbuilds-count
= number_with_delimiter(@pipelines_count)
- %li{class: ('active' if @scope == 'running')}
+ %li{class: ('active' if @scope == 'running')}>
= link_to project_pipelines_path(@project, scope: :running) do
Running
%span.badge.js-running-count
= number_with_delimiter(@running_or_pending_count)
- %li{class: ('active' if @scope == 'branches')}
+ %li{class: ('active' if @scope == 'branches')}>
= link_to project_pipelines_path(@project, scope: :branches) do
Branches
- %li{class: ('active' if @scope == 'tags')}
+ %li{class: ('active' if @scope == 'tags')}>
= link_to project_pipelines_path(@project, scope: :tags) do
Tags
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index bdeb704b6da..4f1cec20f85 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -21,6 +21,7 @@
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
%button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
= icon("search")
+ = render 'shared/members/sort_dropdown'
- if @group_links.any?
= render 'groups', group_links: @group_links
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 a676c0290a0..01a77a952d1 100644
--- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
@@ -1,5 +1,4 @@
-- pretty_path_with_namespace = "#{@project ? @project.namespace.name : 'namespace'} / #{@project ? @project.name : 'name'}"
-- run_actions_text = "Perform common operations on this project: #{pretty_path_with_namespace}"
+- 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
@@ -27,7 +26,7 @@
.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 / #{pretty_path_with_namespace}", class: 'form-control input-sm', readonly: 'readonly'
+ = 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')
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 c50093cf47c..8a214e1de58 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -64,20 +64,11 @@
- unless @repository.gitlab_ci_yml
%li.missing
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
- Set Up CI
-
- %li.project-repo-buttons.right
- .project-right-buttons
- - if current_user
- = render 'shared/members/access_request_buttons', source: @project
- = render "projects/buttons/koding"
-
- .btn-group.project-repo-btn-group
- = render 'projects/buttons/download', project: @project, ref: @ref
- = render 'projects/buttons/dropdown'
+ Set up CI
+ - if koding_enabled? && @repository.koding_yml.blank?
+ %li.missing
+ = link_to 'Set up Koding', add_koding_stack_path(@project)
- .pull-right
- = render 'shared/notifications/button', notification_setting: @notification_setting
- if @repository.commit
.project-last-commit{ class: container_class }
= render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project
diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml
index 32e1f8a21b0..068a6610350 100644
--- a/app/views/projects/snippets/_actions.html.haml
+++ b/app/views/projects/snippets/_actions.html.haml
@@ -1,13 +1,13 @@
.hidden-xs
- - if can?(current_user, :create_project_snippet, @project)
- = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New snippet" do
- New snippet
- - if can?(current_user, :update_project_snippet, @snippet)
- = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do
- Delete
- if can?(current_user, :update_project_snippet, @snippet)
- = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do
+ = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped" do
Edit
+ - if can?(current_user, :update_project_snippet, @snippet)
+ = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do
+ Delete
+ - if can?(current_user, :create_project_snippet, @project)
+ = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do
+ New snippet
- if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet)
.visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml
index e77e1b026f6..84e05cd6d88 100644
--- a/app/views/projects/snippets/index.html.haml
+++ b/app/views/projects/snippets/index.html.haml
@@ -1,11 +1,19 @@
- page_title "Snippets"
-.sub-header-block
- - if can?(current_user, :create_project_snippet, @project)
- = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do
- New snippet
+- if current_user
+ .top-area
+ - include_private = @project.team.member?(current_user) || current_user.admin?
+ = render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private }
+
+ .nav-controls.hidden-xs
+ - if can?(current_user, :create_project_snippet, @project)
+ = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New snippet" do
+ New snippet
- .oneline
- Share code pastes with others out of git repository
+- if can?(current_user, :create_project_snippet, @project)
+ .visible-xs
+ &nbsp;
+ = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-block", title: "New snippet" do
+ New snippet
= render 'snippets/snippets'
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index 9503dbded13..79d87b7db12 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -8,10 +8,11 @@
= blob_icon 0, @snippet.file_name
= @snippet.file_name
.file-actions
- = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
+ = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']", class: "btn btn-sm")
= link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank"
= render 'shared/snippets/blob'
- = render 'award_emoji/awards_block', awardable: @snippet, inline: true
+ .row-content-block.top-block.content-component-block
+ = render 'award_emoji/awards_block', awardable: @snippet, inline: true
%div#notes= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml
index 1d8fa10db0c..6919b210a00 100644
--- a/app/views/projects/stage/_graph.html.haml
+++ b/app/views/projects/stage/_graph.html.haml
@@ -10,13 +10,11 @@
- status_groups.each do |group_name, grouped_statuses|
- if grouped_statuses.one?
- status = grouped_statuses.first
- - is_playable = status.playable? && can?(current_user, :update_build, @project)
- %li.build{ class: ("playable" if is_playable) }
+ %li.build{ 'id' => "ci-badge-#{group_name}" }
.curve
- .build-content
- = render "projects/#{status.to_partial_path}_pipeline", subject: status
+ = render 'ci/status/graph_badge', subject: status
- else
- %li.build
+ %li.build{ 'id' => "ci-badge-#{group_name}" }
.curve
- .dropdown.inline.build-content
- = render "projects/stage/in_stage_group", name: group_name, subject: grouped_statuses
+ = render 'projects/stage/in_stage_group', name: group_name, subject: grouped_statuses
+
diff --git a/app/views/projects/stage/_in_stage_group.html.haml b/app/views/projects/stage/_in_stage_group.html.haml
index 2b26ad9d6fa..b15f7eaeab2 100644
--- a/app/views/projects/stage/_in_stage_group.html.haml
+++ b/app/views/projects/stage/_in_stage_group.html.haml
@@ -1,13 +1,13 @@
- group_status = CommitStatus.where(id: subject).status
-%button.dropdown-menu-toggle.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}" } }
+%button.dropdown-menu-toggle.build-content.has-tooltip{ type: 'button', data: { toggle: 'dropdown'} }
%span{class: "ci-status-icon ci-status-icon-#{group_status}"}
= ci_icon_for_status(group_status)
- %span.ci-status-text
+ %span.ci-status-text{ 'data-toggle' => 'tooltip', 'data-title' => "#{name} - #{group_status}" }
= name
- %span.badge= subject.size
+ %span.dropdown-counter-badge= subject.size
.dropdown-menu.grouped-pipeline-dropdown
.arrow
%ul
- subject.each do |status|
- %li
- = render "projects/#{status.to_partial_path}_pipeline", subject: status
+ %li.dropdown-build
+ = render 'ci/status/graph_badge', subject: status
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
index 21e378b8735..d37c376c36b 100644
--- a/app/views/projects/tree/_tree_content.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -5,14 +5,11 @@
%tr
%th Name
%th.hidden-xs
- .pull-left Last Commit
+ .pull-left Last commit
.last-commit.hidden-sm.pull-left
- &nbsp;
- %i.fa.fa-angle-right
- &nbsp;
- %small.light
+ %small.light
+ = clipboard_button(clipboard_text: @commit.id)
= link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
- &ndash;
= time_ago_with_tooltip(@commit.committed_date)
= @commit.full_title
%small.commit-history-link-spacer &#124;
diff --git a/app/views/repository_check_mailer/notify.html.haml b/app/views/repository_check_mailer/notify.html.haml
index a585147ddd1..94e5a5d9709 100644
--- a/app/views/repository_check_mailer/notify.html.haml
+++ b/app/views/repository_check_mailer/notify.html.haml
@@ -2,7 +2,7 @@
#{@message}.
%p
- = link_to "See the affected projects in the GitLab admin panel", admin_namespaces_projects_url(last_repository_check_failed: 1)
+ = link_to "See the affected projects in the GitLab admin panel", admin_projects_url(last_repository_check_failed: 1)
%p
You are receiving this message because you are a GitLab administrator for #{Gitlab.config.gitlab.url}.
diff --git a/app/views/repository_check_mailer/notify.text.haml b/app/views/repository_check_mailer/notify.text.haml
index 93db151329e..0902c50d052 100644
--- a/app/views/repository_check_mailer/notify.text.haml
+++ b/app/views/repository_check_mailer/notify.text.haml
@@ -1,6 +1,6 @@
#{@message}.
\
-View details: #{admin_namespaces_projects_url(last_repository_check_failed: 1)}
+View details: #{admin_projects_url(last_repository_check_failed: 1)}
You are receiving this message because you are a GitLab administrator
for #{Gitlab.config.gitlab.url}.
diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml
index c414acb6a11..027d42396b4 100644
--- a/app/views/search/results/_snippet_title.html.haml
+++ b/app/views/search/results/_snippet_title.html.haml
@@ -14,7 +14,7 @@
= link_to snippet_title.project.name_with_namespace, namespace_project_path(snippet_title.project.namespace, snippet_title.project)
.snippet-info
- = "##{snippet_title.id}"
+ = snippet_title.to_reference
%span
by
= link_to user_snippets_path(snippet_title.author) do
diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml
index 0a38327baa2..c196bc06b17 100644
--- a/app/views/shared/_commit_message_container.html.haml
+++ b/app/views/shared/_commit_message_container.html.haml
@@ -1,5 +1,6 @@
.form-group.commit_message-group
- nonce = SecureRandom.hex
+ - descriptions = local_assigns.slice(:message_with_description, :message_without_description)
= label_tag "commit_message-#{nonce}", class: 'control-label' do
Commit message
.col-sm-10
@@ -8,9 +9,17 @@
= text_area_tag 'commit_message',
(params[:commit_message] || local_assigns[:text] || local_assigns[:placeholder]),
class: 'form-control js-commit-message', placeholder: local_assigns[:placeholder],
+ data: descriptions,
required: true, rows: (local_assigns[:rows] || 3),
id: "commit_message-#{nonce}"
- if local_assigns[:hint]
%p.hint
Try to keep the first line under 52 characters
and the others under 72.
+ - if descriptions.present?
+ %p.hint.js-with-description-hint
+ = link_to "#", class: "js-with-description-link" do
+ Include description in commit message
+ %p.hint.js-without-description-hint.hide
+ = link_to "#", class: "js-without-description-link" do
+ Don't include description in commit message
diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index 73d288e2236..186ed4a7c8b 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -2,17 +2,17 @@
- counts = milestone_counts(@project.milestones)
%ul.nav-links
- %li{class: milestone_class_for_state(params[:state], 'opened', true)}
+ %li{class: milestone_class_for_state(params[:state], 'opened', true)}>
= link_to milestones_filter_path(state: 'opened') do
Open
- if @project
%span.badge #{counts[:opened]}
- %li{class: milestone_class_for_state(params[:state], 'closed')}
+ %li{class: milestone_class_for_state(params[:state], 'closed')}>
= link_to milestones_filter_path(state: 'closed') do
Closed
- if @project
%span.badge #{counts[:closed]}
- %li{class: milestone_class_for_state(params[:state], 'all')}
+ %li{class: milestone_class_for_state(params[:state], 'all')}>
= link_to milestones_filter_path(state: 'all') do
All
- if @project
diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml
index 60353aee7f1..b6047ece592 100644
--- a/app/views/shared/builds/_tabs.html.haml
+++ b/app/views/shared/builds/_tabs.html.haml
@@ -1,23 +1,23 @@
%ul.nav-links
- %li{ class: ('active' if scope.nil?) }
+ %li{ class: ('active' if scope.nil?) }>
= link_to build_path_proc.call(nil) do
All
%span.badge.js-totalbuilds-count
= number_with_delimiter(all_builds.count(:id))
- %li{ class: ('active' if scope == 'pending') }
+ %li{ class: ('active' if scope == 'pending') }>
= link_to build_path_proc.call('pending') do
Pending
%span.badge
= number_with_delimiter(all_builds.pending.count(:id))
- %li{ class: ('active' if scope == 'running') }
+ %li{ class: ('active' if scope == 'running') }>
= link_to build_path_proc.call('running') do
Running
%span.badge
= number_with_delimiter(all_builds.running.count(:id))
- %li{ class: ('active' if scope == 'finished') }
+ %li{ class: ('active' if scope == 'finished') }>
= link_to build_path_proc.call('finished') do
Finished
%span.badge
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 19221e3391f..8164f61797c 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -28,7 +28,7 @@
= image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title
= link_to group, class: 'group-name' do
- = group.name
+ = group.full_name
- if group_member
as
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.svg b/app/views/shared/icons/_icon_status_canceled.svg
index 41a210a8ed9..bd5d04e1cd7 100644..100755
--- a/app/views/shared/icons/_icon_status_canceled.svg
+++ b/app/views/shared/icons/_icon_status_canceled.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill-rule="evenodd"><path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/><rect width="8" height="2" x="3" y="6" transform="rotate(45 7 7)" rx=".5"/></g></svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_created.svg b/app/views/shared/icons/_icon_status_created.svg
index 1f5c3b51b03..326ad04e017 100644..100755
--- a/app/views/shared/icons/_icon_status_created.svg
+++ b/app/views/shared/icons/_icon_status_created.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" enable-background="new 0 0 14 14"><path d="M12.5,7 C12.5,4 10,1.5 7,1.5 C4,1.5 1.5,4 1.5,7 C1.5,10 4,12.5 7,12.5 C10,12.5 12.5,10 12.5,7 L12.5,7 Z M0,7 C0,3.1 3.1,0 7,0 C10.9,0 14,3.1 14,7 C14,10.9 10.9,14 7,14 C3.1,14 0,10.9 0,7 L0,7 Z" /><circle cx="7" cy="7" r="3.25"/></svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_failed.svg b/app/views/shared/icons/_icon_status_failed.svg
index af267b8938a..64da5aa31fc 100644..100755
--- a/app/views/shared/icons/_icon_status_failed.svg
+++ b/app/views/shared/icons/_icon_status_failed.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill-rule="evenodd"><path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/><path d="M7.72916667,6.27083333 L7.72916667,4.28939247 C7.72916667,4.12531853 7.59703895,4 7.43405116,4 L6.56594884,4 C6.40541585,4 6.27083333,4.12956542 6.27083333,4.28939247 L6.27083333,6.27083333 L4.28939247,6.27083333 C4.12531853,6.27083333 4,6.40296105 4,6.56594884 L4,7.43405116 C4,7.59458415 4.12956542,7.72916667 4.28939247,7.72916667 L6.27083333,7.72916667 L6.27083333,9.71060753 C6.27083333,9.87468147 6.40296105,10 6.56594884,10 L7.43405116,10 C7.59458415,10 7.72916667,9.87043458 7.72916667,9.71060753 L7.72916667,7.72916667 L9.71060753,7.72916667 C9.87468147,7.72916667 10,7.59703895 10,7.43405116 L10,6.56594884 C10,6.40541585 9.87043458,6.27083333 9.71060753,6.27083333 L7.72916667,6.27083333 Z" transform="rotate(-45 7 7)"/></g></svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_manual.svg b/app/views/shared/icons/_icon_status_manual.svg
new file mode 100755
index 00000000000..c98839f51a9
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_manual.svg
@@ -0,0 +1 @@
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_pending.svg b/app/views/shared/icons/_icon_status_pending.svg
index 516231d1b44..02d5da407e3 100644..100755
--- a/app/views/shared/icons/_icon_status_pending.svg
+++ b/app/views/shared/icons/_icon_status_pending.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill-rule="evenodd"><path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/><path d="M4.69999981,5.30065012 C4.69999981,5.13460564 4.83842754,5 5.00354719,5 L5.89645243,5 C6.06409702,5 6.19999981,5.13308716 6.19999981,5.30065012 L6.19999981,8.69934988 C6.19999981,8.86539436 6.06157207,9 5.89645243,9 L5.00354719,9 C4.8359026,9 4.69999981,8.86691284 4.69999981,8.69934988 L4.69999981,5.30065012 Z M7.69999981,5.30065012 C7.69999981,5.13460564 7.83842754,5 8.00354719,5 L8.89645243,5 C9.06409702,5 9.19999981,5.13308716 9.19999981,5.30065012 L9.19999981,8.69934988 C9.19999981,8.86539436 9.06157207,9 8.89645243,9 L8.00354719,9 C7.8359026,9 7.69999981,8.86691284 7.69999981,8.69934988 L7.69999981,5.30065012 Z"/></g></svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_running.svg b/app/views/shared/icons/_icon_status_running.svg
index d2618bce200..532f4fee33c 100644..100755
--- a/app/views/shared/icons/_icon_status_running.svg
+++ b/app/views/shared/icons/_icon_status_running.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill-rule="evenodd"><path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/><path d="M7,3 C9.209139,3 11,4.790861 11,7 C11,9.209139 9.209139,11 7,11 C5.65802855,11 4.47040669,10.3391508 3.74481446,9.32513253 L7,7 L7,3 L7,3 Z"/></g></svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_skipped.svg b/app/views/shared/icons/_icon_status_skipped.svg
index 701f33bcbea..1998dfef9ea 100644..100755
--- a/app/views/shared/icons/_icon_status_skipped.svg
+++ b/app/views/shared/icons/_icon_status_skipped.svg
@@ -1 +1 @@
-<svg width="14" height="14" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M10 17.857c4.286 0 7.857-3.571 7.857-7.857S14.286 2.143 10 2.143 2.143 5.714 2.143 10 5.714 17.857 10 17.857M10 0c5.571 0 10 4.429 10 10s-4.429 10-10 10S0 15.571 0 10 4.429 0 10 0"/><path d="M10.986 11l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.644-2.644a1.505 1.505 0 0 0 0-2.126l-2.644-2.644a1 1 0 0 0-1.414 1.414L10.986 9H6.4a1 1 0 0 0 0 2h4.586z"/></g></svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7.69 7.7l-.905.905a.7.7 0 0 0 .99.99l1.85-1.85c.411-.412.411-1.078 0-1.49l-1.85-1.85a.7.7 0 0 0-.99.99l.905.905H4.48a.7.7 0 0 0 0 1.4h3.21z"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_success.svg b/app/views/shared/icons/_icon_status_success.svg
index b7c21ba6971..eed5006bebe 100644..100755
--- a/app/views/shared/icons/_icon_status_success.svg
+++ b/app/views/shared/icons/_icon_status_success.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill-rule="evenodd"><path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/><path d="M7.29166667,7.875 L5.54840803,7.875 C5.38293028,7.875 5.25,8.00712771 5.25,8.17011551 L5.25,9.03821782 C5.25,9.19875081 5.38360183,9.33333333 5.54840803,9.33333333 L8.24853534,9.33333333 C8.52035522,9.33333333 8.75,9.11228506 8.75,8.83960819 L8.75,8.46475969 L8.75,4.07392947 C8.75,3.92144267 8.61787229,3.79166667 8.45488449,3.79166667 L7.58678218,3.79166667 C7.42624919,3.79166667 7.29166667,3.91804003 7.29166667,4.07392947 L7.29166667,7.875 Z" transform="rotate(45 7 6.563)"/></g></svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_warning.svg b/app/views/shared/icons/_icon_status_warning.svg
index 9191e0050a6..cb785635b7e 100644..100755
--- a/app/views/shared/icons/_icon_status_warning.svg
+++ b/app/views/shared/icons/_icon_status_warning.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill-rule="evenodd"><path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/><path d="M6,3.49769878 C6,3.22282734 6.21403503,3 6.50468445,3 L7.49531555,3 C7.77404508,3 8,3.21484375 8,3.49769878 L8,7.50230122 C8,7.77717266 7.78596497,8 7.49531555,8 L6.50468445,8 C6.22595492,8 6,7.78515625 6,7.50230122 L6,3.49769878 Z M6,9.50468445 C6,9.22595492 6.21403503,9 6.50468445,9 L7.49531555,9 C7.77404508,9 8,9.21403503 8,9.50468445 L8,10.4953156 C8,10.7740451 7.78596497,11 7.49531555,11 L6.50468445,11 C6.22595492,11 6,10.785965 6,10.4953156 L6,9.50468445 Z"/></g></svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></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/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index 0af92b59584..d938edf4dbd 100644
--- a/app/views/shared/issuable/_nav.html.haml
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -3,23 +3,23 @@
- issuables = @issues || @merge_requests
%ul.nav-links.issues-state-filters
- %li{class: ("active" if params[:state] == 'opened')}
+ %li{class: ("active" if params[:state] == 'opened')}>
= link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened." do
#{issuables_state_counter_text(type, :opened)}
- if type == :merge_requests
- %li{class: ("active" if params[:state] == 'merged')}
+ %li{class: ("active" if params[:state] == 'merged')}>
= link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.' do
#{issuables_state_counter_text(type, :merged)}
- %li{class: ("active" if params[:state] == 'closed')}
+ %li{class: ("active" if params[:state] == 'closed')}>
= link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.' do
#{issuables_state_counter_text(type, :closed)}
- else
- %li{class: ("active" if params[:state] == 'closed')}
+ %li{class: ("active" if params[:state] == 'closed')}>
= link_to page_filter_path(state: 'closed', label: true), id: 'state-all', title: 'Filter by issues that are currently closed.' do
#{issuables_state_counter_text(type, :closed)}
- %li{class: ("active" if params[:state] == 'all')}
+ %li{class: ("active" if params[:state] == 'all')}>
= link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}." do
#{issuables_state_counter_text(type, :all)}
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 958f8413e1d..9fe1be5a597 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -167,4 +167,4 @@
new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}');
gl.Subscription.bindAll('.subscription');
new gl.DueDateSelectors();
- sidebar = new Sidebar();
+ window.sidebar = new Sidebar();
diff --git a/app/views/shared/members/_access_request_buttons.html.haml b/app/views/shared/members/_access_request_buttons.html.haml
index e166dfab710..fb795ad1c72 100644
--- a/app/views/shared/members/_access_request_buttons.html.haml
+++ b/app/views/shared/members/_access_request_buttons.html.haml
@@ -1,16 +1,17 @@
- model_name = source.model_name.to_s.downcase
-- if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id))
- = link_to "Leave #{model_name}", polymorphic_path([:leave, source, :members]),
- method: :delete,
- data: { confirm: leave_confirmation_message(source) },
- class: 'btn'
-- elsif requester = source.requesters.find_by(user_id: current_user.id)
- = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]),
- method: :delete,
- data: { confirm: remove_member_message(requester) },
- class: 'btn'
-- elsif source.request_access_enabled && can?(current_user, :request_access, source)
- = link_to 'Request Access', polymorphic_path([:request_access, source, :members]),
- method: :post,
- class: 'btn'
+.project-action-button.inline
+ - if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id))
+ = link_to "Leave #{model_name}", polymorphic_path([:leave, source, :members]),
+ method: :delete,
+ data: { confirm: leave_confirmation_message(source) },
+ class: 'btn'
+ - elsif requester = source.requesters.find_by(user_id: current_user.id)
+ = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]),
+ method: :delete,
+ data: { confirm: remove_member_message(requester) },
+ class: 'btn'
+ - elsif source.request_access_enabled && can?(current_user, :request_access, source)
+ = link_to 'Request Access', polymorphic_path([:request_access, source, :members]),
+ method: :post,
+ class: 'btn'
diff --git a/app/views/shared/members/_sort_dropdown.html.haml b/app/views/shared/members/_sort_dropdown.html.haml
new file mode 100644
index 00000000000..bad0891f9f2
--- /dev/null
+++ b/app/views/shared/members/_sort_dropdown.html.haml
@@ -0,0 +1,9 @@
+.dropdown.inline.member-sort-dropdown
+ = dropdown_toggle(member_sort_options_hash[@sort], { toggle: 'dropdown' })
+ %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
+ %li.dropdown-header
+ Sort by
+ - member_sort_options_hash.each do |value, title|
+ %li
+ = link_to filter_group_project_member_path(sort: value), class: ("is-active" if @sort == value) do
+ = title
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/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index fbad0d05de3..1d072c16b32 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -1,5 +1,5 @@
- if notification_setting
- .dropdown.notification-dropdown
+ .js-notification-dropdown.notification-dropdown.project-action-button.dropdown.inline
= form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f|
= hidden_setting_source_input(notification_setting)
= f.hidden_field :level, class: "notification_setting_level"
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index d7506e07ff6..d084f5e9684 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -8,10 +8,6 @@
%span.creator
authored
= time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago')
- - if @snippet.updated_at != @snippet.created_at
- %span
- = icon('edit', title: 'edited')
- = time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago')
by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "hidden-xs")}
.snippet-actions
@@ -20,5 +16,9 @@
- else
= render "snippets/actions"
-%h2.snippet-title.prepend-top-0.append-bottom-0
- = markdown_field(@snippet, :title)
+.snippet-header
+ %h2.snippet-title.prepend-top-0.append-bottom-0
+ = markdown_field(@snippet, :title)
+
+ - if @snippet.updated_at != @snippet.created_at
+ = edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago')
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index ea17bec8677..5d2d2317f22 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -1,17 +1,16 @@
+- link_project = local_assigns.fetch(:link_project, false)
+
%li.snippet-row
= image_tag avatar_icon(snippet.author_email), class: "avatar s40 hidden-xs", alt: ''
.title
= link_to reliable_snippet_path(snippet) do
= snippet.title
- - if snippet.private?
- %span.label.label-gray.hidden-xs
- = icon('lock')
- private
- %span.monospace.pull-right.hidden-xs
- = snippet.file_name
+ - if snippet.file_name
+ %span.snippet-filename.monospace.hidden-xs
+ = snippet.file_name
- %ul.controls.visible-xs
+ %ul.controls
%li
- note_count = snippet.notes.user.count
= link_to reliable_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
@@ -22,11 +21,17 @@
= visibility_level_label(snippet.visibility_level)
= visibility_level_icon(snippet.visibility_level, fw: false)
- %small.pull-right.cgray.hidden-xs
- - if snippet.project_id?
- = link_to snippet.project.name_with_namespace, namespace_project_path(snippet.project.namespace, snippet.project)
-
- .snippet-info.hidden-xs
+ .snippet-info
+ #{snippet.to_reference} &middot;
+ authored #{time_ago_with_tooltip(snippet.created_at, placement: 'bottom', html_class: 'snippet-created-ago')}
+ by
= link_to user_snippets_path(snippet.author) do
= snippet.author_name
- authored #{time_ago_with_tooltip(snippet.created_at)}
+ - if link_project && snippet.project_id?
+ %span.hidden-xs
+ in
+ = link_to namespace_project_path(snippet.project.namespace, snippet.project) do
+ = snippet.project.name_with_namespace
+
+ .pull-right.snippet-updated-at
+ %span updated #{time_ago_with_tooltip(snippet.updated_at, placement: 'bottom')}
diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml
new file mode 100644
index 00000000000..5074afb63a1
--- /dev/null
+++ b/app/views/shared/tokens/_scopes_form.html.haml
@@ -0,0 +1,9 @@
+- scopes = local_assigns.fetch(:scopes)
+- prefix = local_assigns.fetch(:prefix)
+- token = local_assigns.fetch(:token)
+
+- scopes.each do |scope|
+ %fieldset
+ = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}"
+ = label_tag "#{prefix}_scopes_#{scope}", scope
+ %span= "(#{t(scope, scope: [:doorkeeper, :scopes])})"
diff --git a/app/views/shared/tokens/_scopes_list.html.haml b/app/views/shared/tokens/_scopes_list.html.haml
new file mode 100644
index 00000000000..f99e905e95c
--- /dev/null
+++ b/app/views/shared/tokens/_scopes_list.html.haml
@@ -0,0 +1,13 @@
+- token = local_assigns.fetch(:token)
+
+- return unless token.scopes.present?
+
+%tr
+ %td
+ Scopes
+ %td
+ %ul.scopes-list.append-bottom-0
+ - token.scopes.each do |scope|
+ %li
+ %span.scope-name= scope
+ = "(#{t(scope, scope: [:doorkeeper, :scopes])})"
diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml
index 1d0e549ed3d..95fc7198104 100644
--- a/app/views/snippets/_actions.html.haml
+++ b/app/views/snippets/_actions.html.haml
@@ -1,13 +1,13 @@
.hidden-xs
- - if current_user
- = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New snippet" do
- New snippet
- - if can?(current_user, :admin_personal_snippet, @snippet)
- = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do
- Delete
- if can?(current_user, :update_personal_snippet, @snippet)
- = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do
+ = link_to edit_snippet_path(@snippet), class: "btn btn-grouped" do
Edit
+ - if can?(current_user, :admin_personal_snippet, @snippet)
+ = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do
+ Delete
+ - if current_user
+ = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do
+ New snippet
- if current_user
.visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml
index 77b66ca74b6..ac3701233ad 100644
--- a/app/views/snippets/_snippets.html.haml
+++ b/app/views/snippets/_snippets.html.haml
@@ -1,8 +1,9 @@
- remote = local_assigns.fetch(:remote, false)
+- link_project = local_assigns.fetch(:link_project, false)
.snippets-list-holder
%ul.content-list
- = render partial: 'shared/snippets/snippet', collection: @snippets
+ = render partial: 'shared/snippets/snippet', collection: @snippets, locals: { link_project: link_project }
- if @snippets.empty?
%li
.nothing-here-block Nothing here.
diff --git a/app/views/snippets/_snippets_scope_menu.html.haml b/app/views/snippets/_snippets_scope_menu.html.haml
new file mode 100644
index 00000000000..2dda5fed647
--- /dev/null
+++ b/app/views/snippets/_snippets_scope_menu.html.haml
@@ -0,0 +1,31 @@
+- subject = local_assigns.fetch(:subject, current_user)
+- include_private = local_assigns.fetch(:include_private, false)
+
+.nav-links.snippet-scope-menu
+ %li{ class: ("active" unless params[:scope]) }
+ = link_to subject_snippets_path(subject) do
+ All
+ %span.badge
+ - if include_private
+ = subject.snippets.count
+ - else
+ = subject.snippets.public_and_internal.count
+
+ - if include_private
+ %li{ class: ("active" if params[:scope] == "are_private") }
+ = link_to subject_snippets_path(subject, scope: 'are_private') do
+ Private
+ %span.badge
+ = subject.snippets.are_private.count
+
+ %li{ class: ("active" if params[:scope] == "are_internal") }
+ = link_to subject_snippets_path(subject, scope: 'are_internal') do
+ Internal
+ %span.badge
+ = subject.snippets.are_internal.count
+
+ %li{ class: ("active" if params[:scope] == "are_public") }
+ = link_to subject_snippets_path(subject, scope: 'are_public') do
+ Public
+ %span.badge
+ = subject.snippets.are_public.count
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index 27d7a6c5bb6..837a1a0cc8c 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -7,9 +7,9 @@
= blob_icon 0, @snippet.file_name
= @snippet.file_name
.file-actions
- = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
+ = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']", class: "btn btn-sm")
= link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank"
= link_to 'Download', download_snippet_path(@snippet), class: "btn btn-sm"
= render 'shared/snippets/blob'
-= render 'award_emoji/awards_block', awardable: @snippet, inline: true \ No newline at end of file
+= render 'award_emoji/awards_block', awardable: @snippet, inline: true
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/18435-autocomplete-is-not-performant.yml b/changelogs/unreleased/18435-autocomplete-is-not-performant.yml
new file mode 100644
index 00000000000..019c55e27dc
--- /dev/null
+++ b/changelogs/unreleased/18435-autocomplete-is-not-performant.yml
@@ -0,0 +1,4 @@
+---
+title: Made comment autocomplete more performant and removed some loading bugs
+merge_request: 6856
+author:
diff --git a/changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml b/changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml
new file mode 100644
index 00000000000..742b10e72aa
--- /dev/null
+++ b/changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml
@@ -0,0 +1,4 @@
+---
+title: group authors in contribution graph with case insensitive email handle comparison
+merge_request: 8021
+author:
diff --git a/changelogs/unreleased/20052-actions-table-vscroll.yml b/changelogs/unreleased/20052-actions-table-vscroll.yml
new file mode 100644
index 00000000000..779cd08de09
--- /dev/null
+++ b/changelogs/unreleased/20052-actions-table-vscroll.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent overflow with vertical scroll when we have space to show content
+merge_request: 8061
+author:
diff --git a/changelogs/unreleased/20492-access-token-scopes.yml b/changelogs/unreleased/20492-access-token-scopes.yml
new file mode 100644
index 00000000000..a9424ded662
--- /dev/null
+++ b/changelogs/unreleased/20492-access-token-scopes.yml
@@ -0,0 +1,4 @@
+---
+title: Add scopes for personal access tokens and OAuth tokens
+merge_request: 5951
+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/22604-manual-actions.yml b/changelogs/unreleased/22604-manual-actions.yml
new file mode 100644
index 00000000000..7335e597292
--- /dev/null
+++ b/changelogs/unreleased/22604-manual-actions.yml
@@ -0,0 +1,4 @@
+---
+title: Resolve "Manual actions on pipeline graph"
+merge_request: 7931
+author:
diff --git a/changelogs/unreleased/22849-ci-build-ref-slug.yml b/changelogs/unreleased/22849-ci-build-ref-slug.yml
new file mode 100644
index 00000000000..b159ecca6d8
--- /dev/null
+++ b/changelogs/unreleased/22849-ci-build-ref-slug.yml
@@ -0,0 +1,4 @@
+---
+title: Introduce $CI_BUILD_REF_SLUG
+merge_request: 8072
+author:
diff --git a/changelogs/unreleased/22864-add-environment-slug.yml b/changelogs/unreleased/22864-add-environment-slug.yml
new file mode 100644
index 00000000000..f90f79337d5
--- /dev/null
+++ b/changelogs/unreleased/22864-add-environment-slug.yml
@@ -0,0 +1,4 @@
+---
+title: Add a slug to environments
+merge_request: 7983
+author:
diff --git a/changelogs/unreleased/22864-kubernetes-service.yml b/changelogs/unreleased/22864-kubernetes-service.yml
new file mode 100644
index 00000000000..ea1323cbeb0
--- /dev/null
+++ b/changelogs/unreleased/22864-kubernetes-service.yml
@@ -0,0 +1,4 @@
+---
+title: Introduce deployment services, starting with a KubernetesService
+merge_request: 7994
+author:
diff --git a/changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml b/changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml
deleted file mode 100644
index 99dbe4a32a0..00000000000
--- a/changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Moved Leave Project and Leave Group buttons to access_request_buttons from
- the settings dropdown
-merge_request: 7600
-author:
diff --git a/changelogs/unreleased/23573-sort-functionality-for-project-member.yml b/changelogs/unreleased/23573-sort-functionality-for-project-member.yml
new file mode 100644
index 00000000000..73de0a6351b
--- /dev/null
+++ b/changelogs/unreleased/23573-sort-functionality-for-project-member.yml
@@ -0,0 +1,4 @@
+---
+title: Add sorting functionality for group/project members
+merge_request: 7032
+author:
diff --git a/changelogs/unreleased/24803-change-cursor-for-ca-stages.yml b/changelogs/unreleased/24803-change-cursor-for-ca-stages.yml
new file mode 100644
index 00000000000..b9d84c0ce31
--- /dev/null
+++ b/changelogs/unreleased/24803-change-cursor-for-ca-stages.yml
@@ -0,0 +1,5 @@
+---
+title: Changed cursor icon to pointer when mousing over stages on the Cycle Analytics
+ pages
+merge_request:
+author: Ryan Harris
diff --git a/changelogs/unreleased/24824-dropdown-items-focus.yml b/changelogs/unreleased/24824-dropdown-items-focus.yml
new file mode 100644
index 00000000000..66970c2a9a5
--- /dev/null
+++ b/changelogs/unreleased/24824-dropdown-items-focus.yml
@@ -0,0 +1,4 @@
+---
+title: Add focus state to dropdown items
+merge_request:
+author:
diff --git a/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml b/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml
new file mode 100644
index 00000000000..cc7c2604824
--- /dev/null
+++ b/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml
@@ -0,0 +1,4 @@
+---
+title: Improve bulk assignment for issuables
+merge_request:
+author:
diff --git a/changelogs/unreleased/24927-custom-event-polyfill-test.yml b/changelogs/unreleased/24927-custom-event-polyfill-test.yml
new file mode 100644
index 00000000000..879c28a951e
--- /dev/null
+++ b/changelogs/unreleased/24927-custom-event-polyfill-test.yml
@@ -0,0 +1,4 @@
+---
+title: Adds tests for custom event polyfill
+merge_request: 7996
+author:
diff --git a/changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml b/changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml
new file mode 100644
index 00000000000..b8ba9391530
--- /dev/null
+++ b/changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml
@@ -0,0 +1,4 @@
+---
+title: Add image controls to MR diffs
+merge_request: 7919
+author:
diff --git a/changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml b/changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml
new file mode 100644
index 00000000000..62030d3fc45
--- /dev/null
+++ b/changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent user creating issue or MR without signing in for a group
+merge_request: 7902
+author:
diff --git a/changelogs/unreleased/25136-last-deployment-link.yml b/changelogs/unreleased/25136-last-deployment-link.yml
new file mode 100644
index 00000000000..eab1534aa66
--- /dev/null
+++ b/changelogs/unreleased/25136-last-deployment-link.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Latest deployment link is broken
+merge_request: 7839
+author:
diff --git a/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml b/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml
new file mode 100644
index 00000000000..531b0f83099
--- /dev/null
+++ b/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml
@@ -0,0 +1,4 @@
+---
+title: Improve help message for issue create slash command
+merge_request: 7850
+author:
diff --git a/changelogs/unreleased/25171-fix-mr-features-settings-hidden-when-builds-are-disabled.yml b/changelogs/unreleased/25171-fix-mr-features-settings-hidden-when-builds-are-disabled.yml
deleted file mode 100644
index a7576e2cbdb..00000000000
--- a/changelogs/unreleased/25171-fix-mr-features-settings-hidden-when-builds-are-disabled.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove wrong '.builds-feature' class from the MR settings fieldset
-merge_request: 7930
-author:
diff --git a/changelogs/unreleased/25207-text-overflow-env-table.yml b/changelogs/unreleased/25207-text-overflow-env-table.yml
new file mode 100644
index 00000000000..69348281a50
--- /dev/null
+++ b/changelogs/unreleased/25207-text-overflow-env-table.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent enviroment table to overflow when name has underscores
+merge_request: 8142
+author:
diff --git a/changelogs/unreleased/25301-git-2-11-force-push-bug.yml b/changelogs/unreleased/25301-git-2-11-force-push-bug.yml
new file mode 100644
index 00000000000..afe57729c48
--- /dev/null
+++ b/changelogs/unreleased/25301-git-2-11-force-push-bug.yml
@@ -0,0 +1,4 @@
+---
+title: Accept environment variables from the `pre-receive` script
+merge_request: 7967
+author:
diff --git a/changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml b/changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml
new file mode 100644
index 00000000000..b12eab26b67
--- /dev/null
+++ b/changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml
@@ -0,0 +1,4 @@
+---
+title: Ensure issuable state changes only fire webhooks once
+merge_request:
+author:
diff --git a/changelogs/unreleased/25482-fix-api-sudo.yml b/changelogs/unreleased/25482-fix-api-sudo.yml
new file mode 100644
index 00000000000..4c11fe1622e
--- /dev/null
+++ b/changelogs/unreleased/25482-fix-api-sudo.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Memoize the current_user so that sudo can work properly'
+merge_request: 8017
+author:
diff --git a/changelogs/unreleased/25483-broken-tabs.yml b/changelogs/unreleased/25483-broken-tabs.yml
new file mode 100644
index 00000000000..d6c92014bea
--- /dev/null
+++ b/changelogs/unreleased/25483-broken-tabs.yml
@@ -0,0 +1,4 @@
+---
+title: Fix TypeError: Cannot read property 'initTabs' on commit builds tab
+merge_request: 8009
+author:
diff --git a/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml b/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml
new file mode 100644
index 00000000000..c6a92547c5c
--- /dev/null
+++ b/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml
@@ -0,0 +1,4 @@
+---
+title: Added go back anchor on error pages.
+merge_request: 8087
+author:
diff --git a/changelogs/unreleased/25617-todos-filter-placeholder.yml b/changelogs/unreleased/25617-todos-filter-placeholder.yml
new file mode 100644
index 00000000000..5d0adb04ef3
--- /dev/null
+++ b/changelogs/unreleased/25617-todos-filter-placeholder.yml
@@ -0,0 +1,4 @@
+---
+title: 25617 Fix placeholder color of todo filters
+merge_request:
+author:
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/25743-clean-up-css-for-project-alerts-and-flash-notifications.yml b/changelogs/unreleased/25743-clean-up-css-for-project-alerts-and-flash-notifications.yml
new file mode 100644
index 00000000000..0a81124de0d
--- /dev/null
+++ b/changelogs/unreleased/25743-clean-up-css-for-project-alerts-and-flash-notifications.yml
@@ -0,0 +1,4 @@
+---
+title: fix colors and margins for adjacent alert banners
+merge_request: 8151
+author:
diff --git a/changelogs/unreleased/8003-katex-math.yml b/changelogs/unreleased/8003-katex-math.yml
new file mode 100644
index 00000000000..a40dcde1393
--- /dev/null
+++ b/changelogs/unreleased/8003-katex-math.yml
@@ -0,0 +1,4 @@
+---
+title: Added support for math rendering, using KaTeX, in Markdown and asciidoc
+merge_request: 8003
+author: Munken
diff --git a/changelogs/unreleased/abuse_report-fixture.yml b/changelogs/unreleased/abuse_report-fixture.yml
new file mode 100644
index 00000000000..47478a2048b
--- /dev/null
+++ b/changelogs/unreleased/abuse_report-fixture.yml
@@ -0,0 +1,4 @@
+---
+title: Replace static fixture for abuse_reports_spec
+merge_request: 7644
+author: winniehell
diff --git a/changelogs/unreleased/add_info_to_qr.yml b/changelogs/unreleased/add_info_to_qr.yml
new file mode 100644
index 00000000000..a4b0354a9c9
--- /dev/null
+++ b/changelogs/unreleased/add_info_to_qr.yml
@@ -0,0 +1,4 @@
+---
+title: Add GitLab host to 2FA QR code and manual info
+merge_request: 6941
+author:
diff --git a/changelogs/unreleased/allow-more-filenames.yml b/changelogs/unreleased/allow-more-filenames.yml
new file mode 100644
index 00000000000..7989f94e528
--- /dev/null
+++ b/changelogs/unreleased/allow-more-filenames.yml
@@ -0,0 +1,4 @@
+---
+title: Allow all alphanumeric characters in file names
+merge_request: 8002
+author: winniehell
diff --git a/changelogs/unreleased/api-cherry-pick.yml b/changelogs/unreleased/api-cherry-pick.yml
new file mode 100644
index 00000000000..5f4cee450b9
--- /dev/null
+++ b/changelogs/unreleased/api-cherry-pick.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Ability to cherry pick a commit'
+merge_request: 8047
+author: Robert Schilling
diff --git a/changelogs/unreleased/api-simple-group-project.yml b/changelogs/unreleased/api-simple-group-project.yml
new file mode 100644
index 00000000000..54c8de610a6
--- /dev/null
+++ b/changelogs/unreleased/api-simple-group-project.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Simple representation of group''s projects'
+merge_request: 8060
+author: Robert Schilling
diff --git a/changelogs/unreleased/awards_handler.yml b/changelogs/unreleased/awards_handler.yml
new file mode 100644
index 00000000000..1f9904c0691
--- /dev/null
+++ b/changelogs/unreleased/awards_handler.yml
@@ -0,0 +1,4 @@
+---
+title: Replace static fixture for awards_handler_spec
+merge_request: 7661
+author: winniehell
diff --git a/changelogs/unreleased/bitbucket-oauth2.yml b/changelogs/unreleased/bitbucket-oauth2.yml
new file mode 100644
index 00000000000..97d82518b7b
--- /dev/null
+++ b/changelogs/unreleased/bitbucket-oauth2.yml
@@ -0,0 +1,4 @@
+---
+title: Refactor Bitbucket importer to use BitBucket API Version 2
+merge_request:
+author:
diff --git a/changelogs/unreleased/change_development_build_fixtures.yml b/changelogs/unreleased/change_development_build_fixtures.yml
new file mode 100644
index 00000000000..b5dc3792745
--- /dev/null
+++ b/changelogs/unreleased/change_development_build_fixtures.yml
@@ -0,0 +1,4 @@
+---
+title: Ci::Builds have same ref as Ci::Pipeline in dev fixtures
+merge_request:
+author: twonegatives
diff --git a/changelogs/unreleased/chomp-git-status-message.yml b/changelogs/unreleased/chomp-git-status-message.yml
new file mode 100644
index 00000000000..f70607df7a1
--- /dev/null
+++ b/changelogs/unreleased/chomp-git-status-message.yml
@@ -0,0 +1,5 @@
+---
+title: For single line git commit messages, the close quote should be on the same
+ line as the open quote
+merge_request:
+author:
diff --git a/changelogs/unreleased/dockerfile-templates.yml b/changelogs/unreleased/dockerfile-templates.yml
new file mode 100644
index 00000000000..e4db46cdf9a
--- /dev/null
+++ b/changelogs/unreleased/dockerfile-templates.yml
@@ -0,0 +1,4 @@
+---
+title: Add support for Dockerfile templates
+merge_request: 7247
+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/expose-deployment-variables.yml b/changelogs/unreleased/expose-deployment-variables.yml
new file mode 100644
index 00000000000..7663d5b6ae5
--- /dev/null
+++ b/changelogs/unreleased/expose-deployment-variables.yml
@@ -0,0 +1,4 @@
+---
+title: Pass variables from deployment project services to CI runner
+merge_request: 8107
+author:
diff --git a/changelogs/unreleased/file-template-dropwdown-proper-position.yml b/changelogs/unreleased/file-template-dropwdown-proper-position.yml
new file mode 100644
index 00000000000..cf2a622b7e6
--- /dev/null
+++ b/changelogs/unreleased/file-template-dropwdown-proper-position.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed file template dropdown for the "New File" editor for smaller/zoomed screens
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-blame-500.yml b/changelogs/unreleased/fix-blame-500.yml
new file mode 100644
index 00000000000..379d81aaa44
--- /dev/null
+++ b/changelogs/unreleased/fix-blame-500.yml
@@ -0,0 +1,4 @@
+---
+title: Fix blame 500 error on invalid path.
+merge_request: 25761
+author: Jeff Stubler
diff --git a/changelogs/unreleased/fix-import-export-build-token.yml b/changelogs/unreleased/fix-import-export-build-token.yml
new file mode 100644
index 00000000000..622487e6829
--- /dev/null
+++ b/changelogs/unreleased/fix-import-export-build-token.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Import/Export duplicated builds error
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-import-export-ee-services.yml b/changelogs/unreleased/fix-import-export-ee-services.yml
new file mode 100644
index 00000000000..c0aacbc96f8
--- /dev/null
+++ b/changelogs/unreleased/fix-import-export-ee-services.yml
@@ -0,0 +1,4 @@
+---
+title: Fix missing service error importing from EE to CE
+merge_request: 8144
+author:
diff --git a/changelogs/unreleased/fix-import-export-mr-error.yml b/changelogs/unreleased/fix-import-export-mr-error.yml
new file mode 100644
index 00000000000..e1137bca131
--- /dev/null
+++ b/changelogs/unreleased/fix-import-export-mr-error.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Import/Export merge requests error while importing
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-milestone-summary.yml b/changelogs/unreleased/fix-milestone-summary.yml
deleted file mode 100644
index 3045a15054c..00000000000
--- a/changelogs/unreleased/fix-milestone-summary.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Displays milestone remaining days only when it's present
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-slack-pipeline-message-by-api.yml b/changelogs/unreleased/fix-slack-pipeline-message-by-api.yml
new file mode 100644
index 00000000000..aa5ad5cd8d6
--- /dev/null
+++ b/changelogs/unreleased/fix-slack-pipeline-message-by-api.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Slack pipeline message from pipelines made by API
+merge_request: 8059
+author:
diff --git a/changelogs/unreleased/fix-yaml-variables.yml b/changelogs/unreleased/fix-yaml-variables.yml
new file mode 100644
index 00000000000..3abff1e3b08
--- /dev/null
+++ b/changelogs/unreleased/fix-yaml-variables.yml
@@ -0,0 +1,4 @@
+---
+title: Convert CI YAML variables keys into strings
+merge_request: 8088
+author:
diff --git a/changelogs/unreleased/gem-update-grape.yml b/changelogs/unreleased/gem-update-grape.yml
new file mode 100644
index 00000000000..46b6702d9fd
--- /dev/null
+++ b/changelogs/unreleased/gem-update-grape.yml
@@ -0,0 +1,4 @@
+---
+title: 'Gem update: Update grape to 0.18.0'
+merge_request:
+author: Robert Schilling
diff --git a/changelogs/unreleased/gitlab-workhorse-multipart.yml b/changelogs/unreleased/gitlab-workhorse-multipart.yml
new file mode 100644
index 00000000000..23c2139cf93
--- /dev/null
+++ b/changelogs/unreleased/gitlab-workhorse-multipart.yml
@@ -0,0 +1,4 @@
+---
+title: Replace Rack::Multipart with GitLab-Workhorse based solution
+merge_request: 5867
+author:
diff --git a/changelogs/unreleased/group-members-in-project-members-view.yml b/changelogs/unreleased/group-members-in-project-members-view.yml
deleted file mode 100644
index 415e2b6b1e2..00000000000
--- a/changelogs/unreleased/group-members-in-project-members-view.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Shows group members in project members list
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue_13270.yml b/changelogs/unreleased/issue_13270.yml
new file mode 100644
index 00000000000..9c15c436876
--- /dev/null
+++ b/changelogs/unreleased/issue_13270.yml
@@ -0,0 +1,4 @@
+---
+title: Allow to delete tag release note
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_22269.yml b/changelogs/unreleased/issue_22269.yml
new file mode 100644
index 00000000000..6b7164aff77
--- /dev/null
+++ b/changelogs/unreleased/issue_22269.yml
@@ -0,0 +1,4 @@
+---
+title: Create mattermost service
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_24020.yml b/changelogs/unreleased/issue_24020.yml
deleted file mode 100644
index 87310b75296..00000000000
--- a/changelogs/unreleased/issue_24020.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: "fix display hook error message"
-merge_request: 7775
-author: basyura
diff --git a/changelogs/unreleased/issue_25030.yml b/changelogs/unreleased/issue_25030.yml
deleted file mode 100644
index e18b8d6a79b..00000000000
--- a/changelogs/unreleased/issue_25030.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow branch names with dots on API endpoint
-merge_request:
-author:
diff --git a/changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml b/changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml
new file mode 100644
index 00000000000..db1389e2024
--- /dev/null
+++ b/changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml
@@ -0,0 +1,4 @@
+---
+title: Issue#visible_to_user moved to IssuesFinder to prevent accidental use
+merge_request:
+author:
diff --git a/changelogs/unreleased/jej-note-search-uses-finder.yml b/changelogs/unreleased/jej-note-search-uses-finder.yml
new file mode 100644
index 00000000000..1768bdfd487
--- /dev/null
+++ b/changelogs/unreleased/jej-note-search-uses-finder.yml
@@ -0,0 +1,4 @@
+---
+title: Fix missing Note access checks by moving Note#search to updated NoteFinder
+merge_request:
+author:
diff --git a/changelogs/unreleased/ldap_maint_task.yml b/changelogs/unreleased/ldap_maint_task.yml
new file mode 100644
index 00000000000..8acffba0ce5
--- /dev/null
+++ b/changelogs/unreleased/ldap_maint_task.yml
@@ -0,0 +1,4 @@
+---
+title: Add LDAP Rake task to rename a provider
+merge_request: 2181
+author:
diff --git a/changelogs/unreleased/leave-project-btn.yml b/changelogs/unreleased/leave-project-btn.yml
new file mode 100644
index 00000000000..2aa553d7b97
--- /dev/null
+++ b/changelogs/unreleased/leave-project-btn.yml
@@ -0,0 +1,4 @@
+---
+title: Move all action buttons to project header
+merge_request:
+author:
diff --git a/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml b/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml
new file mode 100644
index 00000000000..11250643a23
--- /dev/null
+++ b/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml
@@ -0,0 +1,4 @@
+---
+title: Move admin active tab spinach tests to rspec
+merge_request: 8037
+author: Semyon Pupkov
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/pipeline-build-hitbox.yml b/changelogs/unreleased/pipeline-build-hitbox.yml
new file mode 100644
index 00000000000..051b538a9a3
--- /dev/null
+++ b/changelogs/unreleased/pipeline-build-hitbox.yml
@@ -0,0 +1,4 @@
+---
+title: Make CI badge hitboxes match parent
+merge_request:
+author:
diff --git a/changelogs/unreleased/process-commit-worker-migration-encoding.yml b/changelogs/unreleased/process-commit-worker-migration-encoding.yml
new file mode 100644
index 00000000000..26aabd9b647
--- /dev/null
+++ b/changelogs/unreleased/process-commit-worker-migration-encoding.yml
@@ -0,0 +1,4 @@
+---
+title: Encode input when migrating ProcessCommitWorker jobs to prevent migration errors
+merge_request:
+author:
diff --git a/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml b/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml
new file mode 100644
index 00000000000..754af641add
--- /dev/null
+++ b/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml
@@ -0,0 +1,4 @@
+---
+title: Remove unnecessary commits order message
+merge_request: 8004
+author:
diff --git a/changelogs/unreleased/rounded-labels-fixes.yml b/changelogs/unreleased/rounded-labels-fixes.yml
new file mode 100644
index 00000000000..e0fbc6e3b5a
--- /dev/null
+++ b/changelogs/unreleased/rounded-labels-fixes.yml
@@ -0,0 +1,4 @@
+---
+title: Additional rounded label fixes
+merge_request:
+author:
diff --git a/changelogs/unreleased/seed-runner-token.yml b/changelogs/unreleased/seed-runner-token.yml
new file mode 100644
index 00000000000..e8153be043a
--- /dev/null
+++ b/changelogs/unreleased/seed-runner-token.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for setting the GitLab Runners Registration Token during initial
+ database seeding
+merge_request: 6642
+author:
diff --git a/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml b/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml
new file mode 100644
index 00000000000..bbd7a217493
--- /dev/null
+++ b/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml
@@ -0,0 +1,4 @@
+---
+title: Show commit status from latest pipeline
+merge_request: 7333
+author:
diff --git a/changelogs/unreleased/timeago-perf-fix.yml b/changelogs/unreleased/timeago-perf-fix.yml
deleted file mode 100644
index 265e7db29a9..00000000000
--- a/changelogs/unreleased/timeago-perf-fix.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed timeago re-rendering every timeago
-merge_request:
-author:
diff --git a/changelogs/unreleased/unescape-relative-path.yml b/changelogs/unreleased/unescape-relative-path.yml
deleted file mode 100644
index 755b0379a16..00000000000
--- a/changelogs/unreleased/unescape-relative-path.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Avoid escaping relative links in Markdown twice
-merge_request: 7940
-author: winniehell
diff --git a/changelogs/unreleased/username-exists-root.yml b/changelogs/unreleased/username-exists-root.yml
new file mode 100644
index 00000000000..1ffb3eb435c
--- /dev/null
+++ b/changelogs/unreleased/username-exists-root.yml
@@ -0,0 +1,4 @@
+---
+title: Username exists check respects relative root path
+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 0aa2873f94a..057d60ca869 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -45,7 +45,7 @@ module Gitlab
#
# Parameters filtered:
# - Password (:password, :password_confirmation)
- # - Private tokens (:private_token, :authentication_token)
+ # - Private tokens
# - Two-factor tokens (:otp_attempt)
# - Repo/Project Import URLs (:import_url)
# - Build variables (:variables)
@@ -60,11 +60,13 @@ module Gitlab
encrypted_key
hook
import_url
+ incoming_email_token
key
otp_attempt
password
password_confirmation
private_token
+ runners_token
secret_token
sentry_dsn
variables
@@ -85,6 +87,9 @@ module Gitlab
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"
@@ -98,6 +103,7 @@ 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"
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 327e4a7937c..b8b41a0d86c 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -153,6 +153,12 @@ production: &base
# The location where LFS objects are stored (default: shared/lfs-objects).
# storage_path: shared/lfs-objects
+ ## Mattermost
+ ## For enabling Add to Mattermost button
+ mattermost:
+ enabled: false
+ host: 'https://mattermost.example.com'
+
## Gravatar
## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html
gravatar:
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 9ddd1554811..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'))
@@ -262,6 +262,13 @@ Settings.lfs['enabled'] = true if Settings.lfs['enabled'].nil?
Settings.lfs['storage_path'] = File.expand_path(Settings.lfs['storage_path'] || File.join(Settings.shared['path'], "lfs-objects"), Rails.root)
#
+# Mattermost
+#
+Settings['mattermost'] ||= Settingslogic.new({})
+Settings.mattermost['enabled'] = false if Settings.mattermost['enabled'].nil?
+Settings.mattermost['host'] = nil unless Settings.mattermost.enabled
+
+#
# Gravatar
#
Settings['gravatar'] ||= Settingslogic.new({})
@@ -302,7 +309,7 @@ Settings.cron_jobs['remove_expired_group_links_worker'] ||= Settingslogic.new({}
Settings.cron_jobs['remove_expired_group_links_worker']['cron'] ||= '10 0 * * *'
Settings.cron_jobs['remove_expired_group_links_worker']['job_class'] = 'RemoveExpiredGroupLinksWorker'
Settings.cron_jobs['prune_old_events_worker'] ||= Settingslogic.new({})
-Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '* */6 * * *'
+Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '0 */6 * * *'
Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWorker'
Settings.cron_jobs['trending_projects_worker'] ||= Settingslogic.new({})
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index fc4b0a72add..88cd0f5f652 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -52,8 +52,8 @@ Doorkeeper.configure do
# Define access token scopes for your provider
# For more information go to
# https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes
- default_scopes :api
- # optional_scopes :write, :update
+ default_scopes(*Gitlab::Auth::DEFAULT_SCOPES)
+ optional_scopes(*Gitlab::Auth::OPTIONAL_SCOPES)
# Change the way client credentials are retrieved from the request object.
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
diff --git a/config/initializers/math_lexer.rb b/config/initializers/math_lexer.rb
new file mode 100644
index 00000000000..8a3388a267e
--- /dev/null
+++ b/config/initializers/math_lexer.rb
@@ -0,0 +1,2 @@
+# Touch the lexers so it is registered with Rouge
+Rouge::Lexers::Math
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
index 26c30e523a7..ab5a0561b8c 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -26,3 +26,9 @@ if Gitlab.config.omniauth.enabled
end
end
end
+
+module OmniAuth
+ module Strategies
+ autoload :Bitbucket, Rails.root.join('lib', 'omniauth', 'strategies', 'bitbucket')
+ end
+end
diff --git a/config/initializers/public_key.rb b/config/initializers/public_key.rb
deleted file mode 100644
index e4f09a2d020..00000000000
--- a/config/initializers/public_key.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-path = File.expand_path("~/.ssh/bitbucket_rsa.pub")
-Gitlab::BitbucketImport.public_key = File.read(path) if File.exist?(path)
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 1d7a3f03ace..5a7365bb0f6 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -34,7 +34,7 @@ Sidekiq.configure_server do |config|
# Database pool should be at least `sidekiq_concurrency` + 2
# For more info, see: https://github.com/mperham/sidekiq/blob/master/4.0-Upgrade.md
config = ActiveRecord::Base.configurations[Rails.env] ||
- Rails.application.config.database_configuration[Rails.env]
+ Rails.application.config.database_configuration[Rails.env]
config['pool'] = Sidekiq.options[:concurrency] + 2
ActiveRecord::Base.establish_connection(config)
Rails.logger.debug("Connection Pool size for Sidekiq Server is now: #{ActiveRecord::Base.connection.pool.instance_variable_get('@size')}")
diff --git a/config/initializers/workhorse_multipart.rb b/config/initializers/workhorse_multipart.rb
new file mode 100644
index 00000000000..3e2f25c354a
--- /dev/null
+++ b/config/initializers/workhorse_multipart.rb
@@ -0,0 +1,3 @@
+Rails.application.configure do |config|
+ config.middleware.use(Gitlab::Middleware::Multipart)
+end
diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml
index a4032a21420..1d728282d90 100644
--- a/config/locales/doorkeeper.en.yml
+++ b/config/locales/doorkeeper.en.yml
@@ -59,6 +59,7 @@ en:
unknown: "The access token is invalid"
scopes:
api: Access your API
+ read_user: Read user information
flash:
applications:
diff --git a/config/routes.rb b/config/routes.rb
index 03b47261e7e..06d565df469 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,6 +1,5 @@
require 'sidekiq/web'
require 'sidekiq/cron/web'
-require 'api/api'
require 'constraints/group_url_constrainer'
Rails.application.routes.draw do
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 5ae985da561..0dd2c8f7aef 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -28,9 +28,19 @@ namespace :admin do
resources :applications
- resources :groups, constraints: { id: /[^\/]+/ } do
- member do
+ resources :groups, only: [:index, :new, :create]
+
+ scope(path: 'groups/*id',
+ controller: :groups,
+ constraints: { id: Gitlab::Regex.namespace_route_regex }) do
+
+ scope(as: :group) do
put :members_update
+ get :edit, action: :edit
+ get '/', action: :show
+ patch '/', action: :update
+ put '/', action: :update
+ delete '/', action: :destroy
end
end
@@ -50,14 +60,13 @@ namespace :admin do
resource :system_info, controller: 'system_info', only: [:show]
resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ }
- resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
- root to: 'projects#index', as: :projects
+ resources :projects, only: [:index]
+ scope(path: 'projects/*namespace_id', as: :namespace) do
resources(:projects,
path: '/',
- constraints: { id: /[a-zA-Z.0-9_\-]+/ },
- only: [:index, :show]) do
- root to: 'projects#show'
+ constraints: { id: Gitlab::Regex.project_route_regex },
+ only: [:show]) do
member do
put :transfer
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 0754f0ec3b0..80cc47ab9a8 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -11,6 +11,18 @@ constraints(ProjectUrlConstrainer.new) do
module: :projects,
as: :project) do
+ resources :autocomplete_sources, only: [] do
+ collection do
+ get 'emojis'
+ get 'members'
+ get 'issues'
+ get 'merge_requests'
+ get 'labels'
+ get 'milestones'
+ get 'commands'
+ end
+ end
+
#
# Templates
#
@@ -136,6 +148,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
@@ -316,7 +330,6 @@ constraints(ProjectUrlConstrainer.new) do
post :remove_export
post :generate_new_export
get :download_export
- get :autocomplete_sources
get :activity
get :refs
put :new_issue_address
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/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb
index 08ad3097d34..be95d788850 100644
--- a/db/fixtures/development/14_pipelines.rb
+++ b/db/fixtures/development/14_pipelines.rb
@@ -1,26 +1,50 @@
class Gitlab::Seeder::Pipelines
STAGES = %w[build test deploy notify]
BUILDS = [
- { name: 'build:linux', stage: 'build', status: :success },
- { name: 'build:osx', stage: 'build', status: :success },
- { name: 'rspec:linux 0 3', stage: 'test', status: :success },
- { name: 'rspec:linux 1 3', stage: 'test', status: :success },
- { name: 'rspec:linux 2 3', stage: 'test', status: :success },
- { name: 'rspec:windows 0 3', stage: 'test', status: :success },
- { name: 'rspec:windows 1 3', stage: 'test', status: :success },
- { name: 'rspec:windows 2 3', stage: 'test', status: :success },
- { name: 'rspec:windows 2 3', stage: 'test', status: :success },
- { name: 'rspec:osx', stage: 'test', status_event: :success },
- { name: 'spinach:linux', stage: 'test', status: :success },
- { name: 'spinach:osx', stage: 'test', status: :failed, allow_failure: true},
- { name: 'env:alpha', stage: 'deploy', environment: 'alpha', status: :pending },
- { name: 'env:beta', stage: 'deploy', environment: 'beta', status: :running },
- { name: 'env:gamma', stage: 'deploy', environment: 'gamma', status: :canceled },
- { name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success, options: { environment: { on_stop: 'stop staging' } } },
- { name: 'stop staging', stage: 'deploy', environment: 'staging', when: 'manual', status: :skipped },
- { name: 'production', stage: 'deploy', environment: 'production', when: 'manual', status: :skipped },
+ # build stage
+ { name: 'build:linux', stage: 'build', status: :success,
+ queued_at: 10.hour.ago, started_at: 9.hour.ago, finished_at: 8.hour.ago },
+ { name: 'build:osx', stage: 'build', status: :success,
+ queued_at: 10.hour.ago, started_at: 10.hour.ago, finished_at: 9.hour.ago },
+
+ # test stage
+ { name: 'rspec:linux 0 3', stage: 'test', status: :success,
+ queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago },
+ { name: 'rspec:linux 1 3', stage: 'test', status: :success,
+ queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago },
+ { name: 'rspec:linux 2 3', stage: 'test', status: :success,
+ queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago },
+ { name: 'rspec:windows 0 3', stage: 'test', status: :success,
+ queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago },
+ { name: 'rspec:windows 1 3', stage: 'test', status: :success,
+ queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago },
+ { name: 'rspec:windows 2 3', stage: 'test', status: :success,
+ queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago },
+ { name: 'rspec:windows 2 3', stage: 'test', status: :success,
+ queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago },
+ { name: 'rspec:osx', stage: 'test', status_event: :success,
+ queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago },
+ { name: 'spinach:linux', stage: 'test', status: :success,
+ queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago },
+ { name: 'spinach:osx', stage: 'test', status: :failed, allow_failure: true,
+ queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago },
+
+ # deploy stage
+ { name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success,
+ options: { environment: { action: 'start', on_stop: 'stop staging' } },
+ queued_at: 7.hour.ago, started_at: 6.hour.ago, finished_at: 4.hour.ago },
+ { name: 'stop staging', stage: 'deploy', environment: 'staging',
+ when: 'manual', status: :skipped },
+ { name: 'production', stage: 'deploy', environment: 'production',
+ when: 'manual', status: :skipped },
+
+ # notify stage
{ name: 'slack', stage: 'notify', when: 'manual', status: :created },
]
+ EXTERNAL_JOBS = [
+ { name: 'jenkins', stage: 'test', status: :success,
+ queued_at: 7.hour.ago, started_at: 6.hour.ago, finished_at: 4.hour.ago },
+ ]
def initialize(project)
@project = project
@@ -30,11 +54,12 @@ class Gitlab::Seeder::Pipelines
pipelines.each do |pipeline|
begin
BUILDS.each { |opts| build_create!(pipeline, opts) }
- commit_status_create!(pipeline, name: 'jenkins', stage: 'test', status: :success)
+ EXTERNAL_JOBS.each { |opts| commit_status_create!(pipeline, opts) }
print '.'
rescue ActiveRecord::RecordInvalid
print 'F'
ensure
+ pipeline.update_duration
pipeline.update_status
end
end
@@ -115,7 +140,7 @@ class Gitlab::Seeder::Pipelines
def job_attributes(pipeline, opts)
{ name: 'test build', stage: 'test', stage_idx: stage_index(opts[:stage]),
- ref: 'master', tag: false, user: build_user, project: @project, pipeline: pipeline,
+ ref: pipeline.ref, tag: false, user: build_user, project: @project, pipeline: pipeline,
created_at: Time.now, updated_at: Time.now
}.merge(opts)
end
diff --git a/db/fixtures/production/010_settings.rb b/db/fixtures/production/010_settings.rb
new file mode 100644
index 00000000000..5522f31629a
--- /dev/null
+++ b/db/fixtures/production/010_settings.rb
@@ -0,0 +1,16 @@
+if ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN'].present?
+ settings = ApplicationSetting.current || ApplicationSetting.create_from_defaults
+ settings.set_runners_registration_token(ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN'])
+
+ if settings.save
+ puts "Saved Runner Registration Token".color(:green)
+ else
+ puts "Could not save Runner Registration Token".color(:red)
+ puts
+ settings.errors.full_messages.map do |message|
+ puts "--> #{message}".color(:red)
+ end
+ puts
+ exit 1
+ end
+end
diff --git a/db/migrate/20141006143943_move_slack_service_to_webhook.rb b/db/migrate/20141006143943_move_slack_service_to_webhook.rb
index 8cb120f7007..42e88d6d6e3 100644
--- a/db/migrate/20141006143943_move_slack_service_to_webhook.rb
+++ b/db/migrate/20141006143943_move_slack_service_to_webhook.rb
@@ -1,7 +1,11 @@
# rubocop:disable all
class MoveSlackServiceToWebhook < ActiveRecord::Migration
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Move old fields "token" and "subdomain" to one single field "webhook"'
+
def change
- SlackService.all.each do |slack_service|
+ SlackNotificationService.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/20160823083941_add_column_scopes_to_personal_access_tokens.rb b/db/migrate/20160823083941_add_column_scopes_to_personal_access_tokens.rb
new file mode 100644
index 00000000000..91479de840b
--- /dev/null
+++ b/db/migrate/20160823083941_add_column_scopes_to_personal_access_tokens.rb
@@ -0,0 +1,19 @@
+# The default needs to be `[]`, but all existing access tokens need to have `scopes` set to `['api']`.
+# It's easier to achieve this by adding the column with the `['api']` default, and then changing the default to
+# `[]`.
+
+class AddColumnScopesToPersonalAccessTokens < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :personal_access_tokens, :scopes, :string, default: ['api'].to_yaml
+ end
+
+ def down
+ remove_column :personal_access_tokens, :scopes
+ end
+end
diff --git a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
index 453a44e271a..77e0c40d850 100644
--- a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
+++ b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
@@ -47,14 +47,14 @@ class MigrateProcessCommitWorkerJobs < ActiveRecord::Migration
hash = {
id: commit.oid,
- message: commit.message,
+ message: encode(commit.message),
parent_ids: commit.parent_ids,
authored_date: commit.author[:time],
- author_name: commit.author[:name],
- author_email: commit.author[:email],
+ author_name: encode(commit.author[:name]),
+ author_email: encode(commit.author[:email]),
committed_date: commit.committer[:time],
- committer_email: commit.committer[:email],
- committer_name: commit.committer[:name]
+ committer_email: encode(commit.committer[:email]),
+ committer_name: encode(commit.committer[:name])
}
payload['args'][2] = hash
@@ -89,4 +89,14 @@ class MigrateProcessCommitWorkerJobs < ActiveRecord::Migration
end
end
end
+
+ def encode(data)
+ encoding = Encoding::UTF_8
+
+ if data.encoding == encoding
+ data
+ else
+ data.encode(encoding, invalid: :replace, undef: :replace)
+ end
+ end
end
diff --git a/db/migrate/20161130095245_fill_routes_table.rb b/db/migrate/20161130095245_fill_routes_table.rb
index 6754e583000..c3536d6d911 100644
--- a/db/migrate/20161130095245_fill_routes_table.rb
+++ b/db/migrate/20161130095245_fill_routes_table.rb
@@ -16,6 +16,6 @@ class FillRoutesTable < ActiveRecord::Migration
end
def down
- Route.delete_all(source_type: 'Namespace')
+ execute("DELETE FROM routes WHERE source_type = 'Namespace'")
end
end
diff --git a/db/migrate/20161130101252_fill_projects_routes_table.rb b/db/migrate/20161130101252_fill_projects_routes_table.rb
index 14700583be5..56ba6fcdbe3 100644
--- a/db/migrate/20161130101252_fill_projects_routes_table.rb
+++ b/db/migrate/20161130101252_fill_projects_routes_table.rb
@@ -8,15 +8,23 @@ class FillProjectsRoutesTable < ActiveRecord::Migration
DOWNTIME_REASON = 'No new projects should be created during data copy'
def up
- execute <<-EOF
- INSERT INTO routes
- (source_id, source_type, path)
- (SELECT projects.id, 'Project', concat(namespaces.path, '/', projects.path) FROM projects
- INNER JOIN namespaces ON projects.namespace_id = namespaces.id)
- EOF
+ if Gitlab::Database.postgresql?
+ execute <<-EOF
+ INSERT INTO routes (source_id, source_type, path)
+ (SELECT DISTINCT ON (namespaces.path, projects.path) projects.id, 'Project', concat(namespaces.path, '/', projects.path)
+ FROM projects INNER JOIN namespaces ON projects.namespace_id = namespaces.id
+ ORDER BY namespaces.path, projects.path, projects.id DESC)
+ EOF
+ else
+ execute <<-EOF
+ INSERT INTO routes (source_id, source_type, path)
+ (SELECT projects.id, 'Project', concat(namespaces.path, '/', projects.path)
+ FROM projects INNER JOIN namespaces ON projects.namespace_id = namespaces.id)
+ EOF
+ end
end
def down
- Route.delete_all(source_type: 'Project')
+ execute("DELETE FROM routes WHERE source_type = 'Project'")
end
end
diff --git a/db/migrate/20161202152031_remove_duplicates_from_routes.rb b/db/migrate/20161202152031_remove_duplicates_from_routes.rb
index 510796e05f2..d73b0847506 100644
--- a/db/migrate/20161202152031_remove_duplicates_from_routes.rb
+++ b/db/migrate/20161202152031_remove_duplicates_from_routes.rb
@@ -7,20 +7,21 @@ class RemoveDuplicatesFromRoutes < ActiveRecord::Migration
DOWNTIME = false
def up
- select_all("SELECT path FROM #{quote_table_name(:routes)} GROUP BY path HAVING COUNT(*) > 1").each do |row|
- path = connection.quote(row['path'])
- execute(%Q{
- DELETE FROM #{quote_table_name(:routes)}
- WHERE path = #{path}
- AND id != (
- SELECT id FROM (
- SELECT max(id) AS id
- FROM #{quote_table_name(:routes)}
- WHERE path = #{path}
- ) max_ids
- )
- })
- end
+ # We can skip this migration when running a PostgreSQL database because
+ # we use an optimized query in the "FillProjectsRoutesTable" migration
+ # to fill these values that avoid duplicate entries in the routes table.
+ return unless Gitlab::Database.mysql?
+
+ execute <<-EOF
+ DELETE duplicated_rows.*
+ FROM routes AS duplicated_rows
+ INNER JOIN (
+ SELECT path, MAX(id) as max_id
+ FROM routes
+ GROUP BY path
+ HAVING COUNT(*) > 1
+ ) AS good_rows ON good_rows.path = duplicated_rows.path AND good_rows.max_id <> duplicated_rows.id;
+ EOF
end
def down
diff --git a/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb b/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb
new file mode 100644
index 00000000000..2977917f2d1
--- /dev/null
+++ b/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb
@@ -0,0 +1,36 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveUniqPathIndexFromNamespace < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ constraint_name = 'namespaces_path_key'
+
+ transaction do
+ if index_exists?(:namespaces, :path)
+ remove_index(:namespaces, :path)
+ end
+
+ # In some bizarre cases PostgreSQL might have a separate unique constraint
+ # that we'll need to drop.
+ if constraint_exists?(constraint_name) && Gitlab::Database.postgresql?
+ execute("ALTER TABLE namespaces DROP CONSTRAINT IF EXISTS #{constraint_name};")
+ end
+ end
+ end
+
+ def down
+ unless index_exists?(:namespaces, :path)
+ add_concurrent_index(:namespaces, :path, unique: true)
+ end
+ end
+
+ def constraint_exists?(name)
+ indexes(:namespaces).map(&:name).include?(name)
+ end
+end
diff --git a/db/migrate/20161206153751_add_path_index_to_namespace.rb b/db/migrate/20161206153751_add_path_index_to_namespace.rb
new file mode 100644
index 00000000000..b0bac7d121e
--- /dev/null
+++ b/db/migrate/20161206153751_add_path_index_to_namespace.rb
@@ -0,0 +1,20 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddPathIndexToNamespace < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ add_concurrent_index :namespaces, :path
+ end
+
+ def down
+ if index_exists?(:namespaces, :path)
+ remove_index :namespaces, :path
+ end
+ end
+end
diff --git a/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb b/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb
new file mode 100644
index 00000000000..cc9d4974baa
--- /dev/null
+++ b/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb
@@ -0,0 +1,36 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveUniqNameIndexFromNamespace < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ constraint_name = 'namespaces_name_key'
+
+ transaction do
+ if index_exists?(:namespaces, :name)
+ remove_index(:namespaces, :name)
+ end
+
+ # In some bizarre cases PostgreSQL might have a separate unique constraint
+ # that we'll need to drop.
+ if constraint_exists?(constraint_name) && Gitlab::Database.postgresql?
+ execute("ALTER TABLE namespaces DROP CONSTRAINT IF EXISTS #{constraint_name};")
+ end
+ end
+ end
+
+ def down
+ unless index_exists?(:namespaces, :name)
+ add_concurrent_index(:namespaces, :name, unique: true)
+ end
+ end
+
+ def constraint_exists?(name)
+ indexes(:namespaces).map(&:name).include?(name)
+ end
+end
diff --git a/db/migrate/20161206153754_add_name_index_to_namespace.rb b/db/migrate/20161206153754_add_name_index_to_namespace.rb
new file mode 100644
index 00000000000..b3f3cb68a99
--- /dev/null
+++ b/db/migrate/20161206153754_add_name_index_to_namespace.rb
@@ -0,0 +1,20 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddNameIndexToNamespace < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ add_concurrent_index(:namespaces, [:name, :parent_id], unique: true)
+ end
+
+ def down
+ if index_exists?(:namespaces, [:name, :parent_id])
+ remove_index :namespaces, [:name, :parent_id]
+ end
+ end
+end
diff --git a/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb
new file mode 100644
index 00000000000..b74552e762d
--- /dev/null
+++ b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb
@@ -0,0 +1,53 @@
+class FixupEnvironmentNameUniqueness < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Renaming non-unique environments'
+
+ def up
+ environments = Arel::Table.new(:environments)
+
+ # Get all [project_id, name] pairs that occur more than once
+ finder_sql = environments.
+ group(environments[:project_id], environments[:name]).
+ having(Arel.sql("COUNT(1)").gt(1)).
+ project(environments[:project_id], environments[:name]).
+ to_sql
+
+ conflicting = connection.exec_query(finder_sql)
+
+ conflicting.rows.each do |project_id, name|
+ fix_duplicates(project_id, name)
+ end
+ end
+
+ def down
+ # Nothing to do
+ end
+
+ # Rename conflicting environments by appending "-#{id}" to all but the first
+ def fix_duplicates(project_id, name)
+ environments = Arel::Table.new(:environments)
+ finder_sql = environments.
+ where(environments[:project_id].eq(project_id)).
+ where(environments[:name].eq(name)).
+ order(environments[:id].asc).
+ project(environments[:id], environments[:name]).
+ to_sql
+
+ # Now we have the data for all the conflicting rows
+ conflicts = connection.exec_query(finder_sql).rows
+ conflicts.shift # Leave the first row alone
+
+ conflicts.each do |id, name|
+ update_sql =
+ Arel::UpdateManager.new(ActiveRecord::Base).
+ table(environments).
+ set(environments[:name] => name + "-" + id.to_s).
+ where(environments[:id].eq(id)).
+ to_sql
+
+ connection.exec_update(update_sql, self.class.name, [])
+ end
+ end
+end
diff --git a/db/migrate/20161207231621_create_environment_name_unique_index.rb b/db/migrate/20161207231621_create_environment_name_unique_index.rb
new file mode 100644
index 00000000000..ac680c8d10f
--- /dev/null
+++ b/db/migrate/20161207231621_create_environment_name_unique_index.rb
@@ -0,0 +1,18 @@
+class CreateEnvironmentNameUniqueIndex < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Making a non-unique index into a unique index'
+
+ def up
+ remove_index :environments, [:project_id, :name]
+ add_concurrent_index :environments, [:project_id, :name], unique: true
+ end
+
+ def down
+ remove_index :environments, [:project_id, :name], unique: true
+ add_concurrent_index :environments, [:project_id, :name]
+ end
+end
diff --git a/db/migrate/20161207231626_add_environment_slug.rb b/db/migrate/20161207231626_add_environment_slug.rb
new file mode 100644
index 00000000000..7153e6a32b1
--- /dev/null
+++ b/db/migrate/20161207231626_add_environment_slug.rb
@@ -0,0 +1,60 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddEnvironmentSlug < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Adding NOT NULL column environments.slug with dependent data'
+
+ # Used to generate random suffixes for the slug
+ NUMBERS = '0'..'9'
+ SUFFIX_CHARS = ('a'..'z').to_a + NUMBERS.to_a
+
+ def up
+ environments = Arel::Table.new(:environments)
+
+ add_column :environments, :slug, :string
+ finder = environments.project(:id, :name)
+
+ connection.exec_query(finder.to_sql).rows.each do |id, name|
+ updater = Arel::UpdateManager.new(ActiveRecord::Base).
+ table(environments).
+ set(environments[:slug] => generate_slug(name)).
+ where(environments[:id].eq(id))
+
+ connection.exec_update(updater.to_sql, self.class.name, [])
+ end
+
+ change_column_null :environments, :slug, false
+ end
+
+ def down
+ remove_column :environments, :slug
+ end
+
+ # Copy of the Environment#generate_slug implementation
+ def generate_slug(name)
+ # Lowercase letters and numbers only
+ slugified = name.to_s.downcase.gsub(/[^a-z0-9]/, '-')
+
+ # Must start with a letter
+ slugified = "env-" + slugified if NUMBERS.cover?(slugified[0])
+
+ # Maximum length: 24 characters (OpenShift limitation)
+ slugified = slugified[0..23]
+
+ # Cannot end with a "-" character (Kubernetes label limitation)
+ slugified = slugified[0..-2] if slugified[-1] == "-"
+
+ # Add a random suffix, shortening the current string if necessary, if it
+ # has been slugified. This ensures uniqueness.
+ slugified = slugified[0..16] + "-" + random_suffix if slugified != name
+
+ slugified
+ end
+
+ def random_suffix
+ (0..5).map { SUFFIX_CHARS.sample }.join
+ end
+end
diff --git a/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb
new file mode 100644
index 00000000000..e9fcef1cd45
--- /dev/null
+++ b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb
@@ -0,0 +1,15 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddUniqueIndexForEnvironmentSlug < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Adding a *unique* index to environments.slug'
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index :environments, [:project_id, :slug], unique: true
+ end
+end
diff --git a/db/migrate/20161212142807_add_lower_path_index_to_routes.rb b/db/migrate/20161212142807_add_lower_path_index_to_routes.rb
new file mode 100644
index 00000000000..6958500306f
--- /dev/null
+++ b/db/migrate/20161212142807_add_lower_path_index_to_routes.rb
@@ -0,0 +1,22 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddLowerPathIndexToRoutes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ return unless Gitlab::Database.postgresql?
+
+ execute 'CREATE INDEX CONCURRENTLY index_on_routes_lower_path ON routes (LOWER(path));'
+ end
+
+ def down
+ return unless Gitlab::Database.postgresql?
+
+ remove_index :routes, name: :index_on_routes_lower_path
+ end
+end
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
new file mode 100644
index 00000000000..a7278d7b5a6
--- /dev/null
+++ b/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb
@@ -0,0 +1,14 @@
+class ChangeSlackServiceToSlackNotificationService < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Rename SlackService to SlackNotificationService'
+
+ def up
+ execute("UPDATE services SET type = 'SlackNotificationService' WHERE type = 'SlackService'")
+ end
+
+ def down
+ execute("UPDATE services SET type = 'SlackService' WHERE type = 'SlackNotificationService'")
+ end
+end
diff --git a/db/post_migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb b/db/post_migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb
new file mode 100644
index 00000000000..7df561d82dd
--- /dev/null
+++ b/db/post_migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb
@@ -0,0 +1,19 @@
+# The default needs to be `[]`, but all existing access tokens need to have `scopes` set to `['api']`.
+# It's easier to achieve this by adding the column with the `['api']` default (regular migration), and
+# then changing the default to `[]` (in this post-migration).
+#
+# Details: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951#note_19721973
+
+class ChangePersonalAccessTokensDefaultBackToEmptyArray < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ change_column_default :personal_access_tokens, :scopes, [].to_yaml
+ end
+
+ def down
+ change_column_default :personal_access_tokens, :scopes, ['api'].to_yaml
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 63e52bccce8..eb893c8daef 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: 20161202152035) do
+ActiveRecord::Schema.define(version: 20161213172958) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -98,14 +98,14 @@ ActiveRecord::Schema.define(version: 20161202152035) do
t.text "help_page_text_html"
t.text "shared_runners_text_html"
t.text "after_sign_up_text_html"
+ t.boolean "sidekiq_throttling_enabled", default: false
+ t.string "sidekiq_throttling_queues"
+ t.decimal "sidekiq_throttling_factor"
t.boolean "housekeeping_enabled", default: true, null: false
t.boolean "housekeeping_bitmaps_enabled", default: true, null: false
t.integer "housekeeping_incremental_repack_period", default: 10, null: false
t.integer "housekeeping_full_repack_period", default: 50, null: false
t.integer "housekeeping_gc_period", default: 200, null: false
- t.boolean "sidekiq_throttling_enabled", default: false
- t.string "sidekiq_throttling_queues"
- t.decimal "sidekiq_throttling_factor"
t.boolean "html_emails_enabled", default: true
end
@@ -428,9 +428,11 @@ ActiveRecord::Schema.define(version: 20161202152035) do
t.string "external_url"
t.string "environment_type"
t.string "state", default: "available", null: false
+ t.string "slug", null: false
end
- add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree
+ add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", unique: true, using: :btree
+ add_index "environments", ["project_id", "slug"], name: "index_environments_on_project_id_and_slug", unique: true, using: :btree
create_table "events", force: :cascade do |t|
t.string "target_type"
@@ -738,18 +740,18 @@ ActiveRecord::Schema.define(version: 20161202152035) do
t.integer "visibility_level", default: 20, null: false
t.boolean "request_access_enabled", default: false, null: false
t.datetime "deleted_at"
- t.text "description_html"
t.boolean "lfs_enabled"
+ t.text "description_html"
t.integer "parent_id"
end
add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
add_index "namespaces", ["deleted_at"], name: "index_namespaces_on_deleted_at", using: :btree
- add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree
+ add_index "namespaces", ["name", "parent_id"], name: "index_namespaces_on_name_and_parent_id", unique: true, using: :btree
add_index "namespaces", ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
add_index "namespaces", ["parent_id", "id"], name: "index_namespaces_on_parent_id_and_id", unique: true, using: :btree
- add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree
+ add_index "namespaces", ["path"], name: "index_namespaces_on_path", using: :btree
add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
@@ -853,6 +855,7 @@ ActiveRecord::Schema.define(version: 20161202152035) do
t.datetime "expires_at"
t.datetime "created_at", null: false
t.datetime "updated_at", 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
@@ -1220,8 +1223,8 @@ ActiveRecord::Schema.define(version: 20161202152035) do
t.datetime "otp_grace_period_started_at"
t.boolean "ldap_email", default: false, null: false
t.boolean "external", default: false
- t.string "incoming_email_token"
t.string "organization"
+ t.string "incoming_email_token"
t.boolean "authorized_projects_populated"
end
diff --git a/doc/README.md b/doc/README.md
index eba1e9845b1..8bf33cad5e4 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -8,7 +8,7 @@
- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
- [Container Registry](user/project/container_registry.md) Learn how to use GitLab Container Registry.
- [GitLab basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
-- [Importing to GitLab](workflow/importing/README.md).
+- [Importing to GitLab](workflow/importing/README.md) Import your projects from GitHub, Bitbucket, GitLab.com, FogBugz and SVN into GitLab.
- [Importing and exporting projects between instances](user/project/settings/import_export.md).
- [Markdown](user/markdown.md) GitLab's advanced formatting system.
- [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab.
@@ -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.
+- [Online 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 07e548aaabe..2fc5d0355b5 100644
--- a/doc/administration/auth/README.md
+++ b/doc/administration/auth/README.md
@@ -7,5 +7,6 @@ providers.
and 389 Server
- [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google,
Bitbucket, Facebook, Shibboleth, Crowd and Azure
-- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider
- [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/img/okta_admin_panel.png b/doc/administration/auth/img/okta_admin_panel.png
new file mode 100644
index 00000000000..12e21956715
--- /dev/null
+++ b/doc/administration/auth/img/okta_admin_panel.png
Binary files differ
diff --git a/doc/administration/auth/img/okta_saml_settings.png b/doc/administration/auth/img/okta_saml_settings.png
new file mode 100644
index 00000000000..ee275ece369
--- /dev/null
+++ b/doc/administration/auth/img/okta_saml_settings.png
Binary files differ
diff --git a/doc/administration/auth/okta.md b/doc/administration/auth/okta.md
new file mode 100644
index 00000000000..cb42b7743c5
--- /dev/null
+++ b/doc/administration/auth/okta.md
@@ -0,0 +1,160 @@
+# Okta SSO provider
+
+Okta is a [Single Sign-on provider][okta-sso] that can be used to authenticate
+with GitLab.
+
+The following documentation enables Okta as a SAML provider.
+
+## Configure the Okta application
+
+1. On Okta go to the admin section and choose to **Add an App**.
+1. When the app screen comes up you see another button to **Create an App** and
+ choose SAML 2.0 on the next screen.
+1. Now, very important, add a logo
+ (you can choose it from https://about.gitlab.com/press/). You'll have to
+ crop and resize it.
+1. Next, you'll need the to fill in the SAML general config. Here's an example
+ image.
+
+ ![Okta admin panel view](img/okta_admin_panel.png)
+
+1. The last part of the configuration is the feedback section where you can
+ just say you're a customer and creating an app for internal use.
+1. When you have your app you'll have a few tabs on the top of the app's
+ profile. Click on the SAML 2.0 config instructions button which should
+ look like the following:
+
+ ![Okta SAML settings](img/okta_saml_settings.png)
+
+1. On the screen that comes up take note of the
+ **Identity Provider Single Sign-On URL** which you'll use for the
+ `idp_sso_target_url` on your GitLab config file.
+
+1. **Before you leave Okta make sure you add your user and groups if any.**
+
+---
+
+Now that the Okta app is configured, it's time to enable it in GitLab.
+
+## Configure GitLab
+
+1. On your GitLab server, open the configuration file:
+
+ **For Omnibus GitLab installations**
+
+ ```sh
+ sudo editor /etc/gitlab/gitlab.rb
+ ```
+
+ **For installations from source**
+
+ ```sh
+ cd /home/git/gitlab
+ sudo -u git -H editor config/gitlab.yml
+ ```
+
+1. See [Initial OmniAuth Configuration](../../integration/omniauth.md#initial-omniauth-configuration)
+ for initial settings.
+
+1. To allow your users to use Okta to sign up without having to manually create
+ an account first, don't forget to add the following values to your
+ configuration:
+
+ **For Omnibus GitLab installations**
+
+ ```ruby
+ gitlab_rails['omniauth_allow_single_sign_on'] = ['saml']
+ gitlab_rails['omniauth_block_auto_created_users'] = false
+ ```
+
+ **For installations from source**
+
+ ```yaml
+ allow_single_sign_on: ["saml"]
+ block_auto_created_users: false
+ ```
+
+1. You can also automatically link Okta users with existing GitLab users if
+ their email addresses match by adding the following setting:
+
+ **For Omnibus GitLab installations**
+
+ ```ruby
+ gitlab_rails['omniauth_auto_link_saml_user'] = true
+ ```
+
+ **For installations from source**
+
+ ```yaml
+ auto_link_saml_user: true
+ ```
+
+1. Add the provider configuration.
+
+ >**Notes:**
+ >- Change the value for `assertion_consumer_service_url` to match the HTTPS endpoint
+ of GitLab (append `users/auth/saml/callback` to the HTTPS URL of your GitLab
+ installation to generate the correct value).
+ >- To get the `idp_cert_fingerprint` fingerprint, first download the
+ certificate from the Okta app you registered and then run:
+ `openssl x509 -in okta.cert -noout -fingerprint`. Substitute `okta.cert`
+ with the location of your certificate.
+ >- Change the value of `idp_sso_target_url`, with the value of the
+ **Identity Provider Single Sign-On URL** from the step when you
+ configured the Okta app.
+ >- Change the value of `issuer` to a unique name, which will identify the application
+ to the IdP.
+ >- Leave `name_identifier_format` as-is.
+
+ **For Omnibus GitLab installations**
+
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ name: 'saml',
+ args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://gitlab.oktapreview.com/app/gitlabdev773716_gitlabsaml_1/exk8odl81tBrjpD4B0h7/sso/saml',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+ },
+ label: 'Okta' # optional label for SAML login button, defaults to "Saml"
+ }
+ ]
+ ```
+
+ **For installations from source**
+
+ ```yaml
+ - {
+ name: 'saml',
+ args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://gitlab.oktapreview.com/app/gitlabdev773716_gitlabsaml_1/exk8odl81tBrjpD4B0h7/sso/saml',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+ },
+ label: 'Okta' # optional label for SAML login button, defaults to "Saml"
+ }
+ ```
+
+
+1. [Reconfigure][reconf] or [restart] GitLab for Omnibus and installations
+ from source respectively for the changes to take effect.
+
+You might want to try this out on a incognito browser window.
+
+## Configuring groups
+
+>**Note:**
+Make sure the groups exist and are assigned to the Okta app.
+
+You can take a look of the [SAML documentation][saml] on external groups since
+it works the same.
+
+[okta-sso]: https://www.okta.com/products/single-sign-on/
+[saml]: ../../integration/saml.md#external-groups
+[reconf]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart]: ../restart_gitlab.md#installations-from-source
diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md
index 06291705702..80e5d80aa41 100644
--- a/doc/administration/custom_hooks.md
+++ b/doc/administration/custom_hooks.md
@@ -44,22 +44,30 @@ as appropriate.
## Chained hooks support
-> [Introduced][93] in GitLab Shell 4.1.0.
+> [Introduced][93] in GitLab Shell 4.1.0 and GitLab 8.15.
-The hooks could be also placed in `hooks/<hook_name>.d` (global) or `custom_hooks/<hook_name>.d` (per project)
-directories supporting chained execution of the hooks.
+Hooks can be also placed in `hooks/<hook_name>.d` (global) or
+`custom_hooks/<hook_name>.d` (per project) directories supporting chained
+execution of the hooks.
+
+To look in a different directory for the global custom hooks (those in
+`hooks/<hook_name.d>`), set `custom_hooks_dir` in gitlab-shell config. For
+Omnibus installations, this can be set in `gitlab.rb`; and in source
+installations, this can be set in `gitlab-shell/config.yml`.
The hooks are searched and executed in this order:
+
1. `<project>.git/hooks/` - symlink to `gitlab-shell/hooks` global dir
1. `<project>.git/hooks/<hook_name>` - executed by `git` itself, this is `gitlab-shell/hooks/<hook_name>`
1. `<project>.git/custom_hooks/<hook_name>` - per project hook (this is already existing behavior)
1. `<project>.git/custom_hooks/<hook_name>.d/*` - per project hooks
-1. `<project>.git/hooks/<hook_name>.d/*` - global hooks: all executable files (minus editor backup files)
+1. `<project>.git/hooks/<hook_name>.d/*` OR `<custom_hooks_dir>/<hook_name.d>/*` - global hooks: all executable files (minus editor backup files)
-Files in `.d` directories need to be executable and not match the backup file pattern (`*~`).
+Files in `.d` directories need to be executable and not match the backup file
+pattern (`*~`).
-The hooks of the same type are executed in order and execution stops on the first
-script exiting with non-zero value.
+The hooks of the same type are executed in order and execution stops on the
+first script exiting with a non-zero value.
## Custom error messages
diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md
index b4a953d1ccc..76029b30dd8 100644
--- a/doc/administration/environment_variables.md
+++ b/doc/administration/environment_variables.md
@@ -13,17 +13,18 @@ override certain values.
Variable | Type | Description
-------- | ---- | -----------
-`GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation
-`GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`)
-`RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test`
-`DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development`
-`GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab
-`GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab
-`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
-`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
-`GITLAB_EMAIL_SUBJECT_SUFFIX` | string | The e-mail subject suffix used in e-mails sent by GitLab
-`GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer
-`GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer
+`GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation
+`GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`)
+`RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test`
+`DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development`
+`GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab
+`GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab
+`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
+`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
+`GITLAB_EMAIL_SUBJECT_SUFFIX` | string | The e-mail subject suffix used in e-mails sent by GitLab
+`GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer
+`GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer
+`GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN` | string | Sets the initial registration token used for GitLab Runners
## Complete database variables
diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md
index b36cf18d459..e4f94eb7cb6 100644
--- a/doc/administration/high_availability/database.md
+++ b/doc/administration/high_availability/database.md
@@ -44,6 +44,9 @@ If you use a cloud-managed service, or provide your own PostgreSQL:
gitlab_rails['db_password'] = 'DB password'
postgresql['md5_auth_cidr_addresses'] = ['0.0.0.0/0']
postgresql['listen_address'] = '0.0.0.0'
+
+ # Disable automatic database migrations
+ gitlab_rails['auto_migrate'] = false
```
1. Run `sudo gitlab-ctl reconfigure` to install and configure PostgreSQL.
@@ -102,9 +105,6 @@ If you use a cloud-managed service, or provide your own PostgreSQL:
1. Exit the database prompt by typing `\q` and Enter.
1. Exit the `gitlab-psql` user by running `exit` twice.
1. Run `sudo gitlab-ctl reconfigure` a final time.
-1. Run `sudo touch /etc/gitlab/skip-auto-migrations` to prevent database migrations
- from running on upgrade. Only the primary GitLab application server should
- handle migrations.
---
diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md
index 136f570ac27..e61ea359a6a 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]: [Terminal support](../../ci/environments.md#terminal-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
+ [online 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/high_availability/redis.md b/doc/administration/high_availability/redis.md
index f532a106bc6..b4e7bf21e35 100644
--- a/doc/administration/high_availability/redis.md
+++ b/doc/administration/high_availability/redis.md
@@ -287,14 +287,14 @@ The prerequisites for a HA Redis setup are the following:
redis['password'] = 'redis-password-goes-here'
```
-1. To prevent database migrations from running on upgrade, run:
+1. Only the primary GitLab application server should handle migrations. To
+ prevent database migrations from running on upgrade, add the following
+ configuration to your `/etc/gitlab/gitlab.rb` file:
```
- sudo touch /etc/gitlab/skip-auto-migrations
+ gitlab_rails['auto_migrate'] = false
```
- Only the primary GitLab application server should handle migrations.
-
1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
### Step 2. Configuring the slave Redis instances
diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md
new file mode 100644
index 00000000000..05d0a97e554
--- /dev/null
+++ b/doc/administration/integration/terminal.md
@@ -0,0 +1,73 @@
+# Online terminals
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690)
+in GitLab 8.15. Only project masters and owners can access online 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 [online terminals](../../ci/environments.html#online-terminals)
+for environments.
+
+## How it works
+
+A detailed overview of the architecture of online 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 online 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 online 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 an online terminal. In
+turn, they will receive a `Connection failed` message.
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index d757a3c2a66..4b8d5c5cc87 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -1,4 +1,4 @@
-## Log system
+# Log system
GitLab has an advanced log system where everything is logged so that you
can analyze your instance using various system log files. In addition to
@@ -9,10 +9,10 @@ documentation](http://docs.gitlab.com/ee/administration/audit_events.html)
System log files are typically plain text in a standard log file format.
This guide talks about how to read and use these system log files.
-### production.log
+## `production.log`
This file lives in `/var/log/gitlab/gitlab-rails/production.log` for
-omnibus package or in `/home/git/gitlab/log/production.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/production.log` for
installations from source. (When Gitlab is running in an environment
other than production, the corresponding logfile is shown here.)
@@ -46,10 +46,10 @@ In this example we can see that server processed an HTTP request with URL
19:34:53 +0200. Also we can see that request was processed by
`Projects::TreeController`.
-### application.log
+## `application.log`
This file lives in `/var/log/gitlab/gitlab-rails/application.log` for
-omnibus package or in `/home/git/gitlab/log/application.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/application.log` for
installations from source.
It helps you discover events happening in your instance such as user creation,
@@ -63,10 +63,10 @@ October 07, 2014 11:25: User "Claudie Hodkiewicz" (nasir_stehr@olson.co.uk) was
October 07, 2014 11:25: Project "project133" was removed
```
-### githost.log
+## `githost.log`
This file lives in `/var/log/gitlab/gitlab-rails/githost.log` for
-omnibus package or in `/home/git/gitlab/log/githost.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/githost.log` for
installations from source.
GitLab has to interact with Git repositories but in some rare cases
@@ -81,10 +81,10 @@ December 03, 2014 13:20 -> ERROR -> Command failed [1]: /usr/bin/git --git-dir=/
error: failed to push some refs to '/Users/vsizov/gitlab-development-kit/repositories/gitlabhq/gitlab_git.git'
```
-### sidekiq.log
+## `sidekiq.log`
This file lives in `/var/log/gitlab/gitlab-rails/sidekiq.log` for
-omnibus package or in `/home/git/gitlab/log/sidekiq.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/sidekiq.log` for
installations from source.
GitLab uses background jobs for processing tasks which can take a long
@@ -96,10 +96,10 @@ this file. For example:
2014-06-10T18:18:26Z 14299 TID-55uqo INFO: Booting Sidekiq 3.0.0 with redis options {:url=>"redis://localhost:6379/0", :namespace=>"sidekiq"}
```
-### gitlab-shell.log
+## `gitlab-shell.log`
This file lives in `/var/log/gitlab/gitlab-shell/gitlab-shell.log` for
-omnibus package or in `/home/git/gitlab-shell/gitlab-shell.log` for
+Omnibus GitLab packages or in `/home/git/gitlab-shell/gitlab-shell.log` for
installations from source.
GitLab shell is used by Gitlab for executing Git commands and provide
@@ -110,10 +110,10 @@ I, [2015-02-13T06:17:00.671315 #9291] INFO -- : Adding project root/example.git
I, [2015-02-13T06:17:00.679433 #9291] INFO -- : Moving existing hooks directory and symlinking global hooks directory for /var/opt/gitlab/git-data/repositories/root/example.git.
```
-### unicorn\_stderr.log
+## `unicorn\_stderr.log`
This file lives in `/var/log/gitlab/unicorn/unicorn_stderr.log` for
-omnibus package or in `/home/git/gitlab/log/unicorn_stderr.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/unicorn_stderr.log` for
installations from source.
Unicorn is a high-performance forking Web server which is used for
@@ -136,3 +136,13 @@ I, [2015-02-13T07:16:01.530733 #9047] INFO -- : reaped #<Process::Status: pid 9
I, [2015-02-13T07:16:01.534501 #13379] INFO -- : worker=1 spawned pid=13379
I, [2015-02-13T07:16:01.534848 #13379] INFO -- : worker=1 ready
```
+
+## `repocheck.log`
+
+This file lives in `/var/log/gitlab/gitlab-rails/repocheck.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/repocheck.log` for
+installations from source.
+
+It logs information whenever a [repository check is run][repocheck] on a project.
+
+[repocheck]: repository_checks.md
diff --git a/doc/administration/raketasks/check.md b/doc/administration/raketasks/check.md
index d1d2fed4861..c8b5434c068 100644
--- a/doc/administration/raketasks/check.md
+++ b/doc/administration/raketasks/check.md
@@ -74,24 +74,5 @@ Example output:
The LDAP check Rake task will test the bind_dn and password credentials
(if configured) and will list a sample of LDAP users. This task is also
-executed as part of the `gitlab:check` task, but can run independently
-using the command below.
-
-**Omnibus Installation**
-
-```
-sudo gitlab-rake gitlab:ldap:check
-```
-
-**Source Installation**
-
-```bash
-sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production
-```
-
-By default, the task will return a sample of 100 LDAP users. Change this
-limit by passing a number to the check task:
-
-```bash
-rake gitlab:ldap:check[50]
-```
+executed as part of the `gitlab:check` task, but can run independently.
+See [LDAP Rake Tasks - LDAP Check](ldap.md#check) for details.
diff --git a/doc/administration/raketasks/ldap.md b/doc/administration/raketasks/ldap.md
new file mode 100644
index 00000000000..91fc0133d56
--- /dev/null
+++ b/doc/administration/raketasks/ldap.md
@@ -0,0 +1,120 @@
+# LDAP Rake Tasks
+
+## Check
+
+The LDAP check Rake task will test the `bind_dn` and `password` credentials
+(if configured) and will list a sample of LDAP users. This task is also
+executed as part of the `gitlab:check` task, but can run independently
+using the command below.
+
+**Omnibus Installation**
+
+```
+sudo gitlab-rake gitlab:ldap:check
+```
+
+**Source Installation**
+
+```bash
+sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production
+```
+
+------
+
+By default, the task will return a sample of 100 LDAP users. Change this
+limit by passing a number to the check task:
+
+```bash
+rake gitlab:ldap:check[50]
+```
+
+## Rename a provider
+
+If you change the LDAP server ID in `gitlab.yml` or `gitlab.rb` you will need
+to update all user identities or users will be unable to sign in. Input the
+old and new provider and this task will update all matching identities in the
+database.
+
+`old_provider` and `new_provider` are derived from the prefix `ldap` plus the
+LDAP server ID from the configuration file. For example, in `gitlab.yml` or
+`gitlab.rb` you may see LDAP configuration like this:
+
+```yaml
+main:
+ label: 'LDAP'
+ host: '_your_ldap_server'
+ port: 389
+ uid: 'sAMAccountName'
+ ...
+```
+
+`main` is the LDAP server ID. Together, the unique provider is `ldapmain`.
+
+> **Warning**: If you input an incorrect new provider users will be unable
+to sign in. If this happens, run the task again with the incorrect provider
+as the `old_provider` and the correct provider as the `new_provider`.
+
+**Omnibus Installation**
+
+```bash
+sudo gitlab-rake gitlab:ldap:rename_provider[old_provider,new_provider]
+```
+
+**Source Installation**
+
+```bash
+bundle exec rake gitlab:ldap:rename_provider[old_provider,new_provider] RAILS_ENV=production
+```
+
+### Example
+
+Consider beginning with the default server ID `main` (full provider `ldapmain`).
+If we change `main` to `mycompany`, the `new_provider` is `ldapmycompany`.
+To rename all user identities run the following command:
+
+```bash
+sudo gitlab-rake gitlab:ldap:rename_provider[ldapmain,ldapmycompany]
+```
+
+Example output:
+
+```
+100 users with provider 'ldapmain' will be updated to 'ldapmycompany'.
+If the new provider is incorrect, users will be unable to sign in.
+Do you want to continue (yes/no)? yes
+
+User identities were successfully updated
+```
+
+### Other options
+
+If you do not specify an `old_provider` and `new_provider` you will be prompted
+for them:
+
+**Omnibus Installation**
+
+```bash
+sudo gitlab-rake gitlab:ldap:rename_provider
+```
+
+**Source Installation**
+
+```bash
+bundle exec rake gitlab:ldap:rename_provider RAILS_ENV=production
+```
+
+**Example output:**
+
+```
+What is the old provider? Ex. 'ldapmain': ldapmain
+What is the new provider? Ex. 'ldapcustom': ldapmycompany
+```
+
+------
+
+This tasks also accepts the `force` environment variable which will skip the
+confirmation dialog:
+
+```bash
+sudo gitlab-rake gitlab:ldap:rename_provider[old_provider,new_provider] force=yes
+```
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 0170af00e0e..5c11d0f83bb 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -183,6 +183,44 @@ Example response:
}
```
+## Cherry pick a commit
+
+> [Introduced][ce-8047] in GitLab 8.15.
+
+Cherry picks a commit to a given branch.
+
+```
+POST /projects/:id/repository/commits/:sha/cherry_pick
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
+| `sha` | string | yes | The commit hash |
+| `branch` | string | yes | The name of the branch |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "branch=master" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/cherry_pick"
+```
+
+Example response:
+
+```json
+{
+ "id": "8b090c1b79a14f2bd9e8a738f717824ff53aebad",
+ "short_id": "8b090c1b",
+ "title": "Feature added",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "created_at": "2016-12-12T20:10:39.000+01:00",
+ "committer_name": "Administrator",
+ "committer_email": "admin@example.com",
+ "message": "Feature added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n"
+}
+```
+
## Get the diff of a commit
Get the diff of a commit in a project.
@@ -438,3 +476,4 @@ Example response:
```
[ce-6096]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6096 "Multi-file commit"
+[ce-8047]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8047
diff --git a/doc/api/enviroments.md b/doc/api/enviroments.md
index 87a5fa67124..1299aca8c45 100644
--- a/doc/api/enviroments.md
+++ b/doc/api/enviroments.md
@@ -22,8 +22,9 @@ Example response:
[
{
"id": 1,
- "name": "Env1",
- "external_url": "https://env1.example.gitlab.com"
+ "name": "review/fix-foo",
+ "slug": "review-fix-foo-dfjre3",
+ "external_url": "https://review-fix-foo-dfjre3.example.gitlab.com"
}
]
```
@@ -54,6 +55,7 @@ Example response:
{
"id": 1,
"name": "deploy",
+ "slug": "deploy",
"external_url": "https://deploy.example.gitlab.com"
}
```
@@ -85,6 +87,7 @@ Example response:
{
"id": 1,
"name": "staging",
+ "slug": "staging",
"external_url": "https://staging.example.gitlab.com"
}
```
@@ -112,6 +115,7 @@ Example response:
{
"id": 1,
"name": "deploy",
+ "slug": "deploy",
"external_url": "https://deploy.example.gitlab.com"
}
```
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 5e6f498c365..134d7bda22f 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -50,12 +50,17 @@ GET /groups/:id/projects
Parameters:
-- `archived` (optional) - if passed, limit by archived status
-- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private`
-- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
-- `search` (optional) - Return list of authorized projects according to a search criteria
-- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or path of a group |
+| `archived` | boolean | no | Limit by archived status |
+| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
+| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
+| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Return list of authorized projects matching the search criteria |
+| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
+
+Example response:
```json
[
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 0bc2a5210aa..edffad555a5 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -664,7 +664,6 @@ Parameters:
| `path` | string | no | Custom repository name for new project. By default generated based on name |
| `default_branch` | string | no | `master` by default |
| `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) |
-| `default_branch` | string | no | `master` by default |
| `description` | string | no | Short project description |
| `issues_enabled` | boolean | no | Enable issues for this project |
| `merge_requests_enabled` | boolean | no | Enable merge requests for this project |
diff --git a/doc/api/services.md b/doc/api/services.md
index a5d733fe6c7..1466b8189b0 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -139,6 +139,43 @@ Get Buildkite service settings for a project.
GET /projects/:id/services/buildkite
```
+## Build-Emails
+
+Get emails for GitLab CI builds.
+
+### Create/Edit Build-Emails service
+
+Set Build-Emails service for a project.
+
+```
+PUT /projects/:id/services/builds-email
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `recipients` | string | yes | Comma-separated list of recipient email addresses |
+| `add_pusher` | boolean | no | Add pusher to recipients list |
+| `notify_only_broken_builds` | boolean | no | Notify only broken builds |
+
+
+### Delete Build-Emails service
+
+Delete Build-Emails service for a project.
+
+```
+DELETE /projects/:id/services/builds-email
+```
+
+### Get Build-Emails service settings
+
+Get Build-Emails service settings for a project.
+
+```
+GET /projects/:id/services/builds-email
+```
+
## Campfire
Simple web-based real-time group chat
@@ -476,12 +513,11 @@ PUT /projects/:id/services/jira
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `active` | boolean| no | Enable/disable the JIRA service. |
| `url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project, e.g., `https://jira.example.com`. |
| `project_key` | string | yes | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. |
| `username` | string | no | The username of the user created to be used with GitLab/JIRA. |
| `password` | string | no | The password of the user created to be used with GitLab/JIRA. |
-| `jira_issue_transition_id` | string | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. |
+| `jira_issue_transition_id` | integer | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. |
### Delete JIRA service
@@ -491,6 +527,78 @@ Remove all previously JIRA settings from a project.
DELETE /projects/:id/services/jira
```
+## Mattermost Slash Commands
+
+Ability to receive slash commands from a Mattermost chat instance.
+
+### Create/Edit Mattermost Slash Command service
+
+Set Mattermost Slash Command for a project.
+
+```
+PUT /projects/:id/services/mattermost-slash-commands
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `token` | string | yes | The Mattermost token |
+
+
+### Delete Mattermost Slash Command service
+
+Delete Mattermost Slash Command service for a project.
+
+```
+DELETE /projects/:id/services/mattermost-slash-commands
+```
+
+### Get Mattermost Slash Command service settings
+
+Get Mattermost Slash Command service settings for a project.
+
+```
+GET /projects/:id/services/mattermost-slash-commands
+```
+
+## Pipeline-Emails
+
+Get emails for GitLab CI pipelines.
+
+### Create/Edit Pipeline-Emails service
+
+Set Pipeline-Emails service for a project.
+
+```
+PUT /projects/:id/services/pipelines-email
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `recipients` | string | yes | Comma-separated list of recipient email addresses |
+| `add_pusher` | boolean | no | Add pusher to recipients list |
+| `notify_only_broken_builds` | boolean | no | Notify only broken pipelines |
+
+
+### Delete Pipeline-Emails service
+
+Delete Pipeline-Emails service for a project.
+
+```
+DELETE /projects/:id/services/pipelines-email
+```
+
+### Get Pipeline-Emails service settings
+
+Get Pipeline-Emails service settings for a project.
+
+```
+GET /projects/:id/services/pipelines-email
+```
+
## PivotalTracker
Project Management Software (Source Commits Endpoint)
@@ -595,9 +703,9 @@ Get Redmine service settings for a project.
GET /projects/:id/services/redmine
```
-## Slack
+## Slack notifications
-A team communication tool for the 21st century
+Receive event notifications in Slack
### Create/Edit Slack service
@@ -629,6 +737,40 @@ Get Slack service settings for a project.
GET /projects/:id/services/slack
```
+## Mattermost notifications
+
+Receive event notifications in Mattermost
+
+### Create/Edit Mattermost notifications service
+
+Set Mattermost service for a project.
+
+```
+PUT /projects/:id/services/mattermost
+```
+
+Parameters:
+
+- `webhook` (**required**) - https://mattermost.example/hooks/1298aff...
+- `username` (optional) - username
+- `channel` (optional) - #channel
+
+### Delete Mattermost notifications service
+
+Delete Mattermost Notifications service for a project.
+
+```
+DELETE /projects/:id/services/mattermost
+```
+
+### Get Mattermost notifications service settings
+
+Get Mattermost notifications service settings for a project.
+
+```
+GET /projects/:id/services/mattermost
+```
+
## JetBrains TeamCity CI
A continuous integration and build server
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index 9dd84a5ff81..07d92bb746c 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 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
@@ -86,6 +88,13 @@ will later see, is exposed in various places within GitLab. Each time a job that
has an environment specified and succeeds, a deployment is recorded, remembering
the Git SHA and environment name.
+>**Note:**
+Starting with GitLab 8.15, the environment name is exposed to the Runner in
+two forms: `$CI_ENVIRONMENT_NAME`, and `$CI_ENVIRONMENT_SLUG`. The first is
+the name given in `.gitlab-ci.yml` (with any variables expanded), while the
+second is a "cleaned-up" version of the name, suitable for use in URLs, DNS,
+etc.
+
To sum up, with the above `.gitlab-ci.yml` we have achieved that:
- All branches will run the `test` and `build` jobs.
@@ -157,7 +166,7 @@ that can be found in the deployments page
job with the commit associated with it.
>**Note:**
-Bare in mind that your mileage will vary and it's entirely up to how you define
+Bear in mind that your mileage will vary and it's entirely up to how you define
the deployment process in the job's `script` whether the rollback succeeds or not.
GitLab CI is just following orders.
@@ -226,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!
+## Terminal support
+
+>**Note:**
+Terminal support was added in GitLab 8.15 and is 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
@@ -248,7 +297,7 @@ deploy_review:
- echo "Deploy a review app"
environment:
name: review/$CI_BUILD_REF_NAME
- url: https://$CI_BUILD_REF_NAME.example.com
+ url: https://$CI_BUILD_REF_SLUG.review.example.com
only:
- branches
except:
@@ -259,15 +308,25 @@ Let's break it down in pieces. The job's name is `deploy_review` and it runs
on the `deploy` stage. The `script` at this point is fictional, you'd have to
use your own based on your deployment. Then, we set the `environment` with the
`environment:name` being `review/$CI_BUILD_REF_NAME`. Now that's an interesting
-one. Since the [environment name][env-name] can contain also slashes (`/`), we
-can use this pattern to distinguish between dynamic environments and the regular
+one. Since the [environment name][env-name] can contain slashes (`/`), we can
+use this pattern to distinguish between dynamic environments and the regular
ones.
So, the first part is `review`, followed by a `/` and then `$CI_BUILD_REF_NAME`
-which takes the value of the branch name. We also use the same
-`$CI_BUILD_REF_NAME` value in the `environment:url` so that the environment
-can get a specific and distinct URL for each branch. Again, the way you set up
-the webserver to serve these requests is based on your setup.
+which takes the value of the branch name. Since `$CI_BUILD_REF_NAME` itself may
+also contain `/`, or other characters that would be invalid in a domain name or
+URL, we use `$CI_ENVIRONMENT_SLUG` in the `environment:url` so that the
+environment can get a specific and distinct URL for each branch. In this case,
+given a `$CI_BUILD_REF_NAME` of `100-Do-The-Thing`, the URL will be something
+like `https://review-100-do-the-4f99a2.example.com`. Again, the way you set up
+the web server to serve these requests is based on your setup.
+
+You could also use `$CI_BUILD_REF_SLUG` in `environment:url`, e.g.:
+`https://$CI_BUILD_REF_SLUG.review.example.com`. We use `$CI_ENVIRONMENT_SLUG`
+here because it is guaranteed to be unique, but if you're using a workflow like
+[GitLab Flow][gitlab-flow], collisions are very unlikely, and you may prefer
+environment names to be more closely based on the branch name - the example
+above would give you an URL like `https://100-do-the-thing.review.example.com`
Last but not least, we tell the job to run [`only`][only] on branches
[`except`][only] master.
@@ -299,7 +358,7 @@ deploy_review:
- echo "Deploy a review app"
environment:
name: review/$CI_BUILD_REF_NAME
- url: https://$CI_BUILD_REF_NAME.example.com
+ url: https://$CI_ENVIRONMENT_SLUG.example.com
only:
- branches
except:
@@ -329,16 +388,16 @@ deploy_prod:
A more realistic example would include copying files to a location where a
webserver (NGINX) could then read and serve. The example below will copy the
-`public` directory to `/srv/nginx/$CI_BUILD_REF_NAME/public`:
+`public` directory to `/srv/nginx/$CI_BUILD_REF_SLUG/public`:
```yaml
review_app:
stage: deploy
script:
- - rsync -av --delete public /srv/nginx/$CI_BUILD_REF_NAME
+ - rsync -av --delete public /srv/nginx/$CI_BUILD_REF_SLUG
environment:
name: review/$CI_BUILD_REF_NAME
- url: https://$CI_BUILD_REF_NAME.example.com
+ url: https://$CI_BUILD_REF_SLUG.example.com
```
It is assumed that the user has already setup NGINX and GitLab Runner in the
@@ -346,7 +405,7 @@ server this job will run on.
>**Note:**
Be sure to check out the [limitations](#limitations) section for some edge
-cases regarding naming of you branches and Review Apps.
+cases regarding naming of your branches and Review Apps.
---
@@ -418,7 +477,7 @@ deploy_review:
- echo "Deploy a review app"
environment:
name: review/$CI_BUILD_REF_NAME
- url: https://$CI_BUILD_REF_NAME.example.com
+ url: https://$CI_ENVIRONMENT_SLUG.example.com
on_stop: stop_review
only:
- branches
@@ -480,9 +539,8 @@ exist, you should see something like:
## Checkout deployments locally
-Since 8.13, a reference in the git repository is saved for each deployment. So
-knowing what the state is of your current environments is only a `git fetch`
-away.
+Since 8.13, a reference in the git repository is saved for each deployment, so
+knowing the state of your current environments is only a `git fetch` away.
In your git config, append the `[remote "<your-remote>"]` block with an extra
fetch line:
@@ -493,10 +551,6 @@ fetch = +refs/environments/*:refs/remotes/origin/environments/*
## Limitations
-1. If the branch name contains special characters (`/`), and you use the
- `$CI_BUILD_REF_NAME` variable to dynamically create environments, there might
- be complications during your Review Apps deployment. Follow the
- [issue 22849][ce-22849] for more information.
1. You are limited to use only the [CI predefined variables][variables] in the
`environment: name`. If you try to re-use variables defined inside `script`
as part of the environment name, it will not work.
@@ -512,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
@@ -520,6 +575,6 @@ Below are some links you may find interesting:
[only]: yaml/README.md#only-and-except
[onstop]: yaml/README.md#environment-on_stop
[ce-7015]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7015
-[ce-22849]: https://gitlab.com/gitlab-org/gitlab-ce/issues/22849
+[gitlab-flow]: ../workflow/gitlab_flow.md
[gitlab runner]: https://docs.gitlab.com/runner/
[git-strategy]: yaml/README.md#git-strategy
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/ci/variables/README.md b/doc/ci/variables/README.md
index d142fe266a2..d3b9611b02e 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -13,6 +13,7 @@ this order:
1. [Secret variables](#secret-variables)
1. YAML-defined [job-level variables](../yaml/README.md#job-variables)
1. YAML-defined [global variables](../yaml/README.md#variables)
+1. [Deployment variables](#deployment-variables)
1. [Predefined variables](#predefined-variables-environment-variables) (are the
lowest in the chain)
@@ -40,6 +41,7 @@ version of Runner required.
| **CI_BUILD_NAME** | all | 0.5 | The name of the build as defined in `.gitlab-ci.yml` |
| **CI_BUILD_STAGE** | all | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` |
| **CI_BUILD_REF_NAME** | all | all | The branch or tag name for which project is built |
+| **CI_BUILD_REF_SLUG** | 8.15 | all | `$CI_BUILD_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. |
| **CI_BUILD_REPO** | all | all | The URL to clone the Git repository |
| **CI_BUILD_TRIGGERED** | all | 0.5 | The flag to indicate that build was [triggered] |
| **CI_BUILD_MANUAL** | 8.12 | all | The flag to indicate that build was manually started |
@@ -51,12 +53,17 @@ version of Runner required.
| **CI_PROJECT_PATH** | 8.10 | 0.5 | The namespace with project name |
| **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project |
| **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the build is run |
+| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this build |
+| **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. |
| **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry |
| **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project |
| **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used |
| **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5 | The description of the runner as saved in GitLab |
| **CI_RUNNER_TAGS** | 8.10 | 0.5 | The defined runner tags |
| **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled |
+| **GET_SOURCES_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to fetch sources running a build |
+| **ARTIFACT_DOWNLOAD_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to download artifacts running a build |
+| **RESTORE_CACHE_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to restore the cache running a build |
| **GITLAB_USER_ID** | 8.12 | all | The id of the user who started the build |
| **GITLAB_USER_EMAIL** | 8.12 | all | The email of the user who started the build |
@@ -145,6 +152,20 @@ Secret variables can be added by going to your project's
Once you set them, they will be available for all subsequent builds.
+## Deployment variables
+
+>**Note:**
+This feature requires GitLab CI 8.15 or higher.
+
+[Project services](../../project_services/project_services.md) that are
+responsible for deployment configuration may define their own variables that
+are set in the build environment. These variables are only defined for
+[deployment builds](../environments.md). Please consult the documentation of
+the project services that you are using to learn which variables they define.
+
+An example project service that defines deployment variables is
+[Kubernetes Service](../../project_services/kubernetes.md).
+
## Debug tracing
> Introduced in GitLab Runner 1.7.
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 5f88974d360..7158b2e7895 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -690,18 +690,12 @@ The `stop_review_app` job is **required** to have the following keywords defined
#### dynamic environments
> [Introduced][ce-6323] in GitLab 8.12 and GitLab Runner 1.6.
+ `$CI_ENVIRONMENT_SLUG` was [introduced][ce-7983] in GitLab 8.15
`environment` can also represent a configuration hash with `name` and `url`.
These parameters can use any of the defined [CI variables](#variables)
(including predefined, secure variables and `.gitlab-ci.yml` variables).
->**Note:**
-Be aware than if the branch name contains special characters and you use the
-`$CI_BUILD_REF_NAME` variable to dynamically create environments, there might
-be complications during deployment. Follow the
-[issue 22849](https://gitlab.com/gitlab-org/gitlab-ce/issues/22849) for more
-information.
-
For example:
```
@@ -709,15 +703,17 @@ deploy as review app:
stage: deploy
script: make deploy
environment:
- name: review-apps/$CI_BUILD_REF_NAME
- url: https://$CI_BUILD_REF_NAME.review.example.com/
+ name: review/$CI_BUILD_REF_NAME
+ url: https://$CI_ENVIRONMENT_SLUG.example.com/
```
The `deploy as review app` job will be marked as deployment to dynamically
-create the `review-apps/$CI_BUILD_REF_NAME` environment, which `$CI_BUILD_REF_NAME`
-is an [environment variable][variables] set by the Runner. If for example the
-`deploy as review app` job was run in a branch named `pow`, this environment
-should be accessible under `https://pow.review.example.com/`.
+create the `review/$CI_BUILD_REF_NAME` environment, where `$CI_BUILD_REF_NAME`
+is an [environment variable][variables] set by the Runner. The
+`$CI_ENVIRONMENT_SLUG` variable is based on the environment name, but suitable
+for inclusion in URLs. In this case, if the `deploy as review app` job was run
+in a branch named `pow`, this environment would be accessible with an URL like
+`https://review-pow-aaaaaa.example.com/`.
This of course implies that the underlying server which hosts the application
is properly configured.
@@ -1038,6 +1034,31 @@ variables:
GIT_STRATEGY: none
```
+## Build stages attempts
+
+> Introduced in GitLab, it requires GitLab Runner v1.9+.
+
+You can set the number for attempts the running build will try to execute each
+of the following stages:
+
+| Variable | Description |
+|-------------------------|-------------|
+| **GET_SOURCES_ATTEMPTS** | Number of attempts to fetch sources running a build |
+| **ARTIFACT_DOWNLOAD_ATTEMPTS** | Number of attempts to download artifacts running a build |
+| **RESTORE_CACHE_ATTEMPTS** | Number of attempts to restore the cache running a build |
+
+The default is one single attempt.
+
+Example:
+
+```
+variables:
+ GET_SOURCES_ATTEMPTS: "3"
+```
+
+You can set them in the global [`variables`](#variables) section or the [`variables`](#job-variables)
+section for individual jobs.
+
## Shallow cloning
> Introduced in GitLab 8.9 as an experimental feature. May change in future
@@ -1246,3 +1267,4 @@ CI with various languages.
[environment]: ../environments.md
[ce-6669]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6669
[variables]: ../variables/README.md
+[ce-7983]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7983
diff --git a/doc/development/performance.md b/doc/development/performance.md
index 5c43ae7b79a..f936a49a2aa 100644
--- a/doc/development/performance.md
+++ b/doc/development/performance.md
@@ -37,7 +37,7 @@ graphs/dashboards.
GitLab provides built-in tools to aid the process of improving performance:
* [Sherlock](profiling.md#sherlock)
-* [GitLab Performance Monitoring](../administration/monitoring/performance/monitoring.md)
+* [GitLab Performance Monitoring](../administration/monitoring/performance/introduction.md)
* [Request Profiling](../administration/monitoring/performance/request_profiling.md)
GitLab employees can use GitLab.com's performance monitoring systems located at
diff --git a/doc/development/ux_guide/basics.md b/doc/development/ux_guide/basics.md
index 76b739386a5..e81729556d8 100644
--- a/doc/development/ux_guide/basics.md
+++ b/doc/development/ux_guide/basics.md
@@ -5,6 +5,7 @@
* [Typography](#typography)
* [Icons](#icons)
* [Color](#color)
+* [Cursors](#cursors)
---
@@ -59,3 +60,18 @@ GitLab uses Font Awesome icons throughout our interface.
> TODO: Establish a perspective for color in terms of our personality and rationalize with Marketing usage.
+---
+
+## Cursors
+The mouse cursor is key in helping users understand how to interact with elements on the screen.
+
+| | |
+| :------: | :------- |
+| ![Default cursor](img/cursors-default.png) | Default cursor |
+| ![Pointer cursor](img/cursors-pointer.png) | Pointer cursor: used to indicate that you can click on an element to invoke a command or navigate, such as links and buttons |
+| ![Move cursor](img/cursors-move.png) | Move cursor: used to indicate that you can move an element around on the screen |
+| ![Pan opened cursor](img/cursors-panopened.png) | Pan cursor (opened): indicates that you can grab and move the entire canvas, affecting what is seen in the view port. |
+| ![Pan closed cursor](img/cursors-panclosed.png) | Pan cursor (closed): indicates that you are actively panning the canvas. |
+| ![I-beam cursor](img/cursors-ibeam.png) | I-beam cursor: indicates that this is either text that you can select and copy, or a text field that you can click into to enter text. |
+
+
diff --git a/doc/development/ux_guide/img/cursors-default.png b/doc/development/ux_guide/img/cursors-default.png
new file mode 100644
index 00000000000..c188ec4e351
--- /dev/null
+++ b/doc/development/ux_guide/img/cursors-default.png
Binary files differ
diff --git a/doc/development/ux_guide/img/cursors-ibeam.png b/doc/development/ux_guide/img/cursors-ibeam.png
new file mode 100644
index 00000000000..86f28639982
--- /dev/null
+++ b/doc/development/ux_guide/img/cursors-ibeam.png
Binary files differ
diff --git a/doc/development/ux_guide/img/cursors-move.png b/doc/development/ux_guide/img/cursors-move.png
new file mode 100644
index 00000000000..a9c610eaa88
--- /dev/null
+++ b/doc/development/ux_guide/img/cursors-move.png
Binary files differ
diff --git a/doc/development/ux_guide/img/cursors-panclosed.png b/doc/development/ux_guide/img/cursors-panclosed.png
new file mode 100644
index 00000000000..6d247a765ac
--- /dev/null
+++ b/doc/development/ux_guide/img/cursors-panclosed.png
Binary files differ
diff --git a/doc/development/ux_guide/img/cursors-panopened.png b/doc/development/ux_guide/img/cursors-panopened.png
new file mode 100644
index 00000000000..76f2eeda831
--- /dev/null
+++ b/doc/development/ux_guide/img/cursors-panopened.png
Binary files differ
diff --git a/doc/development/ux_guide/img/cursors-pointer.png b/doc/development/ux_guide/img/cursors-pointer.png
new file mode 100644
index 00000000000..d86dd955fa7
--- /dev/null
+++ b/doc/development/ux_guide/img/cursors-pointer.png
Binary files differ
diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md
index 9122dc62e39..1dfc985eaea 100644
--- a/doc/integration/bitbucket.md
+++ b/doc/integration/bitbucket.md
@@ -5,7 +5,7 @@ Bitbucket.org account.
## Overview
-You can set up Bitbucket.org as an OAuth provider so that you can use your
+You can set up Bitbucket.org as an OAuth2 provider so that you can use your
credentials to authenticate into GitLab or import your projects from
Bitbucket.org.
@@ -18,8 +18,10 @@ Bitbucket.org.
## Bitbucket OmniAuth provider
> **Note:**
-Make sure to first follow the [Initial OmniAuth configuration][init-oauth]
-before proceeding with setting up the Bitbucket integration.
+GitLab 8.15 significantly simplified the way to integrate Bitbucket.org with
+GitLab. You are encouraged to upgrade your GitLab instance if you haven't done
+already. If you're using GitLab 8.14 and below, [use the previous integration
+docs][bb-old].
To enable the Bitbucket OmniAuth provider you must register your application
with Bitbucket.org. Bitbucket will generate an application ID and secret key for
@@ -44,14 +46,13 @@ you to use.
And grant at least the following permissions:
```
- Account: Email
- Repositories: Read, Admin
+ Account: Email, Read
+ Repositories: Read
+ Pull Requests: Read
+ Issues: Read
+ Wiki: Read and Write
```
- >**Note:**
- It may seem a little odd to giving GitLab admin permissions to repositories,
- but this is needed in order for GitLab to be able to clone the repositories.
-
![Bitbucket OAuth settings page](img/bitbucket_oauth_settings_page.png)
1. Select **Save**.
@@ -93,7 +94,8 @@ you to use.
```yaml
- { name: 'bitbucket',
app_id: 'BITBUCKET_APP_KEY',
- app_secret: 'BITBUCKET_APP_SECRET' }
+ app_secret: 'BITBUCKET_APP_SECRET',
+ url: 'https://bitbucket.org/' }
```
---
@@ -112,100 +114,12 @@ well, the user will be returned to GitLab and will be signed in.
## Bitbucket project import
-To allow projects to be imported directly into GitLab, Bitbucket requires two
-extra setup steps compared to [GitHub](github.md) and [GitLab.com](gitlab.md).
-
-Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and
-instead requires GitLab to use SSH and identify itself using your GitLab
-server's SSH key.
-
-To be able to access repositories on Bitbucket, GitLab will automatically
-register your public key with Bitbucket as a deploy key for the repositories to
-be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa` which
-translates to `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages and to
-`/home/git/.ssh/bitbucket_rsa` for installations from source.
-
----
-
-Below are the steps that will allow GitLab to be able to import your projects
-from Bitbucket.
-
-1. Make sure you [have enabled the Bitbucket OAuth support](#bitbucket-omniauth-provider).
-1. Create a new SSH key with an **empty passphrase**:
-
- ```sh
- sudo -u git -H ssh-keygen
- ```
-
- When asked to 'Enter file in which to save the key' enter:
- `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages or
- `/home/git/.ssh/bitbucket_rsa` for installations from source. The name is
- important so make sure to get it right.
-
- > **Warning:**
- This key must NOT be associated with ANY existing Bitbucket accounts. If it
- is, the import will fail with an `Access denied! Please verify you can add
- deploy keys to this repository.` error.
-
-1. Next, you need to to configure the SSH client to use your new key. Open the
- SSH configuration file of the `git` user:
-
- ```
- # For Omnibus packages
- sudo editor /var/opt/gitlab/.ssh/config
-
- # For installations from source
- sudo editor /home/git/.ssh/config
- ```
-
-1. Add a host configuration for `bitbucket.org`:
-
- ```sh
- Host bitbucket.org
- IdentityFile ~/.ssh/bitbucket_rsa
- User git
- ```
-
-1. Save the file and exit.
-1. Manually connect to `bitbucket.org` over SSH, while logged in as the `git`
- user that GitLab will use:
-
- ```sh
- sudo -u git -H ssh bitbucket.org
- ```
-
- That step is performed because GitLab needs to connect to Bitbucket over SSH,
- in order to add `bitbucket.org` to your GitLab server's known SSH hosts.
-
-1. Verify the RSA key fingerprint you'll see in the response matches the one
- in the [Bitbucket documentation][bitbucket-docs] (the specific IP address
- doesn't matter):
-
- ```sh
- The authenticity of host 'bitbucket.org (104.192.143.1)' can't be established.
- RSA key fingerprint is SHA256:zzXQOXSRBEiUtuE8AikJYKwbHaxvSc0ojez9YXaGp1A.
- Are you sure you want to continue connecting (yes/no)?
- ```
-
-1. If the fingerprint matches, type `yes` to continue connecting and have
- `bitbucket.org` be added to your known SSH hosts. After confirming you should
- see a permission denied message. If you see an authentication successful
- message you have done something wrong. The key you are using has already been
- added to a Bitbucket account and will cause the import script to fail. Ensure
- the key you are using CANNOT authenticate with Bitbucket.
-1. Restart GitLab to allow it to find the new public key.
-
-Your GitLab server is now able to connect to Bitbucket over SSH. You should be
-able to see the "Import projects from Bitbucket" option on the New Project page
-enabled.
-
-## Acknowledgements
-
-Special thanks to the writer behind the following article:
-
-- http://stratus3d.com/blog/2015/09/06/migrating-from-bitbucket-to-local-gitlab-server/
+Once the above configuration is set up, you can use Bitbucket to sign into
+GitLab and [start importing your projects][bb-import].
[init-oauth]: omniauth.md#initial-omniauth-configuration
+[bb-import]: ../workflow/importing/import_projects_from_bitbucket.md
+[bb-old]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-14-stable/doc/integration/bitbucket.md
[bitbucket-docs]: https://confluence.atlassian.com/bitbucket/use-the-ssh-protocol-with-bitbucket-cloud-221449711.html#UsetheSSHprotocolwithBitbucketCloud-KnownhostorBitbucket%27spublickeyfingerprints
[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/integration/img/bitbucket_oauth_settings_page.png b/doc/integration/img/bitbucket_oauth_settings_page.png
index 8dbee9762d7..3e6dea6cfe9 100644
--- a/doc/integration/img/bitbucket_oauth_settings_page.png
+++ b/doc/integration/img/bitbucket_oauth_settings_page.png
Binary files differ
diff --git a/doc/project_services/img/kubernetes_configuration.png b/doc/project_services/img/kubernetes_configuration.png
new file mode 100644
index 00000000000..349a2dc8456
--- /dev/null
+++ b/doc/project_services/img/kubernetes_configuration.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_configuration.png b/doc/project_services/img/mattermost_configuration.png
new file mode 100644
index 00000000000..3c5ff5ee317
--- /dev/null
+++ b/doc/project_services/img/mattermost_configuration.png
Binary files differ
diff --git a/doc/project_services/kubernetes.md b/doc/project_services/kubernetes.md
new file mode 100644
index 00000000000..0c5c88dd983
--- /dev/null
+++ b/doc/project_services/kubernetes.md
@@ -0,0 +1,63 @@
+# GitLab Kubernetes / OpenShift integration
+
+GitLab can be configured to interact with Kubernetes, or other systems using the
+Kubernetes API (such as OpenShift).
+
+Each project can be configured to connect to a different Kubernetes cluster, see
+the [configuration](#configuration) section.
+
+If you have a single cluster that you want to use for all your projects,
+you can pre-fill the settings page with a default template. To configure the
+template, see the [Services Templates](services-templates.md) document.
+
+## Configuration
+
+![Kubernetes configuration settings](img/kubernetes_configuration.png)
+
+The Kubernetes service takes the following arguments:
+
+1. Kubernetes namespace
+1. API URL
+1. Service token
+1. Custom CA bundle
+
+The API URL is the URL that GitLab uses to access the Kubernetes API. Kubernetes
+exposes several APIs - we want the "base" URL that is common to all of them,
+e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`.
+
+GitLab authenticates against Kubernetes using service tokens, which are
+scoped to a particular `namespace`. If you don't have a service token yet,
+you can follow the
+[Kubernetes documentation](http://kubernetes.io/docs/user-guide/service-accounts/)
+to create one. You can also view or create service tokens in the
+[Kubernetes dashboard](http://kubernetes.io/docs/user-guide/ui/) - visit
+`Config -> Secrets`.
+
+Fill in the service token and namespace according to the values you just got.
+If the API is using a self-signed TLS certificate, you'll also need to include
+the `ca.crt` contents as the `Custom CA bundle`.
+
+## Deployment variables
+
+The Kubernetes service exposes following
+[deployment variables](../ci/variables/README.md#deployment-variables) in the
+GitLab CI build environment:
+
+- `KUBE_URL` - equal to the API URL
+- `KUBE_TOKEN`
+- `KUBE_NAMESPACE`
+- `KUBE_CA_PEM` - only if a custom CA bundle was specified
+
+## Terminal support
+
+>**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 online [terminal support](../ci/environments.md#terminal-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`.
diff --git a/doc/project_services/mattermost.md b/doc/project_services/mattermost.md
new file mode 100644
index 00000000000..fbc7dfeee6d
--- /dev/null
+++ b/doc/project_services/mattermost.md
@@ -0,0 +1,45 @@
+# Mattermost Notifications Service
+
+## On Mattermost
+
+To enable Mattermost integration you must create an incoming webhook integration:
+
+1. Sign in to your Mattermost instance
+1. Visit incoming webhooks, that will be something like: https://mattermost.example/your_team_name/integrations/incoming_webhooks/add
+1. Choose a display name, description and channel, those can be overridden on GitLab
+1. Save it, copy the **Webhook URL**, we'll need this later for GitLab.
+
+There might be some cases that Incoming Webhooks are blocked by admin, ask your mattermost admin to enable
+it on https://mattermost.example/admin_console/integrations/custom.
+
+Display name override is not enabled by default, you need to ask your admin to enable it on that same section.
+
+## On GitLab
+
+After you set up Mattermost, it's time to set up GitLab.
+
+Go to your project's **Settings > Services > Mattermost Notifications** and you will see a
+checkbox with the following events that can be triggered:
+
+- Push
+- Issue
+- Merge request
+- Note
+- Tag push
+- Build
+- Wiki page
+
+Bellow each of these event checkboxes, you will have an input field to insert
+which Mattermost channel you want to send that event message, with `#town-square`
+being the default. The hash sign is optional.
+
+At the end, fill in your Mattermost details:
+
+| Field | Description |
+| ----- | ----------- |
+| **Webhook** | The incoming webhooks which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo... |
+| **Username** | Optional username which can be on messages sent to Mattermost. Fill this in if you want to change the username of the bot. |
+| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
+
+
+![Mattermost configuration](img/mattermost_configuration.png)
diff --git a/doc/project_services/mattermost_slash_commands.md b/doc/project_services/mattermost_slash_commands.md
index 6fcbf6f1642..1a7c13a29b4 100644
--- a/doc/project_services/mattermost_slash_commands.md
+++ b/doc/project_services/mattermost_slash_commands.md
@@ -65,7 +65,7 @@ the administrator console.
### Step 3. Create a new custom slash command in Mattermost
-Now that you have enabled the custom slash commands in Mattermost and opened
+Now that you have enabled custom slash commands in Mattermost and opened
the Mattermost slash commands service in GitLab, it's time to copy these values
in a new slash command.
@@ -128,20 +128,16 @@ GitLab using the Mattermost commands.
## Available slash commands
-The available slash commands so far are:
+The available slash commands are:
| Command | Description | Example |
| ------- | ----------- | ------- |
-| `/<trigger> issue create <title>\n<description>` | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | `/trigger issue create We need to change the homepage` |
-| `/<trigger> issue show <issue-number>` | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | `/trigger issue show 42` |
-| `/<trigger> deploy <environment> to <environment>` | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | `/trigger deploy staging to production` |
+| <kbd>/&lt;trigger&gt; issue new &lt;title&gt; <kbd>⇧ Shift</kbd>+<kbd>↵ Enter</kbd> &lt;description&gt;</kbd> | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | <samp>/gitlab issue new We need to change the homepage</samp> |
+| <kbd>/&lt;trigger&gt; issue show &lt;issue-number&gt;</kbd> | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | <samp>/gitlab issue show 42</samp> |
+| <kbd>/&lt;trigger&gt; deploy &lt;environment&gt; to &lt;environment&gt;</kbd> | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | <samp>/gitlab deploy staging to production</samp> |
-To see a list of available commands that can interact with GitLab, type the
-trigger word followed by `help`:
-
-```
-/my-project help
-```
+To see a list of available commands to interact with GitLab, type the
+trigger word followed by <kbd>help</kbd>. Example: <samp>/gitlab help</samp>
![Mattermost bot available commands](img/mattermost_bot_available_commands.png)
diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md
index 890f7525b0e..0f398874b8f 100644
--- a/doc/project_services/project_services.md
+++ b/doc/project_services/project_services.md
@@ -42,11 +42,13 @@ further configuration instructions and details. Contributions are welcome.
| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway |
| [JIRA](jira.md) | JIRA issue tracker |
| JetBrains TeamCity CI | A continuous integration and build server |
+| [Kubernetes](kubernetes.md) | A containerized deployment service |
| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands |
+| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost |
+| [Slack Notifications](slack.md) | Receive event notifications in Slack |
| PivotalTracker | Project Management Software (Source Commits Endpoint) |
| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
| [Redmine](redmine.md) | Redmine issue tracker |
-| [Slack](slack.md) | A team communication tool for the 21st century |
## Services Templates
diff --git a/doc/project_services/slack.md b/doc/project_services/slack.md
index 3cfe77c9f85..0b682b43810 100644
--- a/doc/project_services/slack.md
+++ b/doc/project_services/slack.md
@@ -1,4 +1,4 @@
-# Slack Service
+# Slack Notifications Service
## On Slack
@@ -15,7 +15,7 @@ Slack:
After you set up Slack, it's time to set up GitLab.
-Go to your project's **Settings > Services > Slack** and you will see a
+Go to your project's **Settings > Services > Slack Notifications** and you will see a
checkbox with the following events that can be triggered:
- Push
diff --git a/doc/raketasks/README.md b/doc/raketasks/README.md
index a49c43b8ef2..2b81ebc9c59 100644
--- a/doc/raketasks/README.md
+++ b/doc/raketasks/README.md
@@ -4,7 +4,8 @@
- [Check](check.md)
- [Cleanup](cleanup.md)
- [Features](features.md)
-- [Maintenance](maintenance.md) and self-checks
+- [LDAP Maintenance](../administration/raketasks/ldap.md)
+- [General Maintenance](maintenance.md) and self-checks
- [User management](user_management.md)
- [Webhooks](web_hooks.md)
- [Import](import.md) of git repositories in bulk
diff --git a/doc/update/8.13-to-8.14.md b/doc/update/8.13-to-8.14.md
index a0e895773ce..c64d3407461 100644
--- a/doc/update/8.13-to-8.14.md
+++ b/doc/update/8.13-to-8.14.md
@@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-14-stable-ee
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v4.0.0
+sudo -u git -H git checkout v4.0.3
```
### 6. Update gitlab-workhorse
diff --git a/doc/update/8.14-to-8.15.md b/doc/update/8.14-to-8.15.md
index 5556dae2551..8d4bfd913bd 100644
--- a/doc/update/8.14-to-8.15.md
+++ b/doc/update/8.14-to-8.15.md
@@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-15-stable-ee
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v4.0.3
+sudo -u git -H git checkout v4.1.1
```
### 6. Update gitlab-workhorse
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 4d24eb21976..f6484688721 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -319,6 +319,40 @@ Here's a sample video:
![Sample Video](img/markdown_video.mp4)
+### Math
+
+> If this is not rendered correctly, see
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#math
+
+It is possible to have math written with the LaTeX syntax rendered using [KaTeX][katex].
+
+Math written inside ```$``$``` will be rendered inline with the text.
+
+Math written inside triple back quotes, with the language declared as `math`, will be rendered on a separate line.
+
+Example:
+
+ This math is inline $`a^2+b^2=c^2`$.
+
+ This is on a separate line
+ ```math
+ a^2+b^2=c^2
+ ```
+
+Becomes:
+
+This math is inline $`a^2+b^2=c^2`$.
+
+This is on a separate line
+```math
+a^2+b^2=c^2
+```
+
+_Be advised that KaTeX only supports a [subset][katex-subset] of LaTeX._
+
+>**Note:**
+This also works for the asciidoctor `:stem: latexmath`. For details see the [asciidoctor user manual][asciidoctor-manual].
+
## Standard Markdown
### Headers
@@ -764,3 +798,6 @@ A link starting with a `/` is relative to the wiki root.
[markdown.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md
[rouge]: http://rouge.jneen.net/ "Rouge website"
[redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website"
+[katex]: https://github.com/Khan/KaTeX "KaTeX website"
+[katex-subset]: https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX "Macros supported by KaTeX"
+[asciidoctor-manual]: http://asciidoctor.org/docs/user-manual/#activating-stem-support "Asciidoctor user manual" \ No newline at end of file
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/bitbucket_importer/bitbucket_import_grant_access.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.png
deleted file mode 100644
index df55a081803..00000000000
--- a/doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.png
deleted file mode 100644
index 5253889d251..00000000000
--- a/doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.png
deleted file mode 100644
index ffa87ce5b2e..00000000000
--- a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png
deleted file mode 100644
index 1a5661de75d..00000000000
--- a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/importing/img/bitbucket_import_grant_access.png b/doc/workflow/importing/img/bitbucket_import_grant_access.png
new file mode 100644
index 00000000000..429904e621d
--- /dev/null
+++ b/doc/workflow/importing/img/bitbucket_import_grant_access.png
Binary files differ
diff --git a/doc/workflow/importing/img/bitbucket_import_new_project.png b/doc/workflow/importing/img/bitbucket_import_new_project.png
new file mode 100644
index 00000000000..8ed528c2f09
--- /dev/null
+++ b/doc/workflow/importing/img/bitbucket_import_new_project.png
Binary files differ
diff --git a/doc/workflow/importing/img/bitbucket_import_select_project.png b/doc/workflow/importing/img/bitbucket_import_select_project.png
new file mode 100644
index 00000000000..1bca6166ec8
--- /dev/null
+++ b/doc/workflow/importing/img/bitbucket_import_select_project.png
Binary files differ
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/img/import_projects_from_github_new_project_page.png b/doc/workflow/importing/img/import_projects_from_github_new_project_page.png
deleted file mode 100644
index b23ade4480c..00000000000
--- a/doc/workflow/importing/img/import_projects_from_github_new_project_page.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png b/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png
index f50d9266991..1ccb38a815e 100644
--- a/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png
+++ b/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png
Binary files differ
diff --git a/doc/workflow/importing/img/import_projects_from_new_project_page.png b/doc/workflow/importing/img/import_projects_from_new_project_page.png
new file mode 100644
index 00000000000..97ca30b2087
--- /dev/null
+++ b/doc/workflow/importing/img/import_projects_from_new_project_page.png
Binary files differ
diff --git a/doc/workflow/importing/import_projects_from_bitbucket.md b/doc/workflow/importing/import_projects_from_bitbucket.md
index 520c4216295..97380bce172 100644
--- a/doc/workflow/importing/import_projects_from_bitbucket.md
+++ b/doc/workflow/importing/import_projects_from_bitbucket.md
@@ -1,26 +1,62 @@
# Import your project from Bitbucket to GitLab
-It takes just a few steps to import your existing Bitbucket projects to GitLab. But keep in mind that it is possible only if Bitbucket support is enabled on your GitLab instance. You can read more about Bitbucket support [here](../../integration/bitbucket.md).
+Import your projects from Bitbucket to GitLab with minimal effort.
-* Sign in to GitLab.com and go to your dashboard
+## Overview
-* Click on "New project"
+>**Note:**
+The [Bitbucket integration][bb-import] must be first enabled in order to be
+able to import your projects from Bitbucket. Ask your GitLab administrator
+to enable this if not already.
-![New project in GitLab](bitbucket_importer/bitbucket_import_new_project.png)
+- At its current state, the Bitbucket importer can import:
+ - the repository description (GitLab 7.7+)
+ - the Git repository data (GitLab 7.7+)
+ - the issues (GitLab 7.7+)
+ - the issue comments (GitLab 8.15+)
+ - the pull requests (GitLab 8.4+)
+ - the pull request comments (GitLab 8.15+)
+ - the milestones (GitLab 8.15+)
+ - the wiki (GitLab 8.15+)
+- References to pull requests and issues are preserved (GitLab 8.7+)
+- Repository public access is retained. If a repository is private in Bitbucket
+ it will be created as private in GitLab as well.
-* Click on the "Bitbucket" button
-![Bitbucket](bitbucket_importer/bitbucket_import_select_bitbucket.png)
+## How it works
-* Grant GitLab access to your Bitbucket account
+When issues/pull requests are being imported, the Bitbucket importer tries to find
+the Bitbucket author/assignee in GitLab's database using the Bitbucket ID. For this
+to work, the Bitbucket author/assignee should have signed in beforehand in GitLab
+and [**associated their Bitbucket account**][social sign-in]. If the user is not
+found in GitLab's database, the project creator (most of the times the current
+user that started the import process) is set as the author, but a reference on
+the issue about the original Bitbucket author is kept.
-![Grant access](bitbucket_importer/bitbucket_import_grant_access.png)
+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.
-* Click on the projects that you'd like to import or "Import all projects"
+## Importing your Bitbucket repositories
-![Import projects](bitbucket_importer/bitbucket_import_select_project.png)
+1. Sign in to GitLab and go to your dashboard.
+1. Click on **New project**.
-A new GitLab project will be created with your imported data.
+ ![New project in GitLab](img/bitbucket_import_new_project.png)
-### Note
-Milestones and wiki pages are not imported from Bitbucket.
+1. Click on the "Bitbucket" button
+
+ ![Bitbucket](img/import_projects_from_new_project_page.png)
+
+1. Grant GitLab access to your Bitbucket account
+
+ ![Grant access](img/bitbucket_import_grant_access.png)
+
+1. Click on the projects that you'd like to import or **Import all projects**.
+ You can also select the namespace under which each project will be
+ imported.
+
+ ![Import projects](img/bitbucket_import_select_project.png)
+
+[bb-import]: ../../integration/bitbucket.md
+[social sign-in]: ../../user/profile/account/social_sign_in.md
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 c36dfdb78ec..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+)
@@ -40,7 +41,7 @@ namespace that started the import process.
The importer page is visible when you create a new project.
-![New project page on GitLab](img/import_projects_from_github_new_project_page.png)
+![New project page on GitLab](img/import_projects_from_new_project_page.png)
Click on the **GitHub** link and the import authorization process will start.
There are two ways to authorize access to your GitHub repositories:
@@ -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/active_tab.feature b/features/admin/active_tab.feature
deleted file mode 100644
index f5bb06dea7d..00000000000
--- a/features/admin/active_tab.feature
+++ /dev/null
@@ -1,54 +0,0 @@
-@admin
-Feature: Admin Active Tab
- Background:
- Given I sign in as an admin
-
- Scenario: On Admin Home
- Given I visit admin page
- Then the active main tab should be Overview
- And no other main tabs should be active
-
- Scenario: On Admin Projects
- Given I visit admin projects page
- Then the active main tab should be Overview
- And the active sub tab should be Projects
- And no other main tabs should be active
- And no other sub tabs should be active
-
- Scenario: On Admin Groups
- Given I visit admin groups page
- Then the active main tab should be Overview
- And the active sub tab should be Groups
- And no other main tabs should be active
- And no other sub tabs should be active
-
- Scenario: On Admin Users
- Given I visit admin users page
- Then the active main tab should be Overview
- And the active sub tab should be Users
- And no other main tabs should be active
- And no other sub tabs should be active
-
- Scenario: On Admin Logs
- Given I visit admin logs page
- Then the active main tab should be Monitoring
- And the active sub tab should be Logs
- And no other main tabs should be active
- And no other sub tabs should be active
-
- Scenario: On Admin Messages
- Given I visit admin messages page
- Then the active main tab should be Messages
- And no other main tabs should be active
-
- Scenario: On Admin Hooks
- Given I visit admin hooks page
- Then the active main tab should be Hooks
- And no other main tabs should be active
-
- Scenario: On Admin Resque
- Given I visit admin Resque page
- Then the active main tab should be Monitoring
- And the active sub tab should be Resque
- And no other main tabs should be active
- And no other sub tabs should be active
diff --git a/features/admin/appearance.feature b/features/admin/appearance.feature
deleted file mode 100644
index 5c1dd7531c1..00000000000
--- a/features/admin/appearance.feature
+++ /dev/null
@@ -1,37 +0,0 @@
-Feature: Admin Appearance
- Scenario: Create new appearance
- Given I sign in as an admin
- And I visit admin appearance page
- When submit form with new appearance
- Then I should be redirected to admin appearance page
- And I should see newly created appearance
-
- Scenario: Preview appearance
- Given application has custom appearance
- And I sign in as an admin
- When I visit admin appearance page
- And I click preview button
- Then I should see a customized appearance
-
- Scenario: Custom sign-in page
- Given application has custom appearance
- When I visit login page
- Then I should see a customized appearance
-
- Scenario: Appearance logo
- Given application has custom appearance
- And I sign in as an admin
- And I visit admin appearance page
- When I attach a logo
- Then I should see a logo
- And I remove the logo
- Then I should see logo removed
-
- Scenario: Header logos
- Given application has custom appearance
- And I sign in as an admin
- And I visit admin appearance page
- When I attach header logos
- Then I should see header logos
- And I remove the header logos
- Then I should see header logos removed
diff --git a/features/admin/applications.feature b/features/admin/applications.feature
deleted file mode 100644
index 2a00e1666c0..00000000000
--- a/features/admin/applications.feature
+++ /dev/null
@@ -1,18 +0,0 @@
-@admin
-Feature: Admin Applications
- Background:
- Given I sign in as an admin
- And I visit applications page
-
- Scenario: I can manage application
- Then I click on new application button
- And I should see application form
- Then I fill application form out and submit
- And I see application
- Then I click edit
- And I see edit application form
- Then I change name of application and submit
- And I see that application was changed
- Then I visit applications page
- And I click to remove application
- Then I see that application is removed \ No newline at end of file
diff --git a/features/admin/broadcast_messages.feature b/features/admin/broadcast_messages.feature
deleted file mode 100644
index 4f9c651561e..00000000000
--- a/features/admin/broadcast_messages.feature
+++ /dev/null
@@ -1,33 +0,0 @@
-@admin
-Feature: Admin Broadcast Messages
- Background:
- Given I sign in as an admin
- And application already has a broadcast message
- And I visit admin messages page
-
- Scenario: See broadcast messages list
- Then I should see all broadcast messages
-
- Scenario: Create a customized broadcast message
- When submit form with new customized broadcast message
- Then I should be redirected to admin messages page
- And I should see newly created broadcast message
- Then I visit dashboard page
- And I should see a customized broadcast message
-
- Scenario: Edit an existing broadcast message
- When I edit an existing broadcast message
- And I change the broadcast message text
- Then I should be redirected to admin messages page
- And I should see the updated broadcast message
-
- Scenario: Remove an existing broadcast message
- When I remove an existing broadcast message
- Then I should be redirected to admin messages page
- And I should not see the removed broadcast message
-
- @javascript
- Scenario: Live preview a customized broadcast message
- When I visit admin messages page
- And I enter a broadcast message with Markdown
- Then I should see a live preview of the rendered broadcast message
diff --git a/features/admin/labels.feature b/features/admin/labels.feature
deleted file mode 100644
index 1af0e700bd4..00000000000
--- a/features/admin/labels.feature
+++ /dev/null
@@ -1,38 +0,0 @@
-Feature: Admin Issues Labels
- Background:
- Given I sign in as an admin
- And I have labels: "bug", "feature", "enhancement"
- Given I visit admin labels page
-
- Scenario: I should see labels list
- Then I should see label 'bug'
- And I should see label 'feature'
-
- Scenario: I create new label
- Given I submit new label 'support'
- Then I should see label 'support'
-
- Scenario: I edit label
- Given I visit 'bug' label edit page
- When I change label 'bug' to 'fix'
- Then I should not see label 'bug'
- Then I should see label 'fix'
-
- Scenario: I remove label
- When I remove label 'bug'
- Then I should not see label 'bug'
-
- @javascript
- Scenario: I delete all labels
- When I delete all labels
- Then I should see labels help message
-
- Scenario: I create a label with invalid color
- Given I visit admin new label page
- When I submit new label with invalid color
- Then I should see label color error message
-
- Scenario: I create a label that already exists
- Given I visit admin new label page
- When I submit new label 'bug'
- Then I should see label exist error message
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/admin/settings.feature b/features/admin/settings.feature
deleted file mode 100644
index e38eea2cfed..00000000000
--- a/features/admin/settings.feature
+++ /dev/null
@@ -1,19 +0,0 @@
-@admin
-Feature: Admin Settings
- Background:
- Given I sign in as an admin
- And I visit admin settings page
-
- Scenario: Change application settings
- When I modify settings and save form
- Then I should see application settings saved
-
- Scenario: Change Slack Service Template settings
- When I click on "Service Templates"
- And I click on "Slack" service
- And I fill out Slack settings
- Then I check all events and submit form
- And I should see service template settings saved
- Then I click on "Slack" service
- And I should see all checkboxes checked
- And I should see Slack settings saved
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/active_tab.rb b/features/steps/admin/active_tab.rb
deleted file mode 100644
index 9b1689a8198..00000000000
--- a/features/steps/admin/active_tab.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-class Spinach::Features::AdminActiveTab < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedActiveTab
-
- step 'the active main tab should be Overview' do
- ensure_active_main_tab('Overview')
- end
-
- step 'the active sub tab should be Projects' do
- ensure_active_sub_tab('Projects')
- end
-
- step 'the active sub tab should be Groups' do
- ensure_active_sub_tab('Groups')
- end
-
- step 'the active sub tab should be Users' do
- ensure_active_sub_tab('Users')
- end
-
- step 'the active main tab should be Hooks' do
- ensure_active_main_tab('Hooks')
- end
-
- step 'the active main tab should be Monitoring' do
- ensure_active_main_tab('Monitoring')
- end
-
- step 'the active sub tab should be Resque' do
- ensure_active_sub_tab('Background Jobs')
- end
-
- step 'the active sub tab should be Logs' do
- ensure_active_sub_tab('Logs')
- end
-
- step 'the active main tab should be Messages' do
- ensure_active_main_tab('Messages')
- end
-end
diff --git a/features/steps/admin/appearance.rb b/features/steps/admin/appearance.rb
deleted file mode 100644
index 0d1be46d11d..00000000000
--- a/features/steps/admin/appearance.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-class Spinach::Features::AdminAppearance < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
-
- step 'submit form with new appearance' do
- fill_in 'appearance_title', with: 'MyCompany'
- fill_in 'appearance_description', with: 'dev server'
- click_button 'Save'
- end
-
- step 'I should be redirected to admin appearance page' do
- expect(current_path).to eq admin_appearances_path
- expect(page).to have_content 'Appearance settings'
- end
-
- step 'I should see newly created appearance' do
- expect(page).to have_field('appearance_title', with: 'MyCompany')
- expect(page).to have_field('appearance_description', with: 'dev server')
- expect(page).to have_content 'Last edit'
- end
-
- step 'I click preview button' do
- click_link "Preview"
- end
-
- step 'application has custom appearance' do
- create(:appearance)
- end
-
- step 'I should see a customized appearance' do
- expect(page).to have_content appearance.title
- expect(page).to have_content appearance.description
- end
-
- step 'I attach a logo' do
- attach_file(:appearance_logo, Rails.root.join('spec', 'fixtures', 'dk.png'))
- click_button 'Save'
- end
-
- step 'I attach header logos' do
- attach_file(:appearance_header_logo, Rails.root.join('spec', 'fixtures', 'dk.png'))
- click_button 'Save'
- end
-
- step 'I should see a logo' do
- expect(page).to have_xpath('//img[@src="/uploads/appearance/logo/1/dk.png"]')
- end
-
- step 'I should see header logos' do
- expect(page).to have_xpath('//img[@src="/uploads/appearance/header_logo/1/dk.png"]')
- end
-
- step 'I remove the logo' do
- click_link 'Remove logo'
- end
-
- step 'I remove the header logos' do
- click_link 'Remove header logo'
- end
-
- step 'I should see logo removed' do
- expect(page).not_to have_xpath('//img[@src="/uploads/appearance/logo/1/gitlab_logo.png"]')
- end
-
- step 'I should see header logos removed' do
- expect(page).not_to have_xpath('//img[@src="/uploads/appearance/header_logo/1/header_logo_light.png"]')
- end
-
- def appearance
- Appearance.last
- end
-end
diff --git a/features/steps/admin/applications.rb b/features/steps/admin/applications.rb
deleted file mode 100644
index 7c12cb96921..00000000000
--- a/features/steps/admin/applications.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-class Spinach::Features::AdminApplications < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedAdmin
-
- step 'I click on new application button' do
- click_on 'New Application'
- end
-
- step 'I should see application form' do
- expect(page).to have_content "New application"
- end
-
- step 'I fill application form out and submit' do
- fill_in :doorkeeper_application_name, with: 'test'
- fill_in :doorkeeper_application_redirect_uri, with: 'https://test.com'
- click_on "Submit"
- end
-
- step 'I see application' do
- expect(page).to have_content "Application: test"
- expect(page).to have_content "Application Id"
- expect(page).to have_content "Secret"
- end
-
- step 'I click edit' do
- click_on "Edit"
- end
-
- step 'I see edit application form' do
- expect(page).to have_content "Edit application"
- end
-
- step 'I change name of application and submit' do
- expect(page).to have_content "Edit application"
- fill_in :doorkeeper_application_name, with: 'test_changed'
- click_on "Submit"
- end
-
- step 'I see that application was changed' do
- expect(page).to have_content "test_changed"
- expect(page).to have_content "Application Id"
- expect(page).to have_content "Secret"
- end
-
- step 'I click to remove application' do
- page.within '.oauth-applications' do
- click_on "Destroy"
- end
- end
-
- step "I see that application is removed" do
- expect(page.find(".oauth-applications")).not_to have_content "test_changed"
- end
-end
diff --git a/features/steps/admin/broadcast_messages.rb b/features/steps/admin/broadcast_messages.rb
deleted file mode 100644
index af2b4a29313..00000000000
--- a/features/steps/admin/broadcast_messages.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-class Spinach::Features::AdminBroadcastMessages < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
-
- step 'application already has a broadcast message' do
- FactoryGirl.create(:broadcast_message, :expired, message: "Migration to new server")
- end
-
- step 'I should see all broadcast messages' do
- expect(page).to have_content "Migration to new server"
- end
-
- step 'I should be redirected to admin messages page' do
- expect(current_path).to eq admin_broadcast_messages_path
- end
-
- step 'I should see newly created broadcast message' do
- expect(page).to have_content 'Application update from 4:00 CST to 5:00 CST'
- end
-
- step 'submit form with new customized broadcast message' do
- fill_in 'broadcast_message_message', with: 'Application update from **4:00 CST to 5:00 CST**'
- fill_in 'broadcast_message_color', with: '#f2dede'
- fill_in 'broadcast_message_font', with: '#b94a48'
- select Date.today.next_year.year, from: "broadcast_message_ends_at_1i"
- click_button "Add broadcast message"
- end
-
- step 'I should see a customized broadcast message' do
- expect(page).to have_content 'Application update from 4:00 CST to 5:00 CST'
- expect(page).to have_selector 'strong', text: '4:00 CST to 5:00 CST'
- expect(page).to have_selector %(div[style="background-color: #f2dede; color: #b94a48"])
- end
-
- step 'I edit an existing broadcast message' do
- click_link 'Edit'
- end
-
- step 'I change the broadcast message text' do
- fill_in 'broadcast_message_message', with: 'Application update RIGHT NOW'
- click_button 'Update broadcast message'
- end
-
- step 'I should see the updated broadcast message' do
- expect(page).to have_content "Application update RIGHT NOW"
- end
-
- step 'I remove an existing broadcast message' do
- click_link 'Remove'
- end
-
- step 'I should not see the removed broadcast message' do
- expect(page).not_to have_content 'Migration to new server'
- end
-
- step 'I enter a broadcast message with Markdown' do
- fill_in 'broadcast_message_message', with: "Live **Markdown** previews. :tada:"
- end
-
- step 'I should see a live preview of the rendered broadcast message' do
- page.within('.broadcast-message-preview') do
- expect(page).to have_selector('strong', text: 'Markdown')
- expect(page).to have_selector('img.emoji')
- end
- end
-end
diff --git a/features/steps/admin/labels.rb b/features/steps/admin/labels.rb
deleted file mode 100644
index 55ddcc25085..00000000000
--- a/features/steps/admin/labels.rb
+++ /dev/null
@@ -1,117 +0,0 @@
-class Spinach::Features::AdminIssuesLabels < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
-
- step 'I visit \'bug\' label edit page' do
- visit edit_admin_label_path(bug_label)
- end
-
- step 'I visit admin new label page' do
- visit new_admin_label_path
- end
-
- step 'I visit admin labels page' do
- visit admin_labels_path
- end
-
- step 'I remove label \'bug\'' do
- page.within "#label_#{bug_label.id}" do
- click_link 'Delete'
- end
- end
-
- step 'I have labels: "bug", "feature", "enhancement"' do
- ["bug", "feature", "enhancement"].each do |title|
- Label.create(title: title, template: true)
- end
- end
-
- step 'I delete all labels' do
- page.within '.labels' do
- page.all('.btn-remove').each do |remove|
- remove.click
- sleep 0.05
- end
- end
- end
-
- step 'I should see labels help message' do
- page.within '.labels' do
- expect(page).to have_content 'There are no labels yet'
- end
- end
-
- step 'I submit new label \'support\'' do
- visit new_admin_label_path
- fill_in 'Title', with: 'support'
- fill_in 'Background color', with: '#F95610'
- click_button 'Save'
- end
-
- step 'I submit new label \'bug\'' do
- visit new_admin_label_path
- fill_in 'Title', with: 'bug'
- fill_in 'Background color', with: '#F95610'
- click_button 'Save'
- end
-
- step 'I submit new label with invalid color' do
- visit new_admin_label_path
- fill_in 'Title', with: 'support'
- fill_in 'Background color', with: '#12'
- click_button 'Save'
- end
-
- step 'I should see label exist error message' do
- page.within '.label-form' do
- expect(page).to have_content 'Title has already been taken'
- end
- end
-
- step 'I should see label color error message' do
- page.within '.label-form' do
- expect(page).to have_content 'Color must be a valid color code'
- end
- end
-
- step 'I should see label \'feature\'' do
- page.within '.manage-labels-list' do
- expect(page).to have_content 'feature'
- end
- end
-
- step 'I should see label \'bug\'' do
- page.within '.manage-labels-list' do
- expect(page).to have_content 'bug'
- end
- end
-
- step 'I should not see label \'bug\'' do
- page.within '.manage-labels-list' do
- expect(page).not_to have_content 'bug'
- end
- end
-
- step 'I should see label \'support\'' do
- page.within '.manage-labels-list' do
- expect(page).to have_content 'support'
- end
- end
-
- step 'I change label \'bug\' to \'fix\'' do
- fill_in 'Title', with: 'fix'
- fill_in 'Background color', with: '#F15610'
- click_button 'Save'
- end
-
- step 'I should see label \'fix\'' do
- page.within '.manage-labels-list' do
- expect(page).to have_content 'fix'
- end
- end
-
- def bug_label
- Label.templates.find_or_create_by(title: 'bug')
- end
-end
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/group/milestones.rb b/features/steps/group/milestones.rb
index f5fddab357d..c1d1eca9116 100644
--- a/features/steps/group/milestones.rb
+++ b/features/steps/group/milestones.rb
@@ -131,5 +131,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
issue.labels << project.labels.find_by(title: 'bug')
issue.labels << project.labels.find_by(title: 'feature')
end
+
+ current_user.refresh_authorized_projects
end
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/features/steps/project/snippets.rb b/features/steps/project/snippets.rb
index 5e7d539add6..a3bebfa4b71 100644
--- a/features/steps/project/snippets.rb
+++ b/features/steps/project/snippets.rb
@@ -22,7 +22,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps
end
step 'I click link "New snippet"' do
- click_link "New snippet"
+ first(:link, "New snippet").click
end
step 'I click link "Snippet one"' do
diff --git a/features/steps/shared/authentication.rb b/features/steps/shared/authentication.rb
index 735e0ef6108..5c3e724746b 100644
--- a/features/steps/shared/authentication.rb
+++ b/features/steps/shared/authentication.rb
@@ -33,6 +33,6 @@ module SharedAuthentication
end
def current_user
- @user || User.first
+ @user || User.reorder(nil).first
end
end
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index 2bd8ea745e4..15b81fa529b 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -168,7 +168,7 @@ module SharedPaths
end
step 'I visit admin projects page' do
- visit admin_namespaces_projects_path
+ visit admin_projects_path
end
step 'I visit admin users page' do
@@ -195,26 +195,14 @@ module SharedPaths
visit admin_groups_path
end
- step 'I visit admin appearance page' do
- visit admin_appearances_path
- end
-
step 'I visit admin teams page' do
visit admin_teams_path
end
- step 'I visit admin settings page' do
- visit admin_application_settings_path
- end
-
step 'I visit spam logs page' do
visit admin_spam_logs_path
end
- step 'I visit applications page' do
- visit admin_applications_path
- end
-
# ----------------------------------------
# Generic Project
# ----------------------------------------
diff --git a/lib/api/api.rb b/lib/api/api.rb
index cec2702e44d..9d5adffd8f4 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -3,6 +3,8 @@ module API
include APIGuard
version 'v3', using: :path
+ before { allow_access_with_scope :api }
+
rescue_from Gitlab::Access::AccessDeniedError do
rack_response({ 'message' => '403 Forbidden' }.to_json, 403)
end
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index 8cc7a26f1fa..df6db140d0e 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -6,6 +6,9 @@ module API
module APIGuard
extend ActiveSupport::Concern
+ PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
+ PRIVATE_TOKEN_PARAM = :private_token
+
included do |base|
# OAuth2 Resource Server Authentication
use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request|
@@ -44,27 +47,60 @@ module API
access_token = find_access_token
return nil unless access_token
- case validate_access_token(access_token, scopes)
- when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
+ case AccessTokenValidationService.new(access_token).validate(scopes: scopes)
+ when AccessTokenValidationService::INSUFFICIENT_SCOPE
raise InsufficientScopeError.new(scopes)
- when Oauth2::AccessTokenValidationService::EXPIRED
+ when AccessTokenValidationService::EXPIRED
raise ExpiredError
- when Oauth2::AccessTokenValidationService::REVOKED
+ when AccessTokenValidationService::REVOKED
raise RevokedError
- when Oauth2::AccessTokenValidationService::VALID
+ when AccessTokenValidationService::VALID
@current_user = User.find(access_token.resource_owner_id)
end
end
+ def find_user_by_private_token(scopes: [])
+ token_string = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
+
+ return nil unless token_string.present?
+
+ find_user_by_authentication_token(token_string) || find_user_by_personal_access_token(token_string, scopes)
+ end
+
def current_user
@current_user
end
+ # Set the authorization scope(s) allowed for the current request.
+ #
+ # Note: A call to this method adds to any previous scopes in place. This is done because
+ # `Grape` callbacks run from the outside-in: the top-level callback (API::API) runs first, then
+ # the next-level callback (API::API::Users, for example) runs. All these scopes are valid for the
+ # given endpoint (GET `/api/users` is accessible by the `api` and `read_user` scopes), and so they
+ # need to be stored.
+ def allow_access_with_scope(*scopes)
+ @scopes ||= []
+ @scopes.concat(scopes.map(&:to_s))
+ end
+
private
+ def find_user_by_authentication_token(token_string)
+ User.find_by_authentication_token(token_string)
+ end
+
+ def find_user_by_personal_access_token(token_string, scopes)
+ access_token = PersonalAccessToken.active.find_by_token(token_string)
+ return unless access_token
+
+ if AccessTokenValidationService.new(access_token).include_any_scope?(scopes)
+ User.find(access_token.user_id)
+ end
+ end
+
def find_access_token
@access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods)
end
@@ -72,10 +108,6 @@ module API
def doorkeeper_request
@doorkeeper_request ||= ActionDispatch::Request.new(env)
end
-
- def validate_access_token(access_token, scopes)
- Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes)
- end
end
module ClassMethods
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 2670a2d413a..cf2489dbb67 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -1,7 +1,6 @@
require 'mime/types'
module API
- # Projects commits API
class Commits < Grape::API
include PaginationParams
@@ -121,6 +120,41 @@ module API
present paginate(notes), with: Entities::CommitNote
end
+ desc 'Cherry pick commit into a branch' do
+ detail 'This feature was introduced in GitLab 8.15'
+ success Entities::RepoCommit
+ end
+ params do
+ requires :sha, type: String, desc: 'A commit sha to be cherry picked'
+ requires :branch, type: String, desc: 'The name of the branch'
+ end
+ post ':id/repository/commits/:sha/cherry_pick' do
+ authorize! :push_code, user_project
+
+ commit = user_project.commit(params[:sha])
+ not_found!('Commit') unless commit
+
+ branch = user_project.repository.find_branch(params[:branch])
+ not_found!('Branch') unless branch
+
+ commit_params = {
+ commit: commit,
+ create_merge_request: false,
+ source_project: user_project,
+ source_branch: commit.cherry_pick_branch_name,
+ target_branch: params[:branch]
+ }
+
+ result = ::Commits::CherryPickService.new(user_project, current_user, commit_params).execute
+
+ if result[:status] == :success
+ branch = user_project.repository.find_branch(params[:branch])
+ present user_project.repository.commit(branch.dereferenced_target), with: Entities::RepoCommit
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
+
desc 'Post comment to commit' do
success Entities::CommitNote
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index bb8d740532d..cd5c4c4b3ce 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -629,7 +629,7 @@ module API
end
class EnvironmentBasic < Grape::Entity
- expose :id, :name, :external_url
+ expose :id, :name, :slug, :external_url
end
class Environment < EnvironmentBasic
diff --git a/lib/api/environments.rb b/lib/api/environments.rb
index 80bbd9bb6e4..1a7e68f0528 100644
--- a/lib/api/environments.rb
+++ b/lib/api/environments.rb
@@ -1,6 +1,7 @@
module API
# Environments RESTfull API endpoints
class Environments < Grape::API
+ include ::API::Helpers::CustomValidators
include PaginationParams
before { authenticate! }
@@ -29,6 +30,7 @@ module API
params do
requires :name, type: String, desc: 'The name of the environment to be created'
optional :external_url, type: String, desc: 'URL on which this deployment is viewable'
+ optional :slug, absence: { message: "is automatically generated and cannot be changed" }
end
post ':id/environments' do
authorize! :create_environment, user_project
@@ -50,6 +52,7 @@ module API
requires :environment_id, type: Integer, desc: 'The environment ID'
optional :name, type: String, desc: 'The new environment name'
optional :external_url, type: String, desc: 'The new URL on which this deployment is viewable'
+ optional :slug, absence: { message: "is automatically generated and cannot be changed" }
end
put ':id/environments/:environment_id' do
authorize! :update_environment, user_project
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 105d3ee342e..9b9d3df7435 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -125,13 +125,16 @@ module API
default: 'created_at', desc: 'Return projects ordered by field'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return projects sorted in ascending and descending order'
+ optional :simple, type: Boolean, default: false,
+ desc: 'Return only the ID, URL, name, and path of each project'
use :pagination
end
get ":id/projects" do
group = find_group!(params[:id])
projects = GroupProjectsFinder.new(group).execute(current_user)
projects = filter_projects(projects)
- present paginate(projects), with: Entities::Project, user: current_user
+ entity = params[:simple] ? Entities::BasicProjectDetails : Entities::Project
+ present paginate(projects), with: entity, user: current_user
end
desc 'Transfer a project to the group namespace. Available only for admin.' do
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 8b0f8deadfa..4be659fc20b 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -2,72 +2,26 @@ module API
module Helpers
include Gitlab::Utils
- PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
- PRIVATE_TOKEN_PARAM = :private_token
SUDO_HEADER = "HTTP_SUDO"
SUDO_PARAM = :sudo
- def private_token
- params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]
- end
-
- def warden
- env['warden']
- end
-
- # Check the Rails session for valid authentication details
- #
- # Until CSRF protection is added to the API, disallow this method for
- # state-changing endpoints
- def find_user_from_warden
- warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD'])
- end
-
def declared_params(options = {})
options = { include_parent_namespaces: false }.merge(options)
declared(params, options).to_h.symbolize_keys
end
- def find_user_by_private_token
- token = private_token
- return nil unless token.present?
-
- User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
- end
-
def current_user
- @current_user ||= find_user_by_private_token
- @current_user ||= doorkeeper_guard
- @current_user ||= find_user_from_warden
+ return @current_user if defined?(@current_user)
- unless @current_user && Gitlab::UserAccess.new(@current_user).allowed?
- return nil
- end
-
- identifier = sudo_identifier
+ @current_user = initial_current_user
- if identifier
- # We check for private_token because we cannot allow PAT to be used
- forbidden!('Must be admin to use sudo') unless @current_user.is_admin?
- forbidden!('Private token must be specified in order to use sudo') unless private_token_used?
-
- @impersonator = @current_user
- @current_user = User.by_username_or_id(identifier)
- not_found!("No user id or username for: #{identifier}") if @current_user.nil?
- end
+ sudo!
@current_user
end
- def sudo_identifier
- identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER]
-
- # Regex for integers
- if !!(identifier =~ /\A[0-9]+\z/)
- identifier.to_i
- else
- identifier
- end
+ def sudo?
+ initial_current_user != current_user
end
def user_project
@@ -78,6 +32,14 @@ module API
@available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute
end
+ def find_user(id)
+ if id =~ /^\d+$/
+ User.find_by(id: id)
+ else
+ User.find_by(username: id)
+ end
+ end
+
def find_project(id)
if id =~ /^\d+$/
Project.find_by(id: id)
@@ -96,17 +58,6 @@ module API
end
end
- def project_service(project = user_project)
- @project_service ||= project.find_or_initialize_service(params[:service_slug].underscore)
- @project_service || not_found!("Service")
- end
-
- def service_attributes
- @service_attributes ||= project_service.fields.inject([]) do |arr, hash|
- arr << hash[:name].to_sym
- end
- end
-
def find_group(id)
if id =~ /^\d+$/
Group.find_by(id: id)
@@ -354,6 +305,62 @@ module API
private
+ def private_token
+ params[APIGuard::PRIVATE_TOKEN_PARAM] || env[APIGuard::PRIVATE_TOKEN_HEADER]
+ end
+
+ def warden
+ env['warden']
+ end
+
+ # Check the Rails session for valid authentication details
+ #
+ # Until CSRF protection is added to the API, disallow this method for
+ # state-changing endpoints
+ def find_user_from_warden
+ warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD'])
+ end
+
+ def initial_current_user
+ return @initial_current_user if defined?(@initial_current_user)
+
+ @initial_current_user ||= find_user_by_private_token(scopes: @scopes)
+ @initial_current_user ||= doorkeeper_guard(scopes: @scopes)
+ @initial_current_user ||= find_user_from_warden
+
+ unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed?
+ @initial_current_user = nil
+ end
+
+ @initial_current_user
+ end
+
+ def sudo!
+ return unless sudo_identifier
+ return unless initial_current_user
+
+ unless initial_current_user.is_admin?
+ forbidden!('Must be admin to use sudo')
+ end
+
+ # Only private tokens should be used for the SUDO feature
+ unless private_token == initial_current_user.private_token
+ forbidden!('Private token must be specified in order to use sudo')
+ end
+
+ sudoed_user = find_user(sudo_identifier)
+
+ if sudoed_user
+ @current_user = sudoed_user
+ else
+ not_found!("No user id or username for: #{sudo_identifier}")
+ end
+ end
+
+ def sudo_identifier
+ @sudo_identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER]
+ end
+
def add_pagination_headers(paginated_data)
header 'X-Total', paginated_data.total_count.to_s
header 'X-Total-Pages', paginated_data.total_pages.to_s
@@ -386,10 +393,6 @@ module API
links.join(', ')
end
- def private_token_used?
- private_token == @current_user.private_token
- end
-
def secret_token
Gitlab::Shell.secret_token
end
diff --git a/lib/api/helpers/custom_validators.rb b/lib/api/helpers/custom_validators.rb
new file mode 100644
index 00000000000..0a8f3073a50
--- /dev/null
+++ b/lib/api/helpers/custom_validators.rb
@@ -0,0 +1,14 @@
+module API
+ module Helpers
+ module CustomValidators
+ class Absence < Grape::Validations::Base
+ def validate_param!(attr_name, params)
+ return if params.respond_to?(:key?) && !params.key?(attr_name)
+ raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:absence)
+ end
+ end
+ end
+ end
+end
+
+Grape::Validations.register_validator(:absence, ::API::Helpers::CustomValidators::Absence)
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index eb223c1101d..e8975eb57e0 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -52,6 +52,14 @@ module API
:push_code
]
end
+
+ def parse_allowed_environment_variables
+ return if params[:env].blank?
+
+ JSON.parse(params[:env])
+
+ rescue JSON::ParserError
+ end
end
end
end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 7087ce11401..db2d18f935d 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -32,7 +32,11 @@ module API
if wiki?
Gitlab::GitAccessWiki.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
else
- Gitlab::GitAccess.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
+ Gitlab::GitAccess.new(actor,
+ project,
+ protocol,
+ authentication_abilities: ssh_authentication_abilities,
+ env: parse_allowed_environment_variables)
end
access_status = access.check(params[:action], params[:changes])
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 55bdbc6a47c..5d1fe22f2df 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -143,8 +143,8 @@ module API
success Entities::MergeRequest
end
params do
- optional :title, type: String, desc: 'The title of the merge request'
- optional :target_branch, type: String, desc: 'The target branch'
+ optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
+ optional :target_branch, type: String, allow_blank: false, desc: 'The target branch'
optional :state_event, type: String, values: %w[close reopen merge],
desc: 'Status of the merge request'
use :optional_params
diff --git a/lib/api/services.rb b/lib/api/services.rb
index bc427705777..aa97f6af0b2 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -1,84 +1,645 @@
module API
- # Projects API
class Services < Grape::API
+ services = {
+ 'asana' => [
+ {
+ required: true,
+ name: :api_key,
+ type: String,
+ desc: 'User API token'
+ },
+ {
+ required: false,
+ name: :restrict_to_branch,
+ type: String,
+ desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches'
+ }
+ ],
+ 'assembla' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The authentication token'
+ },
+ {
+ required: false,
+ name: :subdomain,
+ type: String,
+ desc: 'Subdomain setting'
+ }
+ ],
+ 'bamboo' => [
+ {
+ required: true,
+ name: :bamboo_url,
+ type: String,
+ desc: 'Bamboo root URL like https://bamboo.example.com'
+ },
+ {
+ required: true,
+ name: :build_key,
+ type: String,
+ desc: 'Bamboo build plan key like'
+ },
+ {
+ required: true,
+ name: :username,
+ type: String,
+ desc: 'A user with API access, if applicable'
+ },
+ {
+ required: true,
+ name: :password,
+ type: String,
+ desc: 'Passord of the user'
+ }
+ ],
+ 'bugzilla' => [
+ {
+ required: true,
+ name: :new_issue_url,
+ type: String,
+ desc: 'New issue URL'
+ },
+ {
+ required: true,
+ name: :issues_url,
+ type: String,
+ desc: 'Issues URL'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'Project URL'
+ },
+ {
+ required: false,
+ name: :description,
+ type: String,
+ desc: 'Description'
+ },
+ {
+ required: false,
+ name: :title,
+ type: String,
+ desc: 'Title'
+ }
+ ],
+ 'buildkite' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Buildkite project GitLab token'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'The buildkite project URL'
+ },
+ {
+ required: false,
+ name: :enable_ssl_verification,
+ type: Boolean,
+ desc: 'Enable SSL verification for communication'
+ }
+ ],
+ 'builds-email' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Comma-separated list of recipient email addresses'
+ },
+ {
+ required: false,
+ name: :add_pusher,
+ type: Boolean,
+ desc: 'Add pusher to recipients list'
+ },
+ {
+ required: false,
+ name: :notify_only_broken_builds,
+ type: Boolean,
+ desc: 'Notify only broken builds'
+ }
+ ],
+ 'campfire' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Campfire token'
+ },
+ {
+ required: false,
+ name: :subdomain,
+ type: String,
+ desc: 'Campfire subdomain'
+ },
+ {
+ required: false,
+ name: :room,
+ type: String,
+ desc: 'Campfire room'
+ },
+ ],
+ 'custom-issue-tracker' => [
+ {
+ required: true,
+ name: :new_issue_url,
+ type: String,
+ desc: 'New issue URL'
+ },
+ {
+ required: true,
+ name: :issues_url,
+ type: String,
+ desc: 'Issues URL'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'Project URL'
+ },
+ {
+ required: false,
+ name: :description,
+ type: String,
+ desc: 'Description'
+ },
+ {
+ required: false,
+ name: :title,
+ type: String,
+ desc: 'Title'
+ }
+ ],
+ 'drone-ci' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Drone CI token'
+ },
+ {
+ required: true,
+ name: :drone_url,
+ type: String,
+ desc: 'Drone CI URL'
+ },
+ {
+ required: false,
+ name: :enable_ssl_verification,
+ type: Boolean,
+ desc: 'Enable SSL verification for communication'
+ }
+ ],
+ 'emails-on-push' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Comma-separated list of recipient email addresses'
+ },
+ {
+ required: false,
+ name: :disable_diffs,
+ type: Boolean,
+ desc: 'Disable code diffs'
+ },
+ {
+ required: false,
+ name: :send_from_committer_email,
+ type: Boolean,
+ desc: 'Send from committer'
+ }
+ ],
+ 'external-wiki' => [
+ {
+ required: true,
+ name: :external_wiki_url,
+ type: String,
+ desc: 'The URL of the external Wiki'
+ }
+ ],
+ 'flowdock' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Flowdock token'
+ }
+ ],
+ 'gemnasium' => [
+ {
+ required: true,
+ name: :api_key,
+ type: String,
+ desc: 'Your personal API key on gemnasium.com'
+ },
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: "The project's slug on gemnasium.com"
+ }
+ ],
+ 'hipchat' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The room token'
+ },
+ {
+ required: false,
+ name: :room,
+ type: String,
+ desc: 'The room name or ID'
+ },
+ {
+ required: false,
+ name: :color,
+ type: String,
+ desc: 'The room color'
+ },
+ {
+ required: false,
+ name: :notify,
+ type: Boolean,
+ desc: 'Enable notifications'
+ },
+ {
+ required: false,
+ name: :api_version,
+ type: String,
+ desc: 'Leave blank for default (v2)'
+ },
+ {
+ required: false,
+ name: :server,
+ type: String,
+ desc: 'Leave blank for default. https://hipchat.example.com'
+ }
+ ],
+ 'irker' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Recipients/channels separated by whitespaces'
+ },
+ {
+ required: false,
+ name: :default_irc_uri,
+ type: String,
+ desc: 'Default: irc://irc.network.net:6697'
+ },
+ {
+ required: false,
+ name: :server_host,
+ type: String,
+ desc: 'Server host. Default localhost'
+ },
+ {
+ required: false,
+ name: :server_port,
+ type: Integer,
+ desc: 'Server port. Default 6659'
+ },
+ {
+ required: false,
+ name: :colorize_messages,
+ type: Boolean,
+ desc: 'Colorize messages'
+ }
+ ],
+ 'jira' => [
+ {
+ required: true,
+ name: :url,
+ type: String,
+ desc: 'The URL to the JIRA project which is being linked to this GitLab project, e.g., https://jira.example.com'
+ },
+ {
+ required: true,
+ name: :project_key,
+ type: String,
+ desc: 'The short identifier for your JIRA project, all uppercase, e.g., PROJ'
+ },
+ {
+ required: false,
+ name: :username,
+ type: String,
+ desc: 'The username of the user created to be used with GitLab/JIRA'
+ },
+ {
+ required: false,
+ name: :password,
+ type: String,
+ desc: 'The password of the user created to be used with GitLab/JIRA'
+ },
+ {
+ required: false,
+ name: :jira_issue_transition_id,
+ type: Integer,
+ desc: 'The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`'
+ }
+ ],
+
+ 'kubernetes' => [
+ {
+ required: true,
+ name: :namespace,
+ type: String,
+ desc: 'The Kubernetes namespace to use'
+ },
+ {
+ required: true,
+ name: :api_url,
+ type: String,
+ desc: 'The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com'
+ },
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The service token to authenticate against the Kubernetes cluster with'
+ },
+ {
+ required: false,
+ name: :ca_pem,
+ type: String,
+ desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)'
+ },
+ ],
+ 'mattermost-slash-commands' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Mattermost token'
+ }
+ ],
+ 'slack-slash-commands' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Slack token'
+ }
+ ],
+ 'pipelines-email' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Comma-separated list of recipient email addresses'
+ },
+ {
+ required: false,
+ name: :notify_only_broken_builds,
+ type: Boolean,
+ desc: 'Notify only broken builds'
+ }
+ ],
+ 'pivotaltracker' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Pivotaltracker token'
+ },
+ {
+ required: false,
+ name: :restrict_to_branch,
+ type: String,
+ desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.'
+ }
+ ],
+ 'pushover' => [
+ {
+ required: true,
+ name: :api_key,
+ type: String,
+ desc: 'The application key'
+ },
+ {
+ required: true,
+ name: :user_key,
+ type: String,
+ desc: 'The user key'
+ },
+ {
+ required: true,
+ name: :priority,
+ type: String,
+ desc: 'The priority'
+ },
+ {
+ required: true,
+ name: :device,
+ type: String,
+ desc: 'Leave blank for all active devices'
+ },
+ {
+ required: true,
+ name: :sound,
+ type: String,
+ desc: 'The sound of the notification'
+ }
+ ],
+ 'redmine' => [
+ {
+ required: true,
+ name: :new_issue_url,
+ type: String,
+ desc: 'The new issue URL'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'The project URL'
+ },
+ {
+ required: true,
+ name: :issues_url,
+ type: String,
+ desc: 'The issues URL'
+ },
+ {
+ required: false,
+ name: :description,
+ type: String,
+ desc: 'The description of the tracker'
+ }
+ ],
+ 'slack-notification' => [
+ {
+ required: true,
+ name: :webhook,
+ type: String,
+ desc: 'The Slack webhook. e.g. https://hooks.slack.com/services/...'
+ },
+ {
+ required: false,
+ name: :new_issue_url,
+ type: String,
+ desc: 'The user name'
+ },
+ {
+ required: false,
+ name: :channel,
+ type: String,
+ desc: 'The channel name'
+ }
+ ],
+ 'mattermost-notification' => [
+ {
+ required: true,
+ name: :webhook,
+ type: String,
+ desc: 'The Mattermost webhook. e.g. http://mattermost_host/hooks/...'
+ }
+ ],
+ 'teamcity' => [
+ {
+ required: true,
+ name: :teamcity_url,
+ type: String,
+ desc: 'TeamCity root URL like https://teamcity.example.com'
+ },
+ {
+ required: true,
+ name: :build_type,
+ type: String,
+ desc: 'Build configuration ID'
+ },
+ {
+ required: true,
+ name: :username,
+ type: String,
+ desc: 'A user with permissions to trigger a manual build'
+ },
+ {
+ required: true,
+ name: :password,
+ type: String,
+ desc: 'The password of the user'
+ }
+ ]
+ }.freeze
+
+ trigger_services = {
+ 'mattermost-slash-commands' => [
+ {
+ name: :token,
+ type: String,
+ desc: 'The Mattermost token'
+ }
+ ]
+ }.freeze
+
resource :projects do
before { authenticate! }
before { authorize_admin_project }
- # Set <service_slug> service for project
- #
- # Example Request:
- #
- # PUT /projects/:id/services/gitlab-ci
- #
- put ':id/services/:service_slug' do
- if project_service
- validators = project_service.class.validators.select do |s|
- s.class == ActiveRecord::Validations::PresenceValidator &&
- s.attributes != [:project_id]
+ helpers do
+ def service_attributes(service)
+ service.fields.inject([]) do |arr, hash|
+ arr << hash[:name].to_sym
end
+ end
+ end
- required_attributes! validators.map(&:attributes).flatten.uniq
- attrs = attributes_for_keys service_attributes
+ services.each do |service_slug, settings|
+ desc "Set #{service_slug} service for project"
+ params do
+ settings.each do |setting|
+ if setting[:required]
+ requires setting[:name], type: setting[:type], desc: setting[:desc]
+ else
+ optional setting[:name], type: setting[:type], desc: setting[:desc]
+ end
+ end
+ end
+ put ":id/services/#{service_slug}" do
+ service = user_project.find_or_initialize_service(service_slug.underscore)
+ service_params = declared_params(include_missing: false).merge(active: true)
- if project_service.update_attributes(attrs.merge(active: true))
+ if service.update_attributes(service_params)
true
else
- not_found!
+ render_api_error!('400 Bad Request', 400)
end
end
end
- # Delete <service_slug> service for project
- #
- # Example Request:
- #
- # DELETE /project/:id/services/gitlab-ci
- #
- delete ':id/services/:service_slug' do
- if project_service
- attrs = service_attributes.inject({}) do |hash, key|
- hash.merge!(key => nil)
- end
+ desc "Delete a service for project"
+ params do
+ requires :service_slug, type: String, values: services.keys, desc: 'The name of the service'
+ end
+ delete ":id/services/:service_slug" do
+ service = user_project.find_or_initialize_service(params[:service_slug].underscore)
- if project_service.update_attributes(attrs.merge(active: false))
- true
- else
- not_found!
- end
+ attrs = service_attributes(service).inject({}) do |hash, key|
+ hash.merge!(key => nil)
+ end
+
+ if service.update_attributes(attrs.merge(active: false))
+ true
+ else
+ render_api_error!('400 Bad Request', 400)
end
end
- # Get <service_slug> service settings for project
- #
- # Example Request:
- #
- # GET /project/:id/services/gitlab-ci
- #
- get ':id/services/:service_slug' do
- present project_service, with: Entities::ProjectService, include_passwords: current_user.is_admin?
+ desc 'Get the service settings for project' do
+ success Entities::ProjectService
+ end
+ params do
+ requires :service_slug, type: String, values: services.keys, desc: 'The name of the service'
+ end
+ get ":id/services/:service_slug" do
+ service = user_project.find_or_initialize_service(params[:service_slug].underscore)
+ present service, with: Entities::ProjectService, include_passwords: current_user.is_admin?
end
end
- resource :projects do
- desc 'Trigger a slash command' do
- detail 'Added in GitLab 8.13'
+ trigger_services.each do |service_slug, settings|
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
end
- post ':id/services/:service_slug/trigger' do
- project = find_project(params[:id])
+ resource :projects do
+ desc "Trigger a slash command for #{service_slug}" do
+ detail 'Added in GitLab 8.13'
+ end
+ params do
+ settings.each do |setting|
+ requires setting[:name], type: setting[:type], desc: setting[:desc]
+ end
+ end
+ post ":id/services/#{service_slug.underscore}/trigger" do
+ project = find_project(params[:id])
- # This is not accurate, but done to prevent leakage of the project names
- not_found!('Service') unless project
+ # This is not accurate, but done to prevent leakage of the project names
+ not_found!('Service') unless project
- service = project_service(project)
+ service = project.find_or_initialize_service(service_slug.underscore)
- result = service.try(:active?) && service.try(:trigger, params)
+ result = service.try(:active?) && service.try(:trigger, params)
- if result
- status result[:status] || 200
- present result
- else
- not_found!('Service')
+ if result
+ status result[:status] || 200
+ present result
+ else
+ not_found!('Service')
+ end
end
end
end
diff --git a/lib/api/templates.rb b/lib/api/templates.rb
index 8a53d9c0095..e23f99256a5 100644
--- a/lib/api/templates.rb
+++ b/lib/api/templates.rb
@@ -8,6 +8,10 @@ module API
gitlab_ci_ymls: {
klass: Gitlab::Template::GitlabCiYmlTemplate,
gitlab_version: 8.9
+ },
+ dockerfiles: {
+ klass: Gitlab::Template::DockerfileTemplate,
+ gitlab_version: 8.15
}
}.freeze
PROJECT_TEMPLATE_REGEX =
@@ -51,7 +55,7 @@ module API
end
params do
optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses'
- end
+ end
get route do
options = {
featured: declared(params).popular.present? ? true : nil
@@ -69,7 +73,7 @@ module API
end
params do
requires :name, type: String, desc: 'The name of the template'
- end
+ end
get route, requirements: { name: /[\w\.-]+/ } do
not_found!('License') unless Licensee::License.find(declared(params).name)
@@ -78,7 +82,7 @@ module API
present template, with: Entities::RepoLicense
end
end
-
+
GLOBAL_TEMPLATE_TYPES.each do |template_type, properties|
klass = properties[:klass]
gitlab_version = properties[:gitlab_version]
@@ -104,7 +108,7 @@ module API
end
params do
requires :name, type: String, desc: 'The name of the template'
- end
+ end
get route do
new_template = klass.find(declared(params).name)
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 1dab799dd61..0842c3874c5 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -2,7 +2,10 @@ module API
class Users < Grape::API
include PaginationParams
- before { authenticate! }
+ before do
+ allow_access_with_scope :read_user if request.get?
+ authenticate!
+ end
resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
helpers do
@@ -353,7 +356,7 @@ module API
success Entities::UserPublic
end
get do
- present current_user, with: @impersonator ? Entities::UserWithPrivateToken : Entities::UserPublic
+ present current_user, with: sudo? ? Entities::UserWithPrivateToken : Entities::UserPublic
end
desc "Get the currently authenticated user's SSH keys" do
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index d904a8bd4ae..fd74eeaebe7 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -248,7 +248,7 @@ module Banzai
end
def projects_relation_for_paths(paths)
- Project.where_paths_in(paths).includes(:namespace)
+ Project.where_full_path_in(paths).includes(:namespace)
end
# Returns projects for the given paths.
diff --git a/lib/banzai/filter/math_filter.rb b/lib/banzai/filter/math_filter.rb
new file mode 100644
index 00000000000..cb037f89337
--- /dev/null
+++ b/lib/banzai/filter/math_filter.rb
@@ -0,0 +1,51 @@
+require 'uri'
+
+module Banzai
+ module Filter
+ # HTML filter that adds class="code math" and removes the dollar sign in $`2+2`$.
+ #
+ class MathFilter < HTML::Pipeline::Filter
+ # This picks out <code>...</code>.
+ INLINE_MATH = 'descendant-or-self::code'.freeze
+
+ # Pick out a code block which is declared math
+ DISPLAY_MATH = "descendant-or-self::pre[contains(@class, 'math') and contains(@class, 'code')]".freeze
+
+ # Attribute indicating inline or display math.
+ STYLE_ATTRIBUTE = 'data-math-style'.freeze
+
+ # Class used for tagging elements that should be rendered
+ TAG_CLASS = 'js-render-math'.freeze
+
+ INLINE_CLASSES = "code math #{TAG_CLASS}".freeze
+
+ DOLLAR_SIGN = '$'.freeze
+
+ def call
+ doc.xpath(INLINE_MATH).each do |code|
+ closing = code.next
+ opening = code.previous
+
+ # We need a sibling before and after.
+ # They should end and start with $ respectively.
+ if closing && opening &&
+ closing.content.first == DOLLAR_SIGN &&
+ opening.content.last == DOLLAR_SIGN
+
+ code[:class] = INLINE_CLASSES
+ code[STYLE_ATTRIBUTE] = 'inline'
+ closing.content = closing.content[1..-1]
+ opening.content = opening.content[0..-2]
+ end
+ end
+
+ doc.xpath(DISPLAY_MATH).each do |el|
+ el[STYLE_ATTRIBUTE] = 'display'
+ el[:class] += " #{TAG_CLASS}"
+ end
+
+ doc
+ end
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 5da2d0b008c..5a1f873496c 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -6,6 +6,7 @@ module Banzai
Filter::SyntaxHighlightFilter,
Filter::SanitizationFilter,
+ Filter::MathFilter,
Filter::UploadLinkFilter,
Filter::VideoLinkFilter,
Filter::ImageLinkFilter,
diff --git a/lib/bitbucket/client.rb b/lib/bitbucket/client.rb
new file mode 100644
index 00000000000..f8ee7e0f9ae
--- /dev/null
+++ b/lib/bitbucket/client.rb
@@ -0,0 +1,58 @@
+module Bitbucket
+ class Client
+ attr_reader :connection
+
+ def initialize(options = {})
+ @connection = Connection.new(options)
+ end
+
+ def issues(repo)
+ path = "/repositories/#{repo}/issues"
+ get_collection(path, :issue)
+ end
+
+ def issue_comments(repo, issue_id)
+ path = "/repositories/#{repo}/issues/#{issue_id}/comments"
+ get_collection(path, :comment)
+ end
+
+ def pull_requests(repo)
+ path = "/repositories/#{repo}/pullrequests?state=ALL"
+ get_collection(path, :pull_request)
+ end
+
+ def pull_request_comments(repo, pull_request)
+ path = "/repositories/#{repo}/pullrequests/#{pull_request}/comments"
+ get_collection(path, :pull_request_comment)
+ end
+
+ def pull_request_diff(repo, pull_request)
+ path = "/repositories/#{repo}/pullrequests/#{pull_request}/diff"
+ connection.get(path)
+ end
+
+ def repo(name)
+ parsed_response = connection.get("/repositories/#{name}")
+ Representation::Repo.new(parsed_response)
+ end
+
+ def repos
+ path = "/repositories?role=member"
+ get_collection(path, :repo)
+ end
+
+ def user
+ @user ||= begin
+ parsed_response = connection.get('/user')
+ Representation::User.new(parsed_response)
+ end
+ end
+
+ private
+
+ def get_collection(path, type)
+ paginator = Paginator.new(connection, path, type)
+ Collection.new(paginator)
+ end
+ end
+end
diff --git a/lib/bitbucket/collection.rb b/lib/bitbucket/collection.rb
new file mode 100644
index 00000000000..3a9379ff680
--- /dev/null
+++ b/lib/bitbucket/collection.rb
@@ -0,0 +1,21 @@
+module Bitbucket
+ class Collection < Enumerator
+ def initialize(paginator)
+ super() do |yielder|
+ loop do
+ paginator.items.each { |item| yielder << item }
+ end
+ end
+
+ lazy
+ end
+
+ def method_missing(method, *args)
+ return super unless self.respond_to?(method)
+
+ self.send(method, *args) do |item|
+ block_given? ? yield(item) : item
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket/connection.rb b/lib/bitbucket/connection.rb
new file mode 100644
index 00000000000..7e55cf4deab
--- /dev/null
+++ b/lib/bitbucket/connection.rb
@@ -0,0 +1,69 @@
+module Bitbucket
+ class Connection
+ DEFAULT_API_VERSION = '2.0'
+ DEFAULT_BASE_URI = 'https://api.bitbucket.org/'
+ DEFAULT_QUERY = {}
+
+ attr_reader :expires_at, :expires_in, :refresh_token, :token
+
+ def initialize(options = {})
+ @api_version = options.fetch(:api_version, DEFAULT_API_VERSION)
+ @base_uri = options.fetch(:base_uri, DEFAULT_BASE_URI)
+ @default_query = options.fetch(:query, DEFAULT_QUERY)
+
+ @token = options[:token]
+ @expires_at = options[:expires_at]
+ @expires_in = options[:expires_in]
+ @refresh_token = options[:refresh_token]
+ end
+
+ def get(path, extra_query = {})
+ refresh! if expired?
+
+ response = connection.get(build_url(path), params: @default_query.merge(extra_query))
+ response.parsed
+ end
+
+ def expired?
+ connection.expired?
+ end
+
+ def refresh!
+ response = connection.refresh!
+
+ @token = response.token
+ @expires_at = response.expires_at
+ @expires_in = response.expires_in
+ @refresh_token = response.refresh_token
+ @connection = nil
+ end
+
+ private
+
+ def client
+ @client ||= OAuth2::Client.new(provider.app_id, provider.app_secret, options)
+ end
+
+ def connection
+ @connection ||= OAuth2::AccessToken.new(client, @token, refresh_token: @refresh_token, expires_at: @expires_at, expires_in: @expires_in)
+ end
+
+ def build_url(path)
+ return path if path.starts_with?(root_url)
+
+ "#{root_url}#{path}"
+ end
+
+ def root_url
+ @root_url ||= "#{@base_uri}#{@api_version}"
+ end
+
+ def provider
+ Gitlab::OAuth::Provider.config_for('bitbucket')
+ end
+
+ def options
+ OmniAuth::Strategies::Bitbucket.default_options[:client_options].deep_symbolize_keys
+ end
+ end
+end
diff --git a/lib/bitbucket/error/unauthorized.rb b/lib/bitbucket/error/unauthorized.rb
new file mode 100644
index 00000000000..5e2eb57bb0e
--- /dev/null
+++ b/lib/bitbucket/error/unauthorized.rb
@@ -0,0 +1,6 @@
+module Bitbucket
+ module Error
+ class Unauthorized < StandardError
+ end
+ end
+end
diff --git a/lib/bitbucket/page.rb b/lib/bitbucket/page.rb
new file mode 100644
index 00000000000..2b0a3fe7b1a
--- /dev/null
+++ b/lib/bitbucket/page.rb
@@ -0,0 +1,34 @@
+module Bitbucket
+ class Page
+ attr_reader :attrs, :items
+
+ def initialize(raw, type)
+ @attrs = parse_attrs(raw)
+ @items = parse_values(raw, representation_class(type))
+ end
+
+ def next?
+ attrs.fetch(:next, false)
+ end
+
+ def next
+ attrs.fetch(:next)
+ end
+
+ private
+
+ def parse_attrs(raw)
+ raw.slice(*%w(size page pagelen next previous)).symbolize_keys
+ end
+
+ def parse_values(raw, bitbucket_rep_class)
+ return [] unless raw['values'] && raw['values'].is_a?(Array)
+
+ bitbucket_rep_class.decorate(raw['values'])
+ end
+
+ def representation_class(type)
+ Bitbucket::Representation.const_get(type.to_s.camelize)
+ end
+ end
+end
diff --git a/lib/bitbucket/paginator.rb b/lib/bitbucket/paginator.rb
new file mode 100644
index 00000000000..135d0d55674
--- /dev/null
+++ b/lib/bitbucket/paginator.rb
@@ -0,0 +1,36 @@
+module Bitbucket
+ class Paginator
+ PAGE_LENGTH = 50 # The minimum length is 10 and the maximum is 100.
+
+ def initialize(connection, url, type)
+ @connection = connection
+ @type = type
+ @url = url
+ @page = nil
+ end
+
+ def items
+ raise StopIteration unless has_next_page?
+
+ @page = fetch_next_page
+ @page.items
+ end
+
+ private
+
+ attr_reader :connection, :page, :url, :type
+
+ def has_next_page?
+ page.nil? || page.next?
+ end
+
+ def next_url
+ page.nil? ? url : page.next
+ end
+
+ def fetch_next_page
+ parsed_response = connection.get(next_url, pagelen: PAGE_LENGTH, sort: :created_on)
+ Page.new(parsed_response, type)
+ end
+ end
+end
diff --git a/lib/bitbucket/representation/base.rb b/lib/bitbucket/representation/base.rb
new file mode 100644
index 00000000000..94adaacc9b5
--- /dev/null
+++ b/lib/bitbucket/representation/base.rb
@@ -0,0 +1,17 @@
+module Bitbucket
+ module Representation
+ class Base
+ def initialize(raw)
+ @raw = raw
+ end
+
+ def self.decorate(entries)
+ entries.map { |entry| new(entry)}
+ end
+
+ private
+
+ attr_reader :raw
+ end
+ end
+end
diff --git a/lib/bitbucket/representation/comment.rb b/lib/bitbucket/representation/comment.rb
new file mode 100644
index 00000000000..4937aa9728f
--- /dev/null
+++ b/lib/bitbucket/representation/comment.rb
@@ -0,0 +1,27 @@
+module Bitbucket
+ module Representation
+ class Comment < Representation::Base
+ def author
+ user['username']
+ end
+
+ def note
+ raw.fetch('content', {}).fetch('raw', nil)
+ end
+
+ def created_at
+ raw['created_on']
+ end
+
+ def updated_at
+ raw['updated_on'] || raw['created_on']
+ end
+
+ private
+
+ def user
+ raw.fetch('user', {})
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket/representation/issue.rb b/lib/bitbucket/representation/issue.rb
new file mode 100644
index 00000000000..054064395c3
--- /dev/null
+++ b/lib/bitbucket/representation/issue.rb
@@ -0,0 +1,53 @@
+module Bitbucket
+ module Representation
+ class Issue < Representation::Base
+ CLOSED_STATUS = %w(resolved invalid duplicate wontfix closed).freeze
+
+ def iid
+ raw['id']
+ end
+
+ def kind
+ raw['kind']
+ end
+
+ def author
+ raw.fetch('reporter', {}).fetch('username', nil)
+ end
+
+ def description
+ raw.fetch('content', {}).fetch('raw', nil)
+ end
+
+ def state
+ closed? ? 'closed' : 'opened'
+ end
+
+ def title
+ raw['title']
+ end
+
+ def milestone
+ raw['milestone']['name'] if raw['milestone'].present?
+ end
+
+ def created_at
+ raw['created_on']
+ end
+
+ def updated_at
+ raw['edited_on']
+ end
+
+ def to_s
+ iid
+ end
+
+ private
+
+ def closed?
+ CLOSED_STATUS.include?(raw['state'])
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket/representation/pull_request.rb b/lib/bitbucket/representation/pull_request.rb
new file mode 100644
index 00000000000..eebf8093380
--- /dev/null
+++ b/lib/bitbucket/representation/pull_request.rb
@@ -0,0 +1,65 @@
+module Bitbucket
+ module Representation
+ class PullRequest < Representation::Base
+ def author
+ raw.fetch('author', {}).fetch('username', nil)
+ end
+
+ def description
+ raw['description']
+ end
+
+ def iid
+ raw['id']
+ end
+
+ def state
+ if raw['state'] == 'MERGED'
+ 'merged'
+ elsif raw['state'] == 'DECLINED'
+ 'closed'
+ else
+ 'opened'
+ end
+ end
+
+ def created_at
+ raw['created_on']
+ end
+
+ def updated_at
+ raw['updated_on']
+ end
+
+ def title
+ raw['title']
+ end
+
+ def source_branch_name
+ source_branch.fetch('branch', {}).fetch('name', nil)
+ end
+
+ def source_branch_sha
+ source_branch.fetch('commit', {}).fetch('hash', nil)
+ end
+
+ def target_branch_name
+ target_branch.fetch('branch', {}).fetch('name', nil)
+ end
+
+ def target_branch_sha
+ target_branch.fetch('commit', {}).fetch('hash', nil)
+ end
+
+ private
+
+ def source_branch
+ raw['source']
+ end
+
+ def target_branch
+ raw['destination']
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket/representation/pull_request_comment.rb b/lib/bitbucket/representation/pull_request_comment.rb
new file mode 100644
index 00000000000..4f8efe03bae
--- /dev/null
+++ b/lib/bitbucket/representation/pull_request_comment.rb
@@ -0,0 +1,39 @@
+module Bitbucket
+ module Representation
+ class PullRequestComment < Comment
+ def iid
+ raw['id']
+ end
+
+ def file_path
+ inline.fetch('path')
+ end
+
+ def old_pos
+ inline.fetch('from')
+ end
+
+ def new_pos
+ inline.fetch('to')
+ end
+
+ def parent_id
+ raw.fetch('parent', {}).fetch('id', nil)
+ end
+
+ def inline?
+ raw.has_key?('inline')
+ end
+
+ def has_parent?
+ raw.has_key?('parent')
+ end
+
+ private
+
+ def inline
+ raw.fetch('inline', {})
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket/representation/repo.rb b/lib/bitbucket/representation/repo.rb
new file mode 100644
index 00000000000..423eff8f2a5
--- /dev/null
+++ b/lib/bitbucket/representation/repo.rb
@@ -0,0 +1,71 @@
+module Bitbucket
+ module Representation
+ class Repo < Representation::Base
+ attr_reader :owner, :slug
+
+ def initialize(raw)
+ super(raw)
+ end
+
+ def owner_and_slug
+ @owner_and_slug ||= full_name.split('/', 2)
+ end
+
+ def owner
+ owner_and_slug.first
+ end
+
+ def slug
+ owner_and_slug.last
+ end
+
+ def clone_url(token = nil)
+ url = raw['links']['clone'].find { |link| link['name'] == 'https' }.fetch('href')
+
+ if token.present?
+ clone_url = URI::parse(url)
+ clone_url.user = "x-token-auth:#{token}"
+ clone_url.to_s
+ else
+ url
+ end
+ end
+
+ def description
+ raw['description']
+ end
+
+ def full_name
+ raw['full_name']
+ end
+
+ def issues_enabled?
+ raw['has_issues']
+ end
+
+ def name
+ raw['name']
+ end
+
+ def valid?
+ raw['scm'] == 'git'
+ end
+
+ def has_wiki?
+ raw['has_wiki']
+ end
+
+ def visibility_level
+ if raw['is_private']
+ Gitlab::VisibilityLevel::PRIVATE
+ else
+ Gitlab::VisibilityLevel::PUBLIC
+ end
+ end
+
+ def to_s
+ full_name
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket/representation/user.rb b/lib/bitbucket/representation/user.rb
new file mode 100644
index 00000000000..ba6b7667b49
--- /dev/null
+++ b/lib/bitbucket/representation/user.rb
@@ -0,0 +1,9 @@
+module Bitbucket
+ module Representation
+ class User < Representation::Base
+ def username
+ raw['username']
+ end
+ end
+ end
+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..31fbd1da108 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?
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index fef652cb975..7463bd719d5 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -118,7 +118,7 @@ module Ci
.merge(job_variables(name))
variables.map do |key, value|
- { key: key, value: value, public: true }
+ { key: key.to_s, value: value, public: true }
end
end
diff --git a/lib/gitlab/allowable.rb b/lib/gitlab/allowable.rb
new file mode 100644
index 00000000000..f48abcc86d5
--- /dev/null
+++ b/lib/gitlab/allowable.rb
@@ -0,0 +1,7 @@
+module Gitlab
+ module Allowable
+ def can?(user, action, subject)
+ Ability.allowed?(user, action, subject)
+ end
+ end
+end
diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb
index 9667df4ffb8..fa234284361 100644
--- a/lib/gitlab/asciidoc.rb
+++ b/lib/gitlab/asciidoc.rb
@@ -1,4 +1,5 @@
require 'asciidoctor'
+require 'asciidoctor/converter/html5'
module Gitlab
# Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters
@@ -23,7 +24,7 @@ module Gitlab
def self.render(input, context, asciidoc_opts = {})
asciidoc_opts.reverse_merge!(
safe: :secure,
- backend: :html5,
+ backend: :gitlab_html5,
attributes: []
)
asciidoc_opts[:attributes].unshift(*DEFAULT_ADOC_ATTRS)
@@ -34,5 +35,29 @@ module Gitlab
html.html_safe
end
+
+ class Html5Converter < Asciidoctor::Converter::Html5Converter
+ extend Asciidoctor::Converter::Config
+
+ register_for 'gitlab_html5'
+
+ def stem(node)
+ return super unless node.style.to_sym == :latexmath
+
+ %(<pre#{id_attribute(node)} class="code math js-render-math #{node.role}" data-math-style="display"><code>#{node.content}</code></pre>)
+ end
+
+ def inline_quoted(node)
+ return super unless node.type.to_sym == :latexmath
+
+ %(<code#{id_attribute(node)} class="code math js-render-math #{node.role}" data-math-style="inline">#{node.text}</code>)
+ end
+
+ private
+
+ def id_attribute(node)
+ node.id ? %( id="#{node.id}") : nil
+ end
+ end
end
end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index aca5d0020cf..8dda65c71ef 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -2,6 +2,10 @@ module Gitlab
module Auth
class MissingPersonalTokenError < StandardError; end
+ SCOPES = [:api, :read_user]
+ DEFAULT_SCOPES = [:api]
+ OPTIONAL_SCOPES = SCOPES - DEFAULT_SCOPES
+
class << self
def find_for_git_client(login, password, project:, ip:)
raise "Must provide an IP for rate limiting" if ip.nil?
@@ -88,7 +92,7 @@ module Gitlab
def oauth_access_token_check(login, password)
if login == "oauth2" && password.present?
token = Doorkeeper::AccessToken.by_token(password)
- if token && token.accessible?
+ if valid_oauth_token?(token)
user = User.find_by(id: token.resource_owner_id)
Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)
end
@@ -97,12 +101,27 @@ module Gitlab
def personal_access_token_check(login, password)
if login && password
- user = User.find_by_personal_access_token(password)
+ token = PersonalAccessToken.active.find_by_token(password)
validation = User.by_login(login)
- Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities) if user.present? && user == validation
+
+ if valid_personal_access_token?(token, validation)
+ Gitlab::Auth::Result.new(validation, nil, :personal_token, full_authentication_abilities)
+ end
end
end
+ def valid_oauth_token?(token)
+ token && token.accessible? && valid_api_token?(token)
+ end
+
+ def valid_personal_access_token?(token, user)
+ token && token.user == user && valid_api_token?(token)
+ end
+
+ def valid_api_token?(token)
+ AccessTokenValidationService.new(token).include_any_scope?(['api'])
+ end
+
def lfs_token_check(login, password)
deploy_key_matches = login.match(/\Alfs\+deploy-key-(\d+)\z/)
diff --git a/lib/gitlab/badge/build/status.rb b/lib/gitlab/badge/build/status.rb
index 50aa45e5406..b762d85b6e5 100644
--- a/lib/gitlab/badge/build/status.rb
+++ b/lib/gitlab/badge/build/status.rb
@@ -20,8 +20,8 @@ module Gitlab
def status
@project.pipelines
- .where(sha: @sha, ref: @ref)
- .status || 'unknown'
+ .where(sha: @sha)
+ .latest_status(@ref) || 'unknown'
end
def metadata
diff --git a/lib/gitlab/bitbucket_import.rb b/lib/gitlab/bitbucket_import.rb
deleted file mode 100644
index 7298152e7e9..00000000000
--- a/lib/gitlab/bitbucket_import.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-module Gitlab
- module BitbucketImport
- mattr_accessor :public_key
- @public_key = nil
- end
-end
diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb
deleted file mode 100644
index 8d1ad62fae0..00000000000
--- a/lib/gitlab/bitbucket_import/client.rb
+++ /dev/null
@@ -1,142 +0,0 @@
-module Gitlab
- module BitbucketImport
- class Client
- class Unauthorized < StandardError; end
-
- attr_reader :consumer, :api
-
- def self.from_project(project)
- import_data_credentials = project.import_data.credentials if project.import_data
- if import_data_credentials && import_data_credentials[:bb_session]
- token = import_data_credentials[:bb_session][:bitbucket_access_token]
- token_secret = import_data_credentials[:bb_session][:bitbucket_access_token_secret]
- new(token, token_secret)
- else
- raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{project.id}"
- end
- end
-
- def initialize(access_token = nil, access_token_secret = nil)
- @consumer = ::OAuth::Consumer.new(
- config.app_id,
- config.app_secret,
- bitbucket_options
- )
-
- if access_token && access_token_secret
- @api = ::OAuth::AccessToken.new(@consumer, access_token, access_token_secret)
- end
- end
-
- def request_token(redirect_uri)
- request_token = consumer.get_request_token(oauth_callback: redirect_uri)
-
- {
- oauth_token: request_token.token,
- oauth_token_secret: request_token.secret,
- oauth_callback_confirmed: request_token.callback_confirmed?.to_s
- }
- end
-
- def authorize_url(request_token, redirect_uri)
- request_token = ::OAuth::RequestToken.from_hash(consumer, request_token) if request_token.is_a?(Hash)
-
- if request_token.callback_confirmed?
- request_token.authorize_url
- else
- request_token.authorize_url(oauth_callback: redirect_uri)
- end
- end
-
- def get_token(request_token, oauth_verifier, redirect_uri)
- request_token = ::OAuth::RequestToken.from_hash(consumer, request_token) if request_token.is_a?(Hash)
-
- if request_token.callback_confirmed?
- request_token.get_access_token(oauth_verifier: oauth_verifier)
- else
- request_token.get_access_token(oauth_callback: redirect_uri)
- end
- end
-
- def user
- JSON.parse(get("/api/1.0/user").body)
- end
-
- def issues(project_identifier)
- all_issues = []
- offset = 0
- per_page = 50 # Maximum number allowed by Bitbucket
- index = 0
-
- begin
- issues = JSON.parse(get(issue_api_endpoint(project_identifier, per_page, offset)).body)
- # Find out how many total issues are present
- total = issues["count"] if index == 0
- all_issues.concat(issues["issues"])
- offset += issues["issues"].count
- index += 1
- end while all_issues.count < total
-
- all_issues
- end
-
- def issue_comments(project_identifier, issue_id)
- comments = JSON.parse(get("/api/1.0/repositories/#{project_identifier}/issues/#{issue_id}/comments").body)
- comments.sort_by { |comment| comment["utc_created_on"] }
- end
-
- def project(project_identifier)
- JSON.parse(get("/api/1.0/repositories/#{project_identifier}").body)
- end
-
- def find_deploy_key(project_identifier, key)
- JSON.parse(get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find do |deploy_key|
- deploy_key["key"].chomp == key.chomp
- end
- end
-
- def add_deploy_key(project_identifier, key)
- deploy_key = find_deploy_key(project_identifier, key)
- return if deploy_key
-
- JSON.parse(api.post("/api/1.0/repositories/#{project_identifier}/deploy-keys", key: key, label: "GitLab import key").body)
- end
-
- def delete_deploy_key(project_identifier, key)
- deploy_key = find_deploy_key(project_identifier, key)
- return unless deploy_key
-
- api.delete("/api/1.0/repositories/#{project_identifier}/deploy-keys/#{deploy_key["pk"]}").code == "204"
- end
-
- def projects
- JSON.parse(get("/api/1.0/user/repositories").body).select { |repo| repo["scm"] == "git" }
- end
-
- def incompatible_projects
- JSON.parse(get("/api/1.0/user/repositories").body).reject { |repo| repo["scm"] == "git" }
- end
-
- private
-
- def get(url)
- response = api.get(url)
- raise Unauthorized if (400..499).cover?(response.code.to_i)
-
- response
- end
-
- def issue_api_endpoint(project_identifier, per_page, offset)
- "/api/1.0/repositories/#{project_identifier}/issues?sort=utc_created_on&limit=#{per_page}&start=#{offset}"
- end
-
- def config
- Gitlab.config.omniauth.providers.find { |provider| provider.name == "bitbucket" }
- end
-
- def bitbucket_options
- OmniAuth::Strategies::Bitbucket.default_options[:client_options].symbolize_keys
- end
- end
- end
-end
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
index f4b5097adb1..44323b47dca 100644
--- a/lib/gitlab/bitbucket_import/importer.rb
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -1,84 +1,247 @@
module Gitlab
module BitbucketImport
class Importer
- attr_reader :project, :client
+ include Gitlab::ShellAdapter
+
+ LABELS = [{ title: 'bug', color: '#FF0000' },
+ { title: 'enhancement', color: '#428BCA' },
+ { title: 'proposal', color: '#69D100' },
+ { title: 'task', color: '#7F8C8D' }].freeze
+
+ attr_reader :project, :client, :errors, :users
def initialize(project)
@project = project
- @client = Client.from_project(@project)
+ @client = Bitbucket::Client.new(project.import_data.credentials)
@formatter = Gitlab::ImportFormatter.new
+ @labels = {}
+ @errors = []
+ @users = {}
end
def execute
- import_issues if has_issues?
+ import_wiki
+ import_issues
+ import_pull_requests
+ handle_errors
true
- rescue ActiveRecord::RecordInvalid => e
- raise Projects::ImportService::Error.new, e.message
- ensure
- Gitlab::BitbucketImport::KeyDeleter.new(project).execute
end
private
- def gitlab_user_id(project, bitbucket_id)
- if bitbucket_id
- user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", bitbucket_id.to_s)
- (user && user.id) || project.creator_id
- else
- project.creator_id
- end
+ def handle_errors
+ return unless errors.any?
+
+ project.update_column(:import_error, {
+ message: 'The remote data could not be fully imported.',
+ errors: errors
+ }.to_json)
+ end
+
+ def gitlab_user_id(project, username)
+ find_user_id(username) || project.creator_id
end
- def identifier
- project.import_source
+ def find_user_id(username)
+ return nil unless username
+
+ return users[username] if users.key?(username)
+
+ users[username] = User.select(:id)
+ .joins(:identities)
+ .find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", username)
+ .try(:id)
end
- def has_issues?
- client.project(identifier)["has_issues"]
+ def repo
+ @repo ||= client.repo(project.import_source)
end
- def import_issues
- issues = client.issues(identifier)
+ def import_wiki
+ return if project.wiki.repository_exists?
- issues.each do |issue|
- body = ''
- reporter = nil
- author = 'Anonymous'
+ path_with_namespace = "#{project.path_with_namespace}.wiki"
+ import_url = project.import_url.sub(/\.git\z/, ".git/wiki")
+ gitlab_shell.import_repository(project.repository_storage_path, path_with_namespace, import_url)
+ rescue StandardError => e
+ errors << { type: :wiki, errors: e.message }
+ end
- if issue["reported_by"] && issue["reported_by"]["username"]
- reporter = issue["reported_by"]["username"]
- author = reporter
+ def import_issues
+ return unless repo.issues_enabled?
+
+ create_labels
+
+ client.issues(repo).each do |issue|
+ begin
+ description = ''
+ description += @formatter.author_line(issue.author) unless find_user_id(issue.author)
+ description += issue.description
+
+ label_name = issue.kind
+ milestone = issue.milestone ? project.milestones.find_or_create_by(title: issue.milestone) : nil
+
+ gitlab_issue = project.issues.create!(
+ iid: issue.iid,
+ title: issue.title,
+ description: description,
+ state: issue.state,
+ author_id: gitlab_user_id(project, issue.author),
+ milestone: milestone,
+ created_at: issue.created_at,
+ updated_at: issue.updated_at
+ )
+
+ gitlab_issue.labels << @labels[label_name]
+
+ import_issue_comments(issue, gitlab_issue) if gitlab_issue.persisted?
+ rescue StandardError => e
+ errors << { type: :issue, iid: issue.iid, errors: e.message }
end
+ end
+ end
- body = @formatter.author_line(author)
- body += issue["content"]
+ def import_issue_comments(issue, gitlab_issue)
+ client.issue_comments(repo, issue.iid).each do |comment|
+ # The note can be blank for issue service messages like "Changed title: ..."
+ # We would like to import those comments as well but there is no any
+ # specific parameter that would allow to process them, it's just an empty comment.
+ # To prevent our importer from just crashing or from creating useless empty comments
+ # we do this check.
+ next unless comment.note.present?
+
+ note = ''
+ note += @formatter.author_line(comment.author) unless find_user_id(comment.author)
+ note += comment.note
+
+ begin
+ gitlab_issue.notes.create!(
+ project: project,
+ note: note,
+ author_id: gitlab_user_id(project, comment.author),
+ created_at: comment.created_at,
+ updated_at: comment.updated_at
+ )
+ rescue StandardError => e
+ errors << { type: :issue_comment, iid: issue.iid, errors: e.message }
+ end
+ end
+ end
- comments = client.issue_comments(identifier, issue["local_id"])
+ def create_labels
+ LABELS.each do |label|
+ @labels[label[:title]] = project.labels.create!(label)
+ end
+ end
- if comments.any?
- body += @formatter.comments_header
+ def import_pull_requests
+ pull_requests = client.pull_requests(repo)
+
+ pull_requests.each do |pull_request|
+ begin
+ description = ''
+ description += @formatter.author_line(pull_request.author) unless find_user_id(pull_request.author)
+ description += pull_request.description
+
+ merge_request = project.merge_requests.create(
+ iid: pull_request.iid,
+ title: pull_request.title,
+ description: description,
+ source_project: project,
+ source_branch: pull_request.source_branch_name,
+ source_branch_sha: pull_request.source_branch_sha,
+ target_project: project,
+ target_branch: pull_request.target_branch_name,
+ target_branch_sha: pull_request.target_branch_sha,
+ state: pull_request.state,
+ author_id: gitlab_user_id(project, pull_request.author),
+ assignee_id: nil,
+ created_at: pull_request.created_at,
+ updated_at: pull_request.updated_at
+ )
+
+ import_pull_request_comments(pull_request, merge_request) if merge_request.persisted?
+ rescue StandardError => e
+ errors << { type: :pull_request, iid: pull_request.iid, errors: e.message }
end
+ end
+ end
+
+ def import_pull_request_comments(pull_request, merge_request)
+ comments = client.pull_request_comments(repo, pull_request.iid)
+
+ inline_comments, pr_comments = comments.partition(&:inline?)
+
+ import_inline_comments(inline_comments, pull_request, merge_request)
+ import_standalone_pr_comments(pr_comments, merge_request)
+ end
- comments.each do |comment|
- author = 'Anonymous'
+ def import_inline_comments(inline_comments, pull_request, merge_request)
+ line_code_map = {}
- if comment["author_info"] && comment["author_info"]["username"]
- author = comment["author_info"]["username"]
- end
+ children, parents = inline_comments.partition(&:has_parent?)
- body += @formatter.comment(author, comment["utc_created_on"], comment["content"])
+ # The Bitbucket API returns threaded replies as parent-child
+ # relationships. We assume that the child can appear in any order in
+ # the JSON.
+ parents.each do |comment|
+ line_code_map[comment.iid] = generate_line_code(comment)
+ end
+
+ children.each do |comment|
+ line_code_map[comment.iid] = line_code_map.fetch(comment.parent_id, nil)
+ end
+
+ inline_comments.each do |comment|
+ begin
+ attributes = pull_request_comment_attributes(comment)
+ attributes.merge!(
+ position: build_position(merge_request, comment),
+ line_code: line_code_map.fetch(comment.iid),
+ type: 'DiffNote')
+
+ merge_request.notes.create!(attributes)
+ rescue StandardError => e
+ errors << { type: :pull_request, iid: comment.iid, errors: e.message }
end
+ end
+ end
+
+ def build_position(merge_request, pr_comment)
+ params = {
+ diff_refs: merge_request.diff_refs,
+ old_path: pr_comment.file_path,
+ new_path: pr_comment.file_path,
+ old_line: pr_comment.old_pos,
+ new_line: pr_comment.new_pos
+ }
- project.issues.create!(
- description: body,
- title: issue["title"],
- state: %w(resolved invalid duplicate wontfix closed).include?(issue["status"]) ? 'closed' : 'opened',
- author_id: gitlab_user_id(project, reporter)
- )
+ Gitlab::Diff::Position.new(params)
+ end
+
+ def import_standalone_pr_comments(pr_comments, merge_request)
+ pr_comments.each do |comment|
+ begin
+ merge_request.notes.create!(pull_request_comment_attributes(comment))
+ rescue StandardError => e
+ errors << { type: :pull_request, iid: comment.iid, errors: e.message }
+ end
end
- rescue ActiveRecord::RecordInvalid => e
- raise Projects::ImportService::Error, e.message
+ end
+
+ def generate_line_code(pr_comment)
+ Gitlab::Diff::LineCode.generate(pr_comment.file_path, pr_comment.new_pos, pr_comment.old_pos)
+ end
+
+ def pull_request_comment_attributes(comment)
+ {
+ project: project,
+ note: comment.note,
+ author_id: gitlab_user_id(project, comment.author),
+ created_at: comment.created_at,
+ updated_at: comment.updated_at
+ }
end
end
end
diff --git a/lib/gitlab/bitbucket_import/key_adder.rb b/lib/gitlab/bitbucket_import/key_adder.rb
deleted file mode 100644
index 0b63f025d0a..00000000000
--- a/lib/gitlab/bitbucket_import/key_adder.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-module Gitlab
- module BitbucketImport
- class KeyAdder
- attr_reader :repo, :current_user, :client
-
- def initialize(repo, current_user, access_params)
- @repo, @current_user = repo, current_user
- @client = Client.new(access_params[:bitbucket_access_token],
- access_params[:bitbucket_access_token_secret])
- end
-
- def execute
- return false unless BitbucketImport.public_key.present?
-
- project_identifier = "#{repo["owner"]}/#{repo["slug"]}"
- client.add_deploy_key(project_identifier, BitbucketImport.public_key)
-
- true
- rescue
- false
- end
- end
- end
-end
diff --git a/lib/gitlab/bitbucket_import/key_deleter.rb b/lib/gitlab/bitbucket_import/key_deleter.rb
deleted file mode 100644
index e03c3155b3e..00000000000
--- a/lib/gitlab/bitbucket_import/key_deleter.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-module Gitlab
- module BitbucketImport
- class KeyDeleter
- attr_reader :project, :current_user, :client
-
- def initialize(project)
- @project = project
- @current_user = project.creator
- @client = Client.from_project(@project)
- end
-
- def execute
- return false unless BitbucketImport.public_key.present?
-
- client.delete_deploy_key(project.import_source, BitbucketImport.public_key)
-
- true
- rescue
- false
- end
- end
- end
-end
diff --git a/lib/gitlab/bitbucket_import/project_creator.rb b/lib/gitlab/bitbucket_import/project_creator.rb
index b90ef0b0fba..d94f70fd1fb 100644
--- a/lib/gitlab/bitbucket_import/project_creator.rb
+++ b/lib/gitlab/bitbucket_import/project_creator.rb
@@ -1,10 +1,11 @@
module Gitlab
module BitbucketImport
class ProjectCreator
- attr_reader :repo, :namespace, :current_user, :session_data
+ attr_reader :repo, :name, :namespace, :current_user, :session_data
- def initialize(repo, namespace, current_user, session_data)
+ def initialize(repo, name, namespace, current_user, session_data)
@repo = repo
+ @name = name
@namespace = namespace
@current_user = current_user
@session_data = session_data
@@ -13,17 +14,24 @@ module Gitlab
def execute
::Projects::CreateService.new(
current_user,
- name: repo["name"],
- path: repo["slug"],
- description: repo["description"],
+ name: name,
+ path: name,
+ description: repo.description,
namespace_id: namespace.id,
- visibility_level: repo["is_private"] ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC,
- import_type: "bitbucket",
- import_source: "#{repo["owner"]}/#{repo["slug"]}",
- import_url: "ssh://git@bitbucket.org/#{repo["owner"]}/#{repo["slug"]}.git",
- import_data: { credentials: { bb_session: session_data } }
+ visibility_level: repo.visibility_level,
+ import_type: 'bitbucket',
+ import_source: repo.full_name,
+ import_url: repo.clone_url(session_data[:token]),
+ import_data: { credentials: session_data },
+ skip_wiki: skip_wiki
).execute
end
+
+ private
+
+ def skip_wiki
+ repo.has_wiki?
+ end
end
end
end
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/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb
index 1dba85c1b51..cefb6775db8 100644
--- a/lib/gitlab/chat_commands/issue_create.rb
+++ b/lib/gitlab/chat_commands/issue_create.rb
@@ -8,7 +8,7 @@ module Gitlab
end
def self.help_message
- 'issue new <title>\n<description>'
+ 'issue new <title> *`⇧ Shift`*+*`↵ Enter`* <description>'
end
def self.allowed?(project, user)
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/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index 6b6a86ffde9..9c391fa92a3 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -4,11 +4,12 @@ module Gitlab
attr_reader :user_access, :project, :skip_authorization
def initialize(
- change, user_access:, project:, skip_authorization: false)
+ change, user_access:, project:, env: {}, skip_authorization: false)
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
@branch_name = Gitlab::Git.branch_name(@ref)
@user_access = user_access
@project = project
+ @env = env
@skip_authorization = skip_authorization
end
@@ -75,7 +76,7 @@ module Gitlab
end
def forced_push?
- Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev)
+ Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev, env: @env)
end
def matching_merge_request?
diff --git a/lib/gitlab/checks/force_push.rb b/lib/gitlab/checks/force_push.rb
index 5fe86553bd0..de0c9049ebf 100644
--- a/lib/gitlab/checks/force_push.rb
+++ b/lib/gitlab/checks/force_push.rb
@@ -1,15 +1,20 @@
module Gitlab
module Checks
class ForcePush
- def self.force_push?(project, oldrev, newrev)
+ def self.force_push?(project, oldrev, newrev, env: {})
return false if project.empty_repo?
# Created or deleted branch
if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev)
false
else
- missed_ref, _ = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} --git-dir=#{project.repository.path_to_repo} rev-list --max-count=1 #{oldrev} ^#{newrev}))
- missed_ref.present?
+ missed_ref, exit_status = Gitlab::Git::RevList.new(oldrev, newrev, project: project, env: env).execute
+
+ if exit_status == 0
+ missed_ref.present?
+ else
+ raise "Got a non-zero exit code while calling out to `git rev-list` in the force-push check."
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb
new file mode 100644
index 00000000000..a979fe7d573
--- /dev/null
+++ b/lib/gitlab/ci/status/build/cancelable.rb
@@ -0,0 +1,37 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class Cancelable < SimpleDelegator
+ include Status::Extended
+
+ def has_action?
+ can?(user, :update_build, subject)
+ end
+
+ def action_icon
+ 'ban'
+ end
+
+ def action_path
+ cancel_namespace_project_build_path(subject.project.namespace,
+ subject.project,
+ subject)
+ end
+
+ def action_method
+ :post
+ end
+
+ def action_title
+ 'Cancel'
+ end
+
+ def self.matches?(build, user)
+ build.cancelable?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/common.rb b/lib/gitlab/ci/status/build/common.rb
new file mode 100644
index 00000000000..3fec2c5d4db
--- /dev/null
+++ b/lib/gitlab/ci/status/build/common.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ module Common
+ def has_details?
+ can?(user, :read_build, subject)
+ end
+
+ def details_path
+ namespace_project_build_path(subject.project.namespace,
+ subject.project,
+ subject)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb
new file mode 100644
index 00000000000..eee9a64120b
--- /dev/null
+++ b/lib/gitlab/ci/status/build/factory.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class Factory < Status::Factory
+ def self.extended_statuses
+ [Status::Build::Stop, Status::Build::Play,
+ Status::Build::Cancelable, Status::Build::Retryable]
+ end
+
+ def self.common_helpers
+ Status::Build::Common
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb
new file mode 100644
index 00000000000..1bf949c96dd
--- /dev/null
+++ b/lib/gitlab/ci/status/build/play.rb
@@ -0,0 +1,57 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class Play < SimpleDelegator
+ include Status::Extended
+
+ def text
+ 'manual'
+ end
+
+ def label
+ 'manual play action'
+ end
+
+ def icon
+ 'icon_status_manual'
+ end
+
+ def group
+ 'manual'
+ end
+
+ def has_action?
+ can?(user, :update_build, subject)
+ end
+
+ def action_icon
+ 'play'
+ end
+
+ def action_title
+ 'Play'
+ end
+
+ def action_class
+ 'ci-play-icon'
+ end
+
+ def action_path
+ play_namespace_project_build_path(subject.project.namespace,
+ subject.project,
+ subject)
+ end
+
+ def action_method
+ :post
+ end
+
+ def self.matches?(build, user)
+ build.playable? && !build.stops_environment?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb
new file mode 100644
index 00000000000..8e38d6a8523
--- /dev/null
+++ b/lib/gitlab/ci/status/build/retryable.rb
@@ -0,0 +1,37 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class Retryable < SimpleDelegator
+ include Status::Extended
+
+ def has_action?
+ can?(user, :update_build, subject)
+ end
+
+ def action_icon
+ 'refresh'
+ end
+
+ def action_title
+ 'Retry'
+ end
+
+ def action_path
+ retry_namespace_project_build_path(subject.project.namespace,
+ subject.project,
+ subject)
+ end
+
+ def action_method
+ :post
+ end
+
+ def self.matches?(build, user)
+ build.retryable?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb
new file mode 100644
index 00000000000..e1dfdb76d41
--- /dev/null
+++ b/lib/gitlab/ci/status/build/stop.rb
@@ -0,0 +1,53 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class Stop < SimpleDelegator
+ include Status::Extended
+
+ def text
+ 'manual'
+ end
+
+ def label
+ 'manual stop action'
+ end
+
+ def icon
+ 'icon_status_manual'
+ end
+
+ def group
+ 'manual'
+ end
+
+ def has_action?
+ can?(user, :update_build, subject)
+ end
+
+ def action_icon
+ 'stop'
+ end
+
+ def action_title
+ 'Stop'
+ end
+
+ def action_path
+ play_namespace_project_build_path(subject.project.namespace,
+ subject.project,
+ subject)
+ end
+
+ def action_method
+ :post
+ end
+
+ def self.matches?(build, user)
+ build.playable? && build.stops_environment?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb
index ce4108fdcf2..73b6ab5a635 100644
--- a/lib/gitlab/ci/status/core.rb
+++ b/lib/gitlab/ci/status/core.rb
@@ -4,10 +4,14 @@ module Gitlab
# Base abstract class fore core status
#
class Core
- include Gitlab::Routing.url_helpers
+ include Gitlab::Routing
+ include Gitlab::Allowable
- def initialize(subject)
+ attr_reader :subject, :user
+
+ def initialize(subject, user)
@subject = subject
+ @user = user
end
def icon
@@ -18,23 +22,12 @@ module Gitlab
raise NotImplementedError
end
- def title
- "#{@subject.class.name.demodulize}: #{label}"
- end
-
- # Deprecation warning: this method is here because we need to maintain
- # backwards compatibility with legacy statuses. We often do something
- # like "ci-status ci-status-#{status}" to set CSS class.
- #
- # `to_s` method should be renamed to `group` at some point, after
- # phasing legacy satuses out.
- #
- def to_s
- self.class.name.demodulize.downcase.underscore
+ def group
+ self.class.name.demodulize.underscore
end
def has_details?
- raise NotImplementedError
+ false
end
def details_path
@@ -42,16 +35,27 @@ module Gitlab
end
def has_action?
- raise NotImplementedError
+ false
end
def action_icon
raise NotImplementedError
end
+ def action_class
+ end
+
def action_path
raise NotImplementedError
end
+
+ def action_method
+ raise NotImplementedError
+ end
+
+ def action_title
+ raise NotImplementedError
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/extended.rb b/lib/gitlab/ci/status/extended.rb
index 6bfb5d38c1f..d367c9bda69 100644
--- a/lib/gitlab/ci/status/extended.rb
+++ b/lib/gitlab/ci/status/extended.rb
@@ -2,8 +2,12 @@ module Gitlab
module Ci
module Status
module Extended
- def matches?(_subject)
- raise NotImplementedError
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def matches?(_subject, _user)
+ raise NotImplementedError
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb
index b2f896f2211..ae9ef895df4 100644
--- a/lib/gitlab/ci/status/factory.rb
+++ b/lib/gitlab/ci/status/factory.rb
@@ -2,10 +2,9 @@ module Gitlab
module Ci
module Status
class Factory
- attr_reader :subject
-
- def initialize(subject)
+ def initialize(subject, user)
@subject = subject
+ @user = user
end
def fabricate!
@@ -16,27 +15,32 @@ module Gitlab
end
end
+ def self.extended_statuses
+ []
+ end
+
+ def self.common_helpers
+ Module.new
+ end
+
private
- def subject_status
- @subject_status ||= subject.status
+ def simple_status
+ @simple_status ||= @subject.status || :created
end
def core_status
Gitlab::Ci::Status
- .const_get(subject_status.capitalize)
- .new(subject)
+ .const_get(simple_status.capitalize)
+ .new(@subject, @user)
+ .extend(self.class.common_helpers)
end
def extended_status
- @extended ||= extended_statuses.find do |status|
- status.matches?(subject)
+ @extended ||= self.class.extended_statuses.find do |status|
+ status.matches?(@subject, @user)
end
end
-
- def extended_statuses
- []
- end
end
end
end
diff --git a/lib/gitlab/ci/status/pipeline/common.rb b/lib/gitlab/ci/status/pipeline/common.rb
index 25e52bec3da..76bfd18bf40 100644
--- a/lib/gitlab/ci/status/pipeline/common.rb
+++ b/lib/gitlab/ci/status/pipeline/common.rb
@@ -4,13 +4,13 @@ module Gitlab
module Pipeline
module Common
def has_details?
- true
+ can?(user, :read_pipeline, subject)
end
def details_path
- namespace_project_pipeline_path(@subject.project.namespace,
- @subject.project,
- @subject)
+ namespace_project_pipeline_path(subject.project.namespace,
+ subject.project,
+ subject)
end
def has_action?
diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb
index 4ac4ec671d0..16dcb326be9 100644
--- a/lib/gitlab/ci/status/pipeline/factory.rb
+++ b/lib/gitlab/ci/status/pipeline/factory.rb
@@ -3,14 +3,12 @@ module Gitlab
module Status
module Pipeline
class Factory < Status::Factory
- private
-
- def extended_statuses
+ def self.extended_statuses
[Pipeline::SuccessWithWarnings]
end
- def core_status
- super.extend(Status::Pipeline::Common)
+ def self.common_helpers
+ Status::Pipeline::Common
end
end
end
diff --git a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb
index 4b040d60df8..24bf8b869e0 100644
--- a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb
+++ b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb
@@ -3,7 +3,7 @@ module Gitlab
module Status
module Pipeline
class SuccessWithWarnings < SimpleDelegator
- extend Status::Extended
+ include Status::Extended
def text
'passed'
@@ -17,11 +17,11 @@ module Gitlab
'icon_status_warning'
end
- def to_s
+ def group
'success_with_warnings'
end
- def self.matches?(pipeline)
+ def self.matches?(pipeline, user)
pipeline.success? && pipeline.has_warnings?
end
end
diff --git a/lib/gitlab/ci/status/stage/common.rb b/lib/gitlab/ci/status/stage/common.rb
index 14c437d2b98..7852f492e1d 100644
--- a/lib/gitlab/ci/status/stage/common.rb
+++ b/lib/gitlab/ci/status/stage/common.rb
@@ -4,14 +4,14 @@ module Gitlab
module Stage
module Common
def has_details?
- true
+ can?(user, :read_pipeline, subject.pipeline)
end
def details_path
- namespace_project_pipeline_path(@subject.project.namespace,
- @subject.project,
- @subject.pipeline,
- anchor: @subject.name)
+ namespace_project_pipeline_path(subject.project.namespace,
+ subject.project,
+ subject.pipeline,
+ anchor: subject.name)
end
def has_action?
diff --git a/lib/gitlab/ci/status/stage/factory.rb b/lib/gitlab/ci/status/stage/factory.rb
index c6522d5ada1..689a5dd45bc 100644
--- a/lib/gitlab/ci/status/stage/factory.rb
+++ b/lib/gitlab/ci/status/stage/factory.rb
@@ -3,10 +3,8 @@ module Gitlab
module Status
module Stage
class Factory < Status::Factory
- private
-
- def core_status
- super.extend(Status::Stage::Common)
+ def self.common_helpers
+ Status::Stage::Common
end
end
end
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/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb
index 85402c2a278..f586c5ab062 100644
--- a/lib/gitlab/email/reply_parser.rb
+++ b/lib/gitlab/email/reply_parser.rb
@@ -69,7 +69,7 @@ module Gitlab
# This one might be controversial but so many reply lines have years, times and end with a colon.
# Let's try it and see how well it works.
break if (l =~ /\d{4}/ && l =~ /\d:\d\d/ && l =~ /\:$/) ||
- (l =~ /On \w+ \d+,? \d+,?.*wrote:/)
+ (l =~ /On \w+ \d+,? \d+,?.*wrote:/)
# Headers on subsequent lines
break if (0..2).all? { |off| lines[idx + off] =~ REPLYING_HEADER_REGEX }
diff --git a/lib/gitlab/gfm/uploads_rewriter.rb b/lib/gitlab/gfm/uploads_rewriter.rb
index abc8c8c55e6..8fab5489616 100644
--- a/lib/gitlab/gfm/uploads_rewriter.rb
+++ b/lib/gitlab/gfm/uploads_rewriter.rb
@@ -1,3 +1,5 @@
+require 'fileutils'
+
module Gitlab
module Gfm
##
@@ -22,7 +24,9 @@ module Gitlab
return markdown unless file.try(:exists?)
new_uploader = FileUploader.new(target_project)
- new_uploader.store!(file)
+ with_link_in_tmp_dir(file.file) do |open_tmp_file|
+ new_uploader.store!(open_tmp_file)
+ end
new_uploader.to_markdown
end
end
@@ -46,6 +50,19 @@ module Gitlab
uploader.retrieve_from_store!(file)
uploader.file
end
+
+ # Because the uploaders use 'move_to_store' we must have a temporary
+ # file that is allowed to be (re)moved.
+ def with_link_in_tmp_dir(file)
+ dir = Dir.mktmpdir('UploadsRewriter', File.dirname(file))
+ # The filename matters to Carrierwave so we make sure to preserve it
+ tmp_file = File.join(dir, File.basename(file))
+ File.link(file, tmp_file)
+ # Open the file to placate Carrierwave
+ File.open(tmp_file) { |open_file| yield open_file }
+ ensure
+ FileUtils.rm_rf(dir)
+ end
end
end
end
diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb
new file mode 100644
index 00000000000..79dd0cf7df2
--- /dev/null
+++ b/lib/gitlab/git/rev_list.rb
@@ -0,0 +1,42 @@
+module Gitlab
+ module Git
+ class RevList
+ attr_reader :project, :env
+
+ ALLOWED_VARIABLES = %w[GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES].freeze
+
+ def initialize(oldrev, newrev, project:, env: nil)
+ @project = project
+ @env = env.presence || {}
+ @args = [Gitlab.config.git.bin_path,
+ "--git-dir=#{project.repository.path_to_repo}",
+ "rev-list",
+ "--max-count=1",
+ oldrev,
+ "^#{newrev}"]
+ end
+
+ def execute
+ Gitlab::Popen.popen(@args, nil, parse_environment_variables)
+ end
+
+ def valid?
+ environment_variables.all? do |(name, value)|
+ value.to_s.start_with?(project.repository.path_to_repo)
+ end
+ end
+
+ private
+
+ def parse_environment_variables
+ return {} unless valid?
+
+ environment_variables
+ end
+
+ def environment_variables
+ @environment_variables ||= env.slice(*ALLOWED_VARIABLES).compact
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 13efc1ed73d..545506f3dfd 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -18,12 +18,13 @@ module Gitlab
attr_reader :actor, :project, :protocol, :user_access, :authentication_abilities
- def initialize(actor, project, protocol, authentication_abilities:)
+ def initialize(actor, project, protocol, authentication_abilities:, env: {})
@actor = actor
@project = project
@protocol = protocol
@authentication_abilities = authentication_abilities
@user_access = UserAccess.new(user, project: project)
+ @env = env
end
def check(cmd, changes)
@@ -147,6 +148,7 @@ module Gitlab
change,
user_access: user_access,
project: project,
+ env: @env,
skip_authorization: deploy_key?).exec
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/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 2c21804fe7a..4d4e04e9e35 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -8,6 +8,8 @@ module Gitlab
gon.shortcuts_path = help_page_path('shortcuts')
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
gon.award_menu_url = emojis_path
+ gon.katex_css_url = ActionController::Base.helpers.asset_path('katex.css')
+ gon.katex_js_url = ActionController::Base.helpers.asset_path('katex.js')
if current_user
gon.current_user_id = current_user.id
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index c551321c18d..cda6ddf0443 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -120,7 +120,7 @@ module Gitlab
members_mapper: members_mapper,
user: @user,
project_id: restored_project.id)
- end
+ end.compact
relation_hash_list.is_a?(Array) ? relation_array : relation_array.first
end
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index a0e80fccad9..65b229ca8ff 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -14,7 +14,7 @@ module Gitlab
priorities: :label_priorities,
label: :project_label }.freeze
- USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id].freeze
+ USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id merge_user_id].freeze
PROJECT_REFERENCES = %w[project_id source_project_id gl_project_id target_project_id].freeze
@@ -40,6 +40,8 @@ module Gitlab
# the relation_hash, updating references with new object IDs, mapping users using
# the "members_mapper" object, also updating notes if required.
def create
+ return nil if unknown_service?
+
setup_models
generate_imported_object
@@ -99,6 +101,8 @@ module Gitlab
def generate_imported_object
if BUILD_MODELS.include?(@relation_name) # call #trace= method after assigning the other attributes
trace = @relation_hash.delete('trace')
+ @relation_hash.delete('token')
+
imported_object do |object|
object.trace = trace
object.commit_id = nil
@@ -215,6 +219,11 @@ module Gitlab
existing_object
end
end
+
+ def unknown_service?
+ @relation_name == :services && parsed_relation_hash['type'] &&
+ !Object.const_defined?(parsed_relation_hash['type'])
+ 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/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb
new file mode 100644
index 00000000000..65713e73a59
--- /dev/null
+++ b/lib/gitlab/middleware/multipart.rb
@@ -0,0 +1,99 @@
+# Gitlab::Middleware::Multipart - a Rack::Multipart replacement
+#
+# Rack::Multipart leaves behind tempfiles in /tmp and uses valuable Ruby
+# process time to copy files around. This alternative solution uses
+# gitlab-workhorse to clean up the tempfiles and puts the tempfiles in a
+# location where copying should not be needed.
+#
+# When gitlab-workhorse finds files in a multipart MIME body it sends
+# a signed message via a request header. This message lists the names of
+# the multipart entries that gitlab-workhorse filtered out of the
+# multipart structure and saved to tempfiles. Workhorse adds new entries
+# in the multipart structure with paths to the tempfiles.
+#
+# The job of this Rack middleware is to detect and decode the message
+# from workhorse. If present, it walks the Rack 'params' hash for the
+# current request, opens the respective tempfiles, and inserts the open
+# Ruby File objects in the params hash where Rack::Multipart would have
+# put them. The goal is that application code deeper down can keep
+# working the way it did with Rack::Multipart without changes.
+#
+# CAVEAT: the code that modifies the params hash is a bit complex. It is
+# conceivable that certain Rack params structures will not be modified
+# correctly. We are not aware of such bugs at this time though.
+#
+
+module Gitlab
+ module Middleware
+ class Multipart
+ RACK_ENV_KEY = 'HTTP_GITLAB_WORKHORSE_MULTIPART_FIELDS'
+
+ class Handler
+ def initialize(env, message)
+ @request = Rack::Request.new(env)
+ @rewritten_fields = message['rewritten_fields']
+ @open_files = []
+ end
+
+ def with_open_files
+ @rewritten_fields.each do |field, tmp_path|
+ parsed_field = Rack::Utils.parse_nested_query(field)
+ raise "unexpected field: #{field.inspect}" unless parsed_field.count == 1
+
+ key, value = parsed_field.first
+ if value.nil?
+ value = File.open(tmp_path)
+ @open_files << value
+ else
+ value = decorate_params_value(value, @request.params[key], tmp_path)
+ end
+ @request.update_param(key, value)
+ end
+
+ yield
+ ensure
+ @open_files.each(&:close)
+ end
+
+ # This function calls itself recursively
+ def decorate_params_value(path_hash, value_hash, tmp_path)
+ unless path_hash.is_a?(Hash) && path_hash.count == 1
+ raise "invalid path: #{path_hash.inspect}"
+ end
+ path_key, path_value = path_hash.first
+
+ unless value_hash.is_a?(Hash) && value_hash[path_key]
+ raise "invalid value hash: #{value_hash.inspect}"
+ end
+
+ case path_value
+ when nil
+ value_hash[path_key] = File.open(tmp_path)
+ @open_files << value_hash[path_key]
+ value_hash
+ when Hash
+ decorate_params_value(path_value, value_hash[path_key], tmp_path)
+ value_hash
+ else
+ raise "unexpected path value: #{path_value.inspect}"
+ end
+ end
+ end
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ encoded_message = env.delete(RACK_ENV_KEY)
+ return @app.call(env) if encoded_message.blank?
+
+ message = Gitlab::Workhorse.decode_jwt(encoded_message)[0]
+
+ Handler.new(env, message).with_open_files do
+ @app.call(env)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/popen.rb b/lib/gitlab/popen.rb
index cc74bb29087..4bc5cda8cb5 100644
--- a/lib/gitlab/popen.rb
+++ b/lib/gitlab/popen.rb
@@ -5,13 +5,13 @@ module Gitlab
module Popen
extend self
- def popen(cmd, path = nil)
+ def popen(cmd, path = nil, vars = {})
unless cmd.is_a?(Array)
raise "System commands must be given as an array of strings"
end
path ||= Dir.pwd
- vars = { "PWD" => path }
+ vars['PWD'] = path
options = { chdir: path }
unless File.directory?(path)
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 66e6b29e798..6bdf3db9cb8 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -110,7 +110,7 @@ module Gitlab
end
def notes
- @notes ||= project.notes.user.search(query, as_user: @current_user).order('updated_at DESC')
+ @notes ||= NotesFinder.new(project, @current_user, search: query).execute.user.order('updated_at DESC')
end
def commits
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index a06cf6a989c..9e0b0e5ea98 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -61,7 +61,7 @@ module Gitlab
end
def file_name_regex
- @file_name_regex ||= /\A[a-zA-Z0-9_\-\.\@]*\z/.freeze
+ @file_name_regex ||= /\A[[[:alnum:]]_\-\.\@]*\z/.freeze
end
def file_name_regex_message
@@ -69,7 +69,7 @@ module Gitlab
end
def file_path_regex
- @file_path_regex ||= /\A[a-zA-Z0-9_\-\.\/\@]*\z/.freeze
+ @file_path_regex ||= /\A[[[:alnum:]]_\-\.\/\@]*\z/.freeze
end
def file_path_regex_message
@@ -123,5 +123,22 @@ module Gitlab
def environment_name_regex_message
"can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.' and spaces"
end
+
+ def kubernetes_namespace_regex
+ /\A[a-z0-9]([-a-z0-9]*[a-z0-9])?\z/
+ end
+
+ def kubernetes_namespace_regex_message
+ "can contain only letters, digits or '-', and cannot start or end with '-'"
+ end
+
+ def environment_slug_regex
+ @environment_slug_regex ||= /\A[a-z]([a-z0-9-]*[a-z0-9])?\z/.freeze
+ end
+
+ def environment_slug_regex_message
+ "can contain only lowercase letters, digits, and '-'. " \
+ "Must start with a letter, and cannot end with '-'"
+ end
end
end
diff --git a/lib/gitlab/routing.rb b/lib/gitlab/routing.rb
index 5132177de51..632e2d87500 100644
--- a/lib/gitlab/routing.rb
+++ b/lib/gitlab/routing.rb
@@ -1,5 +1,11 @@
module Gitlab
module Routing
+ extend ActiveSupport::Concern
+
+ included do
+ include Gitlab::Routing.url_helpers
+ end
+
# Returns the URL helpers Module.
#
# This method caches the output as Rails' "url_helpers" method creates an
diff --git a/lib/gitlab/serialize/ci/variables.rb b/lib/gitlab/serialize/ci/variables.rb
new file mode 100644
index 00000000000..3a9443bfcd9
--- /dev/null
+++ b/lib/gitlab/serialize/ci/variables.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module Serialize
+ module Ci
+ # This serializer could make sure our YAML variables' keys and values
+ # are always strings. This is more for legacy build data because
+ # from now on we convert them into strings before saving to database.
+ module Variables
+ extend self
+
+ def load(string)
+ return unless string
+
+ object = YAML.safe_load(string, [Symbol])
+
+ object.map do |variable|
+ variable[:key] = variable[:key].to_s
+ variable
+ end
+ end
+
+ def dump(object)
+ YAML.dump(object)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sql/union.rb b/lib/gitlab/sql/union.rb
index 1cd89b3a9c4..222021e8802 100644
--- a/lib/gitlab/sql/union.rb
+++ b/lib/gitlab/sql/union.rb
@@ -22,9 +22,7 @@ module Gitlab
# By using "unprepared_statements" we remove the usage of placeholders
# (thus fixing this problem), at a slight performance cost.
fragments = ActiveRecord::Base.connection.unprepared_statement do
- @relations.map do |rel|
- rel.reorder(nil).to_sql
- end
+ @relations.map { |rel| rel.reorder(nil).to_sql }.reject(&:blank?)
end
fragments.join("\nUNION\n")
diff --git a/lib/gitlab/template/dockerfile_template.rb b/lib/gitlab/template/dockerfile_template.rb
new file mode 100644
index 00000000000..d5d3e045a42
--- /dev/null
+++ b/lib/gitlab/template/dockerfile_template.rb
@@ -0,0 +1,30 @@
+module Gitlab
+ module Template
+ class DockerfileTemplate < BaseTemplate
+ def content
+ explanation = "# This file is a template, and might need editing before it works on your project."
+ [explanation, super].join("\n")
+ end
+
+ class << self
+ def extension
+ 'Dockerfile'
+ end
+
+ def categories
+ {
+ "General" => ''
+ }
+ end
+
+ def base_dir
+ Rails.root.join('vendor/dockerfile')
+ end
+
+ def finder(project = nil)
+ Gitlab::Template::Finders::GlobalTemplateFinder.new(self.base_dir, self.extension, self.categories)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 594439a5d4b..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'
@@ -117,8 +130,12 @@ module Gitlab
end
def verify_api_request!(request_headers)
+ decode_jwt(request_headers[INTERNAL_API_REQUEST_HEADER])
+ end
+
+ def decode_jwt(encoded_message)
JWT.decode(
- request_headers[INTERNAL_API_REQUEST_HEADER],
+ encoded_message,
secret,
true,
{ iss: 'gitlab-workhorse', verify_iss: true, algorithm: 'HS256' },
diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb
new file mode 100644
index 00000000000..fb8d7d97f8a
--- /dev/null
+++ b/lib/mattermost/session.rb
@@ -0,0 +1,115 @@
+module Mattermost
+ class NoSessionError < StandardError; 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.
+ #
+ # The process depends on OAuth, but skips a step in the authentication cycle.
+ # For example, usually a user would click the 'login in GitLab' button on
+ # Mattermost, which would yield a 302 status code and redirects you to GitLab
+ # to approve the use of your account on Mattermost. Which would trigger a
+ # callback so Mattermost knows this request is approved and gets the required
+ # data to create the user account etc.
+ #
+ # This class however skips the button click, and also the approval phase to
+ # speed up the process and keep it without manual action and get a session
+ # going.
+ class Session
+ include Doorkeeper::Helpers::Controller
+ include HTTParty
+
+ base_uri Settings.mattermost.host
+
+ attr_accessor :current_resource_owner, :token
+
+ def initialize(current_user)
+ @current_resource_owner = current_user
+ end
+
+ def with_session
+ raise NoSessionError unless create
+
+ begin
+ yield self
+ ensure
+ destroy
+ end
+ end
+
+ # Next methods are needed for Doorkeeper
+ def pre_auth
+ @pre_auth ||= Doorkeeper::OAuth::PreAuthorization.new(
+ Doorkeeper.configuration, server.client_via_uid, params)
+ end
+
+ def authorization
+ @authorization ||= strategy.request
+ end
+
+ def strategy
+ @strategy ||= server.authorization_request(pre_auth.response_type)
+ end
+
+ def request
+ @request ||= OpenStruct.new(parameters: params)
+ end
+
+ def params
+ Rack::Utils.parse_query(oauth_uri.query).symbolize_keys
+ end
+
+ def get(path, options = {})
+ self.class.get(path, options.merge(headers: @headers))
+ end
+
+ def post(path, options = {})
+ self.class.post(path, options.merge(headers: @headers))
+ end
+
+ private
+
+ def create
+ return unless oauth_uri
+ return unless token_uri
+
+ @token = request_token
+ @headers = {
+ Authorization: "Bearer #{@token}"
+ }
+
+ @token
+ end
+
+ def destroy
+ post('/api/v3/users/logout')
+ end
+
+ def oauth_uri
+ return @oauth_uri if defined?(@oauth_uri)
+
+ @oauth_uri = nil
+
+ response = get("/api/v3/oauth/gitlab/login", follow_redirects: false)
+ return unless 300 <= response.code && response.code < 400
+
+ redirect_uri = response.headers['location']
+ return unless redirect_uri
+
+ @oauth_uri = URI.parse(redirect_uri)
+ end
+
+ def token_uri
+ @token_uri ||=
+ if oauth_uri
+ authorization.authorize.redirect_uri if pre_auth.authorizable?
+ end
+ end
+
+ def request_token
+ response = get(token_uri, follow_redirects: false)
+
+ if 200 <= response.code && response.code < 400
+ response.headers['token']
+ end
+ end
+ end
+end
diff --git a/lib/omniauth/strategies/bitbucket.rb b/lib/omniauth/strategies/bitbucket.rb
new file mode 100644
index 00000000000..5a7d67c2390
--- /dev/null
+++ b/lib/omniauth/strategies/bitbucket.rb
@@ -0,0 +1,41 @@
+require 'omniauth-oauth2'
+
+module OmniAuth
+ module Strategies
+ class Bitbucket < OmniAuth::Strategies::OAuth2
+ option :name, 'bitbucket'
+
+ option :client_options, {
+ site: 'https://bitbucket.org',
+ authorize_url: 'https://bitbucket.org/site/oauth2/authorize',
+ token_url: 'https://bitbucket.org/site/oauth2/access_token'
+ }
+
+ uid do
+ raw_info['username']
+ end
+
+ info do
+ {
+ name: raw_info['display_name'],
+ avatar: raw_info['links']['avatar']['href'],
+ email: primary_email
+ }
+ end
+
+ def raw_info
+ @raw_info ||= access_token.get('api/2.0/user').parsed
+ end
+
+ def primary_email
+ primary = emails.find { |i| i['is_primary'] && i['is_confirmed'] }
+ primary && primary['email'] || nil
+ end
+
+ def emails
+ email_response = access_token.get('api/2.0/user/emails').parsed
+ @emails ||= email_response && email_response['values'] || nil
+ end
+ end
+ end
+end
diff --git a/lib/rouge/lexers/math.rb b/lib/rouge/lexers/math.rb
new file mode 100644
index 00000000000..80784adfd76
--- /dev/null
+++ b/lib/rouge/lexers/math.rb
@@ -0,0 +1,21 @@
+module Rouge
+ module Lexers
+ class Math < Lexer
+ title "A passthrough lexer used for LaTeX input"
+ desc "A boring lexer that doesn't highlight anything"
+
+ tag 'math'
+ mimetypes 'text/plain'
+
+ default_options token: 'Text'
+
+ def token
+ @token ||= Token[option :token]
+ end
+
+ def stream_tokens(string, &b)
+ yield self.token, string
+ end
+ end
+ end
+end
diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab
index d521de28e8a..2f7c34a3f31 100644
--- a/lib/support/nginx/gitlab
+++ b/lib/support/nginx/gitlab
@@ -20,6 +20,11 @@ upstream gitlab-workhorse {
server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0;
}
+map $http_upgrade $connection_upgrade_gitlab {
+ default upgrade;
+ '' close;
+}
+
## Normal HTTP host
server {
## Either remove "default_server" from the listen line below,
@@ -53,6 +58,8 @@ server {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade_gitlab;
proxy_pass http://gitlab-workhorse;
}
diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl
index bf014b56cf6..5661394058d 100644
--- a/lib/support/nginx/gitlab-ssl
+++ b/lib/support/nginx/gitlab-ssl
@@ -24,6 +24,11 @@ upstream gitlab-workhorse {
server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0;
}
+map $http_upgrade $connection_upgrade_gitlab_ssl {
+ default upgrade;
+ '' close;
+}
+
## Redirects all HTTP traffic to the HTTPS host
server {
## Either remove "default_server" from the listen line below,
@@ -98,6 +103,9 @@ server {
proxy_set_header X-Forwarded-Ssl on;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade_gitlab_ssl;
+
proxy_pass http://gitlab-workhorse;
}
diff --git a/lib/tasks/gitlab/ldap.rake b/lib/tasks/gitlab/ldap.rake
new file mode 100644
index 00000000000..c66a2a263dc
--- /dev/null
+++ b/lib/tasks/gitlab/ldap.rake
@@ -0,0 +1,40 @@
+namespace :gitlab do
+ namespace :ldap do
+ desc 'GitLab | LDAP | Rename provider'
+ task :rename_provider, [:old_provider, :new_provider] => :environment do |_, args|
+ old_provider = args[:old_provider] ||
+ prompt('What is the old provider? Ex. \'ldapmain\': '.color(:blue))
+ new_provider = args[:new_provider] ||
+ prompt('What is the new provider ID? Ex. \'ldapcustom\': '.color(:blue))
+ puts '' # Add some separation in the output
+
+ identities = Identity.where(provider: old_provider)
+ identity_count = identities.count
+
+ if identities.empty?
+ puts "Found no user identities with '#{old_provider}' provider."
+ puts 'Please check the provider name and try again.'
+ exit 1
+ end
+
+ plural_id_count = ActionController::Base.helpers.pluralize(identity_count, 'user')
+
+ unless ENV['force'] == 'yes'
+ puts "#{plural_id_count} with provider '#{old_provider}' will be updated to '#{new_provider}'"
+ puts 'If the new provider is incorrect, users will be unable to sign in'
+ ask_to_continue
+ puts ''
+ end
+
+ updated_count = identities.update_all(provider: new_provider)
+
+ if updated_count == identity_count
+ puts 'User identities were successfully updated'.color(:green)
+ else
+ plural_updated_count = ActionController::Base.helpers.pluralize(updated_count, 'user')
+ puts 'Some user identities could not be updated'.color(:red)
+ puts "Successfully updated #{plural_updated_count} out of #{plural_id_count} total"
+ end
+ end
+ end
+end
diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake
index 141a0b74ec0..f5caca3ddbf 100644
--- a/lib/tasks/migrate/setup_postgresql.rake
+++ b/lib/tasks/migrate/setup_postgresql.rake
@@ -1,8 +1,12 @@
+require Rails.root.join('lib/gitlab/database')
+require Rails.root.join('lib/gitlab/database/migration_helpers')
require Rails.root.join('db/migrate/20151007120511_namespaces_projects_path_lower_indexes')
require Rails.root.join('db/migrate/20151008110232_add_users_lower_username_email_indexes')
+require Rails.root.join('db/migrate/20161212142807_add_lower_path_index_to_routes')
desc 'GitLab | Sets up PostgreSQL'
task setup_postgresql: :environment do
NamespacesProjectsPathLowerIndexes.new.up
AddUsersLowerUsernameEmailIndexes.new.up
+ AddLowerPathIndexToRoutes.new.up
end
diff --git a/package.json b/package.json
index 961989f8012..49b8210e427 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"scripts": {
- "eslint": "eslint --ext .js,.js.es6 .",
+ "eslint": "eslint --max-warnings 0 --ext .js,.js.es6 .",
"eslint-fix": "npm run eslint -- --fix",
"eslint-report": "npm run eslint -- --format html --output-file ./eslint-report.html"
},
diff --git a/public/404.html b/public/404.html
index 11b29d09a82..b3b3a0fa3f3 100644
--- a/public/404.html
+++ b/public/404.html
@@ -46,6 +46,14 @@
margin: 40px auto;
}
+ a {
+ line-height: 100px;
+ font-weight: normal;
+ color: #4A8BEE;
+ font-size: 18px;
+ text-decoration: none;
+ }
+
.container {
margin: auto 20px;
}
@@ -63,6 +71,7 @@
<hr />
<p>Make sure the address is correct and that the page hasn't moved.</p>
<p>Please contact your GitLab administrator if you think this is a mistake.</p>
+ <a href="javascript:history.back()">Go back</a>
</div>
</body>
</html>
diff --git a/public/422.html b/public/422.html
index 9bd7cb4b7c8..119e54ad8bd 100644
--- a/public/422.html
+++ b/public/422.html
@@ -46,6 +46,14 @@
margin: 40px auto;
}
+ a {
+ line-height: 100px;
+ font-weight: normal;
+ color: #4A8BEE;
+ font-size: 18px;
+ text-decoration: none;
+ }
+
.container {
margin: auto 20px;
}
@@ -63,6 +71,7 @@
<hr />
<p>Make sure you have access to the thing you tried to change.</p>
<p>Please contact your GitLab administrator if you think this is a mistake.</p>
+ <a href="javascript:history.back()">Go back</a>
</div>
</body>
</html>
diff --git a/public/500.html b/public/500.html
index f92e8839f8d..226ef3c40ea 100644
--- a/public/500.html
+++ b/public/500.html
@@ -46,6 +46,14 @@
margin: 40px auto;
}
+ a {
+ line-height: 100px;
+ font-weight: normal;
+ color: #4A8BEE;
+ font-size: 18px;
+ text-decoration: none;
+ }
+
.container {
margin: auto 20px;
}
@@ -63,6 +71,7 @@
<hr />
<p>Try refreshing the page, or going back and attempting the action again.</p>
<p>Please contact your GitLab administrator if this problem persists.</p>
+ <a href="javascript:history.back()">Go back</a>
</div>
</body>
</html>
diff --git a/public/502.html b/public/502.html
index c2be4f130a9..f037b81bace 100644
--- a/public/502.html
+++ b/public/502.html
@@ -46,6 +46,14 @@
margin: 40px auto;
}
+ a {
+ line-height: 100px;
+ font-weight: normal;
+ color: #4A8BEE;
+ font-size: 18px;
+ text-decoration: none;
+ }
+
.container {
margin: auto 20px;
}
@@ -63,6 +71,7 @@
<hr />
<p>Try refreshing the page, or going back and attempting the action again.</p>
<p>Please contact your GitLab administrator if this problem persists.</p>
+ <a href="javascript:history.back()">Go back</a>
</div>
</body>
</html>
diff --git a/public/503.html b/public/503.html
index 8850ffce362..f946a087871 100644
--- a/public/503.html
+++ b/public/503.html
@@ -46,6 +46,14 @@
margin: 40px auto;
}
+ a {
+ line-height: 100px;
+ font-weight: normal;
+ color: #4A8BEE;
+ font-size: 18px;
+ text-decoration: none;
+ }
+
.container {
margin: auto 20px;
}
@@ -63,6 +71,7 @@
<hr />
<p>Try refreshing the page, or going back and attempting the action again.</p>
<p>Please contact your GitLab administrator if this problem persists.</p>
+ <a href="javascript:history.back()">Go back</a>
</div>
</body>
</html>
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index 1d3c9fbbe2f..ce7c0b334ee 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -6,11 +6,11 @@ describe Import::BitbucketController do
let(:user) { create(:user) }
let(:token) { "asdasd12345" }
let(:secret) { "sekrettt" }
- let(:access_params) { { bitbucket_access_token: token, bitbucket_access_token_secret: secret } }
+ let(:refresh_token) { SecureRandom.hex(15) }
+ let(:access_params) { { token: token, expires_at: nil, expires_in: nil, refresh_token: nil } }
def assign_session_tokens
- session[:bitbucket_access_token] = token
- session[:bitbucket_access_token_secret] = secret
+ session[:bitbucket_token] = token
end
before do
@@ -24,29 +24,36 @@ describe Import::BitbucketController do
end
it "updates access token" do
- access_token = double(token: token, secret: secret)
- allow_any_instance_of(Gitlab::BitbucketImport::Client).
+ expires_at = Time.now + 1.day
+ expires_in = 1.day
+ access_token = double(token: token,
+ secret: secret,
+ expires_at: expires_at,
+ expires_in: expires_in,
+ refresh_token: refresh_token)
+ allow_any_instance_of(OAuth2::Client).
to receive(:get_token).and_return(access_token)
stub_omniauth_provider('bitbucket')
get :callback
- expect(session[:bitbucket_access_token]).to eq(token)
- expect(session[:bitbucket_access_token_secret]).to eq(secret)
+ expect(session[:bitbucket_token]).to eq(token)
+ expect(session[:bitbucket_refresh_token]).to eq(refresh_token)
+ expect(session[:bitbucket_expires_at]).to eq(expires_at)
+ expect(session[:bitbucket_expires_in]).to eq(expires_in)
expect(controller).to redirect_to(status_import_bitbucket_url)
end
end
describe "GET status" do
before do
- @repo = OpenStruct.new(slug: 'vim', owner: 'asd')
+ @repo = double(slug: 'vim', owner: 'asd', full_name: 'asd/vim', "valid?" => true)
assign_session_tokens
end
it "assigns variables" do
@project = create(:project, import_type: 'bitbucket', creator_id: user.id)
- client = stub_client(projects: [@repo])
- allow(client).to receive(:incompatible_projects).and_return([])
+ allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo])
get :status
@@ -57,7 +64,7 @@ describe Import::BitbucketController do
it "does not show already added project" do
@project = create(:project, import_type: 'bitbucket', creator_id: user.id, import_source: 'asd/vim')
- stub_client(projects: [@repo])
+ allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo])
get :status
@@ -70,19 +77,16 @@ describe Import::BitbucketController do
let(:bitbucket_username) { user.username }
let(:bitbucket_user) do
- { user: { username: bitbucket_username } }.with_indifferent_access
+ double(username: bitbucket_username)
end
let(:bitbucket_repo) do
- { slug: "vim", owner: bitbucket_username }.with_indifferent_access
+ double(slug: "vim", owner: bitbucket_username, name: 'vim')
end
before do
- allow(Gitlab::BitbucketImport::KeyAdder).
- to receive(:new).with(bitbucket_repo, user, access_params).
- and_return(double(execute: true))
-
- stub_client(user: bitbucket_user, project: bitbucket_repo)
+ allow_any_instance_of(Bitbucket::Client).to receive(:repo).and_return(bitbucket_repo)
+ allow_any_instance_of(Bitbucket::Client).to receive(:user).and_return(bitbucket_user)
assign_session_tokens
end
@@ -90,7 +94,7 @@ describe Import::BitbucketController do
context "when the Bitbucket user and GitLab user's usernames match" do
it "takes the current user's namespace" do
expect(Gitlab::BitbucketImport::ProjectCreator).
- to receive(:new).with(bitbucket_repo, user.namespace, user, access_params).
+ to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params).
and_return(double(execute: true))
post :create, format: :js
@@ -102,7 +106,7 @@ describe Import::BitbucketController do
it "takes the current user's namespace" do
expect(Gitlab::BitbucketImport::ProjectCreator).
- to receive(:new).with(bitbucket_repo, user.namespace, user, access_params).
+ to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params).
and_return(double(execute: true))
post :create, format: :js
@@ -114,7 +118,7 @@ describe Import::BitbucketController do
let(:other_username) { "someone_else" }
before do
- bitbucket_repo["owner"] = other_username
+ allow(bitbucket_repo).to receive(:owner).and_return(other_username)
end
context "when a namespace with the Bitbucket user's username already exists" do
@@ -123,7 +127,7 @@ describe Import::BitbucketController do
context "when the namespace is owned by the GitLab user" do
it "takes the existing namespace" do
expect(Gitlab::BitbucketImport::ProjectCreator).
- to receive(:new).with(bitbucket_repo, existing_namespace, user, access_params).
+ to receive(:new).with(bitbucket_repo, bitbucket_repo.name, existing_namespace, user, access_params).
and_return(double(execute: true))
post :create, format: :js
@@ -156,7 +160,7 @@ describe Import::BitbucketController do
it "takes the new namespace" do
expect(Gitlab::BitbucketImport::ProjectCreator).
- to receive(:new).with(bitbucket_repo, an_instance_of(Group), user, access_params).
+ to receive(:new).with(bitbucket_repo, bitbucket_repo.name, an_instance_of(Group), user, access_params).
and_return(double(execute: true))
post :create, format: :js
@@ -177,7 +181,7 @@ describe Import::BitbucketController do
it "takes the current user's namespace" do
expect(Gitlab::BitbucketImport::ProjectCreator).
- to receive(:new).with(bitbucket_repo, user.namespace, user, access_params).
+ to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params).
and_return(double(execute: true))
post :create, format: :js
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/profiles/personal_access_tokens_spec.rb b/spec/controllers/profiles/personal_access_tokens_spec.rb
new file mode 100644
index 00000000000..45534a3a587
--- /dev/null
+++ b/spec/controllers/profiles/personal_access_tokens_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Profiles::PersonalAccessTokensController do
+ let(:user) { create(:user) }
+
+ describe '#create' do
+ def created_token
+ PersonalAccessToken.order(:created_at).last
+ end
+
+ before { sign_in(user) }
+
+ it "allows creation of a token" do
+ name = FFaker::Product.brand
+
+ post :create, personal_access_token: { name: name }
+
+ expect(created_token).not_to be_nil
+ expect(created_token.name).to eq(name)
+ expect(created_token.expires_at).to be_nil
+ expect(PersonalAccessToken.active).to include(created_token)
+ end
+
+ it "allows creation of a token with an expiry date" do
+ expires_at = 5.days.from_now
+
+ post :create, personal_access_token: { name: FFaker::Product.brand, expires_at: expires_at }
+
+ expect(created_token).not_to be_nil
+ expect(created_token.expires_at.to_i).to eq(expires_at.to_i)
+ end
+
+ context "scopes" do
+ it "allows creation of a token with scopes" do
+ post :create, personal_access_token: { name: FFaker::Product.brand, scopes: ['api', 'read_user'] }
+
+ expect(created_token).not_to be_nil
+ expect(created_token.scopes).to eq(['api', 'read_user'])
+ end
+
+ it "allows creation of a token with no scopes" do
+ post :create, personal_access_token: { name: FFaker::Product.brand, scopes: [] }
+
+ expect(created_token).not_to be_nil
+ expect(created_token.scopes).to eq([])
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/blame_controller_spec.rb b/spec/controllers/projects/blame_controller_spec.rb
index 25f06299a29..4402ca43c65 100644
--- a/spec/controllers/projects/blame_controller_spec.rb
+++ b/spec/controllers/projects/blame_controller_spec.rb
@@ -25,5 +25,10 @@ describe Projects::BlameController do
let(:id) { 'master/files/ruby/popen.rb' }
it { is_expected.to respond_with(:success) }
end
+
+ context "invalid file" do
+ let(:id) { 'master/files/ruby/missing_file.rb'}
+ it { expect(response).to have_http_status(404) }
+ end
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/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb
new file mode 100644
index 00000000000..9fd5c3b85f6
--- /dev/null
+++ b/spec/controllers/projects/releases_controller_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe Projects::ReleasesController do
+ let!(:project) { create(:project) }
+ let!(:user) { create(:user) }
+ let!(:release) { create(:release, project: project) }
+ let!(:tag) { release.tag }
+
+ before do
+ project.team << [user, :developer]
+ sign_in(user)
+ end
+
+ describe 'GET #edit' do
+ it 'initializes a new release' do
+ tag_id = release.tag
+ project.releases.destroy_all
+
+ get :edit, namespace_id: project.namespace.path, project_id: project.path, tag_id: tag_id
+
+ release = assigns(:release)
+ expect(release).not_to be_nil
+ expect(release).not_to be_persisted
+ end
+
+ it 'retrieves an existing release' do
+ get :edit, namespace_id: project.namespace.path, project_id: project.path, tag_id: release.tag
+
+ release = assigns(:release)
+ expect(release).not_to be_nil
+ expect(release).to be_persisted
+ end
+ end
+
+ describe 'PUT #update' do
+ it 'updates release note description' do
+ update_release('description updated')
+
+ release = project.releases.find_by_tag(tag)
+ expect(release.description).to eq("description updated")
+ end
+
+ it 'deletes release note when description is null' do
+ expect { update_release('') }.to change(project.releases, :count).by(-1)
+ end
+ end
+
+ def update_release(description)
+ put :update,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ tag_id: release.tag,
+ release: { description: description }
+ end
+end
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
new file mode 100644
index 00000000000..b7bb9290712
--- /dev/null
+++ b/spec/controllers/search_controller_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe SearchController do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :public) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'finds issue comments' do
+ project = create(:empty_project, :public)
+ note = create(:note_on_issue, project: project)
+
+ get :show, project_id: project.id, scope: 'notes', search: note.note
+
+ expect(assigns[:search_objects].first).to eq note
+ end
+
+ context 'on restricted projects' do
+ context 'when signed out' do
+ before { sign_out(user) }
+
+ it "doesn't expose comments on issues" do
+ project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
+ note = create(:note_on_issue, project: project)
+
+ get :show, project_id: project.id, scope: 'notes', search: note.note
+
+ expect(assigns[:search_objects].count).to eq(0)
+ end
+ end
+
+ it "doesn't expose comments on issues" do
+ project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
+ note = create(:note_on_issue, project: project)
+
+ get :show, project_id: project.id, scope: 'notes', search: note.note
+
+ expect(assigns[:search_objects].count).to eq(0)
+ end
+
+ it "doesn't expose comments on merge_requests" do
+ project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE)
+ note = create(:note_on_merge_request, project: project)
+
+ get :show, project_id: project.id, scope: 'notes', search: note.note
+
+ expect(assigns[:search_objects].count).to eq(0)
+ end
+
+ it "doesn't expose comments on snippets" do
+ project = create(:empty_project, :public, snippets_access_level: ProjectFeature::PRIVATE)
+ note = create(:note_on_project_snippet, project: project)
+
+ get :show, project_id: project.id, scope: 'notes', search: note.note
+
+ expect(assigns[:search_objects].count).to eq(0)
+ end
+ end
+end
diff --git a/spec/db/production/settings.rb b/spec/db/production/settings.rb
new file mode 100644
index 00000000000..a7c5283df94
--- /dev/null
+++ b/spec/db/production/settings.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+require 'rainbow/ext/string'
+
+describe 'seed production settings', lib: true do
+ context 'GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN is set in the environment' do
+ before do
+ allow(ENV).to receive(:[]).and_call_original
+ allow(ENV).to receive(:[]).with('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN').and_return('013456789')
+ end
+
+ it 'writes the token to the database' do
+ load(File.join(__dir__, '../../../db/fixtures/production/010_settings.rb'))
+ expect(ApplicationSetting.current.runners_registration_token).to eq('013456789')
+ end
+ end
+end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index c443af09075..0397d5d4001 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -12,15 +12,17 @@ FactoryGirl.define do
started_at 'Di 29. Okt 09:51:28 CET 2013'
finished_at 'Di 29. Okt 09:53:28 CET 2013'
commands 'ls -a'
+
options do
{
image: "ruby:2.1",
services: ["postgres"]
}
end
+
yaml_variables do
[
- { key: :DB_NAME, value: 'postgres', public: true }
+ { key: 'DB_NAME', value: 'postgres', public: true }
]
end
@@ -60,15 +62,20 @@ FactoryGirl.define do
end
trait :teardown_environment do
- options do
- { environment: { action: 'stop' } }
- end
+ environment 'staging'
+ options environment: { name: 'staging',
+ action: 'stop' }
end
trait :allowed_to_fail do
allow_failure true
end
+ trait :playable do
+ skipped
+ manual
+ end
+
after(:build) do |build, evaluator|
build.project = build.pipeline.project
end
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index ebd3595ea64..ece6beb9fa9 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -19,5 +19,9 @@ FactoryGirl.define do
trait :access_requestable do
request_access_enabled true
end
+
+ trait :nested do
+ parent factory: :group
+ end
end
end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 6919002dedc..a10ba629760 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -67,7 +67,7 @@ FactoryGirl.define do
end
trait :on_project_snippet do
- noteable { create(:snippet, project: project) }
+ noteable { create(:project_snippet, project: project) }
end
trait :system do
diff --git a/spec/factories/personal_access_tokens.rb b/spec/factories/personal_access_tokens.rb
index da4c72bcb5b..811eab7e15b 100644
--- a/spec/factories/personal_access_tokens.rb
+++ b/spec/factories/personal_access_tokens.rb
@@ -5,5 +5,6 @@ FactoryGirl.define do
name { FFaker::Product.brand }
revoked false
expires_at { 5.days.from_now }
+ scopes ['api']
end
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 1166498ddff..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
@@ -133,4 +137,17 @@ FactoryGirl.define do
)
end
end
+
+ factory :kubernetes_project, parent: :empty_project do
+ after :create do |project|
+ project.create_kubernetes_service(
+ active: true,
+ properties: {
+ namespace: project.path,
+ api_url: 'https://kubernetes.example.com',
+ token: 'a' * 40,
+ }
+ )
+ end
+ end
end
diff --git a/spec/features/admin/admin_active_tab_spec.rb b/spec/features/admin/admin_active_tab_spec.rb
new file mode 100644
index 00000000000..16064d60ce2
--- /dev/null
+++ b/spec/features/admin/admin_active_tab_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+RSpec.describe 'admin active tab' do
+ before do
+ login_as :admin
+ end
+
+ shared_examples 'page has active tab' do |title|
+ it "activates #{title} tab" do
+ expect(page).to have_selector('.layout-nav .nav-links > li.active', count: 1)
+ expect(page.find('.layout-nav li.active')).to have_content(title)
+ end
+ end
+
+ shared_examples 'page has active sub tab' do |title|
+ it "activates #{title} sub tab" do
+ expect(page).to have_selector('.sub-nav li.active', count: 1)
+ expect(page.find('.sub-nav li.active')).to have_content(title)
+ end
+ end
+
+ context 'on home page' do
+ before do
+ visit admin_root_path
+ end
+
+ it_behaves_like 'page has active tab', 'Overview'
+ end
+
+ context 'on projects' do
+ before do
+ visit admin_projects_path
+ end
+
+ it_behaves_like 'page has active tab', 'Overview'
+ it_behaves_like 'page has active sub tab', 'Projects'
+ end
+
+ context 'on groups' do
+ before do
+ visit admin_groups_path
+ end
+
+ it_behaves_like 'page has active tab', 'Overview'
+ it_behaves_like 'page has active sub tab', 'Groups'
+ end
+
+ context 'on users' do
+ before do
+ visit admin_users_path
+ end
+
+ it_behaves_like 'page has active tab', 'Overview'
+ it_behaves_like 'page has active sub tab', 'Users'
+ end
+
+ context 'on logs' do
+ before do
+ visit admin_logs_path
+ end
+
+ it_behaves_like 'page has active tab', 'Monitoring'
+ it_behaves_like 'page has active sub tab', 'Logs'
+ end
+
+ context 'on messages' do
+ before do
+ visit admin_broadcast_messages_path
+ end
+
+ it_behaves_like 'page has active tab', 'Messages'
+ end
+
+ context 'on hooks' do
+ before do
+ visit admin_hooks_path
+ end
+
+ it_behaves_like 'page has active tab', 'Hooks'
+ end
+
+ context 'on background jobs' do
+ before do
+ visit admin_background_jobs_path
+ end
+
+ it_behaves_like 'page has active tab', 'Monitoring'
+ it_behaves_like 'page has active sub tab', 'Background Jobs'
+ end
+end
diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb
new file mode 100644
index 00000000000..96d715ef383
--- /dev/null
+++ b/spec/features/admin/admin_appearance_spec.rb
@@ -0,0 +1,76 @@
+require 'spec_helper'
+
+feature 'Admin Appearance', feature: true do
+ let!(:appearance) { create(:appearance) }
+
+ scenario 'Create new appearance' do
+ login_as :admin
+ visit admin_appearances_path
+
+ fill_in 'appearance_title', with: 'MyCompany'
+ fill_in 'appearance_description', with: 'dev server'
+ click_button 'Save'
+
+ expect(current_path).to eq admin_appearances_path
+ expect(page).to have_content 'Appearance settings'
+
+ expect(page).to have_field('appearance_title', with: 'MyCompany')
+ expect(page).to have_field('appearance_description', with: 'dev server')
+ expect(page).to have_content 'Last edit'
+ end
+
+ scenario 'Preview appearance' do
+ login_as :admin
+
+ visit admin_appearances_path
+ click_link "Preview"
+
+ expect_page_has_custom_appearance(appearance)
+ end
+
+ scenario 'Custom sign-in page' do
+ visit new_user_session_path
+ expect_page_has_custom_appearance(appearance)
+ end
+
+ scenario 'Appearance logo' do
+ login_as :admin
+ visit admin_appearances_path
+
+ attach_file(:appearance_logo, logo_fixture)
+ click_button 'Save'
+ expect(page).to have_css(logo_selector)
+
+ click_link 'Remove logo'
+ expect(page).not_to have_css(logo_selector)
+ end
+
+ scenario 'Header logos' do
+ login_as :admin
+ visit admin_appearances_path
+
+ attach_file(:appearance_header_logo, logo_fixture)
+ click_button 'Save'
+ expect(page).to have_css(header_logo_selector)
+
+ click_link 'Remove header logo'
+ expect(page).not_to have_css(header_logo_selector)
+ end
+
+ def expect_page_has_custom_appearance(appearance)
+ expect(page).to have_content appearance.title
+ expect(page).to have_content appearance.description
+ end
+
+ def logo_selector
+ '//img[@src^="/uploads/appearance/logo"]'
+ end
+
+ def header_logo_selector
+ '//img[@src^="/uploads/appearance/header_logo"]'
+ end
+
+ def logo_fixture
+ Rails.root.join('spec', 'fixtures', 'dk.png')
+ end
+end
diff --git a/spec/features/admin/admin_broadcast_messages_spec.rb b/spec/features/admin/admin_broadcast_messages_spec.rb
new file mode 100644
index 00000000000..bc957ec72e1
--- /dev/null
+++ b/spec/features/admin/admin_broadcast_messages_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+feature 'Admin Broadcast Messages', feature: true do
+ before do
+ login_as :admin
+ create(:broadcast_message, :expired, message: 'Migration to new server')
+ visit admin_broadcast_messages_path
+ end
+
+ scenario 'See broadcast messages list' do
+ expect(page).to have_content 'Migration to new server'
+ end
+
+ scenario 'Create a customized broadcast message' do
+ fill_in 'broadcast_message_message', with: 'Application update from **4:00 CST to 5:00 CST**'
+ fill_in 'broadcast_message_color', with: '#f2dede'
+ fill_in 'broadcast_message_font', with: '#b94a48'
+ select Date.today.next_year.year, from: 'broadcast_message_ends_at_1i'
+ click_button 'Add broadcast message'
+
+ expect(current_path).to eq admin_broadcast_messages_path
+ expect(page).to have_content 'Application update from 4:00 CST to 5:00 CST'
+ expect(page).to have_selector 'strong', text: '4:00 CST to 5:00 CST'
+ expect(page).to have_selector %(div[style="background-color: #f2dede; color: #b94a48"])
+ end
+
+ scenario 'Edit an existing broadcast message' do
+ click_link 'Edit'
+ fill_in 'broadcast_message_message', with: 'Application update RIGHT NOW'
+ click_button 'Update broadcast message'
+
+ expect(current_path).to eq admin_broadcast_messages_path
+ expect(page).to have_content 'Application update RIGHT NOW'
+ end
+
+ scenario 'Remove an existing broadcast message' do
+ click_link 'Remove'
+
+ expect(current_path).to eq admin_broadcast_messages_path
+ expect(page).not_to have_content 'Migration to new server'
+ end
+
+ scenario 'Live preview a customized broadcast message', js: true do
+ fill_in 'broadcast_message_message', with: "Live **Markdown** previews. :tada:"
+
+ page.within('.broadcast-message-preview') do
+ expect(page).to have_selector('strong', text: 'Markdown')
+ expect(page).to have_selector('img.emoji')
+ end
+ end
+end
diff --git a/spec/features/admin/admin_deploy_keys_spec.rb b/spec/features/admin/admin_deploy_keys_spec.rb
new file mode 100644
index 00000000000..8bf68480785
--- /dev/null
+++ b/spec/features/admin/admin_deploy_keys_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+RSpec.describe 'admin deploy keys', type: :feature do
+ let!(:deploy_key) { create(:deploy_key, public: true) }
+ let!(:another_deploy_key) { create(:another_deploy_key, public: true) }
+
+ before do
+ login_as(:admin)
+ end
+
+ it 'show all public deploy keys' do
+ visit admin_deploy_keys_path
+
+ expect(page).to have_content(deploy_key.title)
+ expect(page).to have_content(another_deploy_key.title)
+ end
+
+ it 'creates new deploy key' do
+ visit admin_deploy_keys_path
+
+ click_link 'New Deploy Key'
+ fill_in 'deploy_key_title', with: 'laptop'
+ fill_in 'deploy_key_key', with: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop'
+ click_button 'Create'
+
+ expect(current_path).to eq admin_deploy_keys_path
+ expect(page).to have_content('laptop')
+ end
+end
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index f6d625fa7f6..0aa01fc499a 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -21,7 +21,7 @@ feature 'Admin Groups', feature: true do
scenario 'shows the visibility level radio populated with the group visibility_level value' do
group = create(:group, :private)
- visit edit_admin_group_path(group)
+ visit admin_group_edit_path(group)
expect_selected_visibility(group.visibility_level)
end
diff --git a/spec/features/admin/admin_labels_spec.rb b/spec/features/admin/admin_labels_spec.rb
new file mode 100644
index 00000000000..eaa42aad0a7
--- /dev/null
+++ b/spec/features/admin/admin_labels_spec.rb
@@ -0,0 +1,99 @@
+require 'spec_helper'
+
+RSpec.describe 'admin issues labels' do
+ include WaitForAjax
+
+ let!(:bug_label) { Label.create(title: 'bug', template: true) }
+ let!(:feature_label) { Label.create(title: 'feature', template: true) }
+
+ before do
+ login_as :admin
+ end
+
+ describe 'list' do
+ before do
+ visit admin_labels_path
+ end
+
+ it 'renders labels list' do
+ page.within '.manage-labels-list' do
+ expect(page).to have_content('bug')
+ expect(page).to have_content('feature')
+ end
+ end
+
+ it 'deletes label' do
+ page.within "#label_#{bug_label.id}" do
+ click_link 'Delete'
+ end
+
+ page.within '.manage-labels-list' do
+ expect(page).not_to have_content('bug')
+ end
+ end
+
+ it 'deletes all labels', js: true do
+ page.within '.labels' do
+ page.all('.btn-remove').each do |remove|
+ wait_for_ajax
+ remove.click
+ end
+ end
+
+ page.within '.manage-labels-list' do
+ expect(page).not_to have_content('bug')
+ expect(page).not_to have_content('feature_label')
+ end
+ end
+ end
+
+ describe 'create' do
+ before do
+ visit new_admin_label_path
+ end
+
+ it 'creates new label' do
+ fill_in 'Title', with: 'support'
+ fill_in 'Background color', with: '#F95610'
+ click_button 'Save'
+
+ page.within '.manage-labels-list' do
+ expect(page).to have_content('support')
+ end
+ end
+
+ it 'does not creates label with invalid color' do
+ fill_in 'Title', with: 'support'
+ fill_in 'Background color', with: '#12'
+ click_button 'Save'
+
+ page.within '.label-form' do
+ expect(page).to have_content('Color must be a valid color code')
+ end
+ end
+
+ it 'does not creates label if label already exists' do
+ fill_in 'Title', with: 'bug'
+ fill_in 'Background color', with: '#F95610'
+ click_button 'Save'
+
+ page.within '.label-form' do
+ expect(page).to have_content 'Title has already been taken'
+ end
+ end
+ end
+
+ describe 'edit' do
+ it 'changes bug label' do
+ visit edit_admin_label_path(bug_label)
+
+ fill_in 'Title', with: 'fix'
+ fill_in 'Background color', with: '#F15610'
+ click_button 'Save'
+
+ page.within '.manage-labels-list' do
+ expect(page).to have_content('fix')
+ end
+ end
+ end
+end
diff --git a/spec/features/admin/admin_manage_applications_spec.rb b/spec/features/admin/admin_manage_applications_spec.rb
new file mode 100644
index 00000000000..c2c618b5659
--- /dev/null
+++ b/spec/features/admin/admin_manage_applications_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+RSpec.describe 'admin manage applications', feature: true do
+ before do
+ login_as :admin
+ end
+
+ it do
+ visit admin_applications_path
+
+ click_on 'New Application'
+ expect(page).to have_content('New application')
+
+ fill_in :doorkeeper_application_name, with: 'test'
+ fill_in :doorkeeper_application_redirect_uri, with: 'https://test.com'
+ click_on 'Submit'
+ expect(page).to have_content('Application: test')
+ expect(page).to have_content('Application Id')
+ expect(page).to have_content('Secret')
+
+ click_on 'Edit'
+ expect(page).to have_content('Edit application')
+
+ fill_in :doorkeeper_application_name, with: 'test_changed'
+ click_on 'Submit'
+ expect(page).to have_content('test_changed')
+ expect(page).to have_content('Application Id')
+ expect(page).to have_content('Secret')
+
+ visit admin_applications_path
+ page.within '.oauth-applications' do
+ click_on 'Destroy'
+ end
+ expect(page.find('.oauth-applications')).not_to have_content('test_changed')
+ end
+end
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index 30ded9202a4..a5b88812b75 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -1,34 +1,117 @@
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_namespaces_projects_path
+ visit admin_projects_path
end
it "is ok" do
- expect(current_path).to eq(admin_namespaces_projects_path)
+ 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_namespaces_projects_path
- click_link "#{@project.name}"
+ visit admin_projects_path
+ 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/features/steps/admin/settings.rb b/spec/features/admin/admin_settings_spec.rb
index 11dc7f580f0..47fa2f14307 100644
--- a/features/steps/admin/settings.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -1,62 +1,53 @@
-class Spinach::Features::AdminSettings < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedAdmin
- include Gitlab::CurrentSettings
+require 'spec_helper'
- step 'I modify settings and save form' do
+feature 'Admin updates settings', feature: true do
+ before(:each) do
+ login_as :admin
+ visit admin_application_settings_path
+ end
+
+ scenario 'Change application settings' do
uncheck 'Gravatar enabled'
fill_in 'Home page URL', with: 'https://about.gitlab.com/'
fill_in 'Help page text', with: 'Example text'
click_button 'Save'
- end
- step 'I should see application settings saved' do
expect(current_application_settings.gravatar_enabled).to be_falsey
expect(current_application_settings.home_page_url).to eq "https://about.gitlab.com/"
expect(page).to have_content "Application settings saved successfully"
end
- step 'I click on "Service Templates"' do
+ scenario 'Change Slack Notifications Service template settings' do
click_link 'Service Templates'
- end
-
- step 'I click on "Slack" service' do
- click_link 'Slack'
- end
-
- step 'I check all events and submit form' do
- page.check('Active')
- page.check('Push')
- page.check('Tag push')
- page.check('Note')
- page.check('Issue')
- page.check('Merge request')
- page.check('Build')
- page.check('Pipeline')
- click_on 'Save'
- end
-
- step 'I fill out Slack settings' do
+ click_link 'Slack notifications'
fill_in 'Webhook', with: 'http://localhost'
fill_in 'Username', with: 'test_user'
fill_in 'service_push_channel', with: '#test_channel'
page.check('Notify only broken builds')
- end
- step 'I should see service template settings saved' do
+ check_all_events
+ click_on 'Save'
+
expect(page).to have_content 'Application settings saved successfully'
- end
- step 'I should see all checkboxes checked' do
+ click_link 'Slack notifications'
+
page.all('input[type=checkbox]').each do |checkbox|
expect(checkbox).to be_checked
end
- end
-
- step 'I should see Slack settings saved' do
expect(find_field('Webhook').value).to eq 'http://localhost'
expect(find_field('Username').value).to eq 'test_user'
expect(find('#service_push_channel').value).to eq '#test_channel'
end
+
+ def check_all_events
+ page.check('Active')
+ page.check('Push')
+ page.check('Tag push')
+ page.check('Note')
+ page.check('Issue')
+ page.check('Merge request')
+ page.check('Build')
+ page.check('Pipeline')
+ end
end
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 23a504ff965..8f561c8f90b 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -107,7 +107,7 @@ describe 'Commits' do
describe 'Cancel build' do
it 'cancels build' do
visit ci_status_path(pipeline)
- click_on 'Cancel'
+ find('a.btn[title="Cancel"]').click
expect(page).to have_content 'canceled'
end
end
diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb
index 365cb445df1..44dfc2dff45 100644
--- a/spec/features/dashboard/datetime_on_tooltips_spec.rb
+++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb
@@ -36,7 +36,7 @@ feature 'Tooltips on .timeago dates', feature: true, js: true do
visit user_snippets_path(user)
wait_for_ajax()
- page.find('.js-timeago').hover
+ page.find('.js-timeago.snippet-created-ago').hover
end
it 'has the datetime formated correctly' do
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/groups/members/sorting_spec.rb b/spec/features/groups/members/sorting_spec.rb
new file mode 100644
index 00000000000..608aedd3471
--- /dev/null
+++ b/spec/features/groups/members/sorting_spec.rb
@@ -0,0 +1,98 @@
+require 'spec_helper'
+
+feature 'Groups > Members > Sorting', feature: true do
+ let(:owner) { create(:user, name: 'John Doe') }
+ let(:developer) { create(:user, name: 'Mary Jane', last_sign_in_at: 5.days.ago) }
+ let(:group) { create(:group) }
+
+ background do
+ create(:group_member, :owner, user: owner, group: group, created_at: 5.days.ago)
+ create(:group_member, :developer, user: developer, group: group, created_at: 3.days.ago)
+
+ login_as(owner)
+ end
+
+ scenario 'sorts alphabetically by default' do
+ visit_members_list(sort: nil)
+
+ expect(first_member).to include(owner.name)
+ expect(second_member).to include(developer.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
+ end
+
+ scenario 'sorts by access level ascending' do
+ visit_members_list(sort: :access_level_asc)
+
+ expect(first_member).to include(developer.name)
+ expect(second_member).to include(owner.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending')
+ end
+
+ scenario 'sorts by access level descending' do
+ visit_members_list(sort: :access_level_desc)
+
+ expect(first_member).to include(owner.name)
+ expect(second_member).to include(developer.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending')
+ end
+
+ scenario 'sorts by last joined' do
+ visit_members_list(sort: :last_joined)
+
+ expect(first_member).to include(developer.name)
+ expect(second_member).to include(owner.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Last joined')
+ end
+
+ scenario 'sorts by oldest joined' do
+ visit_members_list(sort: :oldest_joined)
+
+ expect(first_member).to include(owner.name)
+ expect(second_member).to include(developer.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined')
+ end
+
+ scenario 'sorts by name ascending' do
+ visit_members_list(sort: :name_asc)
+
+ expect(first_member).to include(owner.name)
+ expect(second_member).to include(developer.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
+ end
+
+ scenario 'sorts by name descending' do
+ visit_members_list(sort: :name_desc)
+
+ expect(first_member).to include(developer.name)
+ expect(second_member).to include(owner.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, descending')
+ end
+
+ scenario 'sorts by recent sign in' do
+ visit_members_list(sort: :recent_sign_in)
+
+ expect(first_member).to include(owner.name)
+ expect(second_member).to include(developer.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in')
+ end
+
+ scenario 'sorts by oldest sign in' do
+ visit_members_list(sort: :oldest_sign_in)
+
+ expect(first_member).to include(developer.name)
+ expect(second_member).to include(owner.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in')
+ end
+
+ def visit_members_list(sort:)
+ visit group_group_members_path(group.to_param, sort: sort)
+ end
+
+ def first_member
+ page.all('ul.content-list > li').first.text
+ end
+
+ def second_member
+ page.all('ul.content-list > li').last.text
+ end
+end
diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb
index bc2c087c9b9..832757b24d4 100644
--- a/spec/features/issues/bulk_assignment_labels_spec.rb
+++ b/spec/features/issues/bulk_assignment_labels_spec.rb
@@ -9,6 +9,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
let!(:issue2) { create(:issue, project: project, title: "Issue 2") }
let!(:bug) { create(:label, project: project, title: 'bug') }
let!(:feature) { create(:label, project: project, title: 'feature') }
+ let!(:wontfix) { create(:label, project: project, title: 'wontfix') }
context 'as an allowed user', js: true do
before do
@@ -291,6 +292,45 @@ feature 'Issues > Labels bulk assignment', feature: true do
expect(find("#issue_#{issue1.id}")).not_to have_content 'feature'
end
end
+
+ # Special case https://gitlab.com/gitlab-org/gitlab-ce/issues/24877
+ context 'unmarking common label' do
+ before do
+ issue1.labels << bug
+ issue1.labels << feature
+ issue2.labels << bug
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ it 'applies label from filtered results' do
+ check 'check_all_issues'
+
+ page.within('.issues_bulk_update') do
+ click_button 'Labels'
+ wait_for_ajax
+
+ expect(find('.dropdown-menu-labels li', text: 'bug')).to have_css('.is-active')
+ expect(find('.dropdown-menu-labels li', text: 'feature')).to have_css('.is-indeterminate')
+
+ click_link 'bug'
+ find('.dropdown-input-field', visible: true).set('wontfix')
+ click_link 'wontfix'
+ end
+
+ update_issues
+
+ page.within '.issues-holder' do
+ expect(find("#issue_#{issue1.id}")).not_to have_content 'bug'
+ expect(find("#issue_#{issue1.id}")).to have_content 'feature'
+ expect(find("#issue_#{issue1.id}")).to have_content 'wontfix'
+
+ expect(find("#issue_#{issue2.id}")).not_to have_content 'bug'
+ expect(find("#issue_#{issue2.id}")).not_to have_content 'feature'
+ expect(find("#issue_#{issue2.id}")).to have_content 'wontfix'
+ end
+ end
+ end
end
context 'as a guest' do
@@ -320,7 +360,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
def open_labels_dropdown(items = [], unmark = false)
page.within('.issues_bulk_update') do
- click_button 'Label'
+ click_button 'Labels'
wait_for_ajax
items.map do |item|
click_link item
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index c421da97d76..da64827b377 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -2,8 +2,9 @@ require 'rails_helper'
feature 'GFM autocomplete', feature: true, js: true do
include WaitForAjax
- let(:user) { create(:user) }
+ let(:user) { create(:user, username: 'someone.special') }
let(:project) { create(:project) }
+ let(:label) { create(:label, project: project, title: 'special+') }
let(:issue) { create(:issue, project: project) }
before do
@@ -23,19 +24,75 @@ feature 'GFM autocomplete', feature: true, js: true do
expect(page).to have_selector('.atwho-container')
end
- it 'opens autocomplete menu when field is prefixed with non-text character' do
+ it 'doesnt open autocomplete menu character is prefixed with text' do
page.within '.timeline-content-form' do
- find('#note_note').native.send_keys('')
+ find('#note_note').native.send_keys('testing')
find('#note_note').native.send_keys('@')
end
- expect(page).to have_selector('.atwho-container')
+ expect(page).not_to have_selector('.atwho-view')
end
- it 'doesnt open autocomplete menu character is prefixed with text' do
+ context 'if a selected value has special characters' do
+ it 'wraps the result in double quotes' do
+ note = find('#note_note')
+ page.within '.timeline-content-form' do
+ note.native.send_keys('')
+ note.native.send_keys("~#{label.title[0]}")
+ sleep 1
+ note.click
+ end
+
+ label_item = find('.atwho-view li', text: label.title)
+
+ expect_to_wrap(true, label_item, note, label.title)
+ end
+
+ it 'doesn\'t wrap for assignee values' do
+ note = find('#note_note')
+ page.within '.timeline-content-form' do
+ note.native.send_keys('')
+ note.native.send_keys("@#{user.username[0]}")
+ sleep 1
+ note.click
+ end
+
+ user_item = find('.atwho-view li', text: user.username)
+
+ expect_to_wrap(false, user_item, note, user.username)
+ end
+
+ it 'doesn\'t wrap for emoji values' do
+ note = find('#note_note')
+ page.within '.timeline-content-form' do
+ note.native.send_keys('')
+ note.native.send_keys(":cartwheel")
+ sleep 1
+ note.click
+ end
+
+ emoji_item = find('.atwho-view li', text: 'cartwheel_tone1')
+
+ expect_to_wrap(false, emoji_item, note, 'cartwheel_tone1')
+ end
+
+ def expect_to_wrap(should_wrap, item, note, value)
+ expect(item).to have_content(value)
+ expect(item).not_to have_content("\"#{value}\"")
+
+ item.click
+
+ if should_wrap
+ expect(note.value).to include("\"#{value}\"")
+ else
+ expect(note.value).not_to include("\"#{value}\"")
+ end
+ end
+ end
+
+ it 'doesnt open autocomplete after non-word character' do
page.within '.timeline-content-form' do
- find('#note_note').native.send_keys('testing')
- find('#note_note').native.send_keys('@')
+ find('#note_note').native.send_keys("@#{user.username[0..2]}!")
end
expect(page).not_to have_selector('.atwho-view')
diff --git a/spec/features/merge_requests/closes_issues_spec.rb b/spec/features/merge_requests/closes_issues_spec.rb
new file mode 100644
index 00000000000..dc32c8f7373
--- /dev/null
+++ b/spec/features/merge_requests/closes_issues_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+feature 'Merge Request closing issues message', feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:issue_1) { create(:issue, project: project)}
+ let(:issue_2) { create(:issue, project: project)}
+ let(:merge_request) do
+ create(
+ :merge_request,
+ :simple,
+ source_project: project,
+ description: merge_request_description
+ )
+ end
+ let(:merge_request_description) { 'Merge Request Description' }
+
+ before do
+ project.team << [user, :master]
+
+ login_as user
+
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ context 'not closing or mentioning any issue' do
+ it 'does not display closing issue message' do
+ expect(page).not_to have_css('.mr-widget-footer')
+ end
+ end
+
+ context 'closing issues but not mentioning any other issue' do
+ let(:merge_request_description) { "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}" }
+
+ it 'does not display closing issue message' do
+ expect(page).to have_content("Accepting this merge request will close issues #{issue_1.to_reference} and #{issue_2.to_reference}")
+ end
+ end
+
+ context 'mentioning issues but not closing them' do
+ let(:merge_request_description) { "Description\n\nRefers to #{issue_1.to_reference} and #{issue_2.to_reference}" }
+
+ it 'does not display closing issue message' do
+ expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but will not closed.")
+ end
+ end
+
+ context 'closing some issues and mentioning, but not closing, others' do
+ let(:merge_request_description) { "Description\n\ncloses #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" }
+
+ it 'does not display closing issue message' do
+ expect(page).to have_content("Accepting this merge request will close issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not closed.")
+ end
+ end
+end
diff --git a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
new file mode 100644
index 00000000000..3dbe26cddb0
--- /dev/null
+++ b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+feature 'Clicking toggle commit message link', feature: true, js: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:issue_1) { create(:issue, project: project)}
+ let(:issue_2) { create(:issue, project: project)}
+ let(:merge_request) do
+ create(
+ :merge_request,
+ :simple,
+ source_project: project,
+ description: "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}"
+ )
+ end
+ let(:textbox) { page.find(:css, '.js-commit-message', visible: false) }
+ let(:include_link) { page.find(:css, '.js-with-description-link', visible: false) }
+ let(:do_not_include_link) { page.find(:css, '.js-without-description-link', visible: false) }
+ let(:default_message) do
+ [
+ "Merge branch 'feature' into 'master'",
+ merge_request.title,
+ "Closes #{issue_1.to_reference} and #{issue_2.to_reference}",
+ "See merge request #{merge_request.to_reference}"
+ ].join("\n\n")
+ end
+ let(:message_with_description) do
+ [
+ "Merge branch 'feature' into 'master'",
+ merge_request.title,
+ merge_request.description,
+ "See merge request #{merge_request.to_reference}"
+ ].join("\n\n")
+ end
+
+ before do
+ project.team << [user, :master]
+
+ login_as user
+
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+
+ expect(textbox).not_to be_visible
+ click_link "Modify commit message"
+ expect(textbox).to be_visible
+ end
+
+ it "toggles commit message between message with description and without description " do
+ expect(textbox.value).to eq(default_message)
+
+ click_link "Include description in commit message"
+
+ expect(textbox.value).to eq(message_with_description)
+
+ click_link "Don't include description in commit message"
+
+ expect(textbox.value).to eq(default_message)
+ end
+
+ it "toggles link between 'Include description' and 'Don't include description'" do
+ expect(include_link).to be_visible
+ expect(do_not_include_link).not_to be_visible
+
+ click_link "Include description in commit message"
+
+ expect(include_link).not_to be_visible
+ expect(do_not_include_link).to be_visible
+
+ click_link "Don't include description in commit message"
+
+ expect(include_link).to be_visible
+ expect(do_not_include_link).not_to be_visible
+ end
+end
diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb
index a78a1c9c890..c2545b0c259 100644
--- a/spec/features/participants_autocomplete_spec.rb
+++ b/spec/features/participants_autocomplete_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
feature 'Member autocomplete', feature: true do
+ include WaitForAjax
+
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
let(:participant) { create(:user) }
@@ -79,11 +81,10 @@ feature 'Member autocomplete', feature: true do
end
def open_member_suggestions
- sleep 1
page.within('.new-note') do
- sleep 1
- find('#note_note').native.send_keys('@')
+ find('#note_note').send_keys('@')
end
+ wait_for_ajax
end
def visit_issue(project, issue)
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
index a85930c7543..55a01057c83 100644
--- a/spec/features/profiles/personal_access_tokens_spec.rb
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -27,28 +27,25 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do
describe "token creation" do
it "allows creation of a token" do
- visit profile_personal_access_tokens_path
- fill_in "Name", with: FFaker::Product.brand
-
- expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1)
- expect(created_personal_access_token).to eq(PersonalAccessToken.last.token)
- expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.name)
- expect(active_personal_access_tokens).to have_text("Never")
- end
+ name = FFaker::Product.brand
- it "allows creation of a token with an expiry date" do
visit profile_personal_access_tokens_path
- fill_in "Name", with: FFaker::Product.brand
+ fill_in "Name", with: name
# Set date to 1st of next month
find_field("Expires at").trigger('focus')
find("a[title='Next']").click
click_on "1"
- expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1)
- expect(created_personal_access_token).to eq(PersonalAccessToken.last.token)
- expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.name)
+ # Scopes
+ check "api"
+ check "read_user"
+
+ click_on "Create Personal Access Token"
+ expect(active_personal_access_tokens).to have_text(name)
expect(active_personal_access_tokens).to have_text(Date.today.next_month.at_beginning_of_month.to_s(:medium))
+ expect(active_personal_access_tokens).to have_text('api')
+ expect(active_personal_access_tokens).to have_text('read_user')
end
context "when creation fails" do
@@ -85,7 +82,7 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do
disallow_personal_access_token_saves!
visit profile_personal_access_tokens_path
- expect { click_on "Revoke" }.not_to change { PersonalAccessToken.inactive.count }
+ click_on "Revoke"
expect(active_personal_access_tokens).to have_text(personal_access_token.name)
expect(page).to have_content("Could not revoke")
end
diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb
index a0ccc472d11..8c4d4320dc5 100644
--- a/spec/features/projects/builds_spec.rb
+++ b/spec/features/projects/builds_spec.rb
@@ -227,6 +227,43 @@ feature 'Builds', :feature do
expect(page).to have_selector('.js-build-value', text: 'TRIGGER_VALUE_1')
end
end
+
+ context 'when build starts environment' do
+ let(:environment) { create(:environment, project: project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ context 'build is successfull and has deployment' do
+ let(:deployment) { create(:deployment) }
+ let(:build) { create(:ci_build, :success, environment: environment.name, deployments: [deployment], pipeline: pipeline) }
+
+ it 'shows a link for the build' do
+ visit namespace_project_build_path(project.namespace, project, build)
+
+ expect(page).to have_link environment.name
+ end
+ end
+
+ context 'build is complete and not successfull' do
+ let(:build) { create(:ci_build, :failed, environment: environment.name, pipeline: pipeline) }
+
+ it 'shows a link for the build' do
+ visit namespace_project_build_path(project.namespace, project, build)
+
+ expect(page).to have_link environment.name
+ end
+ end
+
+ context 'build creates a new deployment' do
+ let!(:deployment) { create(:deployment, environment: environment, sha: project.commit.id) }
+ let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) }
+
+ it 'shows a link to lastest deployment' do
+ visit namespace_project_build_path(project.namespace, project, build)
+
+ expect(page).to have_link('latest deployment')
+ end
+ end
+ end
end
describe "POST /:project/builds/:id/cancel" do
diff --git a/spec/features/projects/files/creating_a_file_spec.rb b/spec/features/projects/files/creating_a_file_spec.rb
new file mode 100644
index 00000000000..ae448706130
--- /dev/null
+++ b/spec/features/projects/files/creating_a_file_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+feature 'User wants to create a file', feature: true do
+ include WaitForAjax
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ background do
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_new_blob_path(project.namespace, project, project.default_branch)
+ end
+
+ def submit_new_file(options)
+ file_name = find('#file_name')
+ file_name.set options[:file_name] || 'README.md'
+
+ file_content = find('#file-content')
+ file_content.set options[:file_content] || 'Some content'
+
+ click_button 'Commit Changes'
+ end
+
+ scenario 'file name contains Chinese characters' do
+ submit_new_file(file_name: '测试.md')
+ expect(page).to have_content 'The file has been successfully created.'
+ end
+
+ scenario 'directory name contains Chinese characters' do
+ submit_new_file(file_name: '中文/测试.md')
+ expect(page).to have_content 'The file has been successfully created.'
+ end
+
+ scenario 'file name contains invalid characters' do
+ submit_new_file(file_name: '\\')
+ expect(page).to have_content 'Your changes could not be committed, because the file name can contain only'
+ end
+
+ scenario 'file name contains directory traversal' do
+ submit_new_file(file_name: '../README.md')
+ expect(page).to have_content 'Your changes could not be committed, because the file name cannot include directory traversal.'
+ end
+end
diff --git a/spec/features/projects/files/dockerfile_dropdown_spec.rb b/spec/features/projects/files/dockerfile_dropdown_spec.rb
new file mode 100644
index 00000000000..32f33a3ca97
--- /dev/null
+++ b/spec/features/projects/files/dockerfile_dropdown_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+feature 'User wants to add a Dockerfile file', feature: true do
+ include WaitForAjax
+
+ before do
+ user = create(:user)
+ project = create(:project)
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: 'Dockerfile')
+ end
+
+ scenario 'user can see Dockerfile dropdown' do
+ expect(page).to have_css('.dockerfile-selector')
+ end
+
+ scenario 'user can pick a Dockerfile file from the dropdown', js: true do
+ find('.js-dockerfile-selector').click
+ wait_for_ajax
+ within '.dockerfile-selector' do
+ find('.dropdown-input-field').set('HTTPd')
+ find('.dropdown-content li', text: 'HTTPd').click
+ end
+ wait_for_ajax
+
+ expect(page).to have_css('.dockerfile-selector .dropdown-toggle-text', text: 'HTTPd')
+ expect(page).to have_content('COPY ./ /usr/local/apache2/htdocs/')
+ end
+end
diff --git a/spec/features/projects/gfm_autocomplete_load_spec.rb b/spec/features/projects/gfm_autocomplete_load_spec.rb
index 1921ea6d8ae..dd9622f16a0 100644
--- a/spec/features/projects/gfm_autocomplete_load_spec.rb
+++ b/spec/features/projects/gfm_autocomplete_load_spec.rb
@@ -10,12 +10,12 @@ describe 'GFM autocomplete loading', feature: true, js: true do
end
it 'does not load on project#show' do
- expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).to eq('')
+ expect(evaluate_script('gl.GfmAutoComplete.dataSources')).to eq({})
end
it 'loads on new issue page' do
visit new_namespace_project_issue_path(project.namespace, project)
- expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).not_to eq('')
+ expect(evaluate_script('gl.GfmAutoComplete.dataSources')).not_to eq({})
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 bfe59bdb90e..d3165d07d7b 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/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb
new file mode 100644
index 00000000000..d6ebb523f95
--- /dev/null
+++ b/spec/features/projects/members/sorting_spec.rb
@@ -0,0 +1,98 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Sorting', feature: true do
+ let(:master) { create(:user, name: 'John Doe') }
+ let(:developer) { create(:user, name: 'Mary Jane', last_sign_in_at: 5.days.ago) }
+ let(:project) { create(:empty_project) }
+
+ background do
+ create(:project_member, :master, user: master, project: project, created_at: 5.days.ago)
+ create(:project_member, :developer, user: developer, project: project, created_at: 3.days.ago)
+
+ login_as(master)
+ end
+
+ scenario 'sorts alphabetically by default' do
+ visit_members_list(sort: nil)
+
+ expect(first_member).to include(master.name)
+ expect(second_member).to include(developer.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
+ end
+
+ scenario 'sorts by access level ascending' do
+ visit_members_list(sort: :access_level_asc)
+
+ expect(first_member).to include(developer.name)
+ expect(second_member).to include(master.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending')
+ end
+
+ scenario 'sorts by access level descending' do
+ visit_members_list(sort: :access_level_desc)
+
+ expect(first_member).to include(master.name)
+ expect(second_member).to include(developer.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending')
+ end
+
+ scenario 'sorts by last joined' do
+ visit_members_list(sort: :last_joined)
+
+ expect(first_member).to include(developer.name)
+ expect(second_member).to include(master.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Last joined')
+ end
+
+ scenario 'sorts by oldest joined' do
+ visit_members_list(sort: :oldest_joined)
+
+ expect(first_member).to include(master.name)
+ expect(second_member).to include(developer.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined')
+ end
+
+ scenario 'sorts by name ascending' do
+ visit_members_list(sort: :name_asc)
+
+ expect(first_member).to include(master.name)
+ expect(second_member).to include(developer.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
+ end
+
+ scenario 'sorts by name descending' do
+ visit_members_list(sort: :name_desc)
+
+ expect(first_member).to include(developer.name)
+ expect(second_member).to include(master.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, descending')
+ end
+
+ scenario 'sorts by recent sign in' do
+ visit_members_list(sort: :recent_sign_in)
+
+ expect(first_member).to include(master.name)
+ expect(second_member).to include(developer.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in')
+ end
+
+ scenario 'sorts by oldest sign in' do
+ visit_members_list(sort: :oldest_sign_in)
+
+ expect(first_member).to include(developer.name)
+ expect(second_member).to include(master.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in')
+ end
+
+ def visit_members_list(sort:)
+ visit namespace_project_project_members_path(project.namespace.to_param, project.to_param, sort: sort)
+ end
+
+ def first_member
+ page.all('ul.content-list > li').first.text
+ end
+
+ def second_member
+ page.all('ul.content-list > li').last.text
+ end
+end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 3350a3aeefc..1210e2745db 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -19,7 +19,7 @@ describe "Pipelines", feature: true, js: true do
@success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build')
@failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test')
@running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy')
- @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual build')
+ @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual-build')
@external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external')
end
@@ -38,6 +38,88 @@ describe "Pipelines", feature: true, js: true do
expect(page).to have_css('#js-tab-pipeline.active')
end
+ describe 'pipeline graph' do
+ context 'when pipeline has running builds' do
+ it 'shows a running icon and a cancel action for the running build' do
+ page.within('#ci-badge-deploy') do
+ expect(page).to have_selector('.ci-status-icon-running')
+ expect(page).to have_selector('.ci-action-icon-container .fa-ban')
+ expect(page).to have_content('deploy')
+ end
+ end
+
+ it 'should be possible to cancel the running build' do
+ find('#ci-badge-deploy .ci-action-icon-container').trigger('click')
+
+ expect(page).not_to have_content('Cancel running')
+ end
+ end
+
+ context 'when pipeline has successful builds' do
+ it 'shows the success icon and a retry action for the successful build' do
+ page.within('#ci-badge-build') do
+ expect(page).to have_selector('.ci-status-icon-success')
+ expect(page).to have_content('build')
+ end
+
+ page.within('#ci-badge-build .ci-action-icon-container') do
+ expect(page).to have_selector('.ci-action-icon-container .fa-refresh')
+ end
+ end
+
+ it 'should be possible to retry the success build' do
+ find('#ci-badge-build .ci-action-icon-container').trigger('click')
+
+ expect(page).not_to have_content('Retry build')
+ end
+ end
+
+ context 'when pipeline has failed builds' do
+ it 'shows the failed icon and a retry action for the failed build' do
+ page.within('#ci-badge-test') do
+ expect(page).to have_selector('.ci-status-icon-failed')
+ expect(page).to have_content('test')
+ end
+
+ page.within('#ci-badge-test .ci-action-icon-container') do
+ expect(page).to have_selector('.ci-action-icon-container .fa-refresh')
+ end
+ end
+
+ it 'should be possible to retry the failed build' do
+ find('#ci-badge-test .ci-action-icon-container').trigger('click')
+
+ expect(page).not_to have_content('Retry build')
+ end
+ end
+
+ context 'when pipeline has manual builds' do
+ it 'shows the skipped icon and a play action for the manual build' do
+ page.within('#ci-badge-manual-build') do
+ expect(page).to have_selector('.ci-status-icon-manual')
+ expect(page).to have_content('manual')
+ end
+
+ page.within('#ci-badge-manual-build .ci-action-icon-container') do
+ expect(page).to have_selector('.ci-action-icon-container .fa-play')
+ end
+ end
+
+ it 'should be possible to play the manual build' do
+ find('#ci-badge-manual-build .ci-action-icon-container').trigger('click')
+
+ expect(page).not_to have_content('Play build')
+ end
+ end
+
+ context 'when pipeline has external build' do
+ it 'shows the success icon and the generic comit status build' do
+ expect(page).to have_selector('.ci-status-icon-success')
+ expect(page).to have_content('jenkins')
+ end
+ end
+ end
+
context 'page tabs' do
it 'shows Pipeline and Builds tabs with link' do
expect(page).to have_link('Pipeline')
@@ -82,7 +164,7 @@ describe "Pipelines", feature: true, js: true do
@success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build')
@failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test')
@running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy')
- @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual build')
+ @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual-build')
@external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external')
end
diff --git a/spec/features/projects/services/slack_service_spec.rb b/spec/features/projects/services/slack_service_spec.rb
index 16541f51d98..320ed13a01d 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) { SlackService.new }
- let(:project) { create(:project, slack_service: service) }
+ let(:service) { SlackNotificationService.new }
+ let(:project) { create(:project, slack_notification_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/features/security/admin_access_spec.rb b/spec/features/security/admin_access_spec.rb
index fe8cd7b7602..e180ca53eb5 100644
--- a/spec/features/security/admin_access_spec.rb
+++ b/spec/features/security/admin_access_spec.rb
@@ -4,7 +4,7 @@ describe "Admin::Projects", feature: true do
include AccessMatchers
describe "GET /admin/projects" do
- subject { admin_namespaces_projects_path }
+ subject { admin_projects_path }
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for :user }
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 7f69e888f32..97737d7ddc7 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -10,24 +10,24 @@ describe IssuesFinder do
let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone, title: 'gitlab') }
let(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'gitlab') }
let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2) }
- let(:closed_issue) { create(:issue, author: user2, assignee: user2, project: project2, state: 'closed') }
- let!(:label_link) { create(:label_link, label: label, target: issue2) }
-
- before do
- project1.team << [user, :master]
- project2.team << [user, :developer]
- project2.team << [user2, :developer]
-
- issue1
- issue2
- issue3
- end
describe '#execute' do
+ let(:closed_issue) { create(:issue, author: user2, assignee: user2, project: project2, state: 'closed') }
+ let!(:label_link) { create(:label_link, label: label, target: issue2) }
let(:search_user) { user }
let(:params) { {} }
let(:issues) { IssuesFinder.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute }
+ before do
+ project1.team << [user, :master]
+ project2.team << [user, :developer]
+ project2.team << [user2, :developer]
+
+ issue1
+ issue2
+ issue3
+ end
+
context 'scope: all' do
let(:scope) { 'all' }
@@ -193,6 +193,15 @@ describe IssuesFinder do
expect(issues).to contain_exactly(issue2, issue3)
end
end
+
+ it 'finds issues user can access due to group' do
+ group = create(:group)
+ project = create(:empty_project, group: group)
+ issue = create(:issue, project: project)
+ group.add_user(user, :owner)
+
+ expect(issues).to include(issue)
+ end
end
context 'personal scope' do
@@ -210,5 +219,43 @@ describe IssuesFinder do
end
end
end
+
+ context 'when project restricts issues' do
+ let(:scope) { nil }
+
+ it "doesn't return team-only issues to non team members" do
+ project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
+ issue = create(:issue, project: project)
+
+ expect(issues).not_to include(issue)
+ end
+
+ it "doesn't return issues if feature disabled" do
+ [project1, project2].each do |project|
+ project.project_feature.update!(issues_access_level: ProjectFeature::DISABLED)
+ end
+
+ expect(issues.count).to eq 0
+ end
+ end
+ end
+
+ describe '.not_restricted_by_confidentiality' do
+ let(:authorized_user) { create(:user) }
+ let(:project) { create(:empty_project, namespace: authorized_user.namespace) }
+ let!(:public_issue) { create(:issue, project: project) }
+ let!(:confidential_issue) { create(:issue, project: project, confidential: true) }
+
+ it 'returns non confidential issues for nil user' do
+ expect(IssuesFinder.send(:not_restricted_by_confidentiality, nil)).to include(public_issue)
+ end
+
+ it 'returns non confidential issues for user not authorized for the issues projects' do
+ expect(IssuesFinder.send(:not_restricted_by_confidentiality, user)).to include(public_issue)
+ end
+
+ it 'returns all issues for user authorized for the issues projects' do
+ expect(IssuesFinder.send(:not_restricted_by_confidentiality, authorized_user)).to include(public_issue, confidential_issue)
+ end
end
end
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index 7c6860372cc..4d21254323c 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -2,59 +2,203 @@ require 'spec_helper'
describe NotesFinder do
let(:user) { create :user }
- let(:project) { create :project }
- let(:note1) { create :note_on_commit, project: project }
- let(:note2) { create :note_on_commit, project: project }
- let(:commit) { note1.noteable }
+ let(:project) { create(:empty_project) }
before do
project.team << [user, :master]
end
describe '#execute' do
- let(:params) { { target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago.to_i } }
+ it 'finds notes on snippets when project is public and user isnt a member'
- before do
- note1
- note2
+ it 'finds notes on merge requests' do
+ create(:note_on_merge_request, project: project)
+
+ notes = described_class.new(project, user).execute
+
+ expect(notes.count).to eq(1)
end
- it 'finds all notes' do
- notes = NotesFinder.new.execute(project, user, params)
- expect(notes.size).to eq(2)
+ it 'finds notes on snippets' do
+ create(:note_on_project_snippet, project: project)
+
+ notes = described_class.new(project, user).execute
+
+ expect(notes.count).to eq(1)
end
- it 'raises an exception for an invalid target_type' do
- params.merge!(target_type: 'invalid')
- expect { NotesFinder.new.execute(project, user, params) }.to raise_error('invalid target_type')
+ it "excludes notes on commits the author can't download" do
+ project = create(:project, :private)
+ note = create(:note_on_commit, project: project)
+ params = { target_type: 'commit', target_id: note.noteable.id }
+
+ notes = described_class.new(project, create(:user), params).execute
+
+ expect(notes.count).to eq(0)
end
- it 'filters out old notes' do
- note2.update_attribute(:updated_at, 2.hours.ago)
- notes = NotesFinder.new.execute(project, user, params)
- expect(notes).to eq([note1])
+ it 'succeeds when no notes found' do
+ notes = described_class.new(project, create(:user)).execute
+
+ expect(notes.count).to eq(0)
end
- context 'confidential issue notes' do
- let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
- let!(:confidential_note) { create(:note, noteable: confidential_issue, project: confidential_issue.project) }
+ context 'on restricted projects' do
+ let(:project) do
+ create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE,
+ snippets_access_level: ProjectFeature::PRIVATE,
+ merge_requests_access_level: ProjectFeature::PRIVATE)
+ end
+
+ it 'publicly excludes notes on merge requests' do
+ create(:note_on_merge_request, project: project)
+
+ notes = described_class.new(project, create(:user)).execute
+
+ expect(notes.count).to eq(0)
+ end
+
+ it 'publicly excludes notes on issues' do
+ create(:note_on_issue, project: project)
+
+ notes = described_class.new(project, create(:user)).execute
+
+ expect(notes.count).to eq(0)
+ end
+
+ it 'publicly excludes notes on snippets' do
+ create(:note_on_project_snippet, project: project)
+
+ notes = described_class.new(project, create(:user)).execute
+
+ expect(notes.count).to eq(0)
+ end
+ end
+
+ context 'for target' do
+ let(:project) { create(:project) }
+ let(:note1) { create :note_on_commit, project: project }
+ let(:note2) { create :note_on_commit, project: project }
+ let(:commit) { note1.noteable }
+ let(:params) { { target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago.to_i } }
+
+ before do
+ note1
+ note2
+ end
+
+ it 'finds all notes' do
+ notes = described_class.new(project, user, params).execute
+ expect(notes.size).to eq(2)
+ end
+
+ it 'finds notes on merge requests' do
+ note = create(:note_on_merge_request, project: project)
+ params = { target_type: 'merge_request', target_id: note.noteable.id }
+
+ notes = described_class.new(project, user, params).execute
+
+ expect(notes).to include(note)
+ end
+
+ it 'finds notes on snippets' do
+ note = create(:note_on_project_snippet, project: project)
+ params = { target_type: 'snippet', target_id: note.noteable.id }
- let(:params) { { target_id: confidential_issue.id, target_type: 'issue', last_fetched_at: 1.hour.ago.to_i } }
+ notes = described_class.new(project, user, params).execute
- it 'returns notes if user can see the issue' do
- expect(NotesFinder.new.execute(project, user, params)).to eq([confidential_note])
+ expect(notes.count).to eq(1)
end
- it 'raises an error if user can not see the issue' do
+ it 'raises an exception for an invalid target_type' do
+ params.merge!(target_type: 'invalid')
+ expect { described_class.new(project, user, params).execute }.to raise_error('invalid target_type')
+ end
+
+ it 'filters out old notes' do
+ note2.update_attribute(:updated_at, 2.hours.ago)
+ notes = described_class.new(project, user, params).execute
+ expect(notes).to eq([note1])
+ end
+
+ context 'confidential issue notes' do
+ let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
+ let!(:confidential_note) { create(:note, noteable: confidential_issue, project: confidential_issue.project) }
+
+ let(:params) { { target_id: confidential_issue.id, target_type: 'issue', last_fetched_at: 1.hour.ago.to_i } }
+
+ it 'returns notes if user can see the issue' do
+ expect(described_class.new(project, user, params).execute).to eq([confidential_note])
+ end
+
+ it 'raises an error if user can not see the issue' do
+ user = create(:user)
+ expect { described_class.new(project, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'raises an error for project members with guest role' do
+ user = create(:user)
+ project.team << [user, :guest]
+
+ expect { described_class.new(project, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
+ end
+
+ describe '.search' do
+ let(:project) { create(:empty_project, :public) }
+ let(:note) { create(:note_on_issue, note: 'WoW', project: project) }
+
+ it 'returns notes with matching content' do
+ expect(described_class.new(note.project, nil, search: note.note).execute).to eq([note])
+ end
+
+ it 'returns notes with matching content regardless of the casing' do
+ expect(described_class.new(note.project, nil, search: 'WOW').execute).to eq([note])
+ end
+
+ it 'returns commit notes user can access' do
+ note = create(:note_on_commit, project: project)
+
+ expect(described_class.new(note.project, create(:user), search: note.note).execute).to eq([note])
+ end
+
+ context "confidential issues" do
+ let(:user) { create(:user) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
+ let(:confidential_note) { create(:note, note: "Random", noteable: confidential_issue, project: confidential_issue.project) }
+
+ it "returns notes with matching content if user can see the issue" do
+ expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to eq([confidential_note])
+ end
+
+ it "does not return notes with matching content if user can not see the issue" do
user = create(:user)
- expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound)
+ expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to be_empty
end
- it 'raises an error for project members with guest role' do
+ it "does not return notes with matching content for project members with guest role" do
user = create(:user)
project.team << [user, :guest]
+ expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to be_empty
+ end
+
+ it "does not return notes with matching content for unauthenticated users" do
+ expect(described_class.new(confidential_note.project, nil, search: confidential_note.note).execute).to be_empty
+ end
+ end
+
+ context 'inlines SQL filters on subqueries for performance' do
+ let(:sql) { described_class.new(note.project, nil, search: note.note).execute.to_sql }
+ let(:number_of_noteable_types) { 4 }
+
+ specify 'project_id check' do
+ expect(sql.scan(/project_id/).count).to be >= (number_of_noteable_types + 2)
+ end
- expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound)
+ specify 'search filter' do
+ expect(sql.scan(/LIKE/).count).to be >= number_of_noteable_types
end
end
end
diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb
index 4427443208a..975e99c5807 100644
--- a/spec/finders/snippets_finder_spec.rb
+++ b/spec/finders/snippets_finder_spec.rb
@@ -93,16 +93,39 @@ describe SnippetsFinder do
expect(snippets).not_to include(@snippet1, @snippet2)
end
- it "returns public and internal snippets for none project members" do
+ it "returns public and internal snippets for non project members" do
snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1)
expect(snippets).to include(@snippet2, @snippet3)
expect(snippets).not_to include(@snippet1)
end
+ it "returns public snippets for non project members" do
+ snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_public")
+ expect(snippets).to include(@snippet3)
+ expect(snippets).not_to include(@snippet1, @snippet2)
+ end
+
+ it "returns internal snippets for non project members" do
+ snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_internal")
+ expect(snippets).to include(@snippet2)
+ expect(snippets).not_to include(@snippet1, @snippet3)
+ end
+
+ it "does not return private snippets for non project members" do
+ snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_private")
+ expect(snippets).not_to include(@snippet1, @snippet2, @snippet3)
+ end
+
it "returns all snippets for project members" do
project1.team << [user, :developer]
snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1)
expect(snippets).to include(@snippet1, @snippet2, @snippet3)
end
+
+ it "returns private snippets for project members" do
+ project1.team << [user, :developer]
+ snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_private")
+ expect(snippets).to include(@snippet1)
+ end
end
end
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 15863d444f8..92053e5a7c6 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe ApplicationHelper do
+ include UploadHelpers
+
describe 'current_controller?' do
it 'returns true when controller matches argument' do
stub_controller_name('foo')
@@ -52,10 +54,8 @@ describe ApplicationHelper do
end
describe 'project_icon' do
- let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') }
-
it 'returns an url for the avatar' do
- project = create(:project, avatar: File.open(avatar_file_path))
+ project = create(:project, avatar: File.open(uploaded_image_temp_path))
avatar_url = "http://#{Gitlab.config.gitlab.host}/uploads/project/avatar/#{project.id}/banana_sample.gif"
expect(helper.project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s).
@@ -74,10 +74,8 @@ describe ApplicationHelper do
end
describe 'avatar_icon' do
- let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') }
-
it 'returns an url for the avatar' do
- user = create(:user, avatar: File.open(avatar_file_path))
+ user = create(:user, avatar: File.open(uploaded_image_temp_path))
expect(helper.avatar_icon(user.email).to_s).
to match("/uploads/user/avatar/#{user.id}/banana_sample.gif")
@@ -88,7 +86,7 @@ describe ApplicationHelper do
# Must be stubbed after the stub above, and separately
stub_config_setting(url: Settings.send(:build_gitlab_url))
- user = create(:user, avatar: File.open(avatar_file_path))
+ user = create(:user, avatar: File.open(uploaded_image_temp_path))
expect(helper.avatar_icon(user.email).to_s).
to match("/gitlab/uploads/user/avatar/#{user.id}/banana_sample.gif")
@@ -102,7 +100,7 @@ describe ApplicationHelper do
describe 'using a User' do
it 'returns an URL for the avatar' do
- user = create(:user, avatar: File.open(avatar_file_path))
+ user = create(:user, avatar: File.open(uploaded_image_temp_path))
expect(helper.avatar_icon(user).to_s).
to match("/uploads/user/avatar/#{user.id}/banana_sample.gif")
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 233d00534e5..c8b0d86425f 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -6,7 +6,7 @@ describe GroupsHelper do
it 'returns an url for the avatar' do
group = create(:group)
- group.avatar = File.open(avatar_file_path)
+ group.avatar = fixture_file_upload(avatar_file_path)
group.save!
expect(group_icon(group.path).to_s).
to match("/uploads/group/avatar/#{group.id}/banana_sample.gif")
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/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6
index a3171353bfb..49e56249565 100644
--- a/spec/javascripts/abuse_reports_spec.js.es6
+++ b/spec/javascripts/abuse_reports_spec.js.es6
@@ -1,42 +1,44 @@
-/* eslint-disable */
+/*= require lib/utils/text_utility */
/*= require abuse_reports */
-/*= require jquery */
-
((global) => {
- const FIXTURE = 'abuse_reports.html';
- const MAX_MESSAGE_LENGTH = 500;
+ describe('Abuse Reports', () => {
+ const FIXTURE = 'abuse_reports/abuse_reports_list.html.raw';
+ const MAX_MESSAGE_LENGTH = 500;
+
+ let messages;
- function assertMaxLength($message) {
- expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH);
- }
+ const assertMaxLength = $message => expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH);
+ const findMessage = searchText => messages.filter(
+ (index, element) => element.innerText.indexOf(searchText) > -1,
+ ).first();
- describe('Abuse Reports', function() {
fixture.preload(FIXTURE);
- beforeEach(function() {
+ beforeEach(function () {
fixture.load(FIXTURE);
- new global.AbuseReports();
+ this.abuseReports = new global.AbuseReports();
+ messages = $('.abuse-reports .message');
});
- it('should truncate long messages', function() {
- const $longMessage = $('#long');
+
+ it('should truncate long messages', () => {
+ const $longMessage = findMessage('LONG MESSAGE');
expect($longMessage.data('original-message')).toEqual(jasmine.anything());
assertMaxLength($longMessage);
});
- it('should not truncate short messages', function() {
- const $shortMessage = $('#short');
+ it('should not truncate short messages', () => {
+ const $shortMessage = findMessage('SHORT MESSAGE');
expect($shortMessage.data('original-message')).not.toEqual(jasmine.anything());
});
- it('should allow clicking a truncated message to expand and collapse the full message', function() {
- const $longMessage = $('#long');
+ it('should allow clicking a truncated message to expand and collapse the full message', () => {
+ const $longMessage = findMessage('LONG MESSAGE');
$longMessage.click();
expect($longMessage.data('original-message').length).toEqual($longMessage.text().length);
$longMessage.click();
assertMaxLength($longMessage);
});
});
-
})(window.gl);
diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6
index 8640cd44085..192da4ee8d9 100644
--- a/spec/javascripts/activities_spec.js.es6
+++ b/spec/javascripts/activities_spec.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-unused-expressions, comma-spacing, prefer-const, no-prototype-builtins, semi, no-new, keyword-spacing, no-plusplus, no-shadow, max-len */
+
/*= require js.cookie.js */
/*= require jquery.endless-scroll.js */
/*= require pager */
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index ac1404f6e1c..89201c8cb8b 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -1,4 +1,5 @@
-/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, no-undef, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, padded-blocks, max-len */
+/* global AwardsHandler */
/*= require awards_handler */
/*= require jquery */
@@ -33,9 +34,9 @@
};
describe('AwardsHandler', function() {
- fixture.preload('awards_handler.html');
+ fixture.preload('issues/open-issue.html.raw');
beforeEach(function() {
- fixture.load('awards_handler.html');
+ fixture.load('issues/open-issue.html.raw');
awardsHandler = new AwardsHandler;
spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) {
return function(url, emoji, cb) {
@@ -113,7 +114,7 @@
});
describe('::getAwardUrl', function() {
return it('should return the url for request', function() {
- return expect(awardsHandler.getAwardUrl()).toBe('/gitlab-org/gitlab-test/issues/8/toggle_award_emoji');
+ return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/issues-project/issues/1/toggle_award_emoji');
});
});
describe('::addAward and ::checkMutuality', function() {
@@ -209,7 +210,7 @@
$('.js-add-award').eq(0).click();
$menu = $('.emoji-menu');
$block = $('.js-awards-block');
- $emoji = $menu.find(".emoji-menu-list-item " + selector);
+ $emoji = $menu.find('.emoji-menu-list:not(.frequent-emojis) ' + selector);
expect($emoji.length).toBe(1);
expect($block.find(selector).length).toBe(0);
$emoji.click();
@@ -224,7 +225,7 @@
openEmojiMenuAndAddEmoji();
$('.js-add-award').eq(0).click();
$block = $('.js-awards-block');
- $emoji = $('.emoji-menu').find(".emoji-menu-list-item " + selector);
+ $emoji = $('.emoji-menu').find(".emoji-menu-list:not(.frequent-emojis) " + selector);
$emoji.click();
return expect($block.find(selector).length).toBe(0);
});
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js
index efb1203eb2f..0f61000bc37 100644
--- a/spec/javascripts/behaviors/quick_submit_spec.js
+++ b/spec/javascripts/behaviors/quick_submit_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, no-undef, jasmine/no-spec-dupes, new-cap, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, jasmine/no-spec-dupes, new-cap, padded-blocks, max-len */
/*= require behaviors/quick_submit */
diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6
index b84dfc8197b..b3a1afa28a5 100644
--- a/spec/javascripts/boards/boards_store_spec.js.es6
+++ b/spec/javascripts/boards/boards_store_spec.js.es6
@@ -1,4 +1,11 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, one-var, no-unused-vars, indent */
+/* global Vue */
+/* global BoardService */
+/* global boardsMockInterceptor */
+/* global Cookies */
+/* global listObj */
+/* global listObjDuplicate */
+
//= require jquery
//= require jquery_ujs
//= require js.cookie
diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6
index 90cb8926545..c8a61a0a9b5 100644
--- a/spec/javascripts/boards/issue_spec.js.es6
+++ b/spec/javascripts/boards/issue_spec.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle */
+/* global BoardService */
+/* global ListIssue */
+
//= require jquery
//= require jquery_ujs
//= require js.cookie
diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6
index dfbcbe3a7c1..7d942ec3d65 100644
--- a/spec/javascripts/boards/list_spec.js.es6
+++ b/spec/javascripts/boards/list_spec.js.es6
@@ -1,4 +1,10 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle */
+/* global Vue */
+/* global boardsMockInterceptor */
+/* global BoardService */
+/* global List */
+/* global listObj */
+
//= require jquery
//= require jquery_ujs
//= require js.cookie
diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6
index fcb3d8f17d8..8d3e2237fda 100644
--- a/spec/javascripts/boards/mock_data.js.es6
+++ b/spec/javascripts/boards/mock_data.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, no-unused-vars, quote-props */
+
const listObj = {
id: 1,
position: 0,
diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6
index 93f73fa0e9a..aadf6f518a8 100644
--- a/spec/javascripts/dashboard_spec.js.es6
+++ b/spec/javascripts/dashboard_spec.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-new, padded-blocks */
+
/*= require sidebar */
/*= require jquery */
/*= require js.cookie */
diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js.es6
index 9fdbab3a9e9..8ece24555c5 100644
--- a/spec/javascripts/datetime_utility_spec.js.es6
+++ b/spec/javascripts/datetime_utility_spec.js.es6
@@ -1,5 +1,5 @@
-/* eslint-disable */
//= require lib/utils/datetime_utility
+
(() => {
describe('Date time utils', () => {
describe('get day name', () => {
diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js.es6
index 9b2845af608..18805d26ac0 100644
--- a/spec/javascripts/diff_comments_store_spec.js.es6
+++ b/spec/javascripts/diff_comments_store_spec.js.es6
@@ -1,8 +1,11 @@
-/* eslint-disable */
+/* eslint-disable no-extra-semi, jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown, max-len */
+/* global CommentsStore */
+
//= require vue
//= require diff_notes/models/discussion
//= require diff_notes/models/note
//= require diff_notes/stores/comments
+
(() => {
function createDiscussion(noteId = 1, resolved = true) {
CommentsStore.create('a', noteId, true, resolved, 'test');
diff --git a/spec/javascripts/extensions/object_spec.js.es6 b/spec/javascripts/extensions/object_spec.js.es6
new file mode 100644
index 00000000000..3b71c255b30
--- /dev/null
+++ b/spec/javascripts/extensions/object_spec.js.es6
@@ -0,0 +1,25 @@
+/*= require extensions/object */
+
+describe('Object extensions', () => {
+ describe('assign', () => {
+ it('merges source object into target object', () => {
+ const targetObj = {};
+ const sourceObj = {
+ foo: 'bar',
+ };
+ Object.assign(targetObj, sourceObj);
+ expect(targetObj.foo).toBe('bar');
+ });
+
+ it('merges object with the same properties', () => {
+ const targetObj = {
+ foo: 'bar',
+ };
+ const sourceObj = {
+ foo: 'baz',
+ };
+ Object.assign(targetObj, sourceObj);
+ expect(targetObj.foo).toBe('baz');
+ });
+ });
+});
diff --git a/spec/javascripts/fixtures/abuse_reports.html.haml b/spec/javascripts/fixtures/abuse_reports.html.haml
deleted file mode 100644
index 2ec302abcb7..00000000000
--- a/spec/javascripts/fixtures/abuse_reports.html.haml
+++ /dev/null
@@ -1,16 +0,0 @@
-.abuse-reports
- .message#long
- Cat ipsum dolor sit amet, hide head under blanket so no one can see.
- Gate keepers of hell eat and than sleep on your face but hunt by meowing
- loudly at 5am next to human slave food dispenser cats go for world
- domination or chase laser, yet poop on grasses chirp at birds. Cat is love,
- cat is life chase after silly colored fish toys around the house climb a
- tree, wait for a fireman jump to fireman then scratch his face fall asleep
- on the washing machine lies down always hungry so caticus cuteicus. Sit on
- human. Spot something, big eyes, big eyes, crouch, shake butt, prepare to
- pounce sleep in the bathroom sink hiss at vacuum cleaner hide head under
- blanket so no one can see throwup on your pillow.
- .message#short
- Cat ipsum dolor sit amet, groom yourself 4 hours - checked, have your
- beauty sleep 18 hours - checked, be fabulous for the rest of the day -
- checked! for shake treat bag.
diff --git a/spec/javascripts/fixtures/abuse_reports.rb b/spec/javascripts/fixtures/abuse_reports.rb
new file mode 100644
index 00000000000..de673f94d72
--- /dev/null
+++ b/spec/javascripts/fixtures/abuse_reports.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Admin::AbuseReportsController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let!(:abuse_report) { create(:abuse_report) }
+ let!(:abuse_report_with_short_message) { create(:abuse_report, message: 'SHORT MESSAGE') }
+ let!(:abuse_report_with_long_message) { create(:abuse_report, message: "LONG MESSAGE\n" * 50) }
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('abuse_reports/')
+ end
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'abuse_reports/abuse_reports_list.html.raw' do |example|
+ get :index
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/fixtures/awards_handler.html.haml b/spec/javascripts/fixtures/awards_handler.html.haml
deleted file mode 100644
index 1ef2e8f8624..00000000000
--- a/spec/javascripts/fixtures/awards_handler.html.haml
+++ /dev/null
@@ -1,52 +0,0 @@
-.issue-details.issuable-details
- .detail-page-description.content-block
- %h2.title Quibusdam sint officiis earum molestiae ipsa autem voluptatem nisi rem.
- .description.js-task-list-container.is-task-list-enabled
- .wiki
- %p Qui exercitationem magnam optio quae fuga earum odio.
- %textarea.hidden.js-task-list-field Qui exercitationem magnam optio quae fuga earum odio.
- %small.edited-text
- .content-block.content-block-small
- .awards.js-awards-block{"data-award-url" => "/gitlab-org/gitlab-test/issues/8/toggle_award_emoji"}
- %button.award-control.btn.js-emoji-btn{"data-placement" => "bottom", "data-title" => "", :type => "button"}
- .icon.emoji-icon.emoji-1F44D{"data-aliases" => "", "data-emoji" => "thumbsup", "data-unicode-name" => "1F44D", :title => "thumbsup"}
- %span.award-control-text.js-counter 0
- %button.award-control.btn.js-emoji-btn{"data-placement" => "bottom", "data-title" => "", :type => "button"}
- .icon.emoji-icon.emoji-1F44E{"data-aliases" => "", "data-emoji" => "thumbsdown", "data-unicode-name" => "1F44E", :title => "thumbsdown"}
- %span.award-control-text.js-counter 0
- .award-menu-holder.js-award-holder
- %button.btn.award-control.js-add-award{:type => "button"}
- %i.fa.fa-smile-o.award-control-icon.award-control-icon-normal
- %i.fa.fa-spinner.fa-spin.award-control-icon.award-control-icon-loading
- %span.award-control-text Add
- %section.issuable-discussion
- #notes
- %ul#notes-list.notes.main-notes-list.timeline
- %li#note_348.note.note-row-348.timeline-entry{"data-author-id" => "18", "data-editable" => ""}
- .timeline-entry-inner
- .timeline-icon
- %a{:href => "/u/agustin"}
- %img.avatar.s40{:alt => "", :src => "#"}/
- .timeline-content
- .note-header
- %a.author_link{:href => "/u/agustin"}
- %span.author Brenna Stokes
- .inline.note-headline-light
- @agustin commented
- %a{:href => "#note_348"}
- %time 11 days ago
- .note-actions
- %span.note-role Reporter
- %a.note-action-button.note-emoji-button.js-add-award.js-note-emoji{"data-position" => "right", :href => "#", :title => "Award Emoji"}
- %i.fa.fa-spinner.fa-spin
- %i.fa.fa-smile-o.link-highlight
- .js-task-list-container.note-body.is-task-list-enabled
- .note-text
- %p Suscipit sunt quia quisquam sed eveniet ipsam.
- .note-awards
- .awards.hidden.js-awards-block{"data-award-url" => "/gitlab-org/gitlab-test/notes/348/toggle_award_emoji"}
- .award-menu-holder.js-award-holder
- %button.btn.award-control.js-add-award{:type => "button"}
- %i.fa.fa-smile-o.award-control-icon.award-control-icon-normal
- %i.fa.fa-spinner.fa-spin.award-control-icon.award-control-icon-loading
- %span.award-control-text Add
diff --git a/spec/javascripts/fixtures/pipeline_graph.html.haml b/spec/javascripts/fixtures/pipeline_graph.html.haml
new file mode 100644
index 00000000000..c0b5ab4411e
--- /dev/null
+++ b/spec/javascripts/fixtures/pipeline_graph.html.haml
@@ -0,0 +1,14 @@
+%div.pipeline-visualization.js-pipeline-graph
+ %ul.stage-column-list
+ %li.stage-column
+ .stage-name
+ %a{:href => "/"}
+ Test
+ .builds-container
+ %ul
+ %li.build
+ .curve
+ %a
+ %svg
+ .ci-status-text
+ stop_review
diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6
index 8ba238018cd..bfaf90e2aee 100644
--- a/spec/javascripts/gl_dropdown_spec.js.es6
+++ b/spec/javascripts/gl_dropdown_spec.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, prefer-const, no-param-reassign, no-plusplus, semi, no-unused-expressions, arrow-spacing, max-len */
+/* global Turbolinks */
+
/*= require jquery */
/*= require gl_dropdown */
/*= require turbolinks */
diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js.es6
index 0713e30e485..5018e87ad6c 100644
--- a/spec/javascripts/gl_field_errors_spec.js.es6
+++ b/spec/javascripts/gl_field_errors_spec.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, arrow-body-style, indent, padded-blocks */
+
//= require jquery
//= require gl_field_errors
diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
index a406e6cc36a..bc5cbeb6a40 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
@@ -1,4 +1,8 @@
-/* eslint-disable quotes, no-undef, indent, semi, object-curly-spacing, jasmine/no-suite-dupes, vars-on-top, no-var, padded-blocks, spaced-comment, max-len */
+/* eslint-disable quotes, indent, semi, object-curly-spacing, jasmine/no-suite-dupes, vars-on-top, no-var, padded-blocks, spaced-comment, max-len */
+/* global d3 */
+/* global ContributorsGraph */
+/* global ContributorsMasterGraph */
+
//= require graphs/stat_graph_contributors_graph
describe("ContributorsGraph", function () {
diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
index 96f39abe13e..751f3d175e2 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
@@ -1,4 +1,6 @@
-/* eslint-disable quotes, padded-blocks, no-var, camelcase, object-curly-spacing, semi, indent, object-property-newline, comma-dangle, comma-spacing, no-undef, spaced-comment, max-len, key-spacing, vars-on-top, quote-props, no-multi-spaces, max-len */
+/* eslint-disable quotes, padded-blocks, no-var, camelcase, object-curly-spacing, semi, indent, object-property-newline, comma-dangle, comma-spacing, spaced-comment, max-len, key-spacing, vars-on-top, quote-props, no-multi-spaces */
+/* global ContributorsStatGraphUtil */
+
//= require graphs/stat_graph_contributors_util
describe("ContributorsStatGraphUtil", function () {
diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js
index f78573b992b..0da124632ae 100644
--- a/spec/javascripts/graphs/stat_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_spec.js
@@ -1,4 +1,6 @@
-/* eslint-disable quotes, padded-blocks, no-undef, semi */
+/* eslint-disable quotes, padded-blocks, semi */
+/* global StatGraph */
+
//= require graphs/stat_graph
describe("StatGraph", function () {
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index 14af6644de1..faab5ae00c2 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -1,4 +1,5 @@
-/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, indent, no-undef, no-trailing-spaces, comma-dangle, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, indent, no-trailing-spaces, comma-dangle, padded-blocks, max-len */
+/* global Issue */
/*= require lib/utils/text_utility */
/*= require issue */
@@ -70,7 +71,7 @@
$('input[type=checkbox]').attr('checked', true).trigger('change');
expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
});
-
+
it('submits an ajax request on tasklist:changed', function() {
spyOn(jQuery, 'ajax').and.callFake(function(req) {
expect(req.type).toBe('PATCH');
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6
index 49687048eb5..0c48d04776f 100644
--- a/spec/javascripts/labels_issue_sidebar_spec.js.es6
+++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable no-new, no-plusplus, object-curly-spacing, prefer-const, semi */
+/* global IssuableContext */
+/* global LabelsSelect */
+
//= require lib/utils/type_utility
//= require jquery
//= require bootstrap
diff --git a/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6 b/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6
new file mode 100644
index 00000000000..3645dd70c55
--- /dev/null
+++ b/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6
@@ -0,0 +1,43 @@
+//= require lib/utils/custom_event_polyfill
+
+describe('Custom Event Polyfill', () => {
+ it('should be defined', () => {
+ expect(window.CustomEvent).toBeDefined();
+ });
+
+ it('should create a `CustomEvent` instance', () => {
+ const e = new window.CustomEvent('foo');
+
+ expect(e.type).toEqual('foo');
+ expect(e.bubbles).toBe(false);
+ expect(e.cancelable).toBe(false);
+ expect(e.detail).toBeFalsy();
+ });
+
+ it('should create a `CustomEvent` instance with a `details` object', () => {
+ const e = new window.CustomEvent('bar', { detail: { foo: 'bar' } });
+
+ expect(e.type).toEqual('bar');
+ expect(e.bubbles).toBe(false);
+ expect(e.cancelable).toBe(false);
+ expect(e.detail.foo).toEqual('bar');
+ });
+
+ it('should create a `CustomEvent` instance with a `bubbles` boolean', () => {
+ const e = new window.CustomEvent('bar', { bubbles: true });
+
+ expect(e.type).toEqual('bar');
+ expect(e.bubbles).toBe(true);
+ expect(e.cancelable).toBe(false);
+ expect(e.detail).toBeFalsy();
+ });
+
+ it('should create a `CustomEvent` instance with a `cancelable` boolean', () => {
+ const e = new window.CustomEvent('bar', { cancelable: true });
+
+ expect(e.type).toEqual('bar');
+ expect(e.bubbles).toBe(false);
+ expect(e.cancelable).toBe(true);
+ expect(e.detail).toBeFalsy();
+ });
+});
diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js
index b8b174a2e53..decdf583410 100644
--- a/spec/javascripts/line_highlighter_spec.js
+++ b/spec/javascripts/line_highlighter_spec.js
@@ -1,4 +1,5 @@
-/* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-undef, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, no-plusplus, jasmine/no-spec-dupes, no-underscore-dangle, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, no-plusplus, jasmine/no-spec-dupes, no-underscore-dangle, padded-blocks, max-len */
+/* global LineHighlighter */
/*= require line_highlighter */
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index cbe2634d3a4..4cf1693af1b 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -1,4 +1,5 @@
-/* eslint-disable space-before-function-paren, no-return-assign, no-undef, padded-blocks */
+/* eslint-disable space-before-function-paren, no-return-assign, padded-blocks */
+/* global MergeRequest */
/*= require merge_request */
diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js
index 8828970d984..a6cb9e47744 100644
--- a/spec/javascripts/new_branch_spec.js
+++ b/spec/javascripts/new_branch_spec.js
@@ -1,4 +1,5 @@
-/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, no-undef, quotes, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, padded-blocks, max-len */
+/* global NewBranchForm */
/*= require jquery-ui/autocomplete */
/*= require new_branch_form */
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 2db182d702b..d3bfa7730fa 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -1,4 +1,6 @@
-/* eslint-disable space-before-function-paren, no-unused-expressions, no-undef, no-var, object-shorthand, comma-dangle, semi, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, semi, padded-blocks, max-len */
+/* global Notes */
+
/*= require notes */
/*= require autosize */
/*= require gl_form */
diff --git a/spec/javascripts/pipelines_spec.js.es6 b/spec/javascripts/pipelines_spec.js.es6
new file mode 100644
index 00000000000..85c9cf4b4f1
--- /dev/null
+++ b/spec/javascripts/pipelines_spec.js.es6
@@ -0,0 +1,25 @@
+//= require pipelines
+
+(() => {
+ describe('Pipelines', () => {
+ fixture.preload('pipeline_graph');
+
+ beforeEach(() => {
+ fixture.load('pipeline_graph');
+ });
+
+ it('should be defined', () => {
+ expect(window.gl.Pipelines).toBeDefined();
+ });
+
+ it('should create a `Pipelines` instance without options', () => {
+ expect(() => { new window.gl.Pipelines(); }).not.toThrow(); //eslint-disable-line
+ });
+
+ it('should create a `Pipelines` instance with options', () => {
+ const pipelines = new window.gl.Pipelines({ foo: 'bar' });
+
+ expect(pipelines.pipelineGraph).toBeDefined();
+ });
+ });
+})();
diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js
index 49211a6b852..bb802a4b5e3 100644
--- a/spec/javascripts/project_title_spec.js
+++ b/spec/javascripts/project_title_spec.js
@@ -1,4 +1,6 @@
-/* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-undef, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, jasmine/no-expect-in-setup-teardown, padded-blocks, max-len */
+
+/* global Project */
/*= require bootstrap */
/*= require select2 */
@@ -21,16 +23,18 @@
return this.project = new Project();
});
return describe('project list', function() {
+ var fakeAjaxResponse = function fakeAjaxResponse(req) {
+ var d;
+ expect(req.url).toBe('/api/v3/projects.json?simple=true');
+ d = $.Deferred();
+ d.resolve(this.projects_data);
+ return d.promise();
+ };
+
beforeEach((function(_this) {
return function() {
_this.projects_data = fixture.load('projects.json')[0];
- return spyOn(jQuery, 'ajax').and.callFake(function(req) {
- var d;
- expect(req.url).toBe('/api/v3/projects.json?simple=true');
- d = $.Deferred();
- d.resolve(_this.projects_data);
- return d.promise();
- });
+ return spyOn(jQuery, 'ajax').and.callFake(fakeAjaxResponse.bind(_this));
};
})(this));
it('to show on toggle click', (function(_this) {
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index 0a9bc546144..a083dbf033a 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -1,4 +1,5 @@
-/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-undef, no-return-assign, new-cap, vars-on-top, semi, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, semi, padded-blocks, max-len */
+/* global Sidebar */
/*= require right_sidebar */
/*= require jquery */
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index e37816b0a8c..7bc898aed5d 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -1,4 +1,5 @@
-/* eslint-disable space-before-function-paren, no-return-assign, no-undef, no-var, quotes, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes, padded-blocks */
+/* global ShortcutsIssuable */
/*= require shortcuts_issuable */
diff --git a/spec/javascripts/subbable_resource_spec.js.es6 b/spec/javascripts/subbable_resource_spec.js.es6
index df395296791..6a70dd856a7 100644
--- a/spec/javascripts/subbable_resource_spec.js.es6
+++ b/spec/javascripts/subbable_resource_spec.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable max-len, arrow-parens, comma-dangle, no-plusplus */
+
//= vue
//= vue-resource
//= require jquery
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
index 944df6d23f7..a8874ab12d3 100644
--- a/spec/javascripts/u2f/authenticate_spec.js
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -1,4 +1,6 @@
-/* eslint-disable space-before-function-paren, new-parens, no-undef, quotes, comma-dangle, no-var, one-var, one-var-declaration-per-line, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, new-parens, quotes, comma-dangle, no-var, one-var, one-var-declaration-per-line, padded-blocks, max-len */
+/* global MockU2FDevice */
+/* global U2FAuthenticate */
/*= require u2f/authenticate */
/*= require u2f/util */
diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js
index 0c73c5772bd..189592ea87a 100644
--- a/spec/javascripts/u2f/register_spec.js
+++ b/spec/javascripts/u2f/register_spec.js
@@ -1,4 +1,6 @@
-/* eslint-disable space-before-function-paren, new-parens, no-undef, quotes, no-var, one-var, one-var-declaration-per-line, comma-dangle, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, new-parens, quotes, no-var, one-var, one-var-declaration-per-line, comma-dangle, padded-blocks, max-len */
+/* global MockU2FDevice */
+/* global U2FRegister */
/*= require u2f/register */
/*= require u2f/util */
diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js
index b9acaaa5a0d..d80ce5a7f7e 100644
--- a/spec/javascripts/zen_mode_spec.js
+++ b/spec/javascripts/zen_mode_spec.js
@@ -1,4 +1,7 @@
-/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-undef, object-shorthand, comma-dangle, no-return-assign, new-cap, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-return-assign, new-cap, padded-blocks, max-len */
+/* global Dropzone */
+/* global Mousetrap */
+/* global ZenMode */
/*= require zen_mode */
diff --git a/spec/lib/banzai/filter/math_filter_spec.rb b/spec/lib/banzai/filter/math_filter_spec.rb
new file mode 100644
index 00000000000..3fe2c7f5d5d
--- /dev/null
+++ b/spec/lib/banzai/filter/math_filter_spec.rb
@@ -0,0 +1,120 @@
+require 'spec_helper'
+
+describe Banzai::Filter::MathFilter, lib: true do
+ include FilterSpecHelper
+
+ it 'leaves regular inline code unchanged' do
+ input = "<code>2+2</code>"
+ doc = filter(input)
+
+ expect(doc.to_s).to eq input
+ end
+
+ it 'removes surrounding dollar signs and adds class code, math and js-render-math' do
+ doc = filter("$<code>2+2</code>$")
+
+ expect(doc.to_s).to eq '<code class="code math js-render-math" data-math-style="inline">2+2</code>'
+ end
+
+ it 'only removes surrounding dollar signs' do
+ doc = filter("test $<code>2+2</code>$ test")
+ before = doc.xpath('descendant-or-self::text()[1]').first
+ after = doc.xpath('descendant-or-self::text()[3]').first
+
+ expect(before.to_s).to eq 'test '
+ expect(after.to_s).to eq ' test'
+ end
+
+ it 'only removes surrounding single dollar sign' do
+ doc = filter("test $$<code>2+2</code>$$ test")
+ before = doc.xpath('descendant-or-self::text()[1]').first
+ after = doc.xpath('descendant-or-self::text()[3]').first
+
+ expect(before.to_s).to eq 'test $'
+ expect(after.to_s).to eq '$ test'
+ end
+
+ it 'adds data-math-style inline attribute to inline math' do
+ doc = filter('$<code>2+2</code>$')
+ code = doc.xpath('descendant-or-self::code').first
+
+ expect(code['data-math-style']).to eq 'inline'
+ end
+
+ it 'adds class code and math to inline math' do
+ doc = filter('$<code>2+2</code>$')
+ code = doc.xpath('descendant-or-self::code').first
+
+ expect(code[:class]).to include("code")
+ expect(code[:class]).to include("math")
+ end
+
+ it 'adds js-render-math class to inline math' do
+ doc = filter('$<code>2+2</code>$')
+ code = doc.xpath('descendant-or-self::code').first
+
+ expect(code[:class]).to include("js-render-math")
+ end
+
+ # Cases with faulty syntax. Should be a no-op
+
+ it 'ignores cases with missing dolar sign at the end' do
+ input = "test $<code>2+2</code> test"
+ doc = filter(input)
+
+ expect(doc.to_s).to eq input
+ end
+
+ it 'ignores cases with missing dolar sign at the beginning' do
+ input = "test <code>2+2</code>$ test"
+ doc = filter(input)
+
+ expect(doc.to_s).to eq input
+ end
+
+ it 'ignores dollar signs if it is not adjacent' do
+ input = '<p>We check strictly $<code>2+2</code> and <code>2+2</code>$ </p>'
+ doc = filter(input)
+
+ expect(doc.to_s).to eq input
+ end
+
+ # Display math
+
+ it 'adds data-math-style display attribute to display math' do
+ doc = filter('<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>')
+ pre = doc.xpath('descendant-or-self::pre').first
+
+ expect(pre['data-math-style']).to eq 'display'
+ end
+
+ it 'adds js-render-math class to display math' do
+ doc = filter('<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>')
+ pre = doc.xpath('descendant-or-self::pre').first
+
+ expect(pre[:class]).to include("js-render-math")
+ end
+
+ it 'ignores code blocks that are not math' do
+ input = '<pre class="code highlight js-syntax-highlight plaintext" v-pre="true"><code>2+2</code></pre>'
+ doc = filter(input)
+
+ expect(doc.to_s).to eq input
+ end
+
+ it 'requires the pre to contain both code and math' do
+ input = '<pre class="highlight js-syntax-highlight plaintext math" v-pre="true"><code>2+2</code></pre>'
+ doc = filter(input)
+
+ expect(doc.to_s).to eq input
+ end
+
+ it 'dollar signs around to display math' do
+ doc = filter('$<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>$')
+ before = doc.xpath('descendant-or-self::text()[1]').first
+ after = doc.xpath('descendant-or-self::text()[3]').first
+
+ expect(before.to_s).to eq '$'
+ expect(after.to_s).to eq '$'
+ end
+end
diff --git a/spec/lib/bitbucket/collection_spec.rb b/spec/lib/bitbucket/collection_spec.rb
new file mode 100644
index 00000000000..015a7f80e03
--- /dev/null
+++ b/spec/lib/bitbucket/collection_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+# Emulates paginator. It returns 2 pages with results
+class TestPaginator
+ def initialize
+ @current_page = 0
+ end
+
+ def items
+ @current_page += 1
+
+ raise StopIteration if @current_page > 2
+
+ ["result_1_page_#{@current_page}", "result_2_page_#{@current_page}"]
+ end
+end
+
+describe Bitbucket::Collection do
+ it "iterates paginator" do
+ collection = described_class.new(TestPaginator.new)
+
+ expect(collection.to_a).to match(["result_1_page_1", "result_2_page_1", "result_1_page_2", "result_2_page_2"])
+ end
+end
diff --git a/spec/lib/bitbucket/connection_spec.rb b/spec/lib/bitbucket/connection_spec.rb
new file mode 100644
index 00000000000..14faeb231a9
--- /dev/null
+++ b/spec/lib/bitbucket/connection_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Bitbucket::Connection do
+ before do
+ allow_any_instance_of(described_class).to receive(:provider).and_return(double(app_id: '', app_secret: ''))
+ end
+
+ describe '#get' do
+ it 'calls OAuth2::AccessToken::get' do
+ expect_any_instance_of(OAuth2::AccessToken).to receive(:get).and_return(double(parsed: true))
+
+ connection = described_class.new({})
+
+ connection.get('/users')
+ end
+ end
+
+ describe '#expired?' do
+ it 'calls connection.expired?' do
+ expect_any_instance_of(OAuth2::AccessToken).to receive(:expired?).and_return(true)
+
+ expect(described_class.new({}).expired?).to be_truthy
+ end
+ end
+
+ describe '#refresh!' do
+ it 'calls connection.refresh!' do
+ response = double(token: nil, expires_at: nil, expires_in: nil, refresh_token: nil)
+
+ expect_any_instance_of(OAuth2::AccessToken).to receive(:refresh!).and_return(response)
+
+ described_class.new({}).refresh!
+ end
+ end
+end
diff --git a/spec/lib/bitbucket/page_spec.rb b/spec/lib/bitbucket/page_spec.rb
new file mode 100644
index 00000000000..04d5a0470b1
--- /dev/null
+++ b/spec/lib/bitbucket/page_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe Bitbucket::Page do
+ let(:response) { { 'values' => [{ 'username' => 'Ben' }], 'pagelen' => 2, 'next' => '' } }
+
+ before do
+ # Autoloading hack
+ Bitbucket::Representation::User.new({})
+ end
+
+ describe '#items' do
+ it 'returns collection of needed objects' do
+ page = described_class.new(response, :user)
+
+ expect(page.items.first).to be_a(Bitbucket::Representation::User)
+ expect(page.items.count).to eq(1)
+ end
+ end
+
+ describe '#attrs' do
+ it 'returns attributes' do
+ page = described_class.new(response, :user)
+
+ expect(page.attrs.keys).to include(:pagelen, :next)
+ end
+ end
+
+ describe '#next?' do
+ it 'returns true' do
+ page = described_class.new(response, :user)
+
+ expect(page.next?).to be_truthy
+ end
+
+ it 'returns false' do
+ response['next'] = nil
+ page = described_class.new(response, :user)
+
+ expect(page.next?).to be_falsey
+ end
+ end
+
+ describe '#next' do
+ it 'returns next attribute' do
+ page = described_class.new(response, :user)
+
+ expect(page.next).to eq('')
+ end
+ end
+end
diff --git a/spec/lib/bitbucket/paginator_spec.rb b/spec/lib/bitbucket/paginator_spec.rb
new file mode 100644
index 00000000000..2c972da682e
--- /dev/null
+++ b/spec/lib/bitbucket/paginator_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe Bitbucket::Paginator do
+ let(:last_page) { double(:page, next?: false, items: ['item_2']) }
+ let(:first_page) { double(:page, next?: true, next: last_page, items: ['item_1']) }
+
+ describe 'items' do
+ it 'return items and raises StopIteration in the end' do
+ paginator = described_class.new(nil, nil, nil)
+
+ allow(paginator).to receive(:fetch_next_page).and_return(first_page)
+ expect(paginator.items).to match(['item_1'])
+
+ allow(paginator).to receive(:fetch_next_page).and_return(last_page)
+ expect(paginator.items).to match(['item_2'])
+
+ allow(paginator).to receive(:fetch_next_page).and_return(nil)
+ expect{ paginator.items }.to raise_error(StopIteration)
+ end
+ end
+end
diff --git a/spec/lib/bitbucket/representation/comment_spec.rb b/spec/lib/bitbucket/representation/comment_spec.rb
new file mode 100644
index 00000000000..fec243a9f96
--- /dev/null
+++ b/spec/lib/bitbucket/representation/comment_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe Bitbucket::Representation::Comment do
+ describe '#author' do
+ it { expect(described_class.new('user' => { 'username' => 'Ben' }).author).to eq('Ben') }
+ it { expect(described_class.new({}).author).to be_nil }
+ end
+
+ describe '#note' do
+ it { expect(described_class.new('content' => { 'raw' => 'Text' }).note).to eq('Text') }
+ it { expect(described_class.new({}).note).to be_nil }
+ end
+
+ describe '#created_at' do
+ it { expect(described_class.new('created_on' => Date.today).created_at).to eq(Date.today) }
+ end
+
+ describe '#updated_at' do
+ it { expect(described_class.new('updated_on' => Date.today).updated_at).to eq(Date.today) }
+ it { expect(described_class.new('created_on' => Date.today).updated_at).to eq(Date.today) }
+ end
+end
diff --git a/spec/lib/bitbucket/representation/issue_spec.rb b/spec/lib/bitbucket/representation/issue_spec.rb
new file mode 100644
index 00000000000..20f47224aa8
--- /dev/null
+++ b/spec/lib/bitbucket/representation/issue_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Bitbucket::Representation::Issue do
+ describe '#iid' do
+ it { expect(described_class.new('id' => 1).iid).to eq(1) }
+ end
+
+ describe '#kind' do
+ it { expect(described_class.new('kind' => 'bug').kind).to eq('bug') }
+ end
+
+ describe '#milestone' do
+ it { expect(described_class.new({ 'milestone' => { 'name' => '1.0' } }).milestone).to eq('1.0') }
+ it { expect(described_class.new({}).milestone).to be_nil }
+ end
+
+ describe '#author' do
+ it { expect(described_class.new({ 'reporter' => { 'username' => 'Ben' } }).author).to eq('Ben') }
+ it { expect(described_class.new({}).author).to be_nil }
+ end
+
+ describe '#description' do
+ it { expect(described_class.new({ 'content' => { 'raw' => 'Text' } }).description).to eq('Text') }
+ it { expect(described_class.new({}).description).to be_nil }
+ end
+
+ describe '#state' do
+ it { expect(described_class.new({ 'state' => 'invalid' }).state).to eq('closed') }
+ it { expect(described_class.new({ 'state' => 'wontfix' }).state).to eq('closed') }
+ it { expect(described_class.new({ 'state' => 'resolved' }).state).to eq('closed') }
+ it { expect(described_class.new({ 'state' => 'duplicate' }).state).to eq('closed') }
+ it { expect(described_class.new({ 'state' => 'closed' }).state).to eq('closed') }
+ it { expect(described_class.new({ 'state' => 'opened' }).state).to eq('opened') }
+ end
+
+ describe '#title' do
+ it { expect(described_class.new('title' => 'Issue').title).to eq('Issue') }
+ end
+
+ describe '#created_at' do
+ it { expect(described_class.new('created_on' => Date.today).created_at).to eq(Date.today) }
+ end
+
+ describe '#updated_at' do
+ it { expect(described_class.new('edited_on' => Date.today).updated_at).to eq(Date.today) }
+ end
+end
diff --git a/spec/lib/bitbucket/representation/pull_request_comment_spec.rb b/spec/lib/bitbucket/representation/pull_request_comment_spec.rb
new file mode 100644
index 00000000000..673dcf22ce8
--- /dev/null
+++ b/spec/lib/bitbucket/representation/pull_request_comment_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Bitbucket::Representation::PullRequestComment do
+ describe '#iid' do
+ it { expect(described_class.new('id' => 1).iid).to eq(1) }
+ end
+
+ describe '#file_path' do
+ it { expect(described_class.new('inline' => { 'path' => '/path' }).file_path).to eq('/path') }
+ end
+
+ describe '#old_pos' do
+ it { expect(described_class.new('inline' => { 'from' => 3 }).old_pos).to eq(3) }
+ end
+
+ describe '#new_pos' do
+ it { expect(described_class.new('inline' => { 'to' => 3 }).new_pos).to eq(3) }
+ end
+
+ describe '#parent_id' do
+ it { expect(described_class.new({ 'parent' => { 'id' => 2 } }).parent_id).to eq(2) }
+ it { expect(described_class.new({}).parent_id).to be_nil }
+ end
+
+ describe '#inline?' do
+ it { expect(described_class.new('inline' => {}).inline?).to be_truthy }
+ it { expect(described_class.new({}).inline?).to be_falsey }
+ end
+
+ describe '#has_parent?' do
+ it { expect(described_class.new('parent' => {}).has_parent?).to be_truthy }
+ it { expect(described_class.new({}).has_parent?).to be_falsey }
+ end
+end
diff --git a/spec/lib/bitbucket/representation/pull_request_spec.rb b/spec/lib/bitbucket/representation/pull_request_spec.rb
new file mode 100644
index 00000000000..30453528be4
--- /dev/null
+++ b/spec/lib/bitbucket/representation/pull_request_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Bitbucket::Representation::PullRequest do
+ describe '#iid' do
+ it { expect(described_class.new('id' => 1).iid).to eq(1) }
+ end
+
+ describe '#author' do
+ it { expect(described_class.new({ 'author' => { 'username' => 'Ben' } }).author).to eq('Ben') }
+ it { expect(described_class.new({}).author).to be_nil }
+ end
+
+ describe '#description' do
+ it { expect(described_class.new({ 'description' => 'Text' }).description).to eq('Text') }
+ it { expect(described_class.new({}).description).to be_nil }
+ end
+
+ describe '#state' do
+ it { expect(described_class.new({ 'state' => 'MERGED' }).state).to eq('merged') }
+ it { expect(described_class.new({ 'state' => 'DECLINED' }).state).to eq('closed') }
+ it { expect(described_class.new({}).state).to eq('opened') }
+ end
+
+ describe '#title' do
+ it { expect(described_class.new('title' => 'Issue').title).to eq('Issue') }
+ end
+
+ describe '#source_branch_name' do
+ it { expect(described_class.new({ source: { branch: { name: 'feature' } } }.with_indifferent_access).source_branch_name).to eq('feature') }
+ it { expect(described_class.new({ source: {} }.with_indifferent_access).source_branch_name).to be_nil }
+ end
+
+ describe '#source_branch_sha' do
+ it { expect(described_class.new({ source: { commit: { hash: 'abcd123' } } }.with_indifferent_access).source_branch_sha).to eq('abcd123') }
+ it { expect(described_class.new({ source: {} }.with_indifferent_access).source_branch_sha).to be_nil }
+ end
+
+ describe '#target_branch_name' do
+ it { expect(described_class.new({ destination: { branch: { name: 'master' } } }.with_indifferent_access).target_branch_name).to eq('master') }
+ it { expect(described_class.new({ destination: {} }.with_indifferent_access).target_branch_name).to be_nil }
+ end
+
+ describe '#target_branch_sha' do
+ it { expect(described_class.new({ destination: { commit: { hash: 'abcd123' } } }.with_indifferent_access).target_branch_sha).to eq('abcd123') }
+ it { expect(described_class.new({ destination: {} }.with_indifferent_access).target_branch_sha).to be_nil }
+ end
+end
diff --git a/spec/lib/bitbucket/representation/repo_spec.rb b/spec/lib/bitbucket/representation/repo_spec.rb
new file mode 100644
index 00000000000..adcd978e1b3
--- /dev/null
+++ b/spec/lib/bitbucket/representation/repo_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Bitbucket::Representation::Repo do
+ describe '#has_wiki?' do
+ it { expect(described_class.new({ 'has_wiki' => false }).has_wiki?).to be_falsey }
+ it { expect(described_class.new({ 'has_wiki' => true }).has_wiki?).to be_truthy }
+ end
+
+ describe '#name' do
+ it { expect(described_class.new({ 'name' => 'test' }).name).to eq('test') }
+ end
+
+ describe '#valid?' do
+ it { expect(described_class.new({ 'scm' => 'hg' }).valid?).to be_falsey }
+ it { expect(described_class.new({ 'scm' => 'git' }).valid?).to be_truthy }
+ end
+
+ describe '#full_name' do
+ it { expect(described_class.new({ 'full_name' => 'test_full' }).full_name).to eq('test_full') }
+ end
+
+ describe '#description' do
+ it { expect(described_class.new({ 'description' => 'desc' }).description).to eq('desc') }
+ end
+
+ describe '#issues_enabled?' do
+ it { expect(described_class.new({ 'has_issues' => false }).issues_enabled?).to be_falsey }
+ it { expect(described_class.new({ 'has_issues' => true }).issues_enabled?).to be_truthy }
+ end
+
+ describe '#owner_and_slug' do
+ it { expect(described_class.new({ 'full_name' => 'ben/test' }).owner_and_slug).to eq(['ben', 'test']) }
+ end
+
+ describe '#owner' do
+ it { expect(described_class.new({ 'full_name' => 'ben/test' }).owner).to eq('ben') }
+ end
+
+ describe '#slug' do
+ it { expect(described_class.new({ 'full_name' => 'ben/test' }).slug).to eq('test') }
+ end
+
+ describe '#clone_url' do
+ it 'builds url' do
+ data = { 'links' => { 'clone' => [ { 'name' => 'https', 'href' => 'https://bibucket.org/test/test.git' }] } }
+ expect(described_class.new(data).clone_url('abc')).to eq('https://x-token-auth:abc@bibucket.org/test/test.git')
+ end
+ end
+end
diff --git a/spec/lib/bitbucket/representation/user_spec.rb b/spec/lib/bitbucket/representation/user_spec.rb
new file mode 100644
index 00000000000..f79ff4edb7b
--- /dev/null
+++ b/spec/lib/bitbucket/representation/user_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+describe Bitbucket::Representation::User do
+ describe '#username' do
+ it 'returns correct value' do
+ user = described_class.new('username' => 'Ben')
+
+ expect(user.username).to eq('Ben')
+ end
+ end
+end
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index ff5dcc06ab3..62d68721574 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -483,7 +483,7 @@ module Ci
context 'when global variables are defined' do
let(:variables) do
- { VAR1: 'value1', VAR2: 'value2' }
+ { 'VAR1' => 'value1', 'VAR2' => 'value2' }
end
let(:config) do
{
@@ -495,18 +495,18 @@ module Ci
it 'returns global variables' do
expect(subject).to contain_exactly(
- { key: :VAR1, value: 'value1', public: true },
- { key: :VAR2, value: 'value2', public: true }
+ { key: 'VAR1', value: 'value1', public: true },
+ { key: 'VAR2', value: 'value2', public: true }
)
end
end
context 'when job and global variables are defined' do
let(:global_variables) do
- { VAR1: 'global1', VAR3: 'global3' }
+ { 'VAR1' => 'global1', 'VAR3' => 'global3' }
end
let(:job_variables) do
- { VAR1: 'value1', VAR2: 'value2' }
+ { 'VAR1' => 'value1', 'VAR2' => 'value2' }
end
let(:config) do
{
@@ -518,9 +518,9 @@ module Ci
it 'returns all unique variables' do
expect(subject).to contain_exactly(
- { key: :VAR3, value: 'global3', public: true },
- { key: :VAR1, value: 'value1', public: true },
- { key: :VAR2, value: 'value2', public: true }
+ { key: 'VAR3', value: 'global3', public: true },
+ { key: 'VAR1', value: 'value1', public: true },
+ { key: 'VAR2', value: 'value2', public: true }
)
end
end
@@ -535,13 +535,13 @@ module Ci
context 'when syntax is correct' do
let(:variables) do
- { VAR1: 'value1', VAR2: 'value2' }
+ { 'VAR1' => 'value1', 'VAR2' => 'value2' }
end
it 'returns job variables' do
expect(subject).to contain_exactly(
- { key: :VAR1, value: 'value1', public: true },
- { key: :VAR2, value: 'value2', public: true }
+ { key: 'VAR1', value: 'value1', public: true },
+ { key: 'VAR2', value: 'value2', public: true }
)
end
end
@@ -549,7 +549,7 @@ module Ci
context 'when syntax is incorrect' do
context 'when variables defined but invalid' do
let(:variables) do
- [ :VAR1, 'value1', :VAR2, 'value2' ]
+ [ 'VAR1', 'value1', 'VAR2', 'value2' ]
end
it 'raises error' do
diff --git a/spec/lib/gitlab/allowable_spec.rb b/spec/lib/gitlab/allowable_spec.rb
new file mode 100644
index 00000000000..87733d53e92
--- /dev/null
+++ b/spec/lib/gitlab/allowable_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Gitlab::Allowable do
+ subject do
+ Class.new.include(described_class).new
+ end
+
+ describe '#can?' do
+ let(:user) { create(:user) }
+
+ context 'when user is allowed to do something' do
+ let(:project) { create(:empty_project, :public) }
+
+ it 'reports correct ability to perform action' do
+ expect(subject.can?(user, :read_project, project)).to be true
+ end
+ end
+
+ context 'when user is not allowed to do something' do
+ let(:project) { create(:empty_project, :private) }
+
+ it 'reports correct ability to perform action' do
+ expect(subject.can?(user, :read_project, project)).to be false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 4aba783dc33..f3843ca64ff 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -11,7 +11,7 @@ module Gitlab
it "converts the input using Asciidoctor and default options" do
expected_asciidoc_opts = {
safe: :secure,
- backend: :html5,
+ backend: :gitlab_html5,
attributes: described_class::DEFAULT_ADOC_ATTRS
}
@@ -27,7 +27,7 @@ module Gitlab
it "merges the options with default ones" do
expected_asciidoc_opts = {
safe: :safe,
- backend: :html5,
+ backend: :gitlab_html5,
attributes: described_class::DEFAULT_ADOC_ATTRS + ['foo']
}
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index c9d64e99f88..f251c0dd25a 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -47,54 +47,76 @@ describe Gitlab::Auth, lib: true do
project.create_drone_ci_service(active: true)
project.drone_ci_service.update(token: 'token')
- ip = 'ip'
-
- expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'drone-ci-token')
- expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities))
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'drone-ci-token')
+ expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities))
end
it 'recognizes master passwords' do
user = create(:user, password: 'password')
- ip = 'ip'
- expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username)
- expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username)
+ expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
end
it 'recognizes user lfs tokens' do
user = create(:user)
- ip = 'ip'
token = Gitlab::LfsToken.new(user).token
- expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username)
- expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities))
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username)
+ expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities))
end
it 'recognizes deploy key lfs tokens' do
key = create(:deploy_key)
- ip = 'ip'
token = Gitlab::LfsToken.new(key).token
- expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: "lfs+deploy-key-#{key.id}")
- expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities))
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}")
+ expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities))
end
- it 'recognizes OAuth tokens' do
- user = create(:user)
- application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
- token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id)
- ip = 'ip'
+ context "while using OAuth tokens as passwords" do
+ it 'succeeds for OAuth tokens with the `api` scope' do
+ user = create(:user)
+ application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
+ token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api")
+
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'oauth2')
+ expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities))
+ end
- expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'oauth2')
- expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities))
+ it 'fails for OAuth tokens with other scopes' do
+ user = create(:user)
+ application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
+ token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "read_user")
+
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'oauth2')
+ expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil))
+ end
+ end
+
+ context "while using personal access tokens as passwords" do
+ it 'succeeds for personal access tokens with the `api` scope' do
+ user = create(:user)
+ personal_access_token = create(:personal_access_token, user: user, scopes: ['api'])
+
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.email)
+ expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities))
+ end
+
+ it 'fails for personal access tokens with other scopes' do
+ user = create(:user)
+ personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
+
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: user.email)
+ expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil))
+ end
end
it 'returns double nil for invalid credentials' do
login = 'foo'
- ip = 'ip'
- expect(gl_auth).to receive(:rate_limit!).with(ip, success: false, login: login)
- expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new)
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login)
+ expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new)
end
end
diff --git a/spec/lib/gitlab/badge/build/status_spec.rb b/spec/lib/gitlab/badge/build/status_spec.rb
index 38eebb2a176..70f03021d36 100644
--- a/spec/lib/gitlab/badge/build/status_spec.rb
+++ b/spec/lib/gitlab/badge/build/status_spec.rb
@@ -69,8 +69,8 @@ describe Gitlab::Badge::Build::Status do
new_build.success!
end
- it 'reports the compound status' do
- expect(badge.status).to eq 'failed'
+ it 'does not take outdated pipeline into account' do
+ expect(badge.status).to eq 'success'
end
end
end
diff --git a/spec/lib/gitlab/bitbucket_import/client_spec.rb b/spec/lib/gitlab/bitbucket_import/client_spec.rb
deleted file mode 100644
index 7543c29bcc4..00000000000
--- a/spec/lib/gitlab/bitbucket_import/client_spec.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::BitbucketImport::Client, lib: true do
- include ImportSpecHelper
-
- let(:token) { '123456' }
- let(:secret) { 'secret' }
- let(:client) { Gitlab::BitbucketImport::Client.new(token, secret) }
-
- before do
- stub_omniauth_provider('bitbucket')
- end
-
- it 'all OAuth client options are symbols' do
- client.consumer.options.keys.each do |key|
- expect(key).to be_kind_of(Symbol)
- end
- end
-
- context 'issues' do
- let(:per_page) { 50 }
- let(:count) { 95 }
- let(:sample_issues) do
- issues = []
-
- count.times do |i|
- issues << { local_id: i }
- end
-
- issues
- end
- let(:first_sample_data) { { count: count, issues: sample_issues[0..per_page - 1] } }
- let(:second_sample_data) { { count: count, issues: sample_issues[per_page..count] } }
- let(:project_id) { 'namespace/repo' }
-
- it 'retrieves issues over a number of pages' do
- stub_request(:get,
- "https://bitbucket.org/api/1.0/repositories/#{project_id}/issues?limit=50&sort=utc_created_on&start=0").
- to_return(status: 200,
- body: first_sample_data.to_json,
- headers: {})
-
- stub_request(:get,
- "https://bitbucket.org/api/1.0/repositories/#{project_id}/issues?limit=50&sort=utc_created_on&start=50").
- to_return(status: 200,
- body: second_sample_data.to_json,
- headers: {})
-
- issues = client.issues(project_id)
- expect(issues.count).to eq(95)
- end
- end
-
- context 'project import' do
- it 'calls .from_project with no errors' do
- project = create(:empty_project)
- project.import_url = "ssh://git@bitbucket.org/test/test.git"
- project.create_or_update_import_data(credentials:
- { user: "git",
- password: nil,
- bb_session: { bitbucket_access_token: "test",
- bitbucket_access_token_secret: "test" } })
-
- expect { described_class.from_project(project) }.not_to raise_error
- end
- end
-end
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index aa00f32becb..72b1ba36b58 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -18,15 +18,21 @@ describe Gitlab::BitbucketImport::Importer, lib: true do
"closed" # undocumented status
]
end
+
let(:sample_issues_statuses) do
issues = []
statuses.map.with_index do |status, index|
issues << {
- local_id: index,
- status: status,
+ id: index,
+ state: status,
title: "Issue #{index}",
- content: "Some content to issue #{index}"
+ kind: 'bug',
+ content: {
+ raw: "Some content to issue #{index}",
+ markup: "markdown",
+ html: "Some content to issue #{index}"
+ }
}
end
@@ -34,14 +40,16 @@ describe Gitlab::BitbucketImport::Importer, lib: true do
end
let(:project_identifier) { 'namespace/repo' }
+
let(:data) do
{
'bb_session' => {
- 'bitbucket_access_token' => "123456",
- 'bitbucket_access_token_secret' => "secret"
+ 'bitbucket_token' => "123456",
+ 'bitbucket_refresh_token' => "secret"
}
}
end
+
let(:project) do
create(
:project,
@@ -49,42 +57,70 @@ describe Gitlab::BitbucketImport::Importer, lib: true do
import_data: ProjectImportData.new(credentials: data)
)
end
+
let(:importer) { Gitlab::BitbucketImport::Importer.new(project) }
+
let(:issues_statuses_sample_data) do
{
count: sample_issues_statuses.count,
- issues: sample_issues_statuses
+ values: sample_issues_statuses
}
end
context 'issues statuses' do
before do
+ # HACK: Bitbucket::Representation.const_get('Issue') seems to return ::Issue without this
+ Bitbucket::Representation::Issue.new({})
+
stub_request(
:get,
- "https://bitbucket.org/api/1.0/repositories/#{project_identifier}"
- ).to_return(status: 200, body: { has_issues: true }.to_json)
+ "https://api.bitbucket.org/2.0/repositories/#{project_identifier}"
+ ).to_return(status: 200,
+ headers: { "Content-Type" => "application/json" },
+ body: { has_issues: true, full_name: project_identifier }.to_json)
stub_request(
:get,
- "https://bitbucket.org/api/1.0/repositories/#{project_identifier}/issues?limit=50&sort=utc_created_on&start=0"
- ).to_return(status: 200, body: issues_statuses_sample_data.to_json)
+ "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/issues?pagelen=50&sort=created_on"
+ ).to_return(status: 200,
+ headers: { "Content-Type" => "application/json" },
+ body: issues_statuses_sample_data.to_json)
+
+ stub_request(:get, "https://api.bitbucket.org/2.0/repositories/namespace/repo?pagelen=50&sort=created_on").
+ with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer', 'User-Agent' => 'Faraday v0.9.2' }).
+ to_return(status: 200,
+ body: "",
+ headers: {})
sample_issues_statuses.each_with_index do |issue, index|
stub_request(
:get,
- "https://bitbucket.org/api/1.0/repositories/#{project_identifier}/issues/#{issue[:local_id]}/comments"
+ "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/issues/#{issue[:id]}/comments?pagelen=50&sort=created_on"
).to_return(
status: 200,
- body: [{ author_info: { username: "username" }, utc_created_on: index }].to_json
+ headers: { "Content-Type" => "application/json" },
+ body: { author_info: { username: "username" }, utc_created_on: index }.to_json
)
end
+
+ stub_request(
+ :get,
+ "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/pullrequests?pagelen=50&sort=created_on&state=ALL"
+ ).to_return(status: 200,
+ headers: { "Content-Type" => "application/json" },
+ body: {}.to_json)
end
- it 'map statuses to open or closed' do
+ it 'maps statuses to open or closed' do
importer.execute
expect(project.issues.where(state: "closed").size).to eq(5)
expect(project.issues.where(state: "opened").size).to eq(2)
end
+
+ it 'calls import_wiki' do
+ expect(importer).to receive(:import_wiki)
+ importer.execute
+ end
end
end
diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
index e1c60e07b4d..773d0d4d288 100644
--- a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
@@ -2,14 +2,19 @@ require 'spec_helper'
describe Gitlab::BitbucketImport::ProjectCreator, lib: true do
let(:user) { create(:user) }
+
let(:repo) do
- {
- name: 'Vim',
- slug: 'vim',
- is_private: true,
- owner: "asd"
- }.with_indifferent_access
+ double(name: 'Vim',
+ slug: 'vim',
+ description: 'Test repo',
+ is_private: true,
+ owner: "asd",
+ full_name: 'Vim repo',
+ visibility_level: Gitlab::VisibilityLevel::PRIVATE,
+ clone_url: 'ssh://git@bitbucket.org/asd/vim.git',
+ has_wiki?: false)
end
+
let(:namespace){ create(:group, owner: user) }
let(:token) { "asdasd12345" }
let(:secret) { "sekrettt" }
@@ -22,7 +27,7 @@ describe Gitlab::BitbucketImport::ProjectCreator, lib: true do
it 'creates project' do
allow_any_instance_of(Project).to receive(:add_import_job)
- project_creator = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, user, access_params)
+ project_creator = Gitlab::BitbucketImport::ProjectCreator.new(repo, 'vim', namespace, user, access_params)
project = project_creator.execute
expect(project.import_url).to eq("ssh://git@bitbucket.org/asd/vim.git")
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/checks/force_push_spec.rb b/spec/lib/gitlab/checks/force_push_spec.rb
new file mode 100644
index 00000000000..f6288011494
--- /dev/null
+++ b/spec/lib/gitlab/checks/force_push_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Gitlab::Checks::ChangeAccess, lib: true do
+ let(:project) { create(:project) }
+
+ context "exit code checking" do
+ it "does not raise a runtime error if the `popen` call to git returns a zero exit code" do
+ allow(Gitlab::Popen).to receive(:popen).and_return(['normal output', 0])
+
+ expect { Gitlab::Checks::ForcePush.force_push?(project, 'oldrev', 'newrev') }.not_to raise_error
+ end
+
+ it "raises a runtime error if the `popen` call to git returns a non-zero exit code" do
+ allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1])
+
+ expect { Gitlab::Checks::ForcePush.force_push?(project, 'oldrev', 'newrev') }.to raise_error(RuntimeError)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
new file mode 100644
index 00000000000..b3c07347de1
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Build::Cancelable do
+ let(:status) { double('core status') }
+ let(:user) { double('user') }
+
+ subject do
+ described_class.new(status)
+ end
+
+ describe '#text' do
+ it 'does not override status text' do
+ expect(status).to receive(:text)
+
+ subject.text
+ end
+ end
+
+ describe '#icon' do
+ it 'does not override status icon' do
+ expect(status).to receive(:icon)
+
+ subject.icon
+ end
+ end
+
+ describe '#label' do
+ it 'does not override status label' do
+ expect(status).to receive(:label)
+
+ subject.label
+ end
+ end
+
+ describe '#group' do
+ it 'does not override status group' do
+ expect(status).to receive(:group)
+
+ subject.group
+ end
+ end
+
+ describe 'action details' do
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build) }
+ let(:status) { Gitlab::Ci::Status::Core.new(build, user) }
+
+ describe '#has_action?' do
+ context 'when user is allowed to update build' do
+ before { build.project.team << [user, :developer] }
+
+ it { is_expected.to have_action }
+ end
+
+ context 'when user is not allowed to update build' do
+ it { is_expected.not_to have_action }
+ end
+ end
+
+ describe '#action_path' do
+ it { expect(subject.action_path).to include "#{build.id}/cancel" }
+ end
+
+ describe '#action_icon' do
+ it { expect(subject.action_icon).to eq 'ban' }
+ end
+
+ describe '#action_title' do
+ it { expect(subject.action_title).to eq 'Cancel' }
+ end
+ end
+
+ describe '.matches?' do
+ subject { described_class.matches?(build, user) }
+
+ context 'when build is cancelable' do
+ let(:build) do
+ create(:ci_build, :running)
+ end
+
+ it 'is a correct match' do
+ expect(subject).to be true
+ end
+ end
+
+ context 'when build is not cancelable' do
+ let(:build) { create(:ci_build, :success) }
+
+ it 'does not match' do
+ expect(subject).to be false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/build/common_spec.rb b/spec/lib/gitlab/ci/status/build/common_spec.rb
new file mode 100644
index 00000000000..40b96b1807b
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/common_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Build::Common do
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build) }
+ let(:project) { build.project }
+
+ subject do
+ Gitlab::Ci::Status::Core
+ .new(build, user)
+ .extend(described_class)
+ end
+
+ describe '#has_action?' do
+ it { is_expected.not_to have_action }
+ end
+
+ describe '#has_details?' do
+ context 'when user has access to read build' do
+ before { project.team << [user, :developer] }
+
+ it { is_expected.to have_details }
+ end
+
+ context 'when user does not have access to read build' do
+ before { project.update(public_builds: false) }
+
+ it { is_expected.not_to have_details }
+ end
+ end
+
+ describe '#details_path' do
+ it 'links to the build details page' do
+ expect(subject.details_path).to include "builds/#{build.id}"
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb
new file mode 100644
index 00000000000..dccb29b5ef6
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb
@@ -0,0 +1,141 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Build::Factory do
+ let(:user) { create(:user) }
+ let(:project) { build.project }
+
+ subject { described_class.new(build, user) }
+ let(:status) { subject.fabricate! }
+
+ before { project.team << [user, :developer] }
+
+ context 'when build is successful' do
+ let(:build) { create(:ci_build, :success) }
+
+ it 'fabricates a retryable build status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'passed'
+ expect(status.icon).to eq 'icon_status_success'
+ expect(status.label).to eq 'passed'
+ expect(status).to have_details
+ expect(status).to have_action
+ end
+ end
+
+ context 'when build is failed' do
+ let(:build) { create(:ci_build, :failed) }
+
+ it 'fabricates a retryable build status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'failed'
+ expect(status.icon).to eq 'icon_status_failed'
+ expect(status.label).to eq 'failed'
+ expect(status).to have_details
+ expect(status).to have_action
+ end
+ end
+
+ context 'when build is a canceled' do
+ let(:build) { create(:ci_build, :canceled) }
+
+ it 'fabricates a retryable build status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'canceled'
+ expect(status.icon).to eq 'icon_status_canceled'
+ expect(status.label).to eq 'canceled'
+ expect(status).to have_details
+ expect(status).to have_action
+ end
+ end
+
+ context 'when build is running' do
+ let(:build) { create(:ci_build, :running) }
+
+ it 'fabricates a canceable build status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'running'
+ expect(status.icon).to eq 'icon_status_running'
+ expect(status.label).to eq 'running'
+ expect(status).to have_details
+ expect(status).to have_action
+ end
+ end
+
+ context 'when build is pending' do
+ let(:build) { create(:ci_build, :pending) }
+
+ it 'fabricates a cancelable build status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'pending'
+ expect(status.icon).to eq 'icon_status_pending'
+ expect(status.label).to eq 'pending'
+ expect(status).to have_details
+ expect(status).to have_action
+ end
+ end
+
+ context 'when build is skipped' do
+ let(:build) { create(:ci_build, :skipped) }
+
+ it 'fabricates a core skipped status' do
+ expect(status).to be_a Gitlab::Ci::Status::Skipped
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'skipped'
+ expect(status.icon).to eq 'icon_status_skipped'
+ expect(status.label).to eq 'skipped'
+ expect(status).to have_details
+ expect(status).not_to have_action
+ end
+ end
+
+ context 'when build is a manual action' do
+ context 'when build is a play action' do
+ let(:build) { create(:ci_build, :playable) }
+
+ it 'fabricates a core skipped status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Play
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'manual'
+ expect(status.icon).to eq 'icon_status_manual'
+ expect(status.label).to eq 'manual play action'
+ expect(status).to have_details
+ expect(status).to have_action
+ end
+ end
+
+ context 'when build is an environment stop action' do
+ let(:build) { create(:ci_build, :playable, :teardown_environment) }
+
+ it 'fabricates a core skipped status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Stop
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'manual'
+ expect(status.icon).to eq 'icon_status_manual'
+ expect(status.label).to eq 'manual stop action'
+ expect(status).to have_details
+ expect(status).to have_action
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb
new file mode 100644
index 00000000000..f1b50a59ce6
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/play_spec.rb
@@ -0,0 +1,86 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Build::Play do
+ let(:status) { double('core') }
+ let(:user) { double('user') }
+
+ subject { described_class.new(status) }
+
+ describe '#text' do
+ it { expect(subject.text).to eq 'manual' }
+ end
+
+ describe '#label' do
+ it { expect(subject.label).to eq 'manual play action' }
+ end
+
+ describe '#icon' do
+ it { expect(subject.icon).to eq 'icon_status_manual' }
+ end
+
+ describe '#group' do
+ it { expect(subject.group).to eq 'manual' }
+ end
+
+ describe 'action details' do
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build) }
+ let(:status) { Gitlab::Ci::Status::Core.new(build, user) }
+
+ describe '#has_action?' do
+ context 'when user is allowed to update build' do
+ before { build.project.team << [user, :developer] }
+
+ it { is_expected.to have_action }
+ end
+
+ context 'when user is not allowed to update build' do
+ it { is_expected.not_to have_action }
+ end
+ end
+
+ describe '#action_path' do
+ it { expect(subject.action_path).to include "#{build.id}/play" }
+ end
+
+ describe '#action_icon' do
+ it { expect(subject.action_icon).to eq 'play' }
+ end
+
+ describe '#action_title' do
+ it { expect(subject.action_title).to eq 'Play' }
+ end
+ end
+
+ describe '.matches?' do
+ subject { described_class.matches?(build, user) }
+
+ context 'when build is playable' do
+ context 'when build stops an environment' do
+ let(:build) do
+ create(:ci_build, :playable, :teardown_environment)
+ end
+
+ it 'does not match' do
+ expect(subject).to be false
+ end
+ end
+
+ context 'when build does not stop an environment' do
+ let(:build) { create(:ci_build, :playable) }
+
+ it 'is a correct match' do
+ expect(subject).to be true
+ end
+ end
+ end
+
+ context 'when build is not playable' do
+ let(:build) { create(:ci_build) }
+
+ it 'does not match' do
+ expect(subject).to be false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/build/retryable_spec.rb b/spec/lib/gitlab/ci/status/build/retryable_spec.rb
new file mode 100644
index 00000000000..62036f8ec5d
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/retryable_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Build::Retryable do
+ let(:status) { double('core status') }
+ let(:user) { double('user') }
+
+ subject do
+ described_class.new(status)
+ end
+
+ describe '#text' do
+ it 'does not override status text' do
+ expect(status).to receive(:text)
+
+ subject.text
+ end
+ end
+
+ describe '#icon' do
+ it 'does not override status icon' do
+ expect(status).to receive(:icon)
+
+ subject.icon
+ end
+ end
+
+ describe '#label' do
+ it 'does not override status label' do
+ expect(status).to receive(:label)
+
+ subject.label
+ end
+ end
+
+ describe '#group' do
+ it 'does not override status group' do
+ expect(status).to receive(:group)
+
+ subject.group
+ end
+ end
+
+ describe 'action details' do
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build) }
+ let(:status) { Gitlab::Ci::Status::Core.new(build, user) }
+
+ describe '#has_action?' do
+ context 'when user is allowed to update build' do
+ before { build.project.team << [user, :developer] }
+
+ it { is_expected.to have_action }
+ end
+
+ context 'when user is not allowed to update build' do
+ it { is_expected.not_to have_action }
+ end
+ end
+
+ describe '#action_path' do
+ it { expect(subject.action_path).to include "#{build.id}/retry" }
+ end
+
+ describe '#action_icon' do
+ it { expect(subject.action_icon).to eq 'refresh' }
+ end
+
+ describe '#action_title' do
+ it { expect(subject.action_title).to eq 'Retry' }
+ end
+ end
+
+ describe '.matches?' do
+ subject { described_class.matches?(build, user) }
+
+ context 'when build is retryable' do
+ let(:build) do
+ create(:ci_build, :success)
+ end
+
+ it 'is a correct match' do
+ expect(subject).to be true
+ end
+ end
+
+ context 'when build is not retryable' do
+ let(:build) { create(:ci_build, :running) }
+
+ it 'does not match' do
+ expect(subject).to be false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb
new file mode 100644
index 00000000000..597e02e86e4
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb
@@ -0,0 +1,88 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Build::Stop do
+ let(:status) { double('core status') }
+ let(:user) { double('user') }
+
+ subject do
+ described_class.new(status)
+ end
+
+ describe '#text' do
+ it { expect(subject.text).to eq 'manual' }
+ end
+
+ describe '#label' do
+ it { expect(subject.label).to eq 'manual stop action' }
+ end
+
+ describe '#icon' do
+ it { expect(subject.icon).to eq 'icon_status_manual' }
+ end
+
+ describe '#group' do
+ it { expect(subject.group).to eq 'manual' }
+ end
+
+ describe 'action details' do
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build) }
+ let(:status) { Gitlab::Ci::Status::Core.new(build, user) }
+
+ describe '#has_action?' do
+ context 'when user is allowed to update build' do
+ before { build.project.team << [user, :developer] }
+
+ it { is_expected.to have_action }
+ end
+
+ context 'when user is not allowed to update build' do
+ it { is_expected.not_to have_action }
+ end
+ end
+
+ describe '#action_path' do
+ it { expect(subject.action_path).to include "#{build.id}/play" }
+ end
+
+ describe '#action_icon' do
+ it { expect(subject.action_icon).to eq 'stop' }
+ end
+
+ describe '#action_title' do
+ it { expect(subject.action_title).to eq 'Stop' }
+ end
+ end
+
+ describe '.matches?' do
+ subject { described_class.matches?(build, user) }
+
+ context 'when build is playable' do
+ context 'when build stops an environment' do
+ let(:build) do
+ create(:ci_build, :playable, :teardown_environment)
+ end
+
+ it 'is a correct match' do
+ expect(subject).to be true
+ end
+ end
+
+ context 'when build does not stop an environment' do
+ let(:build) { create(:ci_build, :playable) }
+
+ it 'does not match' do
+ expect(subject).to be false
+ end
+ end
+ end
+
+ context 'when build is not playable' do
+ let(:build) { create(:ci_build) }
+
+ it 'does not match' do
+ expect(subject).to be false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb
index 619ecbcba67..38412fe2e4f 100644
--- a/spec/lib/gitlab/ci/status/canceled_spec.rb
+++ b/spec/lib/gitlab/ci/status/canceled_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Canceled do
- subject { described_class.new(double('subject')) }
+ subject do
+ described_class.new(double('subject'), double('user'))
+ end
describe '#text' do
it { expect(subject.label).to eq 'canceled' }
@@ -15,7 +17,7 @@ describe Gitlab::Ci::Status::Canceled do
it { expect(subject.icon).to eq 'icon_status_canceled' }
end
- describe '#title' do
- it { expect(subject.title).to eq 'Double: canceled' }
+ describe '#group' do
+ it { expect(subject.group).to eq 'canceled' }
end
end
diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb
index 157302c65a8..6d847484693 100644
--- a/spec/lib/gitlab/ci/status/created_spec.rb
+++ b/spec/lib/gitlab/ci/status/created_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Created do
- subject { described_class.new(double('subject')) }
+ subject do
+ described_class.new(double('subject'), double('user'))
+ end
describe '#text' do
it { expect(subject.label).to eq 'created' }
@@ -15,7 +17,7 @@ describe Gitlab::Ci::Status::Created do
it { expect(subject.icon).to eq 'icon_status_created' }
end
- describe '#title' do
- it { expect(subject.title).to eq 'Double: created' }
+ describe '#group' do
+ it { expect(subject.group).to eq 'created' }
end
end
diff --git a/spec/lib/gitlab/ci/status/extended_spec.rb b/spec/lib/gitlab/ci/status/extended_spec.rb
index 120e121aae5..c2d74ca5cde 100644
--- a/spec/lib/gitlab/ci/status/extended_spec.rb
+++ b/spec/lib/gitlab/ci/status/extended_spec.rb
@@ -2,11 +2,11 @@ require 'spec_helper'
describe Gitlab::Ci::Status::Extended do
subject do
- Class.new.extend(described_class)
+ Class.new.include(described_class)
end
it 'requires subclass to implement matcher' do
- expect { subject.matches?(double) }
+ expect { subject.matches?(double, double) }
.to raise_error(NotImplementedError)
end
end
diff --git a/spec/lib/gitlab/ci/status/factory_spec.rb b/spec/lib/gitlab/ci/status/factory_spec.rb
index d5bd7f7102b..f92a1c149bf 100644
--- a/spec/lib/gitlab/ci/status/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/factory_spec.rb
@@ -2,15 +2,17 @@ require 'spec_helper'
describe Gitlab::Ci::Status::Factory do
subject do
- described_class.new(object)
+ described_class.new(resource, user)
end
+ let(:user) { create(:user) }
+
let(:status) { subject.fabricate! }
context 'when object has a core status' do
HasStatus::AVAILABLE_STATUSES.each do |core_status|
context "when core status is #{core_status}" do
- let(:object) { double(status: core_status) }
+ let(:resource) { double(status: core_status) }
it "fabricates a core status #{core_status}" do
expect(status).to be_a(
diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb
index 0b3cb8168e6..990d686d22c 100644
--- a/spec/lib/gitlab/ci/status/failed_spec.rb
+++ b/spec/lib/gitlab/ci/status/failed_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Failed do
- subject { described_class.new(double('subject')) }
+ subject do
+ described_class.new(double('subject'), double('user'))
+ end
describe '#text' do
it { expect(subject.label).to eq 'failed' }
@@ -15,7 +17,7 @@ describe Gitlab::Ci::Status::Failed do
it { expect(subject.icon).to eq 'icon_status_failed' }
end
- describe '#title' do
- it { expect(subject.title).to eq 'Double: failed' }
+ describe '#group' do
+ it { expect(subject.group).to eq 'failed' }
end
end
diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb
index 57c901c1202..7bb6579c317 100644
--- a/spec/lib/gitlab/ci/status/pending_spec.rb
+++ b/spec/lib/gitlab/ci/status/pending_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Pending do
- subject { described_class.new(double('subject')) }
+ subject do
+ described_class.new(double('subject'), double('user'))
+ end
describe '#text' do
it { expect(subject.label).to eq 'pending' }
@@ -15,7 +17,7 @@ describe Gitlab::Ci::Status::Pending do
it { expect(subject.icon).to eq 'icon_status_pending' }
end
- describe '#title' do
- it { expect(subject.title).to eq 'Double: pending' }
+ describe '#group' do
+ it { expect(subject.group).to eq 'pending' }
end
end
diff --git a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb
index 21adee3f8e7..d665674bf70 100644
--- a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb
+++ b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb
@@ -1,23 +1,36 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Pipeline::Common do
- let(:pipeline) { create(:ci_pipeline) }
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :private) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
subject do
- Class.new(Gitlab::Ci::Status::Core)
- .new(pipeline).extend(described_class)
+ Gitlab::Ci::Status::Core
+ .new(pipeline, user)
+ .extend(described_class)
end
- it 'does not have action' do
- expect(subject).not_to have_action
+ describe '#has_action?' do
+ it { is_expected.not_to have_action }
end
- it 'has details' do
- expect(subject).to have_details
+ describe '#has_details?' do
+ context 'when user has access to read pipeline' do
+ before { project.team << [user, :developer] }
+
+ it { is_expected.to have_details }
+ end
+
+ context 'when user does not have access to read pipeline' do
+ it { is_expected.not_to have_details }
+ end
end
- it 'links to the pipeline details page' do
- expect(subject.details_path)
- .to include "pipelines/#{pipeline.id}"
+ describe '#details_path' do
+ it 'links to the pipeline details page' do
+ expect(subject.details_path)
+ .to include "pipelines/#{pipeline.id}"
+ end
end
end
diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
index d6243940f2e..d4a2dc7fcc1 100644
--- a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
@@ -1,14 +1,21 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Pipeline::Factory do
+ let(:user) { create(:user) }
+ let(:project) { pipeline.project }
+
subject do
- described_class.new(pipeline)
+ described_class.new(pipeline, user)
end
let(:status) do
subject.fabricate!
end
+ before do
+ project.team << [user, :developer]
+ end
+
context 'when pipeline has a core status' do
HasStatus::AVAILABLE_STATUSES.each do |core_status|
context "when core status is #{core_status}" do
diff --git a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb
index 02e526e3de2..979160eb9c4 100644
--- a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb
+++ b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb
@@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do
it { expect(subject.icon).to eq 'icon_status_warning' }
end
+ describe '#group' do
+ it { expect(subject.group).to eq 'success_with_warnings' }
+ end
+
describe '.matches?' do
context 'when pipeline is successful' do
let(:pipeline) do
@@ -29,13 +33,13 @@ describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do
end
it 'is a correct match' do
- expect(described_class.matches?(pipeline)).to eq true
+ expect(described_class.matches?(pipeline, double)).to eq true
end
end
context 'when pipeline does not have warnings' do
it 'does not match' do
- expect(described_class.matches?(pipeline)).to eq false
+ expect(described_class.matches?(pipeline, double)).to eq false
end
end
end
@@ -51,13 +55,13 @@ describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do
end
it 'does not match' do
- expect(described_class.matches?(pipeline)).to eq false
+ expect(described_class.matches?(pipeline, double)).to eq false
end
end
context 'when pipeline does not have warnings' do
it 'does not match' do
- expect(described_class.matches?(pipeline)).to eq false
+ expect(described_class.matches?(pipeline, double)).to eq false
end
end
end
diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb
index c023f1872cc..852d6c06baf 100644
--- a/spec/lib/gitlab/ci/status/running_spec.rb
+++ b/spec/lib/gitlab/ci/status/running_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Running do
- subject { described_class.new(double('subject')) }
+ subject do
+ described_class.new(double('subject'), double('user'))
+ end
describe '#text' do
it { expect(subject.label).to eq 'running' }
@@ -15,7 +17,7 @@ describe Gitlab::Ci::Status::Running do
it { expect(subject.icon).to eq 'icon_status_running' }
end
- describe '#title' do
- it { expect(subject.title).to eq 'Double: running' }
+ describe '#group' do
+ it { expect(subject.group).to eq 'running' }
end
end
diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb
index d4f7f4b3b70..e00b356a24b 100644
--- a/spec/lib/gitlab/ci/status/skipped_spec.rb
+++ b/spec/lib/gitlab/ci/status/skipped_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Skipped do
- subject { described_class.new(double('subject')) }
+ subject do
+ described_class.new(double('subject'), double('user'))
+ end
describe '#text' do
it { expect(subject.label).to eq 'skipped' }
@@ -15,7 +17,7 @@ describe Gitlab::Ci::Status::Skipped do
it { expect(subject.icon).to eq 'icon_status_skipped' }
end
- describe '#title' do
- it { expect(subject.title).to eq 'Double: skipped' }
+ describe '#group' do
+ it { expect(subject.group).to eq 'skipped' }
end
end
diff --git a/spec/lib/gitlab/ci/status/stage/common_spec.rb b/spec/lib/gitlab/ci/status/stage/common_spec.rb
index f3259c6f23e..8814a7614a0 100644
--- a/spec/lib/gitlab/ci/status/stage/common_spec.rb
+++ b/spec/lib/gitlab/ci/status/stage/common_spec.rb
@@ -1,26 +1,43 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Stage::Common do
- let(:pipeline) { create(:ci_empty_pipeline) }
- let(:stage) { build(:ci_stage, pipeline: pipeline, name: 'test') }
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+ let(:stage) do
+ build(:ci_stage, pipeline: pipeline, name: 'test')
+ end
subject do
Class.new(Gitlab::Ci::Status::Core)
- .new(stage).extend(described_class)
+ .new(stage, user).extend(described_class)
end
it 'does not have action' do
expect(subject).not_to have_action
end
- it 'has details' do
- expect(subject).to have_details
- end
-
it 'links to the pipeline details page' do
expect(subject.details_path)
.to include "pipelines/#{pipeline.id}"
expect(subject.details_path)
.to include "##{stage.name}"
end
+
+ context 'when user has permission to read pipeline' do
+ before do
+ project.team << [user, :master]
+ end
+
+ it 'has details' do
+ expect(subject).to have_details
+ end
+ end
+
+ context 'when user does not have permission to read pipeline' do
+ it 'does not have details' do
+ expect(subject).not_to have_details
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
index 17929665c83..6f8721d30c2 100644
--- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
@@ -1,17 +1,26 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Stage::Factory do
- let(:pipeline) { create(:ci_empty_pipeline) }
- let(:stage) { build(:ci_stage, pipeline: pipeline, name: 'test') }
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+ let(:stage) do
+ build(:ci_stage, pipeline: pipeline, name: 'test')
+ end
subject do
- described_class.new(stage)
+ described_class.new(stage, user)
end
let(:status) do
subject.fabricate!
end
+ before do
+ project.team << [user, :developer]
+ end
+
context 'when stage has a core status' do
HasStatus::AVAILABLE_STATUSES.each do |core_status|
context "when core status is #{core_status}" do
diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb
index 9e261a3aa5f..4a89e1faf40 100644
--- a/spec/lib/gitlab/ci/status/success_spec.rb
+++ b/spec/lib/gitlab/ci/status/success_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Success do
- subject { described_class.new(double('subject')) }
+ subject do
+ described_class.new(double('subject'), double('user'))
+ end
describe '#text' do
it { expect(subject.label).to eq 'passed' }
@@ -15,7 +17,7 @@ describe Gitlab::Ci::Status::Success do
it { expect(subject.icon).to eq 'icon_status_success' }
end
- describe '#title' do
- it { expect(subject.title).to eq 'Double: passed' }
+ describe '#group' do
+ it { expect(subject.group).to eq 'success' }
end
end
diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
index d619e401897..f4703dc704f 100644
--- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
@@ -29,7 +29,7 @@ describe Gitlab::Gfm::ReferenceRewriter do
context 'description with ignored elements' do
let(:text) do
"Hi. This references #1, but not `#2`\n" +
- '<pre>and not !1</pre>'
+ '<pre>and not !1</pre>'
end
it { is_expected.to include issue_first.to_reference(new_project) }
diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb
new file mode 100644
index 00000000000..1f9c987be0b
--- /dev/null
+++ b/spec/lib/gitlab/git/rev_list_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+
+describe Gitlab::Git::RevList, lib: true do
+ let(:project) { create(:project) }
+
+ context "validations" do
+ described_class::ALLOWED_VARIABLES.each do |var|
+ context var do
+ it "accepts values starting with the project repo path" do
+ env = { var => "#{project.repository.path_to_repo}/objects" }
+ rev_list = described_class.new('oldrev', 'newrev', project: project, env: env)
+
+ expect(rev_list).to be_valid
+ end
+
+ it "rejects values starting not with the project repo path" do
+ env = { var => "/some/other/path" }
+ rev_list = described_class.new('oldrev', 'newrev', project: project, env: env)
+
+ expect(rev_list).not_to be_valid
+ end
+
+ it "rejects values containing the project repo path but not starting with it" do
+ env = { var => "/some/other/path/#{project.repository.path_to_repo}" }
+ rev_list = described_class.new('oldrev', 'newrev', project: project, env: env)
+
+ 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
+
+ context "#execute" do
+ let(:env) { { "GIT_OBJECT_DIRECTORY" => project.repository.path_to_repo } }
+ let(:rev_list) { Gitlab::Git::RevList.new('oldrev', 'newrev', project: project, env: env) }
+
+ it "calls out to `popen` without environment variables if the record is invalid" do
+ allow(rev_list).to receive(:valid?).and_return(false)
+
+ expect(Open3).to receive(:popen3).with(hash_excluding(env), any_args)
+
+ rev_list.execute
+ end
+
+ it "calls out to `popen` with environment variables if the record is valid" do
+ allow(rev_list).to receive(:valid?).and_return(true)
+
+ expect(Open3).to receive(:popen3).with(hash_including(env), any_args)
+
+ rev_list.execute
+ 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 8e1a28f2723..7e618e2fcf5 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,7 +137,8 @@ project:
- assembla_service
- asana_service
- gemnasium_service
-- slack_service
+- slack_notification_service
+- mattermost_notification_service
- buildkite_service
- bamboo_service
- teamcity_service
@@ -147,6 +149,7 @@ project:
- bugzilla_service
- gitlab_issue_tracker_service
- external_wiki_service
+- kubernetes_service
- forked_project_link
- forked_from_project
- forked_project_links
diff --git a/spec/lib/gitlab/import_export/avatar_restorer_spec.rb b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb
index 5ae178414cc..08a42fd27a2 100644
--- a/spec/lib/gitlab/import_export/avatar_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb
@@ -1,12 +1,14 @@
require 'spec_helper'
describe Gitlab::ImportExport::AvatarRestorer, lib: true do
+ include UploadHelpers
+
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') }
let(:project) { create(:empty_project) }
before do
allow_any_instance_of(described_class).to receive(:avatar_export_file)
- .and_return(Rails.root + "spec/fixtures/dk.png")
+ .and_return(uploaded_image_temp_path)
end
after do
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index ed9df468ced..931d426c87f 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -2517,7 +2517,7 @@
"merge_params": {
"force_remove_source_branch": null
},
- "merge_when_build_succeeds": false,
+ "merge_when_build_succeeds": true,
"merge_user_id": null,
"merge_commit_sha": null,
"deleted_at": null,
@@ -6548,7 +6548,9 @@
"url": null
},
"erased_by_id": null,
- "erased_at": null
+ "erased_at": null,
+ "type": "Ci::Build",
+ "token": "abcd"
},
{
"id": 72,
@@ -7409,6 +7411,28 @@
"category": "common",
"default": false,
"wiki_page_events": true
+ },
+ {
+ "id": 101,
+ "title": "JenkinsDeprecated",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.031Z",
+ "updated_at": "2016-06-14T15:01:51.031Z",
+ "active": false,
+ "properties": {
+
+ },
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "build_events": true,
+ "category": "common",
+ "default": false,
+ "wiki_page_events": true,
+ "type": "JenkinsDeprecatedService"
}
],
"hooks": [
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index 3038ab53ad8..4b07fa53bf5 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -189,6 +189,14 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
end
end
end
+
+ context 'when there is an existing build with build token' do
+ it 'restores project json correctly' do
+ create(:ci_build, token: 'abcd')
+
+ expect(restored_project_json).to be true
+ end
+ end
end
end
end
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/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb
new file mode 100644
index 00000000000..ab1ab22795c
--- /dev/null
+++ b/spec/lib/gitlab/middleware/multipart_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+require 'tempfile'
+
+describe Gitlab::Middleware::Multipart do
+ let(:app) { double(:app) }
+ let(:middleware) { described_class.new(app) }
+
+ it 'opens top-level files' do
+ Tempfile.open('top-level') do |tempfile|
+ env = post_env({ 'file' => tempfile.path }, { 'file.name' => 'filename' }, Gitlab::Workhorse.secret, 'gitlab-workhorse')
+
+ expect(app).to receive(:call) do |env|
+ file = Rack::Request.new(env).params['file']
+ expect(file).to be_a(File)
+ expect(file.path).to eq(tempfile.path)
+ end
+
+ middleware.call(env)
+ end
+ end
+
+ it 'rejects headers signed with the wrong secret' do
+ env = post_env({ 'file' => '/var/empty/nonesuch' }, {}, 'x' * 32, 'gitlab-workhorse')
+
+ expect { middleware.call(env) }.to raise_error(JWT::VerificationError)
+ end
+
+ it 'rejects headers signed with the wrong issuer' do
+ env = post_env({ 'file' => '/var/empty/nonesuch' }, {}, Gitlab::Workhorse.secret, 'acme-inc')
+
+ expect { middleware.call(env) }.to raise_error(JWT::InvalidIssuerError)
+ end
+
+ it 'opens files one level deep' do
+ Tempfile.open('one-level') do |tempfile|
+ in_params = { 'user' => { 'avatar' => { '.name' => 'filename' } } }
+ env = post_env({ 'user[avatar]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse')
+
+ expect(app).to receive(:call) do |env|
+ file = Rack::Request.new(env).params['user']['avatar']
+ expect(file).to be_a(File)
+ expect(file.path).to eq(tempfile.path)
+ end
+
+ middleware.call(env)
+ end
+ end
+
+ it 'opens files two levels deep' do
+ Tempfile.open('two-levels') do |tempfile|
+ in_params = { 'project' => { 'milestone' => { 'themesong' => { '.name' => 'filename' } } } }
+ env = post_env({ 'project[milestone][themesong]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse')
+
+ expect(app).to receive(:call) do |env|
+ file = Rack::Request.new(env).params['project']['milestone']['themesong']
+ expect(file).to be_a(File)
+ expect(file.path).to eq(tempfile.path)
+ end
+
+ middleware.call(env)
+ end
+ end
+
+ def post_env(rewritten_fields, params, secret, issuer)
+ token = JWT.encode({ 'iss' => issuer, 'rewritten_fields' => rewritten_fields }, secret, 'HS256')
+ Rack::MockRequest.env_for(
+ '/',
+ method: 'post',
+ params: params,
+ described_class::RACK_ENV_KEY => token
+ )
+ end
+end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 3cd9863ec6a..14ee386dba6 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -149,4 +149,33 @@ describe Gitlab::ProjectSearchResults, lib: true do
expect(results.issues_count).to eq 3
end
end
+
+ describe 'notes search' do
+ it 'lists notes' do
+ project = create(:empty_project, :public)
+ note = create(:note, project: project)
+
+ results = described_class.new(user, project, note.note)
+
+ expect(results.objects('notes')).to include note
+ end
+
+ it "doesn't list issue notes when access is restricted" do
+ project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
+ note = create(:note_on_issue, project: project)
+
+ results = described_class.new(user, project, note.note)
+
+ expect(results.objects('notes')).not_to include note
+ end
+
+ it "doesn't list merge_request notes when access is restricted" do
+ project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE)
+ note = create(:note_on_merge_request, project: project)
+
+ results = described_class.new(user, project, note.note)
+
+ expect(results.objects('notes')).not_to include note
+ end
+ end
end
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index c51b10bdc69..c78cd30157e 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -29,4 +29,20 @@ describe Gitlab::Regex, lib: true do
describe 'file path regex' do
it { expect('foo@/bar').to match(Gitlab::Regex.file_path_regex) }
end
+
+ describe 'environment slug regex' do
+ def be_matched
+ match(Gitlab::Regex.environment_slug_regex)
+ end
+
+ it { expect('foo').to be_matched }
+ it { expect('foo-1').to be_matched }
+
+ it { expect('FOO').not_to be_matched }
+ it { expect('foo/1').not_to be_matched }
+ it { expect('foo.1').not_to be_matched }
+ it { expect('foo*1').not_to be_matched }
+ it { expect('9foo').not_to be_matched }
+ it { expect('foo-').not_to be_matched }
+ end
end
diff --git a/spec/lib/gitlab/routing_spec.rb b/spec/lib/gitlab/routing_spec.rb
new file mode 100644
index 00000000000..01d5acfc15b
--- /dev/null
+++ b/spec/lib/gitlab/routing_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Gitlab::Routing do
+ context 'when module is included' do
+ subject do
+ Class.new.include(described_class).new
+ end
+
+ it 'makes it possible to access url helpers' do
+ expect(subject).to respond_to(:namespace_project_path)
+ end
+ end
+
+ context 'when module is not included' do
+ subject do
+ Class.new.include(described_class.url_helpers).new
+ end
+
+ it 'exposes url helpers module through a method' do
+ expect(subject).to respond_to(:namespace_project_path)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/serialize/ci/variables_spec.rb b/spec/lib/gitlab/serialize/ci/variables_spec.rb
new file mode 100644
index 00000000000..7ea74da5252
--- /dev/null
+++ b/spec/lib/gitlab/serialize/ci/variables_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe Gitlab::Serialize::Ci::Variables do
+ subject do
+ described_class.load(described_class.dump(object))
+ end
+
+ let(:object) do
+ [{ key: :key, value: 'value', public: true },
+ { key: 'wee', value: 1, public: false }]
+ end
+
+ it 'converts keys into strings' do
+ is_expected.to eq([
+ { key: 'key', value: 'value', public: true },
+ { key: 'wee', value: 1, public: false }])
+ end
+end
diff --git a/spec/lib/gitlab/sql/union_spec.rb b/spec/lib/gitlab/sql/union_spec.rb
index 0cdbab87544..849edb09476 100644
--- a/spec/lib/gitlab/sql/union_spec.rb
+++ b/spec/lib/gitlab/sql/union_spec.rb
@@ -1,16 +1,26 @@
require 'spec_helper'
describe Gitlab::SQL::Union, lib: true do
+ let(:relation_1) { User.where(email: 'alice@example.com').select(:id) }
+ let(:relation_2) { User.where(email: 'bob@example.com').select(:id) }
+
+ def to_sql(relation)
+ relation.reorder(nil).to_sql
+ end
+
describe '#to_sql' do
it 'returns a String joining relations together using a UNION' do
- rel1 = User.where(email: 'alice@example.com')
- rel2 = User.where(email: 'bob@example.com')
- union = described_class.new([rel1, rel2])
+ union = described_class.new([relation_1, relation_2])
+
+ expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}")
+ end
- sql1 = rel1.reorder(nil).to_sql
- sql2 = rel2.reorder(nil).to_sql
+ it 'skips Model.none segements' do
+ empty_relation = User.none
+ union = described_class.new([empty_relation, relation_1, relation_2])
- expect(union.to_sql).to eq("#{sql1}\nUNION\n#{sql2}")
+ expect{User.where("users.id IN (#{union.to_sql})").to_a}.not_to raise_error
+ expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}")
end
end
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/session_spec.rb b/spec/lib/mattermost/session_spec.rb
new file mode 100644
index 00000000000..3c2eddbd221
--- /dev/null
+++ b/spec/lib/mattermost/session_spec.rb
@@ -0,0 +1,99 @@
+require 'spec_helper'
+
+describe Mattermost::Session, type: :request do
+ let(:user) { create(:user) }
+
+ let(:gitlab_url) { "http://gitlab.com" }
+ let(:mattermost_url) { "http://mattermost.com" }
+
+ subject { described_class.new(user) }
+
+ # Needed for doorkeeper to function
+ it { is_expected.to respond_to(:current_resource_owner) }
+ it { is_expected.to respond_to(:request) }
+ it { is_expected.to respond_to(:authorization) }
+ it { is_expected.to respond_to(:strategy) }
+
+ before do
+ described_class.base_uri(mattermost_url)
+ end
+
+ describe '#with session' do
+ let(:location) { 'http://location.tld' }
+ let!(:stub) do
+ WebMock.stub_request(:get, "#{mattermost_url}/api/v3/oauth/gitlab/login").
+ to_return(headers: { 'location' => location }, status: 307)
+ end
+
+ context 'without oauth uri' do
+ it 'makes a request to the oauth uri' do
+ expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
+ end
+ end
+
+ context 'with oauth_uri' do
+ let!(:doorkeeper) do
+ Doorkeeper::Application.create(
+ name: "GitLab Mattermost",
+ redirect_uri: "#{mattermost_url}/signup/gitlab/complete\n#{mattermost_url}/login/gitlab/complete",
+ scopes: "")
+ end
+
+ context 'without token_uri' do
+ it 'can not create a session' do
+ expect do
+ subject.with_session
+ end.to raise_error(Mattermost::NoSessionError)
+ end
+ end
+
+ context 'with token_uri' do
+ let(:state) { "state" }
+ let(:params) do
+ { response_type: "code",
+ client_id: doorkeeper.uid,
+ redirect_uri: "#{mattermost_url}/signup/gitlab/complete",
+ state: state }
+ end
+ let(:location) do
+ "#{gitlab_url}/oauth/authorize?#{URI.encode_www_form(params)}"
+ end
+
+ before do
+ WebMock.stub_request(:get, "#{mattermost_url}/signup/gitlab/complete").
+ with(query: hash_including({ 'state' => state })).
+ to_return do |request|
+ post "/oauth/token",
+ client_id: doorkeeper.uid,
+ client_secret: doorkeeper.secret,
+ redirect_uri: params[:redirect_uri],
+ grant_type: 'authorization_code',
+ code: request.uri.query_values['code']
+
+ if response.status == 200
+ { headers: { 'token' => 'thisworksnow' }, status: 202 }
+ end
+ end
+
+ WebMock.stub_request(:post, "#{mattermost_url}/api/v3/users/logout").
+ to_return(headers: { Authorization: 'token thisworksnow' }, status: 200)
+ end
+
+ it 'can setup a session' do
+ subject.with_session do |session|
+ end
+
+ expect(subject.token).not_to be_nil
+ end
+
+ it 'returns the value of the block' do
+ result = subject.with_session do |session|
+ "value"
+ end
+
+ expect(result).to eq("value")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
index 52428547a9f..6a93deb5412 100644
--- a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
+++ b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
@@ -1,3 +1,5 @@
+# encoding: utf-8
+
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20161124141322_migrate_process_commit_worker_jobs.rb')
@@ -59,6 +61,10 @@ describe MigrateProcessCommitWorkerJobs do
Sidekiq.redis { |r| r.llen('queue:process_commit') }
end
+ def pop_job
+ JSON.load(Sidekiq.redis { |r| r.lpop('queue:process_commit') })
+ end
+
before do
Sidekiq.redis do |redis|
job = JSON.dump(args: [project.id, user.id, commit.oid])
@@ -92,11 +98,28 @@ describe MigrateProcessCommitWorkerJobs do
expect(job_count).to eq(1)
end
+ it 'encodes data to UTF-8' do
+ allow_any_instance_of(Rugged::Repository).to receive(:lookup).
+ with(commit.oid).
+ and_return(commit)
+
+ allow(commit).to receive(:message).
+ and_return('김치'.force_encoding('BINARY'))
+
+ migration.up
+
+ job = pop_job
+
+ # We don't care so much about what is being stored, instead we just want
+ # to make sure the encoding is right so that JSON encoding the data
+ # doesn't produce any errors.
+ expect(job['args'][2]['message'].encoding).to eq(Encoding::UTF_8)
+ end
+
context 'a migrated job' do
let(:job) do
migration.up
-
- JSON.load(Sidekiq.redis { |r| r.lpop('queue:process_commit') })
+ pop_job
end
let(:commit_hash) do
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index d4970e38f7c..cd3b6d51545 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -87,6 +87,26 @@ describe Ci::Build, models: true do
end
end
+ describe '#persisted_environment' do
+ before do
+ @environment = create(:environment, project: project, name: "foo-#{project.default_branch}")
+ end
+
+ subject { build.persisted_environment }
+
+ context 'referenced literally' do
+ let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}") }
+
+ it { is_expected.to eq(@environment) }
+ end
+
+ context 'referenced with a variable' do
+ let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_BUILD_REF_NAME") }
+
+ it { is_expected.to eq(@environment) }
+ end
+ end
+
describe '#trace' do
it { expect(build.trace).to be_nil }
@@ -254,6 +274,24 @@ describe Ci::Build, models: true do
end
end
+ describe '#ref_slug' do
+ {
+ 'master' => 'master',
+ '1-foo' => '1-foo',
+ 'fix/1-foo' => 'fix-1-foo',
+ 'fix-1-foo' => 'fix-1-foo',
+ 'a' * 63 => 'a' * 63,
+ 'a' * 64 => 'a' * 63,
+ 'FOO' => 'foo',
+ }.each do |ref, slug|
+ it "transforms #{ref} to #{slug}" do
+ build.ref = ref
+
+ expect(build.ref_slug).to eq(slug)
+ end
+ end
+ end
+
describe '#variables' do
let(:container_registry_enabled) { false }
let(:predefined_variables) do
@@ -265,6 +303,7 @@ describe Ci::Build, models: true do
{ key: 'CI_BUILD_REF', value: build.sha, public: true },
{ key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true },
{ key: 'CI_BUILD_REF_NAME', value: 'master', public: true },
+ { key: 'CI_BUILD_REF_SLUG', value: 'master', public: true },
{ key: 'CI_BUILD_NAME', value: 'test', public: true },
{ key: 'CI_BUILD_STAGE', value: 'test', public: true },
{ key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
@@ -309,6 +348,22 @@ describe Ci::Build, models: true do
it { user_variables.each { |v| is_expected.to include(v) } }
end
+ context 'when build has an environment' do
+ before do
+ build.update(environment: 'production')
+ create(:environment, project: build.project, name: 'production', slug: 'prod-slug')
+ end
+
+ let(:environment_variables) do
+ [
+ { key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true },
+ { key: 'CI_ENVIRONMENT_SLUG', value: 'prod-slug', public: true }
+ ]
+ end
+
+ it { environment_variables.each { |v| is_expected.to include(v) } }
+ end
+
context 'when build started manually' do
before do
build.update_attributes(when: :manual)
@@ -403,7 +458,7 @@ describe Ci::Build, models: true do
})
end
let(:variables) do
- [{ key: :KEY, value: 'value', public: true }]
+ [{ key: 'KEY', value: 'value', public: true }]
end
it { is_expected.to eq(predefined_variables + variables) }
@@ -451,6 +506,17 @@ describe Ci::Build, models: true do
it { is_expected.to include({ key: 'CI_RUNNER_TAGS', value: 'docker, linux', public: true }) }
end
+ context 'when build is for a deployment' do
+ let(:deployment_variable) { { key: 'KUBERNETES_TOKEN', value: 'TOKEN', public: false } }
+
+ before do
+ build.environment = 'production'
+ allow(project).to receive(:deployment_variables).and_return([deployment_variable])
+ end
+
+ it { is_expected.to include(deployment_variable) }
+ end
+
context 'returns variables in valid order' do
before do
allow(build).to receive(:predefined_variables) { ['predefined'] }
@@ -899,21 +965,87 @@ describe Ci::Build, models: true do
end
end
+ describe '#cancelable?' do
+ subject { build }
+
+ context 'when build is cancelable' do
+ context 'when build is pending' do
+ it { is_expected.to be_cancelable }
+ end
+
+ context 'when build is running' do
+ before do
+ build.run!
+ end
+
+ it { is_expected.to be_cancelable }
+ end
+ end
+
+ context 'when build is not cancelable' do
+ context 'when build is successful' do
+ before do
+ build.success!
+ end
+
+ it { is_expected.not_to be_cancelable }
+ end
+
+ context 'when build is failed' do
+ before do
+ build.drop!
+ end
+
+ it { is_expected.not_to be_cancelable }
+ end
+ end
+ end
+
describe '#retryable?' do
- context 'when build is running' do
- before do
- build.run!
+ subject { build }
+
+ context 'when build is retryable' do
+ context 'when build is successful' do
+ before do
+ build.success!
+ end
+
+ it { is_expected.to be_retryable }
end
- it { expect(build).not_to be_retryable }
+ context 'when build is failed' do
+ before do
+ build.drop!
+ end
+
+ it { is_expected.to be_retryable }
+ end
+
+ context 'when build is canceled' do
+ before do
+ build.cancel!
+ end
+
+ it { is_expected.to be_retryable }
+ end
end
- context 'when build is finished' do
- before do
- build.success!
+ context 'when build is not retryable' do
+ context 'when build is running' do
+ before do
+ build.run!
+ end
+
+ it { is_expected.not_to be_retryable }
end
- it { expect(build).to be_retryable }
+ context 'when build is skipped' do
+ before do
+ build.skip!
+ end
+
+ it { is_expected.not_to be_retryable }
+ end
end
end
@@ -1174,10 +1306,33 @@ describe Ci::Build, models: true do
describe '#expanded_environment_name' do
subject { build.expanded_environment_name }
- context 'when environment uses variables' do
- let(:build) { create(:ci_build, ref: 'master', environment: 'review/$CI_BUILD_REF_NAME') }
+ context 'when environment uses $CI_BUILD_REF_NAME' do
+ let(:build) do
+ create(:ci_build,
+ ref: 'master',
+ environment: 'review/$CI_BUILD_REF_NAME')
+ end
it { is_expected.to eq('review/master') }
end
+
+ context 'when environment uses yaml_variables containing symbol keys' do
+ let(:build) do
+ create(:ci_build,
+ yaml_variables: [{ key: :APP_HOST, value: 'host' }],
+ environment: 'review/$APP_HOST')
+ end
+
+ it { is_expected.to eq('review/host') }
+ end
+ end
+
+ describe '#detailed_status' do
+ let(:user) { create(:user) }
+
+ it 'returns a detailed status' do
+ expect(build.detailed_status(user))
+ .to be_a Gitlab::Ci::Status::Build::Cancelable
+ end
end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 8158e71dd55..52dd41065e9 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -381,6 +381,65 @@ describe Ci::Pipeline, models: true do
end
end
+ shared_context 'with some outdated pipelines' do
+ before do
+ create_pipeline(:canceled, 'ref', 'A')
+ create_pipeline(:success, 'ref', 'A')
+ create_pipeline(:failed, 'ref', 'B')
+ create_pipeline(:skipped, 'feature', 'C')
+ end
+
+ def create_pipeline(status, ref, sha)
+ create(:ci_empty_pipeline, status: status, ref: ref, sha: sha)
+ end
+ end
+
+ describe '.latest' do
+ include_context 'with some outdated pipelines'
+
+ context 'when no ref is specified' do
+ let(:pipelines) { described_class.latest.all }
+
+ it 'returns the latest pipeline for the same ref and different sha' do
+ expect(pipelines.map(&:sha)).to contain_exactly('A', 'B', 'C')
+ expect(pipelines.map(&:status)).
+ to contain_exactly('success', 'failed', 'skipped')
+ end
+ end
+
+ context 'when ref is specified' do
+ let(:pipelines) { described_class.latest('ref').all }
+
+ it 'returns the latest pipeline for ref and different sha' do
+ expect(pipelines.map(&:sha)).to contain_exactly('A', 'B')
+ expect(pipelines.map(&:status)).
+ to contain_exactly('success', 'failed')
+ end
+ end
+ end
+
+ describe '.latest_status' do
+ include_context 'with some outdated pipelines'
+
+ context 'when no ref is specified' do
+ let(:latest_status) { described_class.latest_status }
+
+ it 'returns the latest status for the same ref and different sha' do
+ expect(latest_status).to eq(described_class.latest.status)
+ expect(latest_status).to eq('failed')
+ end
+ end
+
+ context 'when ref is specified' do
+ let(:latest_status) { described_class.latest_status('ref') }
+
+ it 'returns the latest status for ref and different sha' do
+ expect(latest_status).to eq(described_class.latest_status('ref'))
+ expect(latest_status).to eq('failed')
+ end
+ end
+ end
+
describe '#status' do
let!(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') }
@@ -442,11 +501,15 @@ describe Ci::Pipeline, models: true do
end
describe '#detailed_status' do
+ let(:user) { create(:user) }
+
+ subject { pipeline.detailed_status(user) }
+
context 'when pipeline is created' do
let(:pipeline) { create(:ci_pipeline, status: :created) }
it 'returns detailed status for created pipeline' do
- expect(pipeline.detailed_status.text).to eq 'created'
+ expect(subject.text).to eq 'created'
end
end
@@ -454,7 +517,7 @@ describe Ci::Pipeline, models: true do
let(:pipeline) { create(:ci_pipeline, status: :pending) }
it 'returns detailed status for pending pipeline' do
- expect(pipeline.detailed_status.text).to eq 'pending'
+ expect(subject.text).to eq 'pending'
end
end
@@ -462,7 +525,7 @@ describe Ci::Pipeline, models: true do
let(:pipeline) { create(:ci_pipeline, status: :running) }
it 'returns detailed status for running pipeline' do
- expect(pipeline.detailed_status.text).to eq 'running'
+ expect(subject.text).to eq 'running'
end
end
@@ -470,7 +533,7 @@ describe Ci::Pipeline, models: true do
let(:pipeline) { create(:ci_pipeline, status: :success) }
it 'returns detailed status for successful pipeline' do
- expect(pipeline.detailed_status.text).to eq 'passed'
+ expect(subject.text).to eq 'passed'
end
end
@@ -478,7 +541,7 @@ describe Ci::Pipeline, models: true do
let(:pipeline) { create(:ci_pipeline, status: :failed) }
it 'returns detailed status for failed pipeline' do
- expect(pipeline.detailed_status.text).to eq 'failed'
+ expect(subject.text).to eq 'failed'
end
end
@@ -486,7 +549,7 @@ describe Ci::Pipeline, models: true do
let(:pipeline) { create(:ci_pipeline, status: :canceled) }
it 'returns detailed status for canceled pipeline' do
- expect(pipeline.detailed_status.text).to eq 'canceled'
+ expect(subject.text).to eq 'canceled'
end
end
@@ -494,7 +557,7 @@ describe Ci::Pipeline, models: true do
let(:pipeline) { create(:ci_pipeline, status: :skipped) }
it 'returns detailed status for skipped pipeline' do
- expect(pipeline.detailed_status.text).to eq 'skipped'
+ expect(subject.text).to eq 'skipped'
end
end
@@ -506,7 +569,7 @@ describe Ci::Pipeline, models: true do
end
it 'retruns detailed status for successful pipeline with warnings' do
- expect(pipeline.detailed_status.label).to eq 'passed with warnings'
+ expect(subject.label).to eq 'passed with warnings'
end
end
end
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index f232761dba2..8fff38f7cda 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -68,7 +68,9 @@ describe Ci::Stage, models: true do
end
describe '#detailed_status' do
- subject { stage.detailed_status }
+ let(:user) { create(:user) }
+
+ subject { stage.detailed_status(user) }
context 'when build is created' do
let!(:stage_build) { create_job(:ci_build, status: :created) }
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 0935fc0561c..74b50d2908d 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -213,23 +213,19 @@ eos
end
describe '#status' do
- context 'without arguments for compound status' do
- shared_examples 'giving the status from pipeline' do
- it do
- expect(commit.status).to eq(Ci::Pipeline.status)
- end
- end
-
- context 'with pipelines' do
- let!(:pipeline) do
- create(:ci_empty_pipeline, project: project, sha: commit.sha)
+ context 'without ref argument' do
+ before do
+ %w[success failed created pending].each do |status|
+ create(:ci_empty_pipeline,
+ project: project,
+ sha: commit.sha,
+ status: status)
end
-
- it_behaves_like 'giving the status from pipeline'
end
- context 'without pipelines' do
- it_behaves_like 'giving the status from pipeline'
+ it 'gives compound status from latest pipelines' do
+ expect(commit.status).to eq(Ci::Pipeline.latest_status)
+ expect(commit.status).to eq('pending')
end
end
@@ -255,8 +251,9 @@ eos
expect(commit.status('fix')).to eq(pipeline_from_fix.status)
end
- it 'gives compound status if ref is nil' do
- expect(commit.status(nil)).to eq(commit.status)
+ it 'gives compound status from latest pipelines if ref is nil' do
+ expect(commit.status(nil)).to eq(Ci::Pipeline.latest_status)
+ expect(commit.status(nil)).to eq('failed')
end
end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 1ec08c2a9d0..701f3323c0f 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -234,4 +234,13 @@ describe CommitStatus, models: true do
end
end
end
+
+ describe '#detailed_status' do
+ let(:user) { create(:user) }
+
+ it 'returns a detailed status' do
+ expect(commit_status.detailed_status(user))
+ .to be_a Gitlab::Ci::Status::Success
+ end
+ end
end
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/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index 0acefc0c1d5..b556135532f 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -3,6 +3,10 @@ require 'spec_helper'
describe Group, 'Routable' do
let!(:group) { create(:group) }
+ describe 'Validations' do
+ it { is_expected.to validate_presence_of(:route) }
+ end
+
describe 'Associations' do
it { is_expected.to have_one(:route).dependent(:destroy) }
end
@@ -35,16 +39,16 @@ describe Group, 'Routable' do
it { expect(described_class.find_by_full_path('unknown')).to eq(nil) }
end
- describe '.where_paths_in' do
+ describe '.where_full_path_in' do
context 'without any paths' do
it 'returns an empty relation' do
- expect(described_class.where_paths_in([])).to eq([])
+ expect(described_class.where_full_path_in([])).to eq([])
end
end
context 'without any valid paths' do
it 'returns an empty relation' do
- expect(described_class.where_paths_in(%w[unknown])).to eq([])
+ expect(described_class.where_full_path_in(%w[unknown])).to eq([])
end
end
@@ -52,13 +56,13 @@ describe Group, 'Routable' do
let!(:nested_group) { create(:group, parent: group) }
it 'returns the projects matching the paths' do
- result = described_class.where_paths_in([group.to_param, nested_group.to_param])
+ result = described_class.where_full_path_in([group.to_param, nested_group.to_param])
expect(result).to contain_exactly(group, nested_group)
end
it 'returns projects regardless of the casing of paths' do
- result = described_class.where_paths_in([group.to_param.upcase, nested_group.to_param.upcase])
+ result = described_class.where_full_path_in([group.to_param.upcase, nested_group.to_param.upcase])
expect(result).to contain_exactly(group, nested_group)
end
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index eb64f3d0c83..4b0bfa43abf 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -6,6 +6,7 @@ shared_examples 'TokenAuthenticatable' do
it { expect(described_class).to be_private_method_defined(:write_new_token) }
it { expect(described_class).to respond_to("find_by_#{token_field}") }
it { is_expected.to respond_to("ensure_#{token_field}") }
+ it { is_expected.to respond_to("set_#{token_field}") }
it { is_expected.to respond_to("reset_#{token_field}!") }
end
end
@@ -55,6 +56,12 @@ describe ApplicationSetting, 'TokenAuthenticatable' do
end
end
+ describe 'setting new token' do
+ subject { described_class.new.send("set_#{token_field}", '0123456789') }
+
+ it { is_expected.to eq '0123456789' }
+ end
+
describe 'multiple token fields' do
before do
described_class.send(:add_authentication_token_field, :yet_another_token)
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index c8170164898..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
- let(: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) }
@@ -15,15 +16,11 @@ describe Environment, models: true do
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
it { is_expected.to validate_length_of(:name).is_at_most(255) }
- it { is_expected.to validate_length_of(:external_url).is_at_most(255) }
-
- # To circumvent a not null violation of the name column:
- # https://github.com/thoughtbot/shoulda-matchers/issues/336
- it 'validates uniqueness of :external_url' do
- create(:environment)
+ it { is_expected.to validate_uniqueness_of(:slug).scoped_to(:project_id) }
+ it { is_expected.to validate_length_of(:slug).is_at_most(24) }
- is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id)
- end
+ it { is_expected.to validate_length_of(:external_url).is_at_most(255) }
+ it { is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) }
describe '#nullify_external_url' do
it 'replaces a blank url with nil' do
@@ -35,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
@@ -42,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
@@ -69,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 }
@@ -199,4 +194,89 @@ describe Environment, models: true do
expect(environment.actions_for('review/master')).to contain_exactly(review_action)
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
+ end
+
+ it "is not regenerated if name changes" do
+ original_slug = environment.slug
+ environment.update_attributes!(name: environment.name.reverse)
+
+ expect(environment.slug).to eq(original_slug)
+ end
+ end
+
+ describe '#generate_slug' do
+ SUFFIX = "-[a-z0-9]{6}"
+ {
+ "staging-12345678901234567" => "staging-123456789" + SUFFIX,
+ "9-staging-123456789012345" => "env-9-staging-123" + SUFFIX,
+ "staging-1234567890123456" => "staging-1234567890123456",
+ "production" => "production",
+ "PRODUCTION" => "production" + SUFFIX,
+ "review/1-foo" => "review-1-foo" + SUFFIX,
+ "1-foo" => "env-1-foo" + SUFFIX,
+ "1/foo" => "env-1-foo" + SUFFIX,
+ "foo-" => "foo" + SUFFIX,
+ }.each do |name, matcher|
+ it "returns a slug matching #{matcher}, given #{name}" do
+ slug = described_class.new(name: name).generate_slug
+
+ expect(slug).to match(/\A#{matcher}\z/)
+ end
+ end
+ end
end
diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb
index 615cfe3142b..6004bfdb7b7 100644
--- a/spec/models/generic_commit_status_spec.rb
+++ b/spec/models/generic_commit_status_spec.rb
@@ -1,8 +1,11 @@
require 'spec_helper'
describe GenericCommitStatus, models: true do
- let(:pipeline) { FactoryGirl.create :ci_pipeline }
- let(:generic_commit_status) { FactoryGirl.create :generic_commit_status, pipeline: pipeline }
+ let(:pipeline) { create(:ci_pipeline) }
+
+ let(:generic_commit_status) do
+ create(:generic_commit_status, pipeline: pipeline)
+ end
describe '#context' do
subject { generic_commit_status.context }
@@ -17,6 +20,15 @@ describe GenericCommitStatus, models: true do
it { is_expected.to eq([:external]) }
end
+ describe '#detailed_status' do
+ let(:user) { create(:user) }
+
+ it 'returns detailed status object' do
+ expect(generic_commit_status.detailed_status(user))
+ .to be_a Gitlab::Ci::Status::Success
+ end
+ end
+
describe 'set_default_values' do
before do
generic_commit_status.context = nil
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 1613a586a2c..7d5ecfbaa64 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -50,9 +50,8 @@ describe Group, models: true do
describe 'validations' do
it { is_expected.to validate_presence_of :name }
- it { is_expected.to validate_uniqueness_of(:name) }
+ it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) }
it { is_expected.to validate_presence_of :path }
- it { is_expected.to validate_uniqueness_of(:path) }
it { is_expected.not_to validate_presence_of :owner }
end
@@ -271,4 +270,11 @@ describe Group, models: true do
expect(group.web_url).to include("groups/#{group.name}")
end
end
+
+ describe 'nested group' do
+ subject { build(:group, :nested) }
+
+ it { is_expected.to be_valid }
+ it { expect(subject.parent).to be_kind_of(Group) }
+ end
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 24e216329a9..bb56e44db29 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -22,26 +22,6 @@ describe Issue, models: true do
it { is_expected.to have_db_index(:deleted_at) }
end
- describe '.visible_to_user' do
- let(:user) { create(:user) }
- let(:authorized_user) { create(:user) }
- let(:project) { create(:project, namespace: authorized_user.namespace) }
- let!(:public_issue) { create(:issue, project: project) }
- let!(:confidential_issue) { create(:issue, project: project, confidential: true) }
-
- it 'returns non confidential issues for nil user' do
- expect(Issue.visible_to_user(nil).count).to be(1)
- end
-
- it 'returns non confidential issues for user not authorized for the issues projects' do
- expect(Issue.visible_to_user(user).count).to be(1)
- end
-
- it 'returns all issues for user authorized for the issues projects' do
- expect(Issue.visible_to_user(authorized_user).count).to be(2)
- end
- end
-
describe '#to_reference' do
let(:project) { build(:empty_project, name: 'sample-project') }
let(:issue) { build(:issue, iid: 1, project: project) }
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 8b730be91fd..5da00a8636a 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -205,7 +205,7 @@ describe MergeRequest, models: true do
end
end
- describe "#mr_and_commit_notes" do
+ describe "#related_notes" do
let!(:merge_request) { create(:merge_request) }
before do
@@ -217,7 +217,7 @@ describe MergeRequest, models: true do
it "includes notes for commits" do
expect(merge_request.commits).not_to be_empty
- expect(merge_request.mr_and_commit_notes.count).to eq(2)
+ expect(merge_request.related_notes.count).to eq(2)
end
it "includes notes for commits from target project as well" do
@@ -225,7 +225,7 @@ describe MergeRequest, models: true do
project: merge_request.target_project)
expect(merge_request.commits).not_to be_empty
- expect(merge_request.mr_and_commit_notes.count).to eq(3)
+ expect(merge_request.related_notes.count).to eq(3)
end
end
@@ -252,7 +252,7 @@ describe MergeRequest, models: true do
end
end
- describe 'detection of issues to be closed' do
+ describe '#closes_issues' do
let(:issue0) { create :issue, project: subject.project }
let(:issue1) { create :issue, project: subject.project }
@@ -280,14 +280,19 @@ describe MergeRequest, models: true do
expect(subject.closes_issues).to be_empty
end
+ end
+
+ describe '#issues_mentioned_but_not_closing' do
+ it 'detects issues mentioned in description but not closed' do
+ mentioned_issue = create(:issue, project: subject.project)
+
+ subject.project.team << [subject.author, :developer]
+ subject.description = "Is related to #{mentioned_issue.to_reference}"
- it 'detects issues mentioned in the description' do
- issue2 = create(:issue, project: subject.project)
- subject.description = "Closes #{issue2.to_reference}"
allow(subject.project).to receive(:default_branch).
and_return(subject.target_branch)
- expect(subject.closes_issues).to include(issue2)
+ expect(subject.issues_mentioned_but_not_closing).to match_array([mentioned_issue])
end
end
@@ -410,11 +415,17 @@ describe MergeRequest, models: true do
.to match("Remove all technical debt\n\n")
end
- it 'includes its description in the body' do
- request = build(:merge_request, description: 'By removing all code')
+ it 'includes its closed issues in the body' do
+ issue = create(:issue, project: subject.project)
- expect(request.merge_commit_message)
- .to match("By removing all code\n\n")
+ subject.project.team << [subject.author, :developer]
+ subject.description = "This issue Closes #{issue.to_reference}"
+
+ allow(subject.project).to receive(:default_branch).
+ and_return(subject.target_branch)
+
+ expect(subject.merge_commit_message)
+ .to match("Closes #{issue.to_reference}")
end
it 'includes its reference in the body' do
@@ -429,6 +440,20 @@ describe MergeRequest, models: true do
expect(request.merge_commit_message).not_to match("Title\n\n\n\n")
end
+
+ it 'includes its description in the body' do
+ request = build(:merge_request, description: 'By removing all code')
+
+ expect(request.merge_commit_message(include_description: true))
+ .to match("By removing all code\n\n")
+ end
+
+ it 'does not includes its description in the body' do
+ request = build(:merge_request, description: 'By removing all code')
+
+ expect(request.merge_commit_message)
+ .not_to match("By removing all code\n\n")
+ end
end
describe "#reset_merge_when_build_succeeds" do
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 0cc2efae5f9..064f29d2d66 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -24,8 +24,9 @@ describe Milestone, models: true do
it { is_expected.to have_many(:issues) }
end
- let(:milestone) { create(:milestone) }
- let(:issue) { create(:issue) }
+ let(:project) { create(:project, :public) }
+ let(:milestone) { create(:milestone, project: project) }
+ let(:issue) { create(:issue, project: project) }
let(:user) { create(:user) }
describe "#title" do
@@ -110,8 +111,8 @@ describe Milestone, models: true do
describe :items_count do
before do
- milestone.issues << create(:issue)
- milestone.issues << create(:closed_issue)
+ milestone.issues << create(:issue, project: project)
+ milestone.issues << create(:closed_issue, project: project)
milestone.merge_requests << create(:merge_request)
end
@@ -126,7 +127,7 @@ describe Milestone, models: true do
describe '#total_items_count' do
before do
- create :closed_issue, milestone: milestone
+ create :closed_issue, milestone: milestone, project: project
create :merge_request, milestone: milestone
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 7f82e85563b..9fd06bb6b23 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -6,20 +6,16 @@ describe Namespace, models: true do
it { is_expected.to have_many :projects }
it { is_expected.to validate_presence_of(:name) }
- it { is_expected.to validate_uniqueness_of(:name) }
+ it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) }
it { is_expected.to validate_length_of(:name).is_at_most(255) }
it { is_expected.to validate_length_of(:description).is_at_most(255) }
it { is_expected.to validate_presence_of(:path) }
- it { is_expected.to validate_uniqueness_of(:path) }
it { is_expected.to validate_length_of(:path).is_at_most(255) }
it { is_expected.to validate_presence_of(:owner) }
- describe "Mass assignment" do
- end
-
describe "Respond to" do
it { is_expected.to respond_to(:human_name) }
it { is_expected.to respond_to(:to_param) }
@@ -132,4 +128,26 @@ describe Namespace, models: true do
it { expect(group.full_path).to eq(group.path) }
it { expect(nested_group.full_path).to eq("#{group.path}/#{nested_group.path}") }
end
+
+ describe '#full_name' do
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+
+ it { expect(group.full_name).to eq(group.name) }
+ it { expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") }
+ end
+
+ describe '#parents' do
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+ let(:deep_nested_group) { create(:group, parent: nested_group) }
+ let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
+
+ it 'returns the correct parents' do
+ expect(very_deep_nested_group.parents).to eq([group, nested_group, deep_nested_group])
+ expect(deep_nested_group.parents).to eq([group, nested_group])
+ expect(nested_group.parents).to eq([group])
+ expect(group.parents).to eq([])
+ end
+ end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 17a15b12dcb..310fecd8a5c 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -162,44 +162,6 @@ describe Note, models: true do
end
end
- describe '.search' do
- let(:note) { create(:note_on_issue, note: 'WoW') }
-
- it 'returns notes with matching content' do
- expect(described_class.search(note.note)).to eq([note])
- end
-
- it 'returns notes with matching content regardless of the casing' do
- expect(described_class.search('WOW')).to eq([note])
- end
-
- context "confidential issues" do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
- let(:confidential_note) { create(:note, note: "Random", noteable: confidential_issue, project: confidential_issue.project) }
-
- it "returns notes with matching content if user can see the issue" do
- expect(described_class.search(confidential_note.note, as_user: user)).to eq([confidential_note])
- end
-
- it "does not return notes with matching content if user can not see the issue" do
- user = create(:user)
- expect(described_class.search(confidential_note.note, as_user: user)).to be_empty
- end
-
- it "does not return notes with matching content for project members with guest role" do
- user = create(:user)
- project.team << [user, :guest]
- expect(described_class.search(confidential_note.note, as_user: user)).to be_empty
- end
-
- it "does not return notes with matching content for unauthenticated users" do
- expect(described_class.search(confidential_note.note)).to be_empty
- end
- end
- end
-
describe "editable?" do
it "returns true" do
note = build(:note)
diff --git a/spec/models/project_services/slack_service/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb
index 452f4e2782c..50ad5013df9 100644
--- a/spec/models/project_services/slack_service/build_message_spec.rb
+++ b/spec/models/project_services/chat_message/build_message_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
-describe SlackService::BuildMessage do
- subject { SlackService::BuildMessage.new(args) }
+describe ChatMessage::BuildMessage do
+ subject { described_class.new(args) }
let(:args) do
{
@@ -10,7 +10,7 @@ describe SlackService::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 SlackService::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/slack_service/issue_message_spec.rb b/spec/models/project_services/chat_message/issue_message_spec.rb
index 98c36ec088d..190ff4c535d 100644
--- a/spec/models/project_services/slack_service/issue_message_spec.rb
+++ b/spec/models/project_services/chat_message/issue_message_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
-describe SlackService::IssueMessage, models: true do
- subject { SlackService::IssueMessage.new(args) }
+describe ChatMessage::IssueMessage, models: true do
+ subject { described_class.new(args) }
let(:args) do
{
@@ -10,14 +10,14 @@ describe SlackService::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 SlackService::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 SlackService::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/slack_service/merge_message_spec.rb b/spec/models/project_services/chat_message/merge_message_spec.rb
index c5c052d9af1..cc154112e90 100644
--- a/spec/models/project_services/slack_service/merge_message_spec.rb
+++ b/spec/models/project_services/chat_message/merge_message_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
-describe SlackService::MergeMessage, models: true do
- subject { SlackService::MergeMessage.new(args) }
+describe ChatMessage::MergeMessage, models: true do
+ subject { described_class.new(args) }
let(:args) do
{
@@ -10,14 +10,14 @@ describe SlackService::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 SlackService::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 SlackService::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/slack_service/note_message_spec.rb b/spec/models/project_services/chat_message/note_message_spec.rb
index 97f818125d3..da700a08e57 100644
--- a/spec/models/project_services/slack_service/note_message_spec.rb
+++ b/spec/models/project_services/chat_message/note_message_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe SlackService::NoteMessage, models: true do
+describe ChatMessage::NoteMessage, models: true do
let(:color) { '#345' }
before do
@@ -11,15 +11,15 @@ describe SlackService::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'
}
}
@@ -36,9 +36,9 @@ describe SlackService::NoteMessage, models: true do
end
it 'returns a message regarding notes on commits' do
- message = SlackService::NoteMessage.new(@args)
- expect(message.pretext).to eq("test.user <url|commented on " \
- "commit 5f163b2b> in <somewhere.com|project_name>: " \
+ message = described_class.new(@args)
+ 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 = [
{
@@ -62,9 +62,9 @@ describe SlackService::NoteMessage, models: true do
end
it 'returns a message regarding notes on a merge request' do
- message = SlackService::NoteMessage.new(@args)
- expect(message.pretext).to eq("test.user <url|commented on " \
- "merge request !30> in <somewhere.com|project_name>: " \
+ message = described_class.new(@args)
+ 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 = [
{
@@ -88,10 +88,10 @@ describe SlackService::NoteMessage, models: true do
end
it 'returns a message regarding notes on an issue' do
- message = SlackService::NoteMessage.new(@args)
+ 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 = [
{
@@ -114,9 +114,9 @@ describe SlackService::NoteMessage, models: true do
end
it 'returns a message regarding notes on a project snippet' do
- message = SlackService::NoteMessage.new(@args)
- expect(message.pretext).to eq("test.user <url|commented on " \
- "snippet #5> in <somewhere.com|project_name>: " \
+ message = described_class.new(@args)
+ 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
new file mode 100644
index 00000000000..bf2a9616455
--- /dev/null
+++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe ChatMessage::PipelineMessage do
+ subject { described_class.new(args) }
+ let(:user) { { name: 'hacker' } }
+
+ let(:args) do
+ {
+ object_attributes: {
+ id: 123,
+ sha: '97de212e80737a608d939f648d959671fb0a0142',
+ tag: false,
+ ref: 'develop',
+ status: status,
+ duration: duration
+ },
+ project: { path_with_namespace: 'project_name',
+ web_url: 'http://example.gitlab.com' },
+ user: user
+ }
+ end
+
+ let(:message) { build_message }
+
+ context 'pipeline succeeded' do
+ let(:status) { 'success' }
+ let(:color) { 'good' }
+ let(:duration) { 10 }
+ let(:message) { build_message('passed') }
+
+ it 'returns a message with information about succeeded build' do
+ verify_message
+ end
+ end
+
+ context 'pipeline failed' do
+ let(:status) { 'failed' }
+ let(:color) { 'danger' }
+ let(:duration) { 10 }
+
+ it 'returns a message with information about failed build' do
+ verify_message
+ end
+
+ context 'when triggered by API therefore lacking user' do
+ let(:user) { nil }
+ let(:message) { build_message(status, 'API') }
+
+ it 'returns a message stating it is by API' do
+ verify_message
+ end
+ end
+ end
+
+ def verify_message
+ expect(subject.pretext).to be_empty
+ expect(subject.fallback).to eq(message)
+ expect(subject.attachments).to eq([text: message, color: color])
+ end
+
+ def build_message(status_text = status, name = user[:name])
+ "<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/slack_service/push_message_spec.rb b/spec/models/project_services/chat_message/push_message_spec.rb
index 17cd05e24f1..24928873bad 100644
--- a/spec/models/project_services/slack_service/push_message_spec.rb
+++ b/spec/models/project_services/chat_message/push_message_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
-describe SlackService::PushMessage, models: true do
- subject { SlackService::PushMessage.new(args) }
+describe ChatMessage::PushMessage, models: true do
+ subject { described_class.new(args) }
let(:args) do
{
@@ -10,7 +10,7 @@ describe SlackService::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 SlackService::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 SlackService::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 SlackService::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 SlackService::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/slack_service/wiki_page_message_spec.rb b/spec/models/project_services/chat_message/wiki_page_message_spec.rb
index 093911598b0..a2ad61e38e7 100644
--- a/spec/models/project_services/slack_service/wiki_page_message_spec.rb
+++ b/spec/models/project_services/chat_message/wiki_page_message_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe SlackService::WikiPageMessage, models: true do
+describe ChatMessage::WikiPageMessage, models: true do
subject { described_class.new(args) }
let(:args) do
@@ -10,10 +10,10 @@ describe SlackService::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 SlackService::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 SlackService::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_notification_service_spec.rb b/spec/models/project_services/chat_notification_service_spec.rb
new file mode 100644
index 00000000000..c98e7ee14fd
--- /dev/null
+++ b/spec/models/project_services/chat_notification_service_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+describe ChatNotificationService, models: true do
+ describe "Associations" do
+ before do
+ allow(subject).to receive(:activated?).and_return(true)
+ end
+
+ it { is_expected.to validate_presence_of :webhook }
+ 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
new file mode 100644
index 00000000000..4f3cd14e941
--- /dev/null
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -0,0 +1,218 @@
+require 'spec_helper'
+
+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 }
+ end
+
+ describe 'Validations' do
+ context 'when service is active' do
+ before { subject.active = true }
+ it { is_expected.to validate_presence_of(:namespace) }
+ it { is_expected.to validate_presence_of(:api_url) }
+ it { is_expected.to validate_presence_of(:token) }
+
+ context 'namespace format' do
+ before do
+ subject.project = project
+ subject.api_url = "http://example.com"
+ subject.token = "test"
+ end
+
+ {
+ 'foo' => true,
+ '1foo' => true,
+ 'foo1' => true,
+ 'foo-bar' => true,
+ '-foo' => false,
+ 'foo-' => false,
+ 'a' * 63 => true,
+ 'a' * 64 => false,
+ 'a.b' => false,
+ 'a*b' => false,
+ }.each do |namespace, validity|
+ it "should validate #{namespace} as #{validity ? 'valid' : 'invalid'}" do
+ subject.namespace = namespace
+
+ expect(subject.valid?).to eq(validity)
+ end
+ end
+ end
+ end
+
+ context 'when service is inactive' do
+ before { subject.active = false }
+ it { is_expected.not_to validate_presence_of(:namespace) }
+ it { is_expected.not_to validate_presence_of(:api_url) }
+ it { is_expected.not_to validate_presence_of(:token) }
+ end
+ end
+
+ describe '#initialize_properties' do
+ context 'with a project' do
+ it 'defaults to the project name' do
+ expect(described_class.new(project: project).namespace).to eq(project.name)
+ end
+ end
+
+ context 'without a project' do
+ it 'leaves the namespace unset' do
+ expect(described_class.new.namespace).to be_nil
+ end
+ end
+ end
+
+ describe '#test' do
+ before do
+ stub_kubeclient_discover
+ end
+
+ context 'with path prefix in api_url' do
+ let(:discovery_url) { 'https://kubernetes.example.com/prefix/api/v1' }
+
+ it 'tests with the prefix' do
+ service.api_url = 'https://kubernetes.example.com/prefix/'
+
+ expect(service.test[:success]).to be_truthy
+ expect(WebMock).to have_requested(:get, discovery_url).once
+ end
+ end
+
+ context 'with custom CA certificate' do
+ it 'is added to the certificate store' do
+ service.ca_pem = "CA PEM DATA"
+
+ 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)
+
+ expect(service.test[:success]).to be_truthy
+ expect(WebMock).to have_requested(:get, discovery_url).once
+ end
+ end
+
+ context 'success' do
+ it 'reads the discovery endpoint' do
+ expect(service.test[:success]).to be_truthy
+ expect(WebMock).to have_requested(:get, discovery_url).once
+ end
+ end
+
+ context 'failure' do
+ 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
+ end
+ end
+
+ describe '#predefined_variables' do
+ before do
+ subject.api_url = 'https://kube.domain.com'
+ subject.token = 'token'
+ subject.namespace = 'my-project'
+ subject.ca_pem = 'CA PEM DATA'
+ end
+
+ it 'sets KUBE_URL' do
+ expect(subject.predefined_variables).to include(
+ { key: 'KUBE_URL', value: 'https://kube.domain.com', public: true }
+ )
+ end
+
+ it 'sets KUBE_TOKEN' do
+ expect(subject.predefined_variables).to include(
+ { key: 'KUBE_TOKEN', value: 'token', public: false }
+ )
+ end
+
+ it 'sets KUBE_NAMESPACE' do
+ expect(subject.predefined_variables).to include(
+ { key: 'KUBE_NAMESPACE', value: 'my-project', public: true }
+ )
+ end
+
+ it 'sets KUBE_CA_PEM' do
+ expect(subject.predefined_variables).to include(
+ { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true }
+ )
+ 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
new file mode 100644
index 00000000000..7832d6f50cf
--- /dev/null
+++ b/spec/models/project_services/mattermost_notification_service_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe MattermostNotificationService, 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..1ae1483e2a4 100644
--- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb
+++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
@@ -1,99 +1,5 @@
require 'spec_helper'
-describe MattermostSlashCommandsService, models: true do
- describe "Associations" do
- it { is_expected.to respond_to :token }
- 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: service) }
- let(:service) do
- project.create_mattermost_slash_commands_service(
- properties: { token: 'token' }
- )
- 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)
-
- service.trigger(params)
- end
- end
- end
- end
+describe MattermostSlashCommandsService, :models do
+ it_behaves_like "chat slash commands service"
end
diff --git a/spec/models/project_services/slack_notification_service_spec.rb b/spec/models/project_services/slack_notification_service_spec.rb
new file mode 100644
index 00000000000..110b5bf2115
--- /dev/null
+++ b/spec/models/project_services/slack_notification_service_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe SlackNotificationService, models: true do
+ it_behaves_like "slack or mattermost notifications"
+end
diff --git a/spec/models/project_services/slack_service/pipeline_message_spec.rb b/spec/models/project_services/slack_service/pipeline_message_spec.rb
deleted file mode 100644
index 363138a9454..00000000000
--- a/spec/models/project_services/slack_service/pipeline_message_spec.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-require 'spec_helper'
-
-describe SlackService::PipelineMessage do
- subject { SlackService::PipelineMessage.new(args) }
-
- let(:args) do
- {
- object_attributes: {
- id: 123,
- sha: '97de212e80737a608d939f648d959671fb0a0142',
- tag: false,
- ref: 'develop',
- status: status,
- duration: duration
- },
- project: { path_with_namespace: 'project_name',
- web_url: 'example.gitlab.com' },
- user: { name: 'hacker' }
- }
- end
-
- let(:message) { build_message }
-
- context 'pipeline succeeded' do
- let(:status) { 'success' }
- let(:color) { 'good' }
- let(:duration) { 10 }
- let(:message) { build_message('passed') }
-
- it 'returns a message with information about succeeded build' do
- expect(subject.pretext).to be_empty
- expect(subject.fallback).to eq(message)
- expect(subject.attachments).to eq([text: message, color: color])
- end
- end
-
- context 'pipeline failed' do
- let(:status) { 'failed' }
- let(:color) { 'danger' }
- let(:duration) { 10 }
-
- it 'returns a message with information about failed build' do
- expect(subject.pretext).to be_empty
- expect(subject.fallback).to eq(message)
- expect(subject.attachments).to eq([text: message, color: color])
- end
- end
-
- def build_message(status_text = status)
- "<example.gitlab.com|project_name>:" \
- " Pipeline <example.gitlab.com/pipelines/123|#123>" \
- " of <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/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 21ff238841e..0455cd2fe49 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -20,9 +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_service).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(:pushover_service).dependent(:destroy) }
it { is_expected.to have_one(:asana_service).dependent(:destroy) }
it { is_expected.to have_many(:boards).dependent(:destroy) }
@@ -36,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) }
@@ -1457,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) }
@@ -1696,6 +1709,26 @@ describe Project, models: true do
end
end
+ describe '#deployment_variables' do
+ context 'when project has no deployment service' do
+ let(:project) { create(:empty_project) }
+
+ it 'returns an empty array' do
+ expect(project.deployment_variables).to eq []
+ end
+ end
+
+ context 'when project has a deployment service' do
+ let(:project) { create(:kubernetes_project) }
+
+ it 'returns variables from this service' do
+ expect(project.deployment_variables).to include(
+ { key: 'KUBE_TOKEN', value: project.kubernetes_service.token, public: false }
+ )
+ end
+ end
+ end
+
def enable_lfs
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
end
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/models/user_spec.rb b/spec/models/user_spec.rb
index bad6ed9e146..8b20ee81614 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -727,17 +727,6 @@ describe User, models: true do
end
end
- describe 'by_username_or_id' do
- let(:user1) { create(:user, username: 'foo') }
-
- it "gets the correct user" do
- expect(User.by_username_or_id(user1.id)).to eq(user1)
- expect(User.by_username_or_id('foo')).to eq(user1)
- expect(User.by_username_or_id(-1)).to be_nil
- expect(User.by_username_or_id('bar')).to be_nil
- end
- end
-
describe '.find_by_ssh_key_id' do
context 'using an existing SSH key ID' do
let(:user) { create(:user) }
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
new file mode 100644
index 00000000000..a20ac303a53
--- /dev/null
+++ b/spec/policies/group_policy_spec.rb
@@ -0,0 +1,108 @@
+require 'spec_helper'
+
+describe GroupPolicy, models: true do
+ let(:guest) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:master) { create(:user) }
+ let(:owner) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:group) { create(:group) }
+
+ let(:master_permissions) do
+ [
+ :create_projects,
+ :admin_milestones,
+ :admin_label
+ ]
+ end
+
+ let(:owner_permissions) do
+ [
+ :admin_group,
+ :admin_namespace,
+ :admin_group_member,
+ :change_visibility_level
+ ]
+ end
+
+ before do
+ group.add_guest(guest)
+ group.add_reporter(reporter)
+ group.add_developer(developer)
+ group.add_master(master)
+ group.add_owner(owner)
+ end
+
+ subject { described_class.abilities(current_user, group).to_set }
+
+ context 'with no user' do
+ let(:current_user) { nil }
+
+ it do
+ is_expected.to include(:read_group)
+ is_expected.not_to include(*master_permissions)
+ is_expected.not_to include(*owner_permissions)
+ end
+ end
+
+ context 'guests' do
+ let(:current_user) { guest }
+
+ it do
+ is_expected.to include(:read_group)
+ is_expected.not_to include(*master_permissions)
+ is_expected.not_to include(*owner_permissions)
+ end
+ end
+
+ context 'reporter' do
+ let(:current_user) { reporter }
+
+ it do
+ is_expected.to include(:read_group)
+ is_expected.not_to include(*master_permissions)
+ is_expected.not_to include(*owner_permissions)
+ end
+ end
+
+ context 'developer' do
+ let(:current_user) { developer }
+
+ it do
+ is_expected.to include(:read_group)
+ is_expected.not_to include(*master_permissions)
+ is_expected.not_to include(*owner_permissions)
+ end
+ end
+
+ context 'master' do
+ let(:current_user) { master }
+
+ it do
+ is_expected.to include(:read_group)
+ is_expected.to include(*master_permissions)
+ is_expected.not_to include(*owner_permissions)
+ end
+ end
+
+ context 'owner' do
+ let(:current_user) { owner }
+
+ it do
+ is_expected.to include(:read_group)
+ is_expected.to include(*master_permissions)
+ is_expected.to include(*owner_permissions)
+ end
+ end
+
+ context 'admin' do
+ let(:current_user) { admin }
+
+ it do
+ is_expected.to include(:read_group)
+ is_expected.to include(*master_permissions)
+ is_expected.to include(*owner_permissions)
+ end
+ end
+end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index e497bce6943..964cded917c 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -456,6 +456,76 @@ describe API::Commits, api: true do
end
end
+ describe 'POST :id/repository/commits/:sha/cherry_pick' do
+ let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
+
+ context 'authorized user' do
+ it 'cherry picks a commit' do
+ post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'master'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(master_pickable_commit.title)
+ expect(json_response['message']).to eq(master_pickable_commit.message)
+ expect(json_response['author_name']).to eq(master_pickable_commit.author_name)
+ expect(json_response['committer_name']).to eq(user.name)
+ end
+
+ it 'returns 400 if commit is already included in the target branch' do
+ post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'markdown'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('Sorry, we cannot cherry-pick this commit automatically.
+ A cherry-pick may have already been performed with this commit, or a more recent commit may have updated some of its content.')
+ end
+
+ it 'returns 400 if you are not allowed to push to the target branch' do
+ project.team << [user2, :developer]
+ protected_branch = create(:protected_branch, project: project, name: 'feature')
+
+ post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user2), branch: protected_branch.name
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('You are not allowed to push into this branch')
+ end
+
+ it 'returns 400 for missing parameters' do
+ post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user)
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('branch is missing')
+ end
+
+ it 'returns 404 if commit is not found' do
+ post api("/projects/#{project.id}/repository/commits/abcd0123/cherry_pick", user), branch: 'master'
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Commit Not Found')
+ end
+
+ it 'returns 404 if branch is not found' do
+ post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'foo'
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Branch Not Found')
+ end
+
+ it 'returns 400 for missing parameters' do
+ post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user)
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('branch is missing')
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not cherry pick the commit' do
+ post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick"), branch: 'master'
+
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
describe 'Post comment to commit' do
context 'authorized user' do
it 'returns comment' do
diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb
index 5262a623761..bd9ecaf2685 100644
--- a/spec/requests/api/doorkeeper_access_spec.rb
+++ b/spec/requests/api/doorkeeper_access_spec.rb
@@ -5,7 +5,7 @@ describe API::API, api: true do
let!(:user) { create(:user) }
let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) }
- let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id }
+ let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "api" }
describe "when unauthenticated" do
it "returns authentication success" do
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index 126496c43a5..b9d535bc314 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -46,6 +46,7 @@ describe API::Environments, api: true do
expect(response).to have_http_status(201)
expect(json_response['name']).to eq('mepmep')
+ expect(json_response['slug']).to eq('mepmep')
expect(json_response['external']).to be nil
end
@@ -60,6 +61,13 @@ describe API::Environments, api: true do
expect(response).to have_http_status(400)
end
+
+ it 'returns a 400 if slug is specified' do
+ post api("/projects/#{project.id}/environments", user), name: "foo", slug: "foo"
+
+ expect(response).to have_http_status(400)
+ expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed")
+ end
end
context 'a non member' do
@@ -86,6 +94,15 @@ describe API::Environments, api: true do
expect(json_response['external_url']).to eq(url)
end
+ it "won't allow slug to be changed" do
+ slug = environment.slug
+ api_url = api("/projects/#{project.id}/environments/#{environment.id}", user)
+ put api_url, slug: slug + "-foo"
+
+ expect(response).to have_http_status(400)
+ expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed")
+ end
+
it "won't update the external_url if only the name is passed" do
url = environment.external_url
put api("/projects/#{project.id}/environments/#{environment.id}", user),
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 15647b262b6..cdeb965b413 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -2,13 +2,13 @@ require 'spec_helper'
describe API::Groups, api: true do
include ApiHelpers
+ include UploadHelpers
let(:user1) { create(:user, can_create_group: false) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:admin) { create(:admin) }
- let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') }
- let!(:group1) { create(:group, avatar: File.open(avatar_file_path)) }
+ let!(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) }
let!(:group2) { create(:group, :private) }
let!(:project1) { create(:project, namespace: group1) }
let!(:project2) { create(:project, namespace: group2) }
@@ -243,17 +243,28 @@ describe API::Groups, api: true do
expect(json_response.length).to eq(2)
project_names = json_response.map { |proj| proj['name' ] }
expect(project_names).to match_array([project1.name, project3.name])
+ expect(json_response.first['default_branch']).to be_present
+ end
+
+ it "returns the group's projects with simple representation" do
+ get api("/groups/#{group1.id}/projects", user1), simple: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response.length).to eq(2)
+ project_names = json_response.map { |proj| proj['name' ] }
+ expect(project_names).to match_array([project1.name, project3.name])
+ expect(json_response.first['default_branch']).not_to be_present
end
it 'filters the groups projects' do
- public_projet = create(:project, :public, path: 'test1', group: group1)
+ public_project = create(:project, :public, path: 'test1', group: group1)
get api("/groups/#{group1.id}/projects", user1), visibility: 'public'
expect(response).to have_http_status(200)
expect(json_response).to be_an(Array)
expect(json_response.length).to eq(1)
- expect(json_response.first['name']).to eq(public_projet.name)
+ expect(json_response.first['name']).to eq(public_project.name)
end
it "does not return a non existing group" do
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index 3f34309f419..c3d7ac3eef8 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
describe API::Helpers, api: true do
+ include API::APIGuard::HelperMethods
include API::Helpers
- include ApiHelpers
include SentryHelper
let(:user) { create(:user) }
@@ -13,27 +13,27 @@ describe API::Helpers, api: true do
let(:env) { { 'REQUEST_METHOD' => 'GET' } }
let(:request) { Rack::Request.new(env) }
- def set_env(token_usr, identifier)
+ def set_env(user_or_token, identifier)
clear_env
clear_param
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = token_usr.private_token
- env[API::Helpers::SUDO_HEADER] = identifier
+ env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
+ env[API::Helpers::SUDO_HEADER] = identifier.to_s
end
- def set_param(token_usr, identifier)
+ def set_param(user_or_token, identifier)
clear_env
clear_param
- params[API::Helpers::PRIVATE_TOKEN_PARAM] = token_usr.private_token
- params[API::Helpers::SUDO_PARAM] = identifier
+ params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
+ params[API::Helpers::SUDO_PARAM] = identifier.to_s
end
def clear_env
- env.delete(API::Helpers::PRIVATE_TOKEN_HEADER)
+ env.delete(API::APIGuard::PRIVATE_TOKEN_HEADER)
env.delete(API::Helpers::SUDO_HEADER)
end
def clear_param
- params.delete(API::Helpers::PRIVATE_TOKEN_PARAM)
+ params.delete(API::APIGuard::PRIVATE_TOKEN_PARAM)
params.delete(API::Helpers::SUDO_PARAM)
end
@@ -95,22 +95,28 @@ describe API::Helpers, api: true do
describe "when authenticating using a user's private token" do
it "returns nil for an invalid token" do
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
+ env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token'
allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+
expect(current_user).to be_nil
end
it "returns nil for a user without access" do
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
+ env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
+
expect(current_user).to be_nil
end
it "leaves user as is when sudo not specified" do
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
+ env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
+
expect(current_user).to eq(user)
+
clear_env
- params[API::Helpers::PRIVATE_TOKEN_PARAM] = user.private_token
+
+ params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user.private_token
+
expect(current_user).to eq(user)
end
end
@@ -118,37 +124,51 @@ describe API::Helpers, api: true do
describe "when authenticating using a user's personal access tokens" do
let(:personal_access_token) { create(:personal_access_token, user: user) }
+ before do
+ allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false }
+ end
+
it "returns nil for an invalid token" do
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
- allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+ env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token'
+
expect(current_user).to be_nil
end
it "returns nil for a user without access" do
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
+
+ expect(current_user).to be_nil
+ end
+
+ it "returns nil for a token without the appropriate scope" do
+ personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
+ env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ allow_access_with_scope('write_user')
+
expect(current_user).to be_nil
end
it "leaves user as is when sudo not specified" do
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(current_user).to eq(user)
clear_env
- params[API::Helpers::PRIVATE_TOKEN_PARAM] = personal_access_token.token
+ params[API::APIGuard::PRIVATE_TOKEN_PARAM] = personal_access_token.token
+
expect(current_user).to eq(user)
end
it 'does not allow revoked tokens' do
personal_access_token.revoke!
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
- allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+ env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+
expect(current_user).to be_nil
end
it 'does not allow expired tokens' do
personal_access_token.update_attributes!(expires_at: 1.day.ago)
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
- allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+ env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+
expect(current_user).to be_nil
end
end
@@ -163,6 +183,13 @@ describe API::Helpers, api: true do
expect(current_user).to eq(user)
end
+ it 'memoize the current_user: sudo permissions are not run against the sudoed user' do
+ set_env(admin, user.id)
+
+ expect(current_user).to eq(user)
+ expect(current_user).to eq(user)
+ end
+
it 'handles sudo to oneself' do
set_env(admin, admin.id)
@@ -294,33 +321,48 @@ describe API::Helpers, api: true do
end
end
- describe '.sudo_identifier' do
- it "returns integers when input is an int" do
- set_env(admin, '123')
- expect(sudo_identifier).to eq(123)
- set_env(admin, '0001234567890')
- expect(sudo_identifier).to eq(1234567890)
-
- set_param(admin, '123')
- expect(sudo_identifier).to eq(123)
- set_param(admin, '0001234567890')
- expect(sudo_identifier).to eq(1234567890)
+ describe '.sudo?' do
+ context 'when no sudo env or param is passed' do
+ before do
+ doorkeeper_guard_returns(nil)
+ end
+
+ it 'returns false' do
+ expect(sudo?).to be_falsy
+ end
+ end
+
+ context 'when sudo env or param is passed', 'user is not an admin' do
+ before do
+ set_env(user, '123')
+ end
+
+ it 'returns an 403 Forbidden' do
+ expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Must be admin to use sudo"}'
+ end
end
- it "returns string when input is an is not an int" do
- set_env(admin, '12.30')
- expect(sudo_identifier).to eq("12.30")
- set_env(admin, 'hello')
- expect(sudo_identifier).to eq('hello')
- set_env(admin, ' 123')
- expect(sudo_identifier).to eq(' 123')
-
- set_param(admin, '12.30')
- expect(sudo_identifier).to eq("12.30")
- set_param(admin, 'hello')
- expect(sudo_identifier).to eq('hello')
- set_param(admin, ' 123')
- expect(sudo_identifier).to eq(' 123')
+ context 'when sudo env or param is passed', 'user is admin' do
+ context 'personal access token is used' do
+ before do
+ personal_access_token = create(:personal_access_token, user: admin)
+ set_env(personal_access_token.token, user.id)
+ end
+
+ it 'returns an 403 Forbidden' do
+ expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Private token must be specified in order to use sudo"}'
+ end
+ end
+
+ context 'private access token is used' do
+ before do
+ set_env(admin.private_token, user.id)
+ end
+
+ it 'returns true' do
+ expect(sudo?).to be_truthy
+ end
+ end
end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 75b270aa93c..f032d1b683d 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -533,6 +533,22 @@ describe API::MergeRequests, api: true do
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end
+
+ it 'does not update state when title is empty' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', title: nil
+
+ merge_request.reload
+ expect(response).to have_http_status(400)
+ expect(merge_request.state).to eq('opened')
+ end
+
+ it 'does not update state when target_branch is empty' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', target_branch: nil
+
+ merge_request.reload
+ expect(response).to have_http_status(400)
+ expect(merge_request.state).to eq('opened')
+ end
end
describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index c5d67a90abc..8304c408064 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -167,7 +167,7 @@ describe API::Projects, api: true do
expect(json_response).to satisfy do |response|
response.one? do |entry|
entry.has_key?('permissions') &&
- entry['name'] == project.name &&
+ entry['name'] == project.name &&
entry['owner']['username'] == user.username
end
end
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index d30361f53d4..668e39f9dba 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -2,6 +2,7 @@ require "spec_helper"
describe API::Services, api: true do
include ApiHelpers
+
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:user2) { create(:user) }
@@ -98,7 +99,7 @@ describe API::Services, api: true do
post api("/projects/#{project.id}/services/idonotexist/trigger")
expect(response).to have_http_status(404)
- expect(json_response["message"]).to eq("404 Service Not Found")
+ expect(json_response["error"]).to eq("404 Not Found")
end
end
@@ -114,7 +115,7 @@ describe API::Services, api: true do
end
it 'when the service is inactive' do
- post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger")
+ post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger"), params
expect(response).to have_http_status(404)
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index c37dbfa0a33..9e317f3a7e9 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -651,13 +651,12 @@ describe API::Users, api: true do
end
describe "GET /user" do
- let(:personal_access_token) { create(:personal_access_token, user: user) }
- let(:private_token) { user.private_token }
+ let(:personal_access_token) { create(:personal_access_token, user: user).token }
context 'with regular user' do
context 'with personal access token' do
it 'returns 403 without private token when sudo is defined' do
- get api("/user?private_token=#{personal_access_token.token}&sudo=#{user.id}")
+ get api("/user?private_token=#{personal_access_token}&sudo=123")
expect(response).to have_http_status(403)
end
@@ -665,7 +664,7 @@ describe API::Users, api: true do
context 'with private token' do
it 'returns 403 without private token when sudo defined' do
- get api("/user?private_token=#{private_token}&sudo=#{user.id}")
+ get api("/user?private_token=#{user.private_token}&sudo=123")
expect(response).to have_http_status(403)
end
@@ -676,40 +675,44 @@ describe API::Users, api: true do
expect(response).to have_http_status(200)
expect(response).to match_response_schema('user/public')
+ expect(json_response['id']).to eq(user.id)
end
end
context 'with admin' do
- let(:user) { create(:admin) }
+ let(:admin_personal_access_token) { create(:personal_access_token, user: admin).token }
context 'with personal access token' do
it 'returns 403 without private token when sudo defined' do
- get api("/user?private_token=#{personal_access_token.token}&sudo=#{user.id}")
+ get api("/user?private_token=#{admin_personal_access_token}&sudo=#{user.id}")
expect(response).to have_http_status(403)
end
- it 'returns current user without private token when sudo not defined' do
- get api("/user?private_token=#{personal_access_token.token}")
+ it 'returns initial current user without private token when sudo not defined' do
+ get api("/user?private_token=#{admin_personal_access_token}")
expect(response).to have_http_status(200)
expect(response).to match_response_schema('user/public')
+ expect(json_response['id']).to eq(admin.id)
end
end
context 'with private token' do
- it 'returns current user with private token when sudo defined' do
- get api("/user?private_token=#{private_token}&sudo=#{user.id}")
+ it 'returns sudoed user with private token when sudo defined' do
+ get api("/user?private_token=#{admin.private_token}&sudo=#{user.id}")
expect(response).to have_http_status(200)
expect(response).to match_response_schema('user/login')
+ expect(json_response['id']).to eq(user.id)
end
- it 'returns current user without private token when sudo not defined' do
- get api("/user?private_token=#{private_token}")
+ it 'returns initial current user without private token when sudo not defined' do
+ get api("/user?private_token=#{admin.private_token}")
expect(response).to have_http_status(200)
expect(response).to match_response_schema('user/public')
+ expect(json_response['id']).to eq(admin.id)
end
end
end
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index 80652129928..79f12ace999 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -249,7 +249,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 +280,6 @@ describe Ci::API::Builds do
end
before do
- build.run!
initial_patch_the_trace
end
@@ -329,6 +332,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/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index c701b9a1202..5abda28e26f 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -230,7 +230,7 @@ describe 'Git HTTP requests', lib: true do
context "when an oauth token is provided" do
before do
application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
- @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id)
+ @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api")
end
it "downloads get status 200" do
diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb
index 69eeb45ed71..661b671301e 100644
--- a/spec/routing/admin_routing_spec.rb
+++ b/spec/routing/admin_routing_spec.rb
@@ -66,7 +66,8 @@ describe Admin::ProjectsController, "routing" do
end
it "to #show" do
- expect(get("/admin/projects/gitlab")).to route_to('admin/projects#show', namespace_id: 'gitlab')
+ expect(get("/admin/projects/gitlab/gitlab-ce")).to route_to('admin/projects#show', namespace_id: 'gitlab', id: 'gitlab-ce')
+ expect(get("/admin/projects/gitlab/subgroup/gitlab-ce")).to route_to('admin/projects#show', namespace_id: 'gitlab/subgroup', id: 'gitlab-ce')
end
end
@@ -119,3 +120,14 @@ describe Admin::HealthCheckController, "routing" do
expect(get("/admin/health_check")).to route_to('admin/health_check#show')
end
end
+
+describe Admin::GroupsController, "routing" do
+ it "to #index" do
+ expect(get("/admin/groups")).to route_to('admin/groups#index')
+ end
+
+ it "to #show" do
+ expect(get("/admin/groups/gitlab")).to route_to('admin/groups#show', id: 'gitlab')
+ expect(get("/admin/groups/gitlab/subgroup")).to route_to('admin/groups#show', id: 'gitlab/subgroup')
+ end
+end
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/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index b6e7da841b1..77549db2927 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -80,10 +80,6 @@ describe 'project routing' do
expect(get('/gitlab/gitlabhq/edit')).to route_to('projects#edit', namespace_id: 'gitlab', id: 'gitlabhq')
end
- it 'to #autocomplete_sources' do
- expect(get('/gitlab/gitlabhq/autocomplete_sources')).to route_to('projects#autocomplete_sources', namespace_id: 'gitlab', id: 'gitlabhq')
- end
-
describe 'to #show' do
context 'regular name' do
it { expect(get('/gitlab/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq') }
@@ -117,6 +113,21 @@ describe 'project routing' do
end
end
+ # emojis_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/emojis(.:format) projects/autocomplete_sources#emojis
+ # members_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/members(.:format) projects/autocomplete_sources#members
+ # issues_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/issues(.:format) projects/autocomplete_sources#issues
+ # merge_requests_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/merge_requests(.:format) projects/autocomplete_sources#merge_requests
+ # labels_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/labels(.:format) projects/autocomplete_sources#labels
+ # milestones_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/milestones(.:format) projects/autocomplete_sources#milestones
+ # commands_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/commands(.:format) projects/autocomplete_sources#commands
+ describe Projects::AutocompleteSourcesController, 'routing' do
+ [:emojis, :members, :issues, :merge_requests, :labels, :milestones, :commands].each do |action|
+ it "to ##{action}" do
+ expect(get("/gitlab/gitlabhq/autocomplete_sources/#{action}")).to route_to("projects/autocomplete_sources##{action}", namespace_id: 'gitlab', project_id: 'gitlabhq')
+ end
+ end
+ end
+
# pages_project_wikis GET /:project_id/wikis/pages(.:format) projects/wikis#pages
# history_project_wiki GET /:project_id/wikis/:id/history(.:format) projects/wikis#history
# project_wikis POST /:project_id/wikis(.:format) projects/wikis#create
diff --git a/spec/serializers/analytics_build_entity_spec.rb b/spec/serializers/analytics_build_entity_spec.rb
index 6b33fe66a63..86e703a6448 100644
--- a/spec/serializers/analytics_build_entity_spec.rb
+++ b/spec/serializers/analytics_build_entity_spec.rb
@@ -13,6 +13,14 @@ describe AnalyticsBuildEntity do
subject { entity.as_json }
+ before do
+ Timecop.freeze
+ end
+
+ after do
+ Timecop.return
+ end
+
it 'contains the URL' do
expect(subject).to include(:url)
end
diff --git a/spec/services/access_token_validation_service_spec.rb b/spec/services/access_token_validation_service_spec.rb
new file mode 100644
index 00000000000..87f093ee8ce
--- /dev/null
+++ b/spec/services/access_token_validation_service_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe AccessTokenValidationService, services: true do
+ describe ".include_any_scope?" do
+ it "returns true if the required scope is present in the token's scopes" do
+ token = double("token", scopes: [:api, :read_user])
+
+ expect(described_class.new(token).include_any_scope?([:api])).to be(true)
+ end
+
+ it "returns true if more than one of the required scopes is present in the token's scopes" do
+ token = double("token", scopes: [:api, :read_user, :other_scope])
+
+ expect(described_class.new(token).include_any_scope?([:api, :other_scope])).to be(true)
+ end
+
+ it "returns true if the list of required scopes is an exact match for the token's scopes" do
+ token = double("token", scopes: [:api, :read_user, :other_scope])
+
+ expect(described_class.new(token).include_any_scope?([:api, :read_user, :other_scope])).to be(true)
+ end
+
+ it "returns true if the list of required scopes contains all of the token's scopes, in addition to others" do
+ token = double("token", scopes: [:api, :read_user])
+
+ expect(described_class.new(token).include_any_scope?([:api, :read_user, :other_scope])).to be(true)
+ end
+
+ it 'returns true if the list of required scopes is blank' do
+ token = double("token", scopes: [])
+
+ expect(described_class.new(token).include_any_scope?([])).to be(true)
+ end
+
+ it "returns false if there are no scopes in common between the required scopes and the token scopes" do
+ token = double("token", scopes: [:api, :read_user])
+
+ expect(described_class.new(token).include_any_scope?([:other_scope])).to be(false)
+ end
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 4aadd009f3e..ceaca96e25b 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -210,5 +210,22 @@ describe Ci::CreatePipelineService, services: true do
expect(result.manual_actions).not_to be_empty
end
end
+
+ context 'with environment' do
+ before do
+ config = YAML.dump(deploy: { environment: { name: "review/$CI_BUILD_REF_NAME" }, script: 'ls' })
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'creates the environment' do
+ result = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: [{ message: 'some msg' }])
+
+ expect(result).to be_persisted
+ expect(Environment.find_by(name: "review/master")).not_to be_nil
+ end
+ end
end
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 500d224ff98..eafbea46905 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -376,5 +376,10 @@ describe Issues::UpdateService, services: true do
let(:mentionable) { issue }
include_examples 'updating mentions', Issues::UpdateService
end
+
+ include_examples 'issuable update service' do
+ let(:open_issuable) { issue }
+ let(:closed_issuable) { create(:closed_issue, project: project) }
+ end
end
end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 790ef765f3a..88c786947d3 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -320,5 +320,10 @@ describe MergeRequests::UpdateService, services: true do
expect(issue_ids).to be_empty
end
end
+
+ include_examples 'issuable update service' do
+ let(:open_issuable) { merge_request }
+ let(:closed_issuable) { create(:closed_merge_request, source_project: project) }
+ end
end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 90b7e62bc6f..0e8adb68721 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -694,7 +694,7 @@ describe SystemNoteService, services: true do
describe "existing reference" do
before do
- message = "[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\n'#{commit.title}'"
+ message = "[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\n'#{commit.title.chomp}'"
allow_any_instance_of(JIRA::Resource::Issue).to receive(:comments).and_return([OpenStruct.new(body: message)])
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/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/services/issuable_update_service_shared_examples.rb b/spec/support/services/issuable_update_service_shared_examples.rb
new file mode 100644
index 00000000000..a3336755773
--- /dev/null
+++ b/spec/support/services/issuable_update_service_shared_examples.rb
@@ -0,0 +1,17 @@
+shared_examples 'issuable update service' do
+ context 'changing state' do
+ before { expect(project).to receive(:execute_hooks).once }
+
+ context 'to reopened' do
+ it 'executes hooks only once' do
+ described_class.new(project, user, state_event: 'reopen').execute(closed_issuable)
+ end
+ end
+
+ context 'to closed' do
+ it 'executes hooks only once' do
+ described_class.new(project, user, state_event: 'close').execute(open_issuable)
+ end
+ end
+ end
+end
diff --git a/spec/support/services_shared_context.rb b/spec/support/services_shared_context.rb
index d1c999cad4d..66c93890e31 100644
--- a/spec/support/services_shared_context.rb
+++ b/spec/support/services_shared_context.rb
@@ -16,8 +16,14 @@ Service.available_services_names.each do |service|
hash.merge!(k => 'secrettoken')
elsif k =~ /^(.*_url|url|webhook)/
hash.merge!(k => "http://example.com")
+ elsif service_klass.method_defined?("#{k}?")
+ hash.merge!(k => true)
elsif service == 'irker' && k == :recipients
hash.merge!(k => 'irc://irc.network.net:666/#channel')
+ elsif service == 'irker' && k == :server_port
+ hash.merge!(k => 1234)
+ elsif service == 'jira' && k == :jira_issue_transition_id
+ hash.merge!(k => 1234)
else
hash.merge!(k => "someword")
end
diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/support/slack_mattermost_notifications_shared_examples.rb
index c07a70a8069..8582aea5fe5 100644
--- a/spec/models/project_services/slack_service_spec.rb
+++ b/spec/support/slack_mattermost_notifications_shared_examples.rb
@@ -1,7 +1,7 @@
-require 'spec_helper'
+Dir[Rails.root.join("app/models/project_services/chat_message/*.rb")].each { |f| require f }
-describe SlackService, models: true do
- let(:slack) { SlackService.new }
+RSpec.shared_examples 'slack or mattermost notifications' do
+ let(:chat_service) { described_class.new }
let(:webhook_url) { 'https://example.gitlab.com/' }
describe "Associations" do
@@ -24,7 +24,7 @@ describe SlackService, models: true do
end
end
- describe "Execute" do
+ describe "#execute" do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:username) { 'slack_username' }
@@ -35,7 +35,7 @@ describe SlackService, models: true do
end
before do
- allow(slack).to receive_messages(
+ allow(chat_service).to receive_messages(
project: project,
project_id: project.id,
service_hook: true,
@@ -77,54 +77,55 @@ describe SlackService, models: true do
@wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create')
end
- it "calls Slack API for push events" do
- slack.execute(push_sample_data)
+ it "calls Slack/Mattermost API for push events" do
+ chat_service.execute(push_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
- it "calls Slack API for issue events" do
- slack.execute(@issues_sample_data)
+ it "calls Slack/Mattermost API for issue events" do
+ chat_service.execute(@issues_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
- it "calls Slack API for merge requests events" do
- slack.execute(@merge_sample_data)
+ it "calls Slack/Mattermost API for merge requests events" do
+ chat_service.execute(@merge_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
- it "calls Slack API for wiki page events" do
- slack.execute(@wiki_page_sample_data)
+ it "calls Slack/Mattermost API for wiki page events" do
+ chat_service.execute(@wiki_page_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
it 'uses the username as an option for slack when configured' do
- allow(slack).to receive(:username).and_return(username)
+ allow(chat_service).to receive(:username).and_return(username)
+
expect(Slack::Notifier).to receive(:new).
- with(webhook_url, username: username).
+ with(webhook_url, username: username, channel: chat_service.default_channel).
and_return(
double(:slack_service).as_null_object
)
- slack.execute(push_sample_data)
+ chat_service.execute(push_sample_data)
end
it 'uses the channel as an option when it is configured' do
- allow(slack).to receive(:channel).and_return(channel)
+ allow(chat_service).to receive(:channel).and_return(channel)
expect(Slack::Notifier).to receive(:new).
with(webhook_url, channel: channel).
and_return(
double(:slack_service).as_null_object
)
- slack.execute(push_sample_data)
+ chat_service.execute(push_sample_data)
end
context "event channels" do
it "uses the right channel for push event" do
- slack.update_attributes(push_channel: "random")
+ chat_service.update_attributes(push_channel: "random")
expect(Slack::Notifier).to receive(:new).
with(webhook_url, channel: "random").
@@ -132,11 +133,11 @@ describe SlackService, models: true do
double(:slack_service).as_null_object
)
- slack.execute(push_sample_data)
+ chat_service.execute(push_sample_data)
end
it "uses the right channel for merge request event" do
- slack.update_attributes(merge_request_channel: "random")
+ chat_service.update_attributes(merge_request_channel: "random")
expect(Slack::Notifier).to receive(:new).
with(webhook_url, channel: "random").
@@ -144,11 +145,11 @@ describe SlackService, models: true do
double(:slack_service).as_null_object
)
- slack.execute(@merge_sample_data)
+ chat_service.execute(@merge_sample_data)
end
it "uses the right channel for issue event" do
- slack.update_attributes(issue_channel: "random")
+ chat_service.update_attributes(issue_channel: "random")
expect(Slack::Notifier).to receive(:new).
with(webhook_url, channel: "random").
@@ -156,11 +157,11 @@ describe SlackService, models: true do
double(:slack_service).as_null_object
)
- slack.execute(@issues_sample_data)
+ chat_service.execute(@issues_sample_data)
end
it "uses the right channel for wiki event" do
- slack.update_attributes(wiki_page_channel: "random")
+ chat_service.update_attributes(wiki_page_channel: "random")
expect(Slack::Notifier).to receive(:new).
with(webhook_url, channel: "random").
@@ -168,7 +169,7 @@ describe SlackService, models: true do
double(:slack_service).as_null_object
)
- slack.execute(@wiki_page_sample_data)
+ chat_service.execute(@wiki_page_sample_data)
end
context "note event" do
@@ -177,7 +178,7 @@ describe SlackService, models: true do
end
it "uses the right channel" do
- slack.update_attributes(note_channel: "random")
+ chat_service.update_attributes(note_channel: "random")
note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
@@ -187,7 +188,7 @@ describe SlackService, models: true do
double(:slack_service).as_null_object
)
- slack.execute(note_data)
+ chat_service.execute(note_data)
end
end
end
@@ -198,7 +199,7 @@ describe SlackService, models: true do
let(:project) { create(:project, creator_id: user.id) }
before do
- allow(slack).to receive_messages(
+ allow(chat_service).to receive_messages(
project: project,
project_id: project.id,
service_hook: true,
@@ -216,9 +217,9 @@ describe SlackService, models: true do
note: 'a comment on a commit')
end
- it "calls Slack API for commit comment events" do
+ it "calls Slack/Mattermost API for commit comment events" do
data = Gitlab::DataBuilder::Note.build(commit_note, user)
- slack.execute(data)
+ chat_service.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
@@ -232,7 +233,7 @@ describe SlackService, models: true do
it "calls Slack API for merge request comment events" do
data = Gitlab::DataBuilder::Note.build(merge_request_note, user)
- slack.execute(data)
+ chat_service.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
@@ -245,7 +246,7 @@ describe SlackService, models: true do
it "calls Slack API for issue comment events" do
data = Gitlab::DataBuilder::Note.build(issue_note, user)
- slack.execute(data)
+ chat_service.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
@@ -259,7 +260,7 @@ describe SlackService, models: true do
it "calls Slack API for snippet comment events" do
data = Gitlab::DataBuilder::Note.build(snippet_note, user)
- slack.execute(data)
+ chat_service.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
@@ -277,21 +278,21 @@ describe SlackService, models: true do
end
before do
- allow(slack).to receive_messages(
+ allow(chat_service).to receive_messages(
project: project,
service_hook: true,
webhook: webhook_url
)
end
- shared_examples 'call Slack API' do
+ shared_examples 'call Slack/Mattermost API' do
before do
WebMock.stub_request(:post, webhook_url)
end
- it 'calls Slack API for pipeline events' do
+ it 'calls Slack/Mattermost API for pipeline events' do
data = Gitlab::DataBuilder::Pipeline.build(pipeline)
- slack.execute(data)
+ chat_service.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
@@ -300,16 +301,16 @@ describe SlackService, models: true do
context 'with failed pipeline' do
let(:status) { 'failed' }
- it_behaves_like 'call Slack API'
+ it_behaves_like 'call Slack/Mattermost API'
end
context 'with succeeded pipeline' do
let(:status) { 'success' }
context 'with default to notify_only_broken_pipelines' do
- it 'does not call Slack API for pipeline events' do
+ it 'does not call Slack/Mattermost API for pipeline events' do
data = Gitlab::DataBuilder::Pipeline.build(pipeline)
- result = slack.execute(data)
+ result = chat_service.execute(data)
expect(result).to be_falsy
end
@@ -317,10 +318,10 @@ describe SlackService, models: true do
context 'with setting notify_only_broken_pipelines to false' do
before do
- slack.notify_only_broken_pipelines = false
+ chat_service.notify_only_broken_pipelines = false
end
- it_behaves_like 'call Slack API'
+ it_behaves_like 'call Slack/Mattermost API'
end
end
end
diff --git a/spec/support/upload_helpers.rb b/spec/support/upload_helpers.rb
new file mode 100644
index 00000000000..5eead80c935
--- /dev/null
+++ b/spec/support/upload_helpers.rb
@@ -0,0 +1,16 @@
+require 'fileutils'
+
+module UploadHelpers
+ extend self
+
+ def uploaded_image_temp_path
+ basename = 'banana_sample.gif'
+ orig_path = File.join(Rails.root, 'spec', 'fixtures', basename)
+ tmp_path = File.join(Rails.root, 'tmp', 'tests', basename)
+ # Because we use 'move_to_store' on all uploaders, we create a new
+ # tempfile on each call: the file we return here will be renamed in most
+ # cases.
+ FileUtils.copy(orig_path, tmp_path)
+ tmp_path
+ end
+end
diff --git a/spec/tasks/gitlab/ldap_rake_spec.rb b/spec/tasks/gitlab/ldap_rake_spec.rb
new file mode 100644
index 00000000000..12d442b9820
--- /dev/null
+++ b/spec/tasks/gitlab/ldap_rake_spec.rb
@@ -0,0 +1,13 @@
+require 'rake_helper'
+
+describe 'gitlab:ldap:rename_provider rake task' do
+ it 'completes without error' do
+ Rake.application.rake_require 'tasks/gitlab/ldap'
+ stub_warn_user_is_not_gitlab
+ ENV['force'] = 'yes'
+
+ create(:identity) # Necessary to prevent `exit 1` from the task.
+
+ run_rake_task('gitlab:ldap:rename_provider', 'ldapmain', 'ldapfoo')
+ end
+end
diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb
new file mode 100644
index 00000000000..6098be5cd45
--- /dev/null
+++ b/spec/uploaders/attachment_uploader_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe AttachmentUploader do
+ let(:issue) { build(:issue) }
+ subject { described_class.new(issue) }
+
+ describe '#move_to_cache' do
+ it 'is true' do
+ expect(subject.move_to_cache).to eq(true)
+ end
+ end
+
+ describe '#move_to_store' do
+ it 'is true' do
+ expect(subject.move_to_store).to eq(true)
+ end
+ end
+end
diff --git a/spec/uploaders/avatar_uploader_spec.rb b/spec/uploaders/avatar_uploader_spec.rb
new file mode 100644
index 00000000000..1f0e8732587
--- /dev/null
+++ b/spec/uploaders/avatar_uploader_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe AvatarUploader do
+ let(:user) { build(:user) }
+ subject { described_class.new(user) }
+
+ describe '#move_to_cache' do
+ it 'is true' do
+ expect(subject.move_to_cache).to eq(true)
+ end
+ end
+
+ describe '#move_to_store' do
+ it 'is true' do
+ expect(subject.move_to_store).to eq(true)
+ end
+ end
+end
diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb
index e8300abed5d..bc86adfef68 100644
--- a/spec/uploaders/file_uploader_spec.rb
+++ b/spec/uploaders/file_uploader_spec.rb
@@ -42,4 +42,16 @@ describe FileUploader do
expect(@uploader.image_or_video?).to be false
end
end
+
+ describe '#move_to_cache' do
+ it 'is true' do
+ expect(@uploader.move_to_cache).to eq(true)
+ end
+ end
+
+ describe '#move_to_store' do
+ it 'is true' do
+ expect(@uploader.move_to_store).to eq(true)
+ end
+ end
end
diff --git a/spec/views/projects/commit/_commit_box.html.haml_spec.rb b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
index 16bf0698c4b..e741e3cf9b6 100644
--- a/spec/views/projects/commit/_commit_box.html.haml_spec.rb
+++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
@@ -13,7 +13,7 @@ describe 'projects/commit/_commit_box.html.haml' do
it 'shows the commit SHA' do
render
- expect(rendered).to have_text("Commit #{Commit.truncate_sha(project.commit.sha)}")
+ expect(rendered).to have_text("#{Commit.truncate_sha(project.commit.sha)}")
end
it 'shows the last pipeline that ran for the commit' do
diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb
index bf027499c94..a066ea078e6 100644
--- a/spec/views/projects/pipelines/show.html.haml_spec.rb
+++ b/spec/views/projects/pipelines/show.html.haml_spec.rb
@@ -28,7 +28,7 @@ describe 'projects/pipelines/show' do
it 'shows a graph with grouped stages' do
render
- expect(rendered).to have_css('.pipeline-graph')
+ expect(rendered).to have_css('.js-pipeline-graph')
expect(rendered).to have_css('.grouped-pipeline-dropdown')
# stages
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/fonts/KaTeX_AMS-Regular.eot b/vendor/assets/fonts/KaTeX_AMS-Regular.eot
new file mode 100644
index 00000000000..784276a3cbf
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_AMS-Regular.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_AMS-Regular.ttf b/vendor/assets/fonts/KaTeX_AMS-Regular.ttf
new file mode 100644
index 00000000000..6f1e0be2028
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_AMS-Regular.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_AMS-Regular.woff b/vendor/assets/fonts/KaTeX_AMS-Regular.woff
new file mode 100644
index 00000000000..4dded4733b3
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_AMS-Regular.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_AMS-Regular.woff2 b/vendor/assets/fonts/KaTeX_AMS-Regular.woff2
new file mode 100644
index 00000000000..ea81079c4e2
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_AMS-Regular.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Bold.eot b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.eot
new file mode 100644
index 00000000000..1a0db0c568e
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Bold.ttf b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.ttf
new file mode 100644
index 00000000000..b94907dad11
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff
new file mode 100644
index 00000000000..799fa8122ca
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff2 b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff2
new file mode 100644
index 00000000000..73bb5422878
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Regular.eot b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.eot
new file mode 100644
index 00000000000..6cc83d0922c
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Regular.ttf b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.ttf
new file mode 100644
index 00000000000..cf51e2021e4
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff
new file mode 100644
index 00000000000..f5e5c623577
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff2 b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff2
new file mode 100644
index 00000000000..dd76d3488d5
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Bold.eot b/vendor/assets/fonts/KaTeX_Fraktur-Bold.eot
new file mode 100644
index 00000000000..1960b106656
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Fraktur-Bold.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Bold.ttf b/vendor/assets/fonts/KaTeX_Fraktur-Bold.ttf
new file mode 100644
index 00000000000..7b0790f1ae8
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Fraktur-Bold.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff b/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff
new file mode 100644
index 00000000000..dc325713291
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff2 b/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff2
new file mode 100644
index 00000000000..fdc429227ad
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Regular.eot b/vendor/assets/fonts/KaTeX_Fraktur-Regular.eot
new file mode 100644
index 00000000000..e4e73796aea
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Fraktur-Regular.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Regular.ttf b/vendor/assets/fonts/KaTeX_Fraktur-Regular.ttf
new file mode 100644
index 00000000000..063bc0263eb
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Fraktur-Regular.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff b/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff
new file mode 100644
index 00000000000..c4b18d863f3
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff2 b/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff2
new file mode 100644
index 00000000000..4318d938e26
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Bold.eot b/vendor/assets/fonts/KaTeX_Main-Bold.eot
new file mode 100644
index 00000000000..80fbd022363
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Main-Bold.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Bold.ttf b/vendor/assets/fonts/KaTeX_Main-Bold.ttf
new file mode 100644
index 00000000000..8e10722afae
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Main-Bold.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Bold.woff b/vendor/assets/fonts/KaTeX_Main-Bold.woff
new file mode 100644
index 00000000000..43b361a6005
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Main-Bold.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Bold.woff2 b/vendor/assets/fonts/KaTeX_Main-Bold.woff2
new file mode 100644
index 00000000000..af57a96c148
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Main-Bold.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Italic.eot b/vendor/assets/fonts/KaTeX_Main-Italic.eot
new file mode 100644
index 00000000000..fc770166b5e
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Main-Italic.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Italic.ttf b/vendor/assets/fonts/KaTeX_Main-Italic.ttf
new file mode 100644
index 00000000000..d124495d7b6
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Main-Italic.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Italic.woff b/vendor/assets/fonts/KaTeX_Main-Italic.woff
new file mode 100644
index 00000000000..e623236bc44
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Main-Italic.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Italic.woff2 b/vendor/assets/fonts/KaTeX_Main-Italic.woff2
new file mode 100644
index 00000000000..944e9740bdf
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Main-Italic.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Regular.eot b/vendor/assets/fonts/KaTeX_Main-Regular.eot
new file mode 100644
index 00000000000..dc60c090c7a
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Main-Regular.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Regular.ttf b/vendor/assets/fonts/KaTeX_Main-Regular.ttf
new file mode 100644
index 00000000000..da5797ffcce
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Main-Regular.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Regular.woff b/vendor/assets/fonts/KaTeX_Main-Regular.woff
new file mode 100644
index 00000000000..37db672e821
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Main-Regular.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Regular.woff2 b/vendor/assets/fonts/KaTeX_Main-Regular.woff2
new file mode 100644
index 00000000000..48820424893
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Main-Regular.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Math-BoldItalic.eot b/vendor/assets/fonts/KaTeX_Math-BoldItalic.eot
new file mode 100644
index 00000000000..52c8b8c6b40
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Math-BoldItalic.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Math-BoldItalic.ttf b/vendor/assets/fonts/KaTeX_Math-BoldItalic.ttf
new file mode 100644
index 00000000000..a8b527c7ef6
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Math-BoldItalic.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff b/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff
new file mode 100644
index 00000000000..8940e0b5801
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff2 b/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff2
new file mode 100644
index 00000000000..15cf56d3408
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Math-Italic.eot b/vendor/assets/fonts/KaTeX_Math-Italic.eot
new file mode 100644
index 00000000000..64c8992c477
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Math-Italic.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Math-Italic.ttf b/vendor/assets/fonts/KaTeX_Math-Italic.ttf
new file mode 100644
index 00000000000..06f39d3a299
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Math-Italic.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Math-Italic.woff b/vendor/assets/fonts/KaTeX_Math-Italic.woff
new file mode 100644
index 00000000000..cf3b4b79e5b
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Math-Italic.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Math-Italic.woff2 b/vendor/assets/fonts/KaTeX_Math-Italic.woff2
new file mode 100644
index 00000000000..5f8c4bfa455
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Math-Italic.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Math-Regular.eot b/vendor/assets/fonts/KaTeX_Math-Regular.eot
new file mode 100644
index 00000000000..5521e6a564d
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Math-Regular.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Math-Regular.ttf b/vendor/assets/fonts/KaTeX_Math-Regular.ttf
new file mode 100644
index 00000000000..73127082370
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Math-Regular.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Math-Regular.woff b/vendor/assets/fonts/KaTeX_Math-Regular.woff
new file mode 100644
index 00000000000..0e2ebdf18af
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Math-Regular.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Math-Regular.woff2 b/vendor/assets/fonts/KaTeX_Math-Regular.woff2
new file mode 100644
index 00000000000..ebe3d028a34
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Math-Regular.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Bold.eot b/vendor/assets/fonts/KaTeX_SansSerif-Bold.eot
new file mode 100644
index 00000000000..1660e76a2b6
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_SansSerif-Bold.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Bold.ttf b/vendor/assets/fonts/KaTeX_SansSerif-Bold.ttf
new file mode 100644
index 00000000000..dbeb7b92ab5
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_SansSerif-Bold.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff b/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff
new file mode 100644
index 00000000000..8f144a8bb31
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff2 b/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff2
new file mode 100644
index 00000000000..329e85557fa
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Italic.eot b/vendor/assets/fonts/KaTeX_SansSerif-Italic.eot
new file mode 100644
index 00000000000..289ae3ff8b7
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_SansSerif-Italic.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Italic.ttf b/vendor/assets/fonts/KaTeX_SansSerif-Italic.ttf
new file mode 100644
index 00000000000..b3a2f38f224
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_SansSerif-Italic.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff b/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff
new file mode 100644
index 00000000000..bddf7ea6579
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff2 b/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff2
new file mode 100644
index 00000000000..5fa767bddd6
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Regular.eot b/vendor/assets/fonts/KaTeX_SansSerif-Regular.eot
new file mode 100644
index 00000000000..1b38b98a180
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_SansSerif-Regular.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Regular.ttf b/vendor/assets/fonts/KaTeX_SansSerif-Regular.ttf
new file mode 100644
index 00000000000..e4712f84775
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_SansSerif-Regular.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff b/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff
new file mode 100644
index 00000000000..33be368048f
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff2 b/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff2
new file mode 100644
index 00000000000..4fcb2e29a05
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Script-Regular.eot b/vendor/assets/fonts/KaTeX_Script-Regular.eot
new file mode 100644
index 00000000000..7870d7f319b
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Script-Regular.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Script-Regular.ttf b/vendor/assets/fonts/KaTeX_Script-Regular.ttf
new file mode 100644
index 00000000000..da4d11308ae
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Script-Regular.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Script-Regular.woff b/vendor/assets/fonts/KaTeX_Script-Regular.woff
new file mode 100644
index 00000000000..d6ae79f998a
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Script-Regular.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Script-Regular.woff2 b/vendor/assets/fonts/KaTeX_Script-Regular.woff2
new file mode 100644
index 00000000000..1b43deb45a8
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Script-Regular.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size1-Regular.eot b/vendor/assets/fonts/KaTeX_Size1-Regular.eot
new file mode 100644
index 00000000000..29950f95ff6
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size1-Regular.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size1-Regular.ttf b/vendor/assets/fonts/KaTeX_Size1-Regular.ttf
new file mode 100644
index 00000000000..194466a655d
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size1-Regular.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size1-Regular.woff b/vendor/assets/fonts/KaTeX_Size1-Regular.woff
new file mode 100644
index 00000000000..237f271edd1
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size1-Regular.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size1-Regular.woff2 b/vendor/assets/fonts/KaTeX_Size1-Regular.woff2
new file mode 100644
index 00000000000..39b6f8f746c
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size1-Regular.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size2-Regular.eot b/vendor/assets/fonts/KaTeX_Size2-Regular.eot
new file mode 100644
index 00000000000..b8b0536f967
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size2-Regular.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size2-Regular.ttf b/vendor/assets/fonts/KaTeX_Size2-Regular.ttf
new file mode 100644
index 00000000000..b41b66a638f
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size2-Regular.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size2-Regular.woff b/vendor/assets/fonts/KaTeX_Size2-Regular.woff
new file mode 100644
index 00000000000..4a3055854ed
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size2-Regular.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size2-Regular.woff2 b/vendor/assets/fonts/KaTeX_Size2-Regular.woff2
new file mode 100644
index 00000000000..3facec1ab89
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size2-Regular.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size3-Regular.eot b/vendor/assets/fonts/KaTeX_Size3-Regular.eot
new file mode 100644
index 00000000000..576b864fae6
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size3-Regular.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size3-Regular.ttf b/vendor/assets/fonts/KaTeX_Size3-Regular.ttf
new file mode 100644
index 00000000000..790ddbbc55f
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size3-Regular.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size3-Regular.woff b/vendor/assets/fonts/KaTeX_Size3-Regular.woff
new file mode 100644
index 00000000000..3a6d062e660
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size3-Regular.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size3-Regular.woff2 b/vendor/assets/fonts/KaTeX_Size3-Regular.woff2
new file mode 100644
index 00000000000..2cffafe5018
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size3-Regular.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size4-Regular.eot b/vendor/assets/fonts/KaTeX_Size4-Regular.eot
new file mode 100644
index 00000000000..c2b045fc3db
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size4-Regular.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size4-Regular.ttf b/vendor/assets/fonts/KaTeX_Size4-Regular.ttf
new file mode 100644
index 00000000000..ce660aa7ff9
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size4-Regular.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size4-Regular.woff b/vendor/assets/fonts/KaTeX_Size4-Regular.woff
new file mode 100644
index 00000000000..7826c6c97a1
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size4-Regular.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size4-Regular.woff2 b/vendor/assets/fonts/KaTeX_Size4-Regular.woff2
new file mode 100644
index 00000000000..c92189812d9
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size4-Regular.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Typewriter-Regular.eot b/vendor/assets/fonts/KaTeX_Typewriter-Regular.eot
new file mode 100644
index 00000000000..4c178f484a8
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Typewriter-Regular.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Typewriter-Regular.ttf b/vendor/assets/fonts/KaTeX_Typewriter-Regular.ttf
new file mode 100644
index 00000000000..b0427ad0a56
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Typewriter-Regular.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff b/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff
new file mode 100644
index 00000000000..78e990488a9
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff2 b/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff2
new file mode 100644
index 00000000000..618de99d480
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff2
Binary files differ
diff --git a/vendor/assets/javascripts/katex.js b/vendor/assets/javascripts/katex.js
new file mode 100644
index 00000000000..beb31ca6a7e
--- /dev/null
+++ b/vendor/assets/javascripts/katex.js
@@ -0,0 +1,8683 @@
+/*
+ The MIT License (MIT)
+
+ Copyright (c) 2015 Khan Academy
+
+ This software also uses portions of the underscore.js project, which is
+ MIT licensed with the following copyright:
+
+ Copyright (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative
+ Reporters & Editors
+
+ 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.
+ */
+
+/*
+ Here is how to build a version of KaTeX that works with gitlab.
+
+ The problem is that the standard procedure for changing font location doesn't work for the empty string.
+
+ 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.
+*/
+
+(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){
+/* eslint no-console:0 */
+/**
+ * This is the main entry point for KaTeX. Here, we expose functions for
+ * rendering expressions either to DOM nodes or to markup strings.
+ *
+ * We also expose the ParseError class to check if errors thrown from KaTeX are
+ * errors in the expression, or errors in javascript handling.
+ */
+
+var ParseError = require("./src/ParseError");
+var Settings = require("./src/Settings");
+
+var buildTree = require("./src/buildTree");
+var parseTree = require("./src/parseTree");
+var utils = require("./src/utils");
+
+/**
+ * Parse and build an expression, and place that expression in the DOM node
+ * given.
+ */
+var render = function(expression, baseNode, options) {
+ utils.clearNode(baseNode);
+
+ var settings = new Settings(options);
+
+ var tree = parseTree(expression, settings);
+ var node = buildTree(tree, expression, settings).toNode();
+
+ baseNode.appendChild(node);
+};
+
+// KaTeX's styles don't work properly in quirks mode. Print out an error, and
+// disable rendering.
+if (typeof document !== "undefined") {
+ if (document.compatMode !== "CSS1Compat") {
+ typeof console !== "undefined" && console.warn(
+ "Warning: KaTeX doesn't work in quirks mode. Make sure your " +
+ "website has a suitable doctype.");
+
+ render = function() {
+ throw new ParseError("KaTeX doesn't work in quirks mode.");
+ };
+ }
+}
+
+/**
+ * Parse and build an expression, and return the markup for that.
+ */
+var renderToString = function(expression, options) {
+ var settings = new Settings(options);
+
+ var tree = parseTree(expression, settings);
+ return buildTree(tree, expression, settings).toMarkup();
+};
+
+/**
+ * Parse an expression and return the parse tree.
+ */
+var generateParseTree = function(expression, options) {
+ var settings = new Settings(options);
+ return parseTree(expression, settings);
+};
+
+module.exports = {
+ render: render,
+ renderToString: renderToString,
+ /**
+ * NOTE: This method is not currently recommended for public use.
+ * The internal tree representation is unstable and is very likely
+ * to change. Use at your own risk.
+ */
+ __parse: generateParseTree,
+ ParseError: ParseError,
+};
+
+},{"./src/ParseError":6,"./src/Settings":8,"./src/buildTree":13,"./src/parseTree":22,"./src/utils":25}],2:[function(require,module,exports){
+/** @flow */
+
+"use strict";
+
+function getRelocatable(re) {
+ // In the future, this could use a WeakMap instead of an expando.
+ if (!re.__matchAtRelocatable) {
+ // Disjunctions are the lowest-precedence operator, so we can make any
+ // pattern match the empty string by appending `|()` to it:
+ // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-patterns
+ var source = re.source + "|()";
+
+ // We always make the new regex global.
+ var flags = "g" + (re.ignoreCase ? "i" : "") + (re.multiline ? "m" : "") + (re.unicode ? "u" : "")
+ // sticky (/.../y) doesn't make sense in conjunction with our relocation
+ // logic, so we ignore it here.
+ ;
+
+ re.__matchAtRelocatable = new RegExp(source, flags);
+ }
+ return re.__matchAtRelocatable;
+}
+
+function matchAt(re, str, pos) {
+ if (re.global || re.sticky) {
+ throw new Error("matchAt(...): Only non-global regexes are supported");
+ }
+ var reloc = getRelocatable(re);
+ reloc.lastIndex = pos;
+ var match = reloc.exec(str);
+ // Last capturing group is our sentinel that indicates whether the regex
+ // matched at the given location.
+ if (match[match.length - 1] == null) {
+ // Original regex matched.
+ match.length = match.length - 1;
+ return match;
+ } else {
+ return null;
+ }
+}
+
+module.exports = matchAt;
+},{}],3:[function(require,module,exports){
+/**
+ * The Lexer class handles tokenizing the input in various ways. Since our
+ * parser expects us to be able to backtrack, the lexer allows lexing from any
+ * given starting point.
+ *
+ * Its main exposed function is the `lex` function, which takes a position to
+ * lex from and a type of token to lex. It defers to the appropriate `_innerLex`
+ * function.
+ *
+ * The various `_innerLex` functions perform the actual lexing of different
+ * kinds.
+ */
+
+var matchAt = require("match-at");
+
+var ParseError = require("./ParseError");
+
+// The main lexer class
+function Lexer(input) {
+ this.input = input;
+ this.pos = 0;
+}
+
+/**
+ * The resulting token returned from `lex`.
+ *
+ * It consists of the token text plus some position information.
+ * The position information is essentially a range in an input string,
+ * but instead of referencing the bare input string, we refer to the lexer.
+ * That way it is possible to attach extra metadata to the input string,
+ * like for example a file name or similar.
+ *
+ * The position information (all three parameters) is optional,
+ * so it is OK to construct synthetic tokens if appropriate.
+ * Not providing available position information may lead to
+ * degraded error reporting, though.
+ *
+ * @param {string} text the text of this token
+ * @param {number=} start the start offset, zero-based inclusive
+ * @param {number=} end the end offset, zero-based exclusive
+ * @param {Lexer=} lexer the lexer which in turn holds the input string
+ */
+function Token(text, start, end, lexer) {
+ this.text = text;
+ this.start = start;
+ this.end = end;
+ this.lexer = lexer;
+}
+
+/**
+ * Given a pair of tokens (this and endToken), compute a “Token” encompassing
+ * the whole input range enclosed by these two.
+ *
+ * @param {Token} endToken last token of the range, inclusive
+ * @param {string} text the text of the newly constructed token
+ */
+Token.prototype.range = function(endToken, text) {
+ if (endToken.lexer !== this.lexer) {
+ return new Token(text); // sorry, no position information available
+ }
+ return new Token(text, this.start, endToken.end, this.lexer);
+};
+
+/* The following tokenRegex
+ * - matches typical whitespace (but not NBSP etc.) using its first group
+ * - does not match any control character \x00-\x1f except whitespace
+ * - does not match a bare backslash
+ * - matches any ASCII character except those just mentioned
+ * - does not match the BMP private use area \uE000-\uF8FF
+ * - does not match bare surrogate code units
+ * - matches any BMP character except for those just described
+ * - matches any valid Unicode surrogate pair
+ * - matches a backslash followed by one or more letters
+ * - matches a backslash followed by any BMP character, including newline
+ * Just because the Lexer matches something doesn't mean it's valid input:
+ * If there is no matching function or symbol definition, the Parser will
+ * still reject the input.
+ */
+var tokenRegex = new RegExp(
+ "([ \r\n\t]+)|" + // whitespace
+ "([!-\\[\\]-\u2027\u202A-\uD7FF\uF900-\uFFFF]" + // single codepoint
+ "|[\uD800-\uDBFF][\uDC00-\uDFFF]" + // surrogate pair
+ "|\\\\(?:[a-zA-Z]+|[^\uD800-\uDFFF])" + // function name
+ ")"
+);
+
+/**
+ * This function lexes a single token.
+ */
+Lexer.prototype.lex = function() {
+ var input = this.input;
+ var pos = this.pos;
+ if (pos === input.length) {
+ return new Token("EOF", pos, pos, this);
+ }
+ var match = matchAt(tokenRegex, input, pos);
+ if (match === null) {
+ throw new ParseError(
+ "Unexpected character: '" + input[pos] + "'",
+ new Token(input[pos], pos, pos + 1, this));
+ }
+ var text = match[2] || " ";
+ var start = this.pos;
+ this.pos += match[0].length;
+ var end = this.pos;
+ return new Token(text, start, end, this);
+};
+
+module.exports = Lexer;
+
+},{"./ParseError":6,"match-at":2}],4:[function(require,module,exports){
+/**
+ * This file contains the “gullet” where macros are expanded
+ * until only non-macro tokens remain.
+ */
+
+var Lexer = require("./Lexer");
+
+function MacroExpander(input, macros) {
+ this.lexer = new Lexer(input);
+ this.macros = macros;
+ this.stack = []; // contains tokens in REVERSE order
+ this.discardedWhiteSpace = [];
+}
+
+/**
+ * Recursively expand first token, then return first non-expandable token.
+ */
+MacroExpander.prototype.nextToken = function() {
+ for (;;) {
+ if (this.stack.length === 0) {
+ this.stack.push(this.lexer.lex());
+ }
+ var topToken = this.stack.pop();
+ var name = topToken.text;
+ if (!(name.charAt(0) === "\\" && this.macros.hasOwnProperty(name))) {
+ return topToken;
+ }
+ var expansion = this.macros[name];
+ if (typeof expansion === "string") {
+ var bodyLexer = new Lexer(expansion);
+ expansion = [];
+ var tok = bodyLexer.lex();
+ while (tok.text !== "EOF") {
+ expansion.push(tok);
+ tok = bodyLexer.lex();
+ }
+ expansion.reverse(); // to fit in with stack using push and pop
+ this.macros[name] = expansion;
+ }
+ this.stack = this.stack.concat(expansion);
+ }
+};
+
+MacroExpander.prototype.get = function(ignoreSpace) {
+ this.discardedWhiteSpace = [];
+ var token = this.nextToken();
+ if (ignoreSpace) {
+ while (token.text === " ") {
+ this.discardedWhiteSpace.push(token);
+ token = this.nextToken();
+ }
+ }
+ return token;
+};
+
+/**
+ * Undo the effect of the preceding call to the get method.
+ * A call to this method MUST be immediately preceded and immediately followed
+ * by a call to get. Only used during mode switching, i.e. after one token
+ * was got in the old mode but should get got again in a new mode
+ * with possibly different whitespace handling.
+ */
+MacroExpander.prototype.unget = function(token) {
+ this.stack.push(token);
+ while (this.discardedWhiteSpace.length !== 0) {
+ this.stack.push(this.discardedWhiteSpace.pop());
+ }
+};
+
+module.exports = MacroExpander;
+
+},{"./Lexer":3}],5:[function(require,module,exports){
+/**
+ * This file contains information about the options that the Parser carries
+ * around with it while parsing. Data is held in an `Options` object, and when
+ * recursing, a new `Options` object can be created with the `.with*` and
+ * `.reset` functions.
+ */
+
+/**
+ * This is the main options class. It contains the style, size, color, and font
+ * of the current parse level. It also contains the style and size of the parent
+ * parse level, so size changes can be handled efficiently.
+ *
+ * Each of the `.with*` and `.reset` functions passes its current style and size
+ * as the parentStyle and parentSize of the new options class, so parent
+ * handling is taken care of automatically.
+ */
+function Options(data) {
+ this.style = data.style;
+ this.color = data.color;
+ this.size = data.size;
+ this.phantom = data.phantom;
+ this.font = data.font;
+
+ if (data.parentStyle === undefined) {
+ this.parentStyle = data.style;
+ } else {
+ this.parentStyle = data.parentStyle;
+ }
+
+ if (data.parentSize === undefined) {
+ this.parentSize = data.size;
+ } else {
+ this.parentSize = data.parentSize;
+ }
+}
+
+/**
+ * Returns a new options object with the same properties as "this". Properties
+ * from "extension" will be copied to the new options object.
+ */
+Options.prototype.extend = function(extension) {
+ var data = {
+ style: this.style,
+ size: this.size,
+ color: this.color,
+ parentStyle: this.style,
+ parentSize: this.size,
+ phantom: this.phantom,
+ font: this.font,
+ };
+
+ for (var key in extension) {
+ if (extension.hasOwnProperty(key)) {
+ data[key] = extension[key];
+ }
+ }
+
+ return new Options(data);
+};
+
+/**
+ * Create a new options object with the given style.
+ */
+Options.prototype.withStyle = function(style) {
+ return this.extend({
+ style: style,
+ });
+};
+
+/**
+ * Create a new options object with the given size.
+ */
+Options.prototype.withSize = function(size) {
+ return this.extend({
+ size: size,
+ });
+};
+
+/**
+ * Create a new options object with the given color.
+ */
+Options.prototype.withColor = function(color) {
+ return this.extend({
+ color: color,
+ });
+};
+
+/**
+ * Create a new options object with "phantom" set to true.
+ */
+Options.prototype.withPhantom = function() {
+ return this.extend({
+ phantom: true,
+ });
+};
+
+/**
+ * Create a new options objects with the give font.
+ */
+Options.prototype.withFont = function(font) {
+ return this.extend({
+ font: font,
+ });
+};
+
+/**
+ * Create a new options object with the same style, size, and color. This is
+ * used so that parent style and size changes are handled correctly.
+ */
+Options.prototype.reset = function() {
+ return this.extend({});
+};
+
+/**
+ * A map of color names to CSS colors.
+ * TODO(emily): Remove this when we have real macros
+ */
+var colorMap = {
+ "katex-blue": "#6495ed",
+ "katex-orange": "#ffa500",
+ "katex-pink": "#ff00af",
+ "katex-red": "#df0030",
+ "katex-green": "#28ae7b",
+ "katex-gray": "gray",
+ "katex-purple": "#9d38bd",
+ "katex-blueA": "#ccfaff",
+ "katex-blueB": "#80f6ff",
+ "katex-blueC": "#63d9ea",
+ "katex-blueD": "#11accd",
+ "katex-blueE": "#0c7f99",
+ "katex-tealA": "#94fff5",
+ "katex-tealB": "#26edd5",
+ "katex-tealC": "#01d1c1",
+ "katex-tealD": "#01a995",
+ "katex-tealE": "#208170",
+ "katex-greenA": "#b6ffb0",
+ "katex-greenB": "#8af281",
+ "katex-greenC": "#74cf70",
+ "katex-greenD": "#1fab54",
+ "katex-greenE": "#0d923f",
+ "katex-goldA": "#ffd0a9",
+ "katex-goldB": "#ffbb71",
+ "katex-goldC": "#ff9c39",
+ "katex-goldD": "#e07d10",
+ "katex-goldE": "#a75a05",
+ "katex-redA": "#fca9a9",
+ "katex-redB": "#ff8482",
+ "katex-redC": "#f9685d",
+ "katex-redD": "#e84d39",
+ "katex-redE": "#bc2612",
+ "katex-maroonA": "#ffbde0",
+ "katex-maroonB": "#ff92c6",
+ "katex-maroonC": "#ed5fa6",
+ "katex-maroonD": "#ca337c",
+ "katex-maroonE": "#9e034e",
+ "katex-purpleA": "#ddd7ff",
+ "katex-purpleB": "#c6b9fc",
+ "katex-purpleC": "#aa87ff",
+ "katex-purpleD": "#7854ab",
+ "katex-purpleE": "#543b78",
+ "katex-mintA": "#f5f9e8",
+ "katex-mintB": "#edf2df",
+ "katex-mintC": "#e0e5cc",
+ "katex-grayA": "#f6f7f7",
+ "katex-grayB": "#f0f1f2",
+ "katex-grayC": "#e3e5e6",
+ "katex-grayD": "#d6d8da",
+ "katex-grayE": "#babec2",
+ "katex-grayF": "#888d93",
+ "katex-grayG": "#626569",
+ "katex-grayH": "#3b3e40",
+ "katex-grayI": "#21242c",
+ "katex-kaBlue": "#314453",
+ "katex-kaGreen": "#71B307",
+};
+
+/**
+ * Gets the CSS color of the current options object, accounting for the
+ * `colorMap`.
+ */
+Options.prototype.getColor = function() {
+ if (this.phantom) {
+ return "transparent";
+ } else {
+ return colorMap[this.color] || this.color;
+ }
+};
+
+module.exports = Options;
+
+},{}],6:[function(require,module,exports){
+/**
+ * This is the ParseError class, which is the main error thrown by KaTeX
+ * functions when something has gone wrong. This is used to distinguish internal
+ * errors from errors in the expression that the user provided.
+ *
+ * If possible, a caller should provide a Token or ParseNode with information
+ * about where in the source string the problem occurred.
+ *
+ * @param {string} message The error message
+ * @param {(Token|ParseNode)=} token An object providing position information
+ */
+function ParseError(message, token) {
+ var error = "KaTeX parse error: " + message;
+ var start;
+ var end;
+
+ if (token && token.lexer && token.start <= token.end) {
+ // If we have the input and a position, make the error a bit fancier
+
+ // Get the input
+ var input = token.lexer.input;
+
+ // Prepend some information
+ start = token.start;
+ end = token.end;
+ if (start === input.length) {
+ error += " at end of input: ";
+ } else {
+ error += " at position " + (start + 1) + ": ";
+ }
+
+ // Underline token in question using combining underscores
+ var underlined = input.slice(start, end).replace(/[^]/g, "$&\u0332");
+
+ // Extract some context from the input and add it to the error
+ var left;
+ if (start > 15) {
+ left = "…" + input.slice(start - 15, start);
+ } else {
+ left = input.slice(0, start);
+ }
+ var right;
+ if (end + 15 < input.length) {
+ right = input.slice(end, end + 15) + "…";
+ } else {
+ right = input.slice(end);
+ }
+ error += left + underlined + right;
+ }
+
+ // Some hackery to make ParseError a prototype of Error
+ // See http://stackoverflow.com/a/8460753
+ var self = new Error(error);
+ self.name = "ParseError";
+ self.__proto__ = ParseError.prototype;
+
+ self.position = start;
+ return self;
+}
+
+// More hackery
+ParseError.prototype.__proto__ = Error.prototype;
+
+module.exports = ParseError;
+
+},{}],7:[function(require,module,exports){
+/* eslint no-constant-condition:0 */
+var functions = require("./functions");
+var environments = require("./environments");
+var MacroExpander = require("./MacroExpander");
+var symbols = require("./symbols");
+var utils = require("./utils");
+var cjkRegex = require("./unicodeRegexes").cjkRegex;
+
+var parseData = require("./parseData");
+var ParseError = require("./ParseError");
+
+/**
+ * This file contains the parser used to parse out a TeX expression from the
+ * input. Since TeX isn't context-free, standard parsers don't work particularly
+ * well.
+ *
+ * The strategy of this parser is as such:
+ *
+ * The main functions (the `.parse...` ones) take a position in the current
+ * parse string to parse tokens from. The lexer (found in Lexer.js, stored at
+ * this.lexer) also supports pulling out tokens at arbitrary places. When
+ * individual tokens are needed at a position, the lexer is called to pull out a
+ * token, which is then used.
+ *
+ * The parser has a property called "mode" indicating the mode that
+ * the parser is currently in. Currently it has to be one of "math" or
+ * "text", which denotes whether the current environment is a math-y
+ * one or a text-y one (e.g. inside \text). Currently, this serves to
+ * limit the functions which can be used in text mode.
+ *
+ * The main functions then return an object which contains the useful data that
+ * was parsed at its given point, and a new position at the end of the parsed
+ * data. The main functions can call each other and continue the parsing by
+ * using the returned position as a new starting point.
+ *
+ * There are also extra `.handle...` functions, which pull out some reused
+ * functionality into self-contained functions.
+ *
+ * The earlier functions return ParseNodes.
+ * The later functions (which are called deeper in the parse) sometimes return
+ * ParseFuncOrArgument, which contain a ParseNode as well as some data about
+ * whether the parsed object is a function which is missing some arguments, or a
+ * standalone object which can be used as an argument to another function.
+ */
+
+/**
+ * Main Parser class
+ */
+function Parser(input, settings) {
+ // Create a new macro expander (gullet) and (indirectly via that) also a
+ // new lexer (mouth) for this parser (stomach, in the language of TeX)
+ this.gullet = new MacroExpander(input, settings.macros);
+ // Store the settings for use in parsing
+ this.settings = settings;
+}
+
+var ParseNode = parseData.ParseNode;
+
+/**
+ * An initial function (without its arguments), or an argument to a function.
+ * The `result` argument should be a ParseNode.
+ */
+function ParseFuncOrArgument(result, isFunction, token) {
+ this.result = result;
+ // Is this a function (i.e. is it something defined in functions.js)?
+ this.isFunction = isFunction;
+ this.token = token;
+}
+
+/**
+ * Checks a result to make sure it has the right type, and throws an
+ * appropriate error otherwise.
+ *
+ * @param {boolean=} consume whether to consume the expected token,
+ * defaults to true
+ */
+Parser.prototype.expect = function(text, consume) {
+ if (this.nextToken.text !== text) {
+ throw new ParseError(
+ "Expected '" + text + "', got '" + this.nextToken.text + "'",
+ this.nextToken
+ );
+ }
+ if (consume !== false) {
+ this.consume();
+ }
+};
+
+/**
+ * Considers the current look ahead token as consumed,
+ * and fetches the one after that as the new look ahead.
+ */
+Parser.prototype.consume = function() {
+ this.nextToken = this.gullet.get(this.mode === "math");
+};
+
+Parser.prototype.switchMode = function(newMode) {
+ this.gullet.unget(this.nextToken);
+ this.mode = newMode;
+ this.consume();
+};
+
+/**
+ * Main parsing function, which parses an entire input.
+ *
+ * @return {?Array.<ParseNode>}
+ */
+Parser.prototype.parse = function() {
+ // Try to parse the input
+ this.mode = "math";
+ this.consume();
+ var parse = this.parseInput();
+ return parse;
+};
+
+/**
+ * Parses an entire input tree.
+ */
+Parser.prototype.parseInput = function() {
+ // Parse an expression
+ var expression = this.parseExpression(false);
+ // If we succeeded, make sure there's an EOF at the end
+ this.expect("EOF", false);
+ return expression;
+};
+
+var endOfExpression = ["}", "\\end", "\\right", "&", "\\\\", "\\cr"];
+
+/**
+ * Parses an "expression", which is a list of atoms.
+ *
+ * @param {boolean} breakOnInfix Should the parsing stop when we hit infix
+ * nodes? This happens when functions have higher precendence
+ * than infix nodes in implicit parses.
+ *
+ * @param {?string} breakOnTokenText The text of the token that the expression
+ * should end with, or `null` if something else should end the
+ * expression.
+ *
+ * @return {ParseNode}
+ */
+Parser.prototype.parseExpression = function(breakOnInfix, breakOnTokenText) {
+ var body = [];
+ // Keep adding atoms to the body until we can't parse any more atoms (either
+ // we reached the end, a }, or a \right)
+ while (true) {
+ var lex = this.nextToken;
+ if (endOfExpression.indexOf(lex.text) !== -1) {
+ break;
+ }
+ if (breakOnTokenText && lex.text === breakOnTokenText) {
+ break;
+ }
+ if (breakOnInfix && functions[lex.text] && functions[lex.text].infix) {
+ break;
+ }
+ var atom = this.parseAtom();
+ if (!atom) {
+ if (!this.settings.throwOnError && lex.text[0] === "\\") {
+ var errorNode = this.handleUnsupportedCmd();
+ body.push(errorNode);
+ continue;
+ }
+
+ break;
+ }
+ body.push(atom);
+ }
+ return this.handleInfixNodes(body);
+};
+
+/**
+ * Rewrites infix operators such as \over with corresponding commands such
+ * as \frac.
+ *
+ * There can only be one infix operator per group. If there's more than one
+ * then the expression is ambiguous. This can be resolved by adding {}.
+ *
+ * @returns {Array}
+ */
+Parser.prototype.handleInfixNodes = function(body) {
+ var overIndex = -1;
+ var funcName;
+
+ for (var i = 0; i < body.length; i++) {
+ var node = body[i];
+ if (node.type === "infix") {
+ if (overIndex !== -1) {
+ throw new ParseError(
+ "only one infix operator per group",
+ node.value.token);
+ }
+ overIndex = i;
+ funcName = node.value.replaceWith;
+ }
+ }
+
+ if (overIndex !== -1) {
+ var numerNode;
+ var denomNode;
+
+ var numerBody = body.slice(0, overIndex);
+ var denomBody = body.slice(overIndex + 1);
+
+ if (numerBody.length === 1 && numerBody[0].type === "ordgroup") {
+ numerNode = numerBody[0];
+ } else {
+ numerNode = new ParseNode("ordgroup", numerBody, this.mode);
+ }
+
+ if (denomBody.length === 1 && denomBody[0].type === "ordgroup") {
+ denomNode = denomBody[0];
+ } else {
+ denomNode = new ParseNode("ordgroup", denomBody, this.mode);
+ }
+
+ var value = this.callFunction(
+ funcName, [numerNode, denomNode], null);
+ return [new ParseNode(value.type, value, this.mode)];
+ } else {
+ return body;
+ }
+};
+
+// The greediness of a superscript or subscript
+var SUPSUB_GREEDINESS = 1;
+
+/**
+ * Handle a subscript or superscript with nice errors.
+ */
+Parser.prototype.handleSupSubscript = function(name) {
+ var symbolToken = this.nextToken;
+ var symbol = symbolToken.text;
+ this.consume();
+ var group = this.parseGroup();
+
+ if (!group) {
+ if (!this.settings.throwOnError && this.nextToken.text[0] === "\\") {
+ return this.handleUnsupportedCmd();
+ } else {
+ throw new ParseError(
+ "Expected group after '" + symbol + "'",
+ symbolToken
+ );
+ }
+ } else if (group.isFunction) {
+ // ^ and _ have a greediness, so handle interactions with functions'
+ // greediness
+ var funcGreediness = functions[group.result].greediness;
+ if (funcGreediness > SUPSUB_GREEDINESS) {
+ return this.parseFunction(group);
+ } else {
+ throw new ParseError(
+ "Got function '" + group.result + "' with no arguments " +
+ "as " + name, symbolToken);
+ }
+ } else {
+ return group.result;
+ }
+};
+
+/**
+ * Converts the textual input of an unsupported command into a text node
+ * contained within a color node whose color is determined by errorColor
+ */
+Parser.prototype.handleUnsupportedCmd = function() {
+ var text = this.nextToken.text;
+ var textordArray = [];
+
+ for (var i = 0; i < text.length; i++) {
+ textordArray.push(new ParseNode("textord", text[i], "text"));
+ }
+
+ var textNode = new ParseNode(
+ "text",
+ {
+ body: textordArray,
+ type: "text",
+ },
+ this.mode);
+
+ var colorNode = new ParseNode(
+ "color",
+ {
+ color: this.settings.errorColor,
+ value: [textNode],
+ type: "color",
+ },
+ this.mode);
+
+ this.consume();
+ return colorNode;
+};
+
+/**
+ * Parses a group with optional super/subscripts.
+ *
+ * @return {?ParseNode}
+ */
+Parser.prototype.parseAtom = function() {
+ // The body of an atom is an implicit group, so that things like
+ // \left(x\right)^2 work correctly.
+ var base = this.parseImplicitGroup();
+
+ // In text mode, we don't have superscripts or subscripts
+ if (this.mode === "text") {
+ return base;
+ }
+
+ // Note that base may be empty (i.e. null) at this point.
+
+ var superscript;
+ var subscript;
+ while (true) {
+ // Lex the first token
+ var lex = this.nextToken;
+
+ if (lex.text === "\\limits" || lex.text === "\\nolimits") {
+ // We got a limit control
+ if (!base || base.type !== "op") {
+ throw new ParseError(
+ "Limit controls must follow a math operator",
+ lex);
+ } else {
+ var limits = lex.text === "\\limits";
+ base.value.limits = limits;
+ base.value.alwaysHandleSupSub = true;
+ }
+ this.consume();
+ } else if (lex.text === "^") {
+ // We got a superscript start
+ if (superscript) {
+ throw new ParseError("Double superscript", lex);
+ }
+ superscript = this.handleSupSubscript("superscript");
+ } else if (lex.text === "_") {
+ // We got a subscript start
+ if (subscript) {
+ throw new ParseError("Double subscript", lex);
+ }
+ subscript = this.handleSupSubscript("subscript");
+ } else if (lex.text === "'") {
+ // We got a prime
+ var prime = new ParseNode("textord", "\\prime", this.mode);
+
+ // Many primes can be grouped together, so we handle this here
+ var primes = [prime];
+ this.consume();
+ // Keep lexing tokens until we get something that's not a prime
+ while (this.nextToken.text === "'") {
+ // For each one, add another prime to the list
+ primes.push(prime);
+ this.consume();
+ }
+ // Put them into an ordgroup as the superscript
+ superscript = new ParseNode("ordgroup", primes, this.mode);
+ } else {
+ // If it wasn't ^, _, or ', stop parsing super/subscripts
+ break;
+ }
+ }
+
+ if (superscript || subscript) {
+ // If we got either a superscript or subscript, create a supsub
+ return new ParseNode("supsub", {
+ base: base,
+ sup: superscript,
+ sub: subscript,
+ }, this.mode);
+ } else {
+ // Otherwise return the original body
+ return base;
+ }
+};
+
+// A list of the size-changing functions, for use in parseImplicitGroup
+var sizeFuncs = [
+ "\\tiny", "\\scriptsize", "\\footnotesize", "\\small", "\\normalsize",
+ "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge",
+];
+
+// A list of the style-changing functions, for use in parseImplicitGroup
+var styleFuncs = [
+ "\\displaystyle", "\\textstyle", "\\scriptstyle", "\\scriptscriptstyle",
+];
+
+/**
+ * Parses an implicit group, which is a group that starts at the end of a
+ * specified, and ends right before a higher explicit group ends, or at EOL. It
+ * is used for functions that appear to affect the current style, like \Large or
+ * \textrm, where instead of keeping a style we just pretend that there is an
+ * implicit grouping after it until the end of the group. E.g.
+ * small text {\Large large text} small text again
+ * It is also used for \left and \right to get the correct grouping.
+ *
+ * @return {?ParseNode}
+ */
+Parser.prototype.parseImplicitGroup = function() {
+ var start = this.parseSymbol();
+
+ if (start == null) {
+ // If we didn't get anything we handle, fall back to parseFunction
+ return this.parseFunction();
+ }
+
+ var func = start.result;
+ var body;
+
+ if (func === "\\left") {
+ // If we see a left:
+ // Parse the entire left function (including the delimiter)
+ var left = this.parseFunction(start);
+ // Parse out the implicit body
+ body = this.parseExpression(false);
+ // Check the next token
+ this.expect("\\right", false);
+ var right = this.parseFunction();
+ return new ParseNode("leftright", {
+ body: body,
+ left: left.value.value,
+ right: right.value.value,
+ }, this.mode);
+ } else if (func === "\\begin") {
+ // begin...end is similar to left...right
+ var begin = this.parseFunction(start);
+ var envName = begin.value.name;
+ if (!environments.hasOwnProperty(envName)) {
+ throw new ParseError(
+ "No such environment: " + envName, begin.value.nameGroup);
+ }
+ // Build the environment object. Arguments and other information will
+ // be made available to the begin and end methods using properties.
+ var env = environments[envName];
+ var args = this.parseArguments("\\begin{" + envName + "}", env);
+ var context = {
+ mode: this.mode,
+ envName: envName,
+ parser: this,
+ positions: args.pop(),
+ };
+ var result = env.handler(context, args);
+ this.expect("\\end", false);
+ var endNameToken = this.nextToken;
+ var end = this.parseFunction();
+ if (end.value.name !== envName) {
+ throw new ParseError(
+ "Mismatch: \\begin{" + envName + "} matched " +
+ "by \\end{" + end.value.name + "}",
+ endNameToken);
+ }
+ result.position = end.position;
+ return result;
+ } else if (utils.contains(sizeFuncs, func)) {
+ // If we see a sizing function, parse out the implict body
+ body = this.parseExpression(false);
+ return new ParseNode("sizing", {
+ // Figure out what size to use based on the list of functions above
+ size: "size" + (utils.indexOf(sizeFuncs, func) + 1),
+ value: body,
+ }, this.mode);
+ } else if (utils.contains(styleFuncs, func)) {
+ // If we see a styling function, parse out the implict body
+ body = this.parseExpression(true);
+ return new ParseNode("styling", {
+ // Figure out what style to use by pulling out the style from
+ // the function name
+ style: func.slice(1, func.length - 5),
+ value: body,
+ }, this.mode);
+ } else {
+ // Defer to parseFunction if it's not a function we handle
+ return this.parseFunction(start);
+ }
+};
+
+/**
+ * Parses an entire function, including its base and all of its arguments.
+ * The base might either have been parsed already, in which case
+ * it is provided as an argument, or it's the next group in the input.
+ *
+ * @param {ParseFuncOrArgument=} baseGroup optional as described above
+ * @return {?ParseNode}
+ */
+Parser.prototype.parseFunction = function(baseGroup) {
+ if (!baseGroup) {
+ baseGroup = this.parseGroup();
+ }
+
+ if (baseGroup) {
+ if (baseGroup.isFunction) {
+ var func = baseGroup.result;
+ var funcData = functions[func];
+ if (this.mode === "text" && !funcData.allowedInText) {
+ throw new ParseError(
+ "Can't use function '" + func + "' in text mode",
+ baseGroup.token);
+ }
+
+ var args = this.parseArguments(func, funcData);
+ var token = baseGroup.token;
+ var result = this.callFunction(func, args, args.pop(), token);
+ return new ParseNode(result.type, result, this.mode);
+ } else {
+ return baseGroup.result;
+ }
+ } else {
+ return null;
+ }
+};
+
+/**
+ * Call a function handler with a suitable context and arguments.
+ */
+Parser.prototype.callFunction = function(name, args, positions, token) {
+ var context = {
+ funcName: name,
+ parser: this,
+ positions: positions,
+ token: token,
+ };
+ return functions[name].handler(context, args);
+};
+
+/**
+ * Parses the arguments of a function or environment
+ *
+ * @param {string} func "\name" or "\begin{name}"
+ * @param {{numArgs:number,numOptionalArgs:number|undefined}} funcData
+ * @return the array of arguments, with the list of positions as last element
+ */
+Parser.prototype.parseArguments = function(func, funcData) {
+ var totalArgs = funcData.numArgs + funcData.numOptionalArgs;
+ if (totalArgs === 0) {
+ return [[this.pos]];
+ }
+
+ var baseGreediness = funcData.greediness;
+ var positions = [this.pos];
+ var args = [];
+
+ for (var i = 0; i < totalArgs; i++) {
+ var nextToken = this.nextToken;
+ var argType = funcData.argTypes && funcData.argTypes[i];
+ var arg;
+ if (i < funcData.numOptionalArgs) {
+ if (argType) {
+ arg = this.parseGroupOfType(argType, true);
+ } else {
+ arg = this.parseGroup(true);
+ }
+ if (!arg) {
+ args.push(null);
+ positions.push(this.pos);
+ continue;
+ }
+ } else {
+ if (argType) {
+ arg = this.parseGroupOfType(argType);
+ } else {
+ arg = this.parseGroup();
+ }
+ if (!arg) {
+ if (!this.settings.throwOnError &&
+ this.nextToken.text[0] === "\\") {
+ arg = new ParseFuncOrArgument(
+ this.handleUnsupportedCmd(this.nextToken.text),
+ false);
+ } else {
+ throw new ParseError(
+ "Expected group after '" + func + "'", nextToken);
+ }
+ }
+ }
+ var argNode;
+ if (arg.isFunction) {
+ var argGreediness =
+ functions[arg.result].greediness;
+ if (argGreediness > baseGreediness) {
+ argNode = this.parseFunction(arg);
+ } else {
+ throw new ParseError(
+ "Got function '" + arg.result + "' as " +
+ "argument to '" + func + "'", nextToken);
+ }
+ } else {
+ argNode = arg.result;
+ }
+ args.push(argNode);
+ positions.push(this.pos);
+ }
+
+ args.push(positions);
+
+ return args;
+};
+
+
+/**
+ * Parses a group when the mode is changing.
+ *
+ * @return {?ParseFuncOrArgument}
+ */
+Parser.prototype.parseGroupOfType = function(innerMode, optional) {
+ var outerMode = this.mode;
+ // Handle `original` argTypes
+ if (innerMode === "original") {
+ innerMode = outerMode;
+ }
+
+ if (innerMode === "color") {
+ return this.parseColorGroup(optional);
+ }
+ if (innerMode === "size") {
+ return this.parseSizeGroup(optional);
+ }
+
+ this.switchMode(innerMode);
+ if (innerMode === "text") {
+ // text mode is special because it should ignore the whitespace before
+ // it
+ while (this.nextToken.text === " ") {
+ this.consume();
+ }
+ }
+ // By the time we get here, innerMode is one of "text" or "math".
+ // We switch the mode of the parser, recurse, then restore the old mode.
+ var res = this.parseGroup(optional);
+ this.switchMode(outerMode);
+ return res;
+};
+
+/**
+ * Parses a group, essentially returning the string formed by the
+ * brace-enclosed tokens plus some position information.
+ *
+ * @param {string} modeName Used to describe the mode in error messages
+ * @param {boolean=} optional Whether the group is optional or required
+ */
+Parser.prototype.parseStringGroup = function(modeName, optional) {
+ if (optional && this.nextToken.text !== "[") {
+ return null;
+ }
+ var outerMode = this.mode;
+ this.mode = "text";
+ this.expect(optional ? "[" : "{");
+ var str = "";
+ var firstToken = this.nextToken;
+ var lastToken = firstToken;
+ while (this.nextToken.text !== (optional ? "]" : "}")) {
+ if (this.nextToken.text === "EOF") {
+ throw new ParseError(
+ "Unexpected end of input in " + modeName,
+ firstToken.range(this.nextToken, str));
+ }
+ lastToken = this.nextToken;
+ str += lastToken.text;
+ this.consume();
+ }
+ this.mode = outerMode;
+ this.expect(optional ? "]" : "}");
+ return firstToken.range(lastToken, str);
+};
+
+/**
+ * Parses a color description.
+ */
+Parser.prototype.parseColorGroup = function(optional) {
+ var res = this.parseStringGroup("color", optional);
+ if (!res) {
+ return null;
+ }
+ var match = (/^(#[a-z0-9]+|[a-z]+)$/i).exec(res.text);
+ if (!match) {
+ throw new ParseError("Invalid color: '" + res.text + "'", res);
+ }
+ return new ParseFuncOrArgument(
+ new ParseNode("color", match[0], this.mode),
+ false);
+};
+
+/**
+ * Parses a size specification, consisting of magnitude and unit.
+ */
+Parser.prototype.parseSizeGroup = function(optional) {
+ var res = this.parseStringGroup("size", optional);
+ if (!res) {
+ return null;
+ }
+ var match = (/(-?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/).exec(res.text);
+ if (!match) {
+ throw new ParseError("Invalid size: '" + res.text + "'", res);
+ }
+ var data = {
+ number: +(match[1] + match[2]), // sign + magnitude, cast to number
+ unit: match[3],
+ };
+ if (data.unit !== "em" && data.unit !== "ex") {
+ throw new ParseError("Invalid unit: '" + data.unit + "'", res);
+ }
+ return new ParseFuncOrArgument(
+ new ParseNode("color", data, this.mode),
+ false);
+};
+
+/**
+ * If the argument is false or absent, this parses an ordinary group,
+ * which is either a single nucleus (like "x") or an expression
+ * in braces (like "{x+y}").
+ * If the argument is true, it parses either a bracket-delimited expression
+ * (like "[x+y]") or returns null to indicate the absence of a
+ * bracket-enclosed group.
+ *
+ * @param {boolean=} optional Whether the group is optional or required
+ * @return {?ParseFuncOrArgument}
+ */
+Parser.prototype.parseGroup = function(optional) {
+ var firstToken = this.nextToken;
+ // Try to parse an open brace
+ if (this.nextToken.text === (optional ? "[" : "{")) {
+ // If we get a brace, parse an expression
+ this.consume();
+ var expression = this.parseExpression(false, optional ? "]" : null);
+ var lastToken = this.nextToken;
+ // Make sure we get a close brace
+ this.expect(optional ? "]" : "}");
+ if (this.mode === "text") {
+ this.formLigatures(expression);
+ }
+ return new ParseFuncOrArgument(
+ new ParseNode("ordgroup", expression, this.mode,
+ firstToken, lastToken),
+ false);
+ } else {
+ // Otherwise, just return a nucleus, or nothing for an optional group
+ return optional ? null : this.parseSymbol();
+ }
+};
+
+/**
+ * Form ligature-like combinations of characters for text mode.
+ * This includes inputs like "--", "---", "``" and "''".
+ * The result will simply replace multiple textord nodes with a single
+ * character in each value by a single textord node having multiple
+ * characters in its value. The representation is still ASCII source.
+ *
+ * @param {Array.<ParseNode>} group the nodes of this group,
+ * list will be moified in place
+ */
+Parser.prototype.formLigatures = function(group) {
+ var i;
+ var n = group.length - 1;
+ for (i = 0; i < n; ++i) {
+ var a = group[i];
+ var v = a.value;
+ if (v === "-" && group[i + 1].value === "-") {
+ if (i + 1 < n && group[i + 2].value === "-") {
+ group.splice(i, 3, new ParseNode(
+ "textord", "---", "text", a, group[i + 2]));
+ n -= 2;
+ } else {
+ group.splice(i, 2, new ParseNode(
+ "textord", "--", "text", a, group[i + 1]));
+ n -= 1;
+ }
+ }
+ if ((v === "'" || v === "`") && group[i + 1].value === v) {
+ group.splice(i, 2, new ParseNode(
+ "textord", v + v, "text", a, group[i + 1]));
+ n -= 1;
+ }
+ }
+};
+
+/**
+ * Parse a single symbol out of the string. Here, we handle both the functions
+ * we have defined, as well as the single character symbols
+ *
+ * @return {?ParseFuncOrArgument}
+ */
+Parser.prototype.parseSymbol = function() {
+ var nucleus = this.nextToken;
+
+ if (functions[nucleus.text]) {
+ this.consume();
+ // If there exists a function with this name, we return the function and
+ // say that it is a function.
+ return new ParseFuncOrArgument(
+ nucleus.text,
+ true, nucleus);
+ } else if (symbols[this.mode][nucleus.text]) {
+ this.consume();
+ // Otherwise if this is a no-argument function, find the type it
+ // corresponds to in the symbols map
+ return new ParseFuncOrArgument(
+ new ParseNode(symbols[this.mode][nucleus.text].group,
+ nucleus.text, this.mode, nucleus),
+ false, nucleus);
+ } else if (this.mode === "text" && cjkRegex.test(nucleus.text)) {
+ this.consume();
+ return new ParseFuncOrArgument(
+ new ParseNode("textord", nucleus.text, this.mode, nucleus),
+ false, nucleus);
+ } else {
+ return null;
+ }
+};
+
+Parser.prototype.ParseNode = ParseNode;
+
+module.exports = Parser;
+
+},{"./MacroExpander":4,"./ParseError":6,"./environments":16,"./functions":19,"./parseData":21,"./symbols":23,"./unicodeRegexes":24,"./utils":25}],8:[function(require,module,exports){
+/**
+ * This is a module for storing settings passed into KaTeX. It correctly handles
+ * default settings.
+ */
+
+/**
+ * Helper function for getting a default value if the value is undefined
+ */
+function get(option, defaultValue) {
+ return option === undefined ? defaultValue : option;
+}
+
+/**
+ * The main Settings object
+ *
+ * The current options stored are:
+ * - displayMode: Whether the expression should be typeset by default in
+ * textstyle or displaystyle (default false)
+ */
+function Settings(options) {
+ // allow null options
+ options = options || {};
+ this.displayMode = get(options.displayMode, false);
+ this.throwOnError = get(options.throwOnError, true);
+ this.errorColor = get(options.errorColor, "#cc0000");
+ this.macros = options.macros || {};
+}
+
+module.exports = Settings;
+
+},{}],9:[function(require,module,exports){
+/**
+ * This file contains information and classes for the various kinds of styles
+ * used in TeX. It provides a generic `Style` class, which holds information
+ * about a specific style. It then provides instances of all the different kinds
+ * of styles possible, and provides functions to move between them and get
+ * information about them.
+ */
+
+/**
+ * The main style class. Contains a unique id for the style, a size (which is
+ * the same for cramped and uncramped version of a style), a cramped flag, and a
+ * size multiplier, which gives the size difference between a style and
+ * textstyle.
+ */
+function Style(id, size, multiplier, cramped) {
+ this.id = id;
+ this.size = size;
+ this.cramped = cramped;
+ this.sizeMultiplier = multiplier;
+}
+
+/**
+ * Get the style of a superscript given a base in the current style.
+ */
+Style.prototype.sup = function() {
+ return styles[sup[this.id]];
+};
+
+/**
+ * Get the style of a subscript given a base in the current style.
+ */
+Style.prototype.sub = function() {
+ return styles[sub[this.id]];
+};
+
+/**
+ * Get the style of a fraction numerator given the fraction in the current
+ * style.
+ */
+Style.prototype.fracNum = function() {
+ return styles[fracNum[this.id]];
+};
+
+/**
+ * Get the style of a fraction denominator given the fraction in the current
+ * style.
+ */
+Style.prototype.fracDen = function() {
+ return styles[fracDen[this.id]];
+};
+
+/**
+ * Get the cramped version of a style (in particular, cramping a cramped style
+ * doesn't change the style).
+ */
+Style.prototype.cramp = function() {
+ return styles[cramp[this.id]];
+};
+
+/**
+ * HTML class name, like "displaystyle cramped"
+ */
+Style.prototype.cls = function() {
+ return sizeNames[this.size] + (this.cramped ? " cramped" : " uncramped");
+};
+
+/**
+ * HTML Reset class name, like "reset-textstyle"
+ */
+Style.prototype.reset = function() {
+ return resetNames[this.size];
+};
+
+// IDs of the different styles
+var D = 0;
+var Dc = 1;
+var T = 2;
+var Tc = 3;
+var S = 4;
+var Sc = 5;
+var SS = 6;
+var SSc = 7;
+
+// String names for the different sizes
+var sizeNames = [
+ "displaystyle textstyle",
+ "textstyle",
+ "scriptstyle",
+ "scriptscriptstyle",
+];
+
+// Reset names for the different sizes
+var resetNames = [
+ "reset-textstyle",
+ "reset-textstyle",
+ "reset-scriptstyle",
+ "reset-scriptscriptstyle",
+];
+
+// Instances of the different styles
+var styles = [
+ new Style(D, 0, 1.0, false),
+ new Style(Dc, 0, 1.0, true),
+ new Style(T, 1, 1.0, false),
+ new Style(Tc, 1, 1.0, true),
+ new Style(S, 2, 0.7, false),
+ new Style(Sc, 2, 0.7, true),
+ new Style(SS, 3, 0.5, false),
+ new Style(SSc, 3, 0.5, true),
+];
+
+// Lookup tables for switching from one style to another
+var sup = [S, Sc, S, Sc, SS, SSc, SS, SSc];
+var sub = [Sc, Sc, Sc, Sc, SSc, SSc, SSc, SSc];
+var fracNum = [T, Tc, S, Sc, SS, SSc, SS, SSc];
+var fracDen = [Tc, Tc, Sc, Sc, SSc, SSc, SSc, SSc];
+var cramp = [Dc, Dc, Tc, Tc, Sc, Sc, SSc, SSc];
+
+// We only export some of the styles. Also, we don't export the `Style` class so
+// no more styles can be generated.
+module.exports = {
+ DISPLAY: styles[D],
+ TEXT: styles[T],
+ SCRIPT: styles[S],
+ SCRIPTSCRIPT: styles[SS],
+};
+
+},{}],10:[function(require,module,exports){
+/* eslint no-console:0 */
+/**
+ * This module contains general functions that can be used for building
+ * different kinds of domTree nodes in a consistent manner.
+ */
+
+var domTree = require("./domTree");
+var fontMetrics = require("./fontMetrics");
+var symbols = require("./symbols");
+var utils = require("./utils");
+
+var greekCapitals = [
+ "\\Gamma",
+ "\\Delta",
+ "\\Theta",
+ "\\Lambda",
+ "\\Xi",
+ "\\Pi",
+ "\\Sigma",
+ "\\Upsilon",
+ "\\Phi",
+ "\\Psi",
+ "\\Omega",
+];
+
+// The following have to be loaded from Main-Italic font, using class mainit
+var mainitLetters = [
+ "\u0131", // dotless i, \imath
+ "\u0237", // dotless j, \jmath
+ "\u00a3", // \pounds
+];
+
+/**
+ * Makes a symbolNode after translation via the list of symbols in symbols.js.
+ * Correctly pulls out metrics for the character, and optionally takes a list of
+ * classes to be attached to the node.
+ */
+var makeSymbol = function(value, style, mode, color, classes) {
+ // Replace the value with its replaced value from symbol.js
+ if (symbols[mode][value] && symbols[mode][value].replace) {
+ value = symbols[mode][value].replace;
+ }
+
+ var metrics = fontMetrics.getCharacterMetrics(value, style);
+
+ var symbolNode;
+ if (metrics) {
+ symbolNode = new domTree.symbolNode(
+ value, metrics.height, metrics.depth, metrics.italic, metrics.skew,
+ classes);
+ } else {
+ // TODO(emily): Figure out a good way to only print this in development
+ typeof console !== "undefined" && console.warn(
+ "No character metrics for '" + value + "' in style '" +
+ style + "'");
+ symbolNode = new domTree.symbolNode(value, 0, 0, 0, 0, classes);
+ }
+
+ if (color) {
+ symbolNode.style.color = color;
+ }
+
+ return symbolNode;
+};
+
+/**
+ * Makes a symbol in Main-Regular or AMS-Regular.
+ * Used for rel, bin, open, close, inner, and punct.
+ */
+var mathsym = function(value, mode, color, classes) {
+ // Decide what font to render the symbol in by its entry in the symbols
+ // table.
+ // Have a special case for when the value = \ because the \ is used as a
+ // textord in unsupported command errors but cannot be parsed as a regular
+ // text ordinal and is therefore not present as a symbol in the symbols
+ // table for text
+ if (value === "\\" || symbols[mode][value].font === "main") {
+ return makeSymbol(value, "Main-Regular", mode, color, classes);
+ } else {
+ return makeSymbol(
+ value, "AMS-Regular", mode, color, classes.concat(["amsrm"]));
+ }
+};
+
+/**
+ * Makes a symbol in the default font for mathords and textords.
+ */
+var mathDefault = function(value, mode, color, classes, type) {
+ if (type === "mathord") {
+ return mathit(value, mode, color, classes);
+ } else if (type === "textord") {
+ return makeSymbol(
+ value, "Main-Regular", mode, color, classes.concat(["mathrm"]));
+ } else {
+ throw new Error("unexpected type: " + type + " in mathDefault");
+ }
+};
+
+/**
+ * Makes a symbol in the italic math font.
+ */
+var mathit = function(value, mode, color, classes) {
+ if (/[0-9]/.test(value.charAt(0)) ||
+ // glyphs for \imath and \jmath do not exist in Math-Italic so we
+ // need to use Main-Italic instead
+ utils.contains(mainitLetters, value) ||
+ utils.contains(greekCapitals, value)) {
+ return makeSymbol(
+ value, "Main-Italic", mode, color, classes.concat(["mainit"]));
+ } else {
+ return makeSymbol(
+ value, "Math-Italic", mode, color, classes.concat(["mathit"]));
+ }
+};
+
+/**
+ * Makes either a mathord or textord in the correct font and color.
+ */
+var makeOrd = function(group, options, type) {
+ var mode = group.mode;
+ var value = group.value;
+ if (symbols[mode][value] && symbols[mode][value].replace) {
+ value = symbols[mode][value].replace;
+ }
+
+ var classes = ["mord"];
+ var color = options.getColor();
+
+ var font = options.font;
+ if (font) {
+ if (font === "mathit" || utils.contains(mainitLetters, value)) {
+ return mathit(value, mode, color, classes);
+ } else {
+ var fontName = fontMap[font].fontName;
+ if (fontMetrics.getCharacterMetrics(value, fontName)) {
+ return makeSymbol(
+ value, fontName, mode, color, classes.concat([font]));
+ } else {
+ return mathDefault(value, mode, color, classes, type);
+ }
+ }
+ } else {
+ return mathDefault(value, mode, color, classes, type);
+ }
+};
+
+/**
+ * Calculate the height, depth, and maxFontSize of an element based on its
+ * children.
+ */
+var sizeElementFromChildren = function(elem) {
+ var height = 0;
+ var depth = 0;
+ var maxFontSize = 0;
+
+ if (elem.children) {
+ for (var i = 0; i < elem.children.length; i++) {
+ if (elem.children[i].height > height) {
+ height = elem.children[i].height;
+ }
+ if (elem.children[i].depth > depth) {
+ depth = elem.children[i].depth;
+ }
+ if (elem.children[i].maxFontSize > maxFontSize) {
+ maxFontSize = elem.children[i].maxFontSize;
+ }
+ }
+ }
+
+ elem.height = height;
+ elem.depth = depth;
+ elem.maxFontSize = maxFontSize;
+};
+
+/**
+ * Makes a span with the given list of classes, list of children, and color.
+ */
+var makeSpan = function(classes, children, color) {
+ var span = new domTree.span(classes, children);
+
+ sizeElementFromChildren(span);
+
+ if (color) {
+ span.style.color = color;
+ }
+
+ return span;
+};
+
+/**
+ * Makes a document fragment with the given list of children.
+ */
+var makeFragment = function(children) {
+ var fragment = new domTree.documentFragment(children);
+
+ sizeElementFromChildren(fragment);
+
+ return fragment;
+};
+
+/**
+ * Makes an element placed in each of the vlist elements to ensure that each
+ * element has the same max font size. To do this, we create a zero-width space
+ * with the correct font size.
+ */
+var makeFontSizer = function(options, fontSize) {
+ var fontSizeInner = makeSpan([], [new domTree.symbolNode("\u200b")]);
+ fontSizeInner.style.fontSize =
+ (fontSize / options.style.sizeMultiplier) + "em";
+
+ var fontSizer = makeSpan(
+ ["fontsize-ensurer", "reset-" + options.size, "size5"],
+ [fontSizeInner]);
+
+ return fontSizer;
+};
+
+/**
+ * Makes a vertical list by stacking elements and kerns on top of each other.
+ * Allows for many different ways of specifying the positioning method.
+ *
+ * Arguments:
+ * - children: A list of child or kern nodes to be stacked on top of each other
+ * (i.e. the first element will be at the bottom, and the last at
+ * the top). Element nodes are specified as
+ * {type: "elem", elem: node}
+ * while kern nodes are specified as
+ * {type: "kern", size: size}
+ * - positionType: The method by which the vlist should be positioned. Valid
+ * values are:
+ * - "individualShift": The children list only contains elem
+ * nodes, and each node contains an extra
+ * "shift" value of how much it should be
+ * shifted (note that shifting is always
+ * moving downwards). positionData is
+ * ignored.
+ * - "top": The positionData specifies the topmost point of
+ * the vlist (note this is expected to be a height,
+ * so positive values move up)
+ * - "bottom": The positionData specifies the bottommost point
+ * of the vlist (note this is expected to be a
+ * depth, so positive values move down
+ * - "shift": The vlist will be positioned such that its
+ * baseline is positionData away from the baseline
+ * of the first child. Positive values move
+ * downwards.
+ * - "firstBaseline": The vlist will be positioned such that
+ * its baseline is aligned with the
+ * baseline of the first child.
+ * positionData is ignored. (this is
+ * equivalent to "shift" with
+ * positionData=0)
+ * - positionData: Data used in different ways depending on positionType
+ * - options: An Options object
+ *
+ */
+var makeVList = function(children, positionType, positionData, options) {
+ var depth;
+ var currPos;
+ var i;
+ if (positionType === "individualShift") {
+ var oldChildren = children;
+ children = [oldChildren[0]];
+
+ // Add in kerns to the list of children to get each element to be
+ // shifted to the correct specified shift
+ depth = -oldChildren[0].shift - oldChildren[0].elem.depth;
+ currPos = depth;
+ for (i = 1; i < oldChildren.length; i++) {
+ var diff = -oldChildren[i].shift - currPos -
+ oldChildren[i].elem.depth;
+ var size = diff -
+ (oldChildren[i - 1].elem.height +
+ oldChildren[i - 1].elem.depth);
+
+ currPos = currPos + diff;
+
+ children.push({type: "kern", size: size});
+ children.push(oldChildren[i]);
+ }
+ } else if (positionType === "top") {
+ // We always start at the bottom, so calculate the bottom by adding up
+ // all the sizes
+ var bottom = positionData;
+ for (i = 0; i < children.length; i++) {
+ if (children[i].type === "kern") {
+ bottom -= children[i].size;
+ } else {
+ bottom -= children[i].elem.height + children[i].elem.depth;
+ }
+ }
+ depth = bottom;
+ } else if (positionType === "bottom") {
+ depth = -positionData;
+ } else if (positionType === "shift") {
+ depth = -children[0].elem.depth - positionData;
+ } else if (positionType === "firstBaseline") {
+ depth = -children[0].elem.depth;
+ } else {
+ depth = 0;
+ }
+
+ // Make the fontSizer
+ var maxFontSize = 0;
+ for (i = 0; i < children.length; i++) {
+ if (children[i].type === "elem") {
+ maxFontSize = Math.max(maxFontSize, children[i].elem.maxFontSize);
+ }
+ }
+ var fontSizer = makeFontSizer(options, maxFontSize);
+
+ // Create a new list of actual children at the correct offsets
+ var realChildren = [];
+ currPos = depth;
+ for (i = 0; i < children.length; i++) {
+ if (children[i].type === "kern") {
+ currPos += children[i].size;
+ } else {
+ var child = children[i].elem;
+
+ var shift = -child.depth - currPos;
+ currPos += child.height + child.depth;
+
+ var childWrap = makeSpan([], [fontSizer, child]);
+ childWrap.height -= shift;
+ childWrap.depth += shift;
+ childWrap.style.top = shift + "em";
+
+ realChildren.push(childWrap);
+ }
+ }
+
+ // Add in an element at the end with no offset to fix the calculation of
+ // baselines in some browsers (namely IE, sometimes safari)
+ var baselineFix = makeSpan(
+ ["baseline-fix"], [fontSizer, new domTree.symbolNode("\u200b")]);
+ realChildren.push(baselineFix);
+
+ var vlist = makeSpan(["vlist"], realChildren);
+ // Fix the final height and depth, in case there were kerns at the ends
+ // since the makeSpan calculation won't take that in to account.
+ vlist.height = Math.max(currPos, vlist.height);
+ vlist.depth = Math.max(-depth, vlist.depth);
+ return vlist;
+};
+
+// A table of size -> font size for the different sizing functions
+var sizingMultiplier = {
+ size1: 0.5,
+ size2: 0.7,
+ size3: 0.8,
+ size4: 0.9,
+ size5: 1.0,
+ size6: 1.2,
+ size7: 1.44,
+ size8: 1.73,
+ size9: 2.07,
+ size10: 2.49,
+};
+
+// A map of spacing functions to their attributes, like size and corresponding
+// CSS class
+var spacingFunctions = {
+ "\\qquad": {
+ size: "2em",
+ className: "qquad",
+ },
+ "\\quad": {
+ size: "1em",
+ className: "quad",
+ },
+ "\\enspace": {
+ size: "0.5em",
+ className: "enspace",
+ },
+ "\\;": {
+ size: "0.277778em",
+ className: "thickspace",
+ },
+ "\\:": {
+ size: "0.22222em",
+ className: "mediumspace",
+ },
+ "\\,": {
+ size: "0.16667em",
+ className: "thinspace",
+ },
+ "\\!": {
+ size: "-0.16667em",
+ className: "negativethinspace",
+ },
+};
+
+/**
+ * Maps TeX font commands to objects containing:
+ * - variant: string used for "mathvariant" attribute in buildMathML.js
+ * - fontName: the "style" parameter to fontMetrics.getCharacterMetrics
+ */
+// A map between tex font commands an MathML mathvariant attribute values
+var fontMap = {
+ // styles
+ "mathbf": {
+ variant: "bold",
+ fontName: "Main-Bold",
+ },
+ "mathrm": {
+ variant: "normal",
+ fontName: "Main-Regular",
+ },
+
+ // "mathit" is missing because it requires the use of two fonts: Main-Italic
+ // and Math-Italic. This is handled by a special case in makeOrd which ends
+ // up calling mathit.
+
+ // families
+ "mathbb": {
+ variant: "double-struck",
+ fontName: "AMS-Regular",
+ },
+ "mathcal": {
+ variant: "script",
+ fontName: "Caligraphic-Regular",
+ },
+ "mathfrak": {
+ variant: "fraktur",
+ fontName: "Fraktur-Regular",
+ },
+ "mathscr": {
+ variant: "script",
+ fontName: "Script-Regular",
+ },
+ "mathsf": {
+ variant: "sans-serif",
+ fontName: "SansSerif-Regular",
+ },
+ "mathtt": {
+ variant: "monospace",
+ fontName: "Typewriter-Regular",
+ },
+};
+
+module.exports = {
+ fontMap: fontMap,
+ makeSymbol: makeSymbol,
+ mathsym: mathsym,
+ makeSpan: makeSpan,
+ makeFragment: makeFragment,
+ makeVList: makeVList,
+ makeOrd: makeOrd,
+ sizingMultiplier: sizingMultiplier,
+ spacingFunctions: spacingFunctions,
+};
+
+},{"./domTree":15,"./fontMetrics":17,"./symbols":23,"./utils":25}],11:[function(require,module,exports){
+/* eslint no-console:0 */
+/**
+ * This file does the main work of building a domTree structure from a parse
+ * tree. The entry point is the `buildHTML` function, which takes a parse tree.
+ * Then, the buildExpression, buildGroup, and various groupTypes functions are
+ * called, to produce a final HTML tree.
+ */
+
+var ParseError = require("./ParseError");
+var Style = require("./Style");
+
+var buildCommon = require("./buildCommon");
+var delimiter = require("./delimiter");
+var domTree = require("./domTree");
+var fontMetrics = require("./fontMetrics");
+var utils = require("./utils");
+
+var makeSpan = buildCommon.makeSpan;
+
+/**
+ * Take a list of nodes, build them in order, and return a list of the built
+ * nodes. This function handles the `prev` node correctly, and passes the
+ * previous element from the list as the prev of the next element.
+ */
+var buildExpression = function(expression, options, prev) {
+ var groups = [];
+ for (var i = 0; i < expression.length; i++) {
+ var group = expression[i];
+ groups.push(buildGroup(group, options, prev));
+ prev = group;
+ }
+ return groups;
+};
+
+// List of types used by getTypeOfGroup,
+// see https://github.com/Khan/KaTeX/wiki/Examining-TeX#group-types
+var groupToType = {
+ mathord: "mord",
+ textord: "mord",
+ bin: "mbin",
+ rel: "mrel",
+ text: "mord",
+ open: "mopen",
+ close: "mclose",
+ inner: "minner",
+ genfrac: "mord",
+ array: "mord",
+ spacing: "mord",
+ punct: "mpunct",
+ ordgroup: "mord",
+ op: "mop",
+ katex: "mord",
+ overline: "mord",
+ underline: "mord",
+ rule: "mord",
+ leftright: "minner",
+ sqrt: "mord",
+ accent: "mord",
+};
+
+/**
+ * Gets the final math type of an expression, given its group type. This type is
+ * used to determine spacing between elements, and affects bin elements by
+ * causing them to change depending on what types are around them. This type
+ * must be attached to the outermost node of an element as a CSS class so that
+ * spacing with its surrounding elements works correctly.
+ *
+ * Some elements can be mapped one-to-one from group type to math type, and
+ * those are listed in the `groupToType` table.
+ *
+ * Others (usually elements that wrap around other elements) often have
+ * recursive definitions, and thus call `getTypeOfGroup` on their inner
+ * elements.
+ */
+var getTypeOfGroup = function(group) {
+ if (group == null) {
+ // Like when typesetting $^3$
+ return groupToType.mathord;
+ } else if (group.type === "supsub") {
+ return getTypeOfGroup(group.value.base);
+ } else if (group.type === "llap" || group.type === "rlap") {
+ return getTypeOfGroup(group.value);
+ } else if (group.type === "color") {
+ return getTypeOfGroup(group.value.value);
+ } else if (group.type === "sizing") {
+ return getTypeOfGroup(group.value.value);
+ } else if (group.type === "styling") {
+ return getTypeOfGroup(group.value.value);
+ } else if (group.type === "delimsizing") {
+ return groupToType[group.value.delimType];
+ } else {
+ return groupToType[group.type];
+ }
+};
+
+/**
+ * Sometimes, groups perform special rules when they have superscripts or
+ * subscripts attached to them. This function lets the `supsub` group know that
+ * its inner element should handle the superscripts and subscripts instead of
+ * handling them itself.
+ */
+var shouldHandleSupSub = function(group, options) {
+ if (!group) {
+ return false;
+ } else if (group.type === "op") {
+ // Operators handle supsubs differently when they have limits
+ // (e.g. `\displaystyle\sum_2^3`)
+ return group.value.limits &&
+ (options.style.size === Style.DISPLAY.size ||
+ group.value.alwaysHandleSupSub);
+ } else if (group.type === "accent") {
+ return isCharacterBox(group.value.base);
+ } else {
+ return null;
+ }
+};
+
+/**
+ * Sometimes we want to pull out the innermost element of a group. In most
+ * cases, this will just be the group itself, but when ordgroups and colors have
+ * a single element, we want to pull that out.
+ */
+var getBaseElem = function(group) {
+ if (!group) {
+ return false;
+ } else if (group.type === "ordgroup") {
+ if (group.value.length === 1) {
+ return getBaseElem(group.value[0]);
+ } else {
+ return group;
+ }
+ } else if (group.type === "color") {
+ if (group.value.value.length === 1) {
+ return getBaseElem(group.value.value[0]);
+ } else {
+ return group;
+ }
+ } else if (group.type === "font") {
+ return getBaseElem(group.value.body);
+ } else {
+ return group;
+ }
+};
+
+/**
+ * TeXbook algorithms often reference "character boxes", which are simply groups
+ * with a single character in them. To decide if something is a character box,
+ * we find its innermost group, and see if it is a single character.
+ */
+var isCharacterBox = function(group) {
+ var baseElem = getBaseElem(group);
+
+ // These are all they types of groups which hold single characters
+ return baseElem.type === "mathord" ||
+ baseElem.type === "textord" ||
+ baseElem.type === "bin" ||
+ baseElem.type === "rel" ||
+ baseElem.type === "inner" ||
+ baseElem.type === "open" ||
+ baseElem.type === "close" ||
+ baseElem.type === "punct";
+};
+
+var makeNullDelimiter = function(options) {
+ return makeSpan([
+ "sizing", "reset-" + options.size, "size5",
+ options.style.reset(), Style.TEXT.cls(),
+ "nulldelimiter",
+ ]);
+};
+
+/**
+ * This is a map of group types to the function used to handle that type.
+ * Simpler types come at the beginning, while complicated types come afterwards.
+ */
+var groupTypes = {};
+
+groupTypes.mathord = function(group, options, prev) {
+ return buildCommon.makeOrd(group, options, "mathord");
+};
+
+groupTypes.textord = function(group, options, prev) {
+ return buildCommon.makeOrd(group, options, "textord");
+};
+
+groupTypes.bin = function(group, options, prev) {
+ var className = "mbin";
+ // Pull out the most recent element. Do some special handling to find
+ // things at the end of a \color group. Note that we don't use the same
+ // logic for ordgroups (which count as ords).
+ var prevAtom = prev;
+ while (prevAtom && prevAtom.type === "color") {
+ var atoms = prevAtom.value.value;
+ prevAtom = atoms[atoms.length - 1];
+ }
+ // See TeXbook pg. 442-446, Rules 5 and 6, and the text before Rule 19.
+ // Here, we determine whether the bin should turn into an ord. We
+ // currently only apply Rule 5.
+ if (!prev || utils.contains(["mbin", "mopen", "mrel", "mop", "mpunct"],
+ getTypeOfGroup(prevAtom))) {
+ group.type = "textord";
+ className = "mord";
+ }
+
+ return buildCommon.mathsym(
+ group.value, group.mode, options.getColor(), [className]);
+};
+
+groupTypes.rel = function(group, options, prev) {
+ return buildCommon.mathsym(
+ group.value, group.mode, options.getColor(), ["mrel"]);
+};
+
+groupTypes.open = function(group, options, prev) {
+ return buildCommon.mathsym(
+ group.value, group.mode, options.getColor(), ["mopen"]);
+};
+
+groupTypes.close = function(group, options, prev) {
+ return buildCommon.mathsym(
+ group.value, group.mode, options.getColor(), ["mclose"]);
+};
+
+groupTypes.inner = function(group, options, prev) {
+ return buildCommon.mathsym(
+ group.value, group.mode, options.getColor(), ["minner"]);
+};
+
+groupTypes.punct = function(group, options, prev) {
+ return buildCommon.mathsym(
+ group.value, group.mode, options.getColor(), ["mpunct"]);
+};
+
+groupTypes.ordgroup = function(group, options, prev) {
+ return makeSpan(
+ ["mord", options.style.cls()],
+ buildExpression(group.value, options.reset())
+ );
+};
+
+groupTypes.text = function(group, options, prev) {
+ return makeSpan(["text", "mord", options.style.cls()],
+ buildExpression(group.value.body, options.reset()));
+};
+
+groupTypes.color = function(group, options, prev) {
+ var elements = buildExpression(
+ group.value.value,
+ options.withColor(group.value.color),
+ prev
+ );
+
+ // \color isn't supposed to affect the type of the elements it contains.
+ // To accomplish this, we wrap the results in a fragment, so the inner
+ // elements will be able to directly interact with their neighbors. For
+ // example, `\color{red}{2 +} 3` has the same spacing as `2 + 3`
+ return new buildCommon.makeFragment(elements);
+};
+
+groupTypes.supsub = function(group, options, prev) {
+ // Superscript and subscripts are handled in the TeXbook on page
+ // 445-446, rules 18(a-f).
+
+ // Here is where we defer to the inner group if it should handle
+ // superscripts and subscripts itself.
+ if (shouldHandleSupSub(group.value.base, options)) {
+ return groupTypes[group.value.base.type](group, options, prev);
+ }
+
+ var base = buildGroup(group.value.base, options.reset());
+ var supmid;
+ var submid;
+ var sup;
+ var sub;
+
+ if (group.value.sup) {
+ sup = buildGroup(group.value.sup,
+ options.withStyle(options.style.sup()));
+ supmid = makeSpan(
+ [options.style.reset(), options.style.sup().cls()], [sup]);
+ }
+
+ if (group.value.sub) {
+ sub = buildGroup(group.value.sub,
+ options.withStyle(options.style.sub()));
+ submid = makeSpan(
+ [options.style.reset(), options.style.sub().cls()], [sub]);
+ }
+
+ // Rule 18a
+ var supShift;
+ var subShift;
+ if (isCharacterBox(group.value.base)) {
+ supShift = 0;
+ subShift = 0;
+ } else {
+ supShift = base.height - fontMetrics.metrics.supDrop;
+ subShift = base.depth + fontMetrics.metrics.subDrop;
+ }
+
+ // Rule 18c
+ var minSupShift;
+ if (options.style === Style.DISPLAY) {
+ minSupShift = fontMetrics.metrics.sup1;
+ } else if (options.style.cramped) {
+ minSupShift = fontMetrics.metrics.sup3;
+ } else {
+ minSupShift = fontMetrics.metrics.sup2;
+ }
+
+ // scriptspace is a font-size-independent size, so scale it
+ // appropriately
+ var multiplier = Style.TEXT.sizeMultiplier *
+ options.style.sizeMultiplier;
+ var scriptspace =
+ (0.5 / fontMetrics.metrics.ptPerEm) / multiplier + "em";
+
+ var supsub;
+ if (!group.value.sup) {
+ // Rule 18b
+ subShift = Math.max(
+ subShift, fontMetrics.metrics.sub1,
+ sub.height - 0.8 * fontMetrics.metrics.xHeight);
+
+ supsub = buildCommon.makeVList([
+ {type: "elem", elem: submid},
+ ], "shift", subShift, options);
+
+ supsub.children[0].style.marginRight = scriptspace;
+
+ // Subscripts shouldn't be shifted by the base's italic correction.
+ // Account for that by shifting the subscript back the appropriate
+ // amount. Note we only do this when the base is a single symbol.
+ if (base instanceof domTree.symbolNode) {
+ supsub.children[0].style.marginLeft = -base.italic + "em";
+ }
+ } else if (!group.value.sub) {
+ // Rule 18c, d
+ supShift = Math.max(supShift, minSupShift,
+ sup.depth + 0.25 * fontMetrics.metrics.xHeight);
+
+ supsub = buildCommon.makeVList([
+ {type: "elem", elem: supmid},
+ ], "shift", -supShift, options);
+
+ supsub.children[0].style.marginRight = scriptspace;
+ } else {
+ supShift = Math.max(
+ supShift, minSupShift,
+ sup.depth + 0.25 * fontMetrics.metrics.xHeight);
+ subShift = Math.max(subShift, fontMetrics.metrics.sub2);
+
+ var ruleWidth = fontMetrics.metrics.defaultRuleThickness;
+
+ // Rule 18e
+ if ((supShift - sup.depth) - (sub.height - subShift) <
+ 4 * ruleWidth) {
+ subShift = 4 * ruleWidth - (supShift - sup.depth) + sub.height;
+ var psi = 0.8 * fontMetrics.metrics.xHeight -
+ (supShift - sup.depth);
+ if (psi > 0) {
+ supShift += psi;
+ subShift -= psi;
+ }
+ }
+
+ supsub = buildCommon.makeVList([
+ {type: "elem", elem: submid, shift: subShift},
+ {type: "elem", elem: supmid, shift: -supShift},
+ ], "individualShift", null, options);
+
+ // See comment above about subscripts not being shifted
+ if (base instanceof domTree.symbolNode) {
+ supsub.children[0].style.marginLeft = -base.italic + "em";
+ }
+
+ supsub.children[0].style.marginRight = scriptspace;
+ supsub.children[1].style.marginRight = scriptspace;
+ }
+
+ // We ensure to wrap the supsub vlist in a span.msupsub to reset text-align
+ return makeSpan([getTypeOfGroup(group.value.base)],
+ [base, makeSpan(["msupsub"], [supsub])]);
+};
+
+groupTypes.genfrac = function(group, options, prev) {
+ // Fractions are handled in the TeXbook on pages 444-445, rules 15(a-e).
+ // Figure out what style this fraction should be in based on the
+ // function used
+ var fstyle = options.style;
+ if (group.value.size === "display") {
+ fstyle = Style.DISPLAY;
+ } else if (group.value.size === "text") {
+ fstyle = Style.TEXT;
+ }
+
+ var nstyle = fstyle.fracNum();
+ var dstyle = fstyle.fracDen();
+
+ var numer = buildGroup(group.value.numer, options.withStyle(nstyle));
+ var numerreset = makeSpan([fstyle.reset(), nstyle.cls()], [numer]);
+
+ var denom = buildGroup(group.value.denom, options.withStyle(dstyle));
+ var denomreset = makeSpan([fstyle.reset(), dstyle.cls()], [denom]);
+
+ var ruleWidth;
+ if (group.value.hasBarLine) {
+ ruleWidth = fontMetrics.metrics.defaultRuleThickness /
+ options.style.sizeMultiplier;
+ } else {
+ ruleWidth = 0;
+ }
+
+ // Rule 15b
+ var numShift;
+ var clearance;
+ var denomShift;
+ if (fstyle.size === Style.DISPLAY.size) {
+ numShift = fontMetrics.metrics.num1;
+ if (ruleWidth > 0) {
+ clearance = 3 * ruleWidth;
+ } else {
+ clearance = 7 * fontMetrics.metrics.defaultRuleThickness;
+ }
+ denomShift = fontMetrics.metrics.denom1;
+ } else {
+ if (ruleWidth > 0) {
+ numShift = fontMetrics.metrics.num2;
+ clearance = ruleWidth;
+ } else {
+ numShift = fontMetrics.metrics.num3;
+ clearance = 3 * fontMetrics.metrics.defaultRuleThickness;
+ }
+ denomShift = fontMetrics.metrics.denom2;
+ }
+
+ var frac;
+ if (ruleWidth === 0) {
+ // Rule 15c
+ var candiateClearance =
+ (numShift - numer.depth) - (denom.height - denomShift);
+ if (candiateClearance < clearance) {
+ numShift += 0.5 * (clearance - candiateClearance);
+ denomShift += 0.5 * (clearance - candiateClearance);
+ }
+
+ frac = buildCommon.makeVList([
+ {type: "elem", elem: denomreset, shift: denomShift},
+ {type: "elem", elem: numerreset, shift: -numShift},
+ ], "individualShift", null, options);
+ } else {
+ // Rule 15d
+ var axisHeight = fontMetrics.metrics.axisHeight;
+
+ if ((numShift - numer.depth) - (axisHeight + 0.5 * ruleWidth) <
+ clearance) {
+ numShift +=
+ clearance - ((numShift - numer.depth) -
+ (axisHeight + 0.5 * ruleWidth));
+ }
+
+ if ((axisHeight - 0.5 * ruleWidth) - (denom.height - denomShift) <
+ clearance) {
+ denomShift +=
+ clearance - ((axisHeight - 0.5 * ruleWidth) -
+ (denom.height - denomShift));
+ }
+
+ var mid = makeSpan(
+ [options.style.reset(), Style.TEXT.cls(), "frac-line"]);
+ // Manually set the height of the line because its height is
+ // created in CSS
+ mid.height = ruleWidth;
+
+ var midShift = -(axisHeight - 0.5 * ruleWidth);
+
+ frac = buildCommon.makeVList([
+ {type: "elem", elem: denomreset, shift: denomShift},
+ {type: "elem", elem: mid, shift: midShift},
+ {type: "elem", elem: numerreset, shift: -numShift},
+ ], "individualShift", null, options);
+ }
+
+ // Since we manually change the style sometimes (with \dfrac or \tfrac),
+ // account for the possible size change here.
+ frac.height *= fstyle.sizeMultiplier / options.style.sizeMultiplier;
+ frac.depth *= fstyle.sizeMultiplier / options.style.sizeMultiplier;
+
+ // Rule 15e
+ var delimSize;
+ if (fstyle.size === Style.DISPLAY.size) {
+ delimSize = fontMetrics.metrics.delim1;
+ } else {
+ delimSize = fontMetrics.metrics.getDelim2(fstyle);
+ }
+
+ var leftDelim;
+ var rightDelim;
+ if (group.value.leftDelim == null) {
+ leftDelim = makeNullDelimiter(options);
+ } else {
+ leftDelim = delimiter.customSizedDelim(
+ group.value.leftDelim, delimSize, true,
+ options.withStyle(fstyle), group.mode);
+ }
+ if (group.value.rightDelim == null) {
+ rightDelim = makeNullDelimiter(options);
+ } else {
+ rightDelim = delimiter.customSizedDelim(
+ group.value.rightDelim, delimSize, true,
+ options.withStyle(fstyle), group.mode);
+ }
+
+ return makeSpan(
+ ["mord", options.style.reset(), fstyle.cls()],
+ [leftDelim, makeSpan(["mfrac"], [frac]), rightDelim],
+ options.getColor());
+};
+
+groupTypes.array = function(group, options, prev) {
+ var r;
+ var c;
+ var nr = group.value.body.length;
+ var nc = 0;
+ var body = new Array(nr);
+
+ // Horizontal spacing
+ var pt = 1 / fontMetrics.metrics.ptPerEm;
+ var arraycolsep = 5 * pt; // \arraycolsep in article.cls
+
+ // Vertical spacing
+ var baselineskip = 12 * pt; // see size10.clo
+ // Default \arraystretch from lttab.dtx
+ // TODO(gagern): may get redefined once we have user-defined macros
+ var arraystretch = utils.deflt(group.value.arraystretch, 1);
+ var arrayskip = arraystretch * baselineskip;
+ var arstrutHeight = 0.7 * arrayskip; // \strutbox in ltfsstrc.dtx and
+ var arstrutDepth = 0.3 * arrayskip; // \@arstrutbox in lttab.dtx
+
+ var totalHeight = 0;
+ for (r = 0; r < group.value.body.length; ++r) {
+ var inrow = group.value.body[r];
+ var height = arstrutHeight; // \@array adds an \@arstrut
+ var depth = arstrutDepth; // to each tow (via the template)
+
+ if (nc < inrow.length) {
+ nc = inrow.length;
+ }
+
+ var outrow = new Array(inrow.length);
+ for (c = 0; c < inrow.length; ++c) {
+ var elt = buildGroup(inrow[c], options);
+ if (depth < elt.depth) {
+ depth = elt.depth;
+ }
+ if (height < elt.height) {
+ height = elt.height;
+ }
+ outrow[c] = elt;
+ }
+
+ var gap = 0;
+ if (group.value.rowGaps[r]) {
+ gap = group.value.rowGaps[r].value;
+ switch (gap.unit) {
+ case "em":
+ gap = gap.number;
+ break;
+ case "ex":
+ gap = gap.number * fontMetrics.metrics.emPerEx;
+ break;
+ default:
+ console.error("Can't handle unit " + gap.unit);
+ gap = 0;
+ }
+ if (gap > 0) { // \@argarraycr
+ gap += arstrutDepth;
+ if (depth < gap) {
+ depth = gap; // \@xargarraycr
+ }
+ gap = 0;
+ }
+ }
+
+ outrow.height = height;
+ outrow.depth = depth;
+ totalHeight += height;
+ outrow.pos = totalHeight;
+ totalHeight += depth + gap; // \@yargarraycr
+ body[r] = outrow;
+ }
+
+ var offset = totalHeight / 2 + fontMetrics.metrics.axisHeight;
+ var colDescriptions = group.value.cols || [];
+ var cols = [];
+ var colSep;
+ var colDescrNum;
+ for (c = 0, colDescrNum = 0;
+ // Continue while either there are more columns or more column
+ // descriptions, so trailing separators don't get lost.
+ c < nc || colDescrNum < colDescriptions.length;
+ ++c, ++colDescrNum) {
+
+ var colDescr = colDescriptions[colDescrNum] || {};
+
+ var firstSeparator = true;
+ while (colDescr.type === "separator") {
+ // If there is more than one separator in a row, add a space
+ // between them.
+ if (!firstSeparator) {
+ colSep = makeSpan(["arraycolsep"], []);
+ colSep.style.width =
+ fontMetrics.metrics.doubleRuleSep + "em";
+ cols.push(colSep);
+ }
+
+ if (colDescr.separator === "|") {
+ var separator = makeSpan(
+ ["vertical-separator"],
+ []);
+ separator.style.height = totalHeight + "em";
+ separator.style.verticalAlign =
+ -(totalHeight - offset) + "em";
+
+ cols.push(separator);
+ } else {
+ throw new ParseError(
+ "Invalid separator type: " + colDescr.separator);
+ }
+
+ colDescrNum++;
+ colDescr = colDescriptions[colDescrNum] || {};
+ firstSeparator = false;
+ }
+
+ if (c >= nc) {
+ continue;
+ }
+
+ var sepwidth;
+ if (c > 0 || group.value.hskipBeforeAndAfter) {
+ sepwidth = utils.deflt(colDescr.pregap, arraycolsep);
+ if (sepwidth !== 0) {
+ colSep = makeSpan(["arraycolsep"], []);
+ colSep.style.width = sepwidth + "em";
+ cols.push(colSep);
+ }
+ }
+
+ var col = [];
+ for (r = 0; r < nr; ++r) {
+ var row = body[r];
+ var elem = row[c];
+ if (!elem) {
+ continue;
+ }
+ var shift = row.pos - offset;
+ elem.depth = row.depth;
+ elem.height = row.height;
+ col.push({type: "elem", elem: elem, shift: shift});
+ }
+
+ col = buildCommon.makeVList(col, "individualShift", null, options);
+ col = makeSpan(
+ ["col-align-" + (colDescr.align || "c")],
+ [col]);
+ cols.push(col);
+
+ if (c < nc - 1 || group.value.hskipBeforeAndAfter) {
+ sepwidth = utils.deflt(colDescr.postgap, arraycolsep);
+ if (sepwidth !== 0) {
+ colSep = makeSpan(["arraycolsep"], []);
+ colSep.style.width = sepwidth + "em";
+ cols.push(colSep);
+ }
+ }
+ }
+ body = makeSpan(["mtable"], cols);
+ return makeSpan(["mord"], [body], options.getColor());
+};
+
+groupTypes.spacing = function(group, options, prev) {
+ if (group.value === "\\ " || group.value === "\\space" ||
+ group.value === " " || group.value === "~") {
+ // Spaces are generated by adding an actual space. Each of these
+ // things has an entry in the symbols table, so these will be turned
+ // into appropriate outputs.
+ return makeSpan(
+ ["mord", "mspace"],
+ [buildCommon.mathsym(group.value, group.mode)]
+ );
+ } else {
+ // Other kinds of spaces are of arbitrary width. We use CSS to
+ // generate these.
+ return makeSpan(
+ ["mord", "mspace",
+ buildCommon.spacingFunctions[group.value].className]);
+ }
+};
+
+groupTypes.llap = function(group, options, prev) {
+ var inner = makeSpan(
+ ["inner"], [buildGroup(group.value.body, options.reset())]);
+ var fix = makeSpan(["fix"], []);
+ return makeSpan(
+ ["llap", options.style.cls()], [inner, fix]);
+};
+
+groupTypes.rlap = function(group, options, prev) {
+ var inner = makeSpan(
+ ["inner"], [buildGroup(group.value.body, options.reset())]);
+ var fix = makeSpan(["fix"], []);
+ return makeSpan(
+ ["rlap", options.style.cls()], [inner, fix]);
+};
+
+groupTypes.op = function(group, options, prev) {
+ // Operators are handled in the TeXbook pg. 443-444, rule 13(a).
+ var supGroup;
+ var subGroup;
+ var hasLimits = false;
+ if (group.type === "supsub" ) {
+ // If we have limits, supsub will pass us its group to handle. Pull
+ // out the superscript and subscript and set the group to the op in
+ // its base.
+ supGroup = group.value.sup;
+ subGroup = group.value.sub;
+ group = group.value.base;
+ hasLimits = true;
+ }
+
+ // Most operators have a large successor symbol, but these don't.
+ var noSuccessor = [
+ "\\smallint",
+ ];
+
+ var large = false;
+ if (options.style.size === Style.DISPLAY.size &&
+ group.value.symbol &&
+ !utils.contains(noSuccessor, group.value.body)) {
+
+ // Most symbol operators get larger in displaystyle (rule 13)
+ large = true;
+ }
+
+ var base;
+ var baseShift = 0;
+ var slant = 0;
+ if (group.value.symbol) {
+ // If this is a symbol, create the symbol.
+ var style = large ? "Size2-Regular" : "Size1-Regular";
+ base = buildCommon.makeSymbol(
+ group.value.body, style, "math", options.getColor(),
+ ["op-symbol", large ? "large-op" : "small-op", "mop"]);
+
+ // Shift the symbol so its center lies on the axis (rule 13). It
+ // appears that our fonts have the centers of the symbols already
+ // almost on the axis, so these numbers are very small. Note we
+ // don't actually apply this here, but instead it is used either in
+ // the vlist creation or separately when there are no limits.
+ baseShift = (base.height - base.depth) / 2 -
+ fontMetrics.metrics.axisHeight *
+ options.style.sizeMultiplier;
+
+ // The slant of the symbol is just its italic correction.
+ slant = base.italic;
+ } else {
+ // Otherwise, this is a text operator. Build the text from the
+ // operator's name.
+ // TODO(emily): Add a space in the middle of some of these
+ // operators, like \limsup
+ var output = [];
+ for (var i = 1; i < group.value.body.length; i++) {
+ output.push(buildCommon.mathsym(group.value.body[i], group.mode));
+ }
+ base = makeSpan(["mop"], output, options.getColor());
+ }
+
+ if (hasLimits) {
+ // IE 8 clips \int if it is in a display: inline-block. We wrap it
+ // in a new span so it is an inline, and works.
+ base = makeSpan([], [base]);
+
+ var supmid;
+ var supKern;
+ var submid;
+ var subKern;
+ // We manually have to handle the superscripts and subscripts. This,
+ // aside from the kern calculations, is copied from supsub.
+ if (supGroup) {
+ var sup = buildGroup(
+ supGroup, options.withStyle(options.style.sup()));
+ supmid = makeSpan(
+ [options.style.reset(), options.style.sup().cls()], [sup]);
+
+ supKern = Math.max(
+ fontMetrics.metrics.bigOpSpacing1,
+ fontMetrics.metrics.bigOpSpacing3 - sup.depth);
+ }
+
+ if (subGroup) {
+ var sub = buildGroup(
+ subGroup, options.withStyle(options.style.sub()));
+ submid = makeSpan(
+ [options.style.reset(), options.style.sub().cls()],
+ [sub]);
+
+ subKern = Math.max(
+ fontMetrics.metrics.bigOpSpacing2,
+ fontMetrics.metrics.bigOpSpacing4 - sub.height);
+ }
+
+ // Build the final group as a vlist of the possible subscript, base,
+ // and possible superscript.
+ var finalGroup;
+ var top;
+ var bottom;
+ if (!supGroup) {
+ top = base.height - baseShift;
+
+ finalGroup = buildCommon.makeVList([
+ {type: "kern", size: fontMetrics.metrics.bigOpSpacing5},
+ {type: "elem", elem: submid},
+ {type: "kern", size: subKern},
+ {type: "elem", elem: base},
+ ], "top", top, options);
+
+ // Here, we shift the limits by the slant of the symbol. Note
+ // that we are supposed to shift the limits by 1/2 of the slant,
+ // but since we are centering the limits adding a full slant of
+ // margin will shift by 1/2 that.
+ finalGroup.children[0].style.marginLeft = -slant + "em";
+ } else if (!subGroup) {
+ bottom = base.depth + baseShift;
+
+ finalGroup = buildCommon.makeVList([
+ {type: "elem", elem: base},
+ {type: "kern", size: supKern},
+ {type: "elem", elem: supmid},
+ {type: "kern", size: fontMetrics.metrics.bigOpSpacing5},
+ ], "bottom", bottom, options);
+
+ // See comment above about slants
+ finalGroup.children[1].style.marginLeft = slant + "em";
+ } else if (!supGroup && !subGroup) {
+ // This case probably shouldn't occur (this would mean the
+ // supsub was sending us a group with no superscript or
+ // subscript) but be safe.
+ return base;
+ } else {
+ bottom = fontMetrics.metrics.bigOpSpacing5 +
+ submid.height + submid.depth +
+ subKern +
+ base.depth + baseShift;
+
+ finalGroup = buildCommon.makeVList([
+ {type: "kern", size: fontMetrics.metrics.bigOpSpacing5},
+ {type: "elem", elem: submid},
+ {type: "kern", size: subKern},
+ {type: "elem", elem: base},
+ {type: "kern", size: supKern},
+ {type: "elem", elem: supmid},
+ {type: "kern", size: fontMetrics.metrics.bigOpSpacing5},
+ ], "bottom", bottom, options);
+
+ // See comment above about slants
+ finalGroup.children[0].style.marginLeft = -slant + "em";
+ finalGroup.children[2].style.marginLeft = slant + "em";
+ }
+
+ return makeSpan(["mop", "op-limits"], [finalGroup]);
+ } else {
+ if (group.value.symbol) {
+ base.style.top = baseShift + "em";
+ }
+
+ return base;
+ }
+};
+
+groupTypes.katex = function(group, options, prev) {
+ // The KaTeX logo. The offsets for the K and a were chosen to look
+ // good, but the offsets for the T, E, and X were taken from the
+ // definition of \TeX in TeX (see TeXbook pg. 356)
+ var k = makeSpan(
+ ["k"], [buildCommon.mathsym("K", group.mode)]);
+ var a = makeSpan(
+ ["a"], [buildCommon.mathsym("A", group.mode)]);
+
+ a.height = (a.height + 0.2) * 0.75;
+ a.depth = (a.height - 0.2) * 0.75;
+
+ var t = makeSpan(
+ ["t"], [buildCommon.mathsym("T", group.mode)]);
+ var e = makeSpan(
+ ["e"], [buildCommon.mathsym("E", group.mode)]);
+
+ e.height = (e.height - 0.2155);
+ e.depth = (e.depth + 0.2155);
+
+ var x = makeSpan(
+ ["x"], [buildCommon.mathsym("X", group.mode)]);
+
+ return makeSpan(
+ ["katex-logo", "mord"], [k, a, t, e, x], options.getColor());
+};
+
+groupTypes.overline = function(group, options, prev) {
+ // Overlines are handled in the TeXbook pg 443, Rule 9.
+
+ // Build the inner group in the cramped style.
+ var innerGroup = buildGroup(group.value.body,
+ options.withStyle(options.style.cramp()));
+
+ var ruleWidth = fontMetrics.metrics.defaultRuleThickness /
+ options.style.sizeMultiplier;
+
+ // Create the line above the body
+ var line = makeSpan(
+ [options.style.reset(), Style.TEXT.cls(), "overline-line"]);
+ line.height = ruleWidth;
+ line.maxFontSize = 1.0;
+
+ // Generate the vlist, with the appropriate kerns
+ var vlist = buildCommon.makeVList([
+ {type: "elem", elem: innerGroup},
+ {type: "kern", size: 3 * ruleWidth},
+ {type: "elem", elem: line},
+ {type: "kern", size: ruleWidth},
+ ], "firstBaseline", null, options);
+
+ return makeSpan(["overline", "mord"], [vlist], options.getColor());
+};
+
+groupTypes.underline = function(group, options, prev) {
+ // Underlines are handled in the TeXbook pg 443, Rule 10.
+
+ // Build the inner group.
+ var innerGroup = buildGroup(group.value.body, options);
+
+ var ruleWidth = fontMetrics.metrics.defaultRuleThickness /
+ options.style.sizeMultiplier;
+
+ // Create the line above the body
+ var line = makeSpan(
+ [options.style.reset(), Style.TEXT.cls(), "underline-line"]);
+ line.height = ruleWidth;
+ line.maxFontSize = 1.0;
+
+ // Generate the vlist, with the appropriate kerns
+ var vlist = buildCommon.makeVList([
+ {type: "kern", size: ruleWidth},
+ {type: "elem", elem: line},
+ {type: "kern", size: 3 * ruleWidth},
+ {type: "elem", elem: innerGroup},
+ ], "top", innerGroup.height, options);
+
+ return makeSpan(["underline", "mord"], [vlist], options.getColor());
+};
+
+groupTypes.sqrt = function(group, options, prev) {
+ // Square roots are handled in the TeXbook pg. 443, Rule 11.
+
+ // First, we do the same steps as in overline to build the inner group
+ // and line
+ var inner = buildGroup(group.value.body,
+ options.withStyle(options.style.cramp()));
+
+ var ruleWidth = fontMetrics.metrics.defaultRuleThickness /
+ options.style.sizeMultiplier;
+
+ var line = makeSpan(
+ [options.style.reset(), Style.TEXT.cls(), "sqrt-line"], [],
+ options.getColor());
+ line.height = ruleWidth;
+ line.maxFontSize = 1.0;
+
+ var phi = ruleWidth;
+ if (options.style.id < Style.TEXT.id) {
+ phi = fontMetrics.metrics.xHeight;
+ }
+
+ // Calculate the clearance between the body and line
+ var lineClearance = ruleWidth + phi / 4;
+
+ var innerHeight =
+ (inner.height + inner.depth) * options.style.sizeMultiplier;
+ var minDelimiterHeight = innerHeight + lineClearance + ruleWidth;
+
+ // Create a \surd delimiter of the required minimum size
+ var delim = makeSpan(["sqrt-sign"], [
+ delimiter.customSizedDelim("\\surd", minDelimiterHeight,
+ false, options, group.mode)],
+ options.getColor());
+
+ var delimDepth = (delim.height + delim.depth) - ruleWidth;
+
+ // Adjust the clearance based on the delimiter size
+ if (delimDepth > inner.height + inner.depth + lineClearance) {
+ lineClearance =
+ (lineClearance + delimDepth - inner.height - inner.depth) / 2;
+ }
+
+ // Shift the delimiter so that its top lines up with the top of the line
+ var delimShift = -(inner.height + lineClearance + ruleWidth) + delim.height;
+ delim.style.top = delimShift + "em";
+ delim.height -= delimShift;
+ delim.depth += delimShift;
+
+ // We add a special case here, because even when `inner` is empty, we
+ // still get a line. So, we use a simple heuristic to decide if we
+ // should omit the body entirely. (note this doesn't work for something
+ // like `\sqrt{\rlap{x}}`, but if someone is doing that they deserve for
+ // it not to work.
+ var body;
+ if (inner.height === 0 && inner.depth === 0) {
+ body = makeSpan();
+ } else {
+ body = buildCommon.makeVList([
+ {type: "elem", elem: inner},
+ {type: "kern", size: lineClearance},
+ {type: "elem", elem: line},
+ {type: "kern", size: ruleWidth},
+ ], "firstBaseline", null, options);
+ }
+
+ if (!group.value.index) {
+ return makeSpan(["sqrt", "mord"], [delim, body]);
+ } else {
+ // Handle the optional root index
+
+ // The index is always in scriptscript style
+ var root = buildGroup(
+ group.value.index,
+ options.withStyle(Style.SCRIPTSCRIPT));
+ var rootWrap = makeSpan(
+ [options.style.reset(), Style.SCRIPTSCRIPT.cls()],
+ [root]);
+
+ // Figure out the height and depth of the inner part
+ var innerRootHeight = Math.max(delim.height, body.height);
+ var innerRootDepth = Math.max(delim.depth, body.depth);
+
+ // The amount the index is shifted by. This is taken from the TeX
+ // source, in the definition of `\r@@t`.
+ var toShift = 0.6 * (innerRootHeight - innerRootDepth);
+
+ // Build a VList with the superscript shifted up correctly
+ var rootVList = buildCommon.makeVList(
+ [{type: "elem", elem: rootWrap}],
+ "shift", -toShift, options);
+ // Add a class surrounding it so we can add on the appropriate
+ // kerning
+ var rootVListWrap = makeSpan(["root"], [rootVList]);
+
+ return makeSpan(["sqrt", "mord"], [rootVListWrap, delim, body]);
+ }
+};
+
+groupTypes.sizing = function(group, options, prev) {
+ // Handle sizing operators like \Huge. Real TeX doesn't actually allow
+ // these functions inside of math expressions, so we do some special
+ // handling.
+ var inner = buildExpression(group.value.value,
+ options.withSize(group.value.size), prev);
+
+ var span = makeSpan(["mord"],
+ [makeSpan(["sizing", "reset-" + options.size, group.value.size,
+ options.style.cls()],
+ inner)]);
+
+ // Calculate the correct maxFontSize manually
+ var fontSize = buildCommon.sizingMultiplier[group.value.size];
+ span.maxFontSize = fontSize * options.style.sizeMultiplier;
+
+ return span;
+};
+
+groupTypes.styling = function(group, options, prev) {
+ // Style changes are handled in the TeXbook on pg. 442, Rule 3.
+
+ // Figure out what style we're changing to.
+ var style = {
+ "display": Style.DISPLAY,
+ "text": Style.TEXT,
+ "script": Style.SCRIPT,
+ "scriptscript": Style.SCRIPTSCRIPT,
+ };
+
+ var newStyle = style[group.value.style];
+
+ // Build the inner expression in the new style.
+ var inner = buildExpression(
+ group.value.value, options.withStyle(newStyle), prev);
+
+ return makeSpan([options.style.reset(), newStyle.cls()], inner);
+};
+
+groupTypes.font = function(group, options, prev) {
+ var font = group.value.font;
+ return buildGroup(group.value.body, options.withFont(font), prev);
+};
+
+groupTypes.delimsizing = function(group, options, prev) {
+ var delim = group.value.value;
+
+ if (delim === ".") {
+ // Empty delimiters still count as elements, even though they don't
+ // show anything.
+ return makeSpan([groupToType[group.value.delimType]]);
+ }
+
+ // Use delimiter.sizedDelim to generate the delimiter.
+ return makeSpan(
+ [groupToType[group.value.delimType]],
+ [delimiter.sizedDelim(
+ delim, group.value.size, options, group.mode)]);
+};
+
+groupTypes.leftright = function(group, options, prev) {
+ // Build the inner expression
+ var inner = buildExpression(group.value.body, options.reset());
+
+ var innerHeight = 0;
+ var innerDepth = 0;
+
+ // Calculate its height and depth
+ for (var i = 0; i < inner.length; i++) {
+ innerHeight = Math.max(inner[i].height, innerHeight);
+ innerDepth = Math.max(inner[i].depth, innerDepth);
+ }
+
+ // The size of delimiters is the same, regardless of what style we are
+ // in. Thus, to correctly calculate the size of delimiter we need around
+ // a group, we scale down the inner size based on the size.
+ innerHeight *= options.style.sizeMultiplier;
+ innerDepth *= options.style.sizeMultiplier;
+
+ var leftDelim;
+ if (group.value.left === ".") {
+ // Empty delimiters in \left and \right make null delimiter spaces.
+ leftDelim = makeNullDelimiter(options);
+ } else {
+ // Otherwise, use leftRightDelim to generate the correct sized
+ // delimiter.
+ leftDelim = delimiter.leftRightDelim(
+ group.value.left, innerHeight, innerDepth, options,
+ group.mode);
+ }
+ // Add it to the beginning of the expression
+ inner.unshift(leftDelim);
+
+ var rightDelim;
+ // Same for the right delimiter
+ if (group.value.right === ".") {
+ rightDelim = makeNullDelimiter(options);
+ } else {
+ rightDelim = delimiter.leftRightDelim(
+ group.value.right, innerHeight, innerDepth, options,
+ group.mode);
+ }
+ // Add it to the end of the expression.
+ inner.push(rightDelim);
+
+ return makeSpan(
+ ["minner", options.style.cls()], inner, options.getColor());
+};
+
+groupTypes.rule = function(group, options, prev) {
+ // Make an empty span for the rule
+ var rule = makeSpan(["mord", "rule"], [], options.getColor());
+
+ // Calculate the shift, width, and height of the rule, and account for units
+ var shift = 0;
+ if (group.value.shift) {
+ shift = group.value.shift.number;
+ if (group.value.shift.unit === "ex") {
+ shift *= fontMetrics.metrics.xHeight;
+ }
+ }
+
+ var width = group.value.width.number;
+ if (group.value.width.unit === "ex") {
+ width *= fontMetrics.metrics.xHeight;
+ }
+
+ var height = group.value.height.number;
+ if (group.value.height.unit === "ex") {
+ height *= fontMetrics.metrics.xHeight;
+ }
+
+ // The sizes of rules are absolute, so make it larger if we are in a
+ // smaller style.
+ shift /= options.style.sizeMultiplier;
+ width /= options.style.sizeMultiplier;
+ height /= options.style.sizeMultiplier;
+
+ // Style the rule to the right size
+ rule.style.borderRightWidth = width + "em";
+ rule.style.borderTopWidth = height + "em";
+ rule.style.bottom = shift + "em";
+
+ // Record the height and width
+ rule.width = width;
+ rule.height = height + shift;
+ rule.depth = -shift;
+
+ return rule;
+};
+
+groupTypes.kern = function(group, options, prev) {
+ // Make an empty span for the rule
+ var rule = makeSpan(["mord", "rule"], [], options.getColor());
+
+ var dimension = 0;
+ if (group.value.dimension) {
+ dimension = group.value.dimension.number;
+ if (group.value.dimension.unit === "ex") {
+ dimension *= fontMetrics.metrics.xHeight;
+ }
+ }
+
+ dimension /= options.style.sizeMultiplier;
+
+ rule.style.marginLeft = dimension + "em";
+
+ return rule;
+};
+
+groupTypes.accent = function(group, options, prev) {
+ // Accents are handled in the TeXbook pg. 443, rule 12.
+ var base = group.value.base;
+
+ var supsubGroup;
+ if (group.type === "supsub") {
+ // If our base is a character box, and we have superscripts and
+ // subscripts, the supsub will defer to us. In particular, we want
+ // to attach the superscripts and subscripts to the inner body (so
+ // that the position of the superscripts and subscripts won't be
+ // affected by the height of the accent). We accomplish this by
+ // sticking the base of the accent into the base of the supsub, and
+ // rendering that, while keeping track of where the accent is.
+
+ // The supsub group is the group that was passed in
+ var supsub = group;
+ // The real accent group is the base of the supsub group
+ group = supsub.value.base;
+ // The character box is the base of the accent group
+ base = group.value.base;
+ // Stick the character box into the base of the supsub group
+ supsub.value.base = base;
+
+ // Rerender the supsub group with its new base, and store that
+ // result.
+ supsubGroup = buildGroup(
+ supsub, options.reset(), prev);
+ }
+
+ // Build the base group
+ var body = buildGroup(
+ base, options.withStyle(options.style.cramp()));
+
+ // Calculate the skew of the accent. This is based on the line "If the
+ // nucleus is not a single character, let s = 0; otherwise set s to the
+ // kern amount for the nucleus followed by the \skewchar of its font."
+ // Note that our skew metrics are just the kern between each character
+ // and the skewchar.
+ var skew;
+ if (isCharacterBox(base)) {
+ // If the base is a character box, then we want the skew of the
+ // innermost character. To do that, we find the innermost character:
+ var baseChar = getBaseElem(base);
+ // Then, we render its group to get the symbol inside it
+ var baseGroup = buildGroup(
+ baseChar, options.withStyle(options.style.cramp()));
+ // Finally, we pull the skew off of the symbol.
+ skew = baseGroup.skew;
+ // Note that we now throw away baseGroup, because the layers we
+ // removed with getBaseElem might contain things like \color which
+ // we can't get rid of.
+ // TODO(emily): Find a better way to get the skew
+ } else {
+ skew = 0;
+ }
+
+ // calculate the amount of space between the body and the accent
+ var clearance = Math.min(body.height, fontMetrics.metrics.xHeight);
+
+ // Build the accent
+ var accent = buildCommon.makeSymbol(
+ group.value.accent, "Main-Regular", "math", options.getColor());
+ // Remove the italic correction of the accent, because it only serves to
+ // shift the accent over to a place we don't want.
+ accent.italic = 0;
+
+ // The \vec character that the fonts use is a combining character, and
+ // thus shows up much too far to the left. To account for this, we add a
+ // specific class which shifts the accent over to where we want it.
+ // TODO(emily): Fix this in a better way, like by changing the font
+ var vecClass = group.value.accent === "\\vec" ? "accent-vec" : null;
+
+ var accentBody = makeSpan(["accent-body", vecClass], [
+ makeSpan([], [accent])]);
+
+ accentBody = buildCommon.makeVList([
+ {type: "elem", elem: body},
+ {type: "kern", size: -clearance},
+ {type: "elem", elem: accentBody},
+ ], "firstBaseline", null, options);
+
+ // Shift the accent over by the skew. Note we shift by twice the skew
+ // because we are centering the accent, so by adding 2*skew to the left,
+ // we shift it to the right by 1*skew.
+ accentBody.children[1].style.marginLeft = 2 * skew + "em";
+
+ var accentWrap = makeSpan(["mord", "accent"], [accentBody]);
+
+ if (supsubGroup) {
+ // Here, we replace the "base" child of the supsub with our newly
+ // generated accent.
+ supsubGroup.children[0] = accentWrap;
+
+ // Since we don't rerun the height calculation after replacing the
+ // accent, we manually recalculate height.
+ supsubGroup.height = Math.max(accentWrap.height, supsubGroup.height);
+
+ // Accents should always be ords, even when their innards are not.
+ supsubGroup.classes[0] = "mord";
+
+ return supsubGroup;
+ } else {
+ return accentWrap;
+ }
+};
+
+groupTypes.phantom = function(group, options, prev) {
+ var elements = buildExpression(
+ group.value.value,
+ options.withPhantom(),
+ prev
+ );
+
+ // \phantom isn't supposed to affect the elements it contains.
+ // See "color" for more details.
+ return new buildCommon.makeFragment(elements);
+};
+
+/**
+ * buildGroup is the function that takes a group and calls the correct groupType
+ * function for it. It also handles the interaction of size and style changes
+ * between parents and children.
+ */
+var buildGroup = function(group, options, prev) {
+ if (!group) {
+ return makeSpan();
+ }
+
+ if (groupTypes[group.type]) {
+ // Call the groupTypes function
+ var groupNode = groupTypes[group.type](group, options, prev);
+ var multiplier;
+
+ // If the style changed between the parent and the current group,
+ // account for the size difference
+ if (options.style !== options.parentStyle) {
+ multiplier = options.style.sizeMultiplier /
+ options.parentStyle.sizeMultiplier;
+
+ groupNode.height *= multiplier;
+ groupNode.depth *= multiplier;
+ }
+
+ // If the size changed between the parent and the current group, account
+ // for that size difference.
+ if (options.size !== options.parentSize) {
+ multiplier = buildCommon.sizingMultiplier[options.size] /
+ buildCommon.sizingMultiplier[options.parentSize];
+
+ groupNode.height *= multiplier;
+ groupNode.depth *= multiplier;
+ }
+
+ return groupNode;
+ } else {
+ throw new ParseError(
+ "Got group of unknown type: '" + group.type + "'");
+ }
+};
+
+/**
+ * Take an entire parse tree, and build it into an appropriate set of HTML
+ * nodes.
+ */
+var buildHTML = function(tree, options) {
+ // buildExpression is destructive, so we need to make a clone
+ // of the incoming tree so that it isn't accidentally changed
+ tree = JSON.parse(JSON.stringify(tree));
+
+ // Build the expression contained in the tree
+ var expression = buildExpression(tree, options);
+ var body = makeSpan(["base", options.style.cls()], expression);
+
+ // Add struts, which ensure that the top of the HTML element falls at the
+ // height of the expression, and the bottom of the HTML element falls at the
+ // depth of the expression.
+ var topStrut = makeSpan(["strut"]);
+ var bottomStrut = makeSpan(["strut", "bottom"]);
+
+ topStrut.style.height = body.height + "em";
+ bottomStrut.style.height = (body.height + body.depth) + "em";
+ // We'd like to use `vertical-align: top` but in IE 9 this lowers the
+ // baseline of the box to the bottom of this strut (instead staying in the
+ // normal place) so we use an absolute value for vertical-align instead
+ bottomStrut.style.verticalAlign = -body.depth + "em";
+
+ // Wrap the struts and body together
+ var htmlNode = makeSpan(["katex-html"], [topStrut, bottomStrut, body]);
+
+ htmlNode.setAttribute("aria-hidden", "true");
+
+ return htmlNode;
+};
+
+module.exports = buildHTML;
+
+},{"./ParseError":6,"./Style":9,"./buildCommon":10,"./delimiter":14,"./domTree":15,"./fontMetrics":17,"./utils":25}],12:[function(require,module,exports){
+/**
+ * This file converts a parse tree into a cooresponding MathML tree. The main
+ * entry point is the `buildMathML` function, which takes a parse tree from the
+ * parser.
+ */
+
+var buildCommon = require("./buildCommon");
+var fontMetrics = require("./fontMetrics");
+var mathMLTree = require("./mathMLTree");
+var ParseError = require("./ParseError");
+var symbols = require("./symbols");
+var utils = require("./utils");
+
+var makeSpan = buildCommon.makeSpan;
+var fontMap = buildCommon.fontMap;
+
+/**
+ * Takes a symbol and converts it into a MathML text node after performing
+ * optional replacement from symbols.js.
+ */
+var makeText = function(text, mode) {
+ if (symbols[mode][text] && symbols[mode][text].replace) {
+ text = symbols[mode][text].replace;
+ }
+
+ return new mathMLTree.TextNode(text);
+};
+
+/**
+ * Returns the math variant as a string or null if none is required.
+ */
+var getVariant = function(group, options) {
+ var font = options.font;
+ if (!font) {
+ return null;
+ }
+
+ var mode = group.mode;
+ if (font === "mathit") {
+ return "italic";
+ }
+
+ var value = group.value;
+ if (utils.contains(["\\imath", "\\jmath"], value)) {
+ return null;
+ }
+
+ if (symbols[mode][value] && symbols[mode][value].replace) {
+ value = symbols[mode][value].replace;
+ }
+
+ var fontName = fontMap[font].fontName;
+ if (fontMetrics.getCharacterMetrics(value, fontName)) {
+ return fontMap[options.font].variant;
+ }
+
+ return null;
+};
+
+/**
+ * Functions for handling the different types of groups found in the parse
+ * tree. Each function should take a parse group and return a MathML node.
+ */
+var groupTypes = {};
+
+groupTypes.mathord = function(group, options) {
+ var node = new mathMLTree.MathNode(
+ "mi",
+ [makeText(group.value, group.mode)]);
+
+ var variant = getVariant(group, options);
+ if (variant) {
+ node.setAttribute("mathvariant", variant);
+ }
+ return node;
+};
+
+groupTypes.textord = function(group, options) {
+ var text = makeText(group.value, group.mode);
+
+ var variant = getVariant(group, options) || "normal";
+
+ var node;
+ if (/[0-9]/.test(group.value)) {
+ // TODO(kevinb) merge adjacent <mn> nodes
+ // do it as a post processing step
+ node = new mathMLTree.MathNode("mn", [text]);
+ if (options.font) {
+ node.setAttribute("mathvariant", variant);
+ }
+ } else {
+ node = new mathMLTree.MathNode("mi", [text]);
+ node.setAttribute("mathvariant", variant);
+ }
+
+ return node;
+};
+
+groupTypes.bin = function(group) {
+ var node = new mathMLTree.MathNode(
+ "mo", [makeText(group.value, group.mode)]);
+
+ return node;
+};
+
+groupTypes.rel = function(group) {
+ var node = new mathMLTree.MathNode(
+ "mo", [makeText(group.value, group.mode)]);
+
+ return node;
+};
+
+groupTypes.open = function(group) {
+ var node = new mathMLTree.MathNode(
+ "mo", [makeText(group.value, group.mode)]);
+
+ return node;
+};
+
+groupTypes.close = function(group) {
+ var node = new mathMLTree.MathNode(
+ "mo", [makeText(group.value, group.mode)]);
+
+ return node;
+};
+
+groupTypes.inner = function(group) {
+ var node = new mathMLTree.MathNode(
+ "mo", [makeText(group.value, group.mode)]);
+
+ return node;
+};
+
+groupTypes.punct = function(group) {
+ var node = new mathMLTree.MathNode(
+ "mo", [makeText(group.value, group.mode)]);
+
+ node.setAttribute("separator", "true");
+
+ return node;
+};
+
+groupTypes.ordgroup = function(group, options) {
+ var inner = buildExpression(group.value, options);
+
+ var node = new mathMLTree.MathNode("mrow", inner);
+
+ return node;
+};
+
+groupTypes.text = function(group, options) {
+ var inner = buildExpression(group.value.body, options);
+
+ var node = new mathMLTree.MathNode("mtext", inner);
+
+ return node;
+};
+
+groupTypes.color = function(group, options) {
+ var inner = buildExpression(group.value.value, options);
+
+ var node = new mathMLTree.MathNode("mstyle", inner);
+
+ node.setAttribute("mathcolor", group.value.color);
+
+ return node;
+};
+
+groupTypes.supsub = function(group, options) {
+ var children = [buildGroup(group.value.base, options)];
+
+ if (group.value.sub) {
+ children.push(buildGroup(group.value.sub, options));
+ }
+
+ if (group.value.sup) {
+ children.push(buildGroup(group.value.sup, options));
+ }
+
+ var nodeType;
+ if (!group.value.sub) {
+ nodeType = "msup";
+ } else if (!group.value.sup) {
+ nodeType = "msub";
+ } else {
+ nodeType = "msubsup";
+ }
+
+ var node = new mathMLTree.MathNode(nodeType, children);
+
+ return node;
+};
+
+groupTypes.genfrac = function(group, options) {
+ var node = new mathMLTree.MathNode(
+ "mfrac",
+ [buildGroup(group.value.numer, options),
+ buildGroup(group.value.denom, options)]);
+
+ if (!group.value.hasBarLine) {
+ node.setAttribute("linethickness", "0px");
+ }
+
+ if (group.value.leftDelim != null || group.value.rightDelim != null) {
+ var withDelims = [];
+
+ if (group.value.leftDelim != null) {
+ var leftOp = new mathMLTree.MathNode(
+ "mo", [new mathMLTree.TextNode(group.value.leftDelim)]);
+
+ leftOp.setAttribute("fence", "true");
+
+ withDelims.push(leftOp);
+ }
+
+ withDelims.push(node);
+
+ if (group.value.rightDelim != null) {
+ var rightOp = new mathMLTree.MathNode(
+ "mo", [new mathMLTree.TextNode(group.value.rightDelim)]);
+
+ rightOp.setAttribute("fence", "true");
+
+ withDelims.push(rightOp);
+ }
+
+ var outerNode = new mathMLTree.MathNode("mrow", withDelims);
+
+ return outerNode;
+ }
+
+ return node;
+};
+
+groupTypes.array = function(group, options) {
+ return new mathMLTree.MathNode(
+ "mtable", group.value.body.map(function(row) {
+ return new mathMLTree.MathNode(
+ "mtr", row.map(function(cell) {
+ return new mathMLTree.MathNode(
+ "mtd", [buildGroup(cell, options)]);
+ }));
+ }));
+};
+
+groupTypes.sqrt = function(group, options) {
+ var node;
+ if (group.value.index) {
+ node = new mathMLTree.MathNode(
+ "mroot", [
+ buildGroup(group.value.body, options),
+ buildGroup(group.value.index, options),
+ ]);
+ } else {
+ node = new mathMLTree.MathNode(
+ "msqrt", [buildGroup(group.value.body, options)]);
+ }
+
+ return node;
+};
+
+groupTypes.leftright = function(group, options) {
+ var inner = buildExpression(group.value.body, options);
+
+ if (group.value.left !== ".") {
+ var leftNode = new mathMLTree.MathNode(
+ "mo", [makeText(group.value.left, group.mode)]);
+
+ leftNode.setAttribute("fence", "true");
+
+ inner.unshift(leftNode);
+ }
+
+ if (group.value.right !== ".") {
+ var rightNode = new mathMLTree.MathNode(
+ "mo", [makeText(group.value.right, group.mode)]);
+
+ rightNode.setAttribute("fence", "true");
+
+ inner.push(rightNode);
+ }
+
+ var outerNode = new mathMLTree.MathNode("mrow", inner);
+
+ return outerNode;
+};
+
+groupTypes.accent = function(group, options) {
+ var accentNode = new mathMLTree.MathNode(
+ "mo", [makeText(group.value.accent, group.mode)]);
+
+ var node = new mathMLTree.MathNode(
+ "mover",
+ [buildGroup(group.value.base, options),
+ accentNode]);
+
+ node.setAttribute("accent", "true");
+
+ return node;
+};
+
+groupTypes.spacing = function(group) {
+ var node;
+
+ if (group.value === "\\ " || group.value === "\\space" ||
+ group.value === " " || group.value === "~") {
+ node = new mathMLTree.MathNode(
+ "mtext", [new mathMLTree.TextNode("\u00a0")]);
+ } else {
+ node = new mathMLTree.MathNode("mspace");
+
+ node.setAttribute(
+ "width", buildCommon.spacingFunctions[group.value].size);
+ }
+
+ return node;
+};
+
+groupTypes.op = function(group) {
+ var node;
+
+ // TODO(emily): handle big operators using the `largeop` attribute
+
+ if (group.value.symbol) {
+ // This is a symbol. Just add the symbol.
+ node = new mathMLTree.MathNode(
+ "mo", [makeText(group.value.body, group.mode)]);
+ } else {
+ // This is a text operator. Add all of the characters from the
+ // operator's name.
+ // TODO(emily): Add a space in the middle of some of these
+ // operators, like \limsup.
+ node = new mathMLTree.MathNode(
+ "mi", [new mathMLTree.TextNode(group.value.body.slice(1))]);
+ }
+
+ return node;
+};
+
+groupTypes.katex = function(group) {
+ var node = new mathMLTree.MathNode(
+ "mtext", [new mathMLTree.TextNode("KaTeX")]);
+
+ return node;
+};
+
+groupTypes.font = function(group, options) {
+ var font = group.value.font;
+ return buildGroup(group.value.body, options.withFont(font));
+};
+
+groupTypes.delimsizing = function(group) {
+ var children = [];
+
+ if (group.value.value !== ".") {
+ children.push(makeText(group.value.value, group.mode));
+ }
+
+ var node = new mathMLTree.MathNode("mo", children);
+
+ if (group.value.delimType === "open" ||
+ group.value.delimType === "close") {
+ // Only some of the delimsizing functions act as fences, and they
+ // return "open" or "close" delimTypes.
+ node.setAttribute("fence", "true");
+ } else {
+ // Explicitly disable fencing if it's not a fence, to override the
+ // defaults.
+ node.setAttribute("fence", "false");
+ }
+
+ return node;
+};
+
+groupTypes.styling = function(group, options) {
+ var inner = buildExpression(group.value.value, options);
+
+ var node = new mathMLTree.MathNode("mstyle", inner);
+
+ var styleAttributes = {
+ "display": ["0", "true"],
+ "text": ["0", "false"],
+ "script": ["1", "false"],
+ "scriptscript": ["2", "false"],
+ };
+
+ var attr = styleAttributes[group.value.style];
+
+ node.setAttribute("scriptlevel", attr[0]);
+ node.setAttribute("displaystyle", attr[1]);
+
+ return node;
+};
+
+groupTypes.sizing = function(group, options) {
+ var inner = buildExpression(group.value.value, options);
+
+ var node = new mathMLTree.MathNode("mstyle", inner);
+
+ // TODO(emily): This doesn't produce the correct size for nested size
+ // changes, because we don't keep state of what style we're currently
+ // in, so we can't reset the size to normal before changing it. Now
+ // that we're passing an options parameter we should be able to fix
+ // this.
+ node.setAttribute(
+ "mathsize", buildCommon.sizingMultiplier[group.value.size] + "em");
+
+ return node;
+};
+
+groupTypes.overline = function(group, options) {
+ var operator = new mathMLTree.MathNode(
+ "mo", [new mathMLTree.TextNode("\u203e")]);
+ operator.setAttribute("stretchy", "true");
+
+ var node = new mathMLTree.MathNode(
+ "mover",
+ [buildGroup(group.value.body, options),
+ operator]);
+ node.setAttribute("accent", "true");
+
+ return node;
+};
+
+groupTypes.underline = function(group, options) {
+ var operator = new mathMLTree.MathNode(
+ "mo", [new mathMLTree.TextNode("\u203e")]);
+ operator.setAttribute("stretchy", "true");
+
+ var node = new mathMLTree.MathNode(
+ "munder",
+ [buildGroup(group.value.body, options),
+ operator]);
+ node.setAttribute("accentunder", "true");
+
+ return node;
+};
+
+groupTypes.rule = function(group) {
+ // TODO(emily): Figure out if there's an actual way to draw black boxes
+ // in MathML.
+ var node = new mathMLTree.MathNode("mrow");
+
+ return node;
+};
+
+groupTypes.kern = function(group) {
+ // TODO(kevin): Figure out if there's a way to add space in MathML
+ var node = new mathMLTree.MathNode("mrow");
+
+ return node;
+};
+
+groupTypes.llap = function(group, options) {
+ var node = new mathMLTree.MathNode(
+ "mpadded", [buildGroup(group.value.body, options)]);
+
+ node.setAttribute("lspace", "-1width");
+ node.setAttribute("width", "0px");
+
+ return node;
+};
+
+groupTypes.rlap = function(group, options) {
+ var node = new mathMLTree.MathNode(
+ "mpadded", [buildGroup(group.value.body, options)]);
+
+ node.setAttribute("width", "0px");
+
+ return node;
+};
+
+groupTypes.phantom = function(group, options, prev) {
+ var inner = buildExpression(group.value.value, options);
+ return new mathMLTree.MathNode("mphantom", inner);
+};
+
+/**
+ * Takes a list of nodes, builds them, and returns a list of the generated
+ * MathML nodes. A little simpler than the HTML version because we don't do any
+ * previous-node handling.
+ */
+var buildExpression = function(expression, options) {
+ var groups = [];
+ for (var i = 0; i < expression.length; i++) {
+ var group = expression[i];
+ groups.push(buildGroup(group, options));
+ }
+ return groups;
+};
+
+/**
+ * Takes a group from the parser and calls the appropriate groupTypes function
+ * on it to produce a MathML node.
+ */
+var buildGroup = function(group, options) {
+ if (!group) {
+ return new mathMLTree.MathNode("mrow");
+ }
+
+ if (groupTypes[group.type]) {
+ // Call the groupTypes function
+ return groupTypes[group.type](group, options);
+ } else {
+ throw new ParseError(
+ "Got group of unknown type: '" + group.type + "'");
+ }
+};
+
+/**
+ * Takes a full parse tree and settings and builds a MathML representation of
+ * it. In particular, we put the elements from building the parse tree into a
+ * <semantics> tag so we can also include that TeX source as an annotation.
+ *
+ * Note that we actually return a domTree element with a `<math>` inside it so
+ * we can do appropriate styling.
+ */
+var buildMathML = function(tree, texExpression, options) {
+ var expression = buildExpression(tree, options);
+
+ // Wrap up the expression in an mrow so it is presented in the semantics
+ // tag correctly.
+ var wrapper = new mathMLTree.MathNode("mrow", expression);
+
+ // Build a TeX annotation of the source
+ var annotation = new mathMLTree.MathNode(
+ "annotation", [new mathMLTree.TextNode(texExpression)]);
+
+ annotation.setAttribute("encoding", "application/x-tex");
+
+ var semantics = new mathMLTree.MathNode(
+ "semantics", [wrapper, annotation]);
+
+ var math = new mathMLTree.MathNode("math", [semantics]);
+
+ // You can't style <math> nodes, so we wrap the node in a span.
+ return makeSpan(["katex-mathml"], [math]);
+};
+
+module.exports = buildMathML;
+
+},{"./ParseError":6,"./buildCommon":10,"./fontMetrics":17,"./mathMLTree":20,"./symbols":23,"./utils":25}],13:[function(require,module,exports){
+var buildHTML = require("./buildHTML");
+var buildMathML = require("./buildMathML");
+var buildCommon = require("./buildCommon");
+var Options = require("./Options");
+var Settings = require("./Settings");
+var Style = require("./Style");
+
+var makeSpan = buildCommon.makeSpan;
+
+var buildTree = function(tree, expression, settings) {
+ settings = settings || new Settings({});
+
+ var startStyle = Style.TEXT;
+ if (settings.displayMode) {
+ startStyle = Style.DISPLAY;
+ }
+
+ // Setup the default options
+ var options = new Options({
+ style: startStyle,
+ size: "size5",
+ });
+
+ // `buildHTML` sometimes messes with the parse tree (like turning bins ->
+ // ords), so we build the MathML version first.
+ var mathMLNode = buildMathML(tree, expression, options);
+ var htmlNode = buildHTML(tree, options);
+
+ var katexNode = makeSpan(["katex"], [
+ mathMLNode, htmlNode,
+ ]);
+
+ if (settings.displayMode) {
+ return makeSpan(["katex-display"], [katexNode]);
+ } else {
+ return katexNode;
+ }
+};
+
+module.exports = buildTree;
+
+},{"./Options":5,"./Settings":8,"./Style":9,"./buildCommon":10,"./buildHTML":11,"./buildMathML":12}],14:[function(require,module,exports){
+/**
+ * This file deals with creating delimiters of various sizes. The TeXbook
+ * discusses these routines on page 441-442, in the "Another subroutine sets box
+ * x to a specified variable delimiter" paragraph.
+ *
+ * There are three main routines here. `makeSmallDelim` makes a delimiter in the
+ * normal font, but in either text, script, or scriptscript style.
+ * `makeLargeDelim` makes a delimiter in textstyle, but in one of the Size1,
+ * Size2, Size3, or Size4 fonts. `makeStackedDelim` makes a delimiter out of
+ * smaller pieces that are stacked on top of one another.
+ *
+ * The functions take a parameter `center`, which determines if the delimiter
+ * should be centered around the axis.
+ *
+ * Then, there are three exposed functions. `sizedDelim` makes a delimiter in
+ * one of the given sizes. This is used for things like `\bigl`.
+ * `customSizedDelim` makes a delimiter with a given total height+depth. It is
+ * called in places like `\sqrt`. `leftRightDelim` makes an appropriate
+ * delimiter which surrounds an expression of a given height an depth. It is
+ * used in `\left` and `\right`.
+ */
+
+var ParseError = require("./ParseError");
+var Style = require("./Style");
+
+var buildCommon = require("./buildCommon");
+var fontMetrics = require("./fontMetrics");
+var symbols = require("./symbols");
+var utils = require("./utils");
+
+var makeSpan = buildCommon.makeSpan;
+
+/**
+ * Get the metrics for a given symbol and font, after transformation (i.e.
+ * after following replacement from symbols.js)
+ */
+var getMetrics = function(symbol, font) {
+ if (symbols.math[symbol] && symbols.math[symbol].replace) {
+ return fontMetrics.getCharacterMetrics(
+ symbols.math[symbol].replace, font);
+ } else {
+ return fontMetrics.getCharacterMetrics(
+ symbol, font);
+ }
+};
+
+/**
+ * Builds a symbol in the given font size (note size is an integer)
+ */
+var mathrmSize = function(value, size, mode) {
+ return buildCommon.makeSymbol(value, "Size" + size + "-Regular", mode);
+};
+
+/**
+ * Puts a delimiter span in a given style, and adds appropriate height, depth,
+ * and maxFontSizes.
+ */
+var styleWrap = function(delim, toStyle, options) {
+ var span = makeSpan(
+ ["style-wrap", options.style.reset(), toStyle.cls()], [delim]);
+
+ var multiplier = toStyle.sizeMultiplier / options.style.sizeMultiplier;
+
+ span.height *= multiplier;
+ span.depth *= multiplier;
+ span.maxFontSize = toStyle.sizeMultiplier;
+
+ return span;
+};
+
+/**
+ * Makes a small delimiter. This is a delimiter that comes in the Main-Regular
+ * font, but is restyled to either be in textstyle, scriptstyle, or
+ * scriptscriptstyle.
+ */
+var makeSmallDelim = function(delim, style, center, options, mode) {
+ var text = buildCommon.makeSymbol(delim, "Main-Regular", mode);
+
+ var span = styleWrap(text, style, options);
+
+ if (center) {
+ var shift =
+ (1 - options.style.sizeMultiplier / style.sizeMultiplier) *
+ fontMetrics.metrics.axisHeight;
+
+ span.style.top = shift + "em";
+ span.height -= shift;
+ span.depth += shift;
+ }
+
+ return span;
+};
+
+/**
+ * Makes a large delimiter. This is a delimiter that comes in the Size1, Size2,
+ * Size3, or Size4 fonts. It is always rendered in textstyle.
+ */
+var makeLargeDelim = function(delim, size, center, options, mode) {
+ var inner = mathrmSize(delim, size, mode);
+
+ var span = styleWrap(
+ makeSpan(["delimsizing", "size" + size],
+ [inner], options.getColor()),
+ Style.TEXT, options);
+
+ if (center) {
+ var shift = (1 - options.style.sizeMultiplier) *
+ fontMetrics.metrics.axisHeight;
+
+ span.style.top = shift + "em";
+ span.height -= shift;
+ span.depth += shift;
+ }
+
+ return span;
+};
+
+/**
+ * Make an inner span with the given offset and in the given font. This is used
+ * in `makeStackedDelim` to make the stacking pieces for the delimiter.
+ */
+var makeInner = function(symbol, font, mode) {
+ var sizeClass;
+ // Apply the correct CSS class to choose the right font.
+ if (font === "Size1-Regular") {
+ sizeClass = "delim-size1";
+ } else if (font === "Size4-Regular") {
+ sizeClass = "delim-size4";
+ }
+
+ var inner = makeSpan(
+ ["delimsizinginner", sizeClass],
+ [makeSpan([], [buildCommon.makeSymbol(symbol, font, mode)])]);
+
+ // Since this will be passed into `makeVList` in the end, wrap the element
+ // in the appropriate tag that VList uses.
+ return {type: "elem", elem: inner};
+};
+
+/**
+ * Make a stacked delimiter out of a given delimiter, with the total height at
+ * least `heightTotal`. This routine is mentioned on page 442 of the TeXbook.
+ */
+var makeStackedDelim = function(delim, heightTotal, center, options, mode) {
+ // There are four parts, the top, an optional middle, a repeated part, and a
+ // bottom.
+ var top;
+ var middle;
+ var repeat;
+ var bottom;
+ top = repeat = bottom = delim;
+ middle = null;
+ // Also keep track of what font the delimiters are in
+ var font = "Size1-Regular";
+
+ // We set the parts and font based on the symbol. Note that we use
+ // '\u23d0' instead of '|' and '\u2016' instead of '\\|' for the
+ // repeats of the arrows
+ if (delim === "\\uparrow") {
+ repeat = bottom = "\u23d0";
+ } else if (delim === "\\Uparrow") {
+ repeat = bottom = "\u2016";
+ } else if (delim === "\\downarrow") {
+ top = repeat = "\u23d0";
+ } else if (delim === "\\Downarrow") {
+ top = repeat = "\u2016";
+ } else if (delim === "\\updownarrow") {
+ top = "\\uparrow";
+ repeat = "\u23d0";
+ bottom = "\\downarrow";
+ } else if (delim === "\\Updownarrow") {
+ top = "\\Uparrow";
+ repeat = "\u2016";
+ bottom = "\\Downarrow";
+ } else if (delim === "[" || delim === "\\lbrack") {
+ top = "\u23a1";
+ repeat = "\u23a2";
+ bottom = "\u23a3";
+ font = "Size4-Regular";
+ } else if (delim === "]" || delim === "\\rbrack") {
+ top = "\u23a4";
+ repeat = "\u23a5";
+ bottom = "\u23a6";
+ font = "Size4-Regular";
+ } else if (delim === "\\lfloor") {
+ repeat = top = "\u23a2";
+ bottom = "\u23a3";
+ font = "Size4-Regular";
+ } else if (delim === "\\lceil") {
+ top = "\u23a1";
+ repeat = bottom = "\u23a2";
+ font = "Size4-Regular";
+ } else if (delim === "\\rfloor") {
+ repeat = top = "\u23a5";
+ bottom = "\u23a6";
+ font = "Size4-Regular";
+ } else if (delim === "\\rceil") {
+ top = "\u23a4";
+ repeat = bottom = "\u23a5";
+ font = "Size4-Regular";
+ } else if (delim === "(") {
+ top = "\u239b";
+ repeat = "\u239c";
+ bottom = "\u239d";
+ font = "Size4-Regular";
+ } else if (delim === ")") {
+ top = "\u239e";
+ repeat = "\u239f";
+ bottom = "\u23a0";
+ font = "Size4-Regular";
+ } else if (delim === "\\{" || delim === "\\lbrace") {
+ top = "\u23a7";
+ middle = "\u23a8";
+ bottom = "\u23a9";
+ repeat = "\u23aa";
+ font = "Size4-Regular";
+ } else if (delim === "\\}" || delim === "\\rbrace") {
+ top = "\u23ab";
+ middle = "\u23ac";
+ bottom = "\u23ad";
+ repeat = "\u23aa";
+ font = "Size4-Regular";
+ } else if (delim === "\\lgroup") {
+ top = "\u23a7";
+ bottom = "\u23a9";
+ repeat = "\u23aa";
+ font = "Size4-Regular";
+ } else if (delim === "\\rgroup") {
+ top = "\u23ab";
+ bottom = "\u23ad";
+ repeat = "\u23aa";
+ font = "Size4-Regular";
+ } else if (delim === "\\lmoustache") {
+ top = "\u23a7";
+ bottom = "\u23ad";
+ repeat = "\u23aa";
+ font = "Size4-Regular";
+ } else if (delim === "\\rmoustache") {
+ top = "\u23ab";
+ bottom = "\u23a9";
+ repeat = "\u23aa";
+ font = "Size4-Regular";
+ } else if (delim === "\\surd") {
+ top = "\ue001";
+ bottom = "\u23b7";
+ repeat = "\ue000";
+ font = "Size4-Regular";
+ }
+
+ // Get the metrics of the four sections
+ var topMetrics = getMetrics(top, font);
+ var topHeightTotal = topMetrics.height + topMetrics.depth;
+ var repeatMetrics = getMetrics(repeat, font);
+ var repeatHeightTotal = repeatMetrics.height + repeatMetrics.depth;
+ var bottomMetrics = getMetrics(bottom, font);
+ var bottomHeightTotal = bottomMetrics.height + bottomMetrics.depth;
+ var middleHeightTotal = 0;
+ var middleFactor = 1;
+ if (middle !== null) {
+ var middleMetrics = getMetrics(middle, font);
+ middleHeightTotal = middleMetrics.height + middleMetrics.depth;
+ middleFactor = 2; // repeat symmetrically above and below middle
+ }
+
+ // Calcuate the minimal height that the delimiter can have.
+ // It is at least the size of the top, bottom, and optional middle combined.
+ var minHeight = topHeightTotal + bottomHeightTotal + middleHeightTotal;
+
+ // Compute the number of copies of the repeat symbol we will need
+ var repeatCount = Math.ceil(
+ (heightTotal - minHeight) / (middleFactor * repeatHeightTotal));
+
+ // Compute the total height of the delimiter including all the symbols
+ var realHeightTotal =
+ minHeight + repeatCount * middleFactor * repeatHeightTotal;
+
+ // The center of the delimiter is placed at the center of the axis. Note
+ // that in this context, "center" means that the delimiter should be
+ // centered around the axis in the current style, while normally it is
+ // centered around the axis in textstyle.
+ var axisHeight = fontMetrics.metrics.axisHeight;
+ if (center) {
+ axisHeight *= options.style.sizeMultiplier;
+ }
+ // Calculate the depth
+ var depth = realHeightTotal / 2 - axisHeight;
+
+ // Now, we start building the pieces that will go into the vlist
+
+ // Keep a list of the inner pieces
+ var inners = [];
+
+ // Add the bottom symbol
+ inners.push(makeInner(bottom, font, mode));
+
+ var i;
+ if (middle === null) {
+ // Add that many symbols
+ for (i = 0; i < repeatCount; i++) {
+ inners.push(makeInner(repeat, font, mode));
+ }
+ } else {
+ // When there is a middle bit, we need the middle part and two repeated
+ // sections
+ for (i = 0; i < repeatCount; i++) {
+ inners.push(makeInner(repeat, font, mode));
+ }
+ inners.push(makeInner(middle, font, mode));
+ for (i = 0; i < repeatCount; i++) {
+ inners.push(makeInner(repeat, font, mode));
+ }
+ }
+
+ // Add the top symbol
+ inners.push(makeInner(top, font, mode));
+
+ // Finally, build the vlist
+ var inner = buildCommon.makeVList(inners, "bottom", depth, options);
+
+ return styleWrap(
+ makeSpan(["delimsizing", "mult"], [inner], options.getColor()),
+ Style.TEXT, options);
+};
+
+// There are three kinds of delimiters, delimiters that stack when they become
+// too large
+var stackLargeDelimiters = [
+ "(", ")", "[", "\\lbrack", "]", "\\rbrack",
+ "\\{", "\\lbrace", "\\}", "\\rbrace",
+ "\\lfloor", "\\rfloor", "\\lceil", "\\rceil",
+ "\\surd",
+];
+
+// delimiters that always stack
+var stackAlwaysDelimiters = [
+ "\\uparrow", "\\downarrow", "\\updownarrow",
+ "\\Uparrow", "\\Downarrow", "\\Updownarrow",
+ "|", "\\|", "\\vert", "\\Vert",
+ "\\lvert", "\\rvert", "\\lVert", "\\rVert",
+ "\\lgroup", "\\rgroup", "\\lmoustache", "\\rmoustache",
+];
+
+// and delimiters that never stack
+var stackNeverDelimiters = [
+ "<", ">", "\\langle", "\\rangle", "/", "\\backslash", "\\lt", "\\gt",
+];
+
+// Metrics of the different sizes. Found by looking at TeX's output of
+// $\bigl| // \Bigl| \biggl| \Biggl| \showlists$
+// Used to create stacked delimiters of appropriate sizes in makeSizedDelim.
+var sizeToMaxHeight = [0, 1.2, 1.8, 2.4, 3.0];
+
+/**
+ * Used to create a delimiter of a specific size, where `size` is 1, 2, 3, or 4.
+ */
+var makeSizedDelim = function(delim, size, options, mode) {
+ // < and > turn into \langle and \rangle in delimiters
+ if (delim === "<" || delim === "\\lt") {
+ delim = "\\langle";
+ } else if (delim === ">" || delim === "\\gt") {
+ delim = "\\rangle";
+ }
+
+ // Sized delimiters are never centered.
+ if (utils.contains(stackLargeDelimiters, delim) ||
+ utils.contains(stackNeverDelimiters, delim)) {
+ return makeLargeDelim(delim, size, false, options, mode);
+ } else if (utils.contains(stackAlwaysDelimiters, delim)) {
+ return makeStackedDelim(
+ delim, sizeToMaxHeight[size], false, options, mode);
+ } else {
+ throw new ParseError("Illegal delimiter: '" + delim + "'");
+ }
+};
+
+/**
+ * There are three different sequences of delimiter sizes that the delimiters
+ * follow depending on the kind of delimiter. This is used when creating custom
+ * sized delimiters to decide whether to create a small, large, or stacked
+ * delimiter.
+ *
+ * In real TeX, these sequences aren't explicitly defined, but are instead
+ * defined inside the font metrics. Since there are only three sequences that
+ * are possible for the delimiters that TeX defines, it is easier to just encode
+ * them explicitly here.
+ */
+
+// Delimiters that never stack try small delimiters and large delimiters only
+var stackNeverDelimiterSequence = [
+ {type: "small", style: Style.SCRIPTSCRIPT},
+ {type: "small", style: Style.SCRIPT},
+ {type: "small", style: Style.TEXT},
+ {type: "large", size: 1},
+ {type: "large", size: 2},
+ {type: "large", size: 3},
+ {type: "large", size: 4},
+];
+
+// Delimiters that always stack try the small delimiters first, then stack
+var stackAlwaysDelimiterSequence = [
+ {type: "small", style: Style.SCRIPTSCRIPT},
+ {type: "small", style: Style.SCRIPT},
+ {type: "small", style: Style.TEXT},
+ {type: "stack"},
+];
+
+// Delimiters that stack when large try the small and then large delimiters, and
+// stack afterwards
+var stackLargeDelimiterSequence = [
+ {type: "small", style: Style.SCRIPTSCRIPT},
+ {type: "small", style: Style.SCRIPT},
+ {type: "small", style: Style.TEXT},
+ {type: "large", size: 1},
+ {type: "large", size: 2},
+ {type: "large", size: 3},
+ {type: "large", size: 4},
+ {type: "stack"},
+];
+
+/**
+ * Get the font used in a delimiter based on what kind of delimiter it is.
+ */
+var delimTypeToFont = function(type) {
+ if (type.type === "small") {
+ return "Main-Regular";
+ } else if (type.type === "large") {
+ return "Size" + type.size + "-Regular";
+ } else if (type.type === "stack") {
+ return "Size4-Regular";
+ }
+};
+
+/**
+ * Traverse a sequence of types of delimiters to decide what kind of delimiter
+ * should be used to create a delimiter of the given height+depth.
+ */
+var traverseSequence = function(delim, height, sequence, options) {
+ // Here, we choose the index we should start at in the sequences. In smaller
+ // sizes (which correspond to larger numbers in style.size) we start earlier
+ // in the sequence. Thus, scriptscript starts at index 3-3=0, script starts
+ // at index 3-2=1, text starts at 3-1=2, and display starts at min(2,3-0)=2
+ var start = Math.min(2, 3 - options.style.size);
+ for (var i = start; i < sequence.length; i++) {
+ if (sequence[i].type === "stack") {
+ // This is always the last delimiter, so we just break the loop now.
+ break;
+ }
+
+ var metrics = getMetrics(delim, delimTypeToFont(sequence[i]));
+ var heightDepth = metrics.height + metrics.depth;
+
+ // Small delimiters are scaled down versions of the same font, so we
+ // account for the style change size.
+
+ if (sequence[i].type === "small") {
+ heightDepth *= sequence[i].style.sizeMultiplier;
+ }
+
+ // Check if the delimiter at this size works for the given height.
+ if (heightDepth > height) {
+ return sequence[i];
+ }
+ }
+
+ // If we reached the end of the sequence, return the last sequence element.
+ return sequence[sequence.length - 1];
+};
+
+/**
+ * Make a delimiter of a given height+depth, with optional centering. Here, we
+ * traverse the sequences, and create a delimiter that the sequence tells us to.
+ */
+var makeCustomSizedDelim = function(delim, height, center, options, mode) {
+ if (delim === "<" || delim === "\\lt") {
+ delim = "\\langle";
+ } else if (delim === ">" || delim === "\\gt") {
+ delim = "\\rangle";
+ }
+
+ // Decide what sequence to use
+ var sequence;
+ if (utils.contains(stackNeverDelimiters, delim)) {
+ sequence = stackNeverDelimiterSequence;
+ } else if (utils.contains(stackLargeDelimiters, delim)) {
+ sequence = stackLargeDelimiterSequence;
+ } else {
+ sequence = stackAlwaysDelimiterSequence;
+ }
+
+ // Look through the sequence
+ var delimType = traverseSequence(delim, height, sequence, options);
+
+ // Depending on the sequence element we decided on, call the appropriate
+ // function.
+ if (delimType.type === "small") {
+ return makeSmallDelim(delim, delimType.style, center, options, mode);
+ } else if (delimType.type === "large") {
+ return makeLargeDelim(delim, delimType.size, center, options, mode);
+ } else if (delimType.type === "stack") {
+ return makeStackedDelim(delim, height, center, options, mode);
+ }
+};
+
+/**
+ * Make a delimiter for use with `\left` and `\right`, given a height and depth
+ * of an expression that the delimiters surround.
+ */
+var makeLeftRightDelim = function(delim, height, depth, options, mode) {
+ // We always center \left/\right delimiters, so the axis is always shifted
+ var axisHeight =
+ fontMetrics.metrics.axisHeight * options.style.sizeMultiplier;
+
+ // Taken from TeX source, tex.web, function make_left_right
+ var delimiterFactor = 901;
+ var delimiterExtend = 5.0 / fontMetrics.metrics.ptPerEm;
+
+ var maxDistFromAxis = Math.max(
+ height - axisHeight, depth + axisHeight);
+
+ var totalHeight = Math.max(
+ // In real TeX, calculations are done using integral values which are
+ // 65536 per pt, or 655360 per em. So, the division here truncates in
+ // TeX but doesn't here, producing different results. If we wanted to
+ // exactly match TeX's calculation, we could do
+ // Math.floor(655360 * maxDistFromAxis / 500) *
+ // delimiterFactor / 655360
+ // (To see the difference, compare
+ // x^{x^{\left(\rule{0.1em}{0.68em}\right)}}
+ // in TeX and KaTeX)
+ maxDistFromAxis / 500 * delimiterFactor,
+ 2 * maxDistFromAxis - delimiterExtend);
+
+ // Finally, we defer to `makeCustomSizedDelim` with our calculated total
+ // height
+ return makeCustomSizedDelim(delim, totalHeight, true, options, mode);
+};
+
+module.exports = {
+ sizedDelim: makeSizedDelim,
+ customSizedDelim: makeCustomSizedDelim,
+ leftRightDelim: makeLeftRightDelim,
+};
+
+},{"./ParseError":6,"./Style":9,"./buildCommon":10,"./fontMetrics":17,"./symbols":23,"./utils":25}],15:[function(require,module,exports){
+/**
+ * These objects store the data about the DOM nodes we create, as well as some
+ * extra data. They can then be transformed into real DOM nodes with the
+ * `toNode` function or HTML markup using `toMarkup`. They are useful for both
+ * storing extra properties on the nodes, as well as providing a way to easily
+ * work with the DOM.
+ *
+ * Similar functions for working with MathML nodes exist in mathMLTree.js.
+ */
+var unicodeRegexes = require("./unicodeRegexes");
+var utils = require("./utils");
+
+/**
+ * Create an HTML className based on a list of classes. In addition to joining
+ * with spaces, we also remove null or empty classes.
+ */
+var createClass = function(classes) {
+ classes = classes.slice();
+ for (var i = classes.length - 1; i >= 0; i--) {
+ if (!classes[i]) {
+ classes.splice(i, 1);
+ }
+ }
+
+ return classes.join(" ");
+};
+
+/**
+ * This node represents a span node, with a className, a list of children, and
+ * an inline style. It also contains information about its height, depth, and
+ * maxFontSize.
+ */
+function span(classes, children, height, depth, maxFontSize, style) {
+ this.classes = classes || [];
+ this.children = children || [];
+ this.height = height || 0;
+ this.depth = depth || 0;
+ this.maxFontSize = maxFontSize || 0;
+ this.style = style || {};
+ this.attributes = {};
+}
+
+/**
+ * Sets an arbitrary attribute on the span. Warning: use this wisely. Not all
+ * browsers support attributes the same, and having too many custom attributes
+ * is probably bad.
+ */
+span.prototype.setAttribute = function(attribute, value) {
+ this.attributes[attribute] = value;
+};
+
+/**
+ * Convert the span into an HTML node
+ */
+span.prototype.toNode = function() {
+ var span = document.createElement("span");
+
+ // Apply the class
+ span.className = createClass(this.classes);
+
+ // Apply inline styles
+ for (var style in this.style) {
+ if (Object.prototype.hasOwnProperty.call(this.style, style)) {
+ span.style[style] = this.style[style];
+ }
+ }
+
+ // Apply attributes
+ for (var attr in this.attributes) {
+ if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
+ span.setAttribute(attr, this.attributes[attr]);
+ }
+ }
+
+ // Append the children, also as HTML nodes
+ for (var i = 0; i < this.children.length; i++) {
+ span.appendChild(this.children[i].toNode());
+ }
+
+ return span;
+};
+
+/**
+ * Convert the span into an HTML markup string
+ */
+span.prototype.toMarkup = function() {
+ var markup = "<span";
+
+ // Add the class
+ if (this.classes.length) {
+ markup += " class=\"";
+ markup += utils.escape(createClass(this.classes));
+ markup += "\"";
+ }
+
+ var styles = "";
+
+ // Add the styles, after hyphenation
+ for (var style in this.style) {
+ if (this.style.hasOwnProperty(style)) {
+ styles += utils.hyphenate(style) + ":" + this.style[style] + ";";
+ }
+ }
+
+ if (styles) {
+ markup += " style=\"" + utils.escape(styles) + "\"";
+ }
+
+ // Add the attributes
+ for (var attr in this.attributes) {
+ if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
+ markup += " " + attr + "=\"";
+ markup += utils.escape(this.attributes[attr]);
+ markup += "\"";
+ }
+ }
+
+ markup += ">";
+
+ // Add the markup of the children, also as markup
+ for (var i = 0; i < this.children.length; i++) {
+ markup += this.children[i].toMarkup();
+ }
+
+ markup += "</span>";
+
+ return markup;
+};
+
+/**
+ * This node represents a document fragment, which contains elements, but when
+ * placed into the DOM doesn't have any representation itself. Thus, it only
+ * contains children and doesn't have any HTML properties. It also keeps track
+ * of a height, depth, and maxFontSize.
+ */
+function documentFragment(children, height, depth, maxFontSize) {
+ this.children = children || [];
+ this.height = height || 0;
+ this.depth = depth || 0;
+ this.maxFontSize = maxFontSize || 0;
+}
+
+/**
+ * Convert the fragment into a node
+ */
+documentFragment.prototype.toNode = function() {
+ // Create a fragment
+ var frag = document.createDocumentFragment();
+
+ // Append the children
+ for (var i = 0; i < this.children.length; i++) {
+ frag.appendChild(this.children[i].toNode());
+ }
+
+ return frag;
+};
+
+/**
+ * Convert the fragment into HTML markup
+ */
+documentFragment.prototype.toMarkup = function() {
+ var markup = "";
+
+ // Simply concatenate the markup for the children together
+ for (var i = 0; i < this.children.length; i++) {
+ markup += this.children[i].toMarkup();
+ }
+
+ return markup;
+};
+
+var iCombinations = {
+ 'î': '\u0131\u0302',
+ 'ï': '\u0131\u0308',
+ 'í': '\u0131\u0301',
+ // 'ī': '\u0131\u0304', // enable when we add Extended Latin
+ 'ì': '\u0131\u0300',
+};
+
+/**
+ * A symbol node contains information about a single symbol. It either renders
+ * to a single text node, or a span with a single text node in it, depending on
+ * whether it has CSS classes, styles, or needs italic correction.
+ */
+function symbolNode(value, height, depth, italic, skew, classes, style) {
+ this.value = value || "";
+ this.height = height || 0;
+ this.depth = depth || 0;
+ this.italic = italic || 0;
+ this.skew = skew || 0;
+ this.classes = classes || [];
+ this.style = style || {};
+ this.maxFontSize = 0;
+
+ // Mark CJK characters with specific classes so that we can specify which
+ // fonts to use. This allows us to render these characters with a serif
+ // font in situations where the browser would either default to a sans serif
+ // or render a placeholder character.
+ if (unicodeRegexes.cjkRegex.test(value)) {
+ // I couldn't find any fonts that contained Hangul as well as all of
+ // the other characters we wanted to test there for it gets its own
+ // CSS class.
+ if (unicodeRegexes.hangulRegex.test(value)) {
+ this.classes.push('hangul_fallback');
+ } else {
+ this.classes.push('cjk_fallback');
+ }
+ }
+
+ if (/[îïíì]/.test(this.value)) { // add ī when we add Extended Latin
+ this.value = iCombinations[this.value];
+ }
+}
+
+/**
+ * Creates a text node or span from a symbol node. Note that a span is only
+ * created if it is needed.
+ */
+symbolNode.prototype.toNode = function() {
+ var node = document.createTextNode(this.value);
+ var span = null;
+
+ if (this.italic > 0) {
+ span = document.createElement("span");
+ span.style.marginRight = this.italic + "em";
+ }
+
+ if (this.classes.length > 0) {
+ span = span || document.createElement("span");
+ span.className = createClass(this.classes);
+ }
+
+ for (var style in this.style) {
+ if (this.style.hasOwnProperty(style)) {
+ span = span || document.createElement("span");
+ span.style[style] = this.style[style];
+ }
+ }
+
+ if (span) {
+ span.appendChild(node);
+ return span;
+ } else {
+ return node;
+ }
+};
+
+/**
+ * Creates markup for a symbol node.
+ */
+symbolNode.prototype.toMarkup = function() {
+ // TODO(alpert): More duplication than I'd like from
+ // span.prototype.toMarkup and symbolNode.prototype.toNode...
+ var needsSpan = false;
+
+ var markup = "<span";
+
+ if (this.classes.length) {
+ needsSpan = true;
+ markup += " class=\"";
+ markup += utils.escape(createClass(this.classes));
+ markup += "\"";
+ }
+
+ var styles = "";
+
+ if (this.italic > 0) {
+ styles += "margin-right:" + this.italic + "em;";
+ }
+ for (var style in this.style) {
+ if (this.style.hasOwnProperty(style)) {
+ styles += utils.hyphenate(style) + ":" + this.style[style] + ";";
+ }
+ }
+
+ if (styles) {
+ needsSpan = true;
+ markup += " style=\"" + utils.escape(styles) + "\"";
+ }
+
+ var escaped = utils.escape(this.value);
+ if (needsSpan) {
+ markup += ">";
+ markup += escaped;
+ markup += "</span>";
+ return markup;
+ } else {
+ return escaped;
+ }
+};
+
+module.exports = {
+ span: span,
+ documentFragment: documentFragment,
+ symbolNode: symbolNode,
+};
+
+},{"./unicodeRegexes":24,"./utils":25}],16:[function(require,module,exports){
+/* eslint no-constant-condition:0 */
+var fontMetrics = require("./fontMetrics");
+var parseData = require("./parseData");
+var ParseError = require("./ParseError");
+
+var ParseNode = parseData.ParseNode;
+
+/**
+ * Parse the body of the environment, with rows delimited by \\ and
+ * columns delimited by &, and create a nested list in row-major order
+ * with one group per cell.
+ */
+function parseArray(parser, result) {
+ var row = [];
+ var body = [row];
+ var rowGaps = [];
+ while (true) {
+ var cell = parser.parseExpression(false, null);
+ row.push(new ParseNode("ordgroup", cell, parser.mode));
+ var next = parser.nextToken.text;
+ if (next === "&") {
+ parser.consume();
+ } else if (next === "\\end") {
+ break;
+ } else if (next === "\\\\" || next === "\\cr") {
+ var cr = parser.parseFunction();
+ rowGaps.push(cr.value.size);
+ row = [];
+ body.push(row);
+ } else {
+ throw new ParseError("Expected & or \\\\ or \\end",
+ parser.nextToken);
+ }
+ }
+ result.body = body;
+ result.rowGaps = rowGaps;
+ return new ParseNode(result.type, result, parser.mode);
+}
+
+/*
+ * An environment definition is very similar to a function definition:
+ * it is declared with a name or a list of names, a set of properties
+ * and a handler containing the actual implementation.
+ *
+ * The properties include:
+ * - numArgs: The number of arguments after the \begin{name} function.
+ * - argTypes: (optional) Just like for a function
+ * - allowedInText: (optional) Whether or not the environment is allowed inside
+ * text mode (default false) (not enforced yet)
+ * - numOptionalArgs: (optional) Just like for a function
+ * A bare number instead of that object indicates the numArgs value.
+ *
+ * The handler function will receive two arguments
+ * - context: information and references provided by the parser
+ * - args: an array of arguments passed to \begin{name}
+ * The context contains the following properties:
+ * - envName: the name of the environment, one of the listed names.
+ * - parser: the parser object
+ * - lexer: the lexer object
+ * - positions: the positions associated with these arguments from args.
+ * The handler must return a ParseResult.
+ */
+
+function defineEnvironment(names, props, handler) {
+ if (typeof names === "string") {
+ names = [names];
+ }
+ if (typeof props === "number") {
+ props = { numArgs: props };
+ }
+ // Set default values of environments
+ var data = {
+ numArgs: props.numArgs || 0,
+ argTypes: props.argTypes,
+ greediness: 1,
+ allowedInText: !!props.allowedInText,
+ numOptionalArgs: props.numOptionalArgs || 0,
+ handler: handler,
+ };
+ for (var i = 0; i < names.length; ++i) {
+ module.exports[names[i]] = data;
+ }
+}
+
+// Arrays are part of LaTeX, defined in lttab.dtx so its documentation
+// is part of the source2e.pdf file of LaTeX2e source documentation.
+defineEnvironment("array", {
+ numArgs: 1,
+}, function(context, args) {
+ var colalign = args[0];
+ colalign = colalign.value.map ? colalign.value : [colalign];
+ var cols = colalign.map(function(node) {
+ var ca = node.value;
+ if ("lcr".indexOf(ca) !== -1) {
+ return {
+ type: "align",
+ align: ca,
+ };
+ } else if (ca === "|") {
+ return {
+ type: "separator",
+ separator: "|",
+ };
+ }
+ throw new ParseError(
+ "Unknown column alignment: " + node.value,
+ node);
+ });
+ var res = {
+ type: "array",
+ cols: cols,
+ hskipBeforeAndAfter: true, // \@preamble in lttab.dtx
+ };
+ res = parseArray(context.parser, res);
+ return res;
+});
+
+// The matrix environments of amsmath builds on the array environment
+// of LaTeX, which is discussed above.
+defineEnvironment([
+ "matrix",
+ "pmatrix",
+ "bmatrix",
+ "Bmatrix",
+ "vmatrix",
+ "Vmatrix",
+], {
+}, function(context) {
+ var delimiters = {
+ "matrix": null,
+ "pmatrix": ["(", ")"],
+ "bmatrix": ["[", "]"],
+ "Bmatrix": ["\\{", "\\}"],
+ "vmatrix": ["|", "|"],
+ "Vmatrix": ["\\Vert", "\\Vert"],
+ }[context.envName];
+ var res = {
+ type: "array",
+ hskipBeforeAndAfter: false, // \hskip -\arraycolsep in amsmath
+ };
+ res = parseArray(context.parser, res);
+ if (delimiters) {
+ res = new ParseNode("leftright", {
+ body: [res],
+ left: delimiters[0],
+ right: delimiters[1],
+ }, context.mode);
+ }
+ return res;
+});
+
+// A cases environment (in amsmath.sty) is almost equivalent to
+// \def\arraystretch{1.2}%
+// \left\{\begin{array}{@{}l@{\quad}l@{}} … \end{array}\right.
+defineEnvironment("cases", {
+}, function(context) {
+ var res = {
+ type: "array",
+ arraystretch: 1.2,
+ cols: [{
+ type: "align",
+ align: "l",
+ pregap: 0,
+ postgap: fontMetrics.metrics.quad,
+ }, {
+ type: "align",
+ align: "l",
+ pregap: 0,
+ postgap: 0,
+ }],
+ };
+ res = parseArray(context.parser, res);
+ res = new ParseNode("leftright", {
+ body: [res],
+ left: "\\{",
+ right: ".",
+ }, context.mode);
+ return res;
+});
+
+// An aligned environment is like the align* environment
+// except it operates within math mode.
+// Note that we assume \nomallineskiplimit to be zero,
+// so that \strut@ is the same as \strut.
+defineEnvironment("aligned", {
+}, function(context) {
+ var res = {
+ type: "array",
+ cols: [],
+ };
+ res = parseArray(context.parser, res);
+ var emptyGroup = new ParseNode("ordgroup", [], context.mode);
+ var numCols = 0;
+ res.value.body.forEach(function(row) {
+ var i;
+ for (i = 1; i < row.length; i += 2) {
+ row[i].value.unshift(emptyGroup);
+ }
+ if (numCols < row.length) {
+ numCols = row.length;
+ }
+ });
+ for (var i = 0; i < numCols; ++i) {
+ var align = "r";
+ var pregap = 0;
+ if (i % 2 === 1) {
+ align = "l";
+ } else if (i > 0) {
+ pregap = 2; // one \qquad between columns
+ }
+ res.value.cols[i] = {
+ type: "align",
+ align: align,
+ pregap: pregap,
+ postgap: 0,
+ };
+ }
+ return res;
+});
+
+},{"./ParseError":6,"./fontMetrics":17,"./parseData":21}],17:[function(require,module,exports){
+/* eslint no-unused-vars:0 */
+
+var Style = require("./Style");
+var cjkRegex = require("./unicodeRegexes").cjkRegex;
+
+/**
+ * This file contains metrics regarding fonts and individual symbols. The sigma
+ * and xi variables, as well as the metricMap map contain data extracted from
+ * TeX, TeX font metrics, and the TTF files. These data are then exposed via the
+ * `metrics` variable and the getCharacterMetrics function.
+ */
+
+// These font metrics are extracted from TeX by using
+// \font\a=cmmi10
+// \showthe\fontdimenX\a
+// where X is the corresponding variable number. These correspond to the font
+// parameters of the symbol fonts. In TeX, there are actually three sets of
+// dimensions, one for each of textstyle, scriptstyle, and scriptscriptstyle,
+// but we only use the textstyle ones, and scale certain dimensions accordingly.
+// See the TeXbook, page 441.
+var sigma1 = 0.025;
+var sigma2 = 0;
+var sigma3 = 0;
+var sigma4 = 0;
+var sigma5 = 0.431;
+var sigma6 = 1;
+var sigma7 = 0;
+var sigma8 = 0.677;
+var sigma9 = 0.394;
+var sigma10 = 0.444;
+var sigma11 = 0.686;
+var sigma12 = 0.345;
+var sigma13 = 0.413;
+var sigma14 = 0.363;
+var sigma15 = 0.289;
+var sigma16 = 0.150;
+var sigma17 = 0.247;
+var sigma18 = 0.386;
+var sigma19 = 0.050;
+var sigma20 = 2.390;
+var sigma21 = 1.01;
+var sigma21Script = 0.81;
+var sigma21ScriptScript = 0.71;
+var sigma22 = 0.250;
+
+// These font metrics are extracted from TeX by using
+// \font\a=cmex10
+// \showthe\fontdimenX\a
+// where X is the corresponding variable number. These correspond to the font
+// parameters of the extension fonts (family 3). See the TeXbook, page 441.
+var xi1 = 0;
+var xi2 = 0;
+var xi3 = 0;
+var xi4 = 0;
+var xi5 = 0.431;
+var xi6 = 1;
+var xi7 = 0;
+var xi8 = 0.04;
+var xi9 = 0.111;
+var xi10 = 0.166;
+var xi11 = 0.2;
+var xi12 = 0.6;
+var xi13 = 0.1;
+
+// This value determines how large a pt is, for metrics which are defined in
+// terms of pts.
+// This value is also used in katex.less; if you change it make sure the values
+// match.
+var ptPerEm = 10.0;
+
+// The space between adjacent `|` columns in an array definition. From
+// `\showthe\doublerulesep` in LaTeX.
+var doubleRuleSep = 2.0 / ptPerEm;
+
+/**
+ * This is just a mapping from common names to real metrics
+ */
+var metrics = {
+ xHeight: sigma5,
+ quad: sigma6,
+ num1: sigma8,
+ num2: sigma9,
+ num3: sigma10,
+ denom1: sigma11,
+ denom2: sigma12,
+ sup1: sigma13,
+ sup2: sigma14,
+ sup3: sigma15,
+ sub1: sigma16,
+ sub2: sigma17,
+ supDrop: sigma18,
+ subDrop: sigma19,
+ axisHeight: sigma22,
+ defaultRuleThickness: xi8,
+ bigOpSpacing1: xi9,
+ bigOpSpacing2: xi10,
+ bigOpSpacing3: xi11,
+ bigOpSpacing4: xi12,
+ bigOpSpacing5: xi13,
+ ptPerEm: ptPerEm,
+ emPerEx: sigma5 / sigma6,
+ doubleRuleSep: doubleRuleSep,
+
+ // TODO(alpert): Missing parallel structure here. We should probably add
+ // style-specific metrics for all of these.
+ delim1: sigma20,
+ getDelim2: function(style) {
+ if (style.size === Style.TEXT.size) {
+ return sigma21;
+ } else if (style.size === Style.SCRIPT.size) {
+ return sigma21Script;
+ } else if (style.size === Style.SCRIPTSCRIPT.size) {
+ return sigma21ScriptScript;
+ }
+ throw new Error("Unexpected style size: " + style.size);
+ },
+};
+
+// This map contains a mapping from font name and character code to character
+// metrics, including height, depth, italic correction, and skew (kern from the
+// character to the corresponding \skewchar)
+// This map is generated via `make metrics`. It should not be changed manually.
+var metricMap = require("./fontMetricsData");
+
+// These are very rough approximations. We default to Times New Roman which
+// should have Latin-1 and Cyrillic characters, but may not depending on the
+// operating system. The metrics do not account for extra height from the
+// accents. In the case of Cyrillic characters which have both ascenders and
+// descenders we prefer approximations with ascenders, primarily to prevent
+// the fraction bar or root line from intersecting the glyph.
+// TODO(kevinb) allow union of multiple glyph metrics for better accuracy.
+var extraCharacterMap = {
+ // Latin-1
+ 'À': 'A',
+ 'Á': 'A',
+ 'Â': 'A',
+ 'Ã': 'A',
+ 'Ä': 'A',
+ 'Å': 'A',
+ 'Æ': 'A',
+ 'Ç': 'C',
+ 'È': 'E',
+ 'É': 'E',
+ 'Ê': 'E',
+ 'Ë': 'E',
+ 'Ì': 'I',
+ 'Í': 'I',
+ 'Î': 'I',
+ 'Ï': 'I',
+ 'Ð': 'D',
+ 'Ñ': 'N',
+ 'Ò': 'O',
+ 'Ó': 'O',
+ 'Ô': 'O',
+ 'Õ': 'O',
+ 'Ö': 'O',
+ 'Ø': 'O',
+ 'Ù': 'U',
+ 'Ú': 'U',
+ 'Û': 'U',
+ 'Ü': 'U',
+ 'Ý': 'Y',
+ 'Þ': 'o',
+ 'ß': 'B',
+ 'à': 'a',
+ 'á': 'a',
+ 'â': 'a',
+ 'ã': 'a',
+ 'ä': 'a',
+ 'å': 'a',
+ 'æ': 'a',
+ 'ç': 'c',
+ 'è': 'e',
+ 'é': 'e',
+ 'ê': 'e',
+ 'ë': 'e',
+ 'ì': 'i',
+ 'í': 'i',
+ 'î': 'i',
+ 'ï': 'i',
+ 'ð': 'd',
+ 'ñ': 'n',
+ 'ò': 'o',
+ 'ó': 'o',
+ 'ô': 'o',
+ 'õ': 'o',
+ 'ö': 'o',
+ 'ø': 'o',
+ 'ù': 'u',
+ 'ú': 'u',
+ 'û': 'u',
+ 'ü': 'u',
+ 'ý': 'y',
+ 'þ': 'o',
+ 'ÿ': 'y',
+
+ // Cyrillic
+ 'А': 'A',
+ 'Б': 'B',
+ 'В': 'B',
+ 'Г': 'F',
+ 'Д': 'A',
+ 'Е': 'E',
+ 'Ж': 'K',
+ 'З': '3',
+ 'И': 'N',
+ 'Й': 'N',
+ 'К': 'K',
+ 'Л': 'N',
+ 'М': 'M',
+ 'Н': 'H',
+ 'О': 'O',
+ 'П': 'N',
+ 'Р': 'P',
+ 'С': 'C',
+ 'Т': 'T',
+ 'У': 'y',
+ 'Ф': 'O',
+ 'Х': 'X',
+ 'Ц': 'U',
+ 'Ч': 'h',
+ 'Ш': 'W',
+ 'Щ': 'W',
+ 'Ъ': 'B',
+ 'Ы': 'X',
+ 'Ь': 'B',
+ 'Э': '3',
+ 'Ю': 'X',
+ 'Я': 'R',
+ 'а': 'a',
+ 'б': 'b',
+ 'в': 'a',
+ 'г': 'r',
+ 'д': 'y',
+ 'е': 'e',
+ 'ж': 'm',
+ 'з': 'e',
+ 'и': 'n',
+ 'й': 'n',
+ 'к': 'n',
+ 'л': 'n',
+ 'м': 'm',
+ 'н': 'n',
+ 'о': 'o',
+ 'п': 'n',
+ 'р': 'p',
+ 'с': 'c',
+ 'т': 'o',
+ 'у': 'y',
+ 'ф': 'b',
+ 'х': 'x',
+ 'ц': 'n',
+ 'ч': 'n',
+ 'ш': 'w',
+ 'щ': 'w',
+ 'ъ': 'a',
+ 'ы': 'm',
+ 'ь': 'a',
+ 'э': 'e',
+ 'ю': 'm',
+ 'я': 'r',
+};
+
+/**
+ * This function is a convenience function for looking up information in the
+ * metricMap table. It takes a character as a string, and a style.
+ *
+ * Note: the `width` property may be undefined if fontMetricsData.js wasn't
+ * built using `Make extended_metrics`.
+ */
+var getCharacterMetrics = function(character, style) {
+ var ch = character.charCodeAt(0);
+ if (character[0] in extraCharacterMap) {
+ ch = extraCharacterMap[character[0]].charCodeAt(0);
+ } else if (cjkRegex.test(character[0])) {
+ ch = 'M'.charCodeAt(0);
+ }
+ var metrics = metricMap[style][ch];
+ if (metrics) {
+ return {
+ depth: metrics[0],
+ height: metrics[1],
+ italic: metrics[2],
+ skew: metrics[3],
+ width: metrics[4],
+ };
+ }
+};
+
+module.exports = {
+ metrics: metrics,
+ getCharacterMetrics: getCharacterMetrics,
+};
+
+},{"./Style":9,"./fontMetricsData":18,"./unicodeRegexes":24}],18:[function(require,module,exports){
+module.exports = {
+ "AMS-Regular": {
+ "65": [0, 0.68889, 0, 0],
+ "66": [0, 0.68889, 0, 0],
+ "67": [0, 0.68889, 0, 0],
+ "68": [0, 0.68889, 0, 0],
+ "69": [0, 0.68889, 0, 0],
+ "70": [0, 0.68889, 0, 0],
+ "71": [0, 0.68889, 0, 0],
+ "72": [0, 0.68889, 0, 0],
+ "73": [0, 0.68889, 0, 0],
+ "74": [0.16667, 0.68889, 0, 0],
+ "75": [0, 0.68889, 0, 0],
+ "76": [0, 0.68889, 0, 0],
+ "77": [0, 0.68889, 0, 0],
+ "78": [0, 0.68889, 0, 0],
+ "79": [0.16667, 0.68889, 0, 0],
+ "80": [0, 0.68889, 0, 0],
+ "81": [0.16667, 0.68889, 0, 0],
+ "82": [0, 0.68889, 0, 0],
+ "83": [0, 0.68889, 0, 0],
+ "84": [0, 0.68889, 0, 0],
+ "85": [0, 0.68889, 0, 0],
+ "86": [0, 0.68889, 0, 0],
+ "87": [0, 0.68889, 0, 0],
+ "88": [0, 0.68889, 0, 0],
+ "89": [0, 0.68889, 0, 0],
+ "90": [0, 0.68889, 0, 0],
+ "107": [0, 0.68889, 0, 0],
+ "165": [0, 0.675, 0.025, 0],
+ "174": [0.15559, 0.69224, 0, 0],
+ "240": [0, 0.68889, 0, 0],
+ "295": [0, 0.68889, 0, 0],
+ "710": [0, 0.825, 0, 0],
+ "732": [0, 0.9, 0, 0],
+ "770": [0, 0.825, 0, 0],
+ "771": [0, 0.9, 0, 0],
+ "989": [0.08167, 0.58167, 0, 0],
+ "1008": [0, 0.43056, 0.04028, 0],
+ "8245": [0, 0.54986, 0, 0],
+ "8463": [0, 0.68889, 0, 0],
+ "8487": [0, 0.68889, 0, 0],
+ "8498": [0, 0.68889, 0, 0],
+ "8502": [0, 0.68889, 0, 0],
+ "8503": [0, 0.68889, 0, 0],
+ "8504": [0, 0.68889, 0, 0],
+ "8513": [0, 0.68889, 0, 0],
+ "8592": [-0.03598, 0.46402, 0, 0],
+ "8594": [-0.03598, 0.46402, 0, 0],
+ "8602": [-0.13313, 0.36687, 0, 0],
+ "8603": [-0.13313, 0.36687, 0, 0],
+ "8606": [0.01354, 0.52239, 0, 0],
+ "8608": [0.01354, 0.52239, 0, 0],
+ "8610": [0.01354, 0.52239, 0, 0],
+ "8611": [0.01354, 0.52239, 0, 0],
+ "8619": [0, 0.54986, 0, 0],
+ "8620": [0, 0.54986, 0, 0],
+ "8621": [-0.13313, 0.37788, 0, 0],
+ "8622": [-0.13313, 0.36687, 0, 0],
+ "8624": [0, 0.69224, 0, 0],
+ "8625": [0, 0.69224, 0, 0],
+ "8630": [0, 0.43056, 0, 0],
+ "8631": [0, 0.43056, 0, 0],
+ "8634": [0.08198, 0.58198, 0, 0],
+ "8635": [0.08198, 0.58198, 0, 0],
+ "8638": [0.19444, 0.69224, 0, 0],
+ "8639": [0.19444, 0.69224, 0, 0],
+ "8642": [0.19444, 0.69224, 0, 0],
+ "8643": [0.19444, 0.69224, 0, 0],
+ "8644": [0.1808, 0.675, 0, 0],
+ "8646": [0.1808, 0.675, 0, 0],
+ "8647": [0.1808, 0.675, 0, 0],
+ "8648": [0.19444, 0.69224, 0, 0],
+ "8649": [0.1808, 0.675, 0, 0],
+ "8650": [0.19444, 0.69224, 0, 0],
+ "8651": [0.01354, 0.52239, 0, 0],
+ "8652": [0.01354, 0.52239, 0, 0],
+ "8653": [-0.13313, 0.36687, 0, 0],
+ "8654": [-0.13313, 0.36687, 0, 0],
+ "8655": [-0.13313, 0.36687, 0, 0],
+ "8666": [0.13667, 0.63667, 0, 0],
+ "8667": [0.13667, 0.63667, 0, 0],
+ "8669": [-0.13313, 0.37788, 0, 0],
+ "8672": [-0.064, 0.437, 0, 0],
+ "8674": [-0.064, 0.437, 0, 0],
+ "8705": [0, 0.825, 0, 0],
+ "8708": [0, 0.68889, 0, 0],
+ "8709": [0.08167, 0.58167, 0, 0],
+ "8717": [0, 0.43056, 0, 0],
+ "8722": [-0.03598, 0.46402, 0, 0],
+ "8724": [0.08198, 0.69224, 0, 0],
+ "8726": [0.08167, 0.58167, 0, 0],
+ "8733": [0, 0.69224, 0, 0],
+ "8736": [0, 0.69224, 0, 0],
+ "8737": [0, 0.69224, 0, 0],
+ "8738": [0.03517, 0.52239, 0, 0],
+ "8739": [0.08167, 0.58167, 0, 0],
+ "8740": [0.25142, 0.74111, 0, 0],
+ "8741": [0.08167, 0.58167, 0, 0],
+ "8742": [0.25142, 0.74111, 0, 0],
+ "8756": [0, 0.69224, 0, 0],
+ "8757": [0, 0.69224, 0, 0],
+ "8764": [-0.13313, 0.36687, 0, 0],
+ "8765": [-0.13313, 0.37788, 0, 0],
+ "8769": [-0.13313, 0.36687, 0, 0],
+ "8770": [-0.03625, 0.46375, 0, 0],
+ "8774": [0.30274, 0.79383, 0, 0],
+ "8776": [-0.01688, 0.48312, 0, 0],
+ "8778": [0.08167, 0.58167, 0, 0],
+ "8782": [0.06062, 0.54986, 0, 0],
+ "8783": [0.06062, 0.54986, 0, 0],
+ "8785": [0.08198, 0.58198, 0, 0],
+ "8786": [0.08198, 0.58198, 0, 0],
+ "8787": [0.08198, 0.58198, 0, 0],
+ "8790": [0, 0.69224, 0, 0],
+ "8791": [0.22958, 0.72958, 0, 0],
+ "8796": [0.08198, 0.91667, 0, 0],
+ "8806": [0.25583, 0.75583, 0, 0],
+ "8807": [0.25583, 0.75583, 0, 0],
+ "8808": [0.25142, 0.75726, 0, 0],
+ "8809": [0.25142, 0.75726, 0, 0],
+ "8812": [0.25583, 0.75583, 0, 0],
+ "8814": [0.20576, 0.70576, 0, 0],
+ "8815": [0.20576, 0.70576, 0, 0],
+ "8816": [0.30274, 0.79383, 0, 0],
+ "8817": [0.30274, 0.79383, 0, 0],
+ "8818": [0.22958, 0.72958, 0, 0],
+ "8819": [0.22958, 0.72958, 0, 0],
+ "8822": [0.1808, 0.675, 0, 0],
+ "8823": [0.1808, 0.675, 0, 0],
+ "8828": [0.13667, 0.63667, 0, 0],
+ "8829": [0.13667, 0.63667, 0, 0],
+ "8830": [0.22958, 0.72958, 0, 0],
+ "8831": [0.22958, 0.72958, 0, 0],
+ "8832": [0.20576, 0.70576, 0, 0],
+ "8833": [0.20576, 0.70576, 0, 0],
+ "8840": [0.30274, 0.79383, 0, 0],
+ "8841": [0.30274, 0.79383, 0, 0],
+ "8842": [0.13597, 0.63597, 0, 0],
+ "8843": [0.13597, 0.63597, 0, 0],
+ "8847": [0.03517, 0.54986, 0, 0],
+ "8848": [0.03517, 0.54986, 0, 0],
+ "8858": [0.08198, 0.58198, 0, 0],
+ "8859": [0.08198, 0.58198, 0, 0],
+ "8861": [0.08198, 0.58198, 0, 0],
+ "8862": [0, 0.675, 0, 0],
+ "8863": [0, 0.675, 0, 0],
+ "8864": [0, 0.675, 0, 0],
+ "8865": [0, 0.675, 0, 0],
+ "8872": [0, 0.69224, 0, 0],
+ "8873": [0, 0.69224, 0, 0],
+ "8874": [0, 0.69224, 0, 0],
+ "8876": [0, 0.68889, 0, 0],
+ "8877": [0, 0.68889, 0, 0],
+ "8878": [0, 0.68889, 0, 0],
+ "8879": [0, 0.68889, 0, 0],
+ "8882": [0.03517, 0.54986, 0, 0],
+ "8883": [0.03517, 0.54986, 0, 0],
+ "8884": [0.13667, 0.63667, 0, 0],
+ "8885": [0.13667, 0.63667, 0, 0],
+ "8888": [0, 0.54986, 0, 0],
+ "8890": [0.19444, 0.43056, 0, 0],
+ "8891": [0.19444, 0.69224, 0, 0],
+ "8892": [0.19444, 0.69224, 0, 0],
+ "8901": [0, 0.54986, 0, 0],
+ "8903": [0.08167, 0.58167, 0, 0],
+ "8905": [0.08167, 0.58167, 0, 0],
+ "8906": [0.08167, 0.58167, 0, 0],
+ "8907": [0, 0.69224, 0, 0],
+ "8908": [0, 0.69224, 0, 0],
+ "8909": [-0.03598, 0.46402, 0, 0],
+ "8910": [0, 0.54986, 0, 0],
+ "8911": [0, 0.54986, 0, 0],
+ "8912": [0.03517, 0.54986, 0, 0],
+ "8913": [0.03517, 0.54986, 0, 0],
+ "8914": [0, 0.54986, 0, 0],
+ "8915": [0, 0.54986, 0, 0],
+ "8916": [0, 0.69224, 0, 0],
+ "8918": [0.0391, 0.5391, 0, 0],
+ "8919": [0.0391, 0.5391, 0, 0],
+ "8920": [0.03517, 0.54986, 0, 0],
+ "8921": [0.03517, 0.54986, 0, 0],
+ "8922": [0.38569, 0.88569, 0, 0],
+ "8923": [0.38569, 0.88569, 0, 0],
+ "8926": [0.13667, 0.63667, 0, 0],
+ "8927": [0.13667, 0.63667, 0, 0],
+ "8928": [0.30274, 0.79383, 0, 0],
+ "8929": [0.30274, 0.79383, 0, 0],
+ "8934": [0.23222, 0.74111, 0, 0],
+ "8935": [0.23222, 0.74111, 0, 0],
+ "8936": [0.23222, 0.74111, 0, 0],
+ "8937": [0.23222, 0.74111, 0, 0],
+ "8938": [0.20576, 0.70576, 0, 0],
+ "8939": [0.20576, 0.70576, 0, 0],
+ "8940": [0.30274, 0.79383, 0, 0],
+ "8941": [0.30274, 0.79383, 0, 0],
+ "8994": [0.19444, 0.69224, 0, 0],
+ "8995": [0.19444, 0.69224, 0, 0],
+ "9416": [0.15559, 0.69224, 0, 0],
+ "9484": [0, 0.69224, 0, 0],
+ "9488": [0, 0.69224, 0, 0],
+ "9492": [0, 0.37788, 0, 0],
+ "9496": [0, 0.37788, 0, 0],
+ "9585": [0.19444, 0.68889, 0, 0],
+ "9586": [0.19444, 0.74111, 0, 0],
+ "9632": [0, 0.675, 0, 0],
+ "9633": [0, 0.675, 0, 0],
+ "9650": [0, 0.54986, 0, 0],
+ "9651": [0, 0.54986, 0, 0],
+ "9654": [0.03517, 0.54986, 0, 0],
+ "9660": [0, 0.54986, 0, 0],
+ "9661": [0, 0.54986, 0, 0],
+ "9664": [0.03517, 0.54986, 0, 0],
+ "9674": [0.11111, 0.69224, 0, 0],
+ "9733": [0.19444, 0.69224, 0, 0],
+ "10003": [0, 0.69224, 0, 0],
+ "10016": [0, 0.69224, 0, 0],
+ "10731": [0.11111, 0.69224, 0, 0],
+ "10846": [0.19444, 0.75583, 0, 0],
+ "10877": [0.13667, 0.63667, 0, 0],
+ "10878": [0.13667, 0.63667, 0, 0],
+ "10885": [0.25583, 0.75583, 0, 0],
+ "10886": [0.25583, 0.75583, 0, 0],
+ "10887": [0.13597, 0.63597, 0, 0],
+ "10888": [0.13597, 0.63597, 0, 0],
+ "10889": [0.26167, 0.75726, 0, 0],
+ "10890": [0.26167, 0.75726, 0, 0],
+ "10891": [0.48256, 0.98256, 0, 0],
+ "10892": [0.48256, 0.98256, 0, 0],
+ "10901": [0.13667, 0.63667, 0, 0],
+ "10902": [0.13667, 0.63667, 0, 0],
+ "10933": [0.25142, 0.75726, 0, 0],
+ "10934": [0.25142, 0.75726, 0, 0],
+ "10935": [0.26167, 0.75726, 0, 0],
+ "10936": [0.26167, 0.75726, 0, 0],
+ "10937": [0.26167, 0.75726, 0, 0],
+ "10938": [0.26167, 0.75726, 0, 0],
+ "10949": [0.25583, 0.75583, 0, 0],
+ "10950": [0.25583, 0.75583, 0, 0],
+ "10955": [0.28481, 0.79383, 0, 0],
+ "10956": [0.28481, 0.79383, 0, 0],
+ "57350": [0.08167, 0.58167, 0, 0],
+ "57351": [0.08167, 0.58167, 0, 0],
+ "57352": [0.08167, 0.58167, 0, 0],
+ "57353": [0, 0.43056, 0.04028, 0],
+ "57356": [0.25142, 0.75726, 0, 0],
+ "57357": [0.25142, 0.75726, 0, 0],
+ "57358": [0.41951, 0.91951, 0, 0],
+ "57359": [0.30274, 0.79383, 0, 0],
+ "57360": [0.30274, 0.79383, 0, 0],
+ "57361": [0.41951, 0.91951, 0, 0],
+ "57366": [0.25142, 0.75726, 0, 0],
+ "57367": [0.25142, 0.75726, 0, 0],
+ "57368": [0.25142, 0.75726, 0, 0],
+ "57369": [0.25142, 0.75726, 0, 0],
+ "57370": [0.13597, 0.63597, 0, 0],
+ "57371": [0.13597, 0.63597, 0, 0],
+ },
+ "Caligraphic-Regular": {
+ "48": [0, 0.43056, 0, 0],
+ "49": [0, 0.43056, 0, 0],
+ "50": [0, 0.43056, 0, 0],
+ "51": [0.19444, 0.43056, 0, 0],
+ "52": [0.19444, 0.43056, 0, 0],
+ "53": [0.19444, 0.43056, 0, 0],
+ "54": [0, 0.64444, 0, 0],
+ "55": [0.19444, 0.43056, 0, 0],
+ "56": [0, 0.64444, 0, 0],
+ "57": [0.19444, 0.43056, 0, 0],
+ "65": [0, 0.68333, 0, 0.19445],
+ "66": [0, 0.68333, 0.03041, 0.13889],
+ "67": [0, 0.68333, 0.05834, 0.13889],
+ "68": [0, 0.68333, 0.02778, 0.08334],
+ "69": [0, 0.68333, 0.08944, 0.11111],
+ "70": [0, 0.68333, 0.09931, 0.11111],
+ "71": [0.09722, 0.68333, 0.0593, 0.11111],
+ "72": [0, 0.68333, 0.00965, 0.11111],
+ "73": [0, 0.68333, 0.07382, 0],
+ "74": [0.09722, 0.68333, 0.18472, 0.16667],
+ "75": [0, 0.68333, 0.01445, 0.05556],
+ "76": [0, 0.68333, 0, 0.13889],
+ "77": [0, 0.68333, 0, 0.13889],
+ "78": [0, 0.68333, 0.14736, 0.08334],
+ "79": [0, 0.68333, 0.02778, 0.11111],
+ "80": [0, 0.68333, 0.08222, 0.08334],
+ "81": [0.09722, 0.68333, 0, 0.11111],
+ "82": [0, 0.68333, 0, 0.08334],
+ "83": [0, 0.68333, 0.075, 0.13889],
+ "84": [0, 0.68333, 0.25417, 0],
+ "85": [0, 0.68333, 0.09931, 0.08334],
+ "86": [0, 0.68333, 0.08222, 0],
+ "87": [0, 0.68333, 0.08222, 0.08334],
+ "88": [0, 0.68333, 0.14643, 0.13889],
+ "89": [0.09722, 0.68333, 0.08222, 0.08334],
+ "90": [0, 0.68333, 0.07944, 0.13889],
+ },
+ "Fraktur-Regular": {
+ "33": [0, 0.69141, 0, 0],
+ "34": [0, 0.69141, 0, 0],
+ "38": [0, 0.69141, 0, 0],
+ "39": [0, 0.69141, 0, 0],
+ "40": [0.24982, 0.74947, 0, 0],
+ "41": [0.24982, 0.74947, 0, 0],
+ "42": [0, 0.62119, 0, 0],
+ "43": [0.08319, 0.58283, 0, 0],
+ "44": [0, 0.10803, 0, 0],
+ "45": [0.08319, 0.58283, 0, 0],
+ "46": [0, 0.10803, 0, 0],
+ "47": [0.24982, 0.74947, 0, 0],
+ "48": [0, 0.47534, 0, 0],
+ "49": [0, 0.47534, 0, 0],
+ "50": [0, 0.47534, 0, 0],
+ "51": [0.18906, 0.47534, 0, 0],
+ "52": [0.18906, 0.47534, 0, 0],
+ "53": [0.18906, 0.47534, 0, 0],
+ "54": [0, 0.69141, 0, 0],
+ "55": [0.18906, 0.47534, 0, 0],
+ "56": [0, 0.69141, 0, 0],
+ "57": [0.18906, 0.47534, 0, 0],
+ "58": [0, 0.47534, 0, 0],
+ "59": [0.12604, 0.47534, 0, 0],
+ "61": [-0.13099, 0.36866, 0, 0],
+ "63": [0, 0.69141, 0, 0],
+ "65": [0, 0.69141, 0, 0],
+ "66": [0, 0.69141, 0, 0],
+ "67": [0, 0.69141, 0, 0],
+ "68": [0, 0.69141, 0, 0],
+ "69": [0, 0.69141, 0, 0],
+ "70": [0.12604, 0.69141, 0, 0],
+ "71": [0, 0.69141, 0, 0],
+ "72": [0.06302, 0.69141, 0, 0],
+ "73": [0, 0.69141, 0, 0],
+ "74": [0.12604, 0.69141, 0, 0],
+ "75": [0, 0.69141, 0, 0],
+ "76": [0, 0.69141, 0, 0],
+ "77": [0, 0.69141, 0, 0],
+ "78": [0, 0.69141, 0, 0],
+ "79": [0, 0.69141, 0, 0],
+ "80": [0.18906, 0.69141, 0, 0],
+ "81": [0.03781, 0.69141, 0, 0],
+ "82": [0, 0.69141, 0, 0],
+ "83": [0, 0.69141, 0, 0],
+ "84": [0, 0.69141, 0, 0],
+ "85": [0, 0.69141, 0, 0],
+ "86": [0, 0.69141, 0, 0],
+ "87": [0, 0.69141, 0, 0],
+ "88": [0, 0.69141, 0, 0],
+ "89": [0.18906, 0.69141, 0, 0],
+ "90": [0.12604, 0.69141, 0, 0],
+ "91": [0.24982, 0.74947, 0, 0],
+ "93": [0.24982, 0.74947, 0, 0],
+ "94": [0, 0.69141, 0, 0],
+ "97": [0, 0.47534, 0, 0],
+ "98": [0, 0.69141, 0, 0],
+ "99": [0, 0.47534, 0, 0],
+ "100": [0, 0.62119, 0, 0],
+ "101": [0, 0.47534, 0, 0],
+ "102": [0.18906, 0.69141, 0, 0],
+ "103": [0.18906, 0.47534, 0, 0],
+ "104": [0.18906, 0.69141, 0, 0],
+ "105": [0, 0.69141, 0, 0],
+ "106": [0, 0.69141, 0, 0],
+ "107": [0, 0.69141, 0, 0],
+ "108": [0, 0.69141, 0, 0],
+ "109": [0, 0.47534, 0, 0],
+ "110": [0, 0.47534, 0, 0],
+ "111": [0, 0.47534, 0, 0],
+ "112": [0.18906, 0.52396, 0, 0],
+ "113": [0.18906, 0.47534, 0, 0],
+ "114": [0, 0.47534, 0, 0],
+ "115": [0, 0.47534, 0, 0],
+ "116": [0, 0.62119, 0, 0],
+ "117": [0, 0.47534, 0, 0],
+ "118": [0, 0.52396, 0, 0],
+ "119": [0, 0.52396, 0, 0],
+ "120": [0.18906, 0.47534, 0, 0],
+ "121": [0.18906, 0.47534, 0, 0],
+ "122": [0.18906, 0.47534, 0, 0],
+ "8216": [0, 0.69141, 0, 0],
+ "8217": [0, 0.69141, 0, 0],
+ "58112": [0, 0.62119, 0, 0],
+ "58113": [0, 0.62119, 0, 0],
+ "58114": [0.18906, 0.69141, 0, 0],
+ "58115": [0.18906, 0.69141, 0, 0],
+ "58116": [0.18906, 0.47534, 0, 0],
+ "58117": [0, 0.69141, 0, 0],
+ "58118": [0, 0.62119, 0, 0],
+ "58119": [0, 0.47534, 0, 0],
+ },
+ "Main-Bold": {
+ "33": [0, 0.69444, 0, 0],
+ "34": [0, 0.69444, 0, 0],
+ "35": [0.19444, 0.69444, 0, 0],
+ "36": [0.05556, 0.75, 0, 0],
+ "37": [0.05556, 0.75, 0, 0],
+ "38": [0, 0.69444, 0, 0],
+ "39": [0, 0.69444, 0, 0],
+ "40": [0.25, 0.75, 0, 0],
+ "41": [0.25, 0.75, 0, 0],
+ "42": [0, 0.75, 0, 0],
+ "43": [0.13333, 0.63333, 0, 0],
+ "44": [0.19444, 0.15556, 0, 0],
+ "45": [0, 0.44444, 0, 0],
+ "46": [0, 0.15556, 0, 0],
+ "47": [0.25, 0.75, 0, 0],
+ "48": [0, 0.64444, 0, 0],
+ "49": [0, 0.64444, 0, 0],
+ "50": [0, 0.64444, 0, 0],
+ "51": [0, 0.64444, 0, 0],
+ "52": [0, 0.64444, 0, 0],
+ "53": [0, 0.64444, 0, 0],
+ "54": [0, 0.64444, 0, 0],
+ "55": [0, 0.64444, 0, 0],
+ "56": [0, 0.64444, 0, 0],
+ "57": [0, 0.64444, 0, 0],
+ "58": [0, 0.44444, 0, 0],
+ "59": [0.19444, 0.44444, 0, 0],
+ "60": [0.08556, 0.58556, 0, 0],
+ "61": [-0.10889, 0.39111, 0, 0],
+ "62": [0.08556, 0.58556, 0, 0],
+ "63": [0, 0.69444, 0, 0],
+ "64": [0, 0.69444, 0, 0],
+ "65": [0, 0.68611, 0, 0],
+ "66": [0, 0.68611, 0, 0],
+ "67": [0, 0.68611, 0, 0],
+ "68": [0, 0.68611, 0, 0],
+ "69": [0, 0.68611, 0, 0],
+ "70": [0, 0.68611, 0, 0],
+ "71": [0, 0.68611, 0, 0],
+ "72": [0, 0.68611, 0, 0],
+ "73": [0, 0.68611, 0, 0],
+ "74": [0, 0.68611, 0, 0],
+ "75": [0, 0.68611, 0, 0],
+ "76": [0, 0.68611, 0, 0],
+ "77": [0, 0.68611, 0, 0],
+ "78": [0, 0.68611, 0, 0],
+ "79": [0, 0.68611, 0, 0],
+ "80": [0, 0.68611, 0, 0],
+ "81": [0.19444, 0.68611, 0, 0],
+ "82": [0, 0.68611, 0, 0],
+ "83": [0, 0.68611, 0, 0],
+ "84": [0, 0.68611, 0, 0],
+ "85": [0, 0.68611, 0, 0],
+ "86": [0, 0.68611, 0.01597, 0],
+ "87": [0, 0.68611, 0.01597, 0],
+ "88": [0, 0.68611, 0, 0],
+ "89": [0, 0.68611, 0.02875, 0],
+ "90": [0, 0.68611, 0, 0],
+ "91": [0.25, 0.75, 0, 0],
+ "92": [0.25, 0.75, 0, 0],
+ "93": [0.25, 0.75, 0, 0],
+ "94": [0, 0.69444, 0, 0],
+ "95": [0.31, 0.13444, 0.03194, 0],
+ "96": [0, 0.69444, 0, 0],
+ "97": [0, 0.44444, 0, 0],
+ "98": [0, 0.69444, 0, 0],
+ "99": [0, 0.44444, 0, 0],
+ "100": [0, 0.69444, 0, 0],
+ "101": [0, 0.44444, 0, 0],
+ "102": [0, 0.69444, 0.10903, 0],
+ "103": [0.19444, 0.44444, 0.01597, 0],
+ "104": [0, 0.69444, 0, 0],
+ "105": [0, 0.69444, 0, 0],
+ "106": [0.19444, 0.69444, 0, 0],
+ "107": [0, 0.69444, 0, 0],
+ "108": [0, 0.69444, 0, 0],
+ "109": [0, 0.44444, 0, 0],
+ "110": [0, 0.44444, 0, 0],
+ "111": [0, 0.44444, 0, 0],
+ "112": [0.19444, 0.44444, 0, 0],
+ "113": [0.19444, 0.44444, 0, 0],
+ "114": [0, 0.44444, 0, 0],
+ "115": [0, 0.44444, 0, 0],
+ "116": [0, 0.63492, 0, 0],
+ "117": [0, 0.44444, 0, 0],
+ "118": [0, 0.44444, 0.01597, 0],
+ "119": [0, 0.44444, 0.01597, 0],
+ "120": [0, 0.44444, 0, 0],
+ "121": [0.19444, 0.44444, 0.01597, 0],
+ "122": [0, 0.44444, 0, 0],
+ "123": [0.25, 0.75, 0, 0],
+ "124": [0.25, 0.75, 0, 0],
+ "125": [0.25, 0.75, 0, 0],
+ "126": [0.35, 0.34444, 0, 0],
+ "168": [0, 0.69444, 0, 0],
+ "172": [0, 0.44444, 0, 0],
+ "175": [0, 0.59611, 0, 0],
+ "176": [0, 0.69444, 0, 0],
+ "177": [0.13333, 0.63333, 0, 0],
+ "180": [0, 0.69444, 0, 0],
+ "215": [0.13333, 0.63333, 0, 0],
+ "247": [0.13333, 0.63333, 0, 0],
+ "305": [0, 0.44444, 0, 0],
+ "567": [0.19444, 0.44444, 0, 0],
+ "710": [0, 0.69444, 0, 0],
+ "711": [0, 0.63194, 0, 0],
+ "713": [0, 0.59611, 0, 0],
+ "714": [0, 0.69444, 0, 0],
+ "715": [0, 0.69444, 0, 0],
+ "728": [0, 0.69444, 0, 0],
+ "729": [0, 0.69444, 0, 0],
+ "730": [0, 0.69444, 0, 0],
+ "732": [0, 0.69444, 0, 0],
+ "768": [0, 0.69444, 0, 0],
+ "769": [0, 0.69444, 0, 0],
+ "770": [0, 0.69444, 0, 0],
+ "771": [0, 0.69444, 0, 0],
+ "772": [0, 0.59611, 0, 0],
+ "774": [0, 0.69444, 0, 0],
+ "775": [0, 0.69444, 0, 0],
+ "776": [0, 0.69444, 0, 0],
+ "778": [0, 0.69444, 0, 0],
+ "779": [0, 0.69444, 0, 0],
+ "780": [0, 0.63194, 0, 0],
+ "824": [0.19444, 0.69444, 0, 0],
+ "915": [0, 0.68611, 0, 0],
+ "916": [0, 0.68611, 0, 0],
+ "920": [0, 0.68611, 0, 0],
+ "923": [0, 0.68611, 0, 0],
+ "926": [0, 0.68611, 0, 0],
+ "928": [0, 0.68611, 0, 0],
+ "931": [0, 0.68611, 0, 0],
+ "933": [0, 0.68611, 0, 0],
+ "934": [0, 0.68611, 0, 0],
+ "936": [0, 0.68611, 0, 0],
+ "937": [0, 0.68611, 0, 0],
+ "8211": [0, 0.44444, 0.03194, 0],
+ "8212": [0, 0.44444, 0.03194, 0],
+ "8216": [0, 0.69444, 0, 0],
+ "8217": [0, 0.69444, 0, 0],
+ "8220": [0, 0.69444, 0, 0],
+ "8221": [0, 0.69444, 0, 0],
+ "8224": [0.19444, 0.69444, 0, 0],
+ "8225": [0.19444, 0.69444, 0, 0],
+ "8242": [0, 0.55556, 0, 0],
+ "8407": [0, 0.72444, 0.15486, 0],
+ "8463": [0, 0.69444, 0, 0],
+ "8465": [0, 0.69444, 0, 0],
+ "8467": [0, 0.69444, 0, 0],
+ "8472": [0.19444, 0.44444, 0, 0],
+ "8476": [0, 0.69444, 0, 0],
+ "8501": [0, 0.69444, 0, 0],
+ "8592": [-0.10889, 0.39111, 0, 0],
+ "8593": [0.19444, 0.69444, 0, 0],
+ "8594": [-0.10889, 0.39111, 0, 0],
+ "8595": [0.19444, 0.69444, 0, 0],
+ "8596": [-0.10889, 0.39111, 0, 0],
+ "8597": [0.25, 0.75, 0, 0],
+ "8598": [0.19444, 0.69444, 0, 0],
+ "8599": [0.19444, 0.69444, 0, 0],
+ "8600": [0.19444, 0.69444, 0, 0],
+ "8601": [0.19444, 0.69444, 0, 0],
+ "8636": [-0.10889, 0.39111, 0, 0],
+ "8637": [-0.10889, 0.39111, 0, 0],
+ "8640": [-0.10889, 0.39111, 0, 0],
+ "8641": [-0.10889, 0.39111, 0, 0],
+ "8656": [-0.10889, 0.39111, 0, 0],
+ "8657": [0.19444, 0.69444, 0, 0],
+ "8658": [-0.10889, 0.39111, 0, 0],
+ "8659": [0.19444, 0.69444, 0, 0],
+ "8660": [-0.10889, 0.39111, 0, 0],
+ "8661": [0.25, 0.75, 0, 0],
+ "8704": [0, 0.69444, 0, 0],
+ "8706": [0, 0.69444, 0.06389, 0],
+ "8707": [0, 0.69444, 0, 0],
+ "8709": [0.05556, 0.75, 0, 0],
+ "8711": [0, 0.68611, 0, 0],
+ "8712": [0.08556, 0.58556, 0, 0],
+ "8715": [0.08556, 0.58556, 0, 0],
+ "8722": [0.13333, 0.63333, 0, 0],
+ "8723": [0.13333, 0.63333, 0, 0],
+ "8725": [0.25, 0.75, 0, 0],
+ "8726": [0.25, 0.75, 0, 0],
+ "8727": [-0.02778, 0.47222, 0, 0],
+ "8728": [-0.02639, 0.47361, 0, 0],
+ "8729": [-0.02639, 0.47361, 0, 0],
+ "8730": [0.18, 0.82, 0, 0],
+ "8733": [0, 0.44444, 0, 0],
+ "8734": [0, 0.44444, 0, 0],
+ "8736": [0, 0.69224, 0, 0],
+ "8739": [0.25, 0.75, 0, 0],
+ "8741": [0.25, 0.75, 0, 0],
+ "8743": [0, 0.55556, 0, 0],
+ "8744": [0, 0.55556, 0, 0],
+ "8745": [0, 0.55556, 0, 0],
+ "8746": [0, 0.55556, 0, 0],
+ "8747": [0.19444, 0.69444, 0.12778, 0],
+ "8764": [-0.10889, 0.39111, 0, 0],
+ "8768": [0.19444, 0.69444, 0, 0],
+ "8771": [0.00222, 0.50222, 0, 0],
+ "8776": [0.02444, 0.52444, 0, 0],
+ "8781": [0.00222, 0.50222, 0, 0],
+ "8801": [0.00222, 0.50222, 0, 0],
+ "8804": [0.19667, 0.69667, 0, 0],
+ "8805": [0.19667, 0.69667, 0, 0],
+ "8810": [0.08556, 0.58556, 0, 0],
+ "8811": [0.08556, 0.58556, 0, 0],
+ "8826": [0.08556, 0.58556, 0, 0],
+ "8827": [0.08556, 0.58556, 0, 0],
+ "8834": [0.08556, 0.58556, 0, 0],
+ "8835": [0.08556, 0.58556, 0, 0],
+ "8838": [0.19667, 0.69667, 0, 0],
+ "8839": [0.19667, 0.69667, 0, 0],
+ "8846": [0, 0.55556, 0, 0],
+ "8849": [0.19667, 0.69667, 0, 0],
+ "8850": [0.19667, 0.69667, 0, 0],
+ "8851": [0, 0.55556, 0, 0],
+ "8852": [0, 0.55556, 0, 0],
+ "8853": [0.13333, 0.63333, 0, 0],
+ "8854": [0.13333, 0.63333, 0, 0],
+ "8855": [0.13333, 0.63333, 0, 0],
+ "8856": [0.13333, 0.63333, 0, 0],
+ "8857": [0.13333, 0.63333, 0, 0],
+ "8866": [0, 0.69444, 0, 0],
+ "8867": [0, 0.69444, 0, 0],
+ "8868": [0, 0.69444, 0, 0],
+ "8869": [0, 0.69444, 0, 0],
+ "8900": [-0.02639, 0.47361, 0, 0],
+ "8901": [-0.02639, 0.47361, 0, 0],
+ "8902": [-0.02778, 0.47222, 0, 0],
+ "8968": [0.25, 0.75, 0, 0],
+ "8969": [0.25, 0.75, 0, 0],
+ "8970": [0.25, 0.75, 0, 0],
+ "8971": [0.25, 0.75, 0, 0],
+ "8994": [-0.13889, 0.36111, 0, 0],
+ "8995": [-0.13889, 0.36111, 0, 0],
+ "9651": [0.19444, 0.69444, 0, 0],
+ "9657": [-0.02778, 0.47222, 0, 0],
+ "9661": [0.19444, 0.69444, 0, 0],
+ "9667": [-0.02778, 0.47222, 0, 0],
+ "9711": [0.19444, 0.69444, 0, 0],
+ "9824": [0.12963, 0.69444, 0, 0],
+ "9825": [0.12963, 0.69444, 0, 0],
+ "9826": [0.12963, 0.69444, 0, 0],
+ "9827": [0.12963, 0.69444, 0, 0],
+ "9837": [0, 0.75, 0, 0],
+ "9838": [0.19444, 0.69444, 0, 0],
+ "9839": [0.19444, 0.69444, 0, 0],
+ "10216": [0.25, 0.75, 0, 0],
+ "10217": [0.25, 0.75, 0, 0],
+ "10815": [0, 0.68611, 0, 0],
+ "10927": [0.19667, 0.69667, 0, 0],
+ "10928": [0.19667, 0.69667, 0, 0],
+ },
+ "Main-Italic": {
+ "33": [0, 0.69444, 0.12417, 0],
+ "34": [0, 0.69444, 0.06961, 0],
+ "35": [0.19444, 0.69444, 0.06616, 0],
+ "37": [0.05556, 0.75, 0.13639, 0],
+ "38": [0, 0.69444, 0.09694, 0],
+ "39": [0, 0.69444, 0.12417, 0],
+ "40": [0.25, 0.75, 0.16194, 0],
+ "41": [0.25, 0.75, 0.03694, 0],
+ "42": [0, 0.75, 0.14917, 0],
+ "43": [0.05667, 0.56167, 0.03694, 0],
+ "44": [0.19444, 0.10556, 0, 0],
+ "45": [0, 0.43056, 0.02826, 0],
+ "46": [0, 0.10556, 0, 0],
+ "47": [0.25, 0.75, 0.16194, 0],
+ "48": [0, 0.64444, 0.13556, 0],
+ "49": [0, 0.64444, 0.13556, 0],
+ "50": [0, 0.64444, 0.13556, 0],
+ "51": [0, 0.64444, 0.13556, 0],
+ "52": [0.19444, 0.64444, 0.13556, 0],
+ "53": [0, 0.64444, 0.13556, 0],
+ "54": [0, 0.64444, 0.13556, 0],
+ "55": [0.19444, 0.64444, 0.13556, 0],
+ "56": [0, 0.64444, 0.13556, 0],
+ "57": [0, 0.64444, 0.13556, 0],
+ "58": [0, 0.43056, 0.0582, 0],
+ "59": [0.19444, 0.43056, 0.0582, 0],
+ "61": [-0.13313, 0.36687, 0.06616, 0],
+ "63": [0, 0.69444, 0.1225, 0],
+ "64": [0, 0.69444, 0.09597, 0],
+ "65": [0, 0.68333, 0, 0],
+ "66": [0, 0.68333, 0.10257, 0],
+ "67": [0, 0.68333, 0.14528, 0],
+ "68": [0, 0.68333, 0.09403, 0],
+ "69": [0, 0.68333, 0.12028, 0],
+ "70": [0, 0.68333, 0.13305, 0],
+ "71": [0, 0.68333, 0.08722, 0],
+ "72": [0, 0.68333, 0.16389, 0],
+ "73": [0, 0.68333, 0.15806, 0],
+ "74": [0, 0.68333, 0.14028, 0],
+ "75": [0, 0.68333, 0.14528, 0],
+ "76": [0, 0.68333, 0, 0],
+ "77": [0, 0.68333, 0.16389, 0],
+ "78": [0, 0.68333, 0.16389, 0],
+ "79": [0, 0.68333, 0.09403, 0],
+ "80": [0, 0.68333, 0.10257, 0],
+ "81": [0.19444, 0.68333, 0.09403, 0],
+ "82": [0, 0.68333, 0.03868, 0],
+ "83": [0, 0.68333, 0.11972, 0],
+ "84": [0, 0.68333, 0.13305, 0],
+ "85": [0, 0.68333, 0.16389, 0],
+ "86": [0, 0.68333, 0.18361, 0],
+ "87": [0, 0.68333, 0.18361, 0],
+ "88": [0, 0.68333, 0.15806, 0],
+ "89": [0, 0.68333, 0.19383, 0],
+ "90": [0, 0.68333, 0.14528, 0],
+ "91": [0.25, 0.75, 0.1875, 0],
+ "93": [0.25, 0.75, 0.10528, 0],
+ "94": [0, 0.69444, 0.06646, 0],
+ "95": [0.31, 0.12056, 0.09208, 0],
+ "97": [0, 0.43056, 0.07671, 0],
+ "98": [0, 0.69444, 0.06312, 0],
+ "99": [0, 0.43056, 0.05653, 0],
+ "100": [0, 0.69444, 0.10333, 0],
+ "101": [0, 0.43056, 0.07514, 0],
+ "102": [0.19444, 0.69444, 0.21194, 0],
+ "103": [0.19444, 0.43056, 0.08847, 0],
+ "104": [0, 0.69444, 0.07671, 0],
+ "105": [0, 0.65536, 0.1019, 0],
+ "106": [0.19444, 0.65536, 0.14467, 0],
+ "107": [0, 0.69444, 0.10764, 0],
+ "108": [0, 0.69444, 0.10333, 0],
+ "109": [0, 0.43056, 0.07671, 0],
+ "110": [0, 0.43056, 0.07671, 0],
+ "111": [0, 0.43056, 0.06312, 0],
+ "112": [0.19444, 0.43056, 0.06312, 0],
+ "113": [0.19444, 0.43056, 0.08847, 0],
+ "114": [0, 0.43056, 0.10764, 0],
+ "115": [0, 0.43056, 0.08208, 0],
+ "116": [0, 0.61508, 0.09486, 0],
+ "117": [0, 0.43056, 0.07671, 0],
+ "118": [0, 0.43056, 0.10764, 0],
+ "119": [0, 0.43056, 0.10764, 0],
+ "120": [0, 0.43056, 0.12042, 0],
+ "121": [0.19444, 0.43056, 0.08847, 0],
+ "122": [0, 0.43056, 0.12292, 0],
+ "126": [0.35, 0.31786, 0.11585, 0],
+ "163": [0, 0.69444, 0, 0],
+ "305": [0, 0.43056, 0, 0.02778],
+ "567": [0.19444, 0.43056, 0, 0.08334],
+ "768": [0, 0.69444, 0, 0],
+ "769": [0, 0.69444, 0.09694, 0],
+ "770": [0, 0.69444, 0.06646, 0],
+ "771": [0, 0.66786, 0.11585, 0],
+ "772": [0, 0.56167, 0.10333, 0],
+ "774": [0, 0.69444, 0.10806, 0],
+ "775": [0, 0.66786, 0.11752, 0],
+ "776": [0, 0.66786, 0.10474, 0],
+ "778": [0, 0.69444, 0, 0],
+ "779": [0, 0.69444, 0.1225, 0],
+ "780": [0, 0.62847, 0.08295, 0],
+ "915": [0, 0.68333, 0.13305, 0],
+ "916": [0, 0.68333, 0, 0],
+ "920": [0, 0.68333, 0.09403, 0],
+ "923": [0, 0.68333, 0, 0],
+ "926": [0, 0.68333, 0.15294, 0],
+ "928": [0, 0.68333, 0.16389, 0],
+ "931": [0, 0.68333, 0.12028, 0],
+ "933": [0, 0.68333, 0.11111, 0],
+ "934": [0, 0.68333, 0.05986, 0],
+ "936": [0, 0.68333, 0.11111, 0],
+ "937": [0, 0.68333, 0.10257, 0],
+ "8211": [0, 0.43056, 0.09208, 0],
+ "8212": [0, 0.43056, 0.09208, 0],
+ "8216": [0, 0.69444, 0.12417, 0],
+ "8217": [0, 0.69444, 0.12417, 0],
+ "8220": [0, 0.69444, 0.1685, 0],
+ "8221": [0, 0.69444, 0.06961, 0],
+ "8463": [0, 0.68889, 0, 0],
+ },
+ "Main-Regular": {
+ "32": [0, 0, 0, 0],
+ "33": [0, 0.69444, 0, 0],
+ "34": [0, 0.69444, 0, 0],
+ "35": [0.19444, 0.69444, 0, 0],
+ "36": [0.05556, 0.75, 0, 0],
+ "37": [0.05556, 0.75, 0, 0],
+ "38": [0, 0.69444, 0, 0],
+ "39": [0, 0.69444, 0, 0],
+ "40": [0.25, 0.75, 0, 0],
+ "41": [0.25, 0.75, 0, 0],
+ "42": [0, 0.75, 0, 0],
+ "43": [0.08333, 0.58333, 0, 0],
+ "44": [0.19444, 0.10556, 0, 0],
+ "45": [0, 0.43056, 0, 0],
+ "46": [0, 0.10556, 0, 0],
+ "47": [0.25, 0.75, 0, 0],
+ "48": [0, 0.64444, 0, 0],
+ "49": [0, 0.64444, 0, 0],
+ "50": [0, 0.64444, 0, 0],
+ "51": [0, 0.64444, 0, 0],
+ "52": [0, 0.64444, 0, 0],
+ "53": [0, 0.64444, 0, 0],
+ "54": [0, 0.64444, 0, 0],
+ "55": [0, 0.64444, 0, 0],
+ "56": [0, 0.64444, 0, 0],
+ "57": [0, 0.64444, 0, 0],
+ "58": [0, 0.43056, 0, 0],
+ "59": [0.19444, 0.43056, 0, 0],
+ "60": [0.0391, 0.5391, 0, 0],
+ "61": [-0.13313, 0.36687, 0, 0],
+ "62": [0.0391, 0.5391, 0, 0],
+ "63": [0, 0.69444, 0, 0],
+ "64": [0, 0.69444, 0, 0],
+ "65": [0, 0.68333, 0, 0],
+ "66": [0, 0.68333, 0, 0],
+ "67": [0, 0.68333, 0, 0],
+ "68": [0, 0.68333, 0, 0],
+ "69": [0, 0.68333, 0, 0],
+ "70": [0, 0.68333, 0, 0],
+ "71": [0, 0.68333, 0, 0],
+ "72": [0, 0.68333, 0, 0],
+ "73": [0, 0.68333, 0, 0],
+ "74": [0, 0.68333, 0, 0],
+ "75": [0, 0.68333, 0, 0],
+ "76": [0, 0.68333, 0, 0],
+ "77": [0, 0.68333, 0, 0],
+ "78": [0, 0.68333, 0, 0],
+ "79": [0, 0.68333, 0, 0],
+ "80": [0, 0.68333, 0, 0],
+ "81": [0.19444, 0.68333, 0, 0],
+ "82": [0, 0.68333, 0, 0],
+ "83": [0, 0.68333, 0, 0],
+ "84": [0, 0.68333, 0, 0],
+ "85": [0, 0.68333, 0, 0],
+ "86": [0, 0.68333, 0.01389, 0],
+ "87": [0, 0.68333, 0.01389, 0],
+ "88": [0, 0.68333, 0, 0],
+ "89": [0, 0.68333, 0.025, 0],
+ "90": [0, 0.68333, 0, 0],
+ "91": [0.25, 0.75, 0, 0],
+ "92": [0.25, 0.75, 0, 0],
+ "93": [0.25, 0.75, 0, 0],
+ "94": [0, 0.69444, 0, 0],
+ "95": [0.31, 0.12056, 0.02778, 0],
+ "96": [0, 0.69444, 0, 0],
+ "97": [0, 0.43056, 0, 0],
+ "98": [0, 0.69444, 0, 0],
+ "99": [0, 0.43056, 0, 0],
+ "100": [0, 0.69444, 0, 0],
+ "101": [0, 0.43056, 0, 0],
+ "102": [0, 0.69444, 0.07778, 0],
+ "103": [0.19444, 0.43056, 0.01389, 0],
+ "104": [0, 0.69444, 0, 0],
+ "105": [0, 0.66786, 0, 0],
+ "106": [0.19444, 0.66786, 0, 0],
+ "107": [0, 0.69444, 0, 0],
+ "108": [0, 0.69444, 0, 0],
+ "109": [0, 0.43056, 0, 0],
+ "110": [0, 0.43056, 0, 0],
+ "111": [0, 0.43056, 0, 0],
+ "112": [0.19444, 0.43056, 0, 0],
+ "113": [0.19444, 0.43056, 0, 0],
+ "114": [0, 0.43056, 0, 0],
+ "115": [0, 0.43056, 0, 0],
+ "116": [0, 0.61508, 0, 0],
+ "117": [0, 0.43056, 0, 0],
+ "118": [0, 0.43056, 0.01389, 0],
+ "119": [0, 0.43056, 0.01389, 0],
+ "120": [0, 0.43056, 0, 0],
+ "121": [0.19444, 0.43056, 0.01389, 0],
+ "122": [0, 0.43056, 0, 0],
+ "123": [0.25, 0.75, 0, 0],
+ "124": [0.25, 0.75, 0, 0],
+ "125": [0.25, 0.75, 0, 0],
+ "126": [0.35, 0.31786, 0, 0],
+ "160": [0, 0, 0, 0],
+ "168": [0, 0.66786, 0, 0],
+ "172": [0, 0.43056, 0, 0],
+ "175": [0, 0.56778, 0, 0],
+ "176": [0, 0.69444, 0, 0],
+ "177": [0.08333, 0.58333, 0, 0],
+ "180": [0, 0.69444, 0, 0],
+ "215": [0.08333, 0.58333, 0, 0],
+ "247": [0.08333, 0.58333, 0, 0],
+ "305": [0, 0.43056, 0, 0],
+ "567": [0.19444, 0.43056, 0, 0],
+ "710": [0, 0.69444, 0, 0],
+ "711": [0, 0.62847, 0, 0],
+ "713": [0, 0.56778, 0, 0],
+ "714": [0, 0.69444, 0, 0],
+ "715": [0, 0.69444, 0, 0],
+ "728": [0, 0.69444, 0, 0],
+ "729": [0, 0.66786, 0, 0],
+ "730": [0, 0.69444, 0, 0],
+ "732": [0, 0.66786, 0, 0],
+ "768": [0, 0.69444, 0, 0],
+ "769": [0, 0.69444, 0, 0],
+ "770": [0, 0.69444, 0, 0],
+ "771": [0, 0.66786, 0, 0],
+ "772": [0, 0.56778, 0, 0],
+ "774": [0, 0.69444, 0, 0],
+ "775": [0, 0.66786, 0, 0],
+ "776": [0, 0.66786, 0, 0],
+ "778": [0, 0.69444, 0, 0],
+ "779": [0, 0.69444, 0, 0],
+ "780": [0, 0.62847, 0, 0],
+ "824": [0.19444, 0.69444, 0, 0],
+ "915": [0, 0.68333, 0, 0],
+ "916": [0, 0.68333, 0, 0],
+ "920": [0, 0.68333, 0, 0],
+ "923": [0, 0.68333, 0, 0],
+ "926": [0, 0.68333, 0, 0],
+ "928": [0, 0.68333, 0, 0],
+ "931": [0, 0.68333, 0, 0],
+ "933": [0, 0.68333, 0, 0],
+ "934": [0, 0.68333, 0, 0],
+ "936": [0, 0.68333, 0, 0],
+ "937": [0, 0.68333, 0, 0],
+ "8211": [0, 0.43056, 0.02778, 0],
+ "8212": [0, 0.43056, 0.02778, 0],
+ "8216": [0, 0.69444, 0, 0],
+ "8217": [0, 0.69444, 0, 0],
+ "8220": [0, 0.69444, 0, 0],
+ "8221": [0, 0.69444, 0, 0],
+ "8224": [0.19444, 0.69444, 0, 0],
+ "8225": [0.19444, 0.69444, 0, 0],
+ "8230": [0, 0.12, 0, 0],
+ "8242": [0, 0.55556, 0, 0],
+ "8407": [0, 0.71444, 0.15382, 0],
+ "8463": [0, 0.68889, 0, 0],
+ "8465": [0, 0.69444, 0, 0],
+ "8467": [0, 0.69444, 0, 0.11111],
+ "8472": [0.19444, 0.43056, 0, 0.11111],
+ "8476": [0, 0.69444, 0, 0],
+ "8501": [0, 0.69444, 0, 0],
+ "8592": [-0.13313, 0.36687, 0, 0],
+ "8593": [0.19444, 0.69444, 0, 0],
+ "8594": [-0.13313, 0.36687, 0, 0],
+ "8595": [0.19444, 0.69444, 0, 0],
+ "8596": [-0.13313, 0.36687, 0, 0],
+ "8597": [0.25, 0.75, 0, 0],
+ "8598": [0.19444, 0.69444, 0, 0],
+ "8599": [0.19444, 0.69444, 0, 0],
+ "8600": [0.19444, 0.69444, 0, 0],
+ "8601": [0.19444, 0.69444, 0, 0],
+ "8614": [0.011, 0.511, 0, 0],
+ "8617": [0.011, 0.511, 0, 0],
+ "8618": [0.011, 0.511, 0, 0],
+ "8636": [-0.13313, 0.36687, 0, 0],
+ "8637": [-0.13313, 0.36687, 0, 0],
+ "8640": [-0.13313, 0.36687, 0, 0],
+ "8641": [-0.13313, 0.36687, 0, 0],
+ "8652": [0.011, 0.671, 0, 0],
+ "8656": [-0.13313, 0.36687, 0, 0],
+ "8657": [0.19444, 0.69444, 0, 0],
+ "8658": [-0.13313, 0.36687, 0, 0],
+ "8659": [0.19444, 0.69444, 0, 0],
+ "8660": [-0.13313, 0.36687, 0, 0],
+ "8661": [0.25, 0.75, 0, 0],
+ "8704": [0, 0.69444, 0, 0],
+ "8706": [0, 0.69444, 0.05556, 0.08334],
+ "8707": [0, 0.69444, 0, 0],
+ "8709": [0.05556, 0.75, 0, 0],
+ "8711": [0, 0.68333, 0, 0],
+ "8712": [0.0391, 0.5391, 0, 0],
+ "8715": [0.0391, 0.5391, 0, 0],
+ "8722": [0.08333, 0.58333, 0, 0],
+ "8723": [0.08333, 0.58333, 0, 0],
+ "8725": [0.25, 0.75, 0, 0],
+ "8726": [0.25, 0.75, 0, 0],
+ "8727": [-0.03472, 0.46528, 0, 0],
+ "8728": [-0.05555, 0.44445, 0, 0],
+ "8729": [-0.05555, 0.44445, 0, 0],
+ "8730": [0.2, 0.8, 0, 0],
+ "8733": [0, 0.43056, 0, 0],
+ "8734": [0, 0.43056, 0, 0],
+ "8736": [0, 0.69224, 0, 0],
+ "8739": [0.25, 0.75, 0, 0],
+ "8741": [0.25, 0.75, 0, 0],
+ "8743": [0, 0.55556, 0, 0],
+ "8744": [0, 0.55556, 0, 0],
+ "8745": [0, 0.55556, 0, 0],
+ "8746": [0, 0.55556, 0, 0],
+ "8747": [0.19444, 0.69444, 0.11111, 0],
+ "8764": [-0.13313, 0.36687, 0, 0],
+ "8768": [0.19444, 0.69444, 0, 0],
+ "8771": [-0.03625, 0.46375, 0, 0],
+ "8773": [-0.022, 0.589, 0, 0],
+ "8776": [-0.01688, 0.48312, 0, 0],
+ "8781": [-0.03625, 0.46375, 0, 0],
+ "8784": [-0.133, 0.67, 0, 0],
+ "8800": [0.215, 0.716, 0, 0],
+ "8801": [-0.03625, 0.46375, 0, 0],
+ "8804": [0.13597, 0.63597, 0, 0],
+ "8805": [0.13597, 0.63597, 0, 0],
+ "8810": [0.0391, 0.5391, 0, 0],
+ "8811": [0.0391, 0.5391, 0, 0],
+ "8826": [0.0391, 0.5391, 0, 0],
+ "8827": [0.0391, 0.5391, 0, 0],
+ "8834": [0.0391, 0.5391, 0, 0],
+ "8835": [0.0391, 0.5391, 0, 0],
+ "8838": [0.13597, 0.63597, 0, 0],
+ "8839": [0.13597, 0.63597, 0, 0],
+ "8846": [0, 0.55556, 0, 0],
+ "8849": [0.13597, 0.63597, 0, 0],
+ "8850": [0.13597, 0.63597, 0, 0],
+ "8851": [0, 0.55556, 0, 0],
+ "8852": [0, 0.55556, 0, 0],
+ "8853": [0.08333, 0.58333, 0, 0],
+ "8854": [0.08333, 0.58333, 0, 0],
+ "8855": [0.08333, 0.58333, 0, 0],
+ "8856": [0.08333, 0.58333, 0, 0],
+ "8857": [0.08333, 0.58333, 0, 0],
+ "8866": [0, 0.69444, 0, 0],
+ "8867": [0, 0.69444, 0, 0],
+ "8868": [0, 0.69444, 0, 0],
+ "8869": [0, 0.69444, 0, 0],
+ "8872": [0.249, 0.75, 0, 0],
+ "8900": [-0.05555, 0.44445, 0, 0],
+ "8901": [-0.05555, 0.44445, 0, 0],
+ "8902": [-0.03472, 0.46528, 0, 0],
+ "8904": [0.005, 0.505, 0, 0],
+ "8942": [0.03, 0.9, 0, 0],
+ "8943": [-0.19, 0.31, 0, 0],
+ "8945": [-0.1, 0.82, 0, 0],
+ "8968": [0.25, 0.75, 0, 0],
+ "8969": [0.25, 0.75, 0, 0],
+ "8970": [0.25, 0.75, 0, 0],
+ "8971": [0.25, 0.75, 0, 0],
+ "8994": [-0.14236, 0.35764, 0, 0],
+ "8995": [-0.14236, 0.35764, 0, 0],
+ "9136": [0.244, 0.744, 0, 0],
+ "9137": [0.244, 0.744, 0, 0],
+ "9651": [0.19444, 0.69444, 0, 0],
+ "9657": [-0.03472, 0.46528, 0, 0],
+ "9661": [0.19444, 0.69444, 0, 0],
+ "9667": [-0.03472, 0.46528, 0, 0],
+ "9711": [0.19444, 0.69444, 0, 0],
+ "9824": [0.12963, 0.69444, 0, 0],
+ "9825": [0.12963, 0.69444, 0, 0],
+ "9826": [0.12963, 0.69444, 0, 0],
+ "9827": [0.12963, 0.69444, 0, 0],
+ "9837": [0, 0.75, 0, 0],
+ "9838": [0.19444, 0.69444, 0, 0],
+ "9839": [0.19444, 0.69444, 0, 0],
+ "10216": [0.25, 0.75, 0, 0],
+ "10217": [0.25, 0.75, 0, 0],
+ "10222": [0.244, 0.744, 0, 0],
+ "10223": [0.244, 0.744, 0, 0],
+ "10229": [0.011, 0.511, 0, 0],
+ "10230": [0.011, 0.511, 0, 0],
+ "10231": [0.011, 0.511, 0, 0],
+ "10232": [0.024, 0.525, 0, 0],
+ "10233": [0.024, 0.525, 0, 0],
+ "10234": [0.024, 0.525, 0, 0],
+ "10236": [0.011, 0.511, 0, 0],
+ "10815": [0, 0.68333, 0, 0],
+ "10927": [0.13597, 0.63597, 0, 0],
+ "10928": [0.13597, 0.63597, 0, 0],
+ },
+ "Math-BoldItalic": {
+ "47": [0.19444, 0.69444, 0, 0],
+ "65": [0, 0.68611, 0, 0],
+ "66": [0, 0.68611, 0.04835, 0],
+ "67": [0, 0.68611, 0.06979, 0],
+ "68": [0, 0.68611, 0.03194, 0],
+ "69": [0, 0.68611, 0.05451, 0],
+ "70": [0, 0.68611, 0.15972, 0],
+ "71": [0, 0.68611, 0, 0],
+ "72": [0, 0.68611, 0.08229, 0],
+ "73": [0, 0.68611, 0.07778, 0],
+ "74": [0, 0.68611, 0.10069, 0],
+ "75": [0, 0.68611, 0.06979, 0],
+ "76": [0, 0.68611, 0, 0],
+ "77": [0, 0.68611, 0.11424, 0],
+ "78": [0, 0.68611, 0.11424, 0],
+ "79": [0, 0.68611, 0.03194, 0],
+ "80": [0, 0.68611, 0.15972, 0],
+ "81": [0.19444, 0.68611, 0, 0],
+ "82": [0, 0.68611, 0.00421, 0],
+ "83": [0, 0.68611, 0.05382, 0],
+ "84": [0, 0.68611, 0.15972, 0],
+ "85": [0, 0.68611, 0.11424, 0],
+ "86": [0, 0.68611, 0.25555, 0],
+ "87": [0, 0.68611, 0.15972, 0],
+ "88": [0, 0.68611, 0.07778, 0],
+ "89": [0, 0.68611, 0.25555, 0],
+ "90": [0, 0.68611, 0.06979, 0],
+ "97": [0, 0.44444, 0, 0],
+ "98": [0, 0.69444, 0, 0],
+ "99": [0, 0.44444, 0, 0],
+ "100": [0, 0.69444, 0, 0],
+ "101": [0, 0.44444, 0, 0],
+ "102": [0.19444, 0.69444, 0.11042, 0],
+ "103": [0.19444, 0.44444, 0.03704, 0],
+ "104": [0, 0.69444, 0, 0],
+ "105": [0, 0.69326, 0, 0],
+ "106": [0.19444, 0.69326, 0.0622, 0],
+ "107": [0, 0.69444, 0.01852, 0],
+ "108": [0, 0.69444, 0.0088, 0],
+ "109": [0, 0.44444, 0, 0],
+ "110": [0, 0.44444, 0, 0],
+ "111": [0, 0.44444, 0, 0],
+ "112": [0.19444, 0.44444, 0, 0],
+ "113": [0.19444, 0.44444, 0.03704, 0],
+ "114": [0, 0.44444, 0.03194, 0],
+ "115": [0, 0.44444, 0, 0],
+ "116": [0, 0.63492, 0, 0],
+ "117": [0, 0.44444, 0, 0],
+ "118": [0, 0.44444, 0.03704, 0],
+ "119": [0, 0.44444, 0.02778, 0],
+ "120": [0, 0.44444, 0, 0],
+ "121": [0.19444, 0.44444, 0.03704, 0],
+ "122": [0, 0.44444, 0.04213, 0],
+ "915": [0, 0.68611, 0.15972, 0],
+ "916": [0, 0.68611, 0, 0],
+ "920": [0, 0.68611, 0.03194, 0],
+ "923": [0, 0.68611, 0, 0],
+ "926": [0, 0.68611, 0.07458, 0],
+ "928": [0, 0.68611, 0.08229, 0],
+ "931": [0, 0.68611, 0.05451, 0],
+ "933": [0, 0.68611, 0.15972, 0],
+ "934": [0, 0.68611, 0, 0],
+ "936": [0, 0.68611, 0.11653, 0],
+ "937": [0, 0.68611, 0.04835, 0],
+ "945": [0, 0.44444, 0, 0],
+ "946": [0.19444, 0.69444, 0.03403, 0],
+ "947": [0.19444, 0.44444, 0.06389, 0],
+ "948": [0, 0.69444, 0.03819, 0],
+ "949": [0, 0.44444, 0, 0],
+ "950": [0.19444, 0.69444, 0.06215, 0],
+ "951": [0.19444, 0.44444, 0.03704, 0],
+ "952": [0, 0.69444, 0.03194, 0],
+ "953": [0, 0.44444, 0, 0],
+ "954": [0, 0.44444, 0, 0],
+ "955": [0, 0.69444, 0, 0],
+ "956": [0.19444, 0.44444, 0, 0],
+ "957": [0, 0.44444, 0.06898, 0],
+ "958": [0.19444, 0.69444, 0.03021, 0],
+ "959": [0, 0.44444, 0, 0],
+ "960": [0, 0.44444, 0.03704, 0],
+ "961": [0.19444, 0.44444, 0, 0],
+ "962": [0.09722, 0.44444, 0.07917, 0],
+ "963": [0, 0.44444, 0.03704, 0],
+ "964": [0, 0.44444, 0.13472, 0],
+ "965": [0, 0.44444, 0.03704, 0],
+ "966": [0.19444, 0.44444, 0, 0],
+ "967": [0.19444, 0.44444, 0, 0],
+ "968": [0.19444, 0.69444, 0.03704, 0],
+ "969": [0, 0.44444, 0.03704, 0],
+ "977": [0, 0.69444, 0, 0],
+ "981": [0.19444, 0.69444, 0, 0],
+ "982": [0, 0.44444, 0.03194, 0],
+ "1009": [0.19444, 0.44444, 0, 0],
+ "1013": [0, 0.44444, 0, 0],
+ },
+ "Math-Italic": {
+ "47": [0.19444, 0.69444, 0, 0],
+ "65": [0, 0.68333, 0, 0.13889],
+ "66": [0, 0.68333, 0.05017, 0.08334],
+ "67": [0, 0.68333, 0.07153, 0.08334],
+ "68": [0, 0.68333, 0.02778, 0.05556],
+ "69": [0, 0.68333, 0.05764, 0.08334],
+ "70": [0, 0.68333, 0.13889, 0.08334],
+ "71": [0, 0.68333, 0, 0.08334],
+ "72": [0, 0.68333, 0.08125, 0.05556],
+ "73": [0, 0.68333, 0.07847, 0.11111],
+ "74": [0, 0.68333, 0.09618, 0.16667],
+ "75": [0, 0.68333, 0.07153, 0.05556],
+ "76": [0, 0.68333, 0, 0.02778],
+ "77": [0, 0.68333, 0.10903, 0.08334],
+ "78": [0, 0.68333, 0.10903, 0.08334],
+ "79": [0, 0.68333, 0.02778, 0.08334],
+ "80": [0, 0.68333, 0.13889, 0.08334],
+ "81": [0.19444, 0.68333, 0, 0.08334],
+ "82": [0, 0.68333, 0.00773, 0.08334],
+ "83": [0, 0.68333, 0.05764, 0.08334],
+ "84": [0, 0.68333, 0.13889, 0.08334],
+ "85": [0, 0.68333, 0.10903, 0.02778],
+ "86": [0, 0.68333, 0.22222, 0],
+ "87": [0, 0.68333, 0.13889, 0],
+ "88": [0, 0.68333, 0.07847, 0.08334],
+ "89": [0, 0.68333, 0.22222, 0],
+ "90": [0, 0.68333, 0.07153, 0.08334],
+ "97": [0, 0.43056, 0, 0],
+ "98": [0, 0.69444, 0, 0],
+ "99": [0, 0.43056, 0, 0.05556],
+ "100": [0, 0.69444, 0, 0.16667],
+ "101": [0, 0.43056, 0, 0.05556],
+ "102": [0.19444, 0.69444, 0.10764, 0.16667],
+ "103": [0.19444, 0.43056, 0.03588, 0.02778],
+ "104": [0, 0.69444, 0, 0],
+ "105": [0, 0.65952, 0, 0],
+ "106": [0.19444, 0.65952, 0.05724, 0],
+ "107": [0, 0.69444, 0.03148, 0],
+ "108": [0, 0.69444, 0.01968, 0.08334],
+ "109": [0, 0.43056, 0, 0],
+ "110": [0, 0.43056, 0, 0],
+ "111": [0, 0.43056, 0, 0.05556],
+ "112": [0.19444, 0.43056, 0, 0.08334],
+ "113": [0.19444, 0.43056, 0.03588, 0.08334],
+ "114": [0, 0.43056, 0.02778, 0.05556],
+ "115": [0, 0.43056, 0, 0.05556],
+ "116": [0, 0.61508, 0, 0.08334],
+ "117": [0, 0.43056, 0, 0.02778],
+ "118": [0, 0.43056, 0.03588, 0.02778],
+ "119": [0, 0.43056, 0.02691, 0.08334],
+ "120": [0, 0.43056, 0, 0.02778],
+ "121": [0.19444, 0.43056, 0.03588, 0.05556],
+ "122": [0, 0.43056, 0.04398, 0.05556],
+ "915": [0, 0.68333, 0.13889, 0.08334],
+ "916": [0, 0.68333, 0, 0.16667],
+ "920": [0, 0.68333, 0.02778, 0.08334],
+ "923": [0, 0.68333, 0, 0.16667],
+ "926": [0, 0.68333, 0.07569, 0.08334],
+ "928": [0, 0.68333, 0.08125, 0.05556],
+ "931": [0, 0.68333, 0.05764, 0.08334],
+ "933": [0, 0.68333, 0.13889, 0.05556],
+ "934": [0, 0.68333, 0, 0.08334],
+ "936": [0, 0.68333, 0.11, 0.05556],
+ "937": [0, 0.68333, 0.05017, 0.08334],
+ "945": [0, 0.43056, 0.0037, 0.02778],
+ "946": [0.19444, 0.69444, 0.05278, 0.08334],
+ "947": [0.19444, 0.43056, 0.05556, 0],
+ "948": [0, 0.69444, 0.03785, 0.05556],
+ "949": [0, 0.43056, 0, 0.08334],
+ "950": [0.19444, 0.69444, 0.07378, 0.08334],
+ "951": [0.19444, 0.43056, 0.03588, 0.05556],
+ "952": [0, 0.69444, 0.02778, 0.08334],
+ "953": [0, 0.43056, 0, 0.05556],
+ "954": [0, 0.43056, 0, 0],
+ "955": [0, 0.69444, 0, 0],
+ "956": [0.19444, 0.43056, 0, 0.02778],
+ "957": [0, 0.43056, 0.06366, 0.02778],
+ "958": [0.19444, 0.69444, 0.04601, 0.11111],
+ "959": [0, 0.43056, 0, 0.05556],
+ "960": [0, 0.43056, 0.03588, 0],
+ "961": [0.19444, 0.43056, 0, 0.08334],
+ "962": [0.09722, 0.43056, 0.07986, 0.08334],
+ "963": [0, 0.43056, 0.03588, 0],
+ "964": [0, 0.43056, 0.1132, 0.02778],
+ "965": [0, 0.43056, 0.03588, 0.02778],
+ "966": [0.19444, 0.43056, 0, 0.08334],
+ "967": [0.19444, 0.43056, 0, 0.05556],
+ "968": [0.19444, 0.69444, 0.03588, 0.11111],
+ "969": [0, 0.43056, 0.03588, 0],
+ "977": [0, 0.69444, 0, 0.08334],
+ "981": [0.19444, 0.69444, 0, 0.08334],
+ "982": [0, 0.43056, 0.02778, 0],
+ "1009": [0.19444, 0.43056, 0, 0.08334],
+ "1013": [0, 0.43056, 0, 0.05556],
+ },
+ "Math-Regular": {
+ "65": [0, 0.68333, 0, 0.13889],
+ "66": [0, 0.68333, 0.05017, 0.08334],
+ "67": [0, 0.68333, 0.07153, 0.08334],
+ "68": [0, 0.68333, 0.02778, 0.05556],
+ "69": [0, 0.68333, 0.05764, 0.08334],
+ "70": [0, 0.68333, 0.13889, 0.08334],
+ "71": [0, 0.68333, 0, 0.08334],
+ "72": [0, 0.68333, 0.08125, 0.05556],
+ "73": [0, 0.68333, 0.07847, 0.11111],
+ "74": [0, 0.68333, 0.09618, 0.16667],
+ "75": [0, 0.68333, 0.07153, 0.05556],
+ "76": [0, 0.68333, 0, 0.02778],
+ "77": [0, 0.68333, 0.10903, 0.08334],
+ "78": [0, 0.68333, 0.10903, 0.08334],
+ "79": [0, 0.68333, 0.02778, 0.08334],
+ "80": [0, 0.68333, 0.13889, 0.08334],
+ "81": [0.19444, 0.68333, 0, 0.08334],
+ "82": [0, 0.68333, 0.00773, 0.08334],
+ "83": [0, 0.68333, 0.05764, 0.08334],
+ "84": [0, 0.68333, 0.13889, 0.08334],
+ "85": [0, 0.68333, 0.10903, 0.02778],
+ "86": [0, 0.68333, 0.22222, 0],
+ "87": [0, 0.68333, 0.13889, 0],
+ "88": [0, 0.68333, 0.07847, 0.08334],
+ "89": [0, 0.68333, 0.22222, 0],
+ "90": [0, 0.68333, 0.07153, 0.08334],
+ "97": [0, 0.43056, 0, 0],
+ "98": [0, 0.69444, 0, 0],
+ "99": [0, 0.43056, 0, 0.05556],
+ "100": [0, 0.69444, 0, 0.16667],
+ "101": [0, 0.43056, 0, 0.05556],
+ "102": [0.19444, 0.69444, 0.10764, 0.16667],
+ "103": [0.19444, 0.43056, 0.03588, 0.02778],
+ "104": [0, 0.69444, 0, 0],
+ "105": [0, 0.65952, 0, 0],
+ "106": [0.19444, 0.65952, 0.05724, 0],
+ "107": [0, 0.69444, 0.03148, 0],
+ "108": [0, 0.69444, 0.01968, 0.08334],
+ "109": [0, 0.43056, 0, 0],
+ "110": [0, 0.43056, 0, 0],
+ "111": [0, 0.43056, 0, 0.05556],
+ "112": [0.19444, 0.43056, 0, 0.08334],
+ "113": [0.19444, 0.43056, 0.03588, 0.08334],
+ "114": [0, 0.43056, 0.02778, 0.05556],
+ "115": [0, 0.43056, 0, 0.05556],
+ "116": [0, 0.61508, 0, 0.08334],
+ "117": [0, 0.43056, 0, 0.02778],
+ "118": [0, 0.43056, 0.03588, 0.02778],
+ "119": [0, 0.43056, 0.02691, 0.08334],
+ "120": [0, 0.43056, 0, 0.02778],
+ "121": [0.19444, 0.43056, 0.03588, 0.05556],
+ "122": [0, 0.43056, 0.04398, 0.05556],
+ "915": [0, 0.68333, 0.13889, 0.08334],
+ "916": [0, 0.68333, 0, 0.16667],
+ "920": [0, 0.68333, 0.02778, 0.08334],
+ "923": [0, 0.68333, 0, 0.16667],
+ "926": [0, 0.68333, 0.07569, 0.08334],
+ "928": [0, 0.68333, 0.08125, 0.05556],
+ "931": [0, 0.68333, 0.05764, 0.08334],
+ "933": [0, 0.68333, 0.13889, 0.05556],
+ "934": [0, 0.68333, 0, 0.08334],
+ "936": [0, 0.68333, 0.11, 0.05556],
+ "937": [0, 0.68333, 0.05017, 0.08334],
+ "945": [0, 0.43056, 0.0037, 0.02778],
+ "946": [0.19444, 0.69444, 0.05278, 0.08334],
+ "947": [0.19444, 0.43056, 0.05556, 0],
+ "948": [0, 0.69444, 0.03785, 0.05556],
+ "949": [0, 0.43056, 0, 0.08334],
+ "950": [0.19444, 0.69444, 0.07378, 0.08334],
+ "951": [0.19444, 0.43056, 0.03588, 0.05556],
+ "952": [0, 0.69444, 0.02778, 0.08334],
+ "953": [0, 0.43056, 0, 0.05556],
+ "954": [0, 0.43056, 0, 0],
+ "955": [0, 0.69444, 0, 0],
+ "956": [0.19444, 0.43056, 0, 0.02778],
+ "957": [0, 0.43056, 0.06366, 0.02778],
+ "958": [0.19444, 0.69444, 0.04601, 0.11111],
+ "959": [0, 0.43056, 0, 0.05556],
+ "960": [0, 0.43056, 0.03588, 0],
+ "961": [0.19444, 0.43056, 0, 0.08334],
+ "962": [0.09722, 0.43056, 0.07986, 0.08334],
+ "963": [0, 0.43056, 0.03588, 0],
+ "964": [0, 0.43056, 0.1132, 0.02778],
+ "965": [0, 0.43056, 0.03588, 0.02778],
+ "966": [0.19444, 0.43056, 0, 0.08334],
+ "967": [0.19444, 0.43056, 0, 0.05556],
+ "968": [0.19444, 0.69444, 0.03588, 0.11111],
+ "969": [0, 0.43056, 0.03588, 0],
+ "977": [0, 0.69444, 0, 0.08334],
+ "981": [0.19444, 0.69444, 0, 0.08334],
+ "982": [0, 0.43056, 0.02778, 0],
+ "1009": [0.19444, 0.43056, 0, 0.08334],
+ "1013": [0, 0.43056, 0, 0.05556],
+ },
+ "SansSerif-Regular": {
+ "33": [0, 0.69444, 0, 0],
+ "34": [0, 0.69444, 0, 0],
+ "35": [0.19444, 0.69444, 0, 0],
+ "36": [0.05556, 0.75, 0, 0],
+ "37": [0.05556, 0.75, 0, 0],
+ "38": [0, 0.69444, 0, 0],
+ "39": [0, 0.69444, 0, 0],
+ "40": [0.25, 0.75, 0, 0],
+ "41": [0.25, 0.75, 0, 0],
+ "42": [0, 0.75, 0, 0],
+ "43": [0.08333, 0.58333, 0, 0],
+ "44": [0.125, 0.08333, 0, 0],
+ "45": [0, 0.44444, 0, 0],
+ "46": [0, 0.08333, 0, 0],
+ "47": [0.25, 0.75, 0, 0],
+ "48": [0, 0.65556, 0, 0],
+ "49": [0, 0.65556, 0, 0],
+ "50": [0, 0.65556, 0, 0],
+ "51": [0, 0.65556, 0, 0],
+ "52": [0, 0.65556, 0, 0],
+ "53": [0, 0.65556, 0, 0],
+ "54": [0, 0.65556, 0, 0],
+ "55": [0, 0.65556, 0, 0],
+ "56": [0, 0.65556, 0, 0],
+ "57": [0, 0.65556, 0, 0],
+ "58": [0, 0.44444, 0, 0],
+ "59": [0.125, 0.44444, 0, 0],
+ "61": [-0.13, 0.37, 0, 0],
+ "63": [0, 0.69444, 0, 0],
+ "64": [0, 0.69444, 0, 0],
+ "65": [0, 0.69444, 0, 0],
+ "66": [0, 0.69444, 0, 0],
+ "67": [0, 0.69444, 0, 0],
+ "68": [0, 0.69444, 0, 0],
+ "69": [0, 0.69444, 0, 0],
+ "70": [0, 0.69444, 0, 0],
+ "71": [0, 0.69444, 0, 0],
+ "72": [0, 0.69444, 0, 0],
+ "73": [0, 0.69444, 0, 0],
+ "74": [0, 0.69444, 0, 0],
+ "75": [0, 0.69444, 0, 0],
+ "76": [0, 0.69444, 0, 0],
+ "77": [0, 0.69444, 0, 0],
+ "78": [0, 0.69444, 0, 0],
+ "79": [0, 0.69444, 0, 0],
+ "80": [0, 0.69444, 0, 0],
+ "81": [0.125, 0.69444, 0, 0],
+ "82": [0, 0.69444, 0, 0],
+ "83": [0, 0.69444, 0, 0],
+ "84": [0, 0.69444, 0, 0],
+ "85": [0, 0.69444, 0, 0],
+ "86": [0, 0.69444, 0.01389, 0],
+ "87": [0, 0.69444, 0.01389, 0],
+ "88": [0, 0.69444, 0, 0],
+ "89": [0, 0.69444, 0.025, 0],
+ "90": [0, 0.69444, 0, 0],
+ "91": [0.25, 0.75, 0, 0],
+ "93": [0.25, 0.75, 0, 0],
+ "94": [0, 0.69444, 0, 0],
+ "95": [0.35, 0.09444, 0.02778, 0],
+ "97": [0, 0.44444, 0, 0],
+ "98": [0, 0.69444, 0, 0],
+ "99": [0, 0.44444, 0, 0],
+ "100": [0, 0.69444, 0, 0],
+ "101": [0, 0.44444, 0, 0],
+ "102": [0, 0.69444, 0.06944, 0],
+ "103": [0.19444, 0.44444, 0.01389, 0],
+ "104": [0, 0.69444, 0, 0],
+ "105": [0, 0.67937, 0, 0],
+ "106": [0.19444, 0.67937, 0, 0],
+ "107": [0, 0.69444, 0, 0],
+ "108": [0, 0.69444, 0, 0],
+ "109": [0, 0.44444, 0, 0],
+ "110": [0, 0.44444, 0, 0],
+ "111": [0, 0.44444, 0, 0],
+ "112": [0.19444, 0.44444, 0, 0],
+ "113": [0.19444, 0.44444, 0, 0],
+ "114": [0, 0.44444, 0.01389, 0],
+ "115": [0, 0.44444, 0, 0],
+ "116": [0, 0.57143, 0, 0],
+ "117": [0, 0.44444, 0, 0],
+ "118": [0, 0.44444, 0.01389, 0],
+ "119": [0, 0.44444, 0.01389, 0],
+ "120": [0, 0.44444, 0, 0],
+ "121": [0.19444, 0.44444, 0.01389, 0],
+ "122": [0, 0.44444, 0, 0],
+ "126": [0.35, 0.32659, 0, 0],
+ "305": [0, 0.44444, 0, 0],
+ "567": [0.19444, 0.44444, 0, 0],
+ "768": [0, 0.69444, 0, 0],
+ "769": [0, 0.69444, 0, 0],
+ "770": [0, 0.69444, 0, 0],
+ "771": [0, 0.67659, 0, 0],
+ "772": [0, 0.60889, 0, 0],
+ "774": [0, 0.69444, 0, 0],
+ "775": [0, 0.67937, 0, 0],
+ "776": [0, 0.67937, 0, 0],
+ "778": [0, 0.69444, 0, 0],
+ "779": [0, 0.69444, 0, 0],
+ "780": [0, 0.63194, 0, 0],
+ "915": [0, 0.69444, 0, 0],
+ "916": [0, 0.69444, 0, 0],
+ "920": [0, 0.69444, 0, 0],
+ "923": [0, 0.69444, 0, 0],
+ "926": [0, 0.69444, 0, 0],
+ "928": [0, 0.69444, 0, 0],
+ "931": [0, 0.69444, 0, 0],
+ "933": [0, 0.69444, 0, 0],
+ "934": [0, 0.69444, 0, 0],
+ "936": [0, 0.69444, 0, 0],
+ "937": [0, 0.69444, 0, 0],
+ "8211": [0, 0.44444, 0.02778, 0],
+ "8212": [0, 0.44444, 0.02778, 0],
+ "8216": [0, 0.69444, 0, 0],
+ "8217": [0, 0.69444, 0, 0],
+ "8220": [0, 0.69444, 0, 0],
+ "8221": [0, 0.69444, 0, 0],
+ },
+ "Script-Regular": {
+ "65": [0, 0.7, 0.22925, 0],
+ "66": [0, 0.7, 0.04087, 0],
+ "67": [0, 0.7, 0.1689, 0],
+ "68": [0, 0.7, 0.09371, 0],
+ "69": [0, 0.7, 0.18583, 0],
+ "70": [0, 0.7, 0.13634, 0],
+ "71": [0, 0.7, 0.17322, 0],
+ "72": [0, 0.7, 0.29694, 0],
+ "73": [0, 0.7, 0.19189, 0],
+ "74": [0.27778, 0.7, 0.19189, 0],
+ "75": [0, 0.7, 0.31259, 0],
+ "76": [0, 0.7, 0.19189, 0],
+ "77": [0, 0.7, 0.15981, 0],
+ "78": [0, 0.7, 0.3525, 0],
+ "79": [0, 0.7, 0.08078, 0],
+ "80": [0, 0.7, 0.08078, 0],
+ "81": [0, 0.7, 0.03305, 0],
+ "82": [0, 0.7, 0.06259, 0],
+ "83": [0, 0.7, 0.19189, 0],
+ "84": [0, 0.7, 0.29087, 0],
+ "85": [0, 0.7, 0.25815, 0],
+ "86": [0, 0.7, 0.27523, 0],
+ "87": [0, 0.7, 0.27523, 0],
+ "88": [0, 0.7, 0.26006, 0],
+ "89": [0, 0.7, 0.2939, 0],
+ "90": [0, 0.7, 0.24037, 0],
+ },
+ "Size1-Regular": {
+ "40": [0.35001, 0.85, 0, 0],
+ "41": [0.35001, 0.85, 0, 0],
+ "47": [0.35001, 0.85, 0, 0],
+ "91": [0.35001, 0.85, 0, 0],
+ "92": [0.35001, 0.85, 0, 0],
+ "93": [0.35001, 0.85, 0, 0],
+ "123": [0.35001, 0.85, 0, 0],
+ "125": [0.35001, 0.85, 0, 0],
+ "710": [0, 0.72222, 0, 0],
+ "732": [0, 0.72222, 0, 0],
+ "770": [0, 0.72222, 0, 0],
+ "771": [0, 0.72222, 0, 0],
+ "8214": [-0.00099, 0.601, 0, 0],
+ "8593": [1e-05, 0.6, 0, 0],
+ "8595": [1e-05, 0.6, 0, 0],
+ "8657": [1e-05, 0.6, 0, 0],
+ "8659": [1e-05, 0.6, 0, 0],
+ "8719": [0.25001, 0.75, 0, 0],
+ "8720": [0.25001, 0.75, 0, 0],
+ "8721": [0.25001, 0.75, 0, 0],
+ "8730": [0.35001, 0.85, 0, 0],
+ "8739": [-0.00599, 0.606, 0, 0],
+ "8741": [-0.00599, 0.606, 0, 0],
+ "8747": [0.30612, 0.805, 0.19445, 0],
+ "8748": [0.306, 0.805, 0.19445, 0],
+ "8749": [0.306, 0.805, 0.19445, 0],
+ "8750": [0.30612, 0.805, 0.19445, 0],
+ "8896": [0.25001, 0.75, 0, 0],
+ "8897": [0.25001, 0.75, 0, 0],
+ "8898": [0.25001, 0.75, 0, 0],
+ "8899": [0.25001, 0.75, 0, 0],
+ "8968": [0.35001, 0.85, 0, 0],
+ "8969": [0.35001, 0.85, 0, 0],
+ "8970": [0.35001, 0.85, 0, 0],
+ "8971": [0.35001, 0.85, 0, 0],
+ "9168": [-0.00099, 0.601, 0, 0],
+ "10216": [0.35001, 0.85, 0, 0],
+ "10217": [0.35001, 0.85, 0, 0],
+ "10752": [0.25001, 0.75, 0, 0],
+ "10753": [0.25001, 0.75, 0, 0],
+ "10754": [0.25001, 0.75, 0, 0],
+ "10756": [0.25001, 0.75, 0, 0],
+ "10758": [0.25001, 0.75, 0, 0],
+ },
+ "Size2-Regular": {
+ "40": [0.65002, 1.15, 0, 0],
+ "41": [0.65002, 1.15, 0, 0],
+ "47": [0.65002, 1.15, 0, 0],
+ "91": [0.65002, 1.15, 0, 0],
+ "92": [0.65002, 1.15, 0, 0],
+ "93": [0.65002, 1.15, 0, 0],
+ "123": [0.65002, 1.15, 0, 0],
+ "125": [0.65002, 1.15, 0, 0],
+ "710": [0, 0.75, 0, 0],
+ "732": [0, 0.75, 0, 0],
+ "770": [0, 0.75, 0, 0],
+ "771": [0, 0.75, 0, 0],
+ "8719": [0.55001, 1.05, 0, 0],
+ "8720": [0.55001, 1.05, 0, 0],
+ "8721": [0.55001, 1.05, 0, 0],
+ "8730": [0.65002, 1.15, 0, 0],
+ "8747": [0.86225, 1.36, 0.44445, 0],
+ "8748": [0.862, 1.36, 0.44445, 0],
+ "8749": [0.862, 1.36, 0.44445, 0],
+ "8750": [0.86225, 1.36, 0.44445, 0],
+ "8896": [0.55001, 1.05, 0, 0],
+ "8897": [0.55001, 1.05, 0, 0],
+ "8898": [0.55001, 1.05, 0, 0],
+ "8899": [0.55001, 1.05, 0, 0],
+ "8968": [0.65002, 1.15, 0, 0],
+ "8969": [0.65002, 1.15, 0, 0],
+ "8970": [0.65002, 1.15, 0, 0],
+ "8971": [0.65002, 1.15, 0, 0],
+ "10216": [0.65002, 1.15, 0, 0],
+ "10217": [0.65002, 1.15, 0, 0],
+ "10752": [0.55001, 1.05, 0, 0],
+ "10753": [0.55001, 1.05, 0, 0],
+ "10754": [0.55001, 1.05, 0, 0],
+ "10756": [0.55001, 1.05, 0, 0],
+ "10758": [0.55001, 1.05, 0, 0],
+ },
+ "Size3-Regular": {
+ "40": [0.95003, 1.45, 0, 0],
+ "41": [0.95003, 1.45, 0, 0],
+ "47": [0.95003, 1.45, 0, 0],
+ "91": [0.95003, 1.45, 0, 0],
+ "92": [0.95003, 1.45, 0, 0],
+ "93": [0.95003, 1.45, 0, 0],
+ "123": [0.95003, 1.45, 0, 0],
+ "125": [0.95003, 1.45, 0, 0],
+ "710": [0, 0.75, 0, 0],
+ "732": [0, 0.75, 0, 0],
+ "770": [0, 0.75, 0, 0],
+ "771": [0, 0.75, 0, 0],
+ "8730": [0.95003, 1.45, 0, 0],
+ "8968": [0.95003, 1.45, 0, 0],
+ "8969": [0.95003, 1.45, 0, 0],
+ "8970": [0.95003, 1.45, 0, 0],
+ "8971": [0.95003, 1.45, 0, 0],
+ "10216": [0.95003, 1.45, 0, 0],
+ "10217": [0.95003, 1.45, 0, 0],
+ },
+ "Size4-Regular": {
+ "40": [1.25003, 1.75, 0, 0],
+ "41": [1.25003, 1.75, 0, 0],
+ "47": [1.25003, 1.75, 0, 0],
+ "91": [1.25003, 1.75, 0, 0],
+ "92": [1.25003, 1.75, 0, 0],
+ "93": [1.25003, 1.75, 0, 0],
+ "123": [1.25003, 1.75, 0, 0],
+ "125": [1.25003, 1.75, 0, 0],
+ "710": [0, 0.825, 0, 0],
+ "732": [0, 0.825, 0, 0],
+ "770": [0, 0.825, 0, 0],
+ "771": [0, 0.825, 0, 0],
+ "8730": [1.25003, 1.75, 0, 0],
+ "8968": [1.25003, 1.75, 0, 0],
+ "8969": [1.25003, 1.75, 0, 0],
+ "8970": [1.25003, 1.75, 0, 0],
+ "8971": [1.25003, 1.75, 0, 0],
+ "9115": [0.64502, 1.155, 0, 0],
+ "9116": [1e-05, 0.6, 0, 0],
+ "9117": [0.64502, 1.155, 0, 0],
+ "9118": [0.64502, 1.155, 0, 0],
+ "9119": [1e-05, 0.6, 0, 0],
+ "9120": [0.64502, 1.155, 0, 0],
+ "9121": [0.64502, 1.155, 0, 0],
+ "9122": [-0.00099, 0.601, 0, 0],
+ "9123": [0.64502, 1.155, 0, 0],
+ "9124": [0.64502, 1.155, 0, 0],
+ "9125": [-0.00099, 0.601, 0, 0],
+ "9126": [0.64502, 1.155, 0, 0],
+ "9127": [1e-05, 0.9, 0, 0],
+ "9128": [0.65002, 1.15, 0, 0],
+ "9129": [0.90001, 0, 0, 0],
+ "9130": [0, 0.3, 0, 0],
+ "9131": [1e-05, 0.9, 0, 0],
+ "9132": [0.65002, 1.15, 0, 0],
+ "9133": [0.90001, 0, 0, 0],
+ "9143": [0.88502, 0.915, 0, 0],
+ "10216": [1.25003, 1.75, 0, 0],
+ "10217": [1.25003, 1.75, 0, 0],
+ "57344": [-0.00499, 0.605, 0, 0],
+ "57345": [-0.00499, 0.605, 0, 0],
+ "57680": [0, 0.12, 0, 0],
+ "57681": [0, 0.12, 0, 0],
+ "57682": [0, 0.12, 0, 0],
+ "57683": [0, 0.12, 0, 0],
+ },
+ "Typewriter-Regular": {
+ "33": [0, 0.61111, 0, 0],
+ "34": [0, 0.61111, 0, 0],
+ "35": [0, 0.61111, 0, 0],
+ "36": [0.08333, 0.69444, 0, 0],
+ "37": [0.08333, 0.69444, 0, 0],
+ "38": [0, 0.61111, 0, 0],
+ "39": [0, 0.61111, 0, 0],
+ "40": [0.08333, 0.69444, 0, 0],
+ "41": [0.08333, 0.69444, 0, 0],
+ "42": [0, 0.52083, 0, 0],
+ "43": [-0.08056, 0.53055, 0, 0],
+ "44": [0.13889, 0.125, 0, 0],
+ "45": [-0.08056, 0.53055, 0, 0],
+ "46": [0, 0.125, 0, 0],
+ "47": [0.08333, 0.69444, 0, 0],
+ "48": [0, 0.61111, 0, 0],
+ "49": [0, 0.61111, 0, 0],
+ "50": [0, 0.61111, 0, 0],
+ "51": [0, 0.61111, 0, 0],
+ "52": [0, 0.61111, 0, 0],
+ "53": [0, 0.61111, 0, 0],
+ "54": [0, 0.61111, 0, 0],
+ "55": [0, 0.61111, 0, 0],
+ "56": [0, 0.61111, 0, 0],
+ "57": [0, 0.61111, 0, 0],
+ "58": [0, 0.43056, 0, 0],
+ "59": [0.13889, 0.43056, 0, 0],
+ "60": [-0.05556, 0.55556, 0, 0],
+ "61": [-0.19549, 0.41562, 0, 0],
+ "62": [-0.05556, 0.55556, 0, 0],
+ "63": [0, 0.61111, 0, 0],
+ "64": [0, 0.61111, 0, 0],
+ "65": [0, 0.61111, 0, 0],
+ "66": [0, 0.61111, 0, 0],
+ "67": [0, 0.61111, 0, 0],
+ "68": [0, 0.61111, 0, 0],
+ "69": [0, 0.61111, 0, 0],
+ "70": [0, 0.61111, 0, 0],
+ "71": [0, 0.61111, 0, 0],
+ "72": [0, 0.61111, 0, 0],
+ "73": [0, 0.61111, 0, 0],
+ "74": [0, 0.61111, 0, 0],
+ "75": [0, 0.61111, 0, 0],
+ "76": [0, 0.61111, 0, 0],
+ "77": [0, 0.61111, 0, 0],
+ "78": [0, 0.61111, 0, 0],
+ "79": [0, 0.61111, 0, 0],
+ "80": [0, 0.61111, 0, 0],
+ "81": [0.13889, 0.61111, 0, 0],
+ "82": [0, 0.61111, 0, 0],
+ "83": [0, 0.61111, 0, 0],
+ "84": [0, 0.61111, 0, 0],
+ "85": [0, 0.61111, 0, 0],
+ "86": [0, 0.61111, 0, 0],
+ "87": [0, 0.61111, 0, 0],
+ "88": [0, 0.61111, 0, 0],
+ "89": [0, 0.61111, 0, 0],
+ "90": [0, 0.61111, 0, 0],
+ "91": [0.08333, 0.69444, 0, 0],
+ "92": [0.08333, 0.69444, 0, 0],
+ "93": [0.08333, 0.69444, 0, 0],
+ "94": [0, 0.61111, 0, 0],
+ "95": [0.09514, 0, 0, 0],
+ "96": [0, 0.61111, 0, 0],
+ "97": [0, 0.43056, 0, 0],
+ "98": [0, 0.61111, 0, 0],
+ "99": [0, 0.43056, 0, 0],
+ "100": [0, 0.61111, 0, 0],
+ "101": [0, 0.43056, 0, 0],
+ "102": [0, 0.61111, 0, 0],
+ "103": [0.22222, 0.43056, 0, 0],
+ "104": [0, 0.61111, 0, 0],
+ "105": [0, 0.61111, 0, 0],
+ "106": [0.22222, 0.61111, 0, 0],
+ "107": [0, 0.61111, 0, 0],
+ "108": [0, 0.61111, 0, 0],
+ "109": [0, 0.43056, 0, 0],
+ "110": [0, 0.43056, 0, 0],
+ "111": [0, 0.43056, 0, 0],
+ "112": [0.22222, 0.43056, 0, 0],
+ "113": [0.22222, 0.43056, 0, 0],
+ "114": [0, 0.43056, 0, 0],
+ "115": [0, 0.43056, 0, 0],
+ "116": [0, 0.55358, 0, 0],
+ "117": [0, 0.43056, 0, 0],
+ "118": [0, 0.43056, 0, 0],
+ "119": [0, 0.43056, 0, 0],
+ "120": [0, 0.43056, 0, 0],
+ "121": [0.22222, 0.43056, 0, 0],
+ "122": [0, 0.43056, 0, 0],
+ "123": [0.08333, 0.69444, 0, 0],
+ "124": [0.08333, 0.69444, 0, 0],
+ "125": [0.08333, 0.69444, 0, 0],
+ "126": [0, 0.61111, 0, 0],
+ "127": [0, 0.61111, 0, 0],
+ "305": [0, 0.43056, 0, 0],
+ "567": [0.22222, 0.43056, 0, 0],
+ "768": [0, 0.61111, 0, 0],
+ "769": [0, 0.61111, 0, 0],
+ "770": [0, 0.61111, 0, 0],
+ "771": [0, 0.61111, 0, 0],
+ "772": [0, 0.56555, 0, 0],
+ "774": [0, 0.61111, 0, 0],
+ "776": [0, 0.61111, 0, 0],
+ "778": [0, 0.61111, 0, 0],
+ "780": [0, 0.56597, 0, 0],
+ "915": [0, 0.61111, 0, 0],
+ "916": [0, 0.61111, 0, 0],
+ "920": [0, 0.61111, 0, 0],
+ "923": [0, 0.61111, 0, 0],
+ "926": [0, 0.61111, 0, 0],
+ "928": [0, 0.61111, 0, 0],
+ "931": [0, 0.61111, 0, 0],
+ "933": [0, 0.61111, 0, 0],
+ "934": [0, 0.61111, 0, 0],
+ "936": [0, 0.61111, 0, 0],
+ "937": [0, 0.61111, 0, 0],
+ "2018": [0, 0.61111, 0, 0],
+ "2019": [0, 0.61111, 0, 0],
+ "8242": [0, 0.61111, 0, 0],
+ },
+};
+
+},{}],19:[function(require,module,exports){
+var utils = require("./utils");
+var ParseError = require("./ParseError");
+
+/* This file contains a list of functions that we parse, identified by
+ * the calls to defineFunction.
+ *
+ * The first argument to defineFunction is a single name or a list of names.
+ * All functions named in such a list will share a single implementation.
+ *
+ * Each declared function can have associated properties, which
+ * include the following:
+ *
+ * - numArgs: The number of arguments the function takes.
+ * If this is the only property, it can be passed as a number
+ * instead of an element of a properties object.
+ * - argTypes: (optional) An array corresponding to each argument of the
+ * function, giving the type of argument that should be parsed. Its
+ * length should be equal to `numArgs + numOptionalArgs`. Valid
+ * types:
+ * - "size": A size-like thing, such as "1em" or "5ex"
+ * - "color": An html color, like "#abc" or "blue"
+ * - "original": The same type as the environment that the
+ * function being parsed is in (e.g. used for the
+ * bodies of functions like \color where the first
+ * argument is special and the second argument is
+ * parsed normally)
+ * Other possible types (probably shouldn't be used)
+ * - "text": Text-like (e.g. \text)
+ * - "math": Normal math
+ * If undefined, this will be treated as an appropriate length
+ * array of "original" strings
+ * - greediness: (optional) The greediness of the function to use ungrouped
+ * arguments.
+ *
+ * E.g. if you have an expression
+ * \sqrt \frac 1 2
+ * since \frac has greediness=2 vs \sqrt's greediness=1, \frac
+ * will use the two arguments '1' and '2' as its two arguments,
+ * then that whole function will be used as the argument to
+ * \sqrt. On the other hand, the expressions
+ * \frac \frac 1 2 3
+ * and
+ * \frac \sqrt 1 2
+ * will fail because \frac and \frac have equal greediness
+ * and \sqrt has a lower greediness than \frac respectively. To
+ * make these parse, we would have to change them to:
+ * \frac {\frac 1 2} 3
+ * and
+ * \frac {\sqrt 1} 2
+ *
+ * The default value is `1`
+ * - allowedInText: (optional) Whether or not the function is allowed inside
+ * text mode (default false)
+ * - numOptionalArgs: (optional) The number of optional arguments the function
+ * should parse. If the optional arguments aren't found,
+ * `null` will be passed to the handler in their place.
+ * (default 0)
+ * - infix: (optional) Must be true if the function is an infix operator.
+ *
+ * The last argument is that implementation, the handler for the function(s).
+ * It is called to handle these functions and their arguments.
+ * It receives two arguments:
+ * - context contains information and references provided by the parser
+ * - args is an array of arguments obtained from TeX input
+ * The context contains the following properties:
+ * - funcName: the text (i.e. name) of the function, including \
+ * - parser: the parser object
+ * - lexer: the lexer object
+ * - positions: the positions in the overall string of the function
+ * and the arguments.
+ * The latter three should only be used to produce error messages.
+ *
+ * The function should return an object with the following keys:
+ * - type: The type of element that this is. This is then used in
+ * buildHTML/buildMathML to determine which function
+ * should be called to build this node into a DOM node
+ * Any other data can be added to the object, which will be passed
+ * in to the function in buildHTML/buildMathML as `group.value`.
+ */
+
+function defineFunction(names, props, handler) {
+ if (typeof names === "string") {
+ names = [names];
+ }
+ if (typeof props === "number") {
+ props = { numArgs: props };
+ }
+ // Set default values of functions
+ var data = {
+ numArgs: props.numArgs,
+ argTypes: props.argTypes,
+ greediness: (props.greediness === undefined) ? 1 : props.greediness,
+ allowedInText: !!props.allowedInText,
+ numOptionalArgs: props.numOptionalArgs || 0,
+ infix: !!props.infix,
+ handler: handler,
+ };
+ for (var i = 0; i < names.length; ++i) {
+ module.exports[names[i]] = data;
+ }
+}
+
+// A normal square root
+defineFunction("\\sqrt", {
+ numArgs: 1,
+ numOptionalArgs: 1,
+}, function(context, args) {
+ var index = args[0];
+ var body = args[1];
+ return {
+ type: "sqrt",
+ body: body,
+ index: index,
+ };
+});
+
+// Some non-mathy text
+defineFunction("\\text", {
+ numArgs: 1,
+ argTypes: ["text"],
+ greediness: 2,
+}, function(context, args) {
+ var body = args[0];
+ // Since the corresponding buildHTML/buildMathML function expects a
+ // list of elements, we normalize for different kinds of arguments
+ // TODO(emily): maybe this should be done somewhere else
+ var inner;
+ if (body.type === "ordgroup") {
+ inner = body.value;
+ } else {
+ inner = [body];
+ }
+
+ return {
+ type: "text",
+ body: inner,
+ };
+});
+
+// A two-argument custom color
+defineFunction("\\color", {
+ numArgs: 2,
+ allowedInText: true,
+ greediness: 3,
+ argTypes: ["color", "original"],
+}, function(context, args) {
+ var color = args[0];
+ var body = args[1];
+ // Normalize the different kinds of bodies (see \text above)
+ var inner;
+ if (body.type === "ordgroup") {
+ inner = body.value;
+ } else {
+ inner = [body];
+ }
+
+ return {
+ type: "color",
+ color: color.value,
+ value: inner,
+ };
+});
+
+// An overline
+defineFunction("\\overline", {
+ numArgs: 1,
+}, function(context, args) {
+ var body = args[0];
+ return {
+ type: "overline",
+ body: body,
+ };
+});
+
+// An underline
+defineFunction("\\underline", {
+ numArgs: 1,
+}, function(context, args) {
+ var body = args[0];
+ return {
+ type: "underline",
+ body: body,
+ };
+});
+
+// A box of the width and height
+defineFunction("\\rule", {
+ numArgs: 2,
+ numOptionalArgs: 1,
+ argTypes: ["size", "size", "size"],
+}, function(context, args) {
+ var shift = args[0];
+ var width = args[1];
+ var height = args[2];
+ return {
+ type: "rule",
+ shift: shift && shift.value,
+ width: width.value,
+ height: height.value,
+ };
+});
+
+defineFunction("\\kern", {
+ numArgs: 1,
+ argTypes: ["size"],
+}, function(context, args) {
+ return {
+ type: "kern",
+ dimension: args[0].value,
+ };
+});
+
+// A KaTeX logo
+defineFunction("\\KaTeX", {
+ numArgs: 0,
+}, function(context) {
+ return {
+ type: "katex",
+ };
+});
+
+defineFunction("\\phantom", {
+ numArgs: 1,
+}, function(context, args) {
+ var body = args[0];
+ var inner;
+ if (body.type === "ordgroup") {
+ inner = body.value;
+ } else {
+ inner = [body];
+ }
+
+ return {
+ type: "phantom",
+ value: inner,
+ };
+});
+
+// Extra data needed for the delimiter handler down below
+var delimiterSizes = {
+ "\\bigl" : {type: "open", size: 1},
+ "\\Bigl" : {type: "open", size: 2},
+ "\\biggl": {type: "open", size: 3},
+ "\\Biggl": {type: "open", size: 4},
+ "\\bigr" : {type: "close", size: 1},
+ "\\Bigr" : {type: "close", size: 2},
+ "\\biggr": {type: "close", size: 3},
+ "\\Biggr": {type: "close", size: 4},
+ "\\bigm" : {type: "rel", size: 1},
+ "\\Bigm" : {type: "rel", size: 2},
+ "\\biggm": {type: "rel", size: 3},
+ "\\Biggm": {type: "rel", size: 4},
+ "\\big" : {type: "textord", size: 1},
+ "\\Big" : {type: "textord", size: 2},
+ "\\bigg" : {type: "textord", size: 3},
+ "\\Bigg" : {type: "textord", size: 4},
+};
+
+var delimiters = [
+ "(", ")", "[", "\\lbrack", "]", "\\rbrack",
+ "\\{", "\\lbrace", "\\}", "\\rbrace",
+ "\\lfloor", "\\rfloor", "\\lceil", "\\rceil",
+ "<", ">", "\\langle", "\\rangle", "\\lt", "\\gt",
+ "\\lvert", "\\rvert", "\\lVert", "\\rVert",
+ "\\lgroup", "\\rgroup", "\\lmoustache", "\\rmoustache",
+ "/", "\\backslash",
+ "|", "\\vert", "\\|", "\\Vert",
+ "\\uparrow", "\\Uparrow",
+ "\\downarrow", "\\Downarrow",
+ "\\updownarrow", "\\Updownarrow",
+ ".",
+];
+
+var fontAliases = {
+ "\\Bbb": "\\mathbb",
+ "\\bold": "\\mathbf",
+ "\\frak": "\\mathfrak",
+};
+
+// Single-argument color functions
+defineFunction([
+ "\\blue", "\\orange", "\\pink", "\\red",
+ "\\green", "\\gray", "\\purple",
+ "\\blueA", "\\blueB", "\\blueC", "\\blueD", "\\blueE",
+ "\\tealA", "\\tealB", "\\tealC", "\\tealD", "\\tealE",
+ "\\greenA", "\\greenB", "\\greenC", "\\greenD", "\\greenE",
+ "\\goldA", "\\goldB", "\\goldC", "\\goldD", "\\goldE",
+ "\\redA", "\\redB", "\\redC", "\\redD", "\\redE",
+ "\\maroonA", "\\maroonB", "\\maroonC", "\\maroonD", "\\maroonE",
+ "\\purpleA", "\\purpleB", "\\purpleC", "\\purpleD", "\\purpleE",
+ "\\mintA", "\\mintB", "\\mintC",
+ "\\grayA", "\\grayB", "\\grayC", "\\grayD", "\\grayE",
+ "\\grayF", "\\grayG", "\\grayH", "\\grayI",
+ "\\kaBlue", "\\kaGreen",
+], {
+ numArgs: 1,
+ allowedInText: true,
+ greediness: 3,
+}, function(context, args) {
+ var body = args[0];
+ var atoms;
+ if (body.type === "ordgroup") {
+ atoms = body.value;
+ } else {
+ atoms = [body];
+ }
+
+ return {
+ type: "color",
+ color: "katex-" + context.funcName.slice(1),
+ value: atoms,
+ };
+});
+
+// There are 2 flags for operators; whether they produce limits in
+// displaystyle, and whether they are symbols and should grow in
+// displaystyle. These four groups cover the four possible choices.
+
+// No limits, not symbols
+defineFunction([
+ "\\arcsin", "\\arccos", "\\arctan", "\\arg", "\\cos", "\\cosh",
+ "\\cot", "\\coth", "\\csc", "\\deg", "\\dim", "\\exp", "\\hom",
+ "\\ker", "\\lg", "\\ln", "\\log", "\\sec", "\\sin", "\\sinh",
+ "\\tan", "\\tanh",
+], {
+ numArgs: 0,
+}, function(context) {
+ return {
+ type: "op",
+ limits: false,
+ symbol: false,
+ body: context.funcName,
+ };
+});
+
+// Limits, not symbols
+defineFunction([
+ "\\det", "\\gcd", "\\inf", "\\lim", "\\liminf", "\\limsup", "\\max",
+ "\\min", "\\Pr", "\\sup",
+], {
+ numArgs: 0,
+}, function(context) {
+ return {
+ type: "op",
+ limits: true,
+ symbol: false,
+ body: context.funcName,
+ };
+});
+
+// No limits, symbols
+defineFunction([
+ "\\int", "\\iint", "\\iiint", "\\oint",
+], {
+ numArgs: 0,
+}, function(context) {
+ return {
+ type: "op",
+ limits: false,
+ symbol: true,
+ body: context.funcName,
+ };
+});
+
+// Limits, symbols
+defineFunction([
+ "\\coprod", "\\bigvee", "\\bigwedge", "\\biguplus", "\\bigcap",
+ "\\bigcup", "\\intop", "\\prod", "\\sum", "\\bigotimes",
+ "\\bigoplus", "\\bigodot", "\\bigsqcup", "\\smallint",
+], {
+ numArgs: 0,
+}, function(context) {
+ return {
+ type: "op",
+ limits: true,
+ symbol: true,
+ body: context.funcName,
+ };
+});
+
+// Fractions
+defineFunction([
+ "\\dfrac", "\\frac", "\\tfrac",
+ "\\dbinom", "\\binom", "\\tbinom",
+], {
+ numArgs: 2,
+ greediness: 2,
+}, function(context, args) {
+ var numer = args[0];
+ var denom = args[1];
+ var hasBarLine;
+ var leftDelim = null;
+ var rightDelim = null;
+ var size = "auto";
+
+ switch (context.funcName) {
+ case "\\dfrac":
+ case "\\frac":
+ case "\\tfrac":
+ hasBarLine = true;
+ break;
+ case "\\dbinom":
+ case "\\binom":
+ case "\\tbinom":
+ hasBarLine = false;
+ leftDelim = "(";
+ rightDelim = ")";
+ break;
+ default:
+ throw new Error("Unrecognized genfrac command");
+ }
+
+ switch (context.funcName) {
+ case "\\dfrac":
+ case "\\dbinom":
+ size = "display";
+ break;
+ case "\\tfrac":
+ case "\\tbinom":
+ size = "text";
+ break;
+ }
+
+ return {
+ type: "genfrac",
+ numer: numer,
+ denom: denom,
+ hasBarLine: hasBarLine,
+ leftDelim: leftDelim,
+ rightDelim: rightDelim,
+ size: size,
+ };
+});
+
+// Left and right overlap functions
+defineFunction(["\\llap", "\\rlap"], {
+ numArgs: 1,
+ allowedInText: true,
+}, function(context, args) {
+ var body = args[0];
+ return {
+ type: context.funcName.slice(1),
+ body: body,
+ };
+});
+
+// Delimiter functions
+defineFunction([
+ "\\bigl", "\\Bigl", "\\biggl", "\\Biggl",
+ "\\bigr", "\\Bigr", "\\biggr", "\\Biggr",
+ "\\bigm", "\\Bigm", "\\biggm", "\\Biggm",
+ "\\big", "\\Big", "\\bigg", "\\Bigg",
+ "\\left", "\\right",
+], {
+ numArgs: 1,
+}, function(context, args) {
+ var delim = args[0];
+ if (!utils.contains(delimiters, delim.value)) {
+ throw new ParseError(
+ "Invalid delimiter: '" + delim.value + "' after '" +
+ context.funcName + "'", delim);
+ }
+
+ // \left and \right are caught somewhere in Parser.js, which is
+ // why this data doesn't match what is in buildHTML.
+ if (context.funcName === "\\left" || context.funcName === "\\right") {
+ return {
+ type: "leftright",
+ value: delim.value,
+ };
+ } else {
+ return {
+ type: "delimsizing",
+ size: delimiterSizes[context.funcName].size,
+ delimType: delimiterSizes[context.funcName].type,
+ value: delim.value,
+ };
+ }
+});
+
+// Sizing functions (handled in Parser.js explicitly, hence no handler)
+defineFunction([
+ "\\tiny", "\\scriptsize", "\\footnotesize", "\\small",
+ "\\normalsize", "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge",
+], 0, null);
+
+// Style changing functions (handled in Parser.js explicitly, hence no
+// handler)
+defineFunction([
+ "\\displaystyle", "\\textstyle", "\\scriptstyle",
+ "\\scriptscriptstyle",
+], 0, null);
+
+defineFunction([
+ // styles
+ "\\mathrm", "\\mathit", "\\mathbf",
+
+ // families
+ "\\mathbb", "\\mathcal", "\\mathfrak", "\\mathscr", "\\mathsf",
+ "\\mathtt",
+
+ // aliases
+ "\\Bbb", "\\bold", "\\frak",
+], {
+ numArgs: 1,
+ greediness: 2,
+}, function(context, args) {
+ var body = args[0];
+ var func = context.funcName;
+ if (func in fontAliases) {
+ func = fontAliases[func];
+ }
+ return {
+ type: "font",
+ font: func.slice(1),
+ body: body,
+ };
+});
+
+// Accents
+defineFunction([
+ "\\acute", "\\grave", "\\ddot", "\\tilde", "\\bar", "\\breve",
+ "\\check", "\\hat", "\\vec", "\\dot",
+ // We don't support expanding accents yet
+ // "\\widetilde", "\\widehat"
+], {
+ numArgs: 1,
+}, function(context, args) {
+ var base = args[0];
+ return {
+ type: "accent",
+ accent: context.funcName,
+ base: base,
+ };
+});
+
+// Infix generalized fractions
+defineFunction(["\\over", "\\choose"], {
+ numArgs: 0,
+ infix: true,
+}, function(context) {
+ var replaceWith;
+ switch (context.funcName) {
+ case "\\over":
+ replaceWith = "\\frac";
+ break;
+ case "\\choose":
+ replaceWith = "\\binom";
+ break;
+ default:
+ throw new Error("Unrecognized infix genfrac command");
+ }
+ return {
+ type: "infix",
+ replaceWith: replaceWith,
+ token: context.token,
+ };
+});
+
+// Row breaks for aligned data
+defineFunction(["\\\\", "\\cr"], {
+ numArgs: 0,
+ numOptionalArgs: 1,
+ argTypes: ["size"],
+}, function(context, args) {
+ var size = args[0];
+ return {
+ type: "cr",
+ size: size,
+ };
+});
+
+// Environment delimiters
+defineFunction(["\\begin", "\\end"], {
+ numArgs: 1,
+ argTypes: ["text"],
+}, function(context, args) {
+ var nameGroup = args[0];
+ if (nameGroup.type !== "ordgroup") {
+ throw new ParseError("Invalid environment name", nameGroup);
+ }
+ var name = "";
+ for (var i = 0; i < nameGroup.value.length; ++i) {
+ name += nameGroup.value[i].value;
+ }
+ return {
+ type: "environment",
+ name: name,
+ nameGroup: nameGroup,
+ };
+});
+
+},{"./ParseError":6,"./utils":25}],20:[function(require,module,exports){
+/**
+ * These objects store data about MathML nodes. This is the MathML equivalent
+ * of the types in domTree.js. Since MathML handles its own rendering, and
+ * since we're mainly using MathML to improve accessibility, we don't manage
+ * any of the styling state that the plain DOM nodes do.
+ *
+ * The `toNode` and `toMarkup` functions work simlarly to how they do in
+ * domTree.js, creating namespaced DOM nodes and HTML text markup respectively.
+ */
+
+var utils = require("./utils");
+
+/**
+ * This node represents a general purpose MathML node of any type. The
+ * constructor requires the type of node to create (for example, `"mo"` or
+ * `"mspace"`, corresponding to `<mo>` and `<mspace>` tags).
+ */
+function MathNode(type, children) {
+ this.type = type;
+ this.attributes = {};
+ this.children = children || [];
+}
+
+/**
+ * Sets an attribute on a MathML node. MathML depends on attributes to convey a
+ * semantic content, so this is used heavily.
+ */
+MathNode.prototype.setAttribute = function(name, value) {
+ this.attributes[name] = value;
+};
+
+/**
+ * Converts the math node into a MathML-namespaced DOM element.
+ */
+MathNode.prototype.toNode = function() {
+ var node = document.createElementNS(
+ "http://www.w3.org/1998/Math/MathML", this.type);
+
+ for (var attr in this.attributes) {
+ if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
+ node.setAttribute(attr, this.attributes[attr]);
+ }
+ }
+
+ for (var i = 0; i < this.children.length; i++) {
+ node.appendChild(this.children[i].toNode());
+ }
+
+ return node;
+};
+
+/**
+ * Converts the math node into an HTML markup string.
+ */
+MathNode.prototype.toMarkup = function() {
+ var markup = "<" + this.type;
+
+ // Add the attributes
+ for (var attr in this.attributes) {
+ if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
+ markup += " " + attr + "=\"";
+ markup += utils.escape(this.attributes[attr]);
+ markup += "\"";
+ }
+ }
+
+ markup += ">";
+
+ for (var i = 0; i < this.children.length; i++) {
+ markup += this.children[i].toMarkup();
+ }
+
+ markup += "</" + this.type + ">";
+
+ return markup;
+};
+
+/**
+ * This node represents a piece of text.
+ */
+function TextNode(text) {
+ this.text = text;
+}
+
+/**
+ * Converts the text node into a DOM text node.
+ */
+TextNode.prototype.toNode = function() {
+ return document.createTextNode(this.text);
+};
+
+/**
+ * Converts the text node into HTML markup (which is just the text itself).
+ */
+TextNode.prototype.toMarkup = function() {
+ return utils.escape(this.text);
+};
+
+module.exports = {
+ MathNode: MathNode,
+ TextNode: TextNode,
+};
+
+},{"./utils":25}],21:[function(require,module,exports){
+/**
+ * The resulting parse tree nodes of the parse tree.
+ *
+ * It is possible to provide position information, so that a ParseNode can
+ * fulfil a role similar to a Token in error reporting.
+ * For details on the corresponding properties see Token constructor.
+ * Providing such information can lead to better error reporting.
+ *
+ * @param {string} type type of node, like e.g. "ordgroup"
+ * @param {?object} value type-specific representation of the node
+ * @param {string} mode parse mode in action for this node,
+ * "math" or "text"
+ * @param {Token=} firstToken first token of the input for this node,
+ * will omit position information if unset
+ * @param {Token=} lastToken last token of the input for this node,
+ * will default to firstToken if unset
+ */
+function ParseNode(type, value, mode, firstToken, lastToken) {
+ this.type = type;
+ this.value = value;
+ this.mode = mode;
+ if (firstToken && (!lastToken || lastToken.lexer === firstToken.lexer)) {
+ this.lexer = firstToken.lexer;
+ this.start = firstToken.start;
+ this.end = (lastToken || firstToken).end;
+ }
+}
+
+module.exports = {
+ ParseNode: ParseNode,
+};
+
+
+},{}],22:[function(require,module,exports){
+/**
+ * Provides a single function for parsing an expression using a Parser
+ * TODO(emily): Remove this
+ */
+
+var Parser = require("./Parser");
+
+/**
+ * Parses an expression using a Parser, then returns the parsed result.
+ */
+var parseTree = function(toParse, settings) {
+ if (!(typeof toParse === 'string' || toParse instanceof String)) {
+ throw new TypeError('KaTeX can only parse string typed expression');
+ }
+ var parser = new Parser(toParse, settings);
+
+ return parser.parse();
+};
+
+module.exports = parseTree;
+
+},{"./Parser":7}],23:[function(require,module,exports){
+/**
+ * This file holds a list of all no-argument functions and single-character
+ * symbols (like 'a' or ';').
+ *
+ * For each of the symbols, there are three properties they can have:
+ * - font (required): the font to be used for this symbol. Either "main" (the
+ normal font), or "ams" (the ams fonts).
+ * - group (required): the ParseNode group type the symbol should have (i.e.
+ "textord", "mathord", etc).
+ See https://github.com/Khan/KaTeX/wiki/Examining-TeX#group-types
+ * - replace: the character that this symbol or function should be
+ * replaced with (i.e. "\phi" has a replace value of "\u03d5", the phi
+ * character in the main font).
+ *
+ * The outermost map in the table indicates what mode the symbols should be
+ * accepted in (e.g. "math" or "text").
+ */
+
+module.exports = {
+ math: {},
+ text: {},
+};
+
+function defineSymbol(mode, font, group, replace, name) {
+ module.exports[mode][name] = {
+ font: font,
+ group: group,
+ replace: replace,
+ };
+}
+
+// Some abbreviations for commonly used strings.
+// This helps minify the code, and also spotting typos using jshint.
+
+// modes:
+var math = "math";
+var text = "text";
+
+// fonts:
+var main = "main";
+var ams = "ams";
+
+// groups:
+var accent = "accent";
+var bin = "bin";
+var close = "close";
+var inner = "inner";
+var mathord = "mathord";
+var op = "op";
+var open = "open";
+var punct = "punct";
+var rel = "rel";
+var spacing = "spacing";
+var textord = "textord";
+
+// Now comes the symbol table
+
+// Relation Symbols
+defineSymbol(math, main, rel, "\u2261", "\\equiv");
+defineSymbol(math, main, rel, "\u227a", "\\prec");
+defineSymbol(math, main, rel, "\u227b", "\\succ");
+defineSymbol(math, main, rel, "\u223c", "\\sim");
+defineSymbol(math, main, rel, "\u22a5", "\\perp");
+defineSymbol(math, main, rel, "\u2aaf", "\\preceq");
+defineSymbol(math, main, rel, "\u2ab0", "\\succeq");
+defineSymbol(math, main, rel, "\u2243", "\\simeq");
+defineSymbol(math, main, rel, "\u2223", "\\mid");
+defineSymbol(math, main, rel, "\u226a", "\\ll");
+defineSymbol(math, main, rel, "\u226b", "\\gg");
+defineSymbol(math, main, rel, "\u224d", "\\asymp");
+defineSymbol(math, main, rel, "\u2225", "\\parallel");
+defineSymbol(math, main, rel, "\u22c8", "\\bowtie");
+defineSymbol(math, main, rel, "\u2323", "\\smile");
+defineSymbol(math, main, rel, "\u2291", "\\sqsubseteq");
+defineSymbol(math, main, rel, "\u2292", "\\sqsupseteq");
+defineSymbol(math, main, rel, "\u2250", "\\doteq");
+defineSymbol(math, main, rel, "\u2322", "\\frown");
+defineSymbol(math, main, rel, "\u220b", "\\ni");
+defineSymbol(math, main, rel, "\u221d", "\\propto");
+defineSymbol(math, main, rel, "\u22a2", "\\vdash");
+defineSymbol(math, main, rel, "\u22a3", "\\dashv");
+defineSymbol(math, main, rel, "\u220b", "\\owns");
+
+// Punctuation
+defineSymbol(math, main, punct, "\u002e", "\\ldotp");
+defineSymbol(math, main, punct, "\u22c5", "\\cdotp");
+
+// Misc Symbols
+defineSymbol(math, main, textord, "\u0023", "\\#");
+defineSymbol(math, main, textord, "\u0026", "\\&");
+defineSymbol(math, main, textord, "\u2135", "\\aleph");
+defineSymbol(math, main, textord, "\u2200", "\\forall");
+defineSymbol(math, main, textord, "\u210f", "\\hbar");
+defineSymbol(math, main, textord, "\u2203", "\\exists");
+defineSymbol(math, main, textord, "\u2207", "\\nabla");
+defineSymbol(math, main, textord, "\u266d", "\\flat");
+defineSymbol(math, main, textord, "\u2113", "\\ell");
+defineSymbol(math, main, textord, "\u266e", "\\natural");
+defineSymbol(math, main, textord, "\u2663", "\\clubsuit");
+defineSymbol(math, main, textord, "\u2118", "\\wp");
+defineSymbol(math, main, textord, "\u266f", "\\sharp");
+defineSymbol(math, main, textord, "\u2662", "\\diamondsuit");
+defineSymbol(math, main, textord, "\u211c", "\\Re");
+defineSymbol(math, main, textord, "\u2661", "\\heartsuit");
+defineSymbol(math, main, textord, "\u2111", "\\Im");
+defineSymbol(math, main, textord, "\u2660", "\\spadesuit");
+
+// Math and Text
+defineSymbol(math, main, textord, "\u2020", "\\dag");
+defineSymbol(math, main, textord, "\u2021", "\\ddag");
+
+// Large Delimiters
+defineSymbol(math, main, close, "\u23b1", "\\rmoustache");
+defineSymbol(math, main, open, "\u23b0", "\\lmoustache");
+defineSymbol(math, main, close, "\u27ef", "\\rgroup");
+defineSymbol(math, main, open, "\u27ee", "\\lgroup");
+
+// Binary Operators
+defineSymbol(math, main, bin, "\u2213", "\\mp");
+defineSymbol(math, main, bin, "\u2296", "\\ominus");
+defineSymbol(math, main, bin, "\u228e", "\\uplus");
+defineSymbol(math, main, bin, "\u2293", "\\sqcap");
+defineSymbol(math, main, bin, "\u2217", "\\ast");
+defineSymbol(math, main, bin, "\u2294", "\\sqcup");
+defineSymbol(math, main, bin, "\u25ef", "\\bigcirc");
+defineSymbol(math, main, bin, "\u2219", "\\bullet");
+defineSymbol(math, main, bin, "\u2021", "\\ddagger");
+defineSymbol(math, main, bin, "\u2240", "\\wr");
+defineSymbol(math, main, bin, "\u2a3f", "\\amalg");
+
+// Arrow Symbols
+defineSymbol(math, main, rel, "\u27f5", "\\longleftarrow");
+defineSymbol(math, main, rel, "\u21d0", "\\Leftarrow");
+defineSymbol(math, main, rel, "\u27f8", "\\Longleftarrow");
+defineSymbol(math, main, rel, "\u27f6", "\\longrightarrow");
+defineSymbol(math, main, rel, "\u21d2", "\\Rightarrow");
+defineSymbol(math, main, rel, "\u27f9", "\\Longrightarrow");
+defineSymbol(math, main, rel, "\u2194", "\\leftrightarrow");
+defineSymbol(math, main, rel, "\u27f7", "\\longleftrightarrow");
+defineSymbol(math, main, rel, "\u21d4", "\\Leftrightarrow");
+defineSymbol(math, main, rel, "\u27fa", "\\Longleftrightarrow");
+defineSymbol(math, main, rel, "\u21a6", "\\mapsto");
+defineSymbol(math, main, rel, "\u27fc", "\\longmapsto");
+defineSymbol(math, main, rel, "\u2197", "\\nearrow");
+defineSymbol(math, main, rel, "\u21a9", "\\hookleftarrow");
+defineSymbol(math, main, rel, "\u21aa", "\\hookrightarrow");
+defineSymbol(math, main, rel, "\u2198", "\\searrow");
+defineSymbol(math, main, rel, "\u21bc", "\\leftharpoonup");
+defineSymbol(math, main, rel, "\u21c0", "\\rightharpoonup");
+defineSymbol(math, main, rel, "\u2199", "\\swarrow");
+defineSymbol(math, main, rel, "\u21bd", "\\leftharpoondown");
+defineSymbol(math, main, rel, "\u21c1", "\\rightharpoondown");
+defineSymbol(math, main, rel, "\u2196", "\\nwarrow");
+defineSymbol(math, main, rel, "\u21cc", "\\rightleftharpoons");
+
+// AMS Negated Binary Relations
+defineSymbol(math, ams, rel, "\u226e", "\\nless");
+defineSymbol(math, ams, rel, "\ue010", "\\nleqslant");
+defineSymbol(math, ams, rel, "\ue011", "\\nleqq");
+defineSymbol(math, ams, rel, "\u2a87", "\\lneq");
+defineSymbol(math, ams, rel, "\u2268", "\\lneqq");
+defineSymbol(math, ams, rel, "\ue00c", "\\lvertneqq");
+defineSymbol(math, ams, rel, "\u22e6", "\\lnsim");
+defineSymbol(math, ams, rel, "\u2a89", "\\lnapprox");
+defineSymbol(math, ams, rel, "\u2280", "\\nprec");
+defineSymbol(math, ams, rel, "\u22e0", "\\npreceq");
+defineSymbol(math, ams, rel, "\u22e8", "\\precnsim");
+defineSymbol(math, ams, rel, "\u2ab9", "\\precnapprox");
+defineSymbol(math, ams, rel, "\u2241", "\\nsim");
+defineSymbol(math, ams, rel, "\ue006", "\\nshortmid");
+defineSymbol(math, ams, rel, "\u2224", "\\nmid");
+defineSymbol(math, ams, rel, "\u22ac", "\\nvdash");
+defineSymbol(math, ams, rel, "\u22ad", "\\nvDash");
+defineSymbol(math, ams, rel, "\u22ea", "\\ntriangleleft");
+defineSymbol(math, ams, rel, "\u22ec", "\\ntrianglelefteq");
+defineSymbol(math, ams, rel, "\u228a", "\\subsetneq");
+defineSymbol(math, ams, rel, "\ue01a", "\\varsubsetneq");
+defineSymbol(math, ams, rel, "\u2acb", "\\subsetneqq");
+defineSymbol(math, ams, rel, "\ue017", "\\varsubsetneqq");
+defineSymbol(math, ams, rel, "\u226f", "\\ngtr");
+defineSymbol(math, ams, rel, "\ue00f", "\\ngeqslant");
+defineSymbol(math, ams, rel, "\ue00e", "\\ngeqq");
+defineSymbol(math, ams, rel, "\u2a88", "\\gneq");
+defineSymbol(math, ams, rel, "\u2269", "\\gneqq");
+defineSymbol(math, ams, rel, "\ue00d", "\\gvertneqq");
+defineSymbol(math, ams, rel, "\u22e7", "\\gnsim");
+defineSymbol(math, ams, rel, "\u2a8a", "\\gnapprox");
+defineSymbol(math, ams, rel, "\u2281", "\\nsucc");
+defineSymbol(math, ams, rel, "\u22e1", "\\nsucceq");
+defineSymbol(math, ams, rel, "\u22e9", "\\succnsim");
+defineSymbol(math, ams, rel, "\u2aba", "\\succnapprox");
+defineSymbol(math, ams, rel, "\u2246", "\\ncong");
+defineSymbol(math, ams, rel, "\ue007", "\\nshortparallel");
+defineSymbol(math, ams, rel, "\u2226", "\\nparallel");
+defineSymbol(math, ams, rel, "\u22af", "\\nVDash");
+defineSymbol(math, ams, rel, "\u22eb", "\\ntriangleright");
+defineSymbol(math, ams, rel, "\u22ed", "\\ntrianglerighteq");
+defineSymbol(math, ams, rel, "\ue018", "\\nsupseteqq");
+defineSymbol(math, ams, rel, "\u228b", "\\supsetneq");
+defineSymbol(math, ams, rel, "\ue01b", "\\varsupsetneq");
+defineSymbol(math, ams, rel, "\u2acc", "\\supsetneqq");
+defineSymbol(math, ams, rel, "\ue019", "\\varsupsetneqq");
+defineSymbol(math, ams, rel, "\u22ae", "\\nVdash");
+defineSymbol(math, ams, rel, "\u2ab5", "\\precneqq");
+defineSymbol(math, ams, rel, "\u2ab6", "\\succneqq");
+defineSymbol(math, ams, rel, "\ue016", "\\nsubseteqq");
+defineSymbol(math, ams, bin, "\u22b4", "\\unlhd");
+defineSymbol(math, ams, bin, "\u22b5", "\\unrhd");
+
+// AMS Negated Arrows
+defineSymbol(math, ams, rel, "\u219a", "\\nleftarrow");
+defineSymbol(math, ams, rel, "\u219b", "\\nrightarrow");
+defineSymbol(math, ams, rel, "\u21cd", "\\nLeftarrow");
+defineSymbol(math, ams, rel, "\u21cf", "\\nRightarrow");
+defineSymbol(math, ams, rel, "\u21ae", "\\nleftrightarrow");
+defineSymbol(math, ams, rel, "\u21ce", "\\nLeftrightarrow");
+
+// AMS Misc
+defineSymbol(math, ams, rel, "\u25b3", "\\vartriangle");
+defineSymbol(math, ams, textord, "\u210f", "\\hslash");
+defineSymbol(math, ams, textord, "\u25bd", "\\triangledown");
+defineSymbol(math, ams, textord, "\u25ca", "\\lozenge");
+defineSymbol(math, ams, textord, "\u24c8", "\\circledS");
+defineSymbol(math, ams, textord, "\u00ae", "\\circledR");
+defineSymbol(math, ams, textord, "\u2221", "\\measuredangle");
+defineSymbol(math, ams, textord, "\u2204", "\\nexists");
+defineSymbol(math, ams, textord, "\u2127", "\\mho");
+defineSymbol(math, ams, textord, "\u2132", "\\Finv");
+defineSymbol(math, ams, textord, "\u2141", "\\Game");
+defineSymbol(math, ams, textord, "\u006b", "\\Bbbk");
+defineSymbol(math, ams, textord, "\u2035", "\\backprime");
+defineSymbol(math, ams, textord, "\u25b2", "\\blacktriangle");
+defineSymbol(math, ams, textord, "\u25bc", "\\blacktriangledown");
+defineSymbol(math, ams, textord, "\u25a0", "\\blacksquare");
+defineSymbol(math, ams, textord, "\u29eb", "\\blacklozenge");
+defineSymbol(math, ams, textord, "\u2605", "\\bigstar");
+defineSymbol(math, ams, textord, "\u2222", "\\sphericalangle");
+defineSymbol(math, ams, textord, "\u2201", "\\complement");
+defineSymbol(math, ams, textord, "\u00f0", "\\eth");
+defineSymbol(math, ams, textord, "\u2571", "\\diagup");
+defineSymbol(math, ams, textord, "\u2572", "\\diagdown");
+defineSymbol(math, ams, textord, "\u25a1", "\\square");
+defineSymbol(math, ams, textord, "\u25a1", "\\Box");
+defineSymbol(math, ams, textord, "\u25ca", "\\Diamond");
+defineSymbol(math, ams, textord, "\u00a5", "\\yen");
+defineSymbol(math, ams, textord, "\u2713", "\\checkmark");
+
+// AMS Hebrew
+defineSymbol(math, ams, textord, "\u2136", "\\beth");
+defineSymbol(math, ams, textord, "\u2138", "\\daleth");
+defineSymbol(math, ams, textord, "\u2137", "\\gimel");
+
+// AMS Greek
+defineSymbol(math, ams, textord, "\u03dd", "\\digamma");
+defineSymbol(math, ams, textord, "\u03f0", "\\varkappa");
+
+// AMS Delimiters
+defineSymbol(math, ams, open, "\u250c", "\\ulcorner");
+defineSymbol(math, ams, close, "\u2510", "\\urcorner");
+defineSymbol(math, ams, open, "\u2514", "\\llcorner");
+defineSymbol(math, ams, close, "\u2518", "\\lrcorner");
+
+// AMS Binary Relations
+defineSymbol(math, ams, rel, "\u2266", "\\leqq");
+defineSymbol(math, ams, rel, "\u2a7d", "\\leqslant");
+defineSymbol(math, ams, rel, "\u2a95", "\\eqslantless");
+defineSymbol(math, ams, rel, "\u2272", "\\lesssim");
+defineSymbol(math, ams, rel, "\u2a85", "\\lessapprox");
+defineSymbol(math, ams, rel, "\u224a", "\\approxeq");
+defineSymbol(math, ams, bin, "\u22d6", "\\lessdot");
+defineSymbol(math, ams, rel, "\u22d8", "\\lll");
+defineSymbol(math, ams, rel, "\u2276", "\\lessgtr");
+defineSymbol(math, ams, rel, "\u22da", "\\lesseqgtr");
+defineSymbol(math, ams, rel, "\u2a8b", "\\lesseqqgtr");
+defineSymbol(math, ams, rel, "\u2251", "\\doteqdot");
+defineSymbol(math, ams, rel, "\u2253", "\\risingdotseq");
+defineSymbol(math, ams, rel, "\u2252", "\\fallingdotseq");
+defineSymbol(math, ams, rel, "\u223d", "\\backsim");
+defineSymbol(math, ams, rel, "\u22cd", "\\backsimeq");
+defineSymbol(math, ams, rel, "\u2ac5", "\\subseteqq");
+defineSymbol(math, ams, rel, "\u22d0", "\\Subset");
+defineSymbol(math, ams, rel, "\u228f", "\\sqsubset");
+defineSymbol(math, ams, rel, "\u227c", "\\preccurlyeq");
+defineSymbol(math, ams, rel, "\u22de", "\\curlyeqprec");
+defineSymbol(math, ams, rel, "\u227e", "\\precsim");
+defineSymbol(math, ams, rel, "\u2ab7", "\\precapprox");
+defineSymbol(math, ams, rel, "\u22b2", "\\vartriangleleft");
+defineSymbol(math, ams, rel, "\u22b4", "\\trianglelefteq");
+defineSymbol(math, ams, rel, "\u22a8", "\\vDash");
+defineSymbol(math, ams, rel, "\u22aa", "\\Vvdash");
+defineSymbol(math, ams, rel, "\u2323", "\\smallsmile");
+defineSymbol(math, ams, rel, "\u2322", "\\smallfrown");
+defineSymbol(math, ams, rel, "\u224f", "\\bumpeq");
+defineSymbol(math, ams, rel, "\u224e", "\\Bumpeq");
+defineSymbol(math, ams, rel, "\u2267", "\\geqq");
+defineSymbol(math, ams, rel, "\u2a7e", "\\geqslant");
+defineSymbol(math, ams, rel, "\u2a96", "\\eqslantgtr");
+defineSymbol(math, ams, rel, "\u2273", "\\gtrsim");
+defineSymbol(math, ams, rel, "\u2a86", "\\gtrapprox");
+defineSymbol(math, ams, bin, "\u22d7", "\\gtrdot");
+defineSymbol(math, ams, rel, "\u22d9", "\\ggg");
+defineSymbol(math, ams, rel, "\u2277", "\\gtrless");
+defineSymbol(math, ams, rel, "\u22db", "\\gtreqless");
+defineSymbol(math, ams, rel, "\u2a8c", "\\gtreqqless");
+defineSymbol(math, ams, rel, "\u2256", "\\eqcirc");
+defineSymbol(math, ams, rel, "\u2257", "\\circeq");
+defineSymbol(math, ams, rel, "\u225c", "\\triangleq");
+defineSymbol(math, ams, rel, "\u223c", "\\thicksim");
+defineSymbol(math, ams, rel, "\u2248", "\\thickapprox");
+defineSymbol(math, ams, rel, "\u2ac6", "\\supseteqq");
+defineSymbol(math, ams, rel, "\u22d1", "\\Supset");
+defineSymbol(math, ams, rel, "\u2290", "\\sqsupset");
+defineSymbol(math, ams, rel, "\u227d", "\\succcurlyeq");
+defineSymbol(math, ams, rel, "\u22df", "\\curlyeqsucc");
+defineSymbol(math, ams, rel, "\u227f", "\\succsim");
+defineSymbol(math, ams, rel, "\u2ab8", "\\succapprox");
+defineSymbol(math, ams, rel, "\u22b3", "\\vartriangleright");
+defineSymbol(math, ams, rel, "\u22b5", "\\trianglerighteq");
+defineSymbol(math, ams, rel, "\u22a9", "\\Vdash");
+defineSymbol(math, ams, rel, "\u2223", "\\shortmid");
+defineSymbol(math, ams, rel, "\u2225", "\\shortparallel");
+defineSymbol(math, ams, rel, "\u226c", "\\between");
+defineSymbol(math, ams, rel, "\u22d4", "\\pitchfork");
+defineSymbol(math, ams, rel, "\u221d", "\\varpropto");
+defineSymbol(math, ams, rel, "\u25c0", "\\blacktriangleleft");
+defineSymbol(math, ams, rel, "\u2234", "\\therefore");
+defineSymbol(math, ams, rel, "\u220d", "\\backepsilon");
+defineSymbol(math, ams, rel, "\u25b6", "\\blacktriangleright");
+defineSymbol(math, ams, rel, "\u2235", "\\because");
+defineSymbol(math, ams, rel, "\u22d8", "\\llless");
+defineSymbol(math, ams, rel, "\u22d9", "\\gggtr");
+defineSymbol(math, ams, bin, "\u22b2", "\\lhd");
+defineSymbol(math, ams, bin, "\u22b3", "\\rhd");
+defineSymbol(math, ams, rel, "\u2242", "\\eqsim");
+defineSymbol(math, main, rel, "\u22c8", "\\Join");
+defineSymbol(math, ams, rel, "\u2251", "\\Doteq");
+
+// AMS Binary Operators
+defineSymbol(math, ams, bin, "\u2214", "\\dotplus");
+defineSymbol(math, ams, bin, "\u2216", "\\smallsetminus");
+defineSymbol(math, ams, bin, "\u22d2", "\\Cap");
+defineSymbol(math, ams, bin, "\u22d3", "\\Cup");
+defineSymbol(math, ams, bin, "\u2a5e", "\\doublebarwedge");
+defineSymbol(math, ams, bin, "\u229f", "\\boxminus");
+defineSymbol(math, ams, bin, "\u229e", "\\boxplus");
+defineSymbol(math, ams, bin, "\u22c7", "\\divideontimes");
+defineSymbol(math, ams, bin, "\u22c9", "\\ltimes");
+defineSymbol(math, ams, bin, "\u22ca", "\\rtimes");
+defineSymbol(math, ams, bin, "\u22cb", "\\leftthreetimes");
+defineSymbol(math, ams, bin, "\u22cc", "\\rightthreetimes");
+defineSymbol(math, ams, bin, "\u22cf", "\\curlywedge");
+defineSymbol(math, ams, bin, "\u22ce", "\\curlyvee");
+defineSymbol(math, ams, bin, "\u229d", "\\circleddash");
+defineSymbol(math, ams, bin, "\u229b", "\\circledast");
+defineSymbol(math, ams, bin, "\u22c5", "\\centerdot");
+defineSymbol(math, ams, bin, "\u22ba", "\\intercal");
+defineSymbol(math, ams, bin, "\u22d2", "\\doublecap");
+defineSymbol(math, ams, bin, "\u22d3", "\\doublecup");
+defineSymbol(math, ams, bin, "\u22a0", "\\boxtimes");
+
+// AMS Arrows
+defineSymbol(math, ams, rel, "\u21e2", "\\dashrightarrow");
+defineSymbol(math, ams, rel, "\u21e0", "\\dashleftarrow");
+defineSymbol(math, ams, rel, "\u21c7", "\\leftleftarrows");
+defineSymbol(math, ams, rel, "\u21c6", "\\leftrightarrows");
+defineSymbol(math, ams, rel, "\u21da", "\\Lleftarrow");
+defineSymbol(math, ams, rel, "\u219e", "\\twoheadleftarrow");
+defineSymbol(math, ams, rel, "\u21a2", "\\leftarrowtail");
+defineSymbol(math, ams, rel, "\u21ab", "\\looparrowleft");
+defineSymbol(math, ams, rel, "\u21cb", "\\leftrightharpoons");
+defineSymbol(math, ams, rel, "\u21b6", "\\curvearrowleft");
+defineSymbol(math, ams, rel, "\u21ba", "\\circlearrowleft");
+defineSymbol(math, ams, rel, "\u21b0", "\\Lsh");
+defineSymbol(math, ams, rel, "\u21c8", "\\upuparrows");
+defineSymbol(math, ams, rel, "\u21bf", "\\upharpoonleft");
+defineSymbol(math, ams, rel, "\u21c3", "\\downharpoonleft");
+defineSymbol(math, ams, rel, "\u22b8", "\\multimap");
+defineSymbol(math, ams, rel, "\u21ad", "\\leftrightsquigarrow");
+defineSymbol(math, ams, rel, "\u21c9", "\\rightrightarrows");
+defineSymbol(math, ams, rel, "\u21c4", "\\rightleftarrows");
+defineSymbol(math, ams, rel, "\u21a0", "\\twoheadrightarrow");
+defineSymbol(math, ams, rel, "\u21a3", "\\rightarrowtail");
+defineSymbol(math, ams, rel, "\u21ac", "\\looparrowright");
+defineSymbol(math, ams, rel, "\u21b7", "\\curvearrowright");
+defineSymbol(math, ams, rel, "\u21bb", "\\circlearrowright");
+defineSymbol(math, ams, rel, "\u21b1", "\\Rsh");
+defineSymbol(math, ams, rel, "\u21ca", "\\downdownarrows");
+defineSymbol(math, ams, rel, "\u21be", "\\upharpoonright");
+defineSymbol(math, ams, rel, "\u21c2", "\\downharpoonright");
+defineSymbol(math, ams, rel, "\u21dd", "\\rightsquigarrow");
+defineSymbol(math, ams, rel, "\u21dd", "\\leadsto");
+defineSymbol(math, ams, rel, "\u21db", "\\Rrightarrow");
+defineSymbol(math, ams, rel, "\u21be", "\\restriction");
+
+defineSymbol(math, main, textord, "\u2018", "`");
+defineSymbol(math, main, textord, "$", "\\$");
+defineSymbol(math, main, textord, "%", "\\%");
+defineSymbol(math, main, textord, "_", "\\_");
+defineSymbol(math, main, textord, "\u2220", "\\angle");
+defineSymbol(math, main, textord, "\u221e", "\\infty");
+defineSymbol(math, main, textord, "\u2032", "\\prime");
+defineSymbol(math, main, textord, "\u25b3", "\\triangle");
+defineSymbol(math, main, textord, "\u0393", "\\Gamma");
+defineSymbol(math, main, textord, "\u0394", "\\Delta");
+defineSymbol(math, main, textord, "\u0398", "\\Theta");
+defineSymbol(math, main, textord, "\u039b", "\\Lambda");
+defineSymbol(math, main, textord, "\u039e", "\\Xi");
+defineSymbol(math, main, textord, "\u03a0", "\\Pi");
+defineSymbol(math, main, textord, "\u03a3", "\\Sigma");
+defineSymbol(math, main, textord, "\u03a5", "\\Upsilon");
+defineSymbol(math, main, textord, "\u03a6", "\\Phi");
+defineSymbol(math, main, textord, "\u03a8", "\\Psi");
+defineSymbol(math, main, textord, "\u03a9", "\\Omega");
+defineSymbol(math, main, textord, "\u00ac", "\\neg");
+defineSymbol(math, main, textord, "\u00ac", "\\lnot");
+defineSymbol(math, main, textord, "\u22a4", "\\top");
+defineSymbol(math, main, textord, "\u22a5", "\\bot");
+defineSymbol(math, main, textord, "\u2205", "\\emptyset");
+defineSymbol(math, ams, textord, "\u2205", "\\varnothing");
+defineSymbol(math, main, mathord, "\u03b1", "\\alpha");
+defineSymbol(math, main, mathord, "\u03b2", "\\beta");
+defineSymbol(math, main, mathord, "\u03b3", "\\gamma");
+defineSymbol(math, main, mathord, "\u03b4", "\\delta");
+defineSymbol(math, main, mathord, "\u03f5", "\\epsilon");
+defineSymbol(math, main, mathord, "\u03b6", "\\zeta");
+defineSymbol(math, main, mathord, "\u03b7", "\\eta");
+defineSymbol(math, main, mathord, "\u03b8", "\\theta");
+defineSymbol(math, main, mathord, "\u03b9", "\\iota");
+defineSymbol(math, main, mathord, "\u03ba", "\\kappa");
+defineSymbol(math, main, mathord, "\u03bb", "\\lambda");
+defineSymbol(math, main, mathord, "\u03bc", "\\mu");
+defineSymbol(math, main, mathord, "\u03bd", "\\nu");
+defineSymbol(math, main, mathord, "\u03be", "\\xi");
+defineSymbol(math, main, mathord, "o", "\\omicron");
+defineSymbol(math, main, mathord, "\u03c0", "\\pi");
+defineSymbol(math, main, mathord, "\u03c1", "\\rho");
+defineSymbol(math, main, mathord, "\u03c3", "\\sigma");
+defineSymbol(math, main, mathord, "\u03c4", "\\tau");
+defineSymbol(math, main, mathord, "\u03c5", "\\upsilon");
+defineSymbol(math, main, mathord, "\u03d5", "\\phi");
+defineSymbol(math, main, mathord, "\u03c7", "\\chi");
+defineSymbol(math, main, mathord, "\u03c8", "\\psi");
+defineSymbol(math, main, mathord, "\u03c9", "\\omega");
+defineSymbol(math, main, mathord, "\u03b5", "\\varepsilon");
+defineSymbol(math, main, mathord, "\u03d1", "\\vartheta");
+defineSymbol(math, main, mathord, "\u03d6", "\\varpi");
+defineSymbol(math, main, mathord, "\u03f1", "\\varrho");
+defineSymbol(math, main, mathord, "\u03c2", "\\varsigma");
+defineSymbol(math, main, mathord, "\u03c6", "\\varphi");
+defineSymbol(math, main, bin, "\u2217", "*");
+defineSymbol(math, main, bin, "+", "+");
+defineSymbol(math, main, bin, "\u2212", "-");
+defineSymbol(math, main, bin, "\u22c5", "\\cdot");
+defineSymbol(math, main, bin, "\u2218", "\\circ");
+defineSymbol(math, main, bin, "\u00f7", "\\div");
+defineSymbol(math, main, bin, "\u00b1", "\\pm");
+defineSymbol(math, main, bin, "\u00d7", "\\times");
+defineSymbol(math, main, bin, "\u2229", "\\cap");
+defineSymbol(math, main, bin, "\u222a", "\\cup");
+defineSymbol(math, main, bin, "\u2216", "\\setminus");
+defineSymbol(math, main, bin, "\u2227", "\\land");
+defineSymbol(math, main, bin, "\u2228", "\\lor");
+defineSymbol(math, main, bin, "\u2227", "\\wedge");
+defineSymbol(math, main, bin, "\u2228", "\\vee");
+defineSymbol(math, main, textord, "\u221a", "\\surd");
+defineSymbol(math, main, open, "(", "(");
+defineSymbol(math, main, open, "[", "[");
+defineSymbol(math, main, open, "\u27e8", "\\langle");
+defineSymbol(math, main, open, "\u2223", "\\lvert");
+defineSymbol(math, main, open, "\u2225", "\\lVert");
+defineSymbol(math, main, close, ")", ")");
+defineSymbol(math, main, close, "]", "]");
+defineSymbol(math, main, close, "?", "?");
+defineSymbol(math, main, close, "!", "!");
+defineSymbol(math, main, close, "\u27e9", "\\rangle");
+defineSymbol(math, main, close, "\u2223", "\\rvert");
+defineSymbol(math, main, close, "\u2225", "\\rVert");
+defineSymbol(math, main, rel, "=", "=");
+defineSymbol(math, main, rel, "<", "<");
+defineSymbol(math, main, rel, ">", ">");
+defineSymbol(math, main, rel, ":", ":");
+defineSymbol(math, main, rel, "\u2248", "\\approx");
+defineSymbol(math, main, rel, "\u2245", "\\cong");
+defineSymbol(math, main, rel, "\u2265", "\\ge");
+defineSymbol(math, main, rel, "\u2265", "\\geq");
+defineSymbol(math, main, rel, "\u2190", "\\gets");
+defineSymbol(math, main, rel, ">", "\\gt");
+defineSymbol(math, main, rel, "\u2208", "\\in");
+defineSymbol(math, main, rel, "\u2209", "\\notin");
+defineSymbol(math, main, rel, "\u2282", "\\subset");
+defineSymbol(math, main, rel, "\u2283", "\\supset");
+defineSymbol(math, main, rel, "\u2286", "\\subseteq");
+defineSymbol(math, main, rel, "\u2287", "\\supseteq");
+defineSymbol(math, ams, rel, "\u2288", "\\nsubseteq");
+defineSymbol(math, ams, rel, "\u2289", "\\nsupseteq");
+defineSymbol(math, main, rel, "\u22a8", "\\models");
+defineSymbol(math, main, rel, "\u2190", "\\leftarrow");
+defineSymbol(math, main, rel, "\u2264", "\\le");
+defineSymbol(math, main, rel, "\u2264", "\\leq");
+defineSymbol(math, main, rel, "<", "\\lt");
+defineSymbol(math, main, rel, "\u2260", "\\ne");
+defineSymbol(math, main, rel, "\u2260", "\\neq");
+defineSymbol(math, main, rel, "\u2192", "\\rightarrow");
+defineSymbol(math, main, rel, "\u2192", "\\to");
+defineSymbol(math, ams, rel, "\u2271", "\\ngeq");
+defineSymbol(math, ams, rel, "\u2270", "\\nleq");
+defineSymbol(math, main, spacing, null, "\\!");
+defineSymbol(math, main, spacing, "\u00a0", "\\ ");
+defineSymbol(math, main, spacing, "\u00a0", "~");
+defineSymbol(math, main, spacing, null, "\\,");
+defineSymbol(math, main, spacing, null, "\\:");
+defineSymbol(math, main, spacing, null, "\\;");
+defineSymbol(math, main, spacing, null, "\\enspace");
+defineSymbol(math, main, spacing, null, "\\qquad");
+defineSymbol(math, main, spacing, null, "\\quad");
+defineSymbol(math, main, spacing, "\u00a0", "\\space");
+defineSymbol(math, main, punct, ",", ",");
+defineSymbol(math, main, punct, ";", ";");
+defineSymbol(math, main, punct, ":", "\\colon");
+defineSymbol(math, ams, bin, "\u22bc", "\\barwedge");
+defineSymbol(math, ams, bin, "\u22bb", "\\veebar");
+defineSymbol(math, main, bin, "\u2299", "\\odot");
+defineSymbol(math, main, bin, "\u2295", "\\oplus");
+defineSymbol(math, main, bin, "\u2297", "\\otimes");
+defineSymbol(math, main, textord, "\u2202", "\\partial");
+defineSymbol(math, main, bin, "\u2298", "\\oslash");
+defineSymbol(math, ams, bin, "\u229a", "\\circledcirc");
+defineSymbol(math, ams, bin, "\u22a1", "\\boxdot");
+defineSymbol(math, main, bin, "\u25b3", "\\bigtriangleup");
+defineSymbol(math, main, bin, "\u25bd", "\\bigtriangledown");
+defineSymbol(math, main, bin, "\u2020", "\\dagger");
+defineSymbol(math, main, bin, "\u22c4", "\\diamond");
+defineSymbol(math, main, bin, "\u22c6", "\\star");
+defineSymbol(math, main, bin, "\u25c3", "\\triangleleft");
+defineSymbol(math, main, bin, "\u25b9", "\\triangleright");
+defineSymbol(math, main, open, "{", "\\{");
+defineSymbol(math, main, close, "}", "\\}");
+defineSymbol(math, main, open, "{", "\\lbrace");
+defineSymbol(math, main, close, "}", "\\rbrace");
+defineSymbol(math, main, open, "[", "\\lbrack");
+defineSymbol(math, main, close, "]", "\\rbrack");
+defineSymbol(math, main, open, "\u230a", "\\lfloor");
+defineSymbol(math, main, close, "\u230b", "\\rfloor");
+defineSymbol(math, main, open, "\u2308", "\\lceil");
+defineSymbol(math, main, close, "\u2309", "\\rceil");
+defineSymbol(math, main, textord, "\\", "\\backslash");
+defineSymbol(math, main, textord, "\u2223", "|");
+defineSymbol(math, main, textord, "\u2223", "\\vert");
+defineSymbol(math, main, textord, "\u2225", "\\|");
+defineSymbol(math, main, textord, "\u2225", "\\Vert");
+defineSymbol(math, main, rel, "\u2191", "\\uparrow");
+defineSymbol(math, main, rel, "\u21d1", "\\Uparrow");
+defineSymbol(math, main, rel, "\u2193", "\\downarrow");
+defineSymbol(math, main, rel, "\u21d3", "\\Downarrow");
+defineSymbol(math, main, rel, "\u2195", "\\updownarrow");
+defineSymbol(math, main, rel, "\u21d5", "\\Updownarrow");
+defineSymbol(math, math, op, "\u2210", "\\coprod");
+defineSymbol(math, math, op, "\u22c1", "\\bigvee");
+defineSymbol(math, math, op, "\u22c0", "\\bigwedge");
+defineSymbol(math, math, op, "\u2a04", "\\biguplus");
+defineSymbol(math, math, op, "\u22c2", "\\bigcap");
+defineSymbol(math, math, op, "\u22c3", "\\bigcup");
+defineSymbol(math, math, op, "\u222b", "\\int");
+defineSymbol(math, math, op, "\u222b", "\\intop");
+defineSymbol(math, math, op, "\u222c", "\\iint");
+defineSymbol(math, math, op, "\u222d", "\\iiint");
+defineSymbol(math, math, op, "\u220f", "\\prod");
+defineSymbol(math, math, op, "\u2211", "\\sum");
+defineSymbol(math, math, op, "\u2a02", "\\bigotimes");
+defineSymbol(math, math, op, "\u2a01", "\\bigoplus");
+defineSymbol(math, math, op, "\u2a00", "\\bigodot");
+defineSymbol(math, math, op, "\u222e", "\\oint");
+defineSymbol(math, math, op, "\u2a06", "\\bigsqcup");
+defineSymbol(math, math, op, "\u222b", "\\smallint");
+defineSymbol(math, main, inner, "\u2026", "\\ldots");
+defineSymbol(math, main, inner, "\u22ef", "\\cdots");
+defineSymbol(math, main, inner, "\u22f1", "\\ddots");
+defineSymbol(math, main, textord, "\u22ee", "\\vdots");
+defineSymbol(math, main, accent, "\u00b4", "\\acute");
+defineSymbol(math, main, accent, "\u0060", "\\grave");
+defineSymbol(math, main, accent, "\u00a8", "\\ddot");
+defineSymbol(math, main, accent, "\u007e", "\\tilde");
+defineSymbol(math, main, accent, "\u00af", "\\bar");
+defineSymbol(math, main, accent, "\u02d8", "\\breve");
+defineSymbol(math, main, accent, "\u02c7", "\\check");
+defineSymbol(math, main, accent, "\u005e", "\\hat");
+defineSymbol(math, main, accent, "\u20d7", "\\vec");
+defineSymbol(math, main, accent, "\u02d9", "\\dot");
+defineSymbol(math, main, mathord, "\u0131", "\\imath");
+defineSymbol(math, main, mathord, "\u0237", "\\jmath");
+
+defineSymbol(text, main, textord, "\u2013", "--");
+defineSymbol(text, main, textord, "\u2014", "---");
+defineSymbol(text, main, textord, "\u2018", "`");
+defineSymbol(text, main, textord, "\u2019", "'");
+defineSymbol(text, main, textord, "\u201c", "``");
+defineSymbol(text, main, textord, "\u201d", "''");
+defineSymbol(math, main, textord, "\u00b0", "\\degree");
+defineSymbol(text, main, textord, "\u00b0", "\\degree");
+defineSymbol(math, main, mathord, "\u00a3", "\\pounds");
+defineSymbol(math, ams, textord, "\u2720", "\\maltese");
+defineSymbol(text, ams, textord, "\u2720", "\\maltese");
+
+defineSymbol(text, main, spacing, "\u00a0", "\\ ");
+defineSymbol(text, main, spacing, "\u00a0", " ");
+defineSymbol(text, main, spacing, "\u00a0", "~");
+
+// There are lots of symbols which are the same, so we add them in afterwards.
+var i;
+var ch;
+
+// All of these are textords in math mode
+var mathTextSymbols = "0123456789/@.\"";
+for (i = 0; i < mathTextSymbols.length; i++) {
+ ch = mathTextSymbols.charAt(i);
+ defineSymbol(math, main, textord, ch, ch);
+}
+
+// All of these are textords in text mode
+var textSymbols = "0123456789!@*()-=+[]\";:?/.,";
+for (i = 0; i < textSymbols.length; i++) {
+ ch = textSymbols.charAt(i);
+ defineSymbol(text, main, textord, ch, ch);
+}
+
+// All of these are textords in text mode, and mathords in math mode
+var letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+for (i = 0; i < letters.length; i++) {
+ ch = letters.charAt(i);
+ defineSymbol(math, main, mathord, ch, ch);
+ defineSymbol(text, main, textord, ch, ch);
+}
+
+// Latin-1 letters
+for (i = 0x00C0; i <= 0x00D6; i++) {
+ ch = String.fromCharCode(i);
+ defineSymbol(text, main, textord, ch, ch);
+}
+
+for (i = 0x00D8; i <= 0x00F6; i++) {
+ ch = String.fromCharCode(i);
+ defineSymbol(text, main, textord, ch, ch);
+}
+
+for (i = 0x00F8; i <= 0x00FF; i++) {
+ ch = String.fromCharCode(i);
+ defineSymbol(text, main, textord, ch, ch);
+}
+
+// Cyrillic
+for (i = 0x0410; i <= 0x044F; i++) {
+ ch = String.fromCharCode(i);
+ defineSymbol(text, main, textord, ch, ch);
+}
+
+},{}],24:[function(require,module,exports){
+var hangulRegex = /[\uAC00-\uD7AF]/;
+
+// This regex combines
+// - Hiragana: [\u3040-\u309F]
+// - Katakana: [\u30A0-\u30FF]
+// - CJK ideograms: [\u4E00-\u9FAF]
+// - Hangul syllables: [\uAC00-\uD7AF]
+// Notably missing are halfwidth Katakana and Romanji glyphs.
+var cjkRegex =
+ /[\u3040-\u309F]|[\u30A0-\u30FF]|[\u4E00-\u9FAF]|[\uAC00-\uD7AF]/;
+
+module.exports = {
+ cjkRegex: cjkRegex,
+ hangulRegex: hangulRegex,
+};
+
+},{}],25:[function(require,module,exports){
+/**
+ * This file contains a list of utility functions which are useful in other
+ * files.
+ */
+
+/**
+ * Provide an `indexOf` function which works in IE8, but defers to native if
+ * possible.
+ */
+var nativeIndexOf = Array.prototype.indexOf;
+var indexOf = function(list, elem) {
+ if (list == null) {
+ return -1;
+ }
+ if (nativeIndexOf && list.indexOf === nativeIndexOf) {
+ return list.indexOf(elem);
+ }
+ var i = 0;
+ var l = list.length;
+ for (; i < l; i++) {
+ if (list[i] === elem) {
+ return i;
+ }
+ }
+ return -1;
+};
+
+/**
+ * Return whether an element is contained in a list
+ */
+var contains = function(list, elem) {
+ return indexOf(list, elem) !== -1;
+};
+
+/**
+ * Provide a default value if a setting is undefined
+ */
+var deflt = function(setting, defaultIfUndefined) {
+ return setting === undefined ? defaultIfUndefined : setting;
+};
+
+// hyphenate and escape adapted from Facebook's React under Apache 2 license
+
+var uppercase = /([A-Z])/g;
+var hyphenate = function(str) {
+ return str.replace(uppercase, "-$1").toLowerCase();
+};
+
+var ESCAPE_LOOKUP = {
+ "&": "&amp;",
+ ">": "&gt;",
+ "<": "&lt;",
+ "\"": "&quot;",
+ "'": "&#x27;",
+};
+
+var ESCAPE_REGEX = /[&><"']/g;
+
+function escaper(match) {
+ return ESCAPE_LOOKUP[match];
+}
+
+/**
+ * Escapes text to prevent scripting attacks.
+ *
+ * @param {*} text Text value to escape.
+ * @return {string} An escaped string.
+ */
+function escape(text) {
+ return ("" + text).replace(ESCAPE_REGEX, escaper);
+}
+
+/**
+ * A function to set the text content of a DOM element in all supported
+ * browsers. Note that we don't define this if there is no document.
+ */
+var setTextContent;
+if (typeof document !== "undefined") {
+ var testNode = document.createElement("span");
+ if ("textContent" in testNode) {
+ setTextContent = function(node, text) {
+ node.textContent = text;
+ };
+ } else {
+ setTextContent = function(node, text) {
+ node.innerText = text;
+ };
+ }
+}
+
+/**
+ * A function to clear a node.
+ */
+function clearNode(node) {
+ setTextContent(node, "");
+}
+
+module.exports = {
+ contains: contains,
+ deflt: deflt,
+ escape: escape,
+ hyphenate: hyphenate,
+ indexOf: indexOf,
+ setTextContent: setTextContent,
+ clearNode: clearNode,
+};
+
+},{}]},{},[1])(1)
+}); \ No newline at end of file
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.css
new file mode 100644
index 00000000000..3e62df2329c
--- /dev/null
+++ b/vendor/assets/stylesheets/katex.css
@@ -0,0 +1,975 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) 2015 Khan Academy
+
+This software also uses portions of the underscore.js project, which is
+MIT licensed with the following copyright:
+
+Copyright (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative
+Reporters & Editors
+
+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.
+*/
+
+/*
+ Here is how to build a version of KaTeX that works with gitlab.
+
+ The problem is that the standard procedure for changing font location doesn't work for the empty string.
+
+ 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.
+*/
+
+@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');
+ 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');
+ 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');
+ 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');
+ 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');
+ 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');
+ 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');
+ 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');
+ 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');
+ 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');
+ 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');
+ 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');
+ 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');
+ 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');
+ 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');
+ 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');
+ font-weight: normal;
+ font-style: normal;
+}
+.katex-display {
+ display: block;
+ margin: 1em 0;
+ text-align: center;
+}
+.katex-display > .katex {
+ display: inline-block;
+ text-align: initial;
+}
+.katex {
+ font: normal 1.21em KaTeX_Main, Times New Roman, serif;
+ line-height: 1.2;
+ white-space: nowrap;
+ text-indent: 0;
+}
+.katex .katex-html {
+ display: inline-block;
+}
+.katex .katex-mathml {
+ position: absolute;
+ clip: rect(1px, 1px, 1px, 1px);
+ padding: 0;
+ border: 0;
+ height: 1px;
+ width: 1px;
+ overflow: hidden;
+}
+.katex .base {
+ display: inline-block;
+}
+.katex .strut {
+ display: inline-block;
+}
+.katex .mathit {
+ font-family: KaTeX_Math;
+ font-style: italic;
+}
+.katex .mathbf {
+ font-family: KaTeX_Main;
+ font-weight: bold;
+}
+.katex .amsrm {
+ font-family: KaTeX_AMS;
+}
+.katex .mathbb {
+ font-family: KaTeX_AMS;
+}
+.katex .mathcal {
+ font-family: KaTeX_Caligraphic;
+}
+.katex .mathfrak {
+ font-family: KaTeX_Fraktur;
+}
+.katex .mathtt {
+ font-family: KaTeX_Typewriter;
+}
+.katex .mathscr {
+ font-family: KaTeX_Script;
+}
+.katex .mathsf {
+ font-family: KaTeX_SansSerif;
+}
+.katex .mainit {
+ font-family: KaTeX_Main;
+ font-style: italic;
+}
+.katex .textstyle > .mord + .mop {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mord + .mbin {
+ margin-left: 0.22222em;
+}
+.katex .textstyle > .mord + .mrel {
+ margin-left: 0.27778em;
+}
+.katex .textstyle > .mord + .minner {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mop + .mord {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mop + .mop {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mop + .mrel {
+ margin-left: 0.27778em;
+}
+.katex .textstyle > .mop + .minner {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mbin + .mord {
+ margin-left: 0.22222em;
+}
+.katex .textstyle > .mbin + .mop {
+ margin-left: 0.22222em;
+}
+.katex .textstyle > .mbin + .mopen {
+ margin-left: 0.22222em;
+}
+.katex .textstyle > .mbin + .minner {
+ margin-left: 0.22222em;
+}
+.katex .textstyle > .mrel + .mord {
+ margin-left: 0.27778em;
+}
+.katex .textstyle > .mrel + .mop {
+ margin-left: 0.27778em;
+}
+.katex .textstyle > .mrel + .mopen {
+ margin-left: 0.27778em;
+}
+.katex .textstyle > .mrel + .minner {
+ margin-left: 0.27778em;
+}
+.katex .textstyle > .mclose + .mop {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mclose + .mbin {
+ margin-left: 0.22222em;
+}
+.katex .textstyle > .mclose + .mrel {
+ margin-left: 0.27778em;
+}
+.katex .textstyle > .mclose + .minner {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .mord {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .mop {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .mrel {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .mopen {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .mclose {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .mpunct {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .minner {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .minner + .mord {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .minner + .mop {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .minner + .mbin {
+ margin-left: 0.22222em;
+}
+.katex .textstyle > .minner + .mrel {
+ margin-left: 0.27778em;
+}
+.katex .textstyle > .minner + .mopen {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .minner + .mpunct {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .minner + .minner {
+ margin-left: 0.16667em;
+}
+.katex .mord + .mop {
+ margin-left: 0.16667em;
+}
+.katex .mop + .mord {
+ margin-left: 0.16667em;
+}
+.katex .mop + .mop {
+ margin-left: 0.16667em;
+}
+.katex .mclose + .mop {
+ margin-left: 0.16667em;
+}
+.katex .minner + .mop {
+ margin-left: 0.16667em;
+}
+.katex .reset-textstyle.textstyle {
+ font-size: 1em;
+}
+.katex .reset-textstyle.scriptstyle {
+ font-size: 0.7em;
+}
+.katex .reset-textstyle.scriptscriptstyle {
+ font-size: 0.5em;
+}
+.katex .reset-scriptstyle.textstyle {
+ font-size: 1.42857em;
+}
+.katex .reset-scriptstyle.scriptstyle {
+ font-size: 1em;
+}
+.katex .reset-scriptstyle.scriptscriptstyle {
+ font-size: 0.71429em;
+}
+.katex .reset-scriptscriptstyle.textstyle {
+ font-size: 2em;
+}
+.katex .reset-scriptscriptstyle.scriptstyle {
+ font-size: 1.4em;
+}
+.katex .reset-scriptscriptstyle.scriptscriptstyle {
+ font-size: 1em;
+}
+.katex .style-wrap {
+ position: relative;
+}
+.katex .vlist {
+ display: inline-block;
+}
+.katex .vlist > span {
+ display: block;
+ height: 0;
+ position: relative;
+}
+.katex .vlist > span > span {
+ display: inline-block;
+}
+.katex .vlist .baseline-fix {
+ display: inline-table;
+ table-layout: fixed;
+}
+.katex .msupsub {
+ text-align: left;
+}
+.katex .mfrac > span > span {
+ text-align: center;
+}
+.katex .mfrac .frac-line {
+ width: 100%;
+}
+.katex .mfrac .frac-line:before {
+ border-bottom-style: solid;
+ border-bottom-width: 1px;
+ content: "";
+ display: block;
+}
+.katex .mfrac .frac-line:after {
+ border-bottom-style: solid;
+ border-bottom-width: 0.04em;
+ content: "";
+ display: block;
+ margin-top: -1px;
+}
+.katex .mspace {
+ display: inline-block;
+}
+.katex .mspace.negativethinspace {
+ margin-left: -0.16667em;
+}
+.katex .mspace.thinspace {
+ width: 0.16667em;
+}
+.katex .mspace.mediumspace {
+ width: 0.22222em;
+}
+.katex .mspace.thickspace {
+ width: 0.27778em;
+}
+.katex .mspace.enspace {
+ width: 0.5em;
+}
+.katex .mspace.quad {
+ width: 1em;
+}
+.katex .mspace.qquad {
+ width: 2em;
+}
+.katex .llap,
+.katex .rlap {
+ width: 0;
+ position: relative;
+}
+.katex .llap > .inner,
+.katex .rlap > .inner {
+ position: absolute;
+}
+.katex .llap > .fix,
+.katex .rlap > .fix {
+ display: inline-block;
+}
+.katex .llap > .inner {
+ right: 0;
+}
+.katex .rlap > .inner {
+ left: 0;
+}
+.katex .katex-logo .a {
+ font-size: 0.75em;
+ margin-left: -0.32em;
+ position: relative;
+ top: -0.2em;
+}
+.katex .katex-logo .t {
+ margin-left: -0.23em;
+}
+.katex .katex-logo .e {
+ margin-left: -0.1667em;
+ position: relative;
+ top: 0.2155em;
+}
+.katex .katex-logo .x {
+ margin-left: -0.125em;
+}
+.katex .rule {
+ display: inline-block;
+ border: solid 0;
+ position: relative;
+}
+.katex .overline .overline-line,
+.katex .underline .underline-line {
+ width: 100%;
+}
+.katex .overline .overline-line:before,
+.katex .underline .underline-line:before {
+ border-bottom-style: solid;
+ border-bottom-width: 1px;
+ content: "";
+ display: block;
+}
+.katex .overline .overline-line:after,
+.katex .underline .underline-line:after {
+ border-bottom-style: solid;
+ border-bottom-width: 0.04em;
+ content: "";
+ display: block;
+ margin-top: -1px;
+}
+.katex .sqrt > .sqrt-sign {
+ position: relative;
+}
+.katex .sqrt .sqrt-line {
+ width: 100%;
+}
+.katex .sqrt .sqrt-line:before {
+ border-bottom-style: solid;
+ border-bottom-width: 1px;
+ content: "";
+ display: block;
+}
+.katex .sqrt .sqrt-line:after {
+ border-bottom-style: solid;
+ border-bottom-width: 0.04em;
+ content: "";
+ display: block;
+ margin-top: -1px;
+}
+.katex .sqrt > .root {
+ margin-left: 0.27777778em;
+ margin-right: -0.55555556em;
+}
+.katex .sizing,
+.katex .fontsize-ensurer {
+ display: inline-block;
+}
+.katex .sizing.reset-size1.size1,
+.katex .fontsize-ensurer.reset-size1.size1 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size1.size2,
+.katex .fontsize-ensurer.reset-size1.size2 {
+ font-size: 1.4em;
+}
+.katex .sizing.reset-size1.size3,
+.katex .fontsize-ensurer.reset-size1.size3 {
+ font-size: 1.6em;
+}
+.katex .sizing.reset-size1.size4,
+.katex .fontsize-ensurer.reset-size1.size4 {
+ font-size: 1.8em;
+}
+.katex .sizing.reset-size1.size5,
+.katex .fontsize-ensurer.reset-size1.size5 {
+ font-size: 2em;
+}
+.katex .sizing.reset-size1.size6,
+.katex .fontsize-ensurer.reset-size1.size6 {
+ font-size: 2.4em;
+}
+.katex .sizing.reset-size1.size7,
+.katex .fontsize-ensurer.reset-size1.size7 {
+ font-size: 2.88em;
+}
+.katex .sizing.reset-size1.size8,
+.katex .fontsize-ensurer.reset-size1.size8 {
+ font-size: 3.46em;
+}
+.katex .sizing.reset-size1.size9,
+.katex .fontsize-ensurer.reset-size1.size9 {
+ font-size: 4.14em;
+}
+.katex .sizing.reset-size1.size10,
+.katex .fontsize-ensurer.reset-size1.size10 {
+ font-size: 4.98em;
+}
+.katex .sizing.reset-size2.size1,
+.katex .fontsize-ensurer.reset-size2.size1 {
+ font-size: 0.71428571em;
+}
+.katex .sizing.reset-size2.size2,
+.katex .fontsize-ensurer.reset-size2.size2 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size2.size3,
+.katex .fontsize-ensurer.reset-size2.size3 {
+ font-size: 1.14285714em;
+}
+.katex .sizing.reset-size2.size4,
+.katex .fontsize-ensurer.reset-size2.size4 {
+ font-size: 1.28571429em;
+}
+.katex .sizing.reset-size2.size5,
+.katex .fontsize-ensurer.reset-size2.size5 {
+ font-size: 1.42857143em;
+}
+.katex .sizing.reset-size2.size6,
+.katex .fontsize-ensurer.reset-size2.size6 {
+ font-size: 1.71428571em;
+}
+.katex .sizing.reset-size2.size7,
+.katex .fontsize-ensurer.reset-size2.size7 {
+ font-size: 2.05714286em;
+}
+.katex .sizing.reset-size2.size8,
+.katex .fontsize-ensurer.reset-size2.size8 {
+ font-size: 2.47142857em;
+}
+.katex .sizing.reset-size2.size9,
+.katex .fontsize-ensurer.reset-size2.size9 {
+ font-size: 2.95714286em;
+}
+.katex .sizing.reset-size2.size10,
+.katex .fontsize-ensurer.reset-size2.size10 {
+ font-size: 3.55714286em;
+}
+.katex .sizing.reset-size3.size1,
+.katex .fontsize-ensurer.reset-size3.size1 {
+ font-size: 0.625em;
+}
+.katex .sizing.reset-size3.size2,
+.katex .fontsize-ensurer.reset-size3.size2 {
+ font-size: 0.875em;
+}
+.katex .sizing.reset-size3.size3,
+.katex .fontsize-ensurer.reset-size3.size3 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size3.size4,
+.katex .fontsize-ensurer.reset-size3.size4 {
+ font-size: 1.125em;
+}
+.katex .sizing.reset-size3.size5,
+.katex .fontsize-ensurer.reset-size3.size5 {
+ font-size: 1.25em;
+}
+.katex .sizing.reset-size3.size6,
+.katex .fontsize-ensurer.reset-size3.size6 {
+ font-size: 1.5em;
+}
+.katex .sizing.reset-size3.size7,
+.katex .fontsize-ensurer.reset-size3.size7 {
+ font-size: 1.8em;
+}
+.katex .sizing.reset-size3.size8,
+.katex .fontsize-ensurer.reset-size3.size8 {
+ font-size: 2.1625em;
+}
+.katex .sizing.reset-size3.size9,
+.katex .fontsize-ensurer.reset-size3.size9 {
+ font-size: 2.5875em;
+}
+.katex .sizing.reset-size3.size10,
+.katex .fontsize-ensurer.reset-size3.size10 {
+ font-size: 3.1125em;
+}
+.katex .sizing.reset-size4.size1,
+.katex .fontsize-ensurer.reset-size4.size1 {
+ font-size: 0.55555556em;
+}
+.katex .sizing.reset-size4.size2,
+.katex .fontsize-ensurer.reset-size4.size2 {
+ font-size: 0.77777778em;
+}
+.katex .sizing.reset-size4.size3,
+.katex .fontsize-ensurer.reset-size4.size3 {
+ font-size: 0.88888889em;
+}
+.katex .sizing.reset-size4.size4,
+.katex .fontsize-ensurer.reset-size4.size4 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size4.size5,
+.katex .fontsize-ensurer.reset-size4.size5 {
+ font-size: 1.11111111em;
+}
+.katex .sizing.reset-size4.size6,
+.katex .fontsize-ensurer.reset-size4.size6 {
+ font-size: 1.33333333em;
+}
+.katex .sizing.reset-size4.size7,
+.katex .fontsize-ensurer.reset-size4.size7 {
+ font-size: 1.6em;
+}
+.katex .sizing.reset-size4.size8,
+.katex .fontsize-ensurer.reset-size4.size8 {
+ font-size: 1.92222222em;
+}
+.katex .sizing.reset-size4.size9,
+.katex .fontsize-ensurer.reset-size4.size9 {
+ font-size: 2.3em;
+}
+.katex .sizing.reset-size4.size10,
+.katex .fontsize-ensurer.reset-size4.size10 {
+ font-size: 2.76666667em;
+}
+.katex .sizing.reset-size5.size1,
+.katex .fontsize-ensurer.reset-size5.size1 {
+ font-size: 0.5em;
+}
+.katex .sizing.reset-size5.size2,
+.katex .fontsize-ensurer.reset-size5.size2 {
+ font-size: 0.7em;
+}
+.katex .sizing.reset-size5.size3,
+.katex .fontsize-ensurer.reset-size5.size3 {
+ font-size: 0.8em;
+}
+.katex .sizing.reset-size5.size4,
+.katex .fontsize-ensurer.reset-size5.size4 {
+ font-size: 0.9em;
+}
+.katex .sizing.reset-size5.size5,
+.katex .fontsize-ensurer.reset-size5.size5 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size5.size6,
+.katex .fontsize-ensurer.reset-size5.size6 {
+ font-size: 1.2em;
+}
+.katex .sizing.reset-size5.size7,
+.katex .fontsize-ensurer.reset-size5.size7 {
+ font-size: 1.44em;
+}
+.katex .sizing.reset-size5.size8,
+.katex .fontsize-ensurer.reset-size5.size8 {
+ font-size: 1.73em;
+}
+.katex .sizing.reset-size5.size9,
+.katex .fontsize-ensurer.reset-size5.size9 {
+ font-size: 2.07em;
+}
+.katex .sizing.reset-size5.size10,
+.katex .fontsize-ensurer.reset-size5.size10 {
+ font-size: 2.49em;
+}
+.katex .sizing.reset-size6.size1,
+.katex .fontsize-ensurer.reset-size6.size1 {
+ font-size: 0.41666667em;
+}
+.katex .sizing.reset-size6.size2,
+.katex .fontsize-ensurer.reset-size6.size2 {
+ font-size: 0.58333333em;
+}
+.katex .sizing.reset-size6.size3,
+.katex .fontsize-ensurer.reset-size6.size3 {
+ font-size: 0.66666667em;
+}
+.katex .sizing.reset-size6.size4,
+.katex .fontsize-ensurer.reset-size6.size4 {
+ font-size: 0.75em;
+}
+.katex .sizing.reset-size6.size5,
+.katex .fontsize-ensurer.reset-size6.size5 {
+ font-size: 0.83333333em;
+}
+.katex .sizing.reset-size6.size6,
+.katex .fontsize-ensurer.reset-size6.size6 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size6.size7,
+.katex .fontsize-ensurer.reset-size6.size7 {
+ font-size: 1.2em;
+}
+.katex .sizing.reset-size6.size8,
+.katex .fontsize-ensurer.reset-size6.size8 {
+ font-size: 1.44166667em;
+}
+.katex .sizing.reset-size6.size9,
+.katex .fontsize-ensurer.reset-size6.size9 {
+ font-size: 1.725em;
+}
+.katex .sizing.reset-size6.size10,
+.katex .fontsize-ensurer.reset-size6.size10 {
+ font-size: 2.075em;
+}
+.katex .sizing.reset-size7.size1,
+.katex .fontsize-ensurer.reset-size7.size1 {
+ font-size: 0.34722222em;
+}
+.katex .sizing.reset-size7.size2,
+.katex .fontsize-ensurer.reset-size7.size2 {
+ font-size: 0.48611111em;
+}
+.katex .sizing.reset-size7.size3,
+.katex .fontsize-ensurer.reset-size7.size3 {
+ font-size: 0.55555556em;
+}
+.katex .sizing.reset-size7.size4,
+.katex .fontsize-ensurer.reset-size7.size4 {
+ font-size: 0.625em;
+}
+.katex .sizing.reset-size7.size5,
+.katex .fontsize-ensurer.reset-size7.size5 {
+ font-size: 0.69444444em;
+}
+.katex .sizing.reset-size7.size6,
+.katex .fontsize-ensurer.reset-size7.size6 {
+ font-size: 0.83333333em;
+}
+.katex .sizing.reset-size7.size7,
+.katex .fontsize-ensurer.reset-size7.size7 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size7.size8,
+.katex .fontsize-ensurer.reset-size7.size8 {
+ font-size: 1.20138889em;
+}
+.katex .sizing.reset-size7.size9,
+.katex .fontsize-ensurer.reset-size7.size9 {
+ font-size: 1.4375em;
+}
+.katex .sizing.reset-size7.size10,
+.katex .fontsize-ensurer.reset-size7.size10 {
+ font-size: 1.72916667em;
+}
+.katex .sizing.reset-size8.size1,
+.katex .fontsize-ensurer.reset-size8.size1 {
+ font-size: 0.28901734em;
+}
+.katex .sizing.reset-size8.size2,
+.katex .fontsize-ensurer.reset-size8.size2 {
+ font-size: 0.40462428em;
+}
+.katex .sizing.reset-size8.size3,
+.katex .fontsize-ensurer.reset-size8.size3 {
+ font-size: 0.46242775em;
+}
+.katex .sizing.reset-size8.size4,
+.katex .fontsize-ensurer.reset-size8.size4 {
+ font-size: 0.52023121em;
+}
+.katex .sizing.reset-size8.size5,
+.katex .fontsize-ensurer.reset-size8.size5 {
+ font-size: 0.57803468em;
+}
+.katex .sizing.reset-size8.size6,
+.katex .fontsize-ensurer.reset-size8.size6 {
+ font-size: 0.69364162em;
+}
+.katex .sizing.reset-size8.size7,
+.katex .fontsize-ensurer.reset-size8.size7 {
+ font-size: 0.83236994em;
+}
+.katex .sizing.reset-size8.size8,
+.katex .fontsize-ensurer.reset-size8.size8 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size8.size9,
+.katex .fontsize-ensurer.reset-size8.size9 {
+ font-size: 1.19653179em;
+}
+.katex .sizing.reset-size8.size10,
+.katex .fontsize-ensurer.reset-size8.size10 {
+ font-size: 1.43930636em;
+}
+.katex .sizing.reset-size9.size1,
+.katex .fontsize-ensurer.reset-size9.size1 {
+ font-size: 0.24154589em;
+}
+.katex .sizing.reset-size9.size2,
+.katex .fontsize-ensurer.reset-size9.size2 {
+ font-size: 0.33816425em;
+}
+.katex .sizing.reset-size9.size3,
+.katex .fontsize-ensurer.reset-size9.size3 {
+ font-size: 0.38647343em;
+}
+.katex .sizing.reset-size9.size4,
+.katex .fontsize-ensurer.reset-size9.size4 {
+ font-size: 0.43478261em;
+}
+.katex .sizing.reset-size9.size5,
+.katex .fontsize-ensurer.reset-size9.size5 {
+ font-size: 0.48309179em;
+}
+.katex .sizing.reset-size9.size6,
+.katex .fontsize-ensurer.reset-size9.size6 {
+ font-size: 0.57971014em;
+}
+.katex .sizing.reset-size9.size7,
+.katex .fontsize-ensurer.reset-size9.size7 {
+ font-size: 0.69565217em;
+}
+.katex .sizing.reset-size9.size8,
+.katex .fontsize-ensurer.reset-size9.size8 {
+ font-size: 0.83574879em;
+}
+.katex .sizing.reset-size9.size9,
+.katex .fontsize-ensurer.reset-size9.size9 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size9.size10,
+.katex .fontsize-ensurer.reset-size9.size10 {
+ font-size: 1.20289855em;
+}
+.katex .sizing.reset-size10.size1,
+.katex .fontsize-ensurer.reset-size10.size1 {
+ font-size: 0.20080321em;
+}
+.katex .sizing.reset-size10.size2,
+.katex .fontsize-ensurer.reset-size10.size2 {
+ font-size: 0.2811245em;
+}
+.katex .sizing.reset-size10.size3,
+.katex .fontsize-ensurer.reset-size10.size3 {
+ font-size: 0.32128514em;
+}
+.katex .sizing.reset-size10.size4,
+.katex .fontsize-ensurer.reset-size10.size4 {
+ font-size: 0.36144578em;
+}
+.katex .sizing.reset-size10.size5,
+.katex .fontsize-ensurer.reset-size10.size5 {
+ font-size: 0.40160643em;
+}
+.katex .sizing.reset-size10.size6,
+.katex .fontsize-ensurer.reset-size10.size6 {
+ font-size: 0.48192771em;
+}
+.katex .sizing.reset-size10.size7,
+.katex .fontsize-ensurer.reset-size10.size7 {
+ font-size: 0.57831325em;
+}
+.katex .sizing.reset-size10.size8,
+.katex .fontsize-ensurer.reset-size10.size8 {
+ font-size: 0.69477912em;
+}
+.katex .sizing.reset-size10.size9,
+.katex .fontsize-ensurer.reset-size10.size9 {
+ font-size: 0.8313253em;
+}
+.katex .sizing.reset-size10.size10,
+.katex .fontsize-ensurer.reset-size10.size10 {
+ font-size: 1em;
+}
+.katex .delimsizing.size1 {
+ font-family: KaTeX_Size1;
+}
+.katex .delimsizing.size2 {
+ font-family: KaTeX_Size2;
+}
+.katex .delimsizing.size3 {
+ font-family: KaTeX_Size3;
+}
+.katex .delimsizing.size4 {
+ font-family: KaTeX_Size4;
+}
+.katex .delimsizing.mult .delim-size1 > span {
+ font-family: KaTeX_Size1;
+}
+.katex .delimsizing.mult .delim-size4 > span {
+ font-family: KaTeX_Size4;
+}
+.katex .nulldelimiter {
+ display: inline-block;
+ width: 0.12em;
+}
+.katex .op-symbol {
+ position: relative;
+}
+.katex .op-symbol.small-op {
+ font-family: KaTeX_Size1;
+}
+.katex .op-symbol.large-op {
+ font-family: KaTeX_Size2;
+}
+.katex .op-limits > .vlist > span {
+ text-align: center;
+}
+.katex .accent > .vlist > span {
+ text-align: center;
+}
+.katex .accent .accent-body > span {
+ width: 0;
+}
+.katex .accent .accent-body.accent-vec > span {
+ position: relative;
+ left: 0.326em;
+}
+.katex .mtable .vertical-separator {
+ display: inline-block;
+ margin: 0 -0.025em;
+ border-right: 0.05em solid black;
+}
+.katex .mtable .arraycolsep {
+ display: inline-block;
+}
+.katex .mtable .col-align-c > .vlist {
+ text-align: center;
+}
+.katex .mtable .col-align-l > .vlist {
+ text-align: left;
+}
+.katex .mtable .col-align-r > .vlist {
+ text-align: right;
+}
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/dockerfile/HTTPdDockerfile b/vendor/dockerfile/HTTPdDockerfile
new file mode 100644
index 00000000000..2f05427323c
--- /dev/null
+++ b/vendor/dockerfile/HTTPdDockerfile
@@ -0,0 +1,3 @@
+FROM httpd:alpine
+
+COPY ./ /usr/local/apache2/htdocs/