summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.csscomb.json32
-rw-r--r--.gitlab-ci.yml1
-rw-r--r--.rubocop.yml958
-rw-r--r--.scss-lint.yml2
-rw-r--r--CHANGELOG88
-rw-r--r--CONTRIBUTING.md23
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile10
-rw-r--r--Gemfile.lock73
-rw-r--r--VERSION2
-rw-r--r--app/assets/javascripts/api.js.coffee2
-rw-r--r--app/assets/javascripts/application.js.coffee15
-rw-r--r--app/assets/javascripts/aside.js.coffee1
-rw-r--r--app/assets/javascripts/awards_handler.coffee2
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee11
-rw-r--r--app/assets/javascripts/gl_crop.js.coffee152
-rw-r--r--app/assets/javascripts/gl_dropdown.js.coffee207
-rw-r--r--app/assets/javascripts/issuable_context.js.coffee51
-rw-r--r--app/assets/javascripts/issuable_form.js.coffee52
-rw-r--r--app/assets/javascripts/issues.js.coffee36
-rw-r--r--app/assets/javascripts/labels_select.js.coffee264
-rw-r--r--app/assets/javascripts/lib/animate.js.coffee13
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.coffee25
-rw-r--r--app/assets/javascripts/milestone_select.js.coffee130
-rw-r--r--app/assets/javascripts/notes.js.coffee12
-rw-r--r--app/assets/javascripts/profile.js.coffee52
-rw-r--r--app/assets/javascripts/project.js.coffee1
-rw-r--r--app/assets/javascripts/right_sidebar.js.coffee55
-rw-r--r--app/assets/javascripts/search_autocomplete.js.coffee279
-rw-r--r--app/assets/javascripts/sidebar.js.coffee2
-rw-r--r--app/assets/javascripts/stat_graph_contributors_util.js.coffee2
-rw-r--r--app/assets/javascripts/todos.js.coffee61
-rw-r--r--app/assets/javascripts/users_select.js.coffee165
-rw-r--r--app/assets/stylesheets/application.scss2
-rw-r--r--app/assets/stylesheets/framework/avatar.scss2
-rw-r--r--app/assets/stylesheets/framework/blocks.scss20
-rw-r--r--app/assets/stylesheets/framework/buttons.scss10
-rw-r--r--app/assets/stylesheets/framework/common.scss7
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss111
-rw-r--r--app/assets/stylesheets/framework/files.scss8
-rw-r--r--app/assets/stylesheets/framework/filters.scss8
-rw-r--r--app/assets/stylesheets/framework/fonts.scss4
-rw-r--r--app/assets/stylesheets/framework/forms.scss36
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss8
-rw-r--r--app/assets/stylesheets/framework/header.scss37
-rw-r--r--app/assets/stylesheets/framework/layout.scss2
-rw-r--r--app/assets/stylesheets/framework/lists.scss7
-rw-r--r--app/assets/stylesheets/framework/mixins.scss2
-rw-r--r--app/assets/stylesheets/framework/mobile.scss2
-rw-r--r--app/assets/stylesheets/framework/nav.scss40
-rw-r--r--app/assets/stylesheets/framework/selects.scss3
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss211
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss4
-rw-r--r--app/assets/stylesheets/framework/typography.scss24
-rw-r--r--app/assets/stylesheets/framework/variables.scss115
-rw-r--r--app/assets/stylesheets/notify.scss24
-rw-r--r--app/assets/stylesheets/pages/admin.scss6
-rw-r--r--app/assets/stylesheets/pages/commits.scss7
-rw-r--r--app/assets/stylesheets/pages/diff.scss12
-rw-r--r--app/assets/stylesheets/pages/events.scss13
-rw-r--r--app/assets/stylesheets/pages/issuable.scss164
-rw-r--r--app/assets/stylesheets/pages/issues.scss8
-rw-r--r--app/assets/stylesheets/pages/labels.scss40
-rw-r--r--app/assets/stylesheets/pages/login.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss6
-rw-r--r--app/assets/stylesheets/pages/note_form.scss4
-rw-r--r--app/assets/stylesheets/pages/notes.scss145
-rw-r--r--app/assets/stylesheets/pages/profile.scss23
-rw-r--r--app/assets/stylesheets/pages/projects.scss34
-rw-r--r--app/assets/stylesheets/pages/search.scss142
-rw-r--r--app/assets/stylesheets/pages/stat_graph.scss2
-rw-r--r--app/assets/stylesheets/pages/status.scss92
-rw-r--r--app/assets/stylesheets/pages/todos.scss23
-rw-r--r--app/assets/stylesheets/pages/tree.scss2
-rw-r--r--app/assets/stylesheets/pages/xterm.scss10
-rw-r--r--app/controllers/admin/application_settings_controller.rb2
-rw-r--r--app/controllers/admin/groups_controller.rb8
-rw-r--r--app/controllers/admin/labels_controller.rb2
-rw-r--r--app/controllers/admin/projects_controller.rb7
-rw-r--r--app/controllers/application_controller.rb63
-rw-r--r--app/controllers/autocomplete_controller.rb2
-rw-r--r--app/controllers/ci/projects_controller.rb10
-rw-r--r--app/controllers/concerns/global_milestones.rb1
-rw-r--r--app/controllers/concerns/issuable_actions.rb23
-rw-r--r--app/controllers/concerns/issues_action.rb2
-rw-r--r--app/controllers/concerns/merge_requests_action.rb2
-rw-r--r--app/controllers/dashboard/application_controller.rb6
-rw-r--r--app/controllers/dashboard/groups_controller.rb2
-rw-r--r--app/controllers/dashboard/labels_controller.rb9
-rw-r--r--app/controllers/dashboard/milestones_controller.rb15
-rw-r--r--app/controllers/dashboard/projects_controller.rb4
-rw-r--r--app/controllers/dashboard/snippets_controller.rb2
-rw-r--r--app/controllers/dashboard/todos_controller.rb19
-rw-r--r--app/controllers/dashboard_controller.rb4
-rw-r--r--app/controllers/explore/groups_controller.rb4
-rw-r--r--app/controllers/explore/projects_controller.rb6
-rw-r--r--app/controllers/explore/snippets_controller.rb2
-rw-r--r--app/controllers/groups/application_controller.rb27
-rw-r--r--app/controllers/groups/avatars_controller.rb2
-rw-r--r--app/controllers/groups/group_members_controller.rb3
-rw-r--r--app/controllers/groups/milestones_controller.rb18
-rw-r--r--app/controllers/groups_controller.rb40
-rw-r--r--app/controllers/namespaces_controller.rb2
-rw-r--r--app/controllers/oauth/authorizations_controller.rb1
-rw-r--r--app/controllers/profiles_controller.rb20
-rw-r--r--app/controllers/projects/application_controller.rb72
-rw-r--r--app/controllers/projects/avatars_controller.rb2
-rw-r--r--app/controllers/projects/badges_controller.rb5
-rw-r--r--app/controllers/projects/branches_controller.rb2
-rw-r--r--app/controllers/projects/forks_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb31
-rw-r--r--app/controllers/projects/labels_controller.rb9
-rw-r--r--app/controllers/projects/merge_requests_controller.rb24
-rw-r--r--app/controllers/projects/milestones_controller.rb9
-rw-r--r--app/controllers/projects/snippets_controller.rb8
-rw-r--r--app/controllers/projects/tags_controller.rb2
-rw-r--r--app/controllers/projects/uploads_controller.rb6
-rw-r--r--app/controllers/projects/wikis_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb19
-rw-r--r--app/controllers/root_controller.rb4
-rw-r--r--app/controllers/snippets_controller.rb2
-rw-r--r--app/controllers/users_controller.rb4
-rw-r--r--app/finders/contributed_projects_finder.rb24
-rw-r--r--app/finders/group_projects_finder.rb42
-rw-r--r--app/finders/groups_finder.rb18
-rw-r--r--app/finders/issuable_finder.rb20
-rw-r--r--app/finders/issues_finder.rb6
-rw-r--r--app/finders/joined_groups_finder.rb24
-rw-r--r--app/finders/personal_projects_finder.rb28
-rw-r--r--app/finders/projects_finder.rb77
-rw-r--r--app/finders/union_finder.rb11
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/blob_helper.rb6
-rw-r--r--app/helpers/button_helper.rb30
-rw-r--r--app/helpers/commits_helper.rb2
-rw-r--r--app/helpers/dropdowns_helper.rb7
-rw-r--r--app/helpers/events_helper.rb10
-rw-r--r--app/helpers/groups_helper.rb4
-rw-r--r--app/helpers/issuables_helper.rb18
-rw-r--r--app/helpers/issues_helper.rb17
-rw-r--r--app/helpers/labels_helper.rb34
-rw-r--r--app/helpers/milestones_helper.rb24
-rw-r--r--app/helpers/notes_helper.rb6
-rw-r--r--app/helpers/preferences_helper.rb4
-rw-r--r--app/helpers/projects_helper.rb8
-rw-r--r--app/helpers/search_helper.rb57
-rw-r--r--app/helpers/todos_helper.rb11
-rw-r--r--app/helpers/visibility_level_helper.rb37
-rw-r--r--app/mailers/emails/issues.rb8
-rw-r--r--app/mailers/notify.rb4
-rw-r--r--app/models/ability.rb119
-rw-r--r--app/models/application_setting.rb3
-rw-r--r--app/models/commit.rb6
-rw-r--r--app/models/commit_range.rb4
-rw-r--r--app/models/concerns/internal_id.rb5
-rw-r--r--app/models/concerns/issuable.rb14
-rw-r--r--app/models/concerns/milestoneish.rb20
-rw-r--r--app/models/event.rb10
-rw-r--r--app/models/external_issue.rb2
-rw-r--r--app/models/global_milestone.rb1
-rw-r--r--app/models/group.rb37
-rw-r--r--app/models/issue.rb37
-rw-r--r--app/models/label.rb10
-rw-r--r--app/models/merge_request.rb32
-rw-r--r--app/models/milestone.rb10
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/project.rb59
-rw-r--r--app/models/project_services/gitlab_issue_tracker_service.rb2
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--app/models/project_services/slack_service/issue_message.rb2
-rw-r--r--app/models/project_wiki.rb12
-rw-r--r--app/models/repository.rb61
-rw-r--r--app/models/snippet.rb4
-rw-r--r--app/models/todo.rb32
-rw-r--r--app/models/user.rb6
-rw-r--r--app/services/base_service.rb7
-rw-r--r--app/services/ci/create_builds_service.rb2
-rw-r--r--app/services/create_snippet_service.rb3
-rw-r--r--app/services/git_push_service.rb2
-rw-r--r--app/services/groups/base_service.rb9
-rw-r--r--app/services/groups/create_service.rb21
-rw-r--r--app/services/groups/update_service.rb20
-rw-r--r--app/services/issues/close_service.rb6
-rw-r--r--app/services/issues/create_service.rb2
-rw-r--r--app/services/issues/move_service.rb101
-rw-r--r--app/services/merge_requests/base_service.rb13
-rw-r--r--app/services/merge_requests/build_service.rb2
-rw-r--r--app/services/merge_requests/post_merge_service.rb2
-rw-r--r--app/services/notification_service.rb22
-rw-r--r--app/services/projects/autocomplete_service.rb6
-rw-r--r--app/services/projects/create_service.rb10
-rw-r--r--app/services/projects/housekeeping_service.rb2
-rw-r--r--app/services/projects/update_service.rb29
-rw-r--r--app/services/search/global_service.rb2
-rw-r--r--app/services/search/project_service.rb3
-rw-r--r--app/services/system_hooks_service.rb22
-rw-r--r--app/services/system_note_service.rb38
-rw-r--r--app/services/todo_service.rb87
-rw-r--r--app/services/update_snippet_service.rb1
-rw-r--r--app/uploaders/file_uploader.rb17
-rw-r--r--app/views/admin/application_settings/_form.html.haml11
-rw-r--r--app/views/admin/applications/_delete_form.html.haml2
-rw-r--r--app/views/admin/groups/_form.html.haml2
-rw-r--r--app/views/admin/groups/index.html.haml3
-rw-r--r--app/views/admin/groups/show.html.haml5
-rw-r--r--app/views/admin/labels/_label.html.haml2
-rw-r--r--app/views/admin/projects/show.html.haml2
-rw-r--r--app/views/ci/projects/index.html.haml20
-rw-r--r--app/views/dashboard/issues.html.haml2
-rw-r--r--app/views/dashboard/todos/_todo.html.haml13
-rw-r--r--app/views/dashboard/todos/index.html.haml14
-rw-r--r--app/views/devise/sessions/_new_crowd.html.haml2
-rw-r--r--app/views/doorkeeper/applications/index.html.haml2
-rw-r--r--app/views/doorkeeper/applications/new.html.haml2
-rw-r--r--app/views/doorkeeper/authorizations/error.html.haml2
-rw-r--r--app/views/doorkeeper/authorizations/show.html.haml2
-rw-r--r--app/views/events/_event.html.haml4
-rw-r--r--app/views/events/event/_common.html.haml2
-rw-r--r--app/views/events/event/_created_project.html.haml18
-rw-r--r--app/views/groups/edit.html.haml3
-rw-r--r--app/views/groups/issues.html.haml2
-rw-r--r--app/views/groups/new.html.haml2
-rw-r--r--app/views/groups/show.html.haml63
-rw-r--r--app/views/layouts/_collapse_button.html.haml4
-rw-r--r--app/views/layouts/_page.html.haml18
-rw-r--r--app/views/layouts/_search.html.haml40
-rw-r--r--app/views/layouts/header/_default.html.haml18
-rw-r--r--app/views/layouts/nav/_admin.html.haml2
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml7
-rw-r--r--app/views/layouts/nav/_explore.html.haml2
-rw-r--r--app/views/layouts/nav/_group.html.haml78
-rw-r--r--app/views/layouts/nav/_profile.html.haml8
-rw-r--r--app/views/layouts/nav/_project.html.haml19
-rw-r--r--app/views/layouts/notify.html.haml30
-rw-r--r--app/views/notify/issue_moved_email.html.haml6
-rw-r--r--app/views/notify/issue_moved_email.text.erb4
-rw-r--r--app/views/profiles/keys/_key.html.haml2
-rw-r--r--app/views/profiles/notifications/show.html.haml33
-rw-r--r--app/views/profiles/passwords/edit.html.haml17
-rw-r--r--app/views/profiles/show.html.haml24
-rw-r--r--app/views/projects/_builds_settings.html.haml8
-rw-r--r--app/views/projects/_home_panel.html.haml18
-rw-r--r--app/views/projects/branches/_branch.html.haml4
-rw-r--r--app/views/projects/branches/index.html.haml2
-rw-r--r--app/views/projects/builds/index.html.haml3
-rw-r--r--app/views/projects/buttons/_download.html.haml2
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml17
-rw-r--r--app/views/projects/buttons/_fork.html.haml6
-rw-r--r--app/views/projects/buttons/_notifications.html.haml2
-rw-r--r--app/views/projects/buttons/_star.html.haml4
-rw-r--r--app/views/projects/commits/_commit_list.html.haml4
-rw-r--r--app/views/projects/commits/_commits.html.haml2
-rw-r--r--app/views/projects/compare/_form.html.haml2
-rw-r--r--app/views/projects/diffs/_diffs.html.haml2
-rw-r--r--app/views/projects/diffs/_file.html.haml24
-rw-r--r--app/views/projects/diffs/_image.html.haml5
-rw-r--r--app/views/projects/diffs/_line.html.haml26
-rw-r--r--app/views/projects/diffs/_text_file.html.haml21
-rw-r--r--app/views/projects/find_file/show.html.haml2
-rw-r--r--app/views/projects/forks/new.html.haml4
-rw-r--r--app/views/projects/issues/_issue.html.haml7
-rw-r--r--app/views/projects/issues/_new_branch.html.haml2
-rw-r--r--app/views/projects/issues/index.html.haml2
-rw-r--r--app/views/projects/issues/show.html.haml6
-rw-r--r--app/views/projects/labels/_label.html.haml2
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml8
-rw-r--r--app/views/projects/merge_requests/_show.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_mr_title.html.haml6
-rw-r--r--app/views/projects/merge_requests/widget/open/_wip.html.haml10
-rw-r--r--app/views/projects/milestones/show.html.haml2
-rw-r--r--app/views/projects/notes/_edit_form.html.haml2
-rw-r--r--app/views/projects/notes/_note.html.haml29
-rw-r--r--app/views/projects/notes/discussions/_active.html.haml16
-rw-r--r--app/views/projects/notes/discussions/_commit.html.haml16
-rw-r--r--app/views/projects/notes/discussions/_outdated.html.haml15
-rw-r--r--app/views/projects/tags/_download.html.haml2
-rw-r--r--app/views/projects/tags/_tag.html.haml4
-rw-r--r--app/views/projects/tags/show.html.haml8
-rw-r--r--app/views/projects/tree/_tree_header.html.haml4
-rw-r--r--app/views/search/results/_issue.html.haml1
-rw-r--r--app/views/search/results/_milestone.html.haml2
-rw-r--r--app/views/search/results/_note.html.haml18
-rw-r--r--app/views/shared/_clone_panel.html.haml6
-rw-r--r--app/views/shared/_group_tips.html.haml1
-rw-r--r--app/views/shared/_label_row.html.haml2
-rw-r--r--app/views/shared/groups/_group.html.haml18
-rw-r--r--app/views/shared/issuable/_filter.html.haml79
-rw-r--r--app/views/shared/issuable/_form.html.haml64
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml44
-rw-r--r--app/views/shared/issuable/_milestone_dropdown.html.haml16
-rw-r--r--app/views/shared/issuable/_participants.html.haml16
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml105
-rw-r--r--app/views/shared/milestones/_issuable.html.haml4
-rw-r--r--app/views/shared/milestones/_labels_tab.html.haml2
-rw-r--r--app/views/shared/milestones/_milestone.html.haml4
-rw-r--r--app/views/shared/milestones/_summary.html.haml8
-rw-r--r--app/views/shared/milestones/_tabs.html.haml4
-rw-r--r--app/views/shared/milestones/_top.html.haml4
-rw-r--r--app/views/shared/projects/_project.html.haml37
-rw-r--r--app/views/shared/snippets/_header.html.haml2
-rw-r--r--app/views/shared/snippets/_snippet.html.haml4
-rw-r--r--app/views/votes/_votes_block.html.haml2
-rw-r--r--app/workers/gitlab_shell_one_shot_worker.rb10
-rw-r--r--app/workers/project_cache_worker.rb3
-rw-r--r--app/workers/project_destroy_worker.rb2
-rw-r--r--app/workers/repository_fork_worker.rb5
-rw-r--r--config/application.rb1
-rw-r--r--config/gitlab.yml.example2
-rw-r--r--config/initializers/1_settings.rb1
-rw-r--r--config/initializers/premailer.rb7
-rw-r--r--config/mail_room.yml2
-rw-r--r--config/routes.rb18
-rw-r--r--db/migrate/20130218141258_convert_closed_to_state_in_issue.rb18
-rw-r--r--db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb22
-rw-r--r--db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb18
-rw-r--r--db/migrate/20130220125544_convert_merge_status_in_merge_request.rb22
-rw-r--r--db/migrate/20130419190306_allow_merges_for_forks.rb8
-rw-r--r--db/migrate/20160223192159_add_confidential_to_issues.rb6
-rw-r--r--db/migrate/20160225090018_add_delete_at_to_issues.rb6
-rw-r--r--db/migrate/20160225101956_add_delete_at_to_merge_requests.rb6
-rw-r--r--db/migrate/20160301124843_add_visibility_level_to_groups.rb29
-rw-r--r--db/migrate/20160308212903_add_default_group_visibility_to_application_settings.rb29
-rw-r--r--db/migrate/20160316192622_change_target_id_to_null_on_todos.rb5
-rw-r--r--db/migrate/20160316204731_add_commit_id_to_todos.rb6
-rw-r--r--db/migrate/20160317092222_add_moved_to_to_issue.rb5
-rw-r--r--db/migrate/20160320204112_index_namespaces_on_visibility_level.rb7
-rw-r--r--db/migrate/20160324020319_remove_todos_for_deleted_issues.rb17
-rw-r--r--db/migrate/20160329144452_add_index_on_pending_delete_projects.rb6
-rw-r--r--db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb17
-rw-r--r--db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb5
-rw-r--r--db/schema.rb17
-rw-r--r--doc/README.md7
-rw-r--r--doc/administration/auth/README.md11
-rw-r--r--doc/administration/auth/ldap.md277
-rw-r--r--doc/api/groups.md1
-rw-r--r--doc/api/issues.md19
-rw-r--r--doc/api/labels.md71
-rw-r--r--doc/api/merge_requests.md19
-rw-r--r--doc/api/projects.md166
-rw-r--r--doc/api/settings.md3
-rw-r--r--doc/ci/yaml/README.md15
-rw-r--r--doc/development/scss_styleguide.md27
-rw-r--r--doc/gitlab-basics/README.md34
-rw-r--r--doc/gitlab-basics/basic-git-commands.md58
-rw-r--r--doc/gitlab-basics/start-using-git.md63
-rw-r--r--doc/incoming_email/README.md108
-rw-r--r--doc/install/installation.md2
-rw-r--r--doc/integration/github.md2
-rw-r--r--doc/integration/ldap.md227
-rw-r--r--doc/intro/README.md41
-rw-r--r--doc/monitoring/performance/grafana_configuration.md118
-rw-r--r--doc/monitoring/performance/img/grafana_dashboard_dropdown.pngbin0 -> 29419 bytes
-rw-r--r--doc/monitoring/performance/img/grafana_dashboard_import.pngbin0 -> 40974 bytes
-rw-r--r--doc/monitoring/performance/img/grafana_data_source_configuration.pngbin0 -> 53402 bytes
-rw-r--r--doc/monitoring/performance/img/grafana_data_source_empty.pngbin0 -> 44058 bytes
-rw-r--r--doc/monitoring/performance/img/grafana_save_icon.pngbin0 -> 16024 bytes
-rw-r--r--doc/monitoring/performance/introduction.md3
-rw-r--r--doc/public_access/public_access.md23
-rw-r--r--doc/raketasks/backup_restore.md3
-rw-r--r--doc/release/README.md10
-rw-r--r--doc/release/howto_rc1.md55
-rw-r--r--doc/release/howto_update_guides.md55
-rw-r--r--doc/release/master.md62
-rw-r--r--doc/release/monthly.md245
-rw-r--r--doc/release/patch.md81
-rw-r--r--doc/release/security.md76
-rw-r--r--doc/update/8.2-to-8.3.md9
-rw-r--r--doc/update/8.3-to-8.4.md9
-rw-r--r--doc/update/8.4-to-8.5.md9
-rw-r--r--doc/update/8.5-to-8.6.md53
-rw-r--r--doc/update/README.md1
-rw-r--r--doc/update/restore_after_failure.md83
-rw-r--r--doc/web_hooks/web_hooks.md9
-rw-r--r--doc/workflow/award_emoji.md48
-rw-r--r--doc/workflow/img/award_emoji_select.pngbin0 -> 65985 bytes
-rw-r--r--doc/workflow/img/award_emoji_votes_least_popular.pngbin0 -> 144501 bytes
-rw-r--r--doc/workflow/img/award_emoji_votes_most_popular.pngbin0 -> 136577 bytes
-rw-r--r--doc/workflow/img/award_emoji_votes_sort_options.pngbin0 -> 162251 bytes
-rw-r--r--doc/workflow/lfs/lfs_administration.md4
-rw-r--r--features/dashboard/todos.feature5
-rw-r--r--features/groups.feature4
-rw-r--r--features/project/issues/issues.feature1
-rw-r--r--features/project/merge_requests.feature8
-rw-r--r--features/project/project.feature9
-rw-r--r--features/steps/dashboard/issues.rb9
-rw-r--r--features/steps/dashboard/todos.rb9
-rw-r--r--features/steps/group/milestones.rb4
-rw-r--r--features/steps/groups.rb6
-rw-r--r--features/steps/project/active_tab.rb4
-rw-r--r--features/steps/project/create.rb4
-rw-r--r--features/steps/project/fork.rb2
-rw-r--r--features/steps/project/issues/issues.rb3
-rw-r--r--features/steps/project/merge_requests.rb8
-rw-r--r--features/steps/project/project.rb12
-rw-r--r--features/steps/shared/project_tab.rb2
-rw-r--r--fixtures/emojis/digests.json8597
-rw-r--r--lib/api/entities.rb10
-rw-r--r--lib/api/groups.rb2
-rw-r--r--lib/api/helpers.rb4
-rw-r--r--lib/api/issues.rb26
-rw-r--r--lib/api/labels.rb24
-rw-r--r--lib/api/merge_requests.rb12
-rw-r--r--lib/api/projects.rb28
-rw-r--r--lib/award_emoji.rb19
-rw-r--r--lib/banzai/filter.rb2
-rw-r--r--lib/banzai/filter/autolink_filter.rb1
-rw-r--r--lib/banzai/filter/commit_range_reference_filter.rb2
-rw-r--r--lib/banzai/filter/commit_reference_filter.rb2
-rw-r--r--lib/banzai/filter/emoji_filter.rb4
-rw-r--r--lib/banzai/filter/external_link_filter.rb2
-rw-r--r--lib/banzai/filter/gollum_tags_filter.rb3
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb5
-rw-r--r--lib/banzai/filter/label_reference_filter.rb2
-rw-r--r--lib/banzai/filter/markdown_filter.rb2
-rw-r--r--lib/banzai/filter/merge_request_reference_filter.rb2
-rw-r--r--lib/banzai/filter/milestone_reference_filter.rb4
-rw-r--r--lib/banzai/filter/redactor_filter.rb2
-rw-r--r--lib/banzai/filter/reference_filter.rb4
-rw-r--r--lib/banzai/filter/reference_gatherer_filter.rb2
-rw-r--r--lib/banzai/filter/relative_link_filter.rb1
-rw-r--r--lib/banzai/filter/sanitization_filter.rb3
-rw-r--r--lib/banzai/filter/snippet_reference_filter.rb2
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb1
-rw-r--r--lib/banzai/filter/table_of_contents_filter.rb2
-rw-r--r--lib/banzai/filter/upload_link_filter.rb1
-rw-r--r--lib/banzai/filter/user_reference_filter.rb2
-rw-r--r--lib/banzai/filter/yaml_front_matter_filter.rb3
-rw-r--r--lib/banzai/pipeline/base_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/wiki_pipeline.rb2
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb21
-rw-r--r--lib/gitlab/badge/build.rb24
-rw-r--r--lib/gitlab/current_settings.rb1
-rw-r--r--lib/gitlab/diff/file.rb4
-rw-r--r--lib/gitlab/email/message/repository_push.rb2
-rw-r--r--lib/gitlab/email/receiver.rb25
-rw-r--r--lib/gitlab/exclusive_lease.rb21
-rw-r--r--lib/gitlab/fogbugz_import/client.rb2
-rw-r--r--lib/gitlab/gfm/reference_rewriter.rb84
-rw-r--r--lib/gitlab/gfm/uploads_rewriter.rb51
-rw-r--r--lib/gitlab/incoming_email.rb16
-rw-r--r--lib/gitlab/note_data_builder.rb2
-rw-r--r--lib/gitlab/project_search_results.rb3
-rw-r--r--lib/gitlab/reference_extractor.rb18
-rw-r--r--lib/gitlab/routing.rb13
-rw-r--r--lib/gitlab/search_results.rb7
-rw-r--r--lib/gitlab/url_builder.rb16
-rw-r--r--lib/gitlab/visibility_level.rb12
-rw-r--r--lib/tasks/gemojione.rake48
-rw-r--r--lib/tasks/gitlab/backup.rake34
-rw-r--r--lib/tasks/gitlab/check.rake15
-rw-r--r--lib/tasks/gitlab/db.rake35
-rw-r--r--spec/controllers/admin/users_controller_spec.rb15
-rw-r--r--spec/controllers/application_controller_spec.rb40
-rw-r--r--spec/controllers/ci/projects_controller_spec.rb21
-rw-r--r--spec/controllers/groups/avatars_controller_spec.rb3
-rw-r--r--spec/controllers/groups_controller_spec.rb59
-rw-r--r--spec/controllers/namespaces_controller_spec.rb31
-rw-r--r--spec/controllers/projects/avatars_controller_spec.rb2
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb185
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb25
-rw-r--r--spec/controllers/projects/snippets_controller_spec.rb107
-rw-r--r--spec/controllers/root_controller_spec.rb22
-rw-r--r--spec/controllers/uploads_controller_spec.rb20
-rw-r--r--spec/factories/broadcast_messages.rb2
-rw-r--r--spec/factories/file_uploader.rb20
-rw-r--r--spec/factories/groups.rb12
-rw-r--r--spec/factories/issues.rb4
-rw-r--r--spec/factories/merge_requests.rb5
-rw-r--r--spec/factories/todos.rb10
-rw-r--r--spec/factories_spec.rb16
-rw-r--r--spec/features/dashboard_milestones_spec.rb29
-rw-r--r--spec/features/issues/filter_by_milestone_spec.rb42
-rw-r--r--spec/features/issues/move_spec.rb87
-rw-r--r--spec/features/issues/new_branch_button_spec.rb2
-rw-r--r--spec/features/issues/update_issues_spec.rb117
-rw-r--r--spec/features/issues_spec.rb77
-rw-r--r--spec/features/merge_requests/filter_by_milestone_spec.rb40
-rw-r--r--spec/features/search_spec.rb43
-rw-r--r--spec/features/security/group/internal_access_spec.rb109
-rw-r--r--spec/features/security/group/private_access_spec.rb109
-rw-r--r--spec/features/security/group/public_access_spec.rb109
-rw-r--r--spec/features/security/group_access_spec.rb284
-rw-r--r--spec/features/security/project/internal_access_spec.rb109
-rw-r--r--spec/features/security/project/private_access_spec.rb110
-rw-r--r--spec/features/security/project/public_access_spec.rb111
-rw-r--r--spec/features/security/project/snippet/internal_access_spec.rb78
-rw-r--r--spec/features/security/project/snippet/private_access_spec.rb63
-rw-r--r--spec/features/security/project/snippet/public_access_spec.rb93
-rw-r--r--spec/finders/group_projects_finder_spec.rb89
-rw-r--r--spec/finders/groups_finder_spec.rb33
-rw-r--r--spec/finders/joined_groups_finder_spec.rb77
-rw-r--r--spec/finders/personal_projects_finder_spec.rb28
-rw-r--r--spec/finders/projects_finder_spec.rb2
-rw-r--r--spec/finders/snippets_finder_spec.rb2
-rw-r--r--spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references.eml42
-rw-r--r--spec/fixtures/emails/valid_reply.eml4
-rw-r--r--spec/helpers/groups_helper_spec.rb (renamed from spec/helpers/groups_helper.rb)0
-rw-r--r--spec/helpers/labels_helper_spec.rb12
-rw-r--r--spec/helpers/preferences_helper_spec.rb4
-rw-r--r--spec/helpers/projects_helper_spec.rb29
-rw-r--r--spec/helpers/visibility_level_helper_spec.rb17
-rw-r--r--spec/lib/award_emoji_spec.rb19
-rw-r--r--spec/lib/banzai/filter/label_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/redactor_filter_spec.rb76
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb54
-rw-r--r--spec/lib/extracts_path_spec.rb2
-rw-r--r--spec/lib/gitlab/badge/build_spec.rb72
-rw-r--r--spec/lib/gitlab/closing_issue_extractor_spec.rb3
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb14
-rw-r--r--spec/lib/gitlab/email/receiver_spec.rb23
-rw-r--r--spec/lib/gitlab/fogbugz_import/client_spec.rb24
-rw-r--r--spec/lib/gitlab/gfm/reference_rewriter_spec.rb89
-rw-r--r--spec/lib/gitlab/gfm/uploads_rewriter_spec.rb66
-rw-r--r--spec/lib/gitlab/incoming_email_spec.rb26
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb69
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb22
-rw-r--r--spec/lib/gitlab/search_results_spec.rb91
-rw-r--r--spec/mailers/notify_spec.rb91
-rw-r--r--spec/mailers/shared/notify.rb76
-rw-r--r--spec/models/application_setting_spec.rb1
-rw-r--r--spec/models/commit_spec.rb13
-rw-r--r--spec/models/concerns/issuable_spec.rb1
-rw-r--r--spec/models/concerns/mentionable_spec.rb5
-rw-r--r--spec/models/concerns/milestoneish_spec.rb104
-rw-r--r--spec/models/event_spec.rb64
-rw-r--r--spec/models/group_spec.rb17
-rw-r--r--spec/models/hooks/system_hook_spec.rb54
-rw-r--r--spec/models/issue_spec.rb70
-rw-r--r--spec/models/merge_request_spec.rb85
-rw-r--r--spec/models/milestone_spec.rb20
-rw-r--r--spec/models/project_security_spec.rb10
-rw-r--r--spec/models/project_services/slack_service/issue_message_spec.rb10
-rw-r--r--spec/models/project_spec.rb82
-rw-r--r--spec/models/project_wiki_spec.rb12
-rw-r--r--spec/models/repository_spec.rb87
-rw-r--r--spec/models/todo_spec.rb85
-rw-r--r--spec/models/user_spec.rb7
-rw-r--r--spec/requests/api/group_members_spec.rb2
-rw-r--r--spec/requests/api/groups_spec.rb2
-rw-r--r--spec/requests/api/issues_spec.rb147
-rw-r--r--spec/requests/api/labels_spec.rb29
-rw-r--r--spec/requests/api/merge_requests_spec.rb48
-rw-r--r--spec/requests/api/projects_spec.rb73
-rw-r--r--spec/services/create_snippet_service_spec.rb2
-rw-r--r--spec/services/git_push_service_spec.rb4
-rw-r--r--spec/services/groups/create_service_spec.rb20
-rw-r--r--spec/services/groups/update_service_spec.rb52
-rw-r--r--spec/services/issues/move_service_spec.rb250
-rw-r--r--spec/services/issues/update_service_spec.rb7
-rw-r--r--spec/services/notification_service_spec.rb88
-rw-r--r--spec/services/projects/autocomplete_service_spec.rb79
-rw-r--r--spec/services/projects/housekeeping_service_spec.rb4
-rw-r--r--spec/services/system_note_service_spec.rb53
-rw-r--r--spec/services/todo_service_spec.rb82
-rw-r--r--spec/services/update_snippet_service_spec.rb2
-rw-r--r--spec/support/carrierwave.rb7
-rw-r--r--spec/support/filter_spec_helper.rb2
-rw-r--r--spec/support/markdown_feature.rb2
-rw-r--r--spec/support/matchers/access_matchers.rb2
-rw-r--r--spec/support/mentionable_shared_examples.rb2
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb44
-rw-r--r--spec/workers/project_cache_worker_spec.rb27
-rw-r--r--spec/workers/repository_fork_worker_spec.rb19
-rw-r--r--vendor/assets/javascripts/cropper.js2993
-rw-r--r--vendor/assets/stylesheets/animate.css11
-rw-r--r--vendor/assets/stylesheets/cropper.css379
568 files changed, 23084 insertions, 4591 deletions
diff --git a/.csscomb.json b/.csscomb.json
index e353e6a63d0..741cc1488b5 100644
--- a/.csscomb.json
+++ b/.csscomb.json
@@ -1,16 +1,20 @@
{
- "always-semicolon": true,
- "color-case": "lower",
- "block-indent": " ",
- "color-shorthand": true,
- "element-case": "lower",
- "space-before-colon": "",
- "space-after-colon": " ",
- "space-before-combinator": " ",
- "space-after-combinator": " ",
- "space-between-declarations": "\n",
- "space-before-opening-brace": " ",
- "space-after-opening-brace": "\n",
- "space-before-closing-brace": "\n",
- "unitless-zero": true
+ "exclude": [
+ "app/assets/stylesheets/framework/tw_bootstrap_variables.scss",
+ "app/assets/stylesheets/framework/fonts.scss"
+ ],
+ "always-semicolon": true,
+ "color-case": "lower",
+ "block-indent": " ",
+ "color-shorthand": true,
+ "element-case": "lower",
+ "space-before-colon": "",
+ "space-after-colon": " ",
+ "space-before-combinator": " ",
+ "space-after-combinator": " ",
+ "space-between-declarations": "\n",
+ "space-before-opening-brace": " ",
+ "space-after-opening-brace": "\n",
+ "space-before-closing-brace": "\n",
+ "unitless-zero": true
}
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 2ad63548d78..53f115c92c8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -128,7 +128,6 @@ scss-lint:
- bundle exec rake scss_lint
tags:
- ruby
- allow_failure: true
brakeman:
stage: test
diff --git a/.rubocop.yml b/.rubocop.yml
index 89aa0591c31..71273ce6098 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1,1041 +1,1061 @@
+AllCops:
+ TargetRubyVersion: 2.1
+ # Cop names are not displayed in offense messages by default. Change behavior
+ # by overriding DisplayCopNames, or by giving the -D/--display-cop-names
+ # option.
+ DisplayCopNames: true
+ # Style guide URLs are not displayed in offense messages by default. Change
+ # behavior by overriding DisplayStyleGuide, or by giving the
+ # -S/--display-style-guide option.
+ DisplayStyleGuide: false
+ # Exclude some GitLab files
+ Exclude:
+ - 'vendor/**/*'
+ - 'db/**/*'
+ - 'tmp/**/*'
+ - 'bin/**/*'
+ - 'lib/backup/**/*'
+ - 'lib/ci/backup/**/*'
+ - 'lib/tasks/**/*'
+ - 'lib/ci/migrate/**/*'
+ - 'lib/email_validator.rb'
+ - 'lib/gitlab/upgrader.rb'
+ - 'lib/gitlab/seeder.rb'
+
+
+##################### Style ##################################
+
+# Check indentation of private/protected visibility modifiers.
Style/AccessModifierIndentation:
- Description: Check indentation of private/protected visibility modifiers.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#indent-public-private-protected'
Enabled: true
+# Check the naming of accessor methods for get_/set_.
Style/AccessorMethodName:
- Description: Check the naming of accessor methods for get_/set_.
Enabled: false
+# Use alias_method instead of alias.
Style/Alias:
- Description: 'Use alias_method instead of alias.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#alias-method'
+ EnforcedStyle: prefer_alias_method
Enabled: true
+# Align the elements of an array literal if they span more than one line.
Style/AlignArray:
- Description: >-
- Align the elements of an array literal if they span more than
- one line.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#align-multiline-arrays'
Enabled: true
+# Align the elements of a hash literal if they span more than one line.
Style/AlignHash:
- Description: >-
- Align the elements of a hash literal if they span more than
- one line.
Enabled: true
+# Align the parameters of a method call if they span more than one line.
Style/AlignParameters:
- Description: >-
- Align the parameters of a method call if they span more
- than one line.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-double-indent'
Enabled: false
+# Use &&/|| instead of and/or.
Style/AndOr:
- Description: 'Use &&/|| instead of and/or.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-and-or-or'
Enabled: false
+# Use `Array#join` instead of `Array#*`.
Style/ArrayJoin:
- Description: 'Use Array#join instead of Array#*.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#array-join'
Enabled: false
+# Use only ascii symbols in comments.
Style/AsciiComments:
- Description: 'Use only ascii symbols in comments.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-comments'
Enabled: true
+# Use only ascii symbols in identifiers.
Style/AsciiIdentifiers:
- Description: 'Use only ascii symbols in identifiers.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-identifiers'
Enabled: true
+# Checks for uses of Module#attr.
Style/Attr:
- Description: 'Checks for uses of Module#attr.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr'
Enabled: false
+# Avoid the use of BEGIN blocks.
Style/BeginBlock:
- Description: 'Avoid the use of BEGIN blocks.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-BEGIN-blocks'
Enabled: true
+# Checks if usage of %() or %Q() matches configuration.
Style/BarePercentLiterals:
- Description: 'Checks if usage of %() or %Q() matches configuration.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q-shorthand'
Enabled: false
+# Do not use block comments.
Style/BlockComments:
- Description: 'Do not use block comments.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-block-comments'
Enabled: false
+# Put end statement of multiline block on its own line.
Style/BlockEndNewline:
- Description: 'Put end statement of multiline block on its own line.'
Enabled: true
+# Avoid using {...} for multi-line blocks (multiline chaining is # always
+# ugly). Prefer {...} over do...end for single-line blocks.
Style/BlockDelimiters:
- Description: >-
- Avoid using {...} for multi-line blocks (multiline chaining is
- always ugly).
- Prefer {...} over do...end for single-line blocks.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks'
Enabled: true
+# Enforce braces style around hash parameters.
Style/BracesAroundHashParameters:
- Description: 'Enforce braces style around hash parameters.'
Enabled: false
+# Avoid explicit use of the case equality operator(===).
Style/CaseEquality:
- Description: 'Avoid explicit use of the case equality operator(===).'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-case-equality'
Enabled: false
+# Indentation of when in a case/when/[else/]end.
Style/CaseIndentation:
- Description: 'Indentation of when in a case/when/[else/]end.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#indent-when-to-case'
Enabled: true
+# Checks for uses of character literals.
Style/CharacterLiteral:
- Description: 'Checks for uses of character literals.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-character-literals'
Enabled: true
+# Use CamelCase for classes and modules.'
Style/ClassAndModuleCamelCase:
- Description: 'Use CamelCase for classes and modules.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#camelcase-classes'
Enabled: true
+# Checks style of children classes and modules.
Style/ClassAndModuleChildren:
- Description: 'Checks style of children classes and modules.'
Enabled: false
+# Enforces consistent use of `Object#is_a?` or `Object#kind_of?`.
Style/ClassCheck:
- Description: 'Enforces consistent use of `Object#is_a?` or `Object#kind_of?`.'
Enabled: false
+# Use self when defining module/class methods.
Style/ClassMethods:
- Description: 'Use self when defining module/class methods.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#def-self-singletons'
Enabled: false
+# Avoid the use of class variables.
Style/ClassVars:
- Description: 'Avoid the use of class variables.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-class-vars'
Enabled: true
+# Do not use :: for method call.
Style/ColonMethodCall:
- Description: 'Do not use :: for method call.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#double-colons'
Enabled: false
+# Checks formatting of special comments (TODO, FIXME, OPTIMIZE, HACK, REVIEW).
Style/CommentAnnotation:
- Description: >-
- Checks formatting of special comments
- (TODO, FIXME, OPTIMIZE, HACK, REVIEW).
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#annotate-keywords'
Enabled: false
+# Indentation of comments.
Style/CommentIndentation:
- Description: 'Indentation of comments.'
Enabled: true
+# Use the return value of `if` and `case` statements for assignment to a
+# variable and variable comparison instead of assigning that variable
+# inside of each branch.
+Style/ConditionalAssignment:
+ Enabled: false
+
+# Constants should use SCREAMING_SNAKE_CASE.
Style/ConstantName:
- Description: 'Constants should use SCREAMING_SNAKE_CASE.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#screaming-snake-case'
Enabled: true
+# Use def with parentheses when there are arguments.
Style/DefWithParentheses:
- Description: 'Use def with parentheses when there are arguments.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens'
Enabled: false
+# Checks for use of deprecated Hash methods.
Style/DeprecatedHashMethods:
- Description: 'Checks for use of deprecated Hash methods.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-key'
Enabled: false
+# Document classes and non-namespace modules.
Style/Documentation:
- Description: 'Document classes and non-namespace modules.'
Enabled: false
+# Checks the position of the dot in multi-line method calls.
Style/DotPosition:
- Description: 'Checks the position of the dot in multi-line method calls.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-multi-line-chains'
Enabled: false
+# Checks for uses of double negation (!!).
Style/DoubleNegation:
- Description: 'Checks for uses of double negation (!!).'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-bang-bang'
Enabled: false
+# Prefer `each_with_object` over `inject` or `reduce`.
Style/EachWithObject:
- Description: 'Prefer `each_with_object` over `inject` or `reduce`.'
Enabled: false
+# Align elses and elsifs correctly.
Style/ElseAlignment:
- Description: 'Align elses and elsifs correctly.'
Enabled: true
+# Avoid empty else-clauses.
Style/EmptyElse:
- Description: 'Avoid empty else-clauses.'
Enabled: false
+# Use empty lines between defs.
Style/EmptyLineBetweenDefs:
- Description: 'Use empty lines between defs.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#empty-lines-between-methods'
Enabled: false
+# Don't use several empty lines in a row.
Style/EmptyLines:
- Description: "Don't use several empty lines in a row."
Enabled: false
+# Keep blank lines around access modifiers.
Style/EmptyLinesAroundAccessModifier:
- Description: "Keep blank lines around access modifiers."
Enabled: false
+# Keeps track of empty lines around block bodies.
Style/EmptyLinesAroundBlockBody:
- Description: "Keeps track of empty lines around block bodies."
Enabled: false
+# Keeps track of empty lines around class bodies.
Style/EmptyLinesAroundClassBody:
- Description: "Keeps track of empty lines around class bodies."
Enabled: false
+# Keeps track of empty lines around module bodies.
Style/EmptyLinesAroundModuleBody:
- Description: "Keeps track of empty lines around module bodies."
Enabled: false
+# Keeps track of empty lines around method bodies.
Style/EmptyLinesAroundMethodBody:
- Description: "Keeps track of empty lines around method bodies."
Enabled: false
+# Prefer literals to Array.new/Hash.new/String.new.
Style/EmptyLiteral:
- Description: 'Prefer literals to Array.new/Hash.new/String.new.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#literal-array-hash'
Enabled: false
+# Avoid the use of END blocks.
Style/EndBlock:
- Description: 'Avoid the use of END blocks.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-END-blocks'
Enabled: false
+# Use Unix-style line endings.
Style/EndOfLine:
- Description: 'Use Unix-style line endings.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#crlf'
Enabled: false
+# Favor the use of Fixnum#even? && Fixnum#odd?
Style/EvenOdd:
- Description: 'Favor the use of Fixnum#even? && Fixnum#odd?'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods'
Enabled: false
+# Do not use unnecessary spacing.
Style/ExtraSpacing:
- Description: 'Do not use unnecessary spacing.'
Enabled: false
+# Use snake_case for source file names.
Style/FileName:
- Description: 'Use snake_case for source file names.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-files'
Enabled: false
+# Checks for flip flops.
Style/FlipFlop:
- Description: 'Checks for flip flops'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-flip-flops'
Enabled: false
+# Checks use of for or each in multiline loops.
Style/For:
- Description: 'Checks use of for or each in multiline loops.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-for-loops'
Enabled: false
+# Enforce the use of Kernel#sprintf, Kernel#format or String#%.
Style/FormatString:
- Description: 'Enforce the use of Kernel#sprintf, Kernel#format or String#%.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#sprintf'
Enabled: false
+# Do not introduce global variables.
Style/GlobalVars:
- Description: 'Do not introduce global variables.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#instance-vars'
Enabled: false
+# Check for conditionals that can be replaced with guard clauses.
Style/GuardClause:
- Description: 'Check for conditionals that can be replaced with guard clauses'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals'
Enabled: false
+# Prefer Ruby 1.9 hash syntax `{ a: 1, b: 2 }`
+# over 1.8 syntax `{ :a => 1, :b => 2 }`.
Style/HashSyntax:
- Description: >-
- Prefer Ruby 1.9 hash syntax { a: 1, b: 2 } over 1.8 syntax
- { :a => 1, :b => 2 }.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-literals'
Enabled: true
+# Finds if nodes inside else, which can be converted to elsif.
+Style/IfInsideElse:
+ Enabled: false
+
+# Favor modifier if/unless usage when you have a single-line body.
Style/IfUnlessModifier:
- Description: >-
- Favor modifier if/unless usage when you have a
- single-line body.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#if-as-a-modifier'
Enabled: false
+# Do not use if x; .... Use the ternary operator instead.
Style/IfWithSemicolon:
- Description: 'Do not use if x; .... Use the ternary operator instead.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-semicolon-ifs'
Enabled: false
+# Checks that conditional statements do not have an identical line at the
+# end of each branch, which can validly be moved out of the conditional.
+Style/IdenticalConditionalBranches:
+ Enabled: false
+
+# Checks the indentation of the first line of the right-hand-side of a
+# multi-line assignment.
+Style/IndentAssignment:
+ Enabled: false
+
+# Keep indentation straight.
Style/IndentationConsistency:
- Description: 'Keep indentation straight.'
Enabled: true
+# Use 2 spaces for indentation.
Style/IndentationWidth:
- Description: 'Use 2 spaces for indentation.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation'
Enabled: true
+# Checks the indentation of the first element in an array literal.
Style/IndentArray:
- Description: >-
- Checks the indentation of the first element in an array
- literal.
Enabled: false
+# Checks the indentation of the first key in a hash literal.
Style/IndentHash:
- Description: 'Checks the indentation of the first key in a hash literal.'
Enabled: false
+# Use Kernel#loop for infinite loops.
Style/InfiniteLoop:
- Description: 'Use Kernel#loop for infinite loops.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#infinite-loop'
Enabled: false
+# Use the new lambda literal syntax for single-line blocks.
Style/Lambda:
- Description: 'Use the new lambda literal syntax for single-line blocks.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#lambda-multi-line'
Enabled: false
+# Use lambda.call(...) instead of lambda.(...).
Style/LambdaCall:
- Description: 'Use lambda.call(...) instead of lambda.(...).'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#proc-call'
Enabled: false
+# Comments should start with a space.
Style/LeadingCommentSpace:
- Description: 'Comments should start with a space.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-space'
Enabled: false
+# Use \ instead of + or << to concatenate two string literals at line end.
Style/LineEndConcatenation:
- Description: >-
- Use \ instead of + or << to concatenate two string literals at
- line end.
Enabled: false
+# Do not use parentheses for method calls with no arguments.
Style/MethodCallParentheses:
- Description: 'Do not use parentheses for method calls with no arguments.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-args-no-parens'
Enabled: false
+# Checks if the method definitions have or don't have parentheses.
Style/MethodDefParentheses:
- Description: >-
- Checks if the method definitions have or don't have
- parentheses.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens'
Enabled: false
+# Use the configured style when naming methods.
Style/MethodName:
- Description: 'Use the configured style when naming methods.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars'
Enabled: false
+# Checks for usage of `extend self` in modules.
Style/ModuleFunction:
- Description: 'Checks for usage of `extend self` in modules.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#module-function'
Enabled: false
+# Avoid multi-line chains of blocks.
Style/MultilineBlockChain:
- Description: 'Avoid multi-line chains of blocks.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks'
Enabled: false
+# Ensures newlines after multiline block do statements.
Style/MultilineBlockLayout:
- Description: 'Ensures newlines after multiline block do statements.'
Enabled: true
+# Do not use then for multi-line if/unless.
Style/MultilineIfThen:
- Description: 'Do not use then for multi-line if/unless.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-then'
Enabled: false
+# Checks indentation of method calls with the dot operator that span more than
+# one line.
+Style/MultilineMethodCallIndentation:
+ Enabled: false
+
+# Checks indentation of binary operations that span more than one line.
Style/MultilineOperationIndentation:
- Description: >-
- Checks indentation of binary operations that span more than
- one line.
Enabled: false
+# Avoid multi-line `? :` (the ternary operator), use if/unless instead.
Style/MultilineTernaryOperator:
- Description: >-
- Avoid multi-line ?: (the ternary operator);
- use if/unless instead.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-multiline-ternary'
Enabled: false
+# Do not assign mutable objects to constants.
+Style/MutableConstant:
+ Enabled: false
+
+# Favor unless over if for negative conditions (or control flow or).
Style/NegatedIf:
- Description: >-
- Favor unless over if for negative conditions
- (or control flow or).
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#unless-for-negatives'
Enabled: false
+# Favor until over while for negative conditions.
Style/NegatedWhile:
- Description: 'Favor until over while for negative conditions.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#until-for-negatives'
Enabled: false
+# Avoid using nested modifiers.
+Style/NestedModifier:
+ Enabled: false
+
+# Parenthesize method calls which are nested inside the argument list of
+# another parenthesized method call.
+Style/NestedParenthesizedCalls:
+ Enabled: false
+
+# Use one expression per branch in a ternary operator.
Style/NestedTernaryOperator:
- Description: 'Use one expression per branch in a ternary operator.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-ternary'
Enabled: true
+# Use `next` to skip iteration instead of a condition at the end.
Style/Next:
- Description: 'Use `next` to skip iteration instead of a condition at the end.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals'
Enabled: false
+# Prefer x.nil? to x == nil.
Style/NilComparison:
- Description: 'Prefer x.nil? to x == nil.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods'
Enabled: true
+# Checks for redundant nil checks.
Style/NonNilCheck:
- Description: 'Checks for redundant nil checks.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-non-nil-checks'
Enabled: true
+# Use ! instead of not.
Style/Not:
- Description: 'Use ! instead of not.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bang-not-not'
Enabled: true
+# Add underscores to large numeric literals to improve their readability.
Style/NumericLiterals:
- Description: >-
- Add underscores to large numeric literals to improve their
- readability.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscores-in-numerics'
Enabled: false
+# Favor the ternary operator(?:) over if/then/else/end constructs.
Style/OneLineConditional:
- Description: >-
- Favor the ternary operator(?:) over
- if/then/else/end constructs.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#ternary-operator'
Enabled: true
+# When defining binary operators, name the argument other.
Style/OpMethod:
- Description: 'When defining binary operators, name the argument other.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#other-arg'
Enabled: false
+# Check for simple usages of parallel assignment. It will only warn when
+# the number of variables matches on both sides of the assignment.
Style/ParallelAssignment:
- Description: >-
- Check for simple usages of parallel assignment.
- It will only warn when the number of variables
- matches on both sides of the assignment.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parallel-assignment'
Enabled: false
+# Don't use parentheses around the condition of an if/unless/while.
Style/ParenthesesAroundCondition:
- Description: >-
- Don't use parentheses around the condition of an
- if/unless/while.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-parens-if'
Enabled: true
+# Use `%`-literal delimiters consistently.
Style/PercentLiteralDelimiters:
- Description: 'Use `%`-literal delimiters consistently'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-literal-braces'
Enabled: false
+# Checks if uses of %Q/%q match the configured preference.
Style/PercentQLiterals:
- Description: 'Checks if uses of %Q/%q match the configured preference.'
Enabled: false
+# Avoid Perl-style regex back references.
Style/PerlBackrefs:
- Description: 'Avoid Perl-style regex back references.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-perl-regexp-last-matchers'
Enabled: false
+# Check the names of predicate methods.
Style/PredicateName:
- Description: 'Check the names of predicate methods.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bool-methods-qmark'
Enabled: false
+# Use proc instead of Proc.new.
Style/Proc:
- Description: 'Use proc instead of Proc.new.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#proc'
Enabled: false
+# Checks the arguments passed to raise/fail.
Style/RaiseArgs:
- Description: 'Checks the arguments passed to raise/fail.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#exception-class-messages'
Enabled: false
+# Don't use begin blocks when they are not needed.
Style/RedundantBegin:
- Description: "Don't use begin blocks when they are not needed."
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#begin-implicit'
Enabled: false
+# Checks for an obsolete RuntimeException argument in raise/fail.
Style/RedundantException:
- Description: "Checks for an obsolete RuntimeException argument in raise/fail."
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-explicit-runtimeerror'
Enabled: false
+# Checks usages of Object#freeze on immutable objects.
+Style/RedundantFreeze:
+ Enabled: false
+
+# TODO: Enable RedundantParentheses Cop.
+# Checks for parentheses that seem not to serve any purpose.
+Style/RedundantParentheses:
+ Enabled: false
+
+# Don't use return where it's not required.
Style/RedundantReturn:
- Description: "Don't use return where it's not required."
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-explicit-return'
Enabled: true
+# Don't use self where it's not needed.
Style/RedundantSelf:
- Description: "Don't use self where it's not needed."
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-self-unless-required'
Enabled: false
+# Use %r for regular expressions matching more than `MaxSlashes` '/'
+# characters. Use %r only for regular expressions matching more
+# than `MaxSlashes` '/' character.
Style/RegexpLiteral:
- Description: >-
- Use %r for regular expressions matching more than
- `MaxSlashes` '/' characters.
- Use %r only for regular expressions matching more than
- `MaxSlashes` '/' character.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-r'
Enabled: false
+# Avoid using rescue in its modifier form.
Style/RescueModifier:
- Description: 'Avoid using rescue in its modifier form.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-rescue-modifiers'
Enabled: false
+# Checks for places where self-assignment shorthand should have been used.
Style/SelfAssignment:
- Description: >-
- Checks for places where self-assignment shorthand should have
- been used.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#self-assignment'
Enabled: false
+# Don't use semicolons to terminate expressions.
Style/Semicolon:
- Description: "Don't use semicolons to terminate expressions."
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-semicolon'
Enabled: false
+# Checks for proper usage of fail and raise.
Style/SignalException:
- Description: 'Checks for proper usage of fail and raise.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#fail-method'
Enabled: false
+# Enforces the names of some block params.
Style/SingleLineBlockParams:
- Description: 'Enforces the names of some block params.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#reduce-blocks'
Enabled: false
+# Avoid single-line methods.
Style/SingleLineMethods:
- Description: 'Avoid single-line methods.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-single-line-methods'
- Enabled: false
-
-Style/SingleSpaceBeforeFirstArg:
- Description: >-
- Checks that exactly one space is used between a method name
- and the first argument for method calls without parentheses.
Enabled: false
+# Use spaces after colons.
Style/SpaceAfterColon:
- Description: 'Use spaces after colons.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
Enabled: false
+# Use spaces after commas.
Style/SpaceAfterComma:
- Description: 'Use spaces after commas.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
- Enabled: false
-
-Style/SpaceAfterControlKeyword:
- Description: 'Use spaces after if/elsif/unless/while/until/case/when.'
Enabled: false
+# Do not put a space between a method name and the opening parenthesis in a
+# method definition.
Style/SpaceAfterMethodName:
- Description: >-
- Do not put a space between a method name and the opening
- parenthesis in a method definition.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces'
Enabled: false
+# Tracks redundant space after the ! operator.
Style/SpaceAfterNot:
- Description: Tracks redundant space after the ! operator.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-space-bang'
Enabled: false
+# Use spaces after semicolons.
Style/SpaceAfterSemicolon:
- Description: 'Use spaces after semicolons.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
Enabled: false
-Style/SpaceBeforeBlockBraces:
- Description: >-
- Checks that the left block brace has or doesn't have space
- before it.
+# Checks that the equals signs in parameter default assignments have or don't
+# have surrounding space depending on configuration.
+Style/SpaceAroundEqualsInParameterDefault:
Enabled: false
-Style/SpaceBeforeComma:
- Description: 'No spaces before commas.'
+# TODO: Enable SpaceAroundKeyword Cop.
+# Use a space around keywords if appropriate.
+Style/SpaceAroundKeyword:
Enabled: false
-Style/SpaceBeforeComment:
- Description: >-
- Checks for missing space between code and a comment on the
- same line.
+# Use a single space around operators.
+Style/SpaceAroundOperators:
Enabled: false
-Style/SpaceBeforeSemicolon:
- Description: 'No spaces before semicolons.'
+# Checks that the left block brace has or doesn't have space before it.
+Style/SpaceBeforeBlockBraces:
Enabled: false
-Style/SpaceInsideBlockBraces:
- Description: >-
- Checks that block braces have or don't have surrounding space.
- For blocks taking parameters, checks that the left brace has
- or doesn't have trailing space.
+# No spaces before commas.
+Style/SpaceBeforeComma:
Enabled: false
-Style/SpaceAroundEqualsInParameterDefault:
- Description: >-
- Checks that the equals signs in parameter default assignments
- have or don't have surrounding space depending on
- configuration.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-around-equals'
+# Checks for missing space between code and a comment on the same line.
+Style/SpaceBeforeComment:
Enabled: false
-Style/SpaceAroundOperators:
- Description: 'Use spaces around operators.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
+# Checks that exactly one space is used between a method name and the first
+# argument for method calls without parentheses.
+Style/SpaceBeforeFirstArg:
+ Enabled: false
+
+# No spaces before semicolons.
+Style/SpaceBeforeSemicolon:
Enabled: false
-Style/SpaceBeforeModifierKeyword:
- Description: 'Put a space before the modifier keyword.'
+# Checks that block braces have or don't have surrounding space.
+# For blocks taking parameters, checks that the left brace has or doesn't
+# have trailing space.
+Style/SpaceInsideBlockBraces:
Enabled: false
+# No spaces after [ or before ].
Style/SpaceInsideBrackets:
- Description: 'No spaces after [ or before ].'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces'
Enabled: false
+# Use spaces inside hash literal braces - or don't.
Style/SpaceInsideHashLiteralBraces:
- Description: "Use spaces inside hash literal braces - or don't."
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
Enabled: true
+# No spaces after ( or before ).
Style/SpaceInsideParens:
- Description: 'No spaces after ( or before ).'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces'
Enabled: false
+# No spaces inside range literals.
Style/SpaceInsideRangeLiteral:
- Description: 'No spaces inside range literals.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-space-inside-range-literals'
Enabled: false
+# Checks for padding/surrounding spaces inside string interpolation.
+Style/SpaceInsideStringInterpolation:
+ Enabled: false
+
+# Avoid Perl-style global variables.
Style/SpecialGlobalVars:
- Description: 'Avoid Perl-style global variables.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-cryptic-perlisms'
Enabled: false
+# Check for the usage of parentheses around stabby lambda arguments.
+Style/StabbyLambdaParentheses:
+ Enabled: false
+
+# Checks if uses of quotes match the configured preference.
Style/StringLiterals:
- Description: 'Checks if uses of quotes match the configured preference.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-string-literals'
Enabled: false
+# Checks if uses of quotes inside expressions in interpolated strings match the
+# configured preference.
Style/StringLiteralsInInterpolation:
- Description: >-
- Checks if uses of quotes inside expressions in interpolated
- strings match the configured preference.
Enabled: false
+# Checks if configured preferred methods are used over non-preferred.
+Style/StringMethods:
+ Enabled: false
+
+# Use %i or %I for arrays of symbols.
+Style/SymbolArray:
+ Enabled: false
+
+# Use symbols as procs instead of blocks when possible.
Style/SymbolProc:
- Description: 'Use symbols as procs instead of blocks when possible.'
Enabled: false
+# No hard tabs.
Style/Tab:
- Description: 'No hard tabs.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation'
Enabled: true
+# Checks trailing blank lines and final newline.
Style/TrailingBlankLines:
- Description: 'Checks trailing blank lines and final newline.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#newline-eof'
Enabled: true
-Style/TrailingComma:
- Description: 'Checks for trailing comma in parameter lists and literals.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas'
+# Checks for trailing comma in array and hash literals.
+Style/TrailingCommaInLiteral:
Enabled: false
+# Checks for trailing comma in argument lists.
+Style/TrailingCommaInArguments:
+ Enabled: false
+
+# Avoid trailing whitespace.
Style/TrailingWhitespace:
- Description: 'Avoid trailing whitespace.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-whitespace'
Enabled: false
+# Checks for the usage of unneeded trailing underscores at the end of
+# parallel variable assignment.
Style/TrailingUnderscoreVariable:
- Description: >-
- Checks for the usage of unneeded trailing underscores at the
- end of parallel variable assignment.
- AllowNamedUnderscoreVariables: true
Enabled: false
+# Prefer attr_* methods to trivial readers/writers.
Style/TrivialAccessors:
- Description: 'Prefer attr_* methods to trivial readers/writers.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr_family'
Enabled: false
+# Do not use unless with else. Rewrite these with the positive case first.
Style/UnlessElse:
- Description: >-
- Do not use unless with else. Rewrite these with the positive
- case first.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-else-with-unless'
Enabled: false
+# Checks for %W when interpolation is not needed.
Style/UnneededCapitalW:
- Description: 'Checks for %W when interpolation is not needed.'
Enabled: false
+# TODO: Enable UnneededInterpolation Cop.
+# Checks for strings that are just an interpolated expression.
+Style/UnneededInterpolation:
+ Enabled: false
+
+# Checks for %q/%Q when single quotes or double quotes would do.
Style/UnneededPercentQ:
- Description: 'Checks for %q/%Q when single quotes or double quotes would do.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q'
Enabled: false
+# Don't interpolate global, instance and class variables directly in strings.
Style/VariableInterpolation:
- Description: >-
- Don't interpolate global, instance and class variables
- directly in strings.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#curlies-interpolate'
Enabled: false
+# Use the configured style when naming variables.
Style/VariableName:
- Description: 'Use the configured style when naming variables.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars'
Enabled: false
+# Use when x then ... for one-line cases.
Style/WhenThen:
- Description: 'Use when x then ... for one-line cases.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#one-line-cases'
Enabled: false
+# Checks for redundant do after while or until.
Style/WhileUntilDo:
- Description: 'Checks for redundant do after while or until.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-multiline-while-do'
Enabled: false
+# Favor modifier while/until usage when you have a single-line body.
Style/WhileUntilModifier:
- Description: >-
- Favor modifier while/until usage when you have a
- single-line body.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#while-as-a-modifier'
Enabled: false
+# Use %w or %W for arrays of words.
Style/WordArray:
- Description: 'Use %w or %W for arrays of words.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-w'
Enabled: false
+# TODO: Enable ZeroLengthPredicate Cop.
+# Use #empty? when testing for objects of length 0.
+Style/ZeroLengthPredicate:
+ Enabled: false
+
+
#################### Metrics ################################
+# A calculated magnitude based on number of assignments,
+# branches, and conditions.
Metrics/AbcSize:
- Description: >-
- A calculated magnitude based on number of assignments,
- branches, and conditions.
Enabled: true
Max: 70
-Metrics/CyclomaticComplexity:
- Description: >-
- A complexity metric that is strongly correlated to the number
- of test cases needed to validate a method.
- Enabled: true
- Max: 17
-
-Metrics/PerceivedComplexity:
- Description: >-
- A complexity metric geared towards measuring complexity for a
- human reader.
- Enabled: true
- Max: 17
-
-Metrics/ParameterLists:
- Description: 'Avoid parameter lists longer than three or four parameters.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#too-many-params'
- Enabled: true
- Max: 8
-
+# Avoid excessive block nesting.
Metrics/BlockNesting:
- Description: 'Avoid excessive block nesting'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#three-is-the-number-thou-shalt-count'
Enabled: true
Max: 4
+# Avoid classes longer than 100 lines of code.
Metrics/ClassLength:
- Description: 'Avoid classes longer than 100 lines of code.'
Enabled: false
+# A complexity metric that is strongly correlated to the number
+# of test cases needed to validate a method.
+Metrics/CyclomaticComplexity:
+ Enabled: true
+ Max: 17
+
+# Limit lines to 80 characters.
Metrics/LineLength:
- Description: 'Limit lines to 80 characters.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#80-character-limits'
Enabled: false
+# Avoid methods longer than 10 lines of code.
Metrics/MethodLength:
- Description: 'Avoid methods longer than 10 lines of code.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#short-methods'
Enabled: false
+# Avoid modules longer than 100 lines of code.
Metrics/ModuleLength:
- Description: 'Avoid modules longer than 100 lines of code.'
Enabled: false
+# Avoid parameter lists longer than three or four parameters.
+Metrics/ParameterLists:
+ Enabled: true
+ Max: 8
+
+# A complexity metric geared towards measuring complexity for a human reader.
+Metrics/PerceivedComplexity:
+ Enabled: true
+ Max: 17
+
+
#################### Lint ################################
-### Warnings
+# Checks for ambiguous operators in the first argument of a method invocation
+# without parentheses.
Lint/AmbiguousOperator:
- Description: >-
- Checks for ambiguous operators in the first argument of a
- method invocation without parentheses.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-as-args'
Enabled: false
+# Checks for ambiguous regexp literals in the first argument of a method
+# invocation without parentheses.
Lint/AmbiguousRegexpLiteral:
- Description: >-
- Checks for ambiguous regexp literals in the first argument of
- a method invocation without parenthesis.
Enabled: false
+# Don't use assignment in conditions.
Lint/AssignmentInCondition:
- Description: "Don't use assignment in conditions."
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#safe-assignment-in-condition'
Enabled: false
+# Align block ends correctly.
Lint/BlockAlignment:
- Description: 'Align block ends correctly.'
Enabled: false
+# Default values in optional keyword arguments and optional ordinal arguments
+# should not refer back to the name of the argument.
+Lint/CircularArgumentReference:
+ Enabled: false
+
+# Checks for condition placed in a confusing position relative to the keyword.
Lint/ConditionPosition:
- Description: >-
- Checks for condition placed in a confusing position relative to
- the keyword.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#same-line-condition'
Enabled: false
+# Check for debugger calls.
Lint/Debugger:
- Description: 'Check for debugger calls.'
Enabled: false
+# Align ends corresponding to defs correctly.
Lint/DefEndAlignment:
- Description: 'Align ends corresponding to defs correctly.'
Enabled: false
+# Check for deprecated class method calls.
Lint/DeprecatedClassMethods:
- Description: 'Check for deprecated class method calls.'
Enabled: false
+# Check for duplicate method definitions.
+Lint/DuplicateMethods:
+ Enabled: false
+
+# Check for duplicate keys in hash literals.
+Lint/DuplicatedKey:
+ Enabled: false
+
+# Check for immutable argument given to each_with_object.
+Lint/EachWithObjectArgument:
+ Enabled: false
+
+# Check for odd code arrangement in an else block.
Lint/ElseLayout:
- Description: 'Check for odd code arrangement in an else block.'
Enabled: false
+# Checks for empty ensure block.
Lint/EmptyEnsure:
- Description: 'Checks for empty ensure block.'
Enabled: false
+# Checks for empty string interpolation.
Lint/EmptyInterpolation:
- Description: 'Checks for empty string interpolation.'
Enabled: false
+# Align ends correctly.
Lint/EndAlignment:
- Description: 'Align ends correctly.'
Enabled: false
+# END blocks should not be placed inside method definitions.
Lint/EndInMethod:
- Description: 'END blocks should not be placed inside method definitions.'
Enabled: false
+# Do not use return in an ensure block.
Lint/EnsureReturn:
- Description: 'Do not use return in an ensure block.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-return-ensure'
Enabled: false
+# The use of eval represents a serious security risk.
Lint/Eval:
- Description: 'The use of eval represents a serious security risk.'
Enabled: false
+# Catches floating-point literals too large or small for Ruby to represent.
+Lint/FloatOutOfRange:
+ Enabled: false
+
+# The number of parameters to format/sprint must match the fields.
+Lint/FormatParameterMismatch:
+ Enabled: false
+
+# Don't suppress exception.
Lint/HandleExceptions:
- Description: "Don't suppress exception."
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions'
Enabled: false
+# TODO: Enable ImplicitStringConcatenation Cop.
+# Checks for adjacent string literals on the same line, which could better be
+# represented as a single string literal.
+Lint/ImplicitStringConcatenation:
+ Enabled: false
+
+# TODO: Enable IneffectiveAccessModifier Cop.
+# Checks for attempts to use `private` or `protected` to set the visibility
+# of a class method, which does not work.
+Lint/IneffectiveAccessModifier:
+ Enabled: false
+
+# Checks for invalid character literals with a non-escaped whitespace
+# character.
Lint/InvalidCharacterLiteral:
- Description: >-
- Checks for invalid character literals with a non-escaped
- whitespace character.
Enabled: false
+# Checks of literals used in conditions.
Lint/LiteralInCondition:
- Description: 'Checks of literals used in conditions.'
Enabled: false
+# Checks for literals used in interpolation.
Lint/LiteralInInterpolation:
- Description: 'Checks for literals used in interpolation.'
Enabled: false
+# Use Kernel#loop with break rather than begin/end/until or begin/end/while
+# for post-loop tests.
Lint/Loop:
- Description: >-
- Use Kernel#loop with break rather than begin/end/until or
- begin/end/while for post-loop tests.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#loop-with-break'
Enabled: false
+# Do not use nested method definitions.
+Lint/NestedMethodDefinition:
+ Enabled: false
+
+# Do not omit the accumulator when calling `next` in a `reduce`/`inject` block.
+Lint/NextWithoutAccumulator:
+ Enabled: false
+
+# Checks for method calls with a space before the opening parenthesis.
Lint/ParenthesesAsGroupedExpression:
- Description: >-
- Checks for method calls with a space before the opening
- parenthesis.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces'
Enabled: true
+# Checks for `rand(1)` calls. Such calls always return `0` and most likely
+# a mistake.
+Lint/RandOne:
+ Enabled: false
+
+# Use parentheses in the method call to avoid confusion about precedence.
Lint/RequireParentheses:
- Description: >-
- Use parentheses in the method call to avoid confusion
- about precedence.
Enabled: false
+# Avoid rescuing the Exception class.
Lint/RescueException:
- Description: 'Avoid rescuing the Exception class.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-blind-rescues'
Enabled: true
+# Do not use the same name as outer local variable for block arguments
+# or block local variables.
Lint/ShadowingOuterLocalVariable:
- Description: >-
- Do not use the same name as outer local variable
- for block arguments or block local variables.
- Enabled: false
-
-Lint/SpaceBeforeFirstArg:
- Description: >-
- Put a space between a method name and the first argument
- in a method call without parentheses.
Enabled: false
+# 'Checks for Object#to_s usage in string interpolation.
Lint/StringConversionInInterpolation:
- Description: 'Checks for Object#to_s usage in string interpolation.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-to-s'
Enabled: false
+# Do not use prefix `_` for a variable that is used.
Lint/UnderscorePrefixedVariableName:
- Description: 'Do not use prefix `_` for a variable that is used.'
Enabled: true
+# Checks for rubocop:disable comments that can be removed.
+# Note: this cop is not disabled when disabling all cops.
+# It must be explicitly disabled.
+Lint/UnneededDisable:
+ Enabled: false
+
+# Checks for unused block arguments.
Lint/UnusedBlockArgument:
- Description: 'Checks for unused block arguments.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars'
Enabled: false
+# Checks for unused method arguments.
Lint/UnusedMethodArgument:
- Description: 'Checks for unused method arguments.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars'
Enabled: false
+# Unreachable code.
Lint/UnreachableCode:
- Description: 'Unreachable code.'
Enabled: false
+# Checks for useless access modifiers.
Lint/UselessAccessModifier:
- Description: 'Checks for useless access modifiers.'
Enabled: false
+# Checks for useless assignment to a local variable.
Lint/UselessAssignment:
- Description: 'Checks for useless assignment to a local variable.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars'
Enabled: true
+# Checks for comparison of something with itself.
Lint/UselessComparison:
- Description: 'Checks for comparison of something with itself.'
Enabled: false
+# Checks for useless `else` in `begin..end` without `rescue`.
Lint/UselessElseWithoutRescue:
- Description: 'Checks for useless `else` in `begin..end` without `rescue`.'
Enabled: false
+# Checks for useless setter call to a local variable.
Lint/UselessSetterCall:
- Description: 'Checks for useless setter call to a local variable.'
Enabled: false
+# Possible use of operator/literal/variable in void context.
Lint/Void:
- Description: 'Possible use of operator/literal/variable in void context.'
Enabled: false
+
+##################### Performance ############################
+
+# TODO: Enable Casecmp Cop.
+# Use `casecmp` rather than `downcase ==`.
+Performance/Casecmp:
+ Enabled: false
+
+# TODO: Enable DoubleStartEndWith Cop.
+# Use `str.{start,end}_with?(x, ..., y, ...)` instead of
+# `str.{start,end}_with?(x, ...) || str.{start,end}_with?(y, ...)`.
+Performance/DoubleStartEndWith:
+ Enabled: false
+
+# TODO: Enable EndWith Cop.
+# Use `end_with?` instead of a regex match anchored to the end of a string.
+Performance/EndWith:
+ Enabled: false
+
+# TODO: Enable LstripRstrip Cop.
+# Use `strip` instead of `lstrip.rstrip`.
+Performance/LstripRstrip:
+ Enabled: false
+
+# TODO: Enable RangeInclude Cop.
+# Use `Range#cover?` instead of `Range#include?`.
+Performance/RangeInclude:
+ Enabled: false
+
+# TODO: Enable RedundantBlockCall Cop.
+# Use `yield` instead of `block.call`.
+Performance/RedundantBlockCall:
+ Enabled: false
+
+# TODO: Enable RedundantMatch Cop.
+# Use `=~` instead of `String#match` or `Regexp#match` in a context where the
+# returned `MatchData` is not needed.
+Performance/RedundantMatch:
+ Enabled: false
+
+# TODO: Enable RedundantMerge Cop.
+# Use `Hash#[]=`, rather than `Hash#merge!` with a single key-value pair.
+Performance/RedundantMerge:
+ # Max number of key-value pairs to consider an offense
+ MaxKeyValuePairs: 2
+ Enabled: false
+
+# TODO: Enable RedundantSortBy Cop.
+# Use `sort` instead of `sort_by { |x| x }`.
+Performance/RedundantSortBy:
+ Enabled: false
+
+# TODO: Enable StartWith Cop.
+# Use `start_with?` instead of a regex match anchored to the beginning of a
+# string.
+Performance/StartWith:
+ Enabled: false
+# Use `tr` instead of `gsub` when you are replacing the same number of
+# characters. Use `delete` instead of `gsub` when you are deleting
+# characters.
+Performance/StringReplacement:
+ Enabled: false
+
+# TODO: Enable TimesMap Cop.
+# Checks for `.times.map` calls.
+Performance/TimesMap:
+ Enabled: false
+
+
##################### Rails ##################################
+# Enables Rails cops.
+Rails:
+ Enabled: true
+
+# Enforces consistent use of action filter methods.
Rails/ActionFilter:
- Description: 'Enforces consistent use of action filter methods.'
Enabled: true
+ EnforcedStyle: action
+# Checks the correct usage of date aware methods, such as `Date.today`,
+# `Date.current`, etc.
Rails/Date:
- Description: >-
- Checks the correct usage of date aware methods,
- such as Date.today, Date.current etc.
Enabled: false
-Rails/DefaultScope:
- Description: 'Checks if the argument passed to default_scope is a block.'
+# Prefer delegate method for delegations.
+Rails/Delegate:
Enabled: false
-Rails/Delegate:
- Description: 'Prefer delegate method for delegations.'
+# Prefer `find_by` over `where.first`.
+Rails/FindBy:
+ Enabled: false
+
+# Prefer `all.find_each` over `all.find`.
+Rails/FindEach:
Enabled: false
+# Prefer has_many :through to has_and_belongs_to_many.
Rails/HasAndBelongsToMany:
- Description: 'Prefer has_many :through to has_and_belongs_to_many.'
Enabled: true
+# Checks for calls to puts, print, etc.
Rails/Output:
- Description: 'Checks for calls to puts, print, etc.'
Enabled: true
+# Checks for incorrect grammar when using methods like `3.day.ago`.
+Rails/PluralizationGrammar:
+ Enabled: false
+
+# Checks for `read_attribute(:attr)` and `write_attribute(:attr, val)`.
Rails/ReadWriteAttribute:
- Description: >-
- Checks for read_attribute(:attr) and
- write_attribute(:attr, val).
Enabled: false
+# Checks the arguments of ActiveRecord scopes.
Rails/ScopeArgs:
- Description: 'Checks the arguments of ActiveRecord scopes.'
Enabled: false
+# Checks the correct usage of time zone aware methods.
+# http://danilenko.org/2012/7/6/rails_timezones
Rails/TimeZone:
- Description: 'Checks the correct usage of time zone aware methods.'
- StyleGuide: 'https://github.com/bbatsov/rails-style-guide#time'
- Reference: 'http://danilenko.org/2012/7/6/rails_timezones'
Enabled: false
+# Use validates :attribute, hash of validations.
Rails/Validation:
- Description: 'Use validates :attribute, hash of validations.'
Enabled: false
-
-
-# Exclude some of GitLab files
-#
-#
-AllCops:
- RunRailsCops: true
- Exclude:
- - 'vendor/**/*'
- - 'db/**/*'
- - 'tmp/**/*'
- - 'bin/**/*'
- - 'lib/backup/**/*'
- - 'lib/ci/backup/**/*'
- - 'lib/tasks/**/*'
- - 'lib/ci/migrate/**/*'
- - 'lib/email_validator.rb'
- - 'lib/gitlab/upgrader.rb'
- - 'lib/gitlab/seeder.rb'
diff --git a/.scss-lint.yml b/.scss-lint.yml
index e350b2073c3..937d3407b60 100644
--- a/.scss-lint.yml
+++ b/.scss-lint.yml
@@ -100,7 +100,7 @@ linters:
# Selectors should always use hyphenated-lowercase, rather than camelCase or
# snake_case.
SelectorFormat:
- enabled: true
+ enabled: false
convention: hyphenated_lowercase
# Prefer the shortest shorthand form possible for properties that support it.
diff --git a/CHANGELOG b/CHANGELOG
index 102908102ef..fc5e06ed94d 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,23 +1,102 @@
Please view this file on the master branch, on stable branches it's out of date.
-v 8.6.0 (unreleased)
+v 8.7.0 (unreleased)
+ - Don't attempt to fetch any tags from a forked repo (Stan Hu)
+ - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan hu)
+ - Preserve time notes/comments have been updated at when moving issue
+ - Make HTTP(s) label consistent on clone bar (Stan Hu)
+ - Expose label description in API (Mariusz Jachimowicz)
+ - Allow back dating on issues when created through the API
+ - Fix avatar stretching by providing a cropping feature
+ - Add endpoints to archive or unarchive a project !3372
+ - Add links to CI setup documentation from project settings and builds pages
+ - Handle nil descriptions in Slack issue messages (Stan Hu)
+ - Add default scope to projects to exclude projects pending deletion
+ - Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.)
+ - Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.)
+ - Gracefully handle notes on deleted commits in merge requests (Stan Hu)
+ - Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla)
+ - Remove "Congratulations!" tweet button on newly-created project. (Connor Shea)
+
+v 8.6.3
+ - Mentions on confidential issues doesn't create todos for non-members. !3374
+ - Destroy related todos when an Issue/MR is deleted. !3376
+ - Fix error 500 when target is nil on todo list. !3376
+ - Fix copying uploads when moving issue to another project. !3382
+ - Ensuring Merge Request API returns boolean values for work_in_progress (Abhi Rao). !3432
+ - Fix raw/rendered diff producing different results on merge requests. !3450
+ - Fix commit comment alignment (Stan Hu). !3466
+ - Fix Error 500 when searching for a comment in a project snippet. !3468
+ - Allow temporary email as notification email. !3477
+ - Fix issue with dropdowns not selecting values. !3478
+ - Update gitlab-shell version and doc to 2.6.12. gitlab-org/gitlab-ee!280
+
+v 8.6.2
+ - Fix dropdown alignment. !3298
+ - Fix issuable sidebar overlaps on tablet. !3299
+ - Make dropdowns pixel perfect. !3337
+ - Fix order of steps to prevent PostgreSQL errors when running migration. !3355
+ - Fix bold text in issuable sidebar. !3358
+ - Fix error with anonymous token in applications settings. !3362
+ - Fix the milestone 'upcoming' filter. !3364 + !3368
+ - Fix comments on confidential issues showing up in activity feed to non-members. !3375
+ - Fix `NoMethodError` when visiting CI root path at `/ci`. !3377
+ - Add a tooltip to new branch button in issue page. !3380
+ - Fix an issue hiding the password form when signed-in with a linked account. !3381
+ - Add links to CI setup documentation from project settings and builds pages. !3384
+ - Fix an issue with width of project select dropdown. !3386
+ - Remove redundant `require`s from Banzai files. !3391
+ - Fix error 500 with cancel button on issuable edit form. !3392 + !3417
+ - Fix background when editing a highlighted note. !3423
+ - Remove tabstop from the WIP toggle links. !3426
+ - Ensure private project snippets are not viewable by unauthorized people.
+ - Gracefully handle notes on deleted commits in merge requests (Stan Hu). !3402
+ - Fixed issue with notification settings not saving. !3452
+
+v 8.6.1
+ - Add option to reload the schema before restoring a database backup. !2807
+ - Display navigation controls on mobile. !3214
+ - Fixed bug where participants would not work correctly on merge requests. !3329
+ - Fix sorting issues by votes on the groups issues page results in SQL errors. !3333
+ - Restrict notifications for confidential issues. !3334
+ - Do not allow to move issue if it has not been persisted. !3340
+ - Add a confirmation step before deleting an issuable. !3341
+ - Fixes issue with signin button overflowing on mobile. !3342
+ - Auto collapses the navigation sidebar when resizing. !3343
+ - Fix build dependencies, when the dependency is a string. !3344
+ - Shows error messages when trying to create label in dropdown menu. !3345
+ - Fixes issue with assign milestone not loading milestone list. !3346
+ - Fix an issue causing the Dashboard/Milestones page to be blank. !3348
+
+v 8.6.0
+ - Add ability to move issue to another project
+ - Prevent tokens in the import URL to be showed by the UI
+ - Fix bug where wrong commit ID was being used in a merge request diff to show old image (Stan Hu)
+ - Add confidential issues
- Bump gitlab_git to 9.0.3 (Stan Hu)
+ - Fix diff image view modes (2-up, swipe, onion skin) not working (Stan Hu)
- Support Golang subpackage fetching (Stan Hu)
- Bump Capybara gem to 2.6.2 (Stan Hu)
- New branch button appears on issues where applicable
- Contributions to forked projects are included in calendar
- Improve the formatting for the user page bio (Connor Shea)
+ - Easily (un)mark merge request as WIP using link
+ - Use specialized system notes when MR is (un)marked as WIP
- Removed the default password from the initial admin account created during
setup. A password can be provided during setup (see installation docs), or
GitLab will ask the user to create a new one upon first visit.
- Fix issue when pushing to projects ending in .wiki
+ - Properly display YAML front matter in Markdown
- Add support for wiki with UTF-8 page names (Hiroyuki Sato)
- Fix wiki search results point to raw source (Hiroyuki Sato)
- Don't load all of GitLab in mail_room
+ - Add information about `image` and `services` field at `job` level in the `.gitlab-ci.yml` documentation (Pat Turner)
+ - HTTP error pages work independently from location and config (Artem Sidorenko)
- Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set
- Memoize @group in Admin::GroupsController (Yatish Mehta)
- Indicate how much an MR diverged from the target branch (Pierre de La Morinerie)
- Added omniauth-auth0 Gem (Daniel Carraro)
+ - Add label description in tooltip to labels in issue index and sidebar
- Strip leading and trailing spaces in URL validator (evuez)
- Add "last_sign_in_at" and "confirmed_at" to GET /users/* API endpoints for admins (evuez)
- Return empty array instead of 404 when commit has no statuses in commit status API
@@ -37,16 +116,21 @@ v 8.6.0 (unreleased)
- Fix bug where Bitbucket `closed` issues were imported as `opened` (Iuri de Silvio)
- Don't show Issues/MRs from archived projects in Groups view
- Fix wrong "iid of max iid" in Issuable sidebar for some merged MRs
+ - Fix empty source_sha on Merge Request when there is no diff (Pierre de La Morinerie)
- Increase the notes polling timeout over time (Roberto Dip)
- Add shortcut to toggle markdown preview (Florent Baldino)
- Show labels in dashboard and group milestone views
+ - Fix an issue when the target branch of a MR had been deleted
- Add main language of a project in the list of projects (Tiago Botelho)
+ - Add #upcoming filter to Milestone filter (Tiago Botelho)
- Add ability to show archived projects on dashboard, explore and group pages
- Move group activity to separate page
- Create external users which are excluded of internal and private projects unless access was explicitly granted
- Continue parameters are checked to ensure redirection goes to the same instance
- User deletion is now done in the background so the request can not time out
- Canceled builds are now ignored in compound build status if marked as `allowed to fail`
+ - Trigger a todo for mentions on commits page
+ - Let project owners and admins soft delete issues and merge requests
v 8.5.8
- Bump Git version requirement to 2.7.4
@@ -62,8 +146,6 @@ v 8.5.5
- Prevent a 500 error in Todos when author was removed
- Fix pagination for filtered dashboard and explore pages
- Fix "Show all" link behavior
- - Add #upcoming filter to Milestone filter (Tiago Botelho)
- - HTTP error pages work independently from location and config (Artem Sidorenko)
v 8.5.4
- Do not cache requests for badges (including builds badge)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7540fa1afcc..511336f384c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -16,6 +16,7 @@
- [Issue tracker guidelines](#issue-tracker-guidelines)
- [Issue weight](#issue-weight)
- [Regression issues](#regression-issues)
+ - [Technical debt](#technical-debt)
- [Merge requests](#merge-requests)
- [Merge request guidelines](#merge-request-guidelines)
- [Merge request description format](#merge-request-description-format)
@@ -242,6 +243,28 @@ addressed.
[8.3 Regressions]: https://gitlab.com/gitlab-org/gitlab-ce/issues/4127
[update the notes]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/pro-tips.md#update-the-regression-issue
+### Technical debt
+
+In order to track things that can be improved in GitLab's codebase, we created
+the ~"technical debt" label in [GitLab's issue tracker][ce-tracker].
+
+This label should be added to issues that describe things that can be improved,
+shortcuts that have been taken, code that needs refactoring, features that need
+additional attention, and all other things that have been left behind due to
+high velocity of development.
+
+Everyone can create an issue, though you may need to ask for adding a specific
+label, if you do not have permissions to do it by yourself. Additional labels
+can be combined with the `technical debt` label, to make it easier to schedule
+the improvements for a release.
+
+Issues tagged with the `technical debt` label have the same priority like issues
+that describe a new feature to be introduced in GitLab, and should be scheduled
+for a release by the appropriate person.
+
+Make sure to mention the merge request that the `technical debt` issue is
+associated with in the description of the issue.
+
## Merge requests
We welcome merge requests with fixes and improvements to GitLab code, tests,
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index bc02b8685c1..24ba9a38de6 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-2.6.11
+2.7.0
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index ef5e4454454..39e898a4f95 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-0.6.5
+0.7.1
diff --git a/Gemfile b/Gemfile
index a3fb6779e9a..006e53e0c10 100644
--- a/Gemfile
+++ b/Gemfile
@@ -51,7 +51,7 @@ gem "browser", '~> 1.0.0'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
-gem "gitlab_git", '~> 9.0'
+gem "gitlab_git", '~> 10.0'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
@@ -222,6 +222,8 @@ gem 'net-ssh', '~> 3.0.1'
# Sentry integration
gem 'sentry-raven', '~> 0.15'
+gem 'premailer-rails', '~> 1.9.0'
+
# Metrics
group :metrics do
gem 'allocations', '~> 1.0', require: false, platform: :mri
@@ -232,7 +234,7 @@ end
group :development do
gem "foreman"
- gem 'brakeman', '~> 3.1.0', require: false
+ gem 'brakeman', '~> 3.2.0', require: false
gem "annotate", "~> 2.6.0"
gem "letter_opener", '~> 1.1.2'
@@ -277,7 +279,7 @@ group :development, :test do
gem 'capybara-screenshot', '~> 1.0.0'
gem 'poltergeist', '~> 1.9.0'
- gem 'teaspoon', '~> 1.0.0'
+ gem 'teaspoon', '~> 1.1.0'
gem 'teaspoon-jasmine', '~> 2.2.0'
gem 'spring', '~> 1.6.4'
@@ -285,7 +287,7 @@ group :development, :test do
gem 'spring-commands-spinach', '~> 1.0.0'
gem 'spring-commands-teaspoon', '~> 0.0.2'
- gem 'rubocop', '~> 0.35.0', require: false
+ gem 'rubocop', '~> 0.38.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false
gem 'coveralls', '~> 0.8.2', require: false
gem 'simplecov', '~> 0.10.0', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 7b0dd83da52..bd41cc84198 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -61,9 +61,7 @@ GEM
faraday_middleware-multi_json (~> 0.0)
oauth2 (~> 1.0)
asciidoctor (1.5.3)
- ast (2.1.0)
- astrolabe (1.3.1)
- parser (~> 2.2)
+ ast (2.2.0)
attr_encrypted (1.3.4)
encryptor (>= 1.3.0)
attr_required (1.0.0)
@@ -86,21 +84,19 @@ GEM
bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
- brakeman (3.1.4)
+ brakeman (3.2.1)
erubis (~> 2.6)
- fastercsv (~> 1.5)
haml (>= 3.0, < 5.0)
highline (>= 1.6.20, < 2.0)
- multi_json (~> 1.2)
- ruby2ruby (>= 2.1.1, < 2.3.0)
- ruby_parser (~> 3.7.0)
+ ruby2ruby (~> 2.3.0)
+ ruby_parser (~> 3.8.1)
safe_yaml (>= 1.0)
sass (~> 3.0)
slim (>= 1.3.6, < 4.0)
terminal-table (~> 1.4)
browser (1.0.1)
builder (3.2.2)
- bullet (4.14.10)
+ bullet (5.0.0)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.9.0)
bundler-audit (0.4.0)
@@ -130,9 +126,9 @@ GEM
coderay (1.1.0)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
- coffee-rails (4.1.0)
+ coffee-rails (4.1.1)
coffee-script (>= 2.2.0)
- railties (>= 4.0.0, < 5.0)
+ railties (>= 4.0.0, < 5.1.x)
coffee-script (2.4.1)
coffee-script-source
execjs
@@ -150,6 +146,8 @@ GEM
crack (0.4.3)
safe_yaml (~> 1.0.0)
creole (0.5.0)
+ css_parser (1.3.7)
+ addressable
d3_rails (3.5.11)
railties (>= 3.1.0)
daemons (1.2.3)
@@ -208,7 +206,6 @@ GEM
faraday_middleware-multi_json (0.0.6)
faraday_middleware
multi_json
- fastercsv (1.5.5)
ffaker (2.0.0)
ffi (1.9.10)
fission (0.5.0)
@@ -328,8 +325,8 @@ GEM
fog-xml (0.1.2)
fog-core
nokogiri (~> 1.5, >= 1.5.11)
- font-awesome-rails (4.5.0.0)
- railties (>= 3.2, < 5.0)
+ font-awesome-rails (4.5.0.1)
+ railties (>= 3.2, < 5.1)
foreman (0.78.0)
thor (~> 0.19.1)
formatador (0.2.5)
@@ -359,11 +356,11 @@ GEM
posix-spawn (~> 0.3)
gitlab_emoji (0.3.1)
gemojione (~> 2.2, >= 2.2.1)
- gitlab_git (9.0.3)
+ gitlab_git (10.0.0)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
- rugged (~> 0.24.0b13)
+ rugged (~> 0.24.0)
gitlab_meta (7.0)
gitlab_omniauth-ldap (1.2.1)
net-ldap (~> 0.9)
@@ -423,6 +420,7 @@ GEM
haml (~> 4.0.0)
nokogiri (~> 1.6.0)
ruby_parser (~> 3.5)
+ htmlentities (4.3.4)
http-cookie (1.0.2)
domain_name (~> 0.5)
http_parser.rb (0.5.3)
@@ -554,8 +552,8 @@ GEM
orm_adapter (0.5.0)
paranoia (2.1.4)
activerecord (~> 4.0)
- parser (2.2.3.0)
- ast (>= 1.1, < 3.0)
+ parser (2.3.0.6)
+ ast (~> 2.2)
pg (0.18.4)
poltergeist (1.9.0)
capybara (~> 2.1)
@@ -564,6 +562,12 @@ GEM
websocket-driver (>= 0.2.0)
posix-spawn (0.3.11)
powerpack (0.1.1)
+ premailer (1.8.6)
+ css_parser (>= 1.3.6)
+ htmlentities (>= 4.0.0)
+ premailer-rails (1.9.0)
+ actionmailer (>= 3, < 5)
+ premailer (~> 1.7, >= 1.7.9)
pry (0.10.3)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
@@ -615,7 +619,7 @@ GEM
activesupport (= 4.2.5.2)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
- rainbow (2.0.0)
+ rainbow (2.1.0)
raindrops (0.15.0)
rake (10.5.0)
raphael-rails (2.1.2)
@@ -687,23 +691,22 @@ GEM
rspec-retry (0.4.5)
rspec-core
rspec-support (3.3.0)
- rubocop (0.35.1)
- astrolabe (~> 1.3)
- parser (>= 2.2.3.0, < 3.0)
+ rubocop (0.38.0)
+ parser (>= 2.3.0.6, < 3.0)
powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0)
ruby-progressbar (~> 1.7)
- tins (<= 1.6.0)
+ unicode-display_width (~> 1.0, >= 1.0.1)
ruby-fogbugz (0.2.1)
crack (~> 0.4)
ruby-progressbar (1.7.5)
ruby-saml (1.1.2)
nokogiri (>= 1.5.10)
uuid (~> 2.3)
- ruby2ruby (2.2.0)
+ ruby2ruby (2.3.0)
ruby_parser (~> 3.1)
sexp_processor (~> 4.0)
- ruby_parser (3.7.2)
+ ruby_parser (3.8.1)
sexp_processor (~> 4.1)
rubyntlm (0.5.2)
rubypants (0.2.0)
@@ -712,7 +715,7 @@ GEM
safe_yaml (1.0.4)
sanitize (2.1.0)
nokogiri (>= 1.4.4)
- sass (3.4.20)
+ sass (3.4.21)
sass-rails (5.0.4)
railties (>= 4.0.0, < 5.0)
sass (~> 3.1)
@@ -736,7 +739,7 @@ GEM
sentry-raven (0.15.6)
faraday (>= 0.7.6)
settingslogic (2.0.9)
- sexp_processor (4.6.0)
+ sexp_processor (4.7.0)
sham_rack (1.3.6)
rack
shoulda-matchers (2.8.0)
@@ -800,8 +803,8 @@ GEM
systemu (2.6.5)
task_list (1.0.2)
html-pipeline
- teaspoon (1.0.2)
- railties (>= 3.2.5, < 5)
+ teaspoon (1.1.5)
+ railties (>= 3.2.5, < 6)
teaspoon-jasmine (2.2.0)
teaspoon (>= 1.0.0)
temple (0.7.6)
@@ -843,6 +846,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.1)
+ unicode-display_width (1.0.2)
unicorn (4.9.0)
kgio (~> 2.6)
rack
@@ -861,7 +865,7 @@ GEM
equalizer (~> 0.0, >= 0.0.9)
warden (1.2.4)
rack (>= 1.0)
- web-console (2.2.1)
+ web-console (2.3.0)
activemodel (>= 4.0)
binding_of_caller (>= 0.7.2)
railties (>= 4.0)
@@ -903,7 +907,7 @@ DEPENDENCIES
better_errors (~> 1.0.1)
binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0)
- brakeman (~> 3.1.0)
+ brakeman (~> 3.2.0)
browser (~> 1.0.0)
bullet
bundler-audit
@@ -942,7 +946,7 @@ DEPENDENCIES
github-markup (~> 1.3.1)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab_emoji (~> 0.3.0)
- gitlab_git (~> 9.0)
+ gitlab_git (~> 10.0)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.1.0)
@@ -992,6 +996,7 @@ DEPENDENCIES
paranoia (~> 2.0)
pg (~> 0.18.2)
poltergeist (~> 1.9.0)
+ premailer-rails (~> 1.9.0)
pry-rails
quiet_assets (~> 1.0.2)
rack-attack (~> 4.3.1)
@@ -1013,7 +1018,7 @@ DEPENDENCIES
rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.3.0)
rspec-retry
- rubocop (~> 0.35.0)
+ rubocop (~> 0.38.0)
ruby-fogbugz (~> 0.2.1)
sanitize (~> 2.0)
sass-rails (~> 5.0.0)
@@ -1040,7 +1045,7 @@ DEPENDENCIES
sprockets (~> 3.3.5)
state_machines-activerecord (~> 0.3.0)
task_list (~> 1.0.2)
- teaspoon (~> 1.0.0)
+ teaspoon (~> 1.1.0)
teaspoon-jasmine (~> 2.2.0)
test_after_commit (~> 0.4.2)
thin (~> 1.6.1)
diff --git a/VERSION b/VERSION
index cac7d91adda..91ab1f99daf 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.6.0-pre
+8.7.0-pre
diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee
index 2ddf8612db3..f3ed9a66715 100644
--- a/app/assets/javascripts/api.js.coffee
+++ b/app/assets/javascripts/api.js.coffee
@@ -74,6 +74,8 @@
dataType: "json"
).done (label) ->
callback(label)
+ .error (message) ->
+ callback(message.responseJSON)
# Return group projects list. Filtered by query
groupProjects: (group_id, query, callback) ->
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index d415bbd3476..f01c67e9474 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -7,6 +7,7 @@
#= require jquery
#= require jquery-ui/autocomplete
#= require jquery-ui/datepicker
+#= require jquery-ui/draggable
#= require jquery-ui/effect-highlight
#= require jquery-ui/sortable
#= require jquery_ujs
@@ -42,6 +43,7 @@
#= require jquery.nicescroll
#= require_tree .
#= require fuzzaldrin-plus
+#= require cropper
window.slugify = (text) ->
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
@@ -138,7 +140,7 @@ $ ->
# Initialize tooltips
$('body').tooltip(
- selector: '.has_tooltip, [data-toggle="tooltip"]'
+ selector: '.has-tooltip, [data-toggle="tooltip"]'
placement: (_, el) ->
$el = $(el)
$el.data('placement') || 'bottom'
@@ -217,13 +219,20 @@ $ ->
$this = $(this)
$this.attr 'value', $this.val()
+ $sidebarGutterToggle = $('.js-sidebar-toggle')
+ $navIconToggle = $('.toggle-nav-collapse')
+
$(document)
.off 'breakpoint:change'
.on 'breakpoint:change', (e, breakpoint) ->
if breakpoint is 'sm' or breakpoint is 'xs'
- $gutterIcon = $('.js-sidebar-toggle').find('i')
+ $gutterIcon = $sidebarGutterToggle.find('i')
if $gutterIcon.hasClass('fa-angle-double-right')
- $gutterIcon.closest('a').trigger('click')
+ $sidebarGutterToggle.trigger('click')
+
+ $navIcon = $navIconToggle.find('.fa')
+ if $navIcon.hasClass('fa-angle-left')
+ $navIconToggle.trigger('click')
$(document)
.off 'click', '.js-sidebar-toggle'
diff --git a/app/assets/javascripts/aside.js.coffee b/app/assets/javascripts/aside.js.coffee
index 85473101944..66ab5054326 100644
--- a/app/assets/javascripts/aside.js.coffee
+++ b/app/assets/javascripts/aside.js.coffee
@@ -5,7 +5,6 @@ class @Aside
e.preventDefault()
btn = $(e.currentTarget)
icon = btn.find('i')
- console.log('1')
if icon.hasClass('fa-angle-left')
btn.parent().find('section').hide()
diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee
index 03a44874161..47b080406d4 100644
--- a/app/assets/javascripts/awards_handler.coffee
+++ b/app/assets/javascripts/awards_handler.coffee
@@ -122,7 +122,7 @@ class @AwardsHandler
nodes = []
nodes.push(
- "<button class='btn award-control js-emoji-btn has_tooltip active' title='me'>",
+ "<button class='btn award-control js-emoji-btn has-tooltip active' title='me'>",
"<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>",
"<span class='award-control-text js-counter'>1</span>",
"</button>"
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index 1be86e3b820..70fd6f50e9c 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -14,7 +14,6 @@ class Dispatcher
path = page.split(':')
shortcut_handler = null
-
switch page
when 'projects:issues:index'
Issues.init()
@@ -25,6 +24,8 @@ class Dispatcher
new ZenMode()
when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show'
new Milestone()
+ when 'dashboard:todos:index'
+ new Todos()
when 'projects:milestones:new', 'projects:milestones:edit'
new ZenMode()
new DropzoneInput($('.milestone-form'))
@@ -145,15 +146,11 @@ class Dispatcher
when 'project_members', 'deploy_keys', 'hooks', 'services', 'protected_branches'
shortcut_handler = new ShortcutsNavigation()
-
# If we haven't installed a custom shortcut handler, install the default one
if not shortcut_handler
new Shortcuts()
initSearch: ->
- opts = $('.search-autocomplete-opts')
- path = opts.data('autocomplete-path')
- project_id = opts.data('autocomplete-project-id')
- project_ref = opts.data('autocomplete-project-ref')
- new SearchAutocomplete(path, project_id, project_ref)
+ # Only when search form is present
+ new SearchAutocomplete() if $('.search').length
diff --git a/app/assets/javascripts/gl_crop.js.coffee b/app/assets/javascripts/gl_crop.js.coffee
new file mode 100644
index 00000000000..df9bfdfa6cc
--- /dev/null
+++ b/app/assets/javascripts/gl_crop.js.coffee
@@ -0,0 +1,152 @@
+class GitLabCrop
+ # Matches everything but the file name
+ FILENAMEREGEX = /^.*[\\\/]/
+
+ constructor: (input, opts = {}) ->
+ @fileInput = $(input)
+
+ # We should rename to avoid spec to fail
+ # Form will submit the proper input filed with a file using FormData
+ @fileInput
+ .attr('name', "#{@fileInput.attr('name')}-trigger")
+ .attr('id', "#{@fileInput.attr('id')}-trigger")
+
+ # Set defaults
+ {
+ @exportWidth = 200
+ @exportHeight = 200
+ @cropBoxWidth = 200
+ @cropBoxHeight = 200
+ @form = @fileInput.parents('form')
+
+ # Required params
+ @filename
+ @previewImage
+ @modalCrop
+ @pickImageEl
+ @uploadImageBtn
+ @modalCropImg
+ } = opts
+
+ # Ensure needed elements are jquery objects
+ # If selector is provided we will convert them to a jQuery Object
+ @filename = @getElement(@filename)
+ @previewImage = @getElement(@previewImage)
+ @pickImageEl = @getElement(@pickImageEl)
+
+ # Modal elements usually are outside the @form element
+ @modalCrop = if _.isString(@modalCrop) then $(@modalCrop) else @modalCrop
+ @uploadImageBtn = if _.isString(@uploadImageBtn) then $(@uploadImageBtn) else @uploadImageBtn
+ @modalCropImg = if _.isString(@modalCropImg) then $(@modalCropImg) else @modalCropImg
+
+ @cropActionsBtn = @modalCrop.find('[data-method]')
+
+ @bindEvents()
+
+ getElement: (selector) ->
+ $(selector, @form)
+
+ bindEvents: ->
+ _this = @
+ @fileInput.on 'change', (e) ->
+ _this.onFileInputChange(e, @)
+
+ @pickImageEl.on 'click', @onPickImageClick
+ @modalCrop.on 'shown.bs.modal', @onModalShow
+ @modalCrop.on 'hidden.bs.modal', @onModalHide
+ @uploadImageBtn.on 'click', @onUploadImageBtnClick
+ @cropActionsBtn.on 'click', (e) ->
+ btn = @
+ _this.onActionBtnClick(btn)
+ @croppedImageBlob = null
+
+ onPickImageClick: =>
+ @fileInput.trigger('click')
+
+ onModalShow: =>
+ _this = @
+ @modalCropImg.cropper(
+ viewMode: 1
+ center: false
+ aspectRatio: 1
+ modal: true
+ scalable: false
+ rotatable: false
+ zoomable: true
+ dragMode: 'move'
+ guides: false
+ zoomOnTouch: false
+ zoomOnWheel: false
+ cropBoxMovable: false
+ cropBoxResizable: false
+ toggleDragModeOnDblclick: false
+ built: ->
+ $image = $(@)
+ container = $image.cropper 'getContainerData'
+ cropBoxWidth = _this.cropBoxWidth;
+ cropBoxHeight = _this.cropBoxHeight;
+
+ $image.cropper('setCropBoxData',
+ width: cropBoxWidth,
+ height: cropBoxHeight,
+ left: (container.width - cropBoxWidth) / 2,
+ top: (container.height - cropBoxHeight) / 2
+ )
+ )
+
+
+ onModalHide: =>
+ @modalCropImg
+ .attr('src', '') # Remove attached image
+ .cropper('destroy') # Destroy cropper instance
+
+ onUploadImageBtnClick: (e) =>
+ e.preventDefault()
+ @setBlob()
+ @setPreview()
+ @modalCrop.modal('hide')
+ @fileInput.val('')
+
+ onActionBtnClick: (btn) ->
+ data = $(btn).data()
+
+ if @modalCropImg.data('cropper') && data.method
+ result = @modalCropImg.cropper data.method, data.option
+
+ onFileInputChange: (e, input) ->
+ @readFile(input)
+
+ readFile: (input) ->
+ _this = @
+ reader = new FileReader
+ reader.onload = ->
+ _this.modalCropImg.attr('src', reader.result)
+ _this.modalCrop.modal('show')
+
+ reader.readAsDataURL(input.files[0])
+
+ dataURLtoBlob: (dataURL) ->
+ binary = atob(dataURL.split(',')[1])
+ array = []
+ for v, k in binary
+ array.push(binary.charCodeAt(k))
+ new Blob([new Uint8Array(array)], type: 'image/png')
+
+ setPreview: ->
+ @previewImage.attr('src', @dataURL)
+ filename = @fileInput.val().replace(FILENAMEREGEX, '')
+ @filename.text(filename)
+
+ setBlob: ->
+ @dataURL = @modalCropImg.cropper('getCroppedCanvas',
+ width: 200
+ height: 200
+ ).toDataURL('image/png')
+ @croppedImageBlob = @dataURLtoBlob(@dataURL)
+
+ getBlob: ->
+ @croppedImageBlob
+
+$.fn.glCrop = (opts) ->
+ return @.each ->
+ $(@).data('glcrop', new GitLabCrop(@, opts))
diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
index 4f038477755..4f032a82e58 100644
--- a/app/assets/javascripts/gl_dropdown.js.coffee
+++ b/app/assets/javascripts/gl_dropdown.js.coffee
@@ -1,13 +1,33 @@
class GitLabDropdownFilter
BLUR_KEYCODES = [27, 40]
+ HAS_VALUE_CLASS = "has-value"
- constructor: (@dropdown, @options) ->
- @input = @dropdown.find(".dropdown-input .dropdown-input-field")
+ constructor: (@input, @options) ->
+ {
+ @filterInputBlur = true
+ } = @options
+
+ $inputContainer = @input.parent()
+ $clearButton = $inputContainer.find('.js-dropdown-input-clear')
+
+ # Clear click
+ $clearButton.on 'click', (e) =>
+ e.preventDefault()
+ e.stopPropagation()
+ @input
+ .val('')
+ .trigger('keyup')
+ .focus()
# Key events
timeout = ""
@input.on "keyup", (e) =>
- if e.keyCode is 13 && @input.val() isnt ""
+ if @input.val() isnt "" and !$inputContainer.hasClass HAS_VALUE_CLASS
+ $inputContainer.addClass HAS_VALUE_CLASS
+ else if @input.val() is "" and $inputContainer.hasClass HAS_VALUE_CLASS
+ $inputContainer.removeClass HAS_VALUE_CLASS
+
+ if e.keyCode is 13 and @input.val() isnt ""
if @options.enterCallback
@options.enterCallback()
return
@@ -17,7 +37,7 @@ class GitLabDropdownFilter
blur_field = @shouldBlur e.keyCode
search_text = @input.val()
- if blur_field
+ if blur_field and @filterInputBlur
@input.blur()
if @options.remote
@@ -77,25 +97,48 @@ class GitLabDropdown
PAGE_TWO_CLASS = "is-page-two"
ACTIVE_CLASS = "is-active"
+ FILTER_INPUT = '.dropdown-input .dropdown-input-field'
+
constructor: (@el, @options) ->
- self = @
@dropdown = $(@el).parent()
+
+ # Set Defaults
+ {
+ # If no input is passed create a default one
+ @filterInput = @getElement(FILTER_INPUT)
+ @highlight = false
+ @filterInputBlur = true
+ @enterCallback = true
+ } = @options
+
+ self = @
+
+ # If selector was passed
+ if _.isString(@filterInput)
+ @filterInput = @getElement(@filterInput)
+
search_fields = if @options.search then @options.search.fields else [];
if @options.data
- # Remote data
- @remote = new GitLabDropdownRemote @options.data, {
- dataType: @options.dataType,
- beforeSend: @toggleLoading.bind(@)
- success: (data) =>
- @fullData = data
+ # If data is an array
+ if _.isArray @options.data
+ @fullData = @options.data
+ @parseData @options.data
+ else
+ # Remote data
+ @remote = new GitLabDropdownRemote @options.data, {
+ dataType: @options.dataType,
+ beforeSend: @toggleLoading.bind(@)
+ success: (data) =>
+ @fullData = data
- @parseData @fullData
- }
+ @parseData @fullData
+ }
- # Init filiterable
+ # Init filterable
if @options.filterable
- @filter = new GitLabDropdownFilter @dropdown,
+ @filter = new GitLabDropdownFilter @filterInput,
+ filterInputBlur: @filterInputBlur
remote: @options.filterRemote
query: @options.data
keys: @options.search.fields
@@ -103,12 +146,16 @@ class GitLabDropdown
return @fullData
callback: (data) =>
@parseData data
+ @highlightRow 1
enterCallback: =>
- @selectFirstRow()
+ if @enterCallback
+ @selectFirstRow()
# Event listeners
+
@dropdown.on "shown.bs.dropdown", @opened
@dropdown.on "hidden.bs.dropdown", @hidden
+ @dropdown.on "click", ".dropdown-menu, .dropdown-menu-close", @shouldPropagate
if @dropdown.find(".dropdown-toggle-page").length
@dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) =>
@@ -124,10 +171,14 @@ class GitLabDropdown
selector = ".dropdown-page-one .dropdown-content a"
@dropdown.on "click", selector, (e) ->
- self.rowClicked $(@)
+ selected = self.rowClicked $(@)
if self.options.clicked
- self.options.clicked()
+ self.options.clicked(selected)
+
+ # Finds an element inside wrapper element
+ getElement: (selector) ->
+ @dropdown.find selector
toggleLoading: ->
$('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS
@@ -157,21 +208,41 @@ class GitLabDropdown
@appendMenu(full_html)
+ shouldPropagate: (e) =>
+ if @options.multiSelect
+ $target = $(e.target)
+ if not $target.hasClass('dropdown-menu-close') and not $target.hasClass('dropdown-menu-close-icon')
+ e.stopPropagation()
+ return false
+ else
+ return true
+
opened: =>
contentHtml = $('.dropdown-content', @dropdown).html()
if @remote && contentHtml is ""
@remote.execute()
if @options.filterable
- @dropdown.find(".dropdown-input-field").focus()
+ @filterInput.focus()
- hidden: =>
+ @dropdown.trigger('shown.gl.dropdown')
+
+ hidden: (e) =>
if @options.filterable
- @dropdown.find(".dropdown-input-field").blur().val("")
+ @dropdown
+ .find(".dropdown-input-field")
+ .blur()
+ .val("")
+ .trigger("keyup")
if @dropdown.find(".dropdown-toggle-page").length
$('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS
+ if @options.hidden
+ @options.hidden.call(@,e)
+
+ @dropdown.trigger('hidden.gl.dropdown')
+
# Render the full menu
renderMenu: (html) ->
@@ -196,20 +267,46 @@ class GitLabDropdown
renderItem: (data) ->
html = ""
+ # Divider
return "<li class='divider'></li>" if data is "divider"
+ # Separator is a full-width divider
+ return "<li class='separator'></li>" if data is "separator"
+
+ # Header
+ return "<li class='dropdown-header'>#{data.header}</li>" if data.header?
+
if @options.renderRow
# Call the render function
html = @options.renderRow(data)
else
- selected = if @options.isSelected then @options.isSelected(data) else false
- url = if @options.url then @options.url(data) else "#"
- text = if @options.text then @options.text(data) else ""
+ if not selected
+ value = if @options.id then @options.id(data) else data.id
+ fieldName = @options.fieldName
+ field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']")
+ if field.length
+ selected = true
+
+ # Set URL
+ if @options.url?
+ url = @options.url(data)
+ else
+ url = if data.url? then data.url else '#'
+
+ # Set Text
+ if @options.text?
+ text = @options.text(data)
+ else
+ text = if data.text? then data.text else ''
+
cssClass = "";
if selected
cssClass = "is-active"
+ if @highlight
+ text = @highlightTextMatches(text, @filterInput.val())
+
html = "<li>"
html += "<a href='#{url}' class='#{cssClass}'>"
html += text
@@ -218,55 +315,79 @@ class GitLabDropdown
return html
+ highlightTextMatches: (text, term) ->
+ occurrences = fuzzaldrinPlus.match(text, term)
+ text.split('').map((character, i) ->
+ if i in occurrences then "<b>#{character}</b>" else character
+ ).join('')
+
noResults: ->
html = "<li>"
- html += "<a href='#' class='is-focused'>"
+ html += "<a class='dropdown-menu-empty-link is-focused'>"
html += "No matching results."
html += "</a>"
html += "</li>"
+ highlightRow: (index) ->
+ if @filterInput.val() isnt ""
+ selector = '.dropdown-content li:first-child a'
+ if @dropdown.find(".dropdown-toggle-page").length
+ selector = ".dropdown-page-one .dropdown-content li:first-child a"
+
+ @getElement(selector).addClass 'is-focused'
+
rowClicked: (el) ->
fieldName = @options.fieldName
- field = @dropdown.parent().find("input[name='#{fieldName}']")
+ selectedIndex = el.parent().index()
+ if @renderedData
+ selectedObject = @renderedData[selectedIndex]
+ value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
+ field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']")
if el.hasClass(ACTIVE_CLASS)
+ el.removeClass(ACTIVE_CLASS)
field.remove()
- else
- fieldName = @options.fieldName
- selectedIndex = el.parent().index()
- if @renderedData
- selectedObject = @renderedData[selectedIndex]
- value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
+ # Toggle the dropdown label
+ if @options.toggleLabel
+ $(@el).find(".dropdown-toggle-text").text @options.toggleLabel
+ else
if !value?
field.remove()
- if @options.multiSelect
- oldValue = field.val()
- if oldValue
- value = "#{oldValue},#{value}"
- else
- @dropdown.find(ACTIVE_CLASS).removeClass ACTIVE_CLASS
+ if not @options.multiSelect
+ @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS
+ @dropdown.parent().find("input[name='#{fieldName}']").remove()
# Toggle active class for the tick mark
- el.toggleClass "is-active"
+ el.addClass ACTIVE_CLASS
+ # Toggle the dropdown label
+ if @options.toggleLabel
+ $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject)
if value?
if !field.length
# Create hidden input for form
- input = "<input type='hidden' name='#{fieldName}' />"
+ input = "<input type='hidden' name='#{fieldName}' value='#{value}' />"
+ if @options.inputId?
+ input = $(input)
+ .attr('id', @options.inputId)
@dropdown.before input
+ else
+ field.val value
- @dropdown.parent().find("input[name='#{fieldName}']").val value
+ return selectedObject
selectFirstRow: ->
selector = '.dropdown-content li:first-child a'
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content li:first-child a"
- # similute a click on the first link
+ # simulate a click on the first link
$(selector).trigger "click"
$.fn.glDropdown = (opts) ->
return @.each ->
- new GitLabDropdown @, opts
+ if (!$.data @, 'glDropdown')
+ $.data(@, 'glDropdown', new GitLabDropdown @, opts)
+
diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee
index e52b73f94f6..2f19513a831 100644
--- a/app/assets/javascripts/issuable_context.js.coffee
+++ b/app/assets/javascripts/issuable_context.js.coffee
@@ -1,8 +1,7 @@
-#= require jquery.waitforimages
-
class @IssuableContext
- constructor: ->
- new UsersSelect()
+ constructor: (currentUser) ->
+ @initParticipants()
+ new UsersSelect(currentUser)
$('select.select2').select2({width: 'resolve', dropdownAutoWidth: true})
$(".issuable-sidebar .inline-update").on "change", "select", ->
@@ -10,10 +9,44 @@ class @IssuableContext
$(".issuable-sidebar .inline-update").on "change", ".js-assignee", ->
$(this).submit()
- $(document).on "click",".edit-link", (e) ->
- block = $(@).parents('.block')
- block.find('.selectbox').show()
- block.find('.value').hide()
- block.find('.js-select2').select2("open")
+ $(document).off("click", ".edit-link").on "click",".edit-link", (e) ->
+ $block = $(@).parents('.block')
+ $selectbox = $block.find('.selectbox')
+ if $selectbox.is(':visible')
+ $selectbox.hide()
+ $block.find('.value').show()
+ else
+ $selectbox.show()
+ $block.find('.value').hide()
+
+ if $selectbox.is(':visible')
+ setTimeout (->
+ $block.find('.dropdown-menu-toggle').trigger 'click'
+ ), 0
+
$(".right-sidebar").niceScroll()
+
+ initParticipants: ->
+ _this = @
+ $(document).on "click", ".js-participants-more", @toggleHiddenParticipants
+
+ $(".js-participants-author").each (i) ->
+ if i >= _this.PARTICIPANTS_ROW_COUNT
+ $(@)
+ .addClass "js-participants-hidden"
+ .hide()
+
+ toggleHiddenParticipants: (e) ->
+ e.preventDefault()
+
+ currentText = $(this).text().trim()
+ lessText = $(this).data("less-text")
+ originalText = $(this).data("original-text")
+
+ if currentText is originalText
+ $(this).text(lessText)
+ else
+ $(this).text(originalText)
+
+ $(".js-participants-hidden").toggle()
diff --git a/app/assets/javascripts/issuable_form.js.coffee b/app/assets/javascripts/issuable_form.js.coffee
index 48c249943f2..7a788f761b7 100644
--- a/app/assets/javascripts/issuable_form.js.coffee
+++ b/app/assets/javascripts/issuable_form.js.coffee
@@ -1,4 +1,7 @@
class @IssuableForm
+ issueMoveConfirmMsg: 'Are you sure you want to move this issue to another project?'
+ wipRegex: /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i
+
constructor: (@form) ->
GitLab.GfmAutoComplete.setup()
new UsersSelect()
@@ -6,14 +9,17 @@ class @IssuableForm
@titleField = @form.find("input[name*='[title]']")
@descriptionField = @form.find("textarea[name*='[description]']")
+ @issueMoveField = @form.find("#move_to_project_id")
return unless @titleField.length && @descriptionField.length
@initAutosave()
- @form.on "submit", @resetAutosave
+ @form.on "submit", @handleSubmit
@form.on "click", ".btn-cancel", @resetAutosave
+ @initWip()
+
initAutosave: ->
new Autosave @titleField, [
document.location.pathname,
@@ -27,6 +33,50 @@ class @IssuableForm
"description"
]
+ handleSubmit: =>
+ if (parseInt(@issueMoveField?.val()) ? 0) > 0
+ return false unless confirm(@issueMoveConfirmMsg)
+
+ @resetAutosave()
+
resetAutosave: =>
@titleField.data("autosave").reset()
@descriptionField.data("autosave").reset()
+
+ initWip: ->
+ @$wipExplanation = @form.find(".js-wip-explanation")
+ @$noWipExplanation = @form.find(".js-no-wip-explanation")
+ return unless @$wipExplanation.length and @$noWipExplanation.length
+
+ @form.on "click", ".js-toggle-wip", @toggleWip
+
+ @titleField.on "keyup blur", @renderWipExplanation
+
+ @renderWipExplanation()
+
+ workInProgress: ->
+ @wipRegex.test @titleField.val()
+
+ renderWipExplanation: =>
+ if @workInProgress()
+ @$wipExplanation.show()
+ @$noWipExplanation.hide()
+ else
+ @$wipExplanation.hide()
+ @$noWipExplanation.show()
+
+ toggleWip: (event) =>
+ event.preventDefault()
+
+ if @workInProgress()
+ @removeWip()
+ else
+ @addWip()
+
+ @renderWipExplanation()
+
+ removeWip: ->
+ @titleField.val @titleField.val().replace(@wipRegex, "")
+
+ addWip: ->
+ @titleField.val "WIP: #{@titleField.val()}"
diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee
index a0acf3028bf..b1479bfb449 100644
--- a/app/assets/javascripts/issues.js.coffee
+++ b/app/assets/javascripts/issues.js.coffee
@@ -1,7 +1,6 @@
@Issues =
init: ->
Issues.initSearch()
- Issues.initSelects()
Issues.initChecks()
$("body").on "ajax:success", ".close_issue, .reopen_issue", ->
@@ -17,18 +16,9 @@
$(this).html totalIssues - 1
reload: ->
- Issues.initSelects()
Issues.initChecks()
$('#filter_issue_search').val($('#issue_search').val())
- initSelects: ->
- $("select#update_state_event").select2(width: 'resolve', dropdownAutoWidth: true)
- $("select#update_assignee_id").select2(width: 'resolve', dropdownAutoWidth: true)
- $("select#update_milestone_id").select2(width: 'resolve', dropdownAutoWidth: true)
- $("select#label_name").select2(width: 'resolve', dropdownAutoWidth: true)
- $("#milestone_id, #assignee_id, #label_name").on "change", ->
- $(this).closest("form").submit()
-
initChecks: ->
$(".check_all_issues").click ->
$(".selected_issue").prop("checked", @checked)
@@ -41,24 +31,28 @@
@timer = null
$("#issue_search").keyup ->
clearTimeout(@timer)
- @timer = setTimeout(Issues.filterResults, 500)
+ @timer = setTimeout( ->
+ Issues.filterResults $("#issue_search_form")
+ , 500)
- filterResults: =>
- form = $("#issue_search_form")
- search = $("#issue_search").val()
- $('.issues-holder').css("opacity", '0.5')
- issues_url = form.attr('action') + '?' + form.serialize()
+ filterResults: (form) =>
+ $('.issues-holder, .merge-requests-holder').css("opacity", '0.5')
+ formAction = form.attr('action')
+ formData = form.serialize()
+ issuesUrl = formAction
+ issuesUrl += ("#{if formAction.indexOf("?") < 0 then '?' else '&'}")
+ issuesUrl += formData
$.ajax
type: "GET"
- url: form.attr('action')
- data: form.serialize()
+ url: formAction
+ data: formData
complete: ->
- $('.issues-holder').css("opacity", '1.0')
+ $('.issues-holder, .merge-requests-holder').css("opacity", '1.0')
success: (data) ->
- $('.issues-holder').html(data.html)
+ $('.issues-holder, .merge-requests-holder').html(data.html)
# Change url so if user reload a page - search results are saved
- history.replaceState {page: issues_url}, document.title, issues_url
+ history.replaceState {page: issuesUrl}, document.title, issuesUrl
Issues.reload()
dataType: "json"
diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee
index 5ade2cb66cb..d1fe116397a 100644
--- a/app/assets/javascripts/labels_select.js.coffee
+++ b/app/assets/javascripts/labels_select.js.coffee
@@ -1,30 +1,86 @@
class @LabelsSelect
constructor: ->
$('.js-label-select').each (i, dropdown) ->
- projectId = $(dropdown).data('project-id')
- labelUrl = $(dropdown).data("labels")
- selectedLabel = $(dropdown).data('selected')
- if selectedLabel
- selectedLabel = selectedLabel.split(",")
+ $dropdown = $(dropdown)
+ projectId = $dropdown.data('project-id')
+ labelUrl = $dropdown.data('labels')
+ issueUpdateURL = $dropdown.data('issueUpdate')
+ selectedLabel = $dropdown.data('selected')
+ if selectedLabel?
+ selectedLabel = selectedLabel.split(',')
newLabelField = $('#new_label_name')
newColorField = $('#new_label_color')
- showNo = $(dropdown).data('show-no')
- showAny = $(dropdown).data('show-any')
+ showNo = $dropdown.data('show-no')
+ showAny = $dropdown.data('show-any')
+ defaultLabel = $dropdown.data('default-label')
+ abilityName = $dropdown.data('ability-name')
+ $selectbox = $dropdown.closest('.selectbox')
+ $block = $selectbox.closest('.block')
+ $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span')
+ $value = $block.find('.value')
+ $loading = $block.find('.block-loading').fadeOut()
if newLabelField.length
+ $newLabelCreateButton = $('.js-new-label-btn')
+ $colorPreview = $('.js-dropdown-label-color-preview')
+ $newLabelError = $dropdown.parent().find('.js-label-error')
+ $newLabelError.hide()
+
+ # Suggested colors in the dropdown to chose from pre-chosen colors
+ $('.suggest-colors-dropdown a').on 'click', (e) ->
+
+ issueURLSplit = issueUpdateURL.split('/') if issueUpdateURL?
+ if issueUpdateURL
+ labelHTMLTemplate = _.template(
+ '<% _.each(labels, function(label){ %>
+ <a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name=<%= label.title %>">
+ <span class="label color-label" style="background-color: <%= label.color %>;">
+ <%= label.title %>
+ </span>
+ </a>
+ <% }); %>'
+ );
+ labelNoneHTMLTemplate = _.template('<div class="light">None</div>')
+
+ if newLabelField.length and $dropdown.hasClass 'js-extra-options'
$('.suggest-colors-dropdown a').on "click", (e) ->
e.preventDefault()
e.stopPropagation()
- newColorField.val $(this).data("color")
- $('.js-dropdown-label-color-preview')
- .css 'background-color', $(this).data("color")
+ newColorField
+ .val($(this).data('color'))
+ .trigger('change')
+ $colorPreview
+ .css 'background-color', $(this).data('color')
+ .parent()
.addClass 'is-active'
- $('.js-new-label-btn').on "click", (e) ->
+ # Cancel button takes back to first page
+ resetForm = ->
+ newLabelField
+ .val ''
+ .trigger 'change'
+ newColorField
+ .val ''
+ .trigger 'change'
+ $colorPreview
+ .css 'background-color', ''
+ .parent()
+ .removeClass 'is-active'
+
+ $('.dropdown-menu-back').on 'click', ->
+ resetForm()
+
+ $('.js-cancel-label-btn').on 'click', (e) ->
e.preventDefault()
e.stopPropagation()
+ resetForm()
+ $('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
- if newLabelField.val() isnt "" && newColorField.val() isnt ""
+ # Listen for change and keyup events on label and color field
+ # This allows us to enable the button when ready
+ enableLabelCreateButton = ->
+ if newLabelField.val() isnt '' and newColorField.val() isnt ''
+ $newLabelError.hide()
$('.js-new-label-btn').disable()
# Create new label with API
@@ -33,49 +89,126 @@ class @LabelsSelect
color: newColorField.val()
}, (label) ->
$('.js-new-label-btn').enable()
- $('.dropdown-menu-back', $(dropdown).parent()).trigger "click"
- $(dropdown).glDropdown(
+ if label.message?
+ $newLabelError
+ .text label.message
+ .show()
+ else
+ $('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
+
+ $newLabelCreateButton.enable()
+ else
+ $newLabelCreateButton.disable()
+
+ newLabelField.on 'keyup change', enableLabelCreateButton
+
+ newColorField.on 'keyup change', enableLabelCreateButton
+
+ # Send the API call to create the label
+ $newLabelCreateButton
+ .disable()
+ .on 'click', (e) ->
+ e.preventDefault()
+ e.stopPropagation()
+
+ if newLabelField.val() isnt '' and newColorField.val() isnt ''
+ $newLabelError.hide()
+ $('.js-new-label-btn').disable()
+
+ # Create new label with API
+ Api.newLabel projectId, {
+ name: newLabelField.val()
+ color: newColorField.val()
+ }, (label) ->
+ $('.js-new-label-btn').enable()
+
+ if label.message?
+ $newLabelError
+ .text label.message
+ .show()
+ else
+ $('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
+
+ saveLabelData = ->
+ selected = $dropdown
+ .closest('.selectbox')
+ .find("input[name='#{$dropdown.data('field-name')}']")
+ .map(->
+ @value
+ ).get()
+ data = {}
+ data[abilityName] = {}
+ data[abilityName].label_ids = selected
+ if not selected.length
+ data[abilityName].label_ids = ['']
+ $loading.fadeIn()
+ $dropdown.trigger('loading.gl.dropdown')
+ $.ajax(
+ type: 'PUT'
+ url: issueUpdateURL
+ dataType: 'JSON'
+ data: data
+ ).done (data) ->
+ $loading.fadeOut()
+ $dropdown.trigger('loaded.gl.dropdown')
+ $selectbox.hide()
+ data.issueURLSplit = issueURLSplit
+ labelCount = 0
+ if data.labels.length
+ template = labelHTMLTemplate(data)
+ labelCount = data.labels.length
+ else
+ template = labelNoneHTMLTemplate()
+ $value
+ .removeAttr('style')
+ .html(template)
+ $sidebarCollapsedValue.text(labelCount)
+
+ $value
+ .find('a')
+ .each((i) ->
+ setTimeout(=>
+ glAnimate($(@), 'pulse')
+ ,200 * i
+ )
+ )
+
+
+ $dropdown.glDropdown(
data: (term, callback) ->
- # We have to fetch the JS version of the labels list because there is no
- # public facing JSON url for labels
$.ajax(
url: labelUrl
).done (data) ->
- html = $(data)
- data = []
- html.find('.label-row a').each ->
- data.push(
- title: $(@).text().trim()
- )
+ if $dropdown.hasClass 'js-extra-options'
+ if showNo
+ data.unshift(
+ id: 0
+ title: 'No Label'
+ )
- if showNo
- data.unshift(
- id: "0"
- title: 'No label'
- )
-
- if showAny
- data.unshift(
- title: 'Any label'
- )
-
- if data.length > 2
- data.splice 2, 0, "divider"
+ if showAny
+ data.unshift(
+ isAny: true
+ title: 'Any Label'
+ )
+ if data.length > 2
+ data.splice 2, 0, 'divider'
callback data
+
renderRow: (label) ->
- if $.isArray(selectedLabel)
- selected = ""
- $.each selectedLabel, (i, selectedLbl) ->
- selectedLbl = selectedLbl.trim()
- if selected is "" && label.title is selectedLbl
- selected = "is-active"
- else
- selected = if label.title is selectedLabel then "is-active" else ""
+ selectedClass = ''
+ if $selectbox.find("input[type='hidden']\
+ [name='#{$dropdown.data('field-name')}']\
+ [value='#{label.id}']").length
+ selectedClass = 'is-active'
+
+ color = if label.color? then "<span class='dropdown-label-box' style='background-color: #{label.color}'></span>" else ""
"<li>
- <a href='#' class='#{selected}'>
+ <a href='#' class='#{selectedClass}'>
+ #{color}
#{label.title}
</a>
</li>"
@@ -83,10 +216,43 @@ class @LabelsSelect
search:
fields: ['title']
selectable: true
- fieldName: $(dropdown).data('field-name')
+
+ toggleLabel: (selected) ->
+ if selected and selected.title isnt 'Any Label'
+ selected.title
+ else
+ defaultLabel
+ fieldName: $dropdown.data('field-name')
id: (label) ->
- label.title
- clicked: ->
- if $(dropdown).hasClass "js-filter-submit"
- $(dropdown).parents('form').submit()
+ if label.isAny?
+ ''
+ else if $dropdown.hasClass "js-filter-submit"
+ label.title
+ else
+ label.id
+
+ hidden: ->
+ $selectbox.hide()
+ # display:block overrides the hide-collapse rule
+ $value.removeAttr('style')
+ if $dropdown.hasClass 'js-multiselect'
+ saveLabelData()
+
+ multiSelect: $dropdown.hasClass 'js-multiselect'
+ clicked: (label) ->
+ page = $('body').data 'page'
+ isIssueIndex = page is 'projects:issues:index'
+ isMRIndex = page is page is 'projects:merge_requests:index'
+
+ if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
+ selectedLabel = label.title
+
+ Issues.filterResults $dropdown.closest('form')
+ else if $dropdown.hasClass 'js-filter-submit'
+ $dropdown.closest('form').submit()
+ else
+ if $dropdown.hasClass 'js-multiselect'
+ return
+ else
+ saveLabelData()
)
diff --git a/app/assets/javascripts/lib/animate.js.coffee b/app/assets/javascripts/lib/animate.js.coffee
new file mode 100644
index 00000000000..8f892b5a2b9
--- /dev/null
+++ b/app/assets/javascripts/lib/animate.js.coffee
@@ -0,0 +1,13 @@
+((w) ->
+
+ w.glAnimate = ($el, animation, done) ->
+ $el
+ .removeClass()
+ .addClass(animation + ' animated')
+ .one 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', ->
+ $(this).removeClass()
+ return
+ return
+ return
+
+) window \ No newline at end of file
diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee
index 8322b4c46ad..839e6ec2c08 100644
--- a/app/assets/javascripts/merge_request_tabs.js.coffee
+++ b/app/assets/javascripts/merge_request_tabs.js.coffee
@@ -3,6 +3,8 @@
# Handles persisting and restoring the current tab selection and lazily-loading
# content on the MergeRequests#show page.
#
+#= require jquery.cookie
+#
# ### Example Markup
#
# <ul class="nav-links merge-request-tabs">
@@ -68,11 +70,15 @@ class @MergeRequestTabs
if action == 'commits'
@loadCommits($target.attr('href'))
+ @expandView()
else if action == 'diffs'
@loadDiff($target.attr('href'))
@shrinkView()
else if action == 'builds'
@loadBuilds($target.attr('href'))
+ @expandView()
+ else
+ @expandView()
@setCurrentAction(action)
@@ -189,11 +195,24 @@ class @MergeRequestTabs
$('.container-fluid').removeClass('container-limited')
shrinkView: ->
- $gutterIcon = $('.js-sidebar-toggle i')
+ $gutterIcon = $('.js-sidebar-toggle i:visible')
# Wait until listeners are set
setTimeout( ->
- # Only when sidebar is collapsed
+ # Only when sidebar is expanded
if $gutterIcon.is('.fa-angle-double-right')
- $gutterIcon.closest('a').trigger('click',[true])
+ $gutterIcon.closest('a').trigger('click', [true])
+ , 0)
+
+ # Expand the issuable sidebar unless the user explicitly collapsed it
+ expandView: ->
+ return if $.cookie('collapsed_gutter') == 'true'
+
+ $gutterIcon = $('.js-sidebar-toggle i:visible')
+
+ # Wait until listeners are set
+ setTimeout( ->
+ # Only when sidebar is collapsed
+ if $gutterIcon.is('.fa-angle-double-left')
+ $gutterIcon.closest('a').trigger('click', [true])
, 0)
diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee
index 5e884454a65..f73127f49f0 100644
--- a/app/assets/javascripts/milestone_select.js.coffee
+++ b/app/assets/javascripts/milestone_select.js.coffee
@@ -1,60 +1,124 @@
class @MilestoneSelect
- constructor: ->
+ constructor: (currentProject) ->
+ if currentProject?
+ _this = @
+ @currentProject = JSON.parse(currentProject)
$('.js-milestone-select').each (i, dropdown) ->
- projectId = $(dropdown).data('project-id')
- milestonesUrl = $(dropdown).data('milestones')
- selectedMilestone = $(dropdown).data('selected')
- showNo = $(dropdown).data('show-no')
- showAny = $(dropdown).data('show-any')
- useId = $(dropdown).data('use-id')
-
- $(dropdown).glDropdown(
+ $dropdown = $(dropdown)
+ projectId = $dropdown.data('project-id')
+ milestonesUrl = $dropdown.data('milestones')
+ issueUpdateURL = $dropdown.data('issueUpdate')
+ selectedMilestone = $dropdown.data('selected')
+ showNo = $dropdown.data('show-no')
+ showAny = $dropdown.data('show-any')
+ showUpcoming = $dropdown.data('show-upcoming')
+ useId = $dropdown.data('use-id')
+ defaultLabel = $dropdown.data('default-label')
+ issuableId = $dropdown.data('issuable-id')
+ abilityName = $dropdown.data('ability-name')
+ $selectbox = $dropdown.closest('.selectbox')
+ $block = $selectbox.closest('.block')
+ $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon')
+ $value = $block.find('.value')
+ $loading = $block.find('.block-loading').fadeOut()
+
+ if issueUpdateURL
+ milestoneLinkTemplate = _.template(
+ '<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"><%= title %></a>'
+ )
+
+ milestoneLinkNoneTemplate = '<div class="light">None</div>'
+
+ $dropdown.glDropdown(
data: (term, callback) ->
$.ajax(
url: milestonesUrl
).done (data) ->
- html = $(data)
- data = []
- html.find('.milestone strong a').each ->
- link = $(@).attr("href").split("/")
- data.push(
- id: link[link.length - 1]
- title: $(@).text().trim()
+ extraOptions = []
+ if showAny
+ extraOptions.push(
+ id: 0
+ name: ''
+ title: 'Any Milestone'
)
if showNo
- data.unshift(
- id: "0"
+ extraOptions.push(
+ id: -1
+ name: 'No Milestone'
title: 'No Milestone'
)
- if showAny
- data.unshift(
- title: 'Any Milestone'
+ if showUpcoming
+ extraOptions.push(
+ id: -2
+ name: '#upcoming'
+ title: 'Upcoming'
)
- if data.length > 2
- data.splice 2, 0, "divider"
+ if extraOptions.length > 2
+ extraOptions.push 'divider'
- callback(data)
+ callback(extraOptions.concat(data))
filterable: true
search:
fields: ['title']
selectable: true
- fieldName: $(dropdown).data('field-name')
+ toggleLabel: (selected) ->
+ if selected && 'id' of selected
+ selected.title
+ else
+ defaultLabel
+ fieldName: $dropdown.data('field-name')
text: (milestone) ->
milestone.title
id: (milestone) ->
if !useId
- if milestone.title isnt "Any milestone"
- milestone.title
- else
- ""
+ milestone.name
else
milestone.id
isSelected: (milestone) ->
- milestone.title is selectedMilestone
- clicked: ->
- if $(dropdown).hasClass "js-filter-submit"
- $(dropdown).parents('form').submit()
+ milestone.name is selectedMilestone
+ hidden: ->
+ $selectbox.hide()
+
+ # display:block overrides the hide-collapse rule
+ $value.removeAttr('style')
+ clicked: (selected) ->
+ if $dropdown.hasClass 'js-filter-bulk-update'
+ return
+
+ if $dropdown.hasClass('js-filter-submit')
+ if selected.name?
+ selectedMilestone = selected.name
+ else
+ selectedMilestone = ''
+ Issues.filterResults $dropdown.closest('form')
+ else
+ selected = $selectbox
+ .find('input[type="hidden"]')
+ .val()
+ data = {}
+ data[abilityName] = {}
+ data[abilityName].milestone_id = selected
+ $loading
+ .fadeIn()
+ $dropdown.trigger('loading.gl.dropdown')
+ $.ajax(
+ type: 'PUT'
+ url: issueUpdateURL
+ data: data
+ ).done (data) ->
+ $dropdown.trigger('loaded.gl.dropdown')
+ $loading.fadeOut()
+ $selectbox.hide()
+ $value.removeAttr('style')
+ if data.milestone?
+ data.milestone.namespace = _this.currentProject.namespace
+ data.milestone.path = _this.currentProject.path
+ $value.html(milestoneLinkTemplate(data.milestone))
+ $sidebarCollapsedValue.find('span').text(data.milestone.title)
+ else
+ $value.html(milestoneLinkNoneTemplate)
+ $sidebarCollapsedValue.find('span').text('No')
)
diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
index b164231e7ef..ff06c57f2b5 100644
--- a/app/assets/javascripts/notes.js.coffee
+++ b/app/assets/javascripts/notes.js.coffee
@@ -361,14 +361,12 @@ class @Notes
showEditForm: (e) ->
e.preventDefault()
note = $(this).closest(".note")
- note.find(".note-body > .note-text").hide()
- note.find(".note-header").hide()
+ note.addClass "is-editting"
form = note.find(".note-edit-form")
isNewForm = form.is(':not(.gfm-form)')
if isNewForm
form.addClass('gfm-form')
form.addClass('current-note-edit-form')
- form.show()
# Show the attachment delete link
note.find(".js-note-attachment-delete").show()
@@ -402,11 +400,9 @@ class @Notes
cancelEdit: (e) ->
e.preventDefault()
note = $(this).closest(".note")
- note.find(".note-body > .note-text").show()
- note.find(".note-header").show()
+ note.removeClass "is-editting"
note.find(".current-note-edit-form")
.removeClass("current-note-edit-form")
- .hide()
###
Called in response to deleting a note of any kind.
@@ -627,10 +623,10 @@ class @Notes
if closebtn.text() isnt closetext
closebtn.text(closetext)
- if reopenbtn.is(':not(.btn-comment-and-reopen)')
+ if reopenbtn.is('.btn-comment-and-reopen')
reopenbtn.removeClass('btn-comment-and-reopen')
- if closebtn.is(':not(.btn-comment-and-close)')
+ if closebtn.is('.btn-comment-and-close')
closebtn.removeClass('btn-comment-and-close')
if discardbtn.is(':visible')
diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee
index 20f87440551..ae87c6c4e40 100644
--- a/app/assets/javascripts/profile.js.coffee
+++ b/app/assets/javascripts/profile.js.coffee
@@ -1,5 +1,9 @@
class @Profile
- constructor: ->
+ constructor: (opts = {}) ->
+ {
+ @form = $('.edit-user')
+ } = opts
+
# Automatically submit the Preferences form when any of its radio buttons change
$('.js-preferences-form').on 'change.preference', 'input[type=radio]', ->
$(this).parents('form').submit()
@@ -17,14 +21,46 @@ class @Profile
$('.update-notifications').on 'ajax:complete', ->
$(this).find('.btn-save').enable()
- $('.js-choose-user-avatar-button').bind "click", ->
- form = $(this).closest("form")
- form.find(".js-user-avatar-input").click()
+ @bindEvents()
+
+ cropOpts =
+ filename: '.js-avatar-filename'
+ previewImage: '.avatar-image .avatar'
+ modalCrop: '.modal-profile-crop'
+ pickImageEl: '.js-choose-user-avatar-button'
+ uploadImageBtn: '.js-upload-user-avatar'
+ modalCropImg: '.modal-profile-crop-image'
+
+ @avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data 'glcrop'
+
+ bindEvents: ->
+ @form.on 'submit', @onSubmitForm
+
+ onSubmitForm: (e) =>
+ e.preventDefault()
+ @saveForm()
+
+ saveForm: ->
+ self = @
+
+ formData = new FormData(@form[0])
+ formData.append('user[avatar]', @avatarGlCrop.getBlob(), 'avatar.png')
- $('.js-user-avatar-input').bind "change", ->
- form = $(this).closest("form")
- filename = $(this).val().replace(/^.*[\\\/]/, '')
- form.find(".js-avatar-filename").text(filename)
+ $.ajax
+ url: @form.attr('action')
+ type: @form.attr('method')
+ data: formData
+ dataType: "json"
+ processData: false
+ contentType: false
+ success: (response) ->
+ new Flash(response.message, 'notice')
+ error: (jqXHR) ->
+ new Flash(jqXHR.responseJSON.message, 'alert')
+ complete: ->
+ window.scrollTo 0, 0
+ # Enable submit button after requests ends
+ self.form.find(':input[disabled]').enable()
$ ->
# Extract the SSH Key title from its comment
diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee
index 76bc4ff42a2..87d313ed67c 100644
--- a/app/assets/javascripts/project.js.coffee
+++ b/app/assets/javascripts/project.js.coffee
@@ -11,7 +11,6 @@ class @Project
$(@).toggleClass('active')
url = $("#project_clone").val()
- console.log("url",url)
# Update the input field
$('#project_clone').val(url)
diff --git a/app/assets/javascripts/right_sidebar.js.coffee b/app/assets/javascripts/right_sidebar.js.coffee
new file mode 100644
index 00000000000..67403554340
--- /dev/null
+++ b/app/assets/javascripts/right_sidebar.js.coffee
@@ -0,0 +1,55 @@
+class @Sidebar
+ constructor: (currentUser) ->
+ @addEventListeners()
+
+ addEventListeners: ->
+ $('aside').on('click', '.sidebar-collapsed-icon', @sidebarCollapseClicked)
+ $('.dropdown').on('hidden.gl.dropdown', @sidebarDropdownHidden)
+ $('.dropdown').on('loading.gl.dropdown', @sidebarDropdownLoading)
+ $('.dropdown').on('loaded.gl.dropdown', @sidebarDropdownLoaded)
+
+ sidebarDropdownLoading: (e) ->
+ $sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon')
+ img = $sidebarCollapsedIcon.find('img')
+ i = $sidebarCollapsedIcon.find('i')
+ $loading = $('<i class="fa fa-spinner fa-spin"></i>')
+ if img.length
+ img.before($loading)
+ img.hide()
+ else if i.length
+ i.before($loading)
+ i.hide()
+
+ sidebarDropdownLoaded: (e) ->
+ $sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon')
+ img = $sidebarCollapsedIcon.find('img')
+ $sidebarCollapsedIcon.find('i.fa-spin').remove()
+ i = $sidebarCollapsedIcon.find('i')
+ if img.length
+ img.show()
+ else
+ i.show()
+
+
+ sidebarCollapseClicked: (e) ->
+ e.preventDefault()
+ $block = $(@).closest('.block')
+
+ $('aside')
+ .find('.gutter-toggle')
+ .trigger('click')
+ $editLink = $block.find('.edit-link')
+
+ if $editLink.length
+ $editLink.trigger('click')
+ $block.addClass('collapse-after-update')
+ $('.page-with-sidebar').addClass('with-overlay')
+
+ sidebarDropdownHidden: (e) ->
+ $block = $(@).closest('.block')
+ if $block.hasClass('collapse-after-update')
+ $block.removeClass('collapse-after-update')
+ $('.page-with-sidebar').removeClass('with-overlay')
+ $('aside')
+ .find('.gutter-toggle')
+ .trigger('click') \ No newline at end of file
diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee
index c1801365266..030655491bf 100644
--- a/app/assets/javascripts/search_autocomplete.js.coffee
+++ b/app/assets/javascripts/search_autocomplete.js.coffee
@@ -1,11 +1,270 @@
class @SearchAutocomplete
- constructor: (search_autocomplete_path, project_id, project_ref) ->
- project_id = '' unless project_id
- project_ref = '' unless project_ref
- query = "?project_id=" + project_id + "&project_ref=" + project_ref
-
- $("#search").autocomplete
- source: search_autocomplete_path + query
- minLength: 1
- select: (event, ui) ->
- location.href = ui.item.url
+
+ KEYCODE =
+ ESCAPE: 27
+ BACKSPACE: 8
+ ENTER: 13
+
+ constructor: (opts = {}) ->
+ {
+ @wrap = $('.search')
+
+ @optsEl = @wrap.find('.search-autocomplete-opts')
+ @autocompletePath = @optsEl.data('autocomplete-path')
+ @projectId = @optsEl.data('autocomplete-project-id') || ''
+ @projectRef = @optsEl.data('autocomplete-project-ref') || ''
+
+ } = opts
+
+ # Dropdown Element
+ @dropdown = @wrap.find('.dropdown')
+ @dropdownContent = @dropdown.find('.dropdown-content')
+
+ @locationBadgeEl = @getElement('.search-location-badge')
+ @locationText = @getElement('.location-text')
+ @scopeInputEl = @getElement('#scope')
+ @searchInput = @getElement('.search-input')
+ @projectInputEl = @getElement('#search_project_id')
+ @groupInputEl = @getElement('#group_id')
+ @searchCodeInputEl = @getElement('#search_code')
+ @repositoryInputEl = @getElement('#repository_ref')
+ @clearInput = @getElement('.js-clear-input')
+
+ @saveOriginalState()
+
+ # Only when user is logged in
+ @createAutocomplete() if gon.current_user_id
+
+ @searchInput.addClass('disabled')
+
+ @saveTextLength()
+
+ @bindEvents()
+
+ # Finds an element inside wrapper element
+ getElement: (selector) ->
+ @wrap.find(selector)
+
+ saveOriginalState: ->
+ @originalState = @serializeState()
+
+ saveTextLength: ->
+ @lastTextLength = @searchInput.val().length
+
+ createAutocomplete: ->
+ @searchInput.glDropdown
+ filterInputBlur: false
+ filterable: true
+ filterRemote: true
+ highlight: true
+ enterCallback: false
+ filterInput: 'input#search'
+ search:
+ fields: ['text']
+ data: @getData.bind(@)
+
+ getData: (term, callback) ->
+ _this = @
+
+ # Do not trigger request if input is empty
+ return if @searchInput.val() is ''
+
+ # Prevent multiple ajax calls
+ return if @loadingSuggestions
+
+ @loadingSuggestions = true
+
+ jqXHR = $.get(@autocompletePath, {
+ project_id: @projectId
+ project_ref: @projectRef
+ term: term
+ }, (response) ->
+ # Hide dropdown menu if no suggestions returns
+ if !response.length
+ _this.disableAutocomplete()
+ return
+
+ data = []
+
+ # List results
+ firstCategory = true
+ for suggestion in response
+
+ # Add group header before list each group
+ if lastCategory isnt suggestion.category
+ data.push 'separator' if !firstCategory
+
+ firstCategory = false if firstCategory
+
+ data.push
+ header: suggestion.category
+
+ lastCategory = suggestion.category
+
+ data.push
+ text: suggestion.label
+ url: suggestion.url
+
+ # Add option to proceed with the search
+ if data.length
+ data.push('separator')
+ data.push
+ text: "Result name contains \"#{term}\""
+ url: "/search?\
+ search=#{term}\
+ &project_id=#{_this.projectInputEl.val()}\
+ &group_id=#{_this.groupInputEl.val()}"
+
+ callback(data)
+ ).always ->
+ _this.loadingSuggestions = false
+
+ serializeState: ->
+ {
+ # Search Criteria
+ search_project_id: @projectInputEl.val()
+ group_id: @groupInputEl.val()
+ search_code: @searchCodeInputEl.val()
+ repository_ref: @repositoryInputEl.val()
+ scope: @scopeInputEl.val()
+
+ # Location badge
+ _location: @locationText.text()
+ }
+
+ bindEvents: ->
+ @searchInput.on 'keydown', @onSearchInputKeyDown
+ @searchInput.on 'keyup', @onSearchInputKeyUp
+ @searchInput.on 'click', @onSearchInputClick
+ @searchInput.on 'focus', @onSearchInputFocus
+ @searchInput.on 'blur', @onSearchInputBlur
+ @clearInput.on 'click', @onRemoveLocationClick
+
+ enableAutocomplete: ->
+ # No need to enable anything if user is not logged in
+ return if !gon.current_user_id
+
+ _this = @
+ @loadingSuggestions = false
+
+ @dropdown.addClass('open')
+ @searchInput.removeClass('disabled')
+
+ onSearchInputKeyDown: =>
+ # Saves last length of the entered text
+ @saveTextLength()
+
+ onSearchInputKeyUp: (e) =>
+ switch e.keyCode
+ when KEYCODE.BACKSPACE
+ # when trying to remove the location badge
+ if @lastTextLength is 0 and @badgePresent()
+ @removeLocationBadge()
+
+ # When removing the last character and no badge is present
+ if @lastTextLength is 1
+ @disableAutocomplete()
+
+ # When removing any character from existin value
+ if @lastTextLength > 1
+ @enableAutocomplete()
+
+ when KEYCODE.ESCAPE
+ @restoreOriginalState()
+
+ else
+ # Handle the case when deleting the input value other than backspace
+ # e.g. Pressing ctrl + backspace or ctrl + x
+ if @searchInput.val() is ''
+ @disableAutocomplete()
+ else
+ # We should display the menu only when input is not empty
+ @enableAutocomplete()
+
+ # Avoid falsy value to be returned
+ return
+
+ onSearchInputClick: (e) =>
+ # Prevents closing the dropdown menu
+ e.stopImmediatePropagation()
+
+ onSearchInputFocus: =>
+ @wrap.addClass('search-active')
+
+ onRemoveLocationClick: (e) =>
+ e.preventDefault()
+ @removeLocationBadge()
+ @searchInput.val('').focus()
+ @skipBlurEvent = true
+
+ onSearchInputBlur: (e) =>
+ @skipBlurEvent = false
+
+ # We should wait to make sure we are not clearing the input instead
+ setTimeout( =>
+ return if @skipBlurEvent
+
+ @wrap.removeClass('search-active')
+
+ # If input is blank then restore state
+ if @searchInput.val() is ''
+ @restoreOriginalState()
+ , 150)
+
+ addLocationBadge: (item) ->
+ category = if item.category? then "#{item.category}: " else ''
+ value = if item.value? then item.value else ''
+
+ html = "<span class='location-badge'>
+ <i class='location-text'>#{category}#{value}</i>
+ </span>"
+ @locationBadgeEl.html(html)
+ @wrap.addClass('has-location-badge')
+
+ restoreOriginalState: ->
+ inputs = Object.keys @originalState
+
+ for input in inputs
+ @getElement("##{input}").val(@originalState[input])
+
+
+ if @originalState._location is ''
+ @locationBadgeEl.empty()
+ else
+ @addLocationBadge(
+ value: @originalState._location
+ )
+
+ @dropdown.removeClass 'open'
+
+ badgePresent: ->
+ @locationBadgeEl.children().length
+
+ resetSearchState: ->
+ inputs = Object.keys @originalState
+
+ for input in inputs
+
+ # _location isnt a input
+ break if input is '_location'
+
+ @getElement("##{input}").val('')
+
+ removeLocationBadge: ->
+ @locationBadgeEl.empty()
+
+ # Reset state
+ @resetSearchState()
+
+ @wrap.removeClass('has-location-badge')
+
+ disableAutocomplete: ->
+ @searchInput.addClass('disabled')
+ @dropdown.removeClass('open')
+ @restoreMenu()
+
+ restoreMenu: ->
+ html = "<ul>
+ <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li>
+ </ul>"
+ @dropdownContent.html(html)
diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee
index eea3f5ee910..e1778511240 100644
--- a/app/assets/javascripts/sidebar.js.coffee
+++ b/app/assets/javascripts/sidebar.js.coffee
@@ -4,8 +4,6 @@ expanded = 'page-sidebar-expanded'
toggleSidebar = ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
$('header').toggleClass("header-collapsed header-expanded")
- $('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded")
- $('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left")
$.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' })
setTimeout ( ->
diff --git a/app/assets/javascripts/stat_graph_contributors_util.js.coffee b/app/assets/javascripts/stat_graph_contributors_util.js.coffee
index f5584bcfe4b..31617c88b4a 100644
--- a/app/assets/javascripts/stat_graph_contributors_util.js.coffee
+++ b/app/assets/javascripts/stat_graph_contributors_util.js.coffee
@@ -95,4 +95,4 @@ window.ContributorsStatGraphUtil =
if date_range is null || date_range[0] <= new Date(date) <= date_range[1]
true
else
- false \ No newline at end of file
+ false
diff --git a/app/assets/javascripts/todos.js.coffee b/app/assets/javascripts/todos.js.coffee
new file mode 100644
index 00000000000..ec2df6c5b73
--- /dev/null
+++ b/app/assets/javascripts/todos.js.coffee
@@ -0,0 +1,61 @@
+class @Todos
+ constructor: (@name) ->
+ @clearListeners()
+ @initBtnListeners()
+
+ clearListeners: ->
+ $('.done-todo').off('click')
+ $('.js-todos-mark-all').off('click')
+ $('.todo').off('click')
+
+ initBtnListeners: ->
+ $('.done-todo').on('click', @doneClicked)
+ $('.js-todos-mark-all').on('click', @allDoneClicked)
+ $('.todo').on('click', @goToTodoUrl)
+
+ doneClicked: (e) =>
+ e.preventDefault()
+ e.stopImmediatePropagation()
+
+ $this = $(e.currentTarget)
+ $this.disable()
+
+ $.ajax
+ type: 'POST'
+ url: $this.attr('href')
+ dataType: 'json'
+ data: '_method': 'delete'
+ success: (data) =>
+ @clearDone $this.closest('li')
+ @updateBadges data
+
+ allDoneClicked: (e) =>
+ e.preventDefault()
+ e.stopImmediatePropagation()
+
+ $this = $(e.currentTarget)
+ $this.disable()
+
+ $.ajax
+ type: 'POST'
+ url: $this.attr('href')
+ dataType: 'json'
+ data: '_method': 'delete'
+ success: (data) =>
+ $this.remove()
+ $('.js-todos-list').remove()
+ @updateBadges data
+
+ clearDone: ($row) ->
+ $ul = $row.closest('ul')
+ $row.remove()
+
+ if not $ul.find('li').length
+ $ul.parents('.panel').remove()
+
+ updateBadges: (data) ->
+ $('.todos-pending .badge, .todos-pending-count').text data.count
+ $('.todos-done .badge').text data.done_count
+
+ goToTodoUrl: ->
+ Turbolinks.visit($(this).data('url'))
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index 987c6f4b8d2..eee9b6e690e 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -1,17 +1,96 @@
class @UsersSelect
- constructor: ->
+ constructor: (currentUser) ->
@usersPath = "/autocomplete/users.json"
@userPath = "/autocomplete/users/:id.json"
+ if currentUser?
+ @currentUser = JSON.parse(currentUser)
$('.js-user-search').each (i, dropdown) =>
- @projectId = $(dropdown).data('project-id')
- @showCurrentUser = $(dropdown).data('current-user')
- showNullUser = $(dropdown).data('null-user')
- showAnyUser = $(dropdown).data('any-user')
- firstUser = $(dropdown).data('first-user')
- selectedId = $(dropdown).data('selected')
-
- $(dropdown).glDropdown(
+ $dropdown = $(dropdown)
+ @projectId = $dropdown.data('project-id')
+ @showCurrentUser = $dropdown.data('current-user')
+ showNullUser = $dropdown.data('null-user')
+ showAnyUser = $dropdown.data('any-user')
+ firstUser = $dropdown.data('first-user')
+ selectedId = $dropdown.data('selected')
+ defaultLabel = $dropdown.data('default-label')
+ issueURL = $dropdown.data('issueUpdate')
+ $selectbox = $dropdown.closest('.selectbox')
+ $block = $selectbox.closest('.block')
+ abilityName = $dropdown.data('ability-name')
+ $value = $block.find('.value')
+ $collapsedSidebar = $block.find('.sidebar-collapsed-user')
+ $loading = $block.find('.block-loading').fadeOut()
+
+ $block.on('click', '.js-assign-yourself', (e) =>
+ e.preventDefault()
+ assignTo(@currentUser.id)
+ )
+
+ assignTo = (selected) ->
+ data = {}
+ data[abilityName] = {}
+ data[abilityName].assignee_id = selected
+ $loading
+ .fadeIn()
+ $dropdown.trigger('loading.gl.dropdown')
+ $.ajax(
+ type: 'PUT'
+ dataType: 'json'
+ url: issueURL
+ data: data
+ ).done (data) ->
+ $dropdown.trigger('loaded.gl.dropdown')
+ $loading.fadeOut()
+ $selectbox.hide()
+
+ if data.assignee
+ user =
+ name: data.assignee.name
+ username: data.assignee.username
+ avatar: data.assignee.avatar_url
+ else
+ user =
+ name: 'Unassigned'
+ username: ''
+ avatar: ''
+ $value.html(assigneeTemplate(user))
+ $collapsedSidebar.html(collapsedAssigneeTemplate(user))
+
+
+ collapsedAssigneeTemplate = _.template(
+ '<% if( avatar ) { %>
+ <a class="author_link" href="/u/<%= username %>">
+ <img width="24" class="avatar avatar-inline s24" alt="" src="<%= avatar %>">
+ <span class="author">Toni Boehm</span>
+ </a>
+ <% } else { %>
+ <i class="fa fa-user"></i>
+ <% } %>'
+ )
+
+ assigneeTemplate = _.template(
+ '<% if (username) { %>
+ <a class="author_link " href="/u/<%= username %>">
+ <% if( avatar ) { %>
+ <img width="32" class="avatar avatar-inline s32" alt="" src="<%= avatar %>">
+ <% } %>
+ <span class="author"><%= name %></span>
+ <span class="username">
+ @<%= username %>
+ </span>
+ </a>
+ <% } else { %>
+ <span class="assign-yourself">
+ No assignee -
+ <a href="#" class="js-assign-yourself">
+ assign yourself
+ </a>
+ </span>
+ <% } %>'
+ )
+
+ $dropdown.glDropdown(
data: (term, callback) =>
@users term, (users) =>
if term.length is 0
@@ -28,6 +107,7 @@ class @UsersSelect
if showNullUser
showDivider += 1
users.unshift(
+ beforeDivider: true
name: 'Unassigned',
id: 0
)
@@ -37,6 +117,7 @@ class @UsersSelect
name = showAnyUser
name = 'Any User' if name == true
anyUser = {
+ beforeDivider: true
name: name,
id: null
}
@@ -52,30 +133,74 @@ class @UsersSelect
search:
fields: ['name', 'username']
selectable: true
- fieldName: $(dropdown).data('field-name')
- clicked: ->
- if $(dropdown).hasClass "js-filter-submit"
- $(dropdown).parents('form').submit()
+ fieldName: $dropdown.data('field-name')
+
+ toggleLabel: (selected) ->
+ if selected && 'id' of selected
+ selected.name
+ else
+ defaultLabel
+
+ inputId: 'issue_assignee_id'
+
+ hidden: (e) ->
+ $selectbox.hide()
+ # display:block overrides the hide-collapse rule
+ $value.removeAttr('style')
+
+ clicked: (user) ->
+ page = $('body').data 'page'
+ isIssueIndex = page is 'projects:issues:index'
+ isMRIndex = page is page is 'projects:merge_requests:index'
+ if $dropdown.hasClass('js-filter-bulk-update')
+ return
+
+ if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
+ selectedId = user.id
+ Issues.filterResults $dropdown.closest('form')
+ else if $dropdown.hasClass 'js-filter-submit'
+ $dropdown.closest('form').submit()
+ else
+ selected = $dropdown
+ .closest('.selectbox')
+ .find("input[name='#{$dropdown.data('field-name')}']").val()
+ assignTo(selected)
+
renderRow: (user) ->
username = if user.username then "@#{user.username}" else ""
avatar = if user.avatar_url then user.avatar_url else false
selected = if user.id is selectedId then "is-active" else ""
img = ""
- if avatar
- img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />"
+ if user.beforeDivider?
+ "<li>
+ <a href='#' class='#{selected}'>
+ #{user.name}
+ </a>
+ </li>"
+ else
+ if avatar
+ img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />"
- "<li>
+ # split into three parts so we can remove the username section if nessesary
+ listWithName = "<li>
<a href='#' class='dropdown-menu-user-link #{selected}'>
#{img}
<strong class='dropdown-menu-user-full-name'>
#{user.name}
- </strong>
- <span class='dropdown-menu-user-username'>
+ </strong>"
+
+ listWithUserName = "<span class='dropdown-menu-user-username'>
#{username}
- </span>
- </a>
+ </span>"
+ listClosingTags = "</a>
</li>"
+
+
+ if username is ''
+ listWithUserName = ''
+
+ listWithName + listWithUserName + listClosingTags
)
$('.ajax-users-select').each (i, select) =>
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 2d301d21ab9..69b3b6586de 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -9,6 +9,8 @@
*= require_self
*= require dropzone/basic
*= require cal-heatmap
+ *= require cropper.css
+ *= require animate
*/
/*
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index b7ffa3e6ffb..5aa425dab6c 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -16,7 +16,7 @@
}
&.group-avatar, &.project-avatar, &.avatar-tile {
- @include border-radius(0px);
+ @include border-radius(0);
}
&.s16 { width: 16px; height: 16px; margin-right: 6px; }
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index c36f29dda0e..62b2af0dbf7 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -107,10 +107,28 @@
margin: 0;
font-size: 23px;
font-weight: normal;
- margin: 16px 0 5px 0;
+ margin: 16px 0 5px;
color: #4c4e54;
font-size: 23px;
line-height: 1.1;
+
+ h1 {
+ color: #313236;
+ margin-bottom: 6px;
+ font-size: 23px;
+ }
+
+ .visibility-icon {
+ display: inline-block;
+ margin-left: 5px;
+ font-size: 18px;
+ color: $gray;
+ }
+
+ p {
+ padding: 0 $gl-padding;
+ color: #5c5d5e;
+ }
}
.cover-desc {
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index fa115a4bf56..657c5f033c7 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -208,3 +208,13 @@
background-color: #e4e7ed !important;
}
}
+
+.btn-loading {
+ &:not(.disabled) .fa {
+ display: none;
+ }
+
+ .fa {
+ margin-right: 5px;
+ }
+}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index bc03c2180be..9b676d759e0 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -292,8 +292,11 @@ table {
}
.btn-sign-in {
- margin-top: 10px;
text-shadow: none;
+
+ @media (min-width: $screen-sm-min) {
+ margin-top: 11px;
+ }
}
.side-filters {
@@ -375,7 +378,7 @@ table {
position: absolute;
top: 0;
right: 0;
- width: 250px !important;
+ min-width: 250px;
visibility: hidden;
}
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index a48b6c17fa0..82dc1acbd01 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -42,7 +42,7 @@
font-size: 15px;
text-align: left;
border: 1px solid $dropdown-toggle-border-color;
- border-radius: 2px;
+ border-radius: $dropdown-border-radius;
outline: 0;
text-overflow: ellipsis;
white-space: nowrap;
@@ -75,12 +75,12 @@
width: 240px;
margin-top: 2px;
margin-bottom: 0;
- padding: 10px 10px;
- font-size: 14px;
+ font-size: 15px;
font-weight: normal;
+ padding: 10px 0;
background-color: $dropdown-bg;
border: 1px solid $dropdown-border-color;
- border-radius: $border-radius-base;
+ border-radius: $dropdown-border-radius;
box-shadow: 0 2px 4px $dropdown-shadow-color;
&.is-loading {
@@ -101,9 +101,17 @@
li {
text-align: left;
list-style: none;
+ padding: 0 10px;
}
.divider {
+ height: 1px;
+ margin: 8px 10px;
+ padding: 0;
+ background-color: $dropdown-divider-color;
+ }
+
+ .separator {
width: 100%;
height: 1px;
margin-top: 8px;
@@ -130,6 +138,27 @@
text-decoration: none;
outline: 0;
}
+
+ &.dropdown-menu-empty-link {
+ &.is-focused {
+ background-color: $dropdown-empty-row-bg;
+ }
+ }
+
+ &.dropdown-menu-user-link {
+ line-height: 16px;
+ }
+ }
+
+ .dropdown-header {
+ color: $dropdown-header-color;
+ font-size: 13px;
+ line-height: 22px;
+ padding: 0 10px 10px;
+ }
+
+ .separator + .dropdown-header {
+ padding-top: 2px;
}
}
@@ -148,6 +177,10 @@
.dropdown-menu-back {
display: block;
}
+
+ .dropdown-content {
+ padding: 0 10px;
+ }
}
}
@@ -161,13 +194,13 @@
}
.dropdown-menu-user-link {
- padding-top: 7px;
+ padding-top: 10px;
padding-bottom: 7px;
}
.dropdown-menu-user-full-name {
display: block;
- font-weight: 600;
+ font-weight: 500;
line-height: 16px;
text-overflow: ellipsis;
overflow: hidden;
@@ -183,7 +216,7 @@
}
.dropdown-select {
- width: 280px;
+ width: $dropdown-width;
}
.dropdown-menu-align-right {
@@ -212,20 +245,11 @@
}
}
-.dropdown-header {
- padding-left: 5px;
- padding-right: 5px;
- color: $dropdown-header-color;
- font-size: 13px;
- line-height: 22px;
-}
.dropdown-title {
position: relative;
- margin-bottom: 10px;
- padding-left: 30px;
- padding-right: 30px;
- padding-bottom: 10px;
+ padding: 0 0 15px;
+ margin: 0 10px 10px;
font-weight: 600;
line-height: 1;
text-align: center;
@@ -237,7 +261,7 @@
.dropdown-title-button {
position: absolute;
- top: -1px;
+ top: 0;
padding: 0;
color: $dropdown-title-btn-color;
font-size: 14px;
@@ -251,25 +275,49 @@
}
.dropdown-menu-close {
- right: 0;
+ right: 7px;
+ width: 20px;
+ height: 20px;
+ top: -1px;
}
.dropdown-menu-back {
- left: 0;
+ left: 7px;
+ top: 2px;
}
.dropdown-input {
position: relative;
margin-bottom: 10px;
+ padding: 0 10px;
.fa {
position: absolute;
top: 10px;
- right: 10px;
+ right: 20px;
color: #c7c7c7;
font-size: 12px;
pointer-events: none;
}
+
+ .dropdown-input-clear {
+ display: none;
+ cursor: pointer;
+ pointer-events: all;
+ right: 22px;
+ top: 9px;
+ font-size: 14px;
+ }
+
+ &.has-value {
+ .dropdown-input-clear {
+ display: block;
+ }
+
+ .dropdown-input-search {
+ display: none;
+ }
+ }
}
.dropdown-input-field {
@@ -286,13 +334,13 @@
border-color: $dropdown-input-focus-border;
box-shadow: 0 0 4px $dropdown-input-focus-shadow;
- + .fa {
+ ~ .fa {
color: $dropdown-link-color;
}
}
&:hover {
- + .fa {
+ ~ .fa {
color: $dropdown-link-color;
}
}
@@ -338,11 +386,12 @@
}
}
-.dropdown-menu-labels {
- .label {
- position: relative;
- width: 30px;
- margin-right: 5px;
- text-indent: -99999px;
- }
+.dropdown-label-box {
+ position: relative;
+ top: 3px;
+ margin-right: 5px;
+ display: inline-block;
+ width: 15px;
+ height: 15px;
+ border-radius: $border-radius-base;
}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 646e2610831..a26ace5cc19 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -3,12 +3,10 @@
*
*/
.file-holder {
- border: none;
border: 1px solid $border-color;
&.readme-holder {
- margin-top: 10px;
- border-bottom: 0;
+ margin: $gl-padding-top 0;
}
table {
@@ -50,6 +48,10 @@
}
}
+ a {
+ color: $gl-dark-link-color;
+ }
+
.left-options {
margin-top: -3px;
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 40a508c1ebc..b05c5df1bd8 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -11,3 +11,11 @@
}
}
}
+
+@media (max-width: $screen-xs-max) {
+ .filter-item {
+ display: block;
+ margin: 0 0 10px;
+ }
+}
+
diff --git a/app/assets/stylesheets/framework/fonts.scss b/app/assets/stylesheets/framework/fonts.scss
index 7a946109e3a..5f9685bc71a 100644
--- a/app/assets/stylesheets/framework/fonts.scss
+++ b/app/assets/stylesheets/framework/fonts.scss
@@ -1,3 +1,7 @@
+// Disabling "SpaceAfterPropertyColon" linter because the linter doesn't like
+// the way the `src` property is formatted in this file.
+// scss-lint:disable SpaceAfterPropertyColon
+
/* latin-ext */
@font-face {
font-family: 'Source Sans Pro';
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 4cb4129b71b..54cb5461113 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -6,40 +6,6 @@ input {
border-radius: $border-radius-base;
}
-input[type='search'] {
- background-color: white;
- padding-left: 10px;
-}
-
-input[type='search'].search-input {
- background-repeat: no-repeat;
- background-position: 10px;
- background-size: 16px;
- background-position-x: 30%;
- padding-left: 10px;
- background-color: $gray-light;
-
- &.search-input[value=""] {
- background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC');
- }
-
- &.search-input::-webkit-input-placeholder {
- text-align: center;
- }
-
- &.search-input:-moz-placeholder { /* Firefox 18- */
- text-align: center;
- }
-
- &.search-input::-moz-placeholder { /* Firefox 19+ */
- text-align: center;
- }
-
- &.search-input:-ms-input-placeholder {
- text-align: center;
- }
-}
-
input[type='text'].danger {
background: #f2dede!important;
border-color: #d66;
@@ -125,7 +91,7 @@ label {
}
.form-control::-webkit-input-placeholder {
- color: #7f8fa4;
+ color: $gl-placeholder-color;
}
.input-group {
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index 2a4cf4fc335..fa9038ebaca 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -33,10 +33,15 @@
background: $color;
}
+ .complex-sidebar .nav-primary {
+ border-right: 1px solid lighten($color, 3%);
+ }
+
.sidebar-wrapper {
background: $color-darker;
.sidebar-user {
+ border-top: 1px solid lighten($color, 3%);
background: $color-darker;
color: $color-light;
@@ -62,7 +67,6 @@
.count {
color: $color-light;
- background: $color-dark;
}
}
@@ -117,4 +121,4 @@ body {
&.ui_violet {
@include gitlab-theme(#98c, $theme-violet, #436, #325);
}
-} \ No newline at end of file
+}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 71a7ecab8ef..724980b2208 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -36,7 +36,7 @@ header {
padding: 0;
.nav > li > a {
- color: #7f8fa4;
+ color: $gl-icon-color;
font-size: 18px;
padding: 0;
margin: ($header-height - 28) / 2 0;
@@ -62,7 +62,7 @@ header {
background-color: #eee;
}
&.active {
- color: #7f8fa4;
+ color: $gl-icon-color;
}
}
}
@@ -70,20 +70,25 @@ header {
.header-content {
height: $header-height;
+ padding-right: 20px;
+
+ @media (min-width: $screen-sm-min) {
+ padding-right: 0;
+ }
.title {
margin: 0;
font-size: 19px;
line-height: $header-height;
font-weight: normal;
- color: #4c4e54;
+ color: $gl-text-color;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: top;
white-space: nowrap;
a {
- color: #4c4e54;
+ color: $gl-text-color;
&:hover {
text-decoration: underline;
}
@@ -112,37 +117,17 @@ header {
}
}
- .search {
- margin-right: 10px;
- margin-left: 10px;
- margin-top: ($header-height - 36) / 2;
-
- form {
- margin: 0;
- padding: 0;
- }
-
- .search-input {
- width: 220px;
-
- &:focus {
- @include box-shadow(none);
- outline: none;
- }
- }
- }
-
.impersonation i {
color: $red-normal;
}
}
@mixin collapsed-header {
- margin-left: $sidebar_collapsed_width;
+ margin-left: 40px;
}
.header-collapsed {
- margin-left: $sidebar_collapsed_width;
+ margin-left: 40px;
@media (min-width: $screen-md-min) {
@include collapsed-header;
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index e901c78d02f..8bb047db2dd 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -16,7 +16,7 @@ body {
}
.container .content {
- margin: 0 0;
+ margin: 0;
}
.navless-container {
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 2b4bb1eebf9..b17c8bcbb1e 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -111,14 +111,17 @@ ul.content-list {
> li {
border-color: $table-border-color;
- color: $list-text-color;
font-size: $list-font-size;
+ color: $list-text-color;
.title {
- color: $list-title-color;
font-weight: 600;
}
+ a {
+ color: $gl-dark-link-color;
+ }
+
.description {
p {
@include str-truncated;
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 377bfa174bd..250d6309291 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -1,7 +1,7 @@
/**
* Generic mixins
*/
- @mixin box-shadow($shadow) {
+@mixin box-shadow($shadow) {
-webkit-box-shadow: $shadow;
-moz-box-shadow: $shadow;
-ms-box-shadow: $shadow;
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 5ea4f9a49db..475340f3f0c 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -107,7 +107,7 @@
}
.page-title {
- .note_created_ago, .new-issue-link {
+ .note-created-ago, .new-issue-link {
display: none;
}
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 5f4ce87b085..fc3b0a422a7 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -100,13 +100,18 @@
> form {
display: inline-block;
+ margin-top: -1px;
+ }
+
+ .icon-label {
+ display: none;
}
input {
height: 34px;
display: inline-block;
position: relative;
- top: 1px;
+ top: 2px;
margin-right: $gl-padding-top;
/* Medium devices (desktops, 992px and up) */
@@ -124,9 +129,38 @@
}
}
- /* Hide on extra small devices (phones) */
@media (max-width: $screen-xs-max) {
- display: none;
+ padding-bottom: 0;
+
+ .btn, form, .dropdown, .dropdown-menu-toggle, .form-control {
+ margin: 0 0 10px;
+ display: block;
+ width: 100%;
+ }
+
+ form {
+ display: block;
+ height: auto;
+
+ input {
+ width: 100%;
+ margin: 0 0 10px;
+ }
+ }
+
+ .input-short {
+ width: 100%;
+ }
+
+ .icon-label {
+ display: inline-block;
+ }
+
+ // Applies on /dashboard/issues
+ .project-item-select-holder {
+ display: block;
+ margin: 0;
+ }
}
/* Small devices (tablets, 768px and lower) */
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index b3371229d5a..e82d052f45a 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -41,9 +41,10 @@
}
.select2-drop {
- @include box-shadow(rgba(76, 86, 103, 0.247059) 0px 0px 1px 0px, rgba(31, 37, 50, 0.317647) 0px 2px 18px 0px);
+ @include box-shadow(rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0);
@include border-radius ($border-radius-default);
border: none;
+ min-width: 175px;
}
.select2-results .select2-result-label {
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index be05db58c40..c741c826ae0 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -1,3 +1,10 @@
+#logo {
+ z-index: 2;
+ position: absolute;
+ width: 58px;
+ cursor: pointer;
+}
+
.page-with-sidebar {
padding-top: $header-height;
transition-duration: .3s;
@@ -18,28 +25,10 @@
position: absolute;
left: 0;
}
-
- #logo {
- z-index: 2;
- position: absolute;
- width: 58px;
- cursor: pointer;
- }
-
- &.right-sidebar-expanded {
- /* Extra small devices (phones, less than 768px) */
- /* No media query since this is the default in Bootstrap */
- padding-right: 0;
- /* Small devices (tablets, 768px and up) */
- @media (min-width: $screen-sm-min) {
- padding-right: $gutter_width;
- }
-
- }
}
.sidebar-wrapper {
- z-index: 999;
+ z-index: 1000;
background: $background-color;
}
@@ -155,7 +144,7 @@
}
a {
- padding: 7px 15px;
+ padding: 7px 12px;
font-size: $gl-font-size;
line-height: 24px;
color: $gray;
@@ -180,10 +169,12 @@
}
.count {
- float: right;
- background: #eee;
- padding: 0 8px;
- @include border-radius(6px);
+ &:before {
+ content: '(';
+ }
+ &:after {
+ content: ')';
+ }
}
&.back-link i {
@@ -202,20 +193,54 @@
}
}
-@mixin expanded-sidebar {
- padding-left: $sidebar_collapsed_width;
+.expand-nav a {
+ color: $gl-icon-color;
+ width: 60px;
+ position: fixed;
+ top: 0;
+ left: 0;
+ font-size: 20px;
+ background: transparent;
+ height: 59px;
+ text-align: center;
+ line-height: 59px;
+ border-bottom: 1px solid #eee;
+ transition-duration: .3s;
+ outline: none;
+ z-index: 100;
- @media (min-width: $screen-md-min) {
- padding-left: $sidebar_width;
+ &:hover {
+ text-decoration: none;
}
+}
- &.right-sidebar-collapsed {
- /* Extra small devices (phones, less than 768px) */
- padding-right: 0;
- /* Small devices (tablets, 768px and up) */
- @media (min-width: $screen-sm-min) {
- padding-right: $sidebar_collapsed_width;
- }
+.collapse-nav a {
+ width: $sidebar_width;
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ font-size: 13px;
+ background: transparent;
+ height: 40px;
+ text-align: center;
+ line-height: 40px;
+ transition-duration: .3s;
+ outline: none;
+
+ &:hover {
+ text-decoration: none;
+ }
+}
+
+.page-sidebar-collapsed {
+ .sidebar-wrapper {
+ display: none;
+ }
+}
+
+.page-sidebar-expanded {
+ @media (min-width: $screen-md-min) {
+ padding-left: $sidebar_width;
}
.sidebar-wrapper {
@@ -225,7 +250,7 @@
width: $sidebar_width;
}
- .nav-sidebar li a{
+ .nav-sidebar li a {
width: 230px;
&.back-link {
@@ -237,91 +262,75 @@
}
}
-@mixin collapsed-sidebar {
- padding-left: $sidebar_collapsed_width;
+.right-sidebar-collapsed {
+ padding-right: 0;
- &.right-sidebar-collapsed {
- /* Extra small devices (phones, less than 768px) */
- padding-right: 0;
- /* Small devices (tablets, 768px and up) */
- @media (min-width: $screen-sm-min) {
- padding-right: $sidebar_collapsed_width;
- }
+ @media (min-width: $screen-sm-min) {
+ padding-right: $sidebar_collapsed_width;
}
- .sidebar-wrapper {
- width: $sidebar_collapsed_width;
+ .sidebar-collapsed-icon {
+ cursor: pointer;
+ }
+}
+
+.right-sidebar-expanded {
+ padding-right: 0;
+
+ @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+ padding-right: $sidebar_collapsed_width;
+ }
+
+ @media (min-width: $screen-md-min) {
+ padding-right: $gutter_width;
+ }
+
+ &.with-overlay {
+ padding-right: $sidebar_collapsed_width;
+ }
+}
+
+.complex-sidebar {
+ display: inline-block;
+
+ .nav-primary {
+ width: 61px;
+ float: left;
+ height: 100vh;
- .header-logo {
- width: $sidebar_collapsed_width;
+ .nav-sidebar {
+ width: 60px;
- a {
- padding-left: ($sidebar_collapsed_width - 36) / 2;
+ li a {
+ width: 60px;
- .gitlab-text-container {
+ span {
display: none;
}
}
}
+ }
+
+ .nav-secondary {
+ $nav-secondary-width: 168px;
+
+ float: left;
+ width: $nav-secondary-width;
.nav-sidebar {
- width: $sidebar_collapsed_width;
+ width: $nav-secondary-width;
li {
- width: auto;
+ width: $nav-secondary-width;
a {
- span {
+ width: $nav-secondary-width;
+
+ i {
display: none;
}
}
}
}
-
- .collapse-nav a {
- width: $sidebar_collapsed_width;
- }
-
- .sidebar-user {
- padding-left: ($sidebar_collapsed_width - 36) / 2;
- width: $sidebar_collapsed_width;
-
- .username {
- display: none;
- }
- }
}
}
-
-.collapse-nav a {
- width: $sidebar_width;
- position: fixed;
- bottom: 0;
- left: 0;
- font-size: 13px;
- background: transparent;
- height: 40px;
- text-align: center;
- line-height: 40px;
- transition-duration: .3s;
- outline: none;
-}
-
-.collapse-nav a:hover {
- text-decoration: none;
- background: #f2f6f7;
-}
-
-.page-sidebar-collapsed {
- /* Extra small devices (phones, less than 768px) */
- @include collapsed-sidebar;
- padding-right: 0;
- /* Small devices (tablets, 768px and up) */
- @media (min-width: $screen-sm-min) {
- @include collapsed-sidebar;
- }
-}
-
-.page-sidebar-expanded {
- @include expanded-sidebar;
-}
diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index f63ac033234..c72af5dad0a 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -56,8 +56,8 @@ $component-active-bg: $brand-info;
//##
$input-color: $text-color;
-$input-border: #e7e9ed;
-$input-border-focus: #7f8fa4;
+$input-border: $border-color;
+$input-border-focus: $focus-border-color;
$legend-color: $text-color;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 949295a1d0c..b1886fbe67b 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -39,8 +39,8 @@
h1 {
font-size: 1.3em;
font-weight: 600;
- margin: 24px 0 12px 0;
- padding: 0 0 10px 0;
+ margin: 24px 0 12px;
+ padding: 0 0 10px;
border-bottom: 1px solid #e7e9ed;
color: #313236;
}
@@ -48,27 +48,27 @@
h2 {
font-size: 1.2em;
font-weight: 600;
- margin: 24px 0 12px 0;
+ margin: 24px 0 12px;
color: #313236;
}
h3 {
- margin: 24px 0 12px 0;
+ margin: 24px 0 12px;
font-size: 1.1em;
}
h4 {
- margin: 24px 0 12px 0;
+ margin: 24px 0 12px;
font-size: 0.98em;
}
h5 {
- margin: 24px 0 12px 0;
+ margin: 24px 0 12px;
font-size: 0.95em;
}
h6 {
- margin: 24px 0 12px 0;
+ margin: 24px 0 12px;
font-size: 0.90em;
}
@@ -76,7 +76,7 @@
color: #7f8fa4;
font-size: inherit;
padding: 8px 21px;
- margin: 12px 0 12px;
+ margin: 12px 0;
border-left: 3px solid #e7e9ed;
}
@@ -88,13 +88,13 @@
p {
color: #5c5d5e;
- margin: 6px 0 0 0;
+ margin: 6px 0 0;
}
table {
@extend .table;
@extend .table-bordered;
- margin: 12px 0 12px 0;
+ margin: 12px 0;
color: #5c5d5e;
th {
background: #f8fafc;
@@ -102,7 +102,7 @@
}
pre {
- margin: 12px 0 12px 0;
+ margin: 12px 0;
font-size: 13px;
line-height: 1.6em;
overflow-x: auto;
@@ -191,7 +191,7 @@ body {
line-height: 1.3;
font-size: 1.25em;
font-weight: 600;
- margin: 12px 7px 12px 7px;
+ margin: 12px 7px;
}
h1, h2, h3, h4, h5, h6 {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 211ead7319d..98fe794d362 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -1,45 +1,77 @@
-$row-hover: #f4f8fe;
-$gl-text-color: #54565b;
-$gl-text-green: #4a2;
-$gl-text-red: #d12f19;
-$gl-text-orange: #d90;
-$gl-header-color: #323232;
-$gl-link-color: #333c48;
-$md-text-color: #444;
-$md-link-color: #3084bb;
-$progress-color: #c0392b;
-$gl-font-size: 15px;
-$list-font-size: 15px;
+/*
+ * Layout
+ */
$sidebar_collapsed_width: 62px;
$sidebar_width: 230px;
$gutter_collapsed_width: 62px;
$gutter_width: 290px;
$gutter_inner_width: 258px;
-$avatar_radius: 50%;
+
+/*
+ * UI elements
+ */
+$border-color: #efeff1;
+$focus-border-color: #3aabf0;
+$table-border-color: #eef0f2;
+$background-color: #faf9f9;
+
+/*
+ * Text
+ */
+$gl-font-size: 15px;
+$gl-title-color: #333;
+$gl-text-color: #555;
+$gl-text-green: #4a2;
+$gl-text-red: #d12f19;
+$gl-text-orange: #d90;
+$gl-link-color: #3084bb;
+$gl-dark-link-color: #333;
+$gl-placeholder-color: #8f8f8f;
+$gl-icon-color: $gl-placeholder-color;
+$gl-gray: $gl-text-color;
+$gl-header-color: $gl-title-color;
+
+/*
+ * Lists
+ */
+$list-font-size: $gl-font-size;
+$list-title-color: $gl-title-color;
+$list-text-color: $gl-text-color;
+
+/*
+ * Markdown
+ */
+$md-text-color: $gl-text-color;
+$md-link-color: $gl-link-color;
+
+/*
+ * Code
+ */
$code_font_size: 13px;
$code_line_height: 1.5;
-$border-color: #efeff1;
-$table-border-color: #eef0f2;
-$background-color: #faf9f9;
-$header-height: 58px;
-$fixed-layout-width: 1280px;
-$gl-gray: #5a5a5a;
+
+/*
+ * Padding
+ */
$gl-padding: 16px;
$gl-btn-padding: 10px;
$gl-vert-padding: 6px;
$gl-padding-top: 10px;
+
+/*
+ * Misc
+ */
+$row-hover: #f4f8fe;
+$progress-color: #c0392b;
+$avatar_radius: 50%;
+$header-height: 58px;
+$fixed-layout-width: 1280px;
$gl-avatar-size: 40px;
-$secondary-text: #7f8fa4;
$error-exclamation-point: #e62958;
-$border-radius-default: 3px;
-$list-title-color: #333;
-$list-text-color: #555;
-
+$border-radius-default: 2px;
$btn-transparent-color: #8f8f8f;
-
$ssh-key-icon-color: #8f8f8f;
$ssh-key-icon-size: 18px;
-
$provider-btn-group-border: #e5e5e5;
$provider-btn-not-active-color: #4688f1;
@@ -136,17 +168,20 @@ $regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif
/*
* Dropdowns
*/
+$dropdown-border-radius: 2px;
+$dropdown-width: 300px;
$dropdown-bg: #fff;
$dropdown-link-color: #555;
-$dropdown-link-hover-bg: rgba(#000, .04);
+$dropdown-link-hover-bg: $row-hover;
+$dropdown-empty-row-bg: rgba(#000, .04);
$dropdown-border-color: rgba(#000, .1);
$dropdown-shadow-color: rgba(#000, .1);
$dropdown-divider-color: rgba(#000, .1);
$dropdown-header-color: #959494;
$dropdown-title-btn-color: #bfbfbf;
-$dropdown-input-color: #c7c7c7;
-$dropdown-input-focus-border: rgb(58, 171, 240);
-$dropdown-input-focus-shadow: rgba(#000, .2);
+$dropdown-input-color: #555;
+$dropdown-input-focus-border: $focus-border-color;
+$dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, .4);
$dropdown-loading-bg: rgba(#fff, .6);
$dropdown-toggle-bg: #fff;
@@ -162,3 +197,23 @@ $dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color;
$award-emoji-menu-bg: #fff;
$award-emoji-menu-border: #f1f2f4;
$award-emoji-new-btn-icon-color: #dcdcdc;
+
+/*
+ * Search Box
+ */
+$search-input-border-color: $dropdown-input-focus-border;
+$search-input-focus-shadow-color: $dropdown-input-focus-shadow;
+$search-input-width: $dropdown-width;
+$location-badge-color: #aaa;
+$location-badge-bg: $gray-normal;
+$location-icon-color: #e7e9ed;
+$location-active-color: $gl-text-color;
+$location-active-bg: $search-input-border-color;
+
+/*
+ * Notes
+ */
+$notes-light-color: #8e8e8e;
+$notes-action-color: #c3c3c3;
+$notes-role-color: #8e8e8e;
+$notes-role-border-color: #e4e4e4;
diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss
new file mode 100644
index 00000000000..0a13a7e0b54
--- /dev/null
+++ b/app/assets/stylesheets/notify.scss
@@ -0,0 +1,24 @@
+img {
+ max-width: 100%;
+ height: auto;
+}
+p.details {
+ font-style: italic;
+ color: #777
+}
+.footer p {
+ font-size: small;
+ color: #777
+}
+pre.commit-message {
+ white-space: pre-wrap;
+}
+.file-stats a {
+ text-decoration: none;
+}
+.file-stats .new-file {
+ color: #090;
+}
+.file-stats .deleted-file {
+ color: #b00;
+}
diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss
index a61161810a3..e05f14e7496 100644
--- a/app/assets/stylesheets/pages/admin.scss
+++ b/app/assets/stylesheets/pages/admin.scss
@@ -34,9 +34,9 @@
background: #fff
}
- .visibility-levels {
- .controls {
- margin-bottom: 9px;
+ .visibility-levels {
+ .controls {
+ margin-bottom: 9px;
}
i {
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index d57be1b2daa..b6011fe7679 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -55,7 +55,7 @@ li.commit {
}
.commit-row-message {
- color: $gl-link-color;
+ color: $gl-dark-link-color;
&:hover {
text-decoration: underline;
@@ -93,12 +93,15 @@ li.commit {
.commit-row-info {
color: $gl-gray;
line-height: 24px;
- font-size: 13px;
a {
color: $gl-gray;
}
+ .avatar {
+ margin-right: 8px;
+ }
+
.committed_ago {
display: inline-block;
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index db06b8288c2..f1368d74b3b 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -1,7 +1,7 @@
// Common
.diff-file {
border: 1px solid $border-color;
- border-top: none;
+ margin-bottom: $gl-padding;
.diff-header {
position: relative;
@@ -132,7 +132,7 @@
}
.image-info {
font-size: 12px;
- margin: 5px 0 0 0;
+ margin: 5px 0 0;
color: grey;
}
@@ -361,3 +361,11 @@
border-color: $border;
}
}
+
+.files {
+ margin-top: -1px;
+
+ .diff-file:last-child {
+ margin-bottom: 0;
+ }
+}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index b39a9abf40f..c66efe978cd 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -6,7 +6,7 @@
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;
- color: #7f8fa4;
+ color: $list-text-color;
&.event-inline {
.avatar {
@@ -21,7 +21,7 @@
}
a {
- color: #4c4e54;
+ color: $gl-dark-link-color;
}
.avatar {
@@ -31,10 +31,7 @@
.event-title {
@include str-truncated(calc(100% - 174px));
font-weight: 600;
-
- .author_name {
- color: #333;
- }
+ color: $list-text-color;
}
.event-body {
@@ -46,10 +43,6 @@
.md {
color: #7f8fa4;
font-size: $gl-font-size;
-
- iframe.twitter-share-button {
- vertical-align: bottom;
- }
}
pre {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 6f93299404c..88c1b614c74 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -1,34 +1,3 @@
-@media (max-width: $screen-sm-max) {
- .issuable-affix {
- margin-top: 20px;
- }
-}
-
-@media (max-width: $screen-md-max) {
- .issuable-affix {
- position: static;
- }
-}
-
-@media (min-width: $screen-md-max) {
- .issuable-affix {
- &.affix-top {
- position: static;
- }
-
- &.affix {
- position: fixed;
- top: 70px;
- margin-right: 35px;
-
- &.no-affix {
- position: relative;
- top: 0;
- }
- }
- }
-}
-
.issuable-details {
section {
.issuable-discussion {
@@ -54,9 +23,17 @@
padding: 6px 10px;
}
}
+
+ &.has-labels {
+ margin-bottom: -5px;
+ }
}
.issuable-sidebar {
+ a {
+ color: inherit;
+ }
+
.block {
@include clearfix;
padding: $gl-padding 0;
@@ -66,8 +43,9 @@
width: $gutter_inner_width;
// --
- &:first-child {
- padding-top: 5px;
+ &.issuable-sidebar-header {
+ padding-top: 0;
+ padding-bottom: 10px;
}
&:last-child {
@@ -75,7 +53,6 @@
}
span {
- margin-top: 7px;
display: inline-block;
}
@@ -84,7 +61,7 @@
}
.issuable-count {
-
+ margin-top: 7px;
}
.gutter-toggle {
@@ -99,24 +76,24 @@
.title {
color: $gl-text-color;
- margin-bottom: 8px;
+ margin-bottom: 10px;
+ line-height: 1;
.avatar {
margin-left: 0;
}
- label {
- font-weight: normal;
- margin-right: 4px;
- }
-
.edit-link {
color: $gl-gray;
+
+ &:hover {
+ color: $md-link-color;
+ }
}
}
.cross-project-reference {
- color: $gl-link-color;
+ color: inherit;
span {
white-space: nowrap;
@@ -144,11 +121,6 @@
.btn-clipboard {
color: $gl-gray;
}
-
- .participants .avatar {
- margin-top: 6px;
- margin-right: 2px;
- }
}
.right-sidebar {
@@ -163,8 +135,18 @@
&.right-sidebar-expanded {
width: $gutter_width;
- hr {
- display: none;
+ .value {
+ line-height: 1;
+
+ .assign-yourself {
+ margin-top: 10px;
+ font-weight: normal;
+ display: block;
+ }
+ }
+
+ .bold {
+ font-weight: 600;
}
.sidebar-collapsed-icon {
@@ -172,8 +154,23 @@
}
.gutter-toggle {
+ margin-top: 7px;
border-left: 1px solid $border-gray-light;
}
+
+ .assignee .avatar {
+ float: left;
+ margin-right: 10px;
+ margin-bottom: 0;
+ margin-left: 0;
+ }
+
+ .username {
+ display: block;
+ margin-top: 4px;
+ font-size: 13px;
+ font-weight: normal;
+ }
}
.subscribe-button {
@@ -193,28 +190,26 @@
width: $sidebar_collapsed_width;
padding-top: 0;
- hr {
- margin: 0;
- color: $gray-normal;
- border-color: $gray-normal;
- width: 62px;
- margin-left: -20px
- }
-
.block {
width: $sidebar_collapsed_width - 1px;
margin-left: -19px;
- padding: 15px 0 0 0;
+ padding: 15px 0 0;
border-bottom: none;
overflow: hidden;
}
+ .participants {
+ border-bottom: 1px solid $border-gray-light;
+ }
+
.hide-collapsed {
display: none;
}
.gutter-toggle {
- margin-left: -36px;
+ width: 100%;
+ margin-left: 0;
+ padding-left: 25px;
}
.sidebar-collapsed-icon {
@@ -229,6 +224,10 @@
margin-top: 0;
}
+ .author {
+ display: none;
+ }
+
.btn-clipboard {
border: none;
@@ -241,6 +240,11 @@
}
}
}
+
+ .sidebar-collapsed-user {
+ padding-bottom: 0;
+ margin-bottom: 10px;
+ }
}
.btn {
@@ -251,6 +255,22 @@
border: 1px solid $border-gray-dark;
}
}
+
+ a:not(.btn) {
+ &:hover {
+ color: $md-link-color;
+ text-decoration: none;
+ }
+ }
+
+ .dropdown-menu-toggle {
+ width: 100%;
+ padding-top: 6px;
+ }
+
+ .open .dropdown-menu {
+ width: 100%;
+ }
}
.btn-default.gutter-toggle {
@@ -270,3 +290,29 @@
color: $gray-darkest;
}
}
+
+.participants-list {
+ margin: -5px;
+}
+
+.participants-author {
+ display: inline-block;
+ padding: 5px;
+
+ .author_link {
+ display: block;
+ }
+
+ .avatar.avatar-inline {
+ margin: 0;
+ }
+}
+
+.participants-more {
+ margin-top: 5px;
+ margin-left: 5px;
+
+ a {
+ color: #8c8c8c;
+ }
+}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 7ac4bc468d6..6a1d28590c2 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -3,7 +3,7 @@
padding: 10px $gl-padding;
position: relative;
- .issue-title {
+ .title {
margin-bottom: 2px;
}
@@ -130,14 +130,14 @@ form.edit-issue {
}
.issue-closed-by-widget {
- color: $secondary-text;
+ color: $gl-text-color;
margin-left: 52px;
}
.editor-details {
display: block;
-
+
@media (min-width: $screen-sm-min) {
display: inline-block;
}
-} \ No newline at end of file
+}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 61ee34b695e..4e02ec4e891 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -9,28 +9,45 @@
}
&.suggest-colors-dropdown {
- margin-bottom: 5px;
+ margin-top: 10px;
+ margin-bottom: 10px;
+ border-radius: $border-radius-base;
+ overflow: hidden;
a {
@include border-radius(0);
- width: 36.7px;
+ width: (100% / 7);
margin-right: 0;
margin-bottom: -5px;
}
}
}
-.dropdown-label-color-preview {
- display: none;
- margin-top: 5px;
- width: 100%;
- height: 25px;
+.dropdown-new-label {
+ .dropdown-content {
+ max-height: 260px;
+ }
+}
+
+.dropdown-label-color-input {
+ position: relative;
+ margin-bottom: 10px;
&.is-active {
- display: block;
+ padding-left: 32px;
}
}
+.dropdown-label-color-preview {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 32px;
+ height: 32px;
+ border-top-left-radius: $border-radius-base;
+ border-bottom-left-radius: $border-radius-base;
+}
+
.label-row {
.label {
padding: 9px;
@@ -45,3 +62,10 @@
.label-subscription {
display: inline-block;
}
+
+.dropdown-labels-error {
+ padding: 5px 10px;
+ margin-bottom: 10px;
+ background-color: $gl-danger;
+ color: $white-light;
+}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index bc41f7d306f..777bcbca5c3 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -45,7 +45,7 @@
.login-heading h3 {
font-weight: 300;
line-height: 1.5;
- margin: 0 0 10px 0;
+ margin: 0 0 10px;
}
.login-footer {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index cee5c47cfb2..7ff63ca20b6 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -230,3 +230,9 @@
}
}
}
+
+.builds {
+ .table-holder {
+ overflow-x: scroll;
+ }
+}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 61783ec46aa..655f88b0c2c 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -26,7 +26,7 @@
display: none;
}
-.new_note, .edit_note {
+.new_note, .note-edit-form {
.note-form-actions {
margin-top: $gl-padding;
}
@@ -71,8 +71,6 @@
}
.note-form-actions {
- background: #fff;
-
.note-form-option {
margin-top: 8px;
margin-left: 30px;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index d408853cc80..072de68c8e2 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -22,7 +22,7 @@ ul.notes {
margin-left: 55px;
}
- .note_created_ago, .note-updated-at {
+ .note-created-ago, .note-updated-at {
white-space: nowrap;
}
@@ -39,53 +39,6 @@ ul.notes {
}
}
- .discussion-header,
- .note-header {
- @extend .cgray;
-
- a:hover {
- text-decoration: none;
- }
-
- .avatar {
- float: left;
- margin-right: 10px;
- }
-
- .discussion-last-update,
- .note-last-update {
- &:before {
- content: "\00b7";
- }
-
- a {
- color: $gl-gray;
-
- &:hover {
- text-decoration: underline;
- }
- }
- }
- .author {
- color: #4c4e54;
- margin-right: 3px;
-
- &:hover {
- color: $gl-link-color;
- }
- }
- .author-username {
- }
-
- .note-role {
- float: right;
- margin-top: 1px;
- border: 1px solid #bbb;
- background-color: transparent;
- color: $gl-gray;
- }
- }
-
.discussion-body {
padding-top: 15px;
}
@@ -100,6 +53,18 @@ ul.notes {
display: block;
position: relative;
+ &.is-editting {
+ .note-header,
+ .note-text,
+ .edited-text {
+ display: none;
+ }
+
+ .note-edit-form {
+ display: block;
+ }
+ }
+
.note-body {
overflow: auto;
@@ -193,33 +158,81 @@ ul.notes {
}
}
+.discussion-header,
+.note-header {
+ a {
+ color: inherit;
+
+ &:hover {
+ color: $gl-link-color;
+ text-decoration: none;
+ }
+ }
+
+ .author_link {
+ font-weight: 600;
+ }
+}
+
+.note-headline-light,
+.discussion-headline-light {
+ color: $notes-light-color;
+}
+
/**
* Actions for Discussions/Notes
*/
-.discussion,
-.note {
- .discussion-actions,
- .note-actions {
- float: right;
- margin-left: 10px;
+.discussion-actions,
+.note-actions {
+ float: right;
+ margin-left: 10px;
+ color: $notes-action-color;
+}
- a {
- margin-left: 5px;
- color: $gl-gray;
+.note-action-button,
+.discussion-action-button {
+ display: inline-block;
+ margin-left: 10px;
+ line-height: 24px;
- i.fa {
- font-size: 16px;
- line-height: 16px;
- }
+ .fa {
+ position: relative;
+ top: 1px;
+ font-size: 17px;
+ }
- &:hover {
- @extend .cgray;
- &.danger { @extend .cred; }
- }
- }
+ .fa-trash-o {
+ top: 0;
+ font-size: 16px;
+ }
+}
+
+.discussion-toggle-button {
+ line-height: 20px;
+ font-size: 13px;
+
+ .fa {
+ margin-right: 3px;
+ font-size: 10px;
+ line-height: 18px;
+ vertical-align: top;
}
}
+
+.note-role {
+ position: relative;
+ top: -2px;
+ display: inline-block;
+ padding-left: 4px;
+ padding-right: 4px;
+ color: $notes-role-color;
+ font-size: 12px;
+ line-height: 20px;
+ border: 1px solid $notes-role-border-color;
+ border-radius: $border-radius-base;
+}
+
.diff-file .note .note-actions {
right: 0;
top: 0;
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 260179074cf..a9656e5cae7 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -54,7 +54,7 @@
}
.account-well {
- padding: 10px 10px;
+ padding: 10px;
background-color: $help-well-bg;
border: 1px solid $help-well-border;
border-radius: $border-radius-base;
@@ -197,3 +197,24 @@
width: 105px;
}
}
+
+.modal-profile-crop {
+ .modal-dialog {
+ width: 380px;
+
+ @media (max-width: $screen-sm-min) {
+ width: auto;
+ }
+
+ }
+
+ .profile-crop-image-container {
+ height: 300px;
+ margin: 0 auto;
+ }
+
+ .crop-controls {
+ padding: 10px 0 0;
+ text-align: center;
+ }
+}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 82c5069638d..4e6aa8cd1a6 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -37,7 +37,7 @@
.dropdown-menu {
left: auto;
width: auto;
- right: 0px;
+ right: 0;
max-width: 240px;
}
}
@@ -68,28 +68,6 @@
}
}
- .project-home-desc {
- h1 {
- color: #313236;
- margin: 0;
- margin-bottom: 6px;
- font-size: 23px;
- font-weight: normal;
- }
-
- .visibility-icon {
- display: inline-block;
- margin-left: 5px;
- font-size: 18px;
- color: $gray;
- }
-
- p {
- padding: 0 $gl-padding;
- color: #5c5d5e;
- }
- }
-
.project-repo-buttons {
margin-top: 20px;
margin-bottom: 0;
@@ -184,7 +162,7 @@
margin-right: 12px;
a {
- margin: -1px !important;
+ margin: -1px;
}
}
@@ -244,13 +222,17 @@
padding: 0;
background: transparent;
border: none;
- line-height: 42px;
+ line-height: 36px;
margin: 0;
> li + li:before {
padding: 0 3px;
color: #999;
}
+
+ a {
+ color: $gl-dark-link-color;
+ }
}
.last-push-widget {
@@ -333,7 +315,7 @@ pre.light-well {
}
.git-empty {
- margin: 0 7px 0 7px;
+ margin: 0 7px;
h5 {
color: #5c5d5e;
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index b6e45024644..3c74d25beb0 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -21,3 +21,145 @@
}
}
+.search {
+ margin-right: 10px;
+ margin-left: 10px;
+ margin-top: ($header-height - 35) / 2;
+
+ form {
+ @extend .form-control;
+ margin: 0;
+ padding: 4px;
+ width: $search-input-width;
+ line-height: 24px;
+ }
+
+ .location-text {
+ font-style: normal;
+ }
+
+ .search-input {
+ border: none;
+ font-size: 14px;
+ outline: none;
+ padding: 0;
+ margin-left: 5px;
+ line-height: 25px;
+ width: 98%;
+ }
+
+ .location-badge {
+ line-height: 25px;
+ padding: 0 5px;
+ border-radius: $border-radius-default;
+ font-size: 14px;
+ font-style: normal;
+ color: $location-badge-color;
+ display: inline-block;
+ background-color: $location-badge-bg;
+ vertical-align: top;
+ }
+
+ .search-input-container {
+ display: -webkit-flex;
+ display: flex;
+ position: relative;
+ }
+
+ .search-location-badge, .search-input-wrap {
+ // Fallback if flexbox is not supported
+ display: inline-block;
+ }
+
+ .search-input-wrap {
+ width: 100%;
+
+ .search-icon, .clear-icon {
+ position: absolute;
+ right: 5px;
+ top: 0;
+ color: $location-icon-color;
+
+ &:before {
+ font-family: FontAwesome;
+ font-weight: normal;
+ font-style: normal;
+ }
+ }
+
+ .search-icon {
+ @extend .fa-search;
+ @include transition(color .15s);
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ }
+
+ .clear-icon {
+ @extend .fa-times;
+ display: none;
+ }
+
+ // Rewrite position. Dropdown menu should be relative to .search-input-container
+ .dropdown {
+ position: static;
+ }
+
+ .dropdown-header {
+ text-transform: uppercase;
+ font-size: 11px;
+ }
+
+ // Custom dropdown positioning
+ .dropdown-menu {
+ top: 30px;
+ left: -5px;
+ padding: 0;
+
+ ul {
+ padding: 10px 0;
+ }
+ }
+
+ .dropdown-content {
+ max-height: 350px;
+ }
+ }
+
+ &.search-active {
+ form {
+ @extend .form-control:focus;
+ border-color: $dropdown-input-focus-border;
+ box-shadow: 0 0 4px $search-input-focus-shadow-color;
+ }
+
+ .location-badge {
+ @include transition(all .15s);
+ background-color: $location-active-bg;
+ color: $white-light;
+ }
+
+ .search-input-wrap {
+ i {
+ color: $location-active-color;
+ }
+ }
+
+ &.has-location-badge {
+ .search-icon {
+ display: none;
+ }
+
+ .clear-icon {
+ cursor: pointer;
+ display: block;
+ }
+ }
+ }
+
+ &.has-location-badge {
+ .search-input-wrap {
+ width: 78%;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss
index b9be47e7700..85a0304196c 100644
--- a/app/assets/stylesheets/pages/stat_graph.scss
+++ b/app/assets/stylesheets/pages/stat_graph.scss
@@ -16,7 +16,7 @@
#contributors {
.contributors-list {
- margin: 0 0 10px 0;
+ margin: 0 0 10px;
list-style: none;
padding: 0;
}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 6f777d11641..5e5e38a0ba6 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -1,54 +1,58 @@
-.ci-status {
- padding: 2px 7px;
- margin-right: 5px;
- border: 1px solid #eee;
- white-space: nowrap;
- @include border-radius(4px);
+.container-fluid .content {
+ .ci-status {
+ padding: 2px 7px;
+ margin-right: 5px;
+ border: 1px solid #eee;
+ white-space: nowrap;
+ @include border-radius(4px);
- &:hover {
- text-decoration: none;
- }
+ &:hover {
+ text-decoration: none;
+ }
- &.ci-failed {
- color: $gl-danger;
- border-color: $gl-danger;
- }
+ &.ci-failed {
+ color: $gl-danger;
+ border-color: $gl-danger;
+ }
- &.ci-success {
- color: $gl-success;
- border-color: $gl-success;
- }
+ &.ci-success {
+ color: $gl-success;
+ border-color: $gl-success;
+ }
- &.ci-info {
- color: $gl-info;
- border-color: $gl-info;
- }
+ &.ci-info {
+ color: $gl-info;
+ border-color: $gl-info;
+ }
- &.ci-disabled {
- color: $gl-gray;
- border-color: $gl-gray;
+ &.ci-canceled,
+ &.ci-skipped,
+ &.ci-disabled {
+ color: $gl-gray;
+ border-color: $gl-gray;
+ }
+
+ &.ci-pending,
+ &.ci-running {
+ color: $gl-warning;
+ border-color: $gl-warning;
+ }
}
- &.ci-pending,
- &.ci-running {
+ .ci-status-icon-success {
+ color: $gl-success;
+ }
+ .ci-status-icon-failed {
+ color: $gl-danger;
+ }
+ .ci-status-icon-running,
+ .ci-status-icon-pending {
color: $gl-warning;
- border-color: $gl-warning;
}
-}
-
-.ci-status-icon-success {
- @extend .cgreen;
-}
-.ci-status-icon-failed {
- @extend .cred;
-}
-.ci-status-icon-running,
-.ci-status-icon-pending {
- // These are standard text color
-}
-.ci-status-icon-canceled,
-.ci-status-icon-disabled,
-.ci-status-icon-not-found,
-.ci-status-icon-skipped {
- @extend .cgray;
+ .ci-status-icon-canceled,
+ .ci-status-icon-disabled,
+ .ci-status-icon-not-found,
+ .ci-status-icon-skipped {
+ color: $gl-gray;
+ }
}
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 27970eba159..e83fa9e3d52 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -6,33 +6,22 @@
.navbar-nav {
li {
.badge.todos-pending-count {
- background-color: #7f8fa4;
+ background-color: $gl-icon-color;
margin-top: -5px;
font-weight: normal;
}
}
}
-.todo-item {
- font-size: $gl-font-size;
- padding-left: $gl-avatar-size + $gl-padding-top;
- color: $secondary-text;
-
- a {
- color: #4c4e54;
- }
-
- .avatar {
- margin-left: -($gl-avatar-size + $gl-padding-top);
+.todo {
+ &:hover {
+ cursor: pointer;
}
+}
+.todo-item {
.todo-title {
@include str-truncated(calc(100% - 174px));
- font-weight: 600;
-
- .author-name {
- color: #333;
- }
}
.todo-body {
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 73c7c9f687c..25b5e95583e 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -41,7 +41,7 @@
vertical-align: middle;
i, a {
- color: $gl-link-color;
+ color: $gl-dark-link-color;
}
img {
diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss
index 8886c1dff56..3f28e402929 100644
--- a/app/assets/stylesheets/pages/xterm.scss
+++ b/app/assets/stylesheets/pages/xterm.scss
@@ -21,19 +21,19 @@
$l-white: #fff;
.term-bold {
- font-weight: bold;
+ font-weight: bold;
}
.term-italic {
- font-style: italic;
+ font-style: italic;
}
.term-conceal {
- visibility: hidden;
+ visibility: hidden;
}
.term-underline {
- text-decoration: underline;
+ text-decoration: underline;
}
.term-cross {
- text-decoration: line-through;
+ text-decoration: line-through;
}
.term-fg-black {
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 04a99d8c84a..f010436bd36 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -52,7 +52,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:require_two_factor_authentication,
:two_factor_grace_period,
:gravatar_enabled,
- :twitter_sharing_enabled,
:sign_in_text,
:help_page_text,
:home_page_url,
@@ -61,6 +60,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:session_expire_delay,
:default_project_visibility,
:default_snippet_visibility,
+ :default_group_visibility,
:restricted_signup_domains_raw,
:version_check_enabled,
:admin_notification_email,
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 668396a0f20..a6db4690df0 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -5,12 +5,12 @@ class Admin::GroupsController < Admin::ApplicationController
@groups = Group.all
@groups = @groups.sort(@sort = params[:sort])
@groups = @groups.search(params[:name]) if params[:name].present?
- @groups = @groups.page(params[:page]).per(PER_PAGE)
+ @groups = @groups.page(params[:page])
end
def show
- @members = @group.members.order("access_level DESC").page(params[:members_page]).per(PER_PAGE)
- @projects = @group.projects.page(params[:projects_page]).per(PER_PAGE)
+ @members = @group.members.order("access_level DESC").page(params[:members_page])
+ @projects = @group.projects.page(params[:projects_page])
end
def new
@@ -59,6 +59,6 @@ class Admin::GroupsController < Admin::ApplicationController
end
def group_params
- params.require(:group).permit(:name, :description, :path, :avatar)
+ params.require(:group).permit(:name, :description, :path, :avatar, :visibility_level)
end
end
diff --git a/app/controllers/admin/labels_controller.rb b/app/controllers/admin/labels_controller.rb
index d79ce2b10fe..d496f08a598 100644
--- a/app/controllers/admin/labels_controller.rb
+++ b/app/controllers/admin/labels_controller.rb
@@ -2,7 +2,7 @@ class Admin::LabelsController < Admin::ApplicationController
before_action :set_label, only: [:show, :edit, :update, :destroy]
def index
- @labels = Label.templates.page(params[:page]).per(PER_PAGE)
+ @labels = Label.templates.page(params[:page])
end
def show
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index ae1de06b983..4089091d569 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -1,7 +1,6 @@
class Admin::ProjectsController < Admin::ApplicationController
before_action :project, only: [:show, :transfer]
before_action :group, only: [:show, :transfer]
- before_action :repository, only: [:show, :transfer]
def index
@projects = Project.all
@@ -12,15 +11,15 @@ class Admin::ProjectsController < Admin::ApplicationController
@projects = @projects.non_archived unless params[:with_archived].present?
@projects = @projects.search(params[:name]) if params[:name].present?
@projects = @projects.sort(@sort = params[:sort])
- @projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page]).per(PER_PAGE)
+ @projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page])
end
def show
if @group
- @group_members = @group.members.order("access_level DESC").page(params[:group_members_page]).per(PER_PAGE)
+ @group_members = @group.members.order("access_level DESC").page(params[:group_members_page])
end
- @project_members = @project.project_members.page(params[:project_members_page]).per(PER_PAGE)
+ @project_members = @project.project_members.page(params[:project_members_page])
end
def transfer
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 1f55b18e0b1..c81cb85dc1b 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -6,8 +6,6 @@ class ApplicationController < ActionController::Base
include GitlabRoutingHelper
include PageLayoutHelper
- PER_PAGE = 20
-
before_action :authenticate_user_from_token!
before_action :authenticate_user!
before_action :validate_user_service_ticket!
@@ -25,7 +23,6 @@ class ApplicationController < ActionController::Base
helper_method :abilities, :can?, :current_application_settings
helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?
- helper_method :repository, :can_collaborate_with_project?
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
@@ -118,47 +115,6 @@ class ApplicationController < ActionController::Base
abilities.allowed?(object, action, subject)
end
- def project
- unless @project
- namespace = params[:namespace_id]
- id = params[:project_id] || params[:id]
-
- # Redirect from
- # localhost/group/project.git
- # to
- # localhost/group/project
- #
- if id =~ /\.git\Z/
- redirect_to request.original_url.gsub(/\.git\/?\Z/, '') and return
- end
-
- project_path = "#{namespace}/#{id}"
- @project = Project.find_with_namespace(project_path)
-
- if @project and can?(current_user, :read_project, @project)
- if @project.path_with_namespace != project_path
- redirect_to request.original_url.gsub(project_path, @project.path_with_namespace) and return
- end
- @project
- elsif current_user.nil?
- @project = nil
- authenticate_user!
- else
- @project = nil
- render_404 and return
- end
- end
- @project
- end
-
- def repository
- @repository ||= project.repository
- end
-
- def authorize_project!(action)
- return access_denied! unless can?(current_user, action, project)
- end
-
def access_denied!
render "errors/access_denied", layout: "errors", status: 404
end
@@ -167,14 +123,6 @@ class ApplicationController < ActionController::Base
render "errors/git_not_found.html", layout: "errors", status: 404
end
- def method_missing(method_sym, *arguments, &block)
- if method_sym.to_s =~ /\Aauthorize_(.*)!\z/
- authorize_project!($1.to_sym)
- else
- super
- end
- end
-
def render_403
head :forbidden
end
@@ -183,10 +131,6 @@ class ApplicationController < ActionController::Base
render file: Rails.root.join("public", "404"), layout: false, status: "404"
end
- def require_non_empty_project
- redirect_to @project if @project.empty_repo?
- end
-
def no_cache_headers
response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
response.headers["Pragma"] = "no-cache"
@@ -412,13 +356,6 @@ class ApplicationController < ActionController::Base
current_user.nil? && root_path == request.path
end
- def can_collaborate_with_project?(project = nil)
- project ||= @project
-
- can?(current_user, :push_code, project) ||
- (current_user && current_user.already_forked?(project))
- end
-
private
def set_default_sort
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 77c8dafc012..81ba58ce49c 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -7,7 +7,7 @@ class AutocompleteController < ApplicationController
@users = @users.search(params[:search]) if params[:search].present?
@users = @users.active
@users = @users.reorder(:name)
- @users = @users.page(params[:page]).per(PER_PAGE)
+ @users = @users.page(params[:page])
if params[:search].blank?
# Include current user if available to filter by "Me"
diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb
index 081e01a75e0..8bf71a1adbb 100644
--- a/app/controllers/ci/projects_controller.rb
+++ b/app/controllers/ci/projects_controller.rb
@@ -1,11 +1,15 @@
module Ci
class ProjectsController < Ci::ApplicationController
before_action :project
- before_action :authorize_read_project!, except: [:badge]
before_action :no_cache, only: [:badge]
+ before_action :authorize_read_project!, except: [:badge, :index]
skip_before_action :authenticate_user!, only: [:badge]
protect_from_forgery
+ def index
+ redirect_to root_path
+ end
+
def show
# Temporary compatibility with CI badges pointing to CI project page
redirect_to namespace_project_path(project.namespace, project)
@@ -35,5 +39,9 @@ module Ci
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
end
+
+ def authorize_read_project!
+ return access_denied! unless can?(current_user, :read_project, project)
+ end
end
end
diff --git a/app/controllers/concerns/global_milestones.rb b/app/controllers/concerns/global_milestones.rb
index 3e4c0e63601..5c503c5b698 100644
--- a/app/controllers/concerns/global_milestones.rb
+++ b/app/controllers/concerns/global_milestones.rb
@@ -6,7 +6,6 @@ module GlobalMilestones
@milestones = MilestonesFinder.new.execute(@projects, params)
@milestones = GlobalMilestone.build_collection(@milestones)
@milestones = @milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
- @milestones = Kaminari.paginate_array(@milestones).page(params[:page]).per(ApplicationController::PER_PAGE)
end
def milestone
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
new file mode 100644
index 00000000000..f40b62446e5
--- /dev/null
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -0,0 +1,23 @@
+module IssuableActions
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :authorize_destroy_issuable!, only: :destroy
+ end
+
+ def destroy
+ issuable.destroy
+
+ name = issuable.class.name.titleize.downcase
+ flash[:notice] = "The #{name} was successfully deleted."
+ redirect_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class])
+ end
+
+ private
+
+ def authorize_destroy_issuable!
+ unless current_user.can?(:"destroy_#{issuable.to_ability_name}", issuable)
+ return access_denied!
+ end
+ end
+end
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index ef8e74a4641..4feabc32b1c 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -3,7 +3,7 @@ module IssuesAction
def issues
@issues = get_issues_collection.non_archived
- @issues = @issues.page(params[:page]).per(ApplicationController::PER_PAGE)
+ @issues = @issues.page(params[:page])
@issues = @issues.preload(:author, :project)
@label = @issuable_finder.labels.first
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
index 9c49596bd0b..06a6b065e7e 100644
--- a/app/controllers/concerns/merge_requests_action.rb
+++ b/app/controllers/concerns/merge_requests_action.rb
@@ -3,7 +3,7 @@ module MergeRequestsAction
def merge_requests
@merge_requests = get_merge_requests_collection.non_archived
- @merge_requests = @merge_requests.page(params[:page]).per(ApplicationController::PER_PAGE)
+ @merge_requests = @merge_requests.page(params[:page])
@merge_requests = @merge_requests.preload(:author, :target_project)
@label = @issuable_finder.labels.first
diff --git a/app/controllers/dashboard/application_controller.rb b/app/controllers/dashboard/application_controller.rb
index 962ea38d6c9..9d3d1c23c28 100644
--- a/app/controllers/dashboard/application_controller.rb
+++ b/app/controllers/dashboard/application_controller.rb
@@ -1,3 +1,9 @@
class Dashboard::ApplicationController < ApplicationController
layout 'dashboard'
+
+ private
+
+ def projects
+ @projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
+ end
end
diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb
index 3bc94ff2187..71ba6153021 100644
--- a/app/controllers/dashboard/groups_controller.rb
+++ b/app/controllers/dashboard/groups_controller.rb
@@ -1,5 +1,5 @@
class Dashboard::GroupsController < Dashboard::ApplicationController
def index
- @group_members = current_user.group_members.page(params[:page]).per(PER_PAGE)
+ @group_members = current_user.group_members.page(params[:page])
end
end
diff --git a/app/controllers/dashboard/labels_controller.rb b/app/controllers/dashboard/labels_controller.rb
new file mode 100644
index 00000000000..23a4ef21ea2
--- /dev/null
+++ b/app/controllers/dashboard/labels_controller.rb
@@ -0,0 +1,9 @@
+class Dashboard::LabelsController < Dashboard::ApplicationController
+ def index
+ labels = Label.where(project_id: projects).select(:title, :color).uniq(:title)
+
+ respond_to do |format|
+ format.json { render json: labels }
+ end
+ end
+end
diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb
index 2bdce0f8a00..fa9c6c054f0 100644
--- a/app/controllers/dashboard/milestones_controller.rb
+++ b/app/controllers/dashboard/milestones_controller.rb
@@ -2,18 +2,19 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
include GlobalMilestones
before_action :projects
- before_action :milestones, only: [:index]
before_action :milestone, only: [:show]
def index
+ respond_to do |format|
+ format.html do
+ @milestones = Kaminari.paginate_array(milestones).page(params[:page])
+ end
+ format.json do
+ render json: milestones
+ end
+ end
end
def show
end
-
- private
-
- def projects
- @projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
- end
end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 0e8b63872ca..71acc244a91 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -8,7 +8,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = filter_projects(@projects)
@projects = @projects.includes(:namespace)
@projects = @projects.sort(@sort = params[:sort])
- @projects = @projects.page(params[:page]).per(PER_PAGE)
+ @projects = @projects.page(params[:page])
@last_push = current_user.recent_push
@@ -32,7 +32,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = filter_projects(@projects)
@projects = @projects.includes(:namespace, :forked_from_project, :tags)
@projects = @projects.sort(@sort = params[:sort])
- @projects = @projects.page(params[:page]).per(PER_PAGE)
+ @projects = @projects.page(params[:page])
@last_push = current_user.recent_push
@groups = []
diff --git a/app/controllers/dashboard/snippets_controller.rb b/app/controllers/dashboard/snippets_controller.rb
index b3594d82530..bcfdbe14be9 100644
--- a/app/controllers/dashboard/snippets_controller.rb
+++ b/app/controllers/dashboard/snippets_controller.rb
@@ -6,6 +6,6 @@ class Dashboard::SnippetsController < Dashboard::ApplicationController
user: current_user,
scope: params[:scope]
)
- @snippets = @snippets.page(params[:page]).per(PER_PAGE)
+ @snippets = @snippets.page(params[:page])
end
end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 43cf8fa71af..5abf97342c3 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -1,25 +1,34 @@
class Dashboard::TodosController < Dashboard::ApplicationController
- before_action :find_todos, only: [:index, :destroy_all]
+ before_action :find_todos, only: [:index, :destroy, :destroy_all]
def index
- @todos = @todos.page(params[:page]).per(PER_PAGE)
+ @todos = @todos.page(params[:page])
end
def destroy
- todo.done!
+ todo.done
+
+ todo_notice = 'Todo was successfully marked as done.'
respond_to do |format|
- format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' }
+ format.html { redirect_to dashboard_todos_path, notice: todo_notice }
format.js { render nothing: true }
+ format.json do
+ render json: { count: @todos.size, done_count: current_user.todos.done.count }
+ end
end
end
def destroy_all
- @todos.each(&:done!)
+ @todos.each(&:done)
respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' }
format.js { render nothing: true }
+ format.json do
+ find_todos
+ render json: { count: @todos.size, done_count: current_user.todos.done.count }
+ end
end
end
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 139e40db180..1dce4a21729 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -34,8 +34,4 @@ class DashboardController < Dashboard::ApplicationController
@events = @event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
-
- def projects
- @projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
- end
end
diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb
index a9bf4321f73..a962f9a0937 100644
--- a/app/controllers/explore/groups_controller.rb
+++ b/app/controllers/explore/groups_controller.rb
@@ -1,8 +1,8 @@
class Explore::GroupsController < Explore::ApplicationController
def index
- @groups = Group.order_id_desc
+ @groups = GroupsFinder.new.execute(current_user)
@groups = @groups.search(params[:search]) if params[:search].present?
@groups = @groups.sort(@sort = params[:sort])
- @groups = @groups.page(params[:page]).per(PER_PAGE)
+ @groups = @groups.page(params[:page])
end
end
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index 8271ca87436..88a0c18180b 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -8,7 +8,7 @@ class Explore::ProjectsController < Explore::ApplicationController
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
- @projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE)
+ @projects = @projects.includes(:namespace).page(params[:page])
respond_to do |format|
format.html
@@ -23,7 +23,7 @@ class Explore::ProjectsController < Explore::ApplicationController
def trending
@projects = TrendingProjectsFinder.new.execute(current_user)
@projects = filter_projects(@projects)
- @projects = @projects.page(params[:page]).per(PER_PAGE)
+ @projects = @projects.page(params[:page])
respond_to do |format|
format.html
@@ -39,7 +39,7 @@ class Explore::ProjectsController < Explore::ApplicationController
@projects = ProjectsFinder.new.execute(current_user)
@projects = filter_projects(@projects)
@projects = @projects.reorder('star_count DESC')
- @projects = @projects.page(params[:page]).per(PER_PAGE)
+ @projects = @projects.page(params[:page])
respond_to do |format|
format.html
diff --git a/app/controllers/explore/snippets_controller.rb b/app/controllers/explore/snippets_controller.rb
index b70ac51d06e..28760c3f84b 100644
--- a/app/controllers/explore/snippets_controller.rb
+++ b/app/controllers/explore/snippets_controller.rb
@@ -1,6 +1,6 @@
class Explore::SnippetsController < Explore::ApplicationController
def index
@snippets = SnippetsFinder.new.execute(current_user, filter: :all)
- @snippets = @snippets.page(params[:page]).per(PER_PAGE)
+ @snippets = @snippets.page(params[:page])
end
end
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
index be801858eaf..949b4a6c25a 100644
--- a/app/controllers/groups/application_controller.rb
+++ b/app/controllers/groups/application_controller.rb
@@ -1,21 +1,32 @@
class Groups::ApplicationController < ApplicationController
layout 'group'
+
+ skip_before_action :authenticate_user!
before_action :group
private
def group
- @group ||= Group.find_by(path: params[:group_id])
- end
+ unless @group
+ id = params[:group_id] || params[:id]
+ @group = Group.find_by(path: id)
+
+ unless @group && can?(current_user, :read_group, @group)
+ @group = nil
- def authorize_read_group!
- unless @group and can?(current_user, :read_group, @group)
- if current_user.nil?
- return authenticate_user!
- else
- return render_404
+ if current_user.nil?
+ authenticate_user!
+ else
+ render_404
+ end
end
end
+
+ @group
+ end
+
+ def group_projects
+ @projects ||= GroupProjectsFinder.new(group).execute(current_user)
end
def authorize_admin_group!
diff --git a/app/controllers/groups/avatars_controller.rb b/app/controllers/groups/avatars_controller.rb
index 76c87366baa..ad2c20b42db 100644
--- a/app/controllers/groups/avatars_controller.rb
+++ b/app/controllers/groups/avatars_controller.rb
@@ -1,4 +1,6 @@
class Groups::AvatarsController < Groups::ApplicationController
+ before_action :authorize_admin_group!
+
def destroy
@group.remove_avatar!
@group.save
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 0e902c4bb43..d5ef33888c6 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -1,8 +1,5 @@
class Groups::GroupMembersController < Groups::ApplicationController
- skip_before_action :authenticate_user!, only: [:index]
-
# Authorize
- before_action :authorize_read_group!
before_action :authorize_admin_group_member!, except: [:index, :leave]
def index
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 0c2a350bc39..b23c3022fb5 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -1,12 +1,16 @@
class Groups::MilestonesController < Groups::ApplicationController
include GlobalMilestones
- before_action :projects
- before_action :milestones, only: [:index]
+ before_action :group_projects
before_action :milestone, only: [:show, :update]
- before_action :authorize_group_milestone!, only: [:create, :update]
+ before_action :authorize_admin_milestones!, only: [:new, :create, :update]
def index
+ respond_to do |format|
+ format.html do
+ @milestones = Kaminari.paginate_array(milestones).page(params[:page])
+ end
+ end
end
def new
@@ -17,7 +21,7 @@ class Groups::MilestonesController < Groups::ApplicationController
project_ids = params[:milestone][:project_ids]
title = milestone_params[:title]
- @group.projects.where(id: project_ids).each do |project|
+ @projects.where(id: project_ids).each do |project|
Milestones::CreateService.new(project, current_user, milestone_params).execute
end
@@ -37,7 +41,7 @@ class Groups::MilestonesController < Groups::ApplicationController
private
- def authorize_group_milestone!
+ def authorize_admin_milestones!
return render_404 unless can?(current_user, :admin_milestones, group)
end
@@ -48,8 +52,4 @@ class Groups::MilestonesController < Groups::ApplicationController
def milestone_path(title)
group_milestone_path(@group, title.to_slug.to_s, title: title)
end
-
- def projects
- @projects ||= @group.projects
- end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 06c5c8be9a5..c1adc999567 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -5,16 +5,15 @@ class GroupsController < Groups::ApplicationController
respond_to :html
- skip_before_action :authenticate_user!, only: [:index, :show, :issues, :merge_requests]
+ before_action :authenticate_user!, only: [:new, :create]
before_action :group, except: [:index, :new, :create]
# Authorize
- before_action :authorize_read_group!, except: [:index, :show, :new, :create, :autocomplete]
before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects]
before_action :authorize_create_group!, only: [:new, :create]
# Load group projects
- before_action :load_projects, except: [:index, :new, :create, :projects, :edit, :update, :autocomplete]
+ before_action :group_projects, only: [:show, :projects, :activity, :issues, :merge_requests]
before_action :event_filter, only: [:activity]
layout :determine_layout
@@ -28,11 +27,9 @@ class GroupsController < Groups::ApplicationController
end
def create
- @group = Group.new(group_params)
- @group.name = @group.path.dup unless @group.name
+ @group = Groups::CreateService.new(current_user, group_params).execute
- if @group.save
- @group.add_owner(current_user)
+ if @group.persisted?
redirect_to @group, notice: "Group '#{@group.name}' was successfully created."
else
render action: "new"
@@ -41,12 +38,13 @@ class GroupsController < Groups::ApplicationController
def show
@last_push = current_user.recent_push if current_user
+
@projects = @projects.includes(:namespace)
@projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
- @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
+ @projects = @projects.page(params[:page]) if params[:filter_projects].blank?
- @shared_projects = @group.shared_projects
+ @shared_projects = GroupProjectsFinder.new(group, only_shared: true).execute(current_user)
respond_to do |format|
format.html
@@ -83,7 +81,7 @@ class GroupsController < Groups::ApplicationController
end
def update
- if @group.update_attributes(group_params)
+ if Groups::UpdateService.new(@group, current_user, group_params).execute
redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated."
else
render action: "edit"
@@ -98,26 +96,6 @@ class GroupsController < Groups::ApplicationController
protected
- def group
- @group ||= Group.find_by(path: params[:id])
- @group || render_404
- end
-
- def load_projects
- @projects ||= ProjectsFinder.new.execute(current_user, group: group).sorted_by_activity
- end
-
- # Dont allow unauthorized access to group
- def authorize_read_group!
- unless @group and (@projects.present? or can?(current_user, :read_group, @group))
- if current_user.nil?
- return authenticate_user!
- else
- return render_404
- end
- end
- end
-
def authorize_create_group!
unless can?(current_user, :create_group, nil)
return render_404
@@ -135,7 +113,7 @@ class GroupsController < Groups::ApplicationController
end
def group_params
- params.require(:group).permit(:name, :description, :path, :avatar, :public, :share_with_group_lock)
+ params.require(:group).permit(:name, :description, :path, :avatar, :public, :visibility_level, :share_with_group_lock)
end
def load_events
diff --git a/app/controllers/namespaces_controller.rb b/app/controllers/namespaces_controller.rb
index 282012c60a1..5a94dcb0dbd 100644
--- a/app/controllers/namespaces_controller.rb
+++ b/app/controllers/namespaces_controller.rb
@@ -14,7 +14,7 @@ class NamespacesController < ApplicationController
if user
redirect_to user_path(user)
- elsif group
+ elsif group && can?(current_user, :read_group, namespace)
redirect_to group_path(group)
elsif current_user.nil?
authenticate_user!
diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb
index 24025d8c723..c721dca58d9 100644
--- a/app/controllers/oauth/authorizations_controller.rb
+++ b/app/controllers/oauth/authorizations_controller.rb
@@ -7,6 +7,7 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
if pre_auth.authorizable?
if skip_authorization? || matching_token?
auth = authorization.authorize
+ session.delete(:user_return_to)
redirect_to auth.redirect_uri
else
render "doorkeeper/authorizations/new"
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 32fca6b838e..c5fa756d02b 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -11,15 +11,16 @@ class ProfilesController < Profiles::ApplicationController
def update
user_params.except!(:email) if @user.ldap_user?
- if @user.update_attributes(user_params)
- flash[:notice] = "Profile was successfully updated"
- else
- messages = @user.errors.full_messages.uniq.join('. ')
- flash[:alert] = "Failed to update profile. #{messages}"
- end
-
respond_to do |format|
- format.html { redirect_back_or_default(default: { action: 'show' }) }
+ if @user.update_attributes(user_params)
+ message = "Profile was successfully updated"
+ format.html { redirect_back_or_default(default: { action: 'show' }, options: { notice: message }) }
+ format.json { render json: { message: message } }
+ else
+ message = @user.errors.full_messages.uniq.join('. ')
+ format.html { redirect_back_or_default(default: { action: 'show' }, options: { alert: "Failed to update profile. #{message}" }) }
+ format.json { render json: { message: message }, status: :unprocessable_entity }
+ end
end
end
@@ -34,8 +35,7 @@ class ProfilesController < Profiles::ApplicationController
def audit_log
@events = AuditEvent.where(entity_type: "User", entity_id: current_user.id).
order("created_at DESC").
- page(params[:page]).
- per(PER_PAGE)
+ page(params[:page])
end
def update_username
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index a326bc58215..657ee94cfd7 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -1,20 +1,74 @@
class Projects::ApplicationController < ApplicationController
+ skip_before_action :authenticate_user!
before_action :project
before_action :repository
layout 'project'
- def authenticate_user!
- # Restrict access to Projects area only
- # for non-signed users
- if !current_user
+ helper_method :repository, :can_collaborate_with_project?
+
+ private
+
+ def project
+ unless @project
+ namespace = params[:namespace_id]
id = params[:project_id] || params[:id]
- project_with_namespace = "#{params[:namespace_id]}/#{id}"
- @project = Project.find_with_namespace(project_with_namespace)
- return if @project && @project.public?
+ # Redirect from
+ # localhost/group/project.git
+ # to
+ # localhost/group/project
+ #
+ if id =~ /\.git\Z/
+ redirect_to request.original_url.gsub(/\.git\/?\Z/, '')
+ return
+ end
+
+ project_path = "#{namespace}/#{id}"
+ @project = Project.find_with_namespace(project_path)
+
+ if @project && can?(current_user, :read_project, @project)
+ if @project.path_with_namespace != project_path
+ redirect_to request.original_url.gsub(project_path, @project.path_with_namespace)
+ end
+ else
+ @project = nil
+
+ if current_user.nil?
+ authenticate_user!
+ else
+ render_404
+ end
+ end
+ end
+
+ @project
+ end
+
+ def repository
+ @repository ||= project.repository
+ end
+
+ def can_collaborate_with_project?(project = nil)
+ project ||= @project
+
+ can?(current_user, :push_code, project) ||
+ (current_user && current_user.already_forked?(project))
+ end
+
+ def authorize_project!(action)
+ return access_denied! unless can?(current_user, action, project)
+ end
+
+ def method_missing(method_sym, *arguments, &block)
+ if method_sym.to_s =~ /\Aauthorize_(.*)!\z/
+ authorize_project!($1.to_sym)
+ else
+ super
end
+ end
- super
+ def require_non_empty_project
+ redirect_to namespace_project_path(@project.namespace, @project) if @project.empty_repo?
end
def require_branch_head
@@ -26,8 +80,6 @@ class Projects::ApplicationController < ApplicationController
end
end
- private
-
def apply_diff_view_cookie!
view = params[:view] || cookies[:diff_view]
cookies.permanent[:diff_view] = params[:view] = view if view
diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb
index a6bebc46b06..72921b3aa14 100644
--- a/app/controllers/projects/avatars_controller.rb
+++ b/app/controllers/projects/avatars_controller.rb
@@ -1,7 +1,7 @@
class Projects::AvatarsController < Projects::ApplicationController
include BlobHelper
- before_action :project
+ before_action :authorize_admin_project!, only: [:destroy]
def show
@blob = @repository.blob_at_branch('master', @project.avatar_in_git)
diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb
index 6ff47c4033a..6d4d4360988 100644
--- a/app/controllers/projects/badges_controller.rb
+++ b/app/controllers/projects/badges_controller.rb
@@ -2,11 +2,12 @@ class Projects::BadgesController < Projects::ApplicationController
before_action :no_cache_headers
def build
+ badge = Gitlab::Badge::Build.new(project, params[:ref])
+
respond_to do |format|
format.html { render_404 }
format.svg do
- image = Ci::ImageForBuildService.new.execute(project, ref: params[:ref])
- send_file(image.path, filename: image.name, disposition: 'inline', type: 'image/svg+xml')
+ send_data(badge.data, type: badge.type, disposition: 'inline')
end
end
end
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 43ea717cbd2..c0a53734921 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -8,7 +8,7 @@ class Projects::BranchesController < Projects::ApplicationController
def index
@sort = params[:sort] || 'name'
@branches = @repository.branches_sorted_by(@sort)
- @branches = Kaminari.paginate_array(@branches).page(params[:page]).per(PER_PAGE)
+ @branches = Kaminari.paginate_array(@branches).page(params[:page])
@max_commits = @branches.reduce(0) do |memo, branch|
diverging_commit_counts = repository.diverging_commit_counts(branch)
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index a1b8632df98..ade01c706a7 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -15,7 +15,7 @@ class Projects::ForksController < Projects::ApplicationController
@sort = params[:sort] || 'id_desc'
@forks = @forks.search(params[:filter_projects]) if params[:filter_projects].present?
- @forks = @forks.order_by(@sort).page(params[:page]).per(PER_PAGE)
+ @forks = @forks.order_by(@sort).page(params[:page])
respond_to do |format|
format.html
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index aa7a178dcf4..6d649e72f84 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -1,11 +1,12 @@
class Projects::IssuesController < Projects::ApplicationController
include ToggleSubscriptionAction
+ include IssuableActions
before_action :module_enabled
before_action :issue, only: [:edit, :update, :show]
# Allow read any issue
- before_action :authorize_read_issue!
+ before_action :authorize_read_issue!, only: [:show]
# Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create]
@@ -33,7 +34,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
- @issues = @issues.page(params[:page]).per(PER_PAGE)
+ @issues = @issues.page(params[:page])
@label = @project.labels.find_by(title: params[:label_name])
respond_to do |format|
@@ -67,7 +68,13 @@ class Projects::IssuesController < Projects::ApplicationController
@merge_requests = @issue.referenced_merge_requests(current_user)
@related_branches = @issue.related_branches - @merge_requests.map(&:source_branch)
- respond_with(@issue)
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: @issue.to_json(include: [:milestone, :labels])
+ end
+ end
+
end
def create
@@ -90,6 +97,12 @@ class Projects::IssuesController < Projects::ApplicationController
def update
@issue = Issues::UpdateService.new(project, current_user, issue_params).execute(issue)
+ if params[:move_to_project_id].to_i > 0
+ new_project = Project.find(params[:move_to_project_id])
+ move_service = Issues::MoveService.new(project, current_user)
+ @issue = move_service.execute(@issue, new_project)
+ end
+
respond_to do |format|
format.js
format.html do
@@ -100,10 +113,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
format.json do
- render json: {
- saved: @issue.valid?,
- assignee_avatar_url: @issue.assignee.try(:avatar_url)
- }
+ render json: @issue.to_json(include: [:milestone, :labels, assignee: { methods: :avatar_url }])
end
end
end
@@ -127,6 +137,11 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
alias_method :subscribable_resource, :issue
+ alias_method :issuable, :issue
+
+ def authorize_read_issue!
+ return render_404 unless can?(current_user, :read_issue, @issue)
+ end
def authorize_update_issue!
return render_404 unless can?(current_user, :update_issue, @issue)
@@ -158,7 +173,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue_params
params.require(:issue).permit(
- :title, :assignee_id, :position, :description,
+ :title, :assignee_id, :position, :description, :confidential,
:milestone_id, :state_event, :task_num, label_ids: []
)
end
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 40d8098690a..ff771ea6d9c 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -11,7 +11,14 @@ class Projects::LabelsController < Projects::ApplicationController
respond_to :js, :html
def index
- @labels = @project.labels.page(params[:page]).per(PER_PAGE)
+ @labels = @project.labels.page(params[:page])
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: @project.labels
+ end
+ end
end
def new
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 728d743045f..49064f5d505 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -1,11 +1,12 @@
class Projects::MergeRequestsController < Projects::ApplicationController
include ToggleSubscriptionAction
include DiffHelper
+ include IssuableActions
before_action :module_enabled
before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check,
- :ci_status, :cancel_merge_when_build_succeeds
+ :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip
]
before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds]
@@ -20,7 +21,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :authorize_create_merge_request!, only: [:new, :create]
# Allow modify merge_request
- before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :sort]
+ before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort]
def index
terms = params['issue_search']
@@ -34,7 +35,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
- @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE)
+ @merge_requests = @merge_requests.page(params[:page])
@merge_requests = @merge_requests.preload(:target_project)
@label = @project.labels.find_by(title: params[:label_name])
@@ -56,8 +57,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
respond_to do |format|
format.html
format.json { render json: @merge_request }
- format.diff { render text: @merge_request.to_diff(current_user) }
- format.patch { render text: @merge_request.to_patch(current_user) }
+ format.diff { render text: @merge_request.to_diff }
+ format.patch { render text: @merge_request.to_patch }
end
end
@@ -153,10 +154,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.target_project, @merge_request])
end
format.json do
- render json: {
- saved: @merge_request.valid?,
- assignee_avatar_url: @merge_request.assignee.try(:avatar_url)
- }
+ render json: @merge_request.to_json(include: [:milestone, :labels, assignee: { methods: :avatar_url }])
end
end
else
@@ -164,6 +162,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
+ def remove_wip
+ MergeRequests::UpdateService.new(project, current_user, title: @merge_request.wipless_title).execute(@merge_request)
+
+ redirect_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request),
+ notice: "The merge request can now be merged."
+ end
+
def merge_check
@merge_request.check_if_can_be_merged
@@ -256,6 +261,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request ||= @project.merge_requests.find_by!(iid: params[:id])
end
alias_method :subscribable_resource, :merge_request
+ alias_method :issuable, :merge_request
def closes_issues
@closes_issues ||= @merge_request.closes_issues
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index da46731d945..f7b6d137bde 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -19,7 +19,14 @@ class Projects::MilestonesController < Projects::ApplicationController
end
@milestones = @milestones.includes(:project)
- @milestones = @milestones.page(params[:page]).per(PER_PAGE)
+ respond_to do |format|
+ format.html do
+ @milestones = @milestones.page(params[:page])
+ end
+ format.json do
+ render json: @milestones.to_json(methods: :name)
+ end
+ end
end
def new
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 92b0caa2efb..6d2901a24a4 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -3,7 +3,7 @@ class Projects::SnippetsController < Projects::ApplicationController
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow read any snippet
- before_action :authorize_read_project_snippet!
+ before_action :authorize_read_project_snippet!, except: [:new, :create, :index]
# Allow write(create) snippet
before_action :authorize_create_project_snippet!, only: [:new, :create]
@@ -21,7 +21,7 @@ class Projects::SnippetsController < Projects::ApplicationController
filter: :by_project,
project: @project
})
- @snippets = @snippets.page(params[:page]).per(PER_PAGE)
+ @snippets = @snippets.page(params[:page])
end
def new
@@ -81,6 +81,10 @@ class Projects::SnippetsController < Projects::ApplicationController
@snippet ||= @project.snippets.find(params[:id])
end
+ def authorize_read_project_snippet!
+ return render_404 unless can?(current_user, :read_project_snippet, @snippet)
+ end
+
def authorize_update_project_snippet!
return render_404 unless can?(current_user, :update_project_snippet, @snippet)
end
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index e580487a2c6..46b242aa5ff 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -7,7 +7,7 @@ class Projects::TagsController < Projects::ApplicationController
def index
sorted = VersionSorter.rsort(@repository.tag_names)
- @tags = Kaminari.paginate_array(sorted).page(params[:page]).per(PER_PAGE)
+ @tags = Kaminari.paginate_array(sorted).page(params[:page])
@releases = project.releases.where(tag: @tags)
end
diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb
index e1fe7ea2114..caed064dfbc 100644
--- a/app/controllers/projects/uploads_controller.rb
+++ b/app/controllers/projects/uploads_controller.rb
@@ -1,7 +1,9 @@
class Projects::UploadsController < Projects::ApplicationController
- skip_before_action :authenticate_user!, :reject_blocked!, :project,
+ skip_before_action :reject_blocked!, :project,
:repository, if: -> { action_name == 'show' && image? }
+ before_action :authorize_upload_file!, only: [:create]
+
def create
link_to_file = ::Projects::UploadService.new(project, params[:file]).
execute
@@ -26,6 +28,8 @@ class Projects::UploadsController < Projects::ApplicationController
send_file uploader.file.path, disposition: disposition
end
+ private
+
def uploader
return @uploader if defined?(@uploader)
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 88fccfed509..02ceb8f4334 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -7,7 +7,7 @@ class Projects::WikisController < Projects::ApplicationController
before_action :load_project_wiki
def pages
- @wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page]).per(PER_PAGE)
+ @wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page])
end
def show
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 36f37221c58..62f53664db3 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -1,7 +1,7 @@
-class ProjectsController < ApplicationController
+class ProjectsController < Projects::ApplicationController
include ExtractsPath
- skip_before_action :authenticate_user!, only: [:show, :activity]
+ before_action :authenticate_user!, except: [:show, :activity]
before_action :project, except: [:new, :create]
before_action :repository, except: [:new, :create]
before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists?
@@ -134,11 +134,11 @@ class ProjectsController < ApplicationController
def autocomplete_sources
note_type = params['type']
note_id = params['type_id']
- autocomplete = ::Projects::AutocompleteService.new(@project)
+ autocomplete = ::Projects::AutocompleteService.new(@project, current_user)
participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id)
@suggestions = {
- emojis: autocomplete_emojis,
+ emojis: AwardEmoji.urls,
issues: autocomplete.issues,
mergerequests: autocomplete.merge_requests,
members: participants
@@ -235,17 +235,6 @@ class ProjectsController < ApplicationController
)
end
- def autocomplete_emojis
- Rails.cache.fetch("autocomplete-emoji-#{Gemojione::VERSION}") do
- Emoji.emojis.map do |name, emoji|
- {
- name: name,
- path: view_context.image_url("#{emoji["unicode"]}.png")
- }
- end
- end
- end
-
def repo_exists?
project.repository_exists? && !project.empty_repo?
end
diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb
index ad04c646e1b..627be74a38f 100644
--- a/app/controllers/root_controller.rb
+++ b/app/controllers/root_controller.rb
@@ -26,6 +26,10 @@ class RootController < Dashboard::ProjectsController
redirect_to activity_dashboard_path
when 'starred_project_activity'
redirect_to activity_dashboard_path(filter: 'starred')
+ when 'groups'
+ redirect_to dashboard_groups_path
+ when 'todos'
+ redirect_to dashboard_todos_path
else
return
end
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index c72df73af46..2daceed039b 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -25,7 +25,7 @@ class SnippetsController < ApplicationController
filter: :by_user,
user: @user,
scope: params[:scope] }).
- page(params[:page]).per(PER_PAGE)
+ page(params[:page])
render 'index'
else
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index e10c633690f..8e7956da48f 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -100,7 +100,7 @@ class UsersController < ApplicationController
def load_projects
@projects =
PersonalProjectsFinder.new(@user).execute(current_user)
- .page(params[:page]).per(PER_PAGE)
+ .page(params[:page])
end
def load_contributed_projects
@@ -108,7 +108,7 @@ class UsersController < ApplicationController
end
def load_groups
- @groups = @user.groups.order_id_desc
+ @groups = JoinedGroupsFinder.new(@user).execute(current_user)
end
def projects_for_current_user
diff --git a/app/finders/contributed_projects_finder.rb b/app/finders/contributed_projects_finder.rb
index 0209649b017..a685719555c 100644
--- a/app/finders/contributed_projects_finder.rb
+++ b/app/finders/contributed_projects_finder.rb
@@ -1,4 +1,4 @@
-class ContributedProjectsFinder
+class ContributedProjectsFinder < UnionFinder
def initialize(user)
@user = user
end
@@ -11,27 +11,19 @@ class ContributedProjectsFinder
#
# Returns an ActiveRecord::Relation.
def execute(current_user = nil)
- if current_user
- relation = projects_visible_to_user(current_user)
- else
- relation = public_projects
- end
+ segments = all_projects(current_user)
- relation.includes(:namespace).order_id_desc
+ find_union(segments, Project).includes(:namespace).order_id_desc
end
private
- def projects_visible_to_user(current_user)
- authorized = @user.contributed_projects.visible_to_user(current_user)
+ def all_projects(current_user)
+ projects = []
- union = Gitlab::SQL::Union.
- new([authorized.select(:id), public_projects.select(:id)])
+ projects << @user.contributed_projects.visible_to_user(current_user) if current_user
+ projects << @user.contributed_projects.public_to_user(current_user)
- Project.where("projects.id IN (#{union.to_sql})")
- end
-
- def public_projects
- @user.contributed_projects.public_only
+ projects
end
end
diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb
new file mode 100644
index 00000000000..3b9a421b118
--- /dev/null
+++ b/app/finders/group_projects_finder.rb
@@ -0,0 +1,42 @@
+class GroupProjectsFinder < UnionFinder
+ def initialize(group, options = {})
+ @group = group
+ @options = options
+ end
+
+ def execute(current_user = nil)
+ segments = group_projects(current_user)
+ find_union(segments, Project)
+ end
+
+ private
+
+ def group_projects(current_user)
+ only_owned = @options.fetch(:only_owned, false)
+ only_shared = @options.fetch(:only_shared, false)
+
+ projects = []
+
+ if current_user
+ if @group.users.include?(current_user)
+ projects << @group.projects unless only_shared
+ projects << @group.shared_projects unless only_owned
+ else
+ unless only_shared
+ projects << @group.projects.visible_to_user(current_user)
+ projects << @group.projects.public_to_user(current_user)
+ end
+
+ unless only_owned
+ projects << @group.shared_projects.visible_to_user(current_user)
+ projects << @group.shared_projects.public_to_user(current_user)
+ end
+ end
+ else
+ projects << @group.projects.public_only unless only_shared
+ projects << @group.shared_projects.public_only unless only_owned
+ end
+
+ projects
+ end
+end
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
new file mode 100644
index 00000000000..4e43f42e9e1
--- /dev/null
+++ b/app/finders/groups_finder.rb
@@ -0,0 +1,18 @@
+class GroupsFinder < UnionFinder
+ def execute(current_user = nil)
+ segments = all_groups(current_user)
+
+ find_union(segments, Group).order_id_desc
+ end
+
+ private
+
+ def all_groups(current_user)
+ groups = []
+
+ groups << current_user.authorized_groups if current_user
+ groups << Group.unscoped.public_to_user(current_user)
+
+ groups
+ end
+end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 19e8c7a92be..f1df6832bf6 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -80,9 +80,10 @@ class IssuableFinder
@projects = project
elsif current_user && params[:authorized_only].presence && !current_user_related?
@projects = current_user.authorized_projects.reorder(nil)
+ elsif group
+ @projects = GroupProjectsFinder.new(group).execute(current_user).reorder(nil)
else
- @projects = ProjectsFinder.new.execute(current_user, group: group).
- reorder(nil)
+ @projects = ProjectsFinder.new.execute(current_user).reorder(nil)
end
end
@@ -171,14 +172,12 @@ class IssuableFinder
def by_scope(items)
case params[:scope]
- when 'created-by-me', 'authored' then
+ when 'created-by-me', 'authored'
items.where(author_id: current_user.id)
- when 'all' then
- items
- when 'assigned-to-me' then
+ when 'assigned-to-me'
items.where(assignee_id: current_user.id)
else
- raise 'You must specify default scope'
+ items
end
end
@@ -198,8 +197,7 @@ class IssuableFinder
end
def by_group(items)
- items = items.of_group(group) if group
-
+ # Selection by group is already covered by `by_project` and `projects`
items
end
@@ -245,7 +243,7 @@ class IssuableFinder
end
def filter_by_upcoming_milestone?
- params[:milestone_title] == '#upcoming'
+ params[:milestone_title] == Milestone::Upcoming.name
end
def by_milestone(items)
@@ -254,7 +252,7 @@ class IssuableFinder
items = items.where(milestone_id: [-1, nil])
elsif filter_by_upcoming_milestone?
upcoming = Milestone.where(project_id: projects).upcoming
- items = items.joins(:milestone).where(milestones: { title: upcoming.title })
+ items = items.joins(:milestone).where(milestones: { title: upcoming.try(:title) })
else
items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] })
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index 20a2b0ce8f0..c2befa5a5b3 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -19,4 +19,10 @@ class IssuesFinder < IssuableFinder
def klass
Issue
end
+
+ private
+
+ def init_collection
+ Issue.visible_to_user(current_user)
+ end
end
diff --git a/app/finders/joined_groups_finder.rb b/app/finders/joined_groups_finder.rb
new file mode 100644
index 00000000000..47174980258
--- /dev/null
+++ b/app/finders/joined_groups_finder.rb
@@ -0,0 +1,24 @@
+class JoinedGroupsFinder < UnionFinder
+ def initialize(user)
+ @user = user
+ end
+
+ # Finds the groups of the source user, optionally limited to those visible to
+ # the current user.
+ def execute(current_user = nil)
+ segments = all_groups(current_user)
+
+ find_union(segments, Group).order_id_desc
+ end
+
+ private
+
+ def all_groups(current_user)
+ groups = []
+
+ groups << @user.authorized_groups.visible_to_user(current_user) if current_user
+ groups << @user.authorized_groups.public_to_user(current_user)
+
+ groups
+ end
+end
diff --git a/app/finders/personal_projects_finder.rb b/app/finders/personal_projects_finder.rb
index a61ffa22990..3ad4bd5f066 100644
--- a/app/finders/personal_projects_finder.rb
+++ b/app/finders/personal_projects_finder.rb
@@ -1,4 +1,4 @@
-class PersonalProjectsFinder
+class PersonalProjectsFinder < UnionFinder
def initialize(user)
@user = user
end
@@ -11,31 +11,19 @@ class PersonalProjectsFinder
#
# Returns an ActiveRecord::Relation.
def execute(current_user = nil)
- if current_user
- relation = projects_visible_to_user(current_user)
- else
- relation = public_projects
- end
+ segments = all_projects(current_user)
- relation.includes(:namespace).order_id_desc
+ find_union(segments, Project).includes(:namespace).order_id_desc
end
private
- def projects_visible_to_user(current_user)
- authorized = @user.personal_projects.visible_to_user(current_user)
+ def all_projects(current_user)
+ projects = []
- union = Gitlab::SQL::Union.
- new([authorized.select(:id), public_and_internal_projects.select(:id)])
+ projects << @user.personal_projects.visible_to_user(current_user) if current_user
+ projects << @user.personal_projects.public_to_user(current_user)
- Project.where("projects.id IN (#{union.to_sql})")
- end
-
- def public_projects
- @user.personal_projects.public_only
- end
-
- def public_and_internal_projects
- @user.personal_projects.public_and_internal_only
+ projects
end
end
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 3a5fc5b5907..2f0a9659d15 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -1,81 +1,18 @@
-class ProjectsFinder
- # Returns all projects, optionally including group projects a user has access
- # to.
- #
- # ## Examples
- #
- # Retrieving all public projects:
- #
- # ProjectsFinder.new.execute
- #
- # Retrieving all public/internal projects and those the given user has access
- # to:
- #
- # ProjectsFinder.new.execute(some_user)
- #
- # Retrieving all public/internal projects as well as the group's projects the
- # user has access to:
- #
- # ProjectsFinder.new.execute(some_user, group: some_group)
- #
- # Returns an ActiveRecord::Relation.
+class ProjectsFinder < UnionFinder
def execute(current_user = nil, options = {})
- group = options[:group]
+ segments = all_projects(current_user)
- if group
- segments = group_projects(current_user, group)
- else
- segments = all_projects(current_user)
- end
-
- if segments.length > 1
- union = Gitlab::SQL::Union.new(segments.map { |s| s.select(:id) })
-
- Project.where("projects.id IN (#{union.to_sql})")
- else
- segments.first
- end
+ find_union(segments, Project)
end
private
- def group_projects(current_user, group)
- return [group.projects.public_only] unless current_user
-
- user_group_projects = [
- group_projects_for_user(current_user, group),
- group.shared_projects.visible_to_user(current_user)
- ]
- if current_user.external?
- user_group_projects << group.projects.public_only
- else
- user_group_projects << group.projects.public_and_internal_only
- end
- end
-
def all_projects(current_user)
- return [public_projects] unless current_user
+ projects = []
- if current_user.external?
- [current_user.authorized_projects, public_projects]
- else
- [current_user.authorized_projects, public_and_internal_projects]
- end
- end
-
- def group_projects_for_user(current_user, group)
- if group.users.include?(current_user)
- group.projects
- else
- group.projects.visible_to_user(current_user)
- end
- end
-
- def public_projects
- Project.unscoped.public_only
- end
+ projects << current_user.authorized_projects if current_user
+ projects << Project.unscoped.public_to_user(current_user)
- def public_and_internal_projects
- Project.unscoped.public_and_internal_only
+ projects
end
end
diff --git a/app/finders/union_finder.rb b/app/finders/union_finder.rb
new file mode 100644
index 00000000000..33cd1a491f3
--- /dev/null
+++ b/app/finders/union_finder.rb
@@ -0,0 +1,11 @@
+class UnionFinder
+ def find_union(segments, klass)
+ if segments.length > 1
+ union = Gitlab::SQL::Union.new(segments.map { |s| s.select(:id) })
+
+ klass.where("#{klass.table_name}.id IN (#{union.to_sql})")
+ else
+ segments.first
+ end
+ end
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 883c2871746..e6ceb213532 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -301,7 +301,7 @@ module ApplicationHelper
if project.nil?
nil
elsif current_controller?(:issues)
- project.issues.send(entity).count
+ project.issues.visible_to_user(current_user).send(entity).count
elsif current_controller?(:merge_requests)
project.merge_requests.send(entity).count
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 23693629a4c..60a0ff32c9c 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -3,10 +3,6 @@ module ApplicationSettingsHelper
current_application_settings.gravatar_enabled?
end
- def twitter_sharing_enabled?
- current_application_settings.twitter_sharing_enabled?
- end
-
def signup_enabled?
current_application_settings.signup_enabled?
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 0f77b3b299a..820d69c230b 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -27,7 +27,7 @@ module BlobHelper
link_opts)
if !on_top_of_branch?(project, ref)
- button_tag "Edit", class: "btn btn-default disabled has_tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
+ button_tag "Edit", class: "btn btn-default disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
elsif can_edit_blob?(blob, project, ref)
link_to "Edit", edit_path, class: 'btn'
elsif can?(current_user, :fork_project, project)
@@ -50,9 +50,9 @@ module BlobHelper
return unless blob
if !on_top_of_branch?(project, ref)
- button_tag label, class: "btn btn-#{btn_class} disabled has_tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
+ button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
elsif blob.lfs_pointer?
- button_tag label, class: "btn btn-#{btn_class} disabled has_tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
+ button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
elsif can_edit_blob?(blob, project, ref)
button_tag label, class: "btn btn-#{btn_class}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
elsif can?(current_user, :fork_project, project)
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index d6c05843743..a9047ede8c5 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -23,36 +23,34 @@ module ButtonHelper
end
def http_clone_button(project)
- klass = 'btn js-protocol-switch'
- klass << ' active' if default_clone_protocol == 'http'
- klass << ' has_tooltip' if current_user.try(:require_password?)
+ klass = 'http-selector'
+ klass << ' has-tooltip' if current_user.try(:require_password?)
protocol = gitlab_config.protocol.upcase
- content_tag :button, protocol,
+ content_tag :a, protocol,
class: klass,
+ href: @project.http_url_to_repo,
data: {
- clone: project.http_url_to_repo,
+ html: true,
+ placement: 'right',
container: 'body',
- html: 'true',
title: "Set a password on your account<br>to pull or push via #{protocol}"
- },
- type: :button
+ }
end
def ssh_clone_button(project)
- klass = 'btn js-protocol-switch'
- klass << ' active' if default_clone_protocol == 'ssh'
- klass << ' has_tooltip' if current_user.try(:require_ssh_key?)
+ klass = 'ssh-selector'
+ klass << ' has-tooltip' if current_user.try(:require_ssh_key?)
- content_tag :button, 'SSH',
+ content_tag :a, 'SSH',
class: klass,
+ href: project.ssh_url_to_repo,
data: {
- clone: project.ssh_url_to_repo,
+ html: true,
+ placement: 'right',
container: 'body',
- html: 'true',
title: 'Add an SSH key to your profile<br>to pull or push via SSH.'
- },
- type: :button
+ }
end
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index f994c9e6170..bde0799f3de 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -182,7 +182,7 @@ module CommitsHelper
end
options = {
- class: "commit-#{options[:source]}-link has_tooltip",
+ class: "commit-#{options[:source]}-link has-tooltip",
data: { 'original-title'.to_sym => sanitize(source_email) }
}
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index 74f326e0b83..14697f774cc 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -24,7 +24,7 @@ module DropdownsHelper
capture(&block) if block && !options.has_key?(:footer_content)
end
- if block && options.has_key?(:footer_content)
+ if block && options[:footer_content]
output << content_tag(:div, class: "dropdown-footer") do
capture(&block)
end
@@ -60,7 +60,7 @@ module DropdownsHelper
title_output << content_tag(:span, title)
title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do
- icon('times')
+ icon('times', class: 'dropdown-menu-close-icon')
end
title_output.html_safe
@@ -70,7 +70,8 @@ module DropdownsHelper
def dropdown_filter(placeholder)
content_tag :div, class: "dropdown-input" do
filter_output = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: placeholder
- filter_output << icon('search')
+ filter_output << icon('search', class: "dropdown-input-search")
+ filter_output << icon('times', class: "dropdown-input-clear js-dropdown-input-clear", role: "button")
filter_output.html_safe
end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 37a888d9c60..a36b13a7db5 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -194,7 +194,7 @@ module EventsHelper
end
def event_to_atom(xml, event)
- if event.proper?
+ if event.visible_to_user?(current_user)
xml.entry do
event_link = event_feed_url(event)
event_title = event_feed_title(event)
@@ -214,4 +214,12 @@ module EventsHelper
end
end
end
+
+ def event_row_class(event)
+ if event.body? || event.created_project?
+ "event-block"
+ else
+ "event-inline"
+ end
+ end
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 1d36969cd62..b1f0a765bb9 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -19,6 +19,10 @@ module GroupsHelper
end
end
+ def can_change_group_visibility_level?(group)
+ can?(current_user, :change_visibility_level, group)
+ end
+
def group_icon(group)
if group.is_a?(String)
group = Group.find_by(path: group)
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 81df2094392..b14b8218d02 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -16,6 +16,16 @@ module IssuablesHelper
base_issuable_scope(issuable).where('iid > ?', issuable.iid).last
end
+ def issuable_json_path(issuable)
+ project = issuable.project
+
+ if issuable.kind_of?(MergeRequest)
+ namespace_project_merge_request_path(project.namespace, project, issuable.iid, :json)
+ else
+ namespace_project_issue_path(project.namespace, project, issuable.iid, :json)
+ end
+ end
+
def prev_issuable_for(issuable)
base_issuable_scope(issuable).where('iid < ?', issuable.iid).first
end
@@ -37,6 +47,14 @@ module IssuablesHelper
end
end
+ def milestone_dropdown_label(milestone_title, default_label = "Milestone")
+ if milestone_title == Milestone::Upcoming.name
+ milestone_title = Milestone::Upcoming.title
+ end
+
+ h(milestone_title.presence || default_label)
+ end
+
private
def sidebar_gutter_collapsed?
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index ae4ebc0854a..24b90fef4fe 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -57,6 +57,19 @@ module IssuesHelper
options_from_collection_for_select(milestones, 'id', 'title', object.milestone_id)
end
+ def project_options(issuable, current_user, ability: :read_project)
+ projects = current_user.authorized_projects
+ projects = projects.select do |project|
+ current_user.can?(ability, project)
+ end
+
+ no_project = OpenStruct.new(id: 0, name_with_namespace: 'No project')
+ projects.unshift(no_project)
+ projects.delete(issuable.project)
+
+ options_from_collection_for_select(projects, :id, :name_with_namespace)
+ end
+
def status_box_class(item)
if item.respond_to?(:expired?) && item.expired?
'status-box-expired'
@@ -98,6 +111,10 @@ module IssuesHelper
end.sort.to_sentence(last_word_connector: ', or ')
end
+ def confidential_icon(issue)
+ icon('eye-slash') if issue.confidential?
+ end
+
def emoji_icon(name, unicode = nil, aliases = [])
unicode ||= Emoji.emoji_filename(name) rescue ""
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 4455dcd0e20..3dded7c2f23 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -32,7 +32,7 @@ module LabelsHelper
# link_to_label(label) { "My Custom Label Text" }
#
# Returns a String
- def link_to_label(label, project: nil, type: :issue, &block)
+ def link_to_label(label, project: nil, type: :issue, tooltip: true, &block)
project ||= @project || label.project
link = send("namespace_project_#{type.to_s.pluralize}_path",
project.namespace,
@@ -42,7 +42,7 @@ module LabelsHelper
if block_given?
link_to link, &block
else
- link_to render_colored_label(label), link
+ link_to render_colored_label(label, tooltip: tooltip), link
end
end
@@ -50,23 +50,24 @@ module LabelsHelper
@project.labels.pluck(:title)
end
- def render_colored_label(label, label_suffix = '')
+ def render_colored_label(label, label_suffix = '', tooltip: true)
label_color = label.color || Label::DEFAULT_COLOR
text_color = text_color_for_bg(label_color)
# Intentionally not using content_tag here so that this method can be called
# by LabelReferenceFilter
- span = %(<span class="label color-label") +
- %(style="background-color: #{label_color}; color: #{text_color}">) +
+ span = %(<span class="label color-label #{"has-tooltip" if tooltip}" ) +
+ %(style="background-color: #{label_color}; color: #{text_color}" ) +
+ %(title="#{escape_once(label.description)}" data-container="body">) +
%(#{escape_once(label.name)}#{label_suffix}</span>)
span.html_safe
end
- def render_colored_cross_project_label(label)
+ def render_colored_cross_project_label(label, tooltip: true)
label_suffix = label.project.name_with_namespace
label_suffix = " <i>in #{escape_once(label_suffix)}</i>"
- render_colored_label(label, label_suffix)
+ render_colored_label(label, label_suffix, tooltip: tooltip)
end
def suggested_colors
@@ -109,19 +110,12 @@ module LabelsHelper
end
end
- def projects_labels_options
- labels =
- if @project
- @project.labels
- else
- Label.where(project_id: @projects)
- end
-
- grouped_labels = GlobalLabel.build_collection(labels)
- grouped_labels.unshift(Label::None)
- grouped_labels.unshift(Label::Any)
-
- options_from_collection_for_select(grouped_labels, 'name', 'title', params[:label_name])
+ def labels_filter_path
+ if @project
+ namespace_project_labels_path(@project.namespace, @project, :json)
+ else
+ dashboard_labels_path(:json)
+ end
end
def label_subscription_status(label)
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index e8ac8788d9d..87fc2db6901 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -38,7 +38,7 @@ module MilestonesHelper
def milestone_progress_bar(milestone)
options = {
class: 'progress-bar progress-bar-success',
- style: "width: #{milestone.percent_complete}%;"
+ style: "width: #{milestone.percent_complete(current_user)}%;"
}
content_tag :div, class: 'progress' do
@@ -46,22 +46,12 @@ module MilestonesHelper
end
end
- def projects_milestones_options
- milestones =
- if @project
- @project.milestones
- else
- Milestone.where(project_id: @projects)
- end.active
-
- epoch = DateTime.parse('1970-01-01')
- grouped_milestones = GlobalMilestone.build_collection(milestones)
- grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
- grouped_milestones.unshift(Milestone::None)
- grouped_milestones.unshift(Milestone::Any)
- grouped_milestones.unshift(Milestone::Upcoming)
-
- options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title])
+ def milestones_filter_dropdown_path
+ if @project
+ namespace_project_milestones_path(@project.namespace, @project, :json)
+ else
+ dashboard_milestones_path(:json)
+ end
end
def milestone_remaining_days(milestone)
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 53c543c28c5..698f90cb27a 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -5,8 +5,10 @@ module NotesHelper
end
def note_target_fields(note)
- hidden_field_tag(:target_type, note.noteable.class.name.underscore) +
- hidden_field_tag(:target_id, note.noteable.id)
+ if note.noteable
+ hidden_field_tag(:target_type, note.noteable.class.name.underscore) +
+ hidden_field_tag(:target_id, note.noteable.id)
+ end
end
def note_editable?(note)
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index c73cb3028ee..c3832cf5d65 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -12,7 +12,9 @@ module PreferencesHelper
projects: 'Your Projects (default)',
stars: 'Starred Projects',
project_activity: "Your Projects' Activity",
- starred_project_activity: "Starred Projects' Activity"
+ starred_project_activity: "Starred Projects' Activity",
+ groups: "Your Groups",
+ todos: "Your Todos"
}.with_indifferent_access.freeze
# Returns an Array usable by a select field for more user-friendly option text
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index b5acb80b720..4e4c6e301d5 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -26,7 +26,7 @@ module ProjectsHelper
image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar]
end
- def link_to_member(project, author, opts = {})
+ def link_to_member(project, author, opts = {}, &block)
default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" }
opts = default_opts.merge(opts)
@@ -44,13 +44,15 @@ module ProjectsHelper
author_html << content_tag(:span, sanitize(author.name), class: opts[:author_class]) if opts[:name]
end
+ author_html << capture(&block) if block
+
author_html = author_html.html_safe
if opts[:name]
link_to(author_html, user_path(author), class: "author_link #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe
else
title = opts[:title].sub(":name", sanitize(author.name))
- link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { 'original-title'.to_sym => title, container: 'body' } ).html_safe
+ link_to(author_html, user_path(author), class: "author_link has-tooltip", data: { 'original-title'.to_sym => title, container: 'body' } ).html_safe
end
end
@@ -207,7 +209,7 @@ module ProjectsHelper
def default_clone_protocol
if !current_user || current_user.require_ssh_key?
- "http"
+ gitlab_config.protocol
else
"ssh"
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 494dad0b41e..8a97a74ad73 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -1,4 +1,5 @@
module SearchHelper
+
def search_autocomplete_opts(term)
return unless current_user
@@ -23,45 +24,44 @@ module SearchHelper
# Autocomplete results for various settings pages
def default_autocomplete
[
- { label: "Profile settings", url: profile_path },
- { label: "SSH Keys", url: profile_keys_path },
- { label: "Dashboard", url: root_path },
- { label: "Admin Section", url: admin_root_path },
+ { category: "Settings", label: "Profile settings", url: profile_path },
+ { category: "Settings", label: "SSH Keys", url: profile_keys_path },
+ { category: "Settings", label: "Dashboard", url: root_path },
+ { category: "Settings", label: "Admin Section", url: admin_root_path },
]
end
# Autocomplete results for internal help pages
def help_autocomplete
[
- { label: "help: API Help", url: help_page_path("api", "README") },
- { label: "help: Markdown Help", url: help_page_path("markdown", "markdown") },
- { label: "help: Permissions Help", url: help_page_path("permissions", "permissions") },
- { label: "help: Public Access Help", url: help_page_path("public_access", "public_access") },
- { label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") },
- { label: "help: SSH Keys Help", url: help_page_path("ssh", "README") },
- { label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") },
- { label: "help: Webhooks Help", url: help_page_path("web_hooks", "web_hooks") },
- { label: "help: Workflow Help", url: help_page_path("workflow", "README") },
+ { category: "Help", label: "API Help", url: help_page_path("api", "README") },
+ { category: "Help", label: "Markdown Help", url: help_page_path("markdown", "markdown") },
+ { category: "Help", label: "Permissions Help", url: help_page_path("permissions", "permissions") },
+ { category: "Help", label: "Public Access Help", url: help_page_path("public_access", "public_access") },
+ { category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks", "README") },
+ { category: "Help", label: "SSH Keys Help", url: help_page_path("ssh", "README") },
+ { category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks", "system_hooks") },
+ { category: "Help", label: "Webhooks Help", url: help_page_path("web_hooks", "web_hooks") },
+ { category: "Help", label: "Workflow Help", url: help_page_path("workflow", "README") },
]
end
# Autocomplete results for the current project, if it's defined
def project_autocomplete
if @project && @project.repository.exists? && @project.repository.root_ref
- prefix = search_result_sanitize(@project.name_with_namespace)
ref = @ref || @project.repository.root_ref
[
- { label: "#{prefix} - Files", url: namespace_project_tree_path(@project.namespace, @project, ref) },
- { label: "#{prefix} - Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) },
- { label: "#{prefix} - Network", url: namespace_project_network_path(@project.namespace, @project, ref) },
- { label: "#{prefix} - Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) },
- { label: "#{prefix} - Issues", url: namespace_project_issues_path(@project.namespace, @project) },
- { label: "#{prefix} - Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) },
- { label: "#{prefix} - Milestones", url: namespace_project_milestones_path(@project.namespace, @project) },
- { label: "#{prefix} - Snippets", url: namespace_project_snippets_path(@project.namespace, @project) },
- { label: "#{prefix} - Members", url: namespace_project_project_members_path(@project.namespace, @project) },
- { label: "#{prefix} - Wiki", url: namespace_project_wikis_path(@project.namespace, @project) },
+ { category: "Current Project", label: "Files", url: namespace_project_tree_path(@project.namespace, @project, ref) },
+ { category: "Current Project", label: "Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) },
+ { category: "Current Project", label: "Network", url: namespace_project_network_path(@project.namespace, @project, ref) },
+ { category: "Current Project", label: "Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) },
+ { category: "Current Project", label: "Issues", url: namespace_project_issues_path(@project.namespace, @project) },
+ { category: "Current Project", label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) },
+ { category: "Current Project", label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) },
+ { category: "Current Project", label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) },
+ { category: "Current Project", label: "Members", url: namespace_project_project_members_path(@project.namespace, @project) },
+ { category: "Current Project", label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) },
]
else
[]
@@ -72,7 +72,9 @@ module SearchHelper
def groups_autocomplete(term, limit = 5)
current_user.authorized_groups.search(term).limit(limit).map do |group|
{
- label: "group: #{search_result_sanitize(group.name)}",
+ category: "Groups",
+ id: group.id,
+ label: "#{search_result_sanitize(group.name)}",
url: group_path(group)
}
end
@@ -83,7 +85,10 @@ module SearchHelper
current_user.authorized_projects.search_by_title(term).
sorted_by_stars.non_archived.limit(limit).map do |p|
{
- label: "project: #{search_result_sanitize(p.name_with_namespace)}",
+ category: "Projects",
+ id: p.id,
+ value: "#{search_result_sanitize(p.name)}",
+ label: "#{search_result_sanitize(p.name_with_namespace)}",
url: namespace_project_path(p.namespace, p)
}
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 07ddc691d85..edc5686cf08 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -16,14 +16,19 @@ module TodosHelper
def todo_target_link(todo)
target = todo.target_type.titleize.downcase
- link_to "#{target} #{todo.target.to_reference}", todo_target_path(todo), { title: h(todo.target.title) }
+ link_to "#{target} #{todo.target_reference}", todo_target_path(todo), { title: todo.target.title }
end
def todo_target_path(todo)
anchor = dom_id(todo.note) if todo.note.present?
- polymorphic_path([todo.project.namespace.becomes(Namespace),
- todo.project, todo.target], anchor: anchor)
+ if todo.for_commit?
+ namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project,
+ todo.target, anchor: anchor)
+ else
+ polymorphic_path([todo.project.namespace.becomes(Namespace),
+ todo.project, todo.target], anchor: anchor)
+ end
end
def todos_filter_params
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index 71d33b445c2..3a83ae15dd8 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -19,6 +19,8 @@ module VisibilityLevelHelper
case form_model
when Project
project_visibility_level_description(level)
+ when Group
+ group_visibility_level_description(level)
when Snippet
snippet_visibility_level_description(level, form_model)
end
@@ -35,6 +37,17 @@ module VisibilityLevelHelper
end
end
+ def group_visibility_level_description(level)
+ case level
+ when Gitlab::VisibilityLevel::PRIVATE
+ "The group and its projects can only be viewed by members."
+ when Gitlab::VisibilityLevel::INTERNAL
+ "The group and any internal projects can be viewed by any logged in user."
+ when Gitlab::VisibilityLevel::PUBLIC
+ "The group and any public projects can be viewed without any authentication."
+ end
+ end
+
def snippet_visibility_level_description(level, snippet = nil)
case level
when Gitlab::VisibilityLevel::PRIVATE
@@ -50,6 +63,23 @@ module VisibilityLevelHelper
end
end
+ def visibility_icon_description(form_model)
+ case form_model
+ when Project
+ project_visibility_icon_description(form_model.visibility_level)
+ when Group
+ group_visibility_icon_description(form_model.visibility_level)
+ end
+ end
+
+ def group_visibility_icon_description(level)
+ "#{visibility_level_label(level)} - #{group_visibility_level_description(level)}"
+ end
+
+ def project_visibility_icon_description(level)
+ "#{visibility_level_label(level)} - #{project_visibility_level_description(level)}"
+ end
+
def visibility_level_label(level)
Project.visibility_levels.key(level)
end
@@ -67,8 +97,11 @@ module VisibilityLevelHelper
current_application_settings.default_snippet_visibility
end
+ def default_group_visibility
+ current_application_settings.default_group_visibility
+ end
+
def skip_level?(form_model, level)
- form_model.is_a?(Project) &&
- !form_model.visibility_level_allowed?(level)
+ form_model.is_a?(Project) && !form_model.visibility_level_allowed?(level)
end
end
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index 5f9adb32e00..6f54c42146c 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -36,6 +36,14 @@ module Emails
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
+ def issue_moved_email(recipient, issue, new_issue, updated_by_user)
+ setup_issue_mail(issue.id, recipient.id)
+
+ @new_issue = new_issue
+ @new_project = new_issue.project
+ mail_answer_thread(issue, issue_thread_options(updated_by_user.id, recipient.id))
+ end
+
private
def setup_issue_mail(issue_id, recipient_id)
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 8cbc9eefc7b..826e5f96fa1 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -110,6 +110,10 @@ class Notify < BaseMailer
headers['Reply-To'] = address
+ fallback_reply_message_id = "<reply-#{reply_key}@#{Gitlab.config.gitlab.host}>".freeze
+ headers['References'] ||= ''
+ headers['References'] << ' ' << fallback_reply_message_id
+
@reply_by_email = true
end
diff --git a/app/models/ability.rb b/app/models/ability.rb
index ccac08b7d3f..c0bf6def7c5 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -27,6 +27,8 @@ class Ability
case true
when subject.is_a?(PersonalSnippet)
anonymous_personal_snippet_abilities(subject)
+ when subject.is_a?(ProjectSnippet)
+ anonymous_project_snippet_abilities(subject)
when subject.is_a?(CommitStatus)
anonymous_commit_status_abilities(subject)
when subject.is_a?(Project) || subject.respond_to?(:project)
@@ -49,7 +51,6 @@ class Ability
rules = [
:read_project,
:read_wiki,
- :read_issue,
:read_label,
:read_milestone,
:read_project_snippet,
@@ -63,6 +64,9 @@ class Ability
# Allow to read builds by anonymous user if guests are allowed
rules << :read_build if project.public_builds?
+ # Allow to read issues by anonymous user if issue is not confidential
+ rules << :read_issue unless subject.is_a?(Issue) && subject.confidential?
+
rules - project_disabled_features_rules(project)
else
[]
@@ -83,7 +87,7 @@ class Ability
subject.group
end
- if group && group.projects.public_only.any?
+ if group && group.public?
[:read_group]
else
[]
@@ -98,6 +102,14 @@ class Ability
end
end
+ def anonymous_project_snippet_abilities(snippet)
+ if snippet.public?
+ [:read_project_snippet]
+ else
+ []
+ end
+ end
+
def global_abilities(user)
rules = []
rules << :create_group if user.can_create_group
@@ -112,6 +124,13 @@ class Ability
# Push abilities on the users team role
rules.push(*project_team_rules(project.team, user))
+ if project.owner == user ||
+ (project.group && project.group.has_owner?(user)) ||
+ user.admin?
+
+ rules.push(*project_owner_rules)
+ end
+
if project.public? || (project.internal? && !user.external?)
rules.push(*public_project_rules)
@@ -119,14 +138,6 @@ class Ability
rules << :read_build if project.public_builds?
end
- if project.owner == user || user.admin?
- rules.push(*project_admin_rules)
- end
-
- if project.group && project.group.has_owner?(user)
- rules.push(*project_admin_rules)
- end
-
if project.archived?
rules -= project_archived_rules
end
@@ -169,7 +180,8 @@ class Ability
:read_note,
:create_project,
:create_issue,
- :create_note
+ :create_note,
+ :upload_file
]
end
@@ -226,14 +238,16 @@ class Ability
]
end
- def project_admin_rules
- @project_admin_rules ||= project_master_rules + [
+ def project_owner_rules
+ @project_owner_rules ||= project_master_rules + [
:change_namespace,
:change_visibility_level,
:rename_project,
:remove_project,
:archive_project,
- :remove_fork_project
+ :remove_fork_project,
+ :destroy_merge_request,
+ :destroy_issue
]
end
@@ -271,11 +285,9 @@ class Ability
def group_abilities(user, group)
rules = []
- if user.admin? || group.users.include?(user) || ProjectsFinder.new.execute(user, group: group).any?
- rules << :read_group
- end
+ rules << :read_group if can_read_group?(user, group)
- # Only group masters and group owners can create new projects in group
+ # Only group masters and group owners can create new projects
if group.has_master?(user) || group.has_owner?(user) || user.admin?
rules += [
:create_projects,
@@ -288,13 +300,23 @@ class Ability
rules += [
:admin_group,
:admin_namespace,
- :admin_group_member
+ :admin_group_member,
+ :change_visibility_level
]
end
rules.flatten
end
+ def can_read_group?(user, group)
+ return true if user.admin?
+ return true if group.public?
+ return true if group.internal? && !user.external?
+ return true if group.users.include?(user)
+
+ GroupProjectsFinder.new(group).execute(user).any?
+ end
+
def namespace_abilities(user, namespace)
rules = []
@@ -321,28 +343,27 @@ class Ability
end
rules += project_abilities(user, subject.project)
+ rules = filter_confidential_issues_abilities(user, subject, rules) if subject.is_a?(Issue)
rules
end
end
- [:note, :project_snippet].each do |name|
- define_method "#{name}_abilities" do |user, subject|
- rules = []
-
- if subject.author == user
- rules += [
- :"read_#{name}",
- :"update_#{name}",
- :"admin_#{name}"
- ]
- end
+ def note_abilities(user, note)
+ rules = []
- if subject.respond_to?(:project) && subject.project
- rules += project_abilities(user, subject.project)
- end
+ if note.author == user
+ rules += [
+ :read_note,
+ :update_note,
+ :admin_note
+ ]
+ end
- rules
+ if note.respond_to?(:project) && note.project
+ rules += project_abilities(user, note.project)
end
+
+ rules
end
def personal_snippet_abilities(user, snippet)
@@ -363,6 +384,24 @@ class Ability
rules
end
+ def project_snippet_abilities(user, snippet)
+ rules = []
+
+ if snippet.author == user || user.admin?
+ rules += [
+ :read_project_snippet,
+ :update_project_snippet,
+ :admin_project_snippet
+ ]
+ end
+
+ if snippet.public? || (snippet.internal? && !user.external?) || (snippet.private? && snippet.project.team.member?(user))
+ rules << :read_project_snippet
+ end
+
+ rules
+ end
+
def group_member_abilities(user, subject)
rules = []
target_user = subject.user
@@ -439,5 +478,17 @@ class Ability
:"admin_#{name}"
]
end
+
+ def filter_confidential_issues_abilities(user, issue, rules)
+ return rules if user.admin? || !issue.confidential?
+
+ unless issue.author == user || issue.assignee == user || issue.project.team.member?(user.id)
+ rules.delete(:admin_issue)
+ rules.delete(:read_issue)
+ rules.delete(:update_issue)
+ end
+
+ rules
+ end
end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 269056e0e77..052cd874733 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -12,12 +12,12 @@
# updated_at :datetime
# home_page_url :string(255)
# default_branch_protection :integer default(2)
-# twitter_sharing_enabled :boolean default(TRUE)
# restricted_visibility_levels :text
# version_check_enabled :boolean default(TRUE)
# max_attachment_size :integer default(10), not null
# default_project_visibility :integer
# default_snippet_visibility :integer
+# default_group_visibility :integer
# restricted_signup_domains :text
# user_oauth_applications :boolean default(TRUE)
# after_sign_out_path :string(255)
@@ -139,7 +139,6 @@ class ApplicationSetting < ActiveRecord::Base
default_branch_protection: Settings.gitlab['default_branch_protection'],
signup_enabled: Settings.gitlab['signup_enabled'],
signin_enabled: Settings.gitlab['signin_enabled'],
- twitter_sharing_enabled: Settings.gitlab['twitter_sharing_enabled'],
gravatar_enabled: Settings.gravatar['enabled'],
sign_in_text: Settings.extra['sign_in_text'],
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
diff --git a/app/models/commit.rb b/app/models/commit.rb
index ce0b85d50cf..d09876a07d9 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -74,14 +74,14 @@ class Commit
#
# This pattern supports cross-project references.
def self.reference_pattern
- %r{
+ @reference_pattern ||= %r{
(?:#{Project.reference_pattern}#{reference_prefix})?
(?<commit>\h{7,40})
}x
end
def self.link_reference_pattern
- super("commit", /(?<commit>\h{7,40})/)
+ @link_reference_pattern ||= super("commit", /(?<commit>\h{7,40})/)
end
def to_reference(from_project = nil)
@@ -230,7 +230,7 @@ class Commit
end
def revert_message
- %Q{Revert "#{title}"\n\n#{revert_description}}
+ %Q{Revert "#{title.strip}"\n\n#{revert_description}}
end
def reverts_commit?(commit)
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index 289dbc57287..51673897d98 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -43,14 +43,14 @@ class CommitRange
#
# This pattern supports cross-project references.
def self.reference_pattern
- %r{
+ @reference_pattern ||= %r{
(?:#{Project.reference_pattern}#{reference_prefix})?
(?<commit_range>#{STRICT_PATTERN})
}x
end
def self.link_reference_pattern
- super("compare", /(?<commit_range>#{PATTERN})/)
+ @link_reference_pattern ||= super("compare", /(?<commit_range>#{PATTERN})/)
end
# Initialize a CommitRange
diff --git a/app/models/concerns/internal_id.rb b/app/models/concerns/internal_id.rb
index 821ed54fb98..51288094ef1 100644
--- a/app/models/concerns/internal_id.rb
+++ b/app/models/concerns/internal_id.rb
@@ -7,7 +7,10 @@ module InternalId
end
def set_iid
- max_iid = project.send(self.class.name.tableize).maximum(:iid)
+ records = project.send(self.class.name.tableize)
+ records = records.with_deleted if self.paranoid?
+ max_iid = records.maximum(:iid)
+
self.iid = max_iid.to_i + 1
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 86ab84615ba..47ac22995ab 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -19,6 +19,7 @@ module Issuable
has_many :notes, as: :noteable, dependent: :destroy
has_many :label_links, as: :target, dependent: :destroy
has_many :labels, through: :label_links
+ has_many :todos, as: :target, dependent: :destroy
validates :author, presence: true
validates :title, presence: true, length: { within: 0..255 }
@@ -41,7 +42,7 @@ module Issuable
scope :join_project, -> { joins(:project) }
scope :references_project, -> { references(:project) }
- scope :non_archived, -> { join_project.merge(Project.non_archived) }
+ scope :non_archived, -> { join_project.merge(Project.non_archived.only(:where)) }
delegate :name,
:email,
@@ -58,6 +59,8 @@ module Issuable
attr_mentionable :description, cache: true
participant :author, :assignee, :notes_with_associations
strip_attributes :title
+
+ acts_as_paranoid
end
module ClassMethods
@@ -209,4 +212,13 @@ module Issuable
Taskable.get_updated_tasks(old_content: previous_changes['description'].first,
new_content: description)
end
+
+ ##
+ # Method that checks if issuable can be moved to another project.
+ #
+ # Should be overridden if issuable can be moved.
+ #
+ def can_move?(*)
+ false
+ end
end
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index d67df7c1d9c..5b8e3f654ea 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -1,18 +1,18 @@
module Milestoneish
- def closed_items_count
- issues.closed.size + merge_requests.closed_and_merged.size
+ def closed_items_count(user = nil)
+ issues_visible_to_user(user).closed.size + merge_requests.closed_and_merged.size
end
- def total_items_count
- issues.size + merge_requests.size
+ def total_items_count(user = nil)
+ issues_visible_to_user(user).size + merge_requests.size
end
- def complete?
- total_items_count == closed_items_count
+ def complete?(user = nil)
+ total_items_count(user) == closed_items_count(user)
end
- def percent_complete
- ((closed_items_count * 100) / total_items_count).abs
+ def percent_complete(user = nil)
+ ((closed_items_count(user) * 100) / total_items_count(user)).abs
rescue ZeroDivisionError
0
end
@@ -22,4 +22,8 @@ module Milestoneish
(due_date - Date.today).to_i
end
+
+ def issues_visible_to_user(user = nil)
+ issues.visible_to_user(user)
+ end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 9a0bbf50f8b..12183524b79 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -73,15 +73,17 @@ class Event < ActiveRecord::Base
end
end
- def proper?
+ def visible_to_user?(user = nil)
if push?
true
elsif membership_changed?
true
elsif created_project?
true
+ elsif issue? || issue_note?
+ Ability.abilities.allowed?(user, :read_issue, note? ? note_target : target)
else
- ((issue? || merge_request? || note?) && target) || milestone?
+ ((merge_request? || note?) && target) || milestone?
end
end
@@ -296,6 +298,10 @@ class Event < ActiveRecord::Base
target.noteable_type == "Commit"
end
+ def issue_note?
+ note? && target && target.noteable_type == "Issue"
+ end
+
def note_project_snippet?
target.noteable_type == "Snippet"
end
diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb
index 2ca79df0a29..b8585d4e577 100644
--- a/app/models/external_issue.rb
+++ b/app/models/external_issue.rb
@@ -31,7 +31,7 @@ class ExternalIssue
# Pattern used to extract `JIRA-123` issue references from text
def self.reference_pattern
- %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
+ @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
end
def to_reference(_from_project = nil)
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index 97bd79af083..da7c265a371 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -14,6 +14,7 @@ class GlobalMilestone
def initialize(title, milestones)
@title = title
+ @name = title
@milestones = milestones
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 9919ca112dc..b332601c59b 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -2,15 +2,16 @@
#
# Table name: namespaces
#
-# id :integer not null, primary key
-# name :string(255) not null
-# path :string(255) not null
-# owner_id :integer
-# created_at :datetime
-# updated_at :datetime
-# type :string(255)
-# description :string(255) default(""), not null
-# avatar :string(255)
+# id :integer not null, primary key
+# name :string(255) not null
+# path :string(255) not null
+# owner_id :integer
+# visibility_level :integer default(20), not null
+# created_at :datetime
+# updated_at :datetime
+# type :string(255)
+# description :string(255) default(""), not null
+# avatar :string(255)
#
require 'carrierwave/orm/activerecord'
@@ -18,6 +19,7 @@ require 'file_size_validator'
class Group < Namespace
include Gitlab::ConfigHelper
+ include Gitlab::VisibilityLevel
include Referable
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
@@ -27,6 +29,8 @@ class Group < Namespace
has_many :shared_projects, through: :project_group_links, source: :project
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
+ validate :visibility_level_allowed_by_projects
+
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
mount_uploader :avatar, AvatarUploader
@@ -74,6 +78,21 @@ class Group < Namespace
name
end
+ def visibility_level_field
+ visibility_level
+ end
+
+ def visibility_level_allowed_by_projects
+ allowed_by_projects = self.projects.where('visibility_level > ?', self.visibility_level).none?
+
+ unless allowed_by_projects
+ level_name = Gitlab::VisibilityLevel.level_name(visibility_level).downcase
+ self.errors.add(:visibility_level, "#{level_name} is not allowed since there are projects with higher visibility.")
+ end
+
+ allowed_by_projects
+ end
+
def avatar_url(size = nil)
if avatar.present?
[gitlab_config.url, avatar.url].join
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 2447f860c5a..e064b0f8b95 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -16,6 +16,7 @@
# state :string(255)
# iid :integer
# updated_by_id :integer
+# moved_to_id :integer
#
require 'carrierwave/orm/activerecord'
@@ -31,10 +32,9 @@ class Issue < ActiveRecord::Base
ActsAsTaggableOn.strict_case_match = true
belongs_to :project
- validates :project, presence: true
+ belongs_to :moved_to, class_name: 'Issue'
- scope :of_group,
- ->(group) { where(project_id: group.projects.select(:id).reorder(nil)) }
+ validates :project, presence: true
scope :cared, ->(user) { where(assignee_id: user) }
scope :open_for, ->(user) { opened.assigned_to(user) }
@@ -58,6 +58,13 @@ class Issue < ActiveRecord::Base
attributes
end
+ def self.visible_to_user(user)
+ return where(confidential: false) if user.blank?
+ return all if user.admin?
+
+ where('issues.confidential = 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.select(:id))
+ end
+
def self.reference_prefix
'#'
end
@@ -66,14 +73,14 @@ class Issue < ActiveRecord::Base
#
# This pattern supports cross-project references.
def self.reference_pattern
- %r{
+ @reference_pattern ||= %r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}(?<issue>\d+)
}x
end
def self.link_reference_pattern
- super("issues", /(?<issue>\d+)/)
+ @link_reference_pattern ||= super("issues", /(?<issue>\d+)/)
end
def to_reference(from_project = nil)
@@ -98,9 +105,8 @@ class Issue < ActiveRecord::Base
end
def related_branches
- return [] if self.project.empty_repo?
- self.project.repository.branch_names.select do |branch|
- branch =~ /\A#{iid}-(?!\d+-stable)/i
+ project.repository.branch_names.select do |branch|
+ branch.end_with?("-#{iid}")
end
end
@@ -131,8 +137,21 @@ class Issue < ActiveRecord::Base
end.uniq.select { |mr| mr.open? && mr.closes_issue?(self) }
end
+ def moved?
+ !moved_to.nil?
+ end
+
+ def can_move?(user, to_project = nil)
+ if to_project
+ return false unless user.can?(:admin_issue, to_project)
+ end
+
+ !moved? && persisted? &&
+ user.can?(:admin_issue, self.project)
+ end
+
def to_branch_name
- "#{iid}-#{title.parameterize}"
+ "#{title.parameterize}-#{iid}"
end
def can_be_worked_on?(current_user)
diff --git a/app/models/label.rb b/app/models/label.rb
index f7ffc0b7f36..55c01cae762 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -56,7 +56,7 @@ class Label < ActiveRecord::Base
# This pattern supports cross-project references.
#
def self.reference_pattern
- %r{
+ @reference_pattern ||= %r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}
(?:
@@ -97,12 +97,12 @@ class Label < ActiveRecord::Base
end
end
- def open_issues_count
- issues.opened.count
+ def open_issues_count(user = nil)
+ issues.visible_to_user(user).opened.count
end
- def closed_issues_count
- issues.closed.count
+ def closed_issues_count(user = nil)
+ issues.visible_to_user(user).closed.count
end
def open_merge_requests_count
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 188325045e2..45c3b0a3a66 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -131,7 +131,6 @@ class MergeRequest < ActiveRecord::Base
validate :validate_branches
validate :validate_fork
- scope :of_group, ->(group) { where("source_project_id in (:group_project_ids) OR target_project_id in (:group_project_ids)", group_project_ids: group.projects.select(:id).reorder(nil)) }
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
@@ -150,14 +149,14 @@ class MergeRequest < ActiveRecord::Base
#
# This pattern supports cross-project references.
def self.reference_pattern
- %r{
+ @reference_pattern ||= %r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}(?<merge_request>\d+)
}x
end
def self.link_reference_pattern
- super("merge_requests", /(?<merge_request>\d+)/)
+ @link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/)
end
# Returns all the merge requests from an ActiveRecord:Relation.
@@ -277,8 +276,14 @@ class MergeRequest < ActiveRecord::Base
self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
end
+ WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
+
def work_in_progress?
- !!(title =~ /\A\[?WIP(\]|:| )/i)
+ !!(title =~ WIP_REGEX)
+ end
+
+ def wipless_title
+ self.title.sub(WIP_REGEX, "")
end
def mergeable?
@@ -326,15 +331,15 @@ class MergeRequest < ActiveRecord::Base
# Returns the raw diff for this merge request
#
# see "git diff"
- def to_diff(current_user)
- target_project.repository.diff_text(target_branch, source_sha)
+ def to_diff
+ target_project.repository.diff_text(diff_base_commit.sha, source_sha)
end
# Returns the commit as a series of email patches.
#
# see "git format-patch"
- def to_patch(current_user)
- target_project.repository.format_patch(target_branch, source_sha)
+ def to_patch
+ target_project.repository.format_patch(diff_base_commit.sha, source_sha)
end
def hook_attrs
@@ -516,11 +521,15 @@ class MergeRequest < ActiveRecord::Base
end
def target_sha
- @target_sha ||= target_project.repository.commit(target_branch).sha
+ @target_sha ||= target_project.repository.commit(target_branch).try(:sha)
end
def source_sha
- last_commit.try(:sha)
+ last_commit.try(:sha) || source_tip.try(:sha)
+ end
+
+ def source_tip
+ source_branch && source_project.repository.commit(source_branch)
end
def fetch_ref
@@ -568,8 +577,11 @@ class MergeRequest < ActiveRecord::Base
end
def compute_diverged_commits_count
+ return 0 unless source_sha && target_sha
+
Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_sha, target_sha).size
end
+ private :compute_diverged_commits_count
def diverged_from_target_branch?
diverged_commits_count > 0
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 374590ba0c5..986184dd301 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -79,17 +79,17 @@ class Milestone < ActiveRecord::Base
end
def self.link_reference_pattern
- super("milestones", /(?<milestone>\d+)/)
+ @link_reference_pattern ||= super("milestones", /(?<milestone>\d+)/)
end
def self.upcoming
- self.where('due_date > ?', Time.now).order(due_date: :asc).first
+ self.where('due_date > ?', Time.now).reorder(due_date: :asc).first
end
def to_reference(from_project = nil)
escaped_title = self.title.gsub("]", "\\]")
- h = Gitlab::Application.routes.url_helpers
+ h = Gitlab::Routing.url_helpers
url = h.namespace_project_milestone_url(self.project.namespace, self.project, self)
"[#{escaped_title}](#{url})"
@@ -121,8 +121,8 @@ class Milestone < ActiveRecord::Base
active? && issues.opened.count.zero?
end
- def is_empty?
- total_items_count.zero?
+ def is_empty?(user = nil)
+ total_items_count(user).zero?
end
def author_id
diff --git a/app/models/note.rb b/app/models/note.rb
index b0c33f2eec5..87ced65c650 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -311,7 +311,7 @@ class Note < ActiveRecord::Base
for_merge_request? && for_diff_line?
end
- def for_project_snippet?
+ def for_snippet?
noteable_type == "Snippet"
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 412c6c6732d..c5022fd4ffc 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -73,7 +73,7 @@ class Project < ActiveRecord::Base
update_column(:last_activity_at, self.created_at)
end
- # update visibility_levet of forks
+ # update visibility_level of forks
after_update :update_forks_visibility_level
def update_forks_visibility_level
return unless visibility_level < visibility_level_was
@@ -197,6 +197,8 @@ class Project < ActiveRecord::Base
validate :avatar_type,
if: ->(project) { project.avatar.present? && project.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
+ validate :visibility_level_allowed_by_group
+ validate :visibility_level_allowed_as_fork
add_authentication_token_field :runners_token
before_save :ensure_runners_token
@@ -204,6 +206,8 @@ class Project < ActiveRecord::Base
mount_uploader :avatar, AvatarUploader
# Scopes
+ default_scope { where(pending_delete: false) }
+
scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }
scope :sorted_by_names, -> { joins(:namespace).reorder('namespaces.name ASC, projects.name ASC') }
@@ -215,8 +219,6 @@ class Project < ActiveRecord::Base
scope :in_group_namespace, -> { joins(:group) }
scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
- scope :public_only, -> { where(visibility_level: Project::PUBLIC) }
- scope :public_and_internal_only, -> { where(visibility_level: Project.public_and_internal_levels) }
scope :non_archived, -> { where(archived: false) }
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
@@ -246,10 +248,6 @@ class Project < ActiveRecord::Base
end
class << self
- def public_and_internal_levels
- [Project::PUBLIC, Project::INTERNAL]
- end
-
def abandoned
where('projects.last_activity_at < ?', 6.months.ago)
end
@@ -308,7 +306,7 @@ class Project < ActiveRecord::Base
end
def find_with_namespace(id)
- namespace_path, project_path = id.split('/')
+ namespace_path, project_path = id.split('/', 2)
return nil if !namespace_path || !project_path
@@ -435,6 +433,7 @@ class Project < ActiveRecord::Base
def safe_import_url
result = URI.parse(self.import_url)
result.password = '*****' unless result.password.nil?
+ result.user = '*****' unless result.user.nil? || result.user == "git" #tokens or other data may be saved as user
result.to_s
rescue
self.import_url
@@ -442,10 +441,25 @@ class Project < ActiveRecord::Base
def check_limit
unless creator.can_create_project? or namespace.kind == 'group'
- errors[:limit_reached] << ("Your project limit is #{creator.projects_limit} projects! Please contact your administrator to increase it")
+ self.errors.add(:limit_reached, "Your project limit is #{creator.projects_limit} projects! Please contact your administrator to increase it")
end
rescue
- errors[:base] << ("Can't check your ability to create project")
+ self.errors.add(:base, "Can't check your ability to create project")
+ end
+
+ def visibility_level_allowed_by_group
+ return if visibility_level_allowed_by_group?
+
+ level_name = Gitlab::VisibilityLevel.level_name(self.visibility_level).downcase
+ group_level_name = Gitlab::VisibilityLevel.level_name(self.group.visibility_level).downcase
+ self.errors.add(:visibility_level, "#{level_name} is not allowed in a #{group_level_name} group.")
+ end
+
+ def visibility_level_allowed_as_fork
+ return if visibility_level_allowed_as_fork?
+
+ level_name = Gitlab::VisibilityLevel.level_name(self.visibility_level).downcase
+ self.errors.add(:visibility_level, "#{level_name} is not allowed since the fork source project has lower visibility.")
end
def to_param
@@ -457,7 +471,7 @@ class Project < ActiveRecord::Base
end
def web_url
- Gitlab::Application.routes.url_helpers.namespace_project_url(self.namespace, self)
+ Gitlab::Routing.url_helpers.namespace_project_url(self.namespace, self)
end
def web_url_without_protocol
@@ -578,7 +592,7 @@ class Project < ActiveRecord::Base
if avatar.present?
[gitlab_config.url, avatar.url].join
elsif avatar_in_git
- Gitlab::Application.routes.url_helpers.namespace_project_avatar_url(namespace, self)
+ Gitlab::Routing.url_helpers.namespace_project_avatar_url(namespace, self)
end
end
@@ -876,6 +890,7 @@ class Project < ActiveRecord::Base
# Forked import is handled asynchronously
unless forked?
if gitlab_shell.add_repository(path_with_namespace)
+ repository.after_create
true
else
errors.add(:base, 'Failed to create repository via gitlab-shell')
@@ -960,9 +975,25 @@ class Project < ActiveRecord::Base
issues.opened.count
end
- def visibility_level_allowed?(level)
+ def visibility_level_allowed_as_fork?(level = self.visibility_level)
return true unless forked?
- Gitlab::VisibilityLevel.allowed_fork_levels(forked_from_project.visibility_level).include?(level.to_i)
+
+ # self.forked_from_project will be nil before the project is saved, so
+ # we need to go through the relation
+ original_project = forked_project_link.forked_from_project
+ return true unless original_project
+
+ level <= original_project.visibility_level
+ end
+
+ def visibility_level_allowed_by_group?(level = self.visibility_level)
+ return true unless group
+
+ level <= group.visibility_level
+ end
+
+ def visibility_level_allowed?(level = self.visibility_level)
+ visibility_level_allowed_as_fork?(level) && visibility_level_allowed_by_group?(level)
end
def runners_token
diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb
index 05436cd0f79..eaa5654b9c6 100644
--- a/app/models/project_services/gitlab_issue_tracker_service.rb
+++ b/app/models/project_services/gitlab_issue_tracker_service.rb
@@ -20,7 +20,7 @@
#
class GitlabIssueTrackerService < IssueTrackerService
- include Gitlab::Application.routes.url_helpers
+ include Gitlab::Routing.url_helpers
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index aba37921c09..1ed42c4f3e7 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -21,7 +21,7 @@
class JiraService < IssueTrackerService
include HTTParty
- include Gitlab::Application.routes.url_helpers
+ include Gitlab::Routing.url_helpers
DEFAULT_API_VERSION = 2
diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/slack_service/issue_message.rb
index 5af24a80609..438ff33fdff 100644
--- a/app/models/project_services/slack_service/issue_message.rb
+++ b/app/models/project_services/slack_service/issue_message.rb
@@ -22,7 +22,7 @@ class SlackService
@issue_url = obj_attr[:url]
@action = obj_attr[:action]
@state = obj_attr[:state]
- @description = obj_attr[:description]
+ @description = obj_attr[:description] || ''
end
def attachments
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 59b1b86d1fb..7c1a61bb0bf 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -123,23 +123,27 @@ class ProjectWiki
end
def repository
- Repository.new(path_with_namespace, @project)
+ @repository ||= Repository.new(path_with_namespace, @project)
end
def default_branch
wiki.class.default_ref
end
- private
-
def create_repo!
if init_repo(path_with_namespace)
- Gollum::Wiki.new(path_to_repo)
+ wiki = Gollum::Wiki.new(path_to_repo)
else
raise CouldNotCreateWikiError
end
+
+ repository.after_create
+
+ wiki
end
+ private
+
def init_repo(path_with_namespace)
gitlab_shell.add_repository(path_with_namespace)
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 036919c27b2..bf76de61148 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -42,12 +42,15 @@ class Repository
end
def exists?
- return false unless raw_repository
+ return @exists unless @exists.nil?
- raw_repository.rugged
- true
- rescue Gitlab::Git::Repository::NoRepository
- false
+ @exists = cache.fetch(:exists?) do
+ begin
+ raw_repository && raw_repository.rugged ? true : false
+ rescue Gitlab::Git::Repository::NoRepository
+ false
+ end
+ end
end
def empty?
@@ -227,12 +230,6 @@ class Repository
send(key)
end
end
-
- branches.each do |branch|
- unless cache.exist?(:"diverging_commit_counts_#{branch.name}")
- send(:diverging_commit_counts, branch)
- end
- end
end
def expire_tags_cache
@@ -301,18 +298,6 @@ class Repository
@tag_count = nil
end
- def rebuild_cache
- cache_keys.each do |key|
- cache.expire(key)
- send(key)
- end
-
- branches.each do |branch|
- cache.expire(:"diverging_commit_counts_#{branch.name}")
- diverging_commit_counts(branch)
- end
- end
-
def lookup_cache
@lookup_cache ||= {}
end
@@ -338,12 +323,25 @@ class Repository
@avatar = nil
end
+ def expire_exists_cache
+ cache.expire(:exists?)
+ @exists = nil
+ end
+
+ # Runs code after a repository has been created.
+ def after_create
+ expire_exists_cache
+ end
+
# Runs code just before a repository is deleted.
def before_delete
+ expire_exists_cache
+
expire_cache if exists?
expire_root_ref_cache
expire_emptiness_caches
+ expire_exists_cache
end
# Runs code just before the HEAD of a repository is changed.
@@ -369,6 +367,7 @@ class Repository
# Runs code after a repository has been forked/imported.
def after_import
expire_emptiness_caches
+ expire_exists_cache
end
# Runs code after a new commit has been pushed.
@@ -470,6 +469,18 @@ class Repository
end
end
+ def gitlab_ci_yml
+ return nil if !exists? || empty?
+
+ @gitlab_ci_yml ||= tree(:head).blobs.find do |file|
+ file.name == '.gitlab-ci.yml'
+ end
+ rescue Rugged::ReferenceError
+ # For unknow reason spinach scenario "Scenario: I change project path"
+ # lead to "Reference 'HEAD' not found" exception from Repository#empty?
+ nil
+ end
+
def head_commit
@head_commit ||= commit(self.root_ref)
end
@@ -809,7 +820,7 @@ class Repository
end
def fetch_ref(source_path, source_ref, target_ref)
- args = %W(#{Gitlab.config.git.bin_path} fetch -f #{source_path} #{source_ref}:#{target_ref})
+ args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
Gitlab::Popen.popen(args, path_to_repo)
end
@@ -880,6 +891,8 @@ class Repository
end
def avatar
+ return nil unless exists?
+
@avatar ||= cache.fetch(:avatar) do
AVATAR_FILES.find do |file|
blob_at_branch('master', file)
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index b9e835a4486..b96e3937281 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -56,14 +56,14 @@ class Snippet < ActiveRecord::Base
#
# This pattern supports cross-project references.
def self.reference_pattern
- %r{
+ @reference_pattern ||= %r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}(?<snippet>\d+)
}x
end
def self.link_reference_pattern
- super("snippets", /(?<snippet>\d+)/)
+ @link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/)
end
def to_reference(from_project = nil)
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 5f91991f781..d85f7bfdf57 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -5,14 +5,15 @@
# id :integer not null, primary key
# user_id :integer not null
# project_id :integer not null
-# target_id :integer not null
+# target_id :integer
# target_type :string not null
# author_id :integer
-# note_id :integer
# action :integer not null
# state :string not null
# created_at :datetime
# updated_at :datetime
+# note_id :integer
+# commit_id :string
#
class Todo < ActiveRecord::Base
@@ -27,7 +28,9 @@ class Todo < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true, allow_nil: true
- validates :action, :project, :target, :user, presence: true
+ validates :action, :project, :target_type, :user, presence: true
+ validates :target_id, presence: true, unless: :for_commit?
+ validates :commit_id, presence: true, if: :for_commit?
default_scope { reorder(id: :desc) }
@@ -36,7 +39,7 @@ class Todo < ActiveRecord::Base
state_machine :state, initial: :pending do
event :done do
- transition [:pending, :done] => :done
+ transition [:pending] => :done
end
state :pending
@@ -50,4 +53,25 @@ class Todo < ActiveRecord::Base
target.title
end
end
+
+ def for_commit?
+ target_type == "Commit"
+ end
+
+ # override to return commits, which are not active record
+ def target
+ if for_commit?
+ project.commit(commit_id) rescue nil
+ else
+ super
+ end
+ end
+
+ def target_reference
+ if for_commit?
+ target.short_id
+ else
+ target.to_reference
+ end
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index c011af03591..2b0bee2099f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -184,7 +184,7 @@ class User < ActiveRecord::Base
# User's Dashboard preference
# Note: When adding an option, it MUST go on the end of the array.
- enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity]
+ enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity, :groups, :todos]
# User's Project preference
# Note: When adding an option, it MUST go on the end of the array.
@@ -408,6 +408,8 @@ class User < ActiveRecord::Base
end
def owns_notification_email
+ return if self.temp_oauth_email?
+
self.errors.add(:notification_email, "is not an email you own") unless self.all_emails.include?(self.notification_email)
end
@@ -435,7 +437,7 @@ class User < ActiveRecord::Base
Group.where("namespaces.id IN (#{union.to_sql})")
end
- # Returns the groups a user is authorized to access.
+ # Returns projects user is authorized to access.
def authorized_projects
Project.where("projects.id IN (#{projects_union.to_sql})")
end
diff --git a/app/services/base_service.rb b/app/services/base_service.rb
index 8563633816c..0d55ba5a981 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -43,12 +43,9 @@ class BaseService
def deny_visibility_level(model, denied_visibility_level = nil)
denied_visibility_level ||= model.visibility_level
- level_name = Gitlab::VisibilityLevel.level_name(denied_visibility_level)
+ level_name = Gitlab::VisibilityLevel.level_name(denied_visibility_level).downcase
- model.errors.add(
- :visibility_level,
- "#{level_name} visibility has been restricted by your GitLab administrator"
- )
+ model.errors.add(:visibility_level, "#{level_name} has been restricted by your GitLab administrator")
end
private
diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb
index 002f7ba1278..2cd51a7610f 100644
--- a/app/services/ci/create_builds_service.rb
+++ b/app/services/ci/create_builds_service.rb
@@ -1,7 +1,7 @@
module Ci
class CreateBuildsService
def execute(commit, stage, ref, tag, user, trigger_request, status)
- builds_attrs = commit.config_processor.builds_for_stage_and_ref(stage, ref, tag)
+ builds_attrs = commit.config_processor.builds_for_stage_and_ref(stage, ref, tag, trigger_request)
# check when to create next build
builds_attrs = builds_attrs.select do |build_attrs|
diff --git a/app/services/create_snippet_service.rb b/app/services/create_snippet_service.rb
index 101a3df5eee..9884cb96661 100644
--- a/app/services/create_snippet_service.rb
+++ b/app/services/create_snippet_service.rb
@@ -6,8 +6,7 @@ class CreateSnippetService < BaseService
snippet = project.snippets.build(params)
end
- unless Gitlab::VisibilityLevel.allowed_for?(current_user,
- params[:visibility_level])
+ unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level])
deny_visibility_level(snippet)
return snippet
end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index 14e2a2c0699..c007d648dd6 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -120,7 +120,7 @@ class GitPushService < BaseService
closed_issues = commit.closes_issues(current_user)
closed_issues.each do |issue|
if can?(current_user, :update_issue, issue)
- Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit)
+ Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit: commit)
end
end
end
diff --git a/app/services/groups/base_service.rb b/app/services/groups/base_service.rb
new file mode 100644
index 00000000000..a8fa098246a
--- /dev/null
+++ b/app/services/groups/base_service.rb
@@ -0,0 +1,9 @@
+module Groups
+ class BaseService < ::BaseService
+ attr_accessor :group, :current_user, :params
+
+ def initialize(group, user, params = {})
+ @group, @current_user, @params = group, user, params.dup
+ end
+ end
+end
diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb
new file mode 100644
index 00000000000..2bccd584dde
--- /dev/null
+++ b/app/services/groups/create_service.rb
@@ -0,0 +1,21 @@
+module Groups
+ class CreateService < Groups::BaseService
+ def initialize(user, params = {})
+ @current_user, @params = user, params.dup
+ end
+
+ def execute
+ @group = Group.new(params)
+
+ unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level])
+ deny_visibility_level(@group)
+ return @group
+ end
+
+ @group.name ||= @group.path.dup
+ @group.save
+ @group.add_owner(current_user)
+ @group
+ end
+ end
+end
diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb
new file mode 100644
index 00000000000..99ad12b1003
--- /dev/null
+++ b/app/services/groups/update_service.rb
@@ -0,0 +1,20 @@
+module Groups
+ class UpdateService < Groups::BaseService
+ def execute
+ # check that user is allowed to set specified visibility_level
+ 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)
+
+ deny_visibility_level(group, new_visibility)
+ return group
+ end
+ end
+
+ group.assign_attributes(params)
+
+ group.save
+ end
+ end
+end
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index 78254b49af3..859c934ea3b 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -1,6 +1,6 @@
module Issues
class CloseService < Issues::BaseService
- def execute(issue, commit = nil)
+ def execute(issue, commit: nil, notifications: true, system_note: true)
if project.jira_tracker? && project.jira_service.active
project.jira_service.execute(commit, issue)
todo_service.close_issue(issue, current_user)
@@ -9,8 +9,8 @@ module Issues
if project.default_issues_tracker? && issue.close
event_service.close_issue(issue, current_user)
- create_note(issue, commit)
- notification_service.close_issue(issue, current_user)
+ create_note(issue, commit) if system_note
+ notification_service.close_issue(issue, current_user) if notifications
todo_service.close_issue(issue, current_user)
execute_hooks(issue, 'close')
end
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index 10787e8873c..e63e1af8766 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -4,7 +4,7 @@ module Issues
filter_params
label_params = params[:label_ids]
issue = project.issues.new(params.except(:label_ids))
- issue.author = current_user
+ issue.author = params[:author] || current_user
if issue.save
issue.update_attributes(label_ids: label_params)
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
new file mode 100644
index 00000000000..82e7090f1ea
--- /dev/null
+++ b/app/services/issues/move_service.rb
@@ -0,0 +1,101 @@
+module Issues
+ class MoveService < Issues::BaseService
+ class MoveError < StandardError; end
+
+ def execute(issue, new_project)
+ @old_issue = issue
+ @old_project = @project
+ @new_project = new_project
+
+ unless issue.can_move?(current_user, new_project)
+ raise MoveError, 'Cannot move issue due to insufficient permissions!'
+ end
+
+ if @project == new_project
+ raise MoveError, 'Cannot move issue to project it originates from!'
+ end
+
+ # Using transaction because of a high resources footprint
+ # on rewriting notes (unfolding references)
+ #
+ ActiveRecord::Base.transaction do
+ # New issue tasks
+ #
+ @new_issue = create_new_issue
+
+ rewrite_notes
+ add_note_moved_from
+
+ # Old issue tasks
+ #
+ add_note_moved_to
+ close_issue
+ mark_as_moved
+ end
+
+ notify_participants
+
+ @new_issue
+ end
+
+ private
+
+ def create_new_issue
+ new_params = { id: nil, iid: nil, label_ids: [], milestone: nil,
+ project: @new_project, author: @old_issue.author,
+ description: rewrite_content(@old_issue.description) }
+
+ new_params = @old_issue.serializable_hash.merge(new_params)
+ CreateService.new(@new_project, @current_user, new_params).execute
+ end
+
+ def rewrite_notes
+ @old_issue.notes.find_each do |note|
+ new_note = note.dup
+ new_params = { project: @new_project, noteable: @new_issue,
+ note: rewrite_content(new_note.note),
+ created_at: note.created_at,
+ updated_at: note.updated_at }
+
+ new_note.update(new_params)
+ end
+ end
+
+ def rewrite_content(content)
+ return unless content
+
+ rewriters = [Gitlab::Gfm::ReferenceRewriter,
+ Gitlab::Gfm::UploadsRewriter]
+
+ rewriters.inject(content) do |text, klass|
+ rewriter = klass.new(text, @old_project, @current_user)
+ rewriter.rewrite(@new_project)
+ end
+ end
+
+ def close_issue
+ close_service = CloseService.new(@old_project, @current_user)
+ close_service.execute(@old_issue, notifications: false, system_note: false)
+ end
+
+ def add_note_moved_from
+ SystemNoteService.noteable_moved(@new_issue, @new_project,
+ @old_issue, @current_user,
+ direction: :from)
+ end
+
+ def add_note_moved_to
+ SystemNoteService.noteable_moved(@old_issue, @old_project,
+ @new_issue, @current_user,
+ direction: :to)
+ end
+
+ def mark_as_moved
+ @old_issue.update(moved_to: @new_issue)
+ end
+
+ def notify_participants
+ notification_service.issue_moved(@old_issue, @new_issue, @current_user)
+ end
+ end
+end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 7b306a8a531..ac5b58db862 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -5,6 +5,19 @@ module MergeRequests
SystemNoteService.change_status(merge_request, merge_request.target_project, current_user, merge_request.state, nil)
end
+ def create_title_change_note(issuable, old_title)
+ removed_wip = old_title =~ MergeRequest::WIP_REGEX && !issuable.work_in_progress?
+ added_wip = old_title !~ MergeRequest::WIP_REGEX && issuable.work_in_progress?
+
+ if removed_wip
+ SystemNoteService.remove_merge_request_wip(issuable, issuable.project, current_user)
+ elsif added_wip
+ SystemNoteService.add_merge_request_wip(issuable, issuable.project, current_user)
+ else
+ super
+ end
+ end
+
def hook_data(merge_request, action)
hook_data = merge_request.to_hook_data(current_user)
merge_request_url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id)
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index fa34753c4fd..6e9152e444e 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -51,7 +51,7 @@ module MergeRequests
# be interpreted as the use wants to close that issue on this project
# Pattern example: 112-fix-mep-mep
# Will lead to appending `Closes #112` to the description
- if match = merge_request.source_branch.match(/\A(\d+)-/)
+ if match = merge_request.source_branch.match(/-(\d+)\z/)
iid = match[1]
closes_issue = "Closes ##{iid}"
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index ebb67c7db65..064910f81f7 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -22,7 +22,7 @@ module MergeRequests
closed_issues = merge_request.closes_issues(current_user)
closed_issues.each do |issue|
if can?(current_user, :update_issue, issue)
- Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request)
+ Issues::CloseService.new(project, current_user, {}).execute(issue, commit: merge_request)
end
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 19a6779dea9..eff0d96f93d 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -162,6 +162,7 @@ class NotificationService
recipients = add_subscribed_users(recipients, note.noteable)
recipients = reject_unsubscribed_users(recipients, note.noteable)
+ recipients = reject_users_without_access(recipients, note.noteable)
recipients.delete(note.author)
recipients = recipients.uniq
@@ -236,6 +237,16 @@ class NotificationService
end
end
+ def issue_moved(issue, new_issue, current_user)
+ recipients = build_recipients(issue, issue.project, current_user)
+
+ recipients.map do |recipient|
+ email = mailer.issue_moved_email(recipient, issue, new_issue, current_user)
+ email.deliver_later
+ email
+ end
+ end
+
protected
# Get project users with WATCH notification level
@@ -366,6 +377,14 @@ class NotificationService
end
end
+ def reject_users_without_access(recipients, target)
+ return recipients unless target.is_a?(Issue)
+
+ recipients.select do |user|
+ user.can?(:read_issue, target)
+ end
+ end
+
def add_subscribed_users(recipients, target)
return recipients unless target.respond_to? :subscribers
@@ -454,15 +473,16 @@ class NotificationService
end
recipients = reject_unsubscribed_users(recipients, target)
+ recipients = reject_users_without_access(recipients, target)
recipients.delete(current_user)
-
recipients.uniq
end
def build_relabeled_recipients(target, current_user, labels:)
recipients = add_labels_subscribers([], target, labels: labels)
recipients = reject_unsubscribed_users(recipients, target)
+ recipients = reject_users_without_access(recipients, target)
recipients.delete(current_user)
recipients.uniq
end
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index 7408e09ed1e..ba50305dbd5 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -1,11 +1,7 @@
module Projects
class AutocompleteService < BaseService
- def initialize(project)
- @project = project
- end
-
def issues
- @project.issues.opened.select([:iid, :title])
+ @project.issues.visible_to_user(current_user).opened.select([:iid, :title])
end
def merge_requests
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index a6820183bee..501e58c1407 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -9,10 +9,8 @@ module Projects
@project = Project.new(params)
- # Make sure that the user is allowed to use the specified visibility
- # level
- unless Gitlab::VisibilityLevel.allowed_for?(current_user,
- params[:visibility_level])
+ # Make sure that the user is allowed to use the specified visibility level
+ unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level])
deny_visibility_level(@project)
return @project
end
@@ -55,9 +53,7 @@ module Projects
@project.save
if @project.persisted? && !@project.import?
- unless @project.create_repository
- raise 'Failed to create repository'
- end
+ raise 'Failed to create repository' unless @project.create_repository
end
end
diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb
index bccd67d3dbf..a0973c5d260 100644
--- a/app/services/projects/housekeeping_service.rb
+++ b/app/services/projects/housekeeping_service.rb
@@ -24,7 +24,7 @@ module Projects
def execute
raise LeaseTaken if !try_obtain_lease
- GitlabShellWorker.perform_async(:gc, @project.path_with_namespace)
+ GitlabShellOneShotWorker.perform_async(:gc, @project.path_with_namespace)
ensure
@project.update_column(:pushes_since_gc, 0)
end
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 895e089bea3..941df08995c 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -3,16 +3,13 @@ module Projects
def execute
# check that user is allowed to set specified visibility_level
new_visibility = params[:visibility_level]
- if new_visibility
- if new_visibility.to_i != project.visibility_level
- unless can?(current_user, :change_visibility_level, project) &&
- Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
- deny_visibility_level(project, new_visibility)
- return project
- end
+ 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)
+
+ deny_visibility_level(project, new_visibility)
+ return project
end
-
- return false unless visibility_level_allowed?(new_visibility)
end
new_branch = params[:default_branch]
@@ -27,19 +24,5 @@ module Projects
end
end
end
-
- private
-
- def visibility_level_allowed?(level)
- return true if project.visibility_level_allowed?(level)
-
- level_name = Gitlab::VisibilityLevel.level_name(level)
- project.errors.add(
- :visibility_level,
- "#{level_name} could not be set as visibility level of this project - parent project settings are more restrictive"
- )
-
- false
- end
end
end
diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb
index e1e94c5cc38..aa9837038a6 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -11,7 +11,7 @@ module Search
projects = ProjectsFinder.new.execute(current_user)
projects = projects.in_namespace(group.id) if group
- Gitlab::SearchResults.new(projects, params[:search])
+ Gitlab::SearchResults.new(current_user, projects, params[:search])
end
end
end
diff --git a/app/services/search/project_service.rb b/app/services/search/project_service.rb
index c08881dce4b..4b500914cfb 100644
--- a/app/services/search/project_service.rb
+++ b/app/services/search/project_service.rb
@@ -7,7 +7,8 @@ module Search
end
def execute
- Gitlab::ProjectSearchResults.new(project,
+ Gitlab::ProjectSearchResults.new(current_user,
+ project,
params[:search],
params[:repository_ref])
end
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index ea2b26ccb52..f0615ec7420 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -95,17 +95,19 @@ class SystemHooksService
end
def project_member_data(model)
+ project = model.project || Project.unscoped.find(model.source_id)
+
{
- project_name: model.project.name,
- project_path: model.project.path,
- project_path_with_namespace: model.project.path_with_namespace,
- project_id: model.project.id,
- user_username: model.user.username,
- user_name: model.user.name,
- user_email: model.user.email,
- user_id: model.user.id,
- access_level: model.human_access,
- project_visibility: Project.visibility_levels.key(model.project.visibility_level_field).downcase
+ project_name: project.name,
+ project_path: project.path,
+ project_path_with_namespace: project.path_with_namespace,
+ project_id: project.id,
+ user_username: model.user.username,
+ user_name: model.user.name,
+ user_email: model.user.email,
+ user_id: model.user.id,
+ access_level: model.human_access,
+ project_visibility: Project.visibility_levels.key(project.visibility_level_field).downcase
}
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index f09b77c4a57..658b086496f 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -144,6 +144,18 @@ class SystemNoteService
create_note(noteable: noteable, project: project, author: author, note: body)
end
+ def self.remove_merge_request_wip(noteable, project, author)
+ body = 'Unmarked this merge request as a Work In Progress'
+
+ create_note(noteable: noteable, project: project, author: author, note: body)
+ end
+
+ def self.add_merge_request_wip(noteable, project, author)
+ body = 'Marked this merge request as a **Work In Progress**'
+
+ create_note(noteable: noteable, project: project, author: author, note: body)
+ end
+
# Called when the title of a Noteable is changed
#
# noteable - Noteable object that responds to `title`
@@ -210,9 +222,9 @@ class SystemNoteService
# Called when a branch is created from the 'new branch' button on a issue
# Example note text:
#
- # "Started branch `201-issue-branch-button`"
+ # "Started branch `issue-branch-button-201`"
def self.new_issue_branch(issue, project, author, branch)
- h = Gitlab::Application.routes.url_helpers
+ h = Gitlab::Routing.url_helpers
link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
body = "Started branch [`#{branch}`](#{link})"
@@ -399,4 +411,26 @@ class SystemNoteService
body = "Marked the task **#{new_task.source}** as #{status_label}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
+
+ # Called when noteable has been moved to another project
+ #
+ # direction - symbol, :to or :from
+ # noteable - Noteable object
+ # noteable_ref - Referenced noteable
+ # author - User performing the move
+ #
+ # Example Note text:
+ #
+ # "Moved to some_namespace/project_new#11"
+ #
+ # Returns the created Note object
+ def self.noteable_moved(noteable, project, noteable_ref, author, direction:)
+ unless [:to, :from].include?(direction)
+ raise ArgumentError, "Invalid direction `#{direction}`"
+ end
+
+ cross_reference = noteable_ref.to_reference(project)
+ body = "Moved #{direction} #{cross_reference}"
+ create_note(noteable: noteable, project: project, author: author, note: body)
+ end
end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 4392e2d17fe..42c5bca90fd 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -103,24 +103,16 @@ class TodoService
# * mark all pending todos related to the target for the current user as done
#
def mark_pending_todos_as_done(target, user)
- pending_todos(user, target.project, target).update_all(state: :done)
+ attributes = attributes_for_target(target)
+ pending_todos(user, attributes).update_all(state: :done)
end
private
- def create_todos(project, target, author, users, action, note = nil)
+ def create_todos(users, attributes)
Array(users).each do |user|
- next if pending_todos(user, project, target).exists?
-
- Todo.create(
- project: project,
- user_id: user.id,
- author_id: author.id,
- target_id: target.id,
- target_type: target.class.name,
- action: action,
- note: note
- )
+ next if pending_todos(user, attributes).exists?
+ Todo.create(attributes.merge(user_id: user.id))
end
end
@@ -130,8 +122,8 @@ class TodoService
end
def handle_note(note, author)
- # Skip system notes, notes on commit, and notes on project snippet
- return if note.system? || ['Commit', 'Snippet'].include?(note.noteable_type)
+ # Skip system notes, and notes on project snippet
+ return if note.system? || note.for_snippet?
project = note.project
target = note.noteable
@@ -142,29 +134,68 @@ class TodoService
def create_assignment_todo(issuable, author)
if issuable.assignee && issuable.assignee != author
- create_todos(issuable.project, issuable, author, issuable.assignee, Todo::ASSIGNED)
+ attributes = attributes_for_todo(issuable.project, issuable, author, Todo::ASSIGNED)
+ create_todos(issuable.assignee, attributes)
end
end
- def create_mention_todos(project, issuable, author, note = nil)
- mentioned_users = filter_mentioned_users(project, note || issuable, author)
- create_todos(project, issuable, author, mentioned_users, Todo::MENTIONED, note)
+ def create_mention_todos(project, target, author, note = nil)
+ mentioned_users = filter_mentioned_users(project, note || target, author)
+ attributes = attributes_for_todo(project, target, author, Todo::MENTIONED, note)
+ create_todos(mentioned_users, attributes)
end
- def filter_mentioned_users(project, target, author)
- mentioned_users = target.mentioned_users.select do |user|
- user.can?(:read_project, project)
+ def attributes_for_target(target)
+ attributes = {
+ project_id: target.project.id,
+ target_id: target.id,
+ target_type: target.class.name,
+ commit_id: nil
+ }
+
+ if target.is_a?(Commit)
+ attributes.merge!(target_id: nil, commit_id: target.id)
end
- mentioned_users.delete(author)
- mentioned_users.uniq
+ attributes
end
- def pending_todos(user, project, target)
- user.todos.pending.where(
+ def attributes_for_todo(project, target, author, action, note = nil)
+ attributes_for_target(target).merge!(
project_id: project.id,
- target_id: target.id,
- target_type: target.class.name
+ author_id: author.id,
+ action: action,
+ note: note
)
end
+
+ def filter_mentioned_users(project, target, author)
+ mentioned_users = target.mentioned_users
+ mentioned_users = reject_users_without_access(mentioned_users, project, target)
+ mentioned_users.delete(author)
+ mentioned_users.uniq
+ end
+
+ def reject_users_without_access(users, project, target)
+ if target.is_a?(Note) && target.for_issue?
+ target = target.noteable
+ end
+
+ if target.is_a?(Issue)
+ select_users(users, :read_issue, target)
+ else
+ select_users(users, :read_project, project)
+ end
+ end
+
+ def select_users(users, ability, subject)
+ users.select do |user|
+ user.can?(ability.to_sym, subject)
+ end
+ end
+
+ def pending_todos(user, criteria = {})
+ valid_keys = [:project_id, :target_id, :target_type, :commit_id]
+ user.todos.pending.where(criteria.slice(*valid_keys))
+ end
end
diff --git a/app/services/update_snippet_service.rb b/app/services/update_snippet_service.rb
index e9328bb7323..93af8f21972 100644
--- a/app/services/update_snippet_service.rb
+++ b/app/services/update_snippet_service.rb
@@ -9,7 +9,6 @@ class UpdateSnippetService < BaseService
def execute
# check that user is allowed to set specified visibility_level
new_visibility = params[:visibility_level]
-
if new_visibility && new_visibility.to_i != snippet.visibility_level
unless Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(snippet, new_visibility)
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 86d24469e05..1af9e9b0edb 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -1,14 +1,15 @@
# encoding: utf-8
class FileUploader < CarrierWave::Uploader::Base
include UploaderHelper
+ MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}
storage :file
attr_accessor :project, :secret
- def initialize(project, secret = self.class.generate_secret)
+ def initialize(project, secret = nil)
@project = project
- @secret = secret
+ @secret = secret || self.class.generate_secret
end
def base_dir
@@ -23,14 +24,14 @@ class FileUploader < CarrierWave::Uploader::Base
File.join(base_dir, 'tmp', @project.path_with_namespace, @secret)
end
- def self.generate_secret
- SecureRandom.hex
- end
-
def secure_url
File.join("/uploads", @secret, file.filename)
end
+ def to_markdown
+ to_h[:markdown]
+ end
+
def to_h
filename = image? ? self.file.basename : self.file.filename
escaped_filename = filename.gsub("]", "\\]")
@@ -45,4 +46,8 @@ class FileUploader < CarrierWave::Uploader::Base
markdown: markdown
}
end
+
+ def self.generate_secret
+ SecureRandom.hex
+ end
end
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index b30dfd109ea..de86dacbb12 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -19,6 +19,10 @@
= f.label :default_snippet_visibility, class: 'control-label col-sm-2'
.col-sm-10
= render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: ProjectSnippet.new)
+ .form-group.group-visibility-level-holder
+ = f.label :default_group_visibility, class: 'control-label col-sm-2'
+ .col-sm-10
+ = render('shared/visibility_radios', model_method: :default_group_visibility, form: f, selected_level: @application_setting.default_group_visibility, form_model: Group.new)
.form-group
= f.label :restricted_visibility_levels, class: 'control-label col-sm-2'
.col-sm-10
@@ -73,13 +77,6 @@
= f.check_box :gravatar_enabled
Gravatar enabled
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :twitter_sharing_enabled do
- = f.check_box :twitter_sharing_enabled, :'aria-describedby' => 'twitter_help_block'
- Twitter enabled
- %span.help-block#twitter_help_block Show users a button to share their newly created public or internal projects on twitter
- .form-group
= f.label :default_projects_limit, class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :default_projects_limit, class: 'form-control'
diff --git a/app/views/admin/applications/_delete_form.html.haml b/app/views/admin/applications/_delete_form.html.haml
index 3147cbd659f..042971e1eed 100644
--- a/app/views/admin/applications/_delete_form.html.haml
+++ b/app/views/admin/applications/_delete_form.html.haml
@@ -1,4 +1,4 @@
- submit_btn_css ||= 'btn btn-link btn-remove btn-sm'
= form_tag admin_application_path(application) do
%input{:name => "_method", :type => "hidden", :value => "delete"}/
- = submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css \ No newline at end of file
+ = submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index 198026a1f75..7f2b1cd235d 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -10,6 +10,8 @@
.col-sm-10
= render 'shared/choose_group_avatar_button', f: f
+ = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
+
- if @group.new_record?
.form-group
.col-sm-offset-2.col-sm-10
diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml
index 118d3cfea07..6bdc885a312 100644
--- a/app/views/admin/groups/index.html.haml
+++ b/app/views/admin/groups/index.html.haml
@@ -46,6 +46,9 @@
%h4
= link_to [:admin, group] do
+ %span{ class: visibility_level_color(group.visibility_level) }
+ = visibility_level_icon(group.visibility_level)
+
%i.fa.fa-folder
= group.name
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 264fa1bf0cd..f309e80a39a 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -28,6 +28,11 @@
= @group.description
%li
+ %span.light Visibility level:
+ %strong
+ = visibility_level_label(@group.visibility_level)
+
+ %li
%span.light Created on:
%strong
= @group.created_at.to_s(:medium)
diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml
index 5736a301910..f417b2e44a4 100644
--- a/app/views/admin/labels/_label.html.haml
+++ b/app/views/admin/labels/_label.html.haml
@@ -1,6 +1,6 @@
%li{id: dom_id(label)}
.label-row
- = render_colored_label(label)
+ = render_colored_label(label, tooltip: false)
= markdown(label.description, pipeline: :single_line)
.pull-right
= link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm'
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index d734e60682a..c638c32a654 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -52,7 +52,7 @@
%li
%span.light fs:
%strong
- = @repository.path_to_repo
+ = @project.repository.path_to_repo
%li
%span.light Size
diff --git a/app/views/ci/projects/index.html.haml b/app/views/ci/projects/index.html.haml
deleted file mode 100644
index 9c2290bc4a5..00000000000
--- a/app/views/ci/projects/index.html.haml
+++ /dev/null
@@ -1,20 +0,0 @@
-.wiki
- %h1
- GitLab CI is now integrated in GitLab UI
- %h2 For existing projects
-
- %p
- Check the following pages to find the CI status you're looking for:
-
- %ul
- %li Projects page - shows CI status for each project.
- %li Project commits page - show CI status for each commit.
-
-
-
- %h2 For new projects
-
- %p
- If you want to enable CI for a new project it is easy as adding
- = link_to ".gitlab-ci.yml", "http://doc.gitlab.com/ce/ci/yaml/README.html"
- file to your repository
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index dfa5f80eef8..1eec4db45a0 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -10,6 +10,8 @@
- if current_user
= link_to issues_dashboard_url(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/issuable/filter', type: :issues
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index 45cfe3da188..aa0aff86d4d 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -1,8 +1,8 @@
-%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo) }
+%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} }
.todo-item.todo-block
= image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:''
- .todo-title
+ .todo-title.title
%span.author-name
- if todo.author
= link_to_author(todo)
@@ -10,13 +10,18 @@
(removed)
%span.todo-label
= todo_action_name(todo)
- = todo_target_link(todo)
+ - if todo.target
+ = todo_target_link(todo)
+ - else
+ (removed)
&middot; #{time_ago_with_tooltip(todo.created_at)}
- if todo.pending?
.todo-actions.pull-right
- = link_to 'Done', [:dashboard, todo], method: :delete, class: 'btn'
+ = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading done-todo' do
+ Done
+ = icon('spinner spin')
.todo-body
.todo-note
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 946d7df3933..f9ec3a89158 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -3,13 +3,15 @@
.top-area
%ul.nav-links
- %li{class: ('active' if params[:state].blank? || params[:state] == 'pending')}
+ - todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending')
+ %li{class: "todos-pending #{todo_pending_active}"}
= link_to todos_filter_path(state: 'pending') do
%span
To do
%span{class: 'badge'}
= todos_pending_count
- %li{class: ('active' if params[:state] == 'done')}
+ - todo_done_active = ('active' if params[:state] == 'done')
+ %li{class: "todos-done #{todo_done_active}"}
= link_to todos_filter_path(state: 'done') do
%span
Done
@@ -18,7 +20,9 @@
.nav-controls
- if @todos.any?(&:pending?)
- = link_to 'Mark all as done', destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn', method: :delete
+ = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete do
+ Mark all as done
+ = icon('spinner spin')
.todos-filters
.gray-content-block.second-block
@@ -42,12 +46,12 @@
.prepend-top-default
- if @todos.any?
- @todos.group_by(&:project).each do |group|
- .panel.panel-default.panel-small
+ .panel.panel-default.panel-small.js-todos-list
- project = group[0]
.panel-heading
= link_to project.name_with_namespace, namespace_project_path(project.namespace, project)
- %ul.well-list.todos-list
+ %ul.content-list.todos-list
= render group[1]
= paginate @todos, theme: "gitlab"
- else
diff --git a/app/views/devise/sessions/_new_crowd.html.haml b/app/views/devise/sessions/_new_crowd.html.haml
index 4974bb7f7fb..8e81671b7e7 100644
--- a/app/views/devise/sessions/_new_crowd.html.haml
+++ b/app/views/devise/sessions/_new_crowd.html.haml
@@ -6,4 +6,4 @@
%label{for: "remember_me"}
= check_box_tag :remember_me, '1', false, id: 'remember_me'
%span Remember me
- = button_tag "Sign in", class: "btn-save btn" \ No newline at end of file
+ = button_tag "Sign in", class: "btn-save btn"
diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml
index ea0b66c932b..55f4a6f287d 100644
--- a/app/views/doorkeeper/applications/index.html.haml
+++ b/app/views/doorkeeper/applications/index.html.haml
@@ -77,7 +77,7 @@
%em Authorization was granted by entering your username and password in the application.
%td= token.created_at
%td= token.scopes
- %td= render 'delete_form', token: token
+ %td= render 'doorkeeper/authorized_applications/delete_form', token: token
- else
.profile-settings-message.text-center
You don't have any authorized applications
diff --git a/app/views/doorkeeper/applications/new.html.haml b/app/views/doorkeeper/applications/new.html.haml
index fd32a468b45..d3692d1f759 100644
--- a/app/views/doorkeeper/applications/new.html.haml
+++ b/app/views/doorkeeper/applications/new.html.haml
@@ -4,4 +4,4 @@
%hr
-= render 'form', application: @application \ No newline at end of file
+= render 'form', application: @application
diff --git a/app/views/doorkeeper/authorizations/error.html.haml b/app/views/doorkeeper/authorizations/error.html.haml
index 7561ec85ed9..a4c607cea60 100644
--- a/app/views/doorkeeper/authorizations/error.html.haml
+++ b/app/views/doorkeeper/authorizations/error.html.haml
@@ -1,3 +1,3 @@
%h3.page-title An error has occurred
%main{:role => "main"}
- %pre= @pre_auth.error_response.body[:error_description] \ No newline at end of file
+ %pre= @pre_auth.error_response.body[:error_description]
diff --git a/app/views/doorkeeper/authorizations/show.html.haml b/app/views/doorkeeper/authorizations/show.html.haml
index 9a402007194..01f9e46f142 100644
--- a/app/views/doorkeeper/authorizations/show.html.haml
+++ b/app/views/doorkeeper/authorizations/show.html.haml
@@ -1,3 +1,3 @@
%h3.page-title Authorization code:
%main{:role => "main"}
- %code#authorization_code= params[:code] \ No newline at end of file
+ %code#authorization_code= params[:code]
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 36fb2d51629..4d20dd5830e 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -1,5 +1,5 @@
-- if event.proper?
- .event-item{class: "#{event.body? ? "event-block" : "event-inline" }"}
+- if event.visible_to_user?(current_user)
+ .event-item{ class: event_row_class(event) }
.event-item-timestamp
#{time_ago_with_tooltip(event.created_at)}
diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml
index e9e16a7646f..c994e3b997d 100644
--- a/app/views/events/event/_common.html.haml
+++ b/app/views/events/event/_common.html.haml
@@ -4,7 +4,7 @@
= event_action_name(event)
- if event.target
- %strong= link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target]
+ %strong= link_to event.target.reference_link_text, [event.project.namespace.becomes(Namespace), event.project, event.target]
= event_preposition(event)
diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml
index 8cf36c711b4..5a2a469ba62 100644
--- a/app/views/events/event/_created_project.html.haml
+++ b/app/views/events/event/_created_project.html.haml
@@ -7,21 +7,3 @@
= link_to_project event.project
- else
= event.project_name
-
-- if !event.project.private? && twitter_sharing_enabled?
- .event-body{"data-user-is" => event.author_id}
- .event-note
- .md
- %p
- Congratulations! Why not share your accomplishment with the world?
-
- %a.twitter-share-button{ |
- href: "https://twitter.com/share", |
- "data-url" => event.project.web_url, |
- "data-text" => "I just #{event.action_name} a new project on GitLab! GitLab is version control on your server.", |
- "data-size" => "medium", |
- "data-related" => "gitlab", |
- "data-hashtags" => "gitlab", |
- "data-count" => "none"}
- Tweet
- %script{src: "//platform.twitter.com/widgets.js"}
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 83936d39b16..ea5a0358392 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -23,6 +23,8 @@
%hr
= link_to 'Remove avatar', group_avatar_path(@group.to_param), data: { confirm: "Group avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
+ = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
+
.form-group
%hr
= f.label :share_with_group_lock, class: 'control-label' do
@@ -32,6 +34,7 @@
= f.check_box :share_with_group_lock
%span.descr Prevent sharing a project with another group within this group
+
.form-actions
= f.submit 'Save group', class: "btn btn-save"
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index b0805593fdc..aea35c50862 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -10,6 +10,8 @@
- if current_user
= link_to issues_group_url(@group, 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/issuable/filter', type: :issues
diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml
index 4bc31cabea6..30ab8aeba13 100644
--- a/app/views/groups/new.html.haml
+++ b/app/views/groups/new.html.haml
@@ -17,6 +17,8 @@
.col-sm-10
= render 'shared/choose_group_avatar_button', f: f
+ = render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group
+
.form-group
.col-sm-offset-2.col-sm-10
= render 'shared/group_tips'
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 23a34ac36dd..3d16ecb097a 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -1,8 +1,5 @@
- @no_container = true
-- unless can?(current_user, :read_group, @group)
- - @disable_search_panel = true
-
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
@@ -18,7 +15,10 @@
= link_to group_icon(@group), target: '_blank' do
= image_tag group_icon(@group), class: "avatar group-avatar s90"
.cover-title
- = @group.name
+ %h1
+ = @group.name
+ %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
+ = visibility_level_icon(@group.visibility_level, fw: false)
.cover-desc.username
@#{@group.path}
@@ -27,34 +27,29 @@
.cover-desc.description
= markdown(@group.description, pipeline: :description)
-- if can?(current_user, :read_group, @group)
- %div{ class: container_class }
- .top-area
- %ul.nav-links
- %li.active
- = link_to "#projects", 'data-toggle' => 'tab' do
- All Projects
- - if @shared_projects.present?
- %li
- = link_to "#shared", 'data-toggle' => 'tab' do
- Shared Projects
- .nav-controls
- = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
- = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
- = render 'shared/projects/dropdown'
- - if can? current_user, :create_projects, @group
- = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
- = icon('plus')
- New Project
-
- .tab-content
- .tab-pane.active#projects
- = render "projects", projects: @projects
-
+%div{ class: container_class }
+ .top-area
+ %ul.nav-links
+ %li.active
+ = link_to "#projects", 'data-toggle' => 'tab' do
+ All Projects
- if @shared_projects.present?
- .tab-pane#shared
- = render "shared_projects", projects: @shared_projects
-
-- else
- %p.nav-links.no-top
- No projects to show
+ %li
+ = link_to "#shared", 'data-toggle' => 'tab' do
+ Shared Projects
+ .nav-controls
+ = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
+ = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
+ = render 'shared/projects/dropdown'
+ - if can? current_user, :create_projects, @group
+ = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
+ = icon('plus')
+ New Project
+
+ .tab-content
+ .tab-pane.active#projects
+ = render "projects", projects: @projects
+
+ - if @shared_projects.present?
+ .tab-pane#shared
+ = render "shared_projects", projects: @shared_projects
diff --git a/app/views/layouts/_collapse_button.html.haml b/app/views/layouts/_collapse_button.html.haml
deleted file mode 100644
index 2ed51d87ca1..00000000000
--- a/app/views/layouts/_collapse_button.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-- if nav_menu_collapsed?
- = link_to icon('angle-right'), '#', class: 'toggle-nav-collapse', title: "Open/Close"
-- else
- = link_to icon('angle-left'), '#', class: 'toggle-nav-collapse', title: "Open/Close"
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index c799e9c588d..9be36273c7d 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -1,5 +1,7 @@
.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" }
= render "layouts/broadcast"
+ .expand-nav
+ = link_to icon('bars'), '#', class: 'toggle-nav-collapse', title: "Open sidebar"
.sidebar-wrapper.nicescroll{ class: nav_sidebar_class }
.header-logo
%a#logo
@@ -8,15 +10,19 @@
.gitlab-text-container
%h3 GitLab
- - if defined?(sidebar) && sidebar
- = render "layouts/nav/#{sidebar}"
- - elsif current_user
- = render 'layouts/nav/dashboard'
+ - primary_sidebar = current_user ? 'dashboard' : 'explore'
+
+ - if defined?(sidebar) && sidebar && sidebar != primary_sidebar
+ .complex-sidebar
+ .nav-primary
+ = render "layouts/nav/#{primary_sidebar}"
+ .nav-secondary
+ = render "layouts/nav/#{sidebar}"
- else
- = render 'layouts/nav/explore'
+ = render "layouts/nav/#{primary_sidebar}"
.collapse-nav
- = render partial: 'layouts/collapse_button'
+ = link_to icon('angle-left'), '#', class: 'toggle-nav-collapse', title: "Hide sidebar"
- if current_user
= link_to current_user, class: 'sidebar-user', title: "Profile" do
= image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36'
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index 54af2c3063c..9d4ab9847a8 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -1,10 +1,33 @@
-.search
- = form_tag search_path, method: :get, class: 'navbar-form pull-left' do |f|
- = search_field_tag "search", nil, placeholder: 'Search', class: "search-input form-control", spellcheck: false, tabindex: "1"
+- if controller.controller_path =~ /^groups/
+ - label = 'This group'
+- if controller.controller_path =~ /^projects/
+ - label = 'This project'
+
+.search.search-form{class: "#{'has-location-badge' if label.present?}"}
+ = form_tag search_path, method: :get, class: 'navbar-form' do |f|
+ .search-input-container
+ .search-location-badge
+ - if label.present?
+ %span.location-badge
+ %i.location-text
+ = label
+ .search-input-wrap
+ .dropdown{ data: {url: search_autocomplete_path } }
+ = search_field_tag "search", nil, placeholder: 'Search', class: "search-input dropdown-menu-toggle", spellcheck: false, tabindex: "1", autocomplete: 'off', data: { toggle: 'dropdown' }
+ .dropdown-menu.dropdown-select
+ = dropdown_content do
+ %ul
+ %li
+ %a.is-focused.dropdown-menu-empty-link
+ Loading...
+ = dropdown_loading
+ %i.search-icon
+ %i.clear-icon.js-clear-input
+
= hidden_field_tag :group_id, @group.try(:id)
- - if @project && @project.persisted?
- = hidden_field_tag :project_id, @project.id
+ = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id'
+ - if @project && @project.persisted?
- if current_controller?(:issues)
= hidden_field_tag :scope, 'issues'
- elsif current_controller?(:merge_requests)
@@ -21,10 +44,3 @@
= hidden_field_tag :repository_ref, @ref
= button_tag 'Go' if ENV['RAILS_ENV'] == 'test'
.search-autocomplete-opts.hide{:'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref }
-
-:javascript
- $('.search-input').on('keyup', function(e) {
- if (e.keyCode == 27) {
- $('.search-input').blur();
- }
- });
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 77d01a7736c..0f3b8119379 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -6,10 +6,9 @@
= icon('bars')
.navbar-collapse.collapse
- %ul.nav.navbar-nav.pull-right
- - unless @disable_search_panel
- %li.hidden-sm.hidden-xs
- = render 'layouts/search'
+ %ul.nav.navbar-nav
+ %li.hidden-sm.hidden-xs
+ = render 'layouts/search'
%li.visible-sm.visible-xs
= link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('search')
@@ -39,13 +38,16 @@
= link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('sign-out')
- else
- .pull-right
- = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
+ %li
+ %div
+ = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
%h1.title= title
= render 'shared/outdated_browser'
+
- if @project && !@project.empty_repo?
- :javascript
- var findFileURL = "#{namespace_project_find_file_path(@project.namespace, @project, @ref || @project.repository.root_ref)}";
+ - if ref = @ref || @project.repository.root_ref
+ :javascript
+ var findFileURL = "#{namespace_project_find_file_path(@project.namespace, @project, ref)}";
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index 280a1b93729..22d1d4d8597 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -95,7 +95,7 @@
Spam Logs
%span.count= number_with_delimiter(SpamLog.count(:all))
- = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do
+ = nav_link(controller: :application_settings) do
= link_to admin_application_settings_path, title: 'Settings' do
= icon('cogs fw')
%span
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index db0cf393922..d1a180e4299 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,7 +1,7 @@
%ul.nav.nav-sidebar
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: 'home'}) do
= link_to dashboard_projects_path, title: 'Projects' do
- = icon('home fw')
+ = icon('bookmark fw')
%span
Projects
= nav_link(controller: :todos) do
@@ -15,12 +15,12 @@
= icon('dashboard fw')
%span
Activity
- = nav_link(controller: :groups) do
+ = nav_link(path: ['dashboard/groups#index', 'explore/groups#index']) do
= link_to dashboard_groups_path, title: 'Groups' do
= icon('group fw')
%span
Groups
- = nav_link(controller: :milestones) do
+ = nav_link(path: 'dashboard#milestones') do
= link_to dashboard_milestones_path, title: 'Milestones' do
= icon('clock-o fw')
%span
@@ -48,7 +48,6 @@
%span
Help
- %li.separate-item
= nav_link(controller: :profile) do
= link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do
= icon('user fw')
diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml
index 48039ca2918..f08c5edf99c 100644
--- a/app/views/layouts/nav/_explore.html.haml
+++ b/app/views/layouts/nav/_explore.html.haml
@@ -1,7 +1,7 @@
%ul.nav.nav-sidebar
= nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do
= link_to explore_root_path, title: 'Projects' do
- = icon('home fw')
+ = icon('bookmark fw')
%span
Projects
= nav_link(controller: :groups) do
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index 59411ae1da1..0b7de9633ec 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -1,51 +1,41 @@
%ul.nav.nav-sidebar
- = nav_link do
- = link_to root_path, title: 'Go to dashboard', class: 'back-link' do
- = icon('caret-square-o-left fw')
- %span
- Go to dashboard
-
- %li.separate-item
-
= nav_link(path: 'groups#show', html_options: {class: 'home'}) do
= link_to group_path(@group), title: 'Home' do
= icon('group fw')
%span
Group
- - if can?(current_user, :read_group, @group)
- = nav_link(path: 'groups#activity') do
- = link_to activity_group_path(@group), title: 'Activity' do
- = icon('dashboard fw')
- %span
- Activity
- - if current_user
- = nav_link(controller: [:group, :milestones]) do
- = link_to group_milestones_path(@group), title: 'Milestones' do
- = icon('clock-o fw')
- %span
- Milestones
- = nav_link(path: 'groups#issues') do
- = link_to issues_group_path(@group), title: 'Issues' do
- = icon('exclamation-circle fw')
- %span
- Issues
- - if current_user
- %span.count= number_with_delimiter(Issue.opened.of_group(@group).count)
- = nav_link(path: 'groups#merge_requests') do
- = link_to merge_requests_group_path(@group), title: 'Merge Requests' do
- = icon('tasks fw')
- %span
- Merge Requests
- - if current_user
- %span.count= number_with_delimiter(MergeRequest.opened.of_group(@group).count)
- = nav_link(controller: [:group_members]) do
- = link_to group_group_members_path(@group), title: 'Members' do
- = icon('users fw')
+ = nav_link(path: 'groups#activity') do
+ = link_to activity_group_path(@group), title: 'Activity' do
+ = icon('dashboard fw')
+ %span
+ Activity
+ = nav_link(controller: [:group, :milestones]) do
+ = link_to group_milestones_path(@group), title: 'Milestones' do
+ = icon('clock-o fw')
+ %span
+ Milestones
+ = nav_link(path: 'groups#issues') do
+ = link_to issues_group_path(@group), title: 'Issues' do
+ = icon('exclamation-circle fw')
+ %span
+ Issues
+ - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
+ %span.count= number_with_delimiter(issues.count)
+ = nav_link(path: 'groups#merge_requests') do
+ = link_to merge_requests_group_path(@group), title: 'Merge Requests' do
+ = icon('tasks fw')
+ %span
+ Merge Requests
+ - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened').execute
+ %span.count= number_with_delimiter(merge_requests.count)
+ = nav_link(controller: [:group_members]) do
+ = link_to group_group_members_path(@group), title: 'Members' do
+ = icon('users fw')
+ %span
+ Members
+ - if can?(current_user, :admin_group, @group)
+ = nav_link do
+ = link_to edit_group_path(@group), title: 'Settings' do
+ = icon ('cogs fw')
%span
- Members
- - if can?(current_user, :admin_group, @group)
- = nav_link(html_options: { class: "separate-item" }) do
- = link_to edit_group_path(@group), title: 'Settings' do
- = icon ('cogs fw')
- %span
- Settings
+ Settings
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index 3b9d31a6fc5..cc119fd64e6 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -1,12 +1,4 @@
%ul.nav.nav-sidebar
- = nav_link do
- = link_to root_path, title: 'Go to dashboard', class: 'back-link' do
- = icon('caret-square-o-left fw')
- %span
- Go to dashboard
-
- %li.separate-item
-
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
= link_to profile_path, title: 'Profile Settings' do
= icon('user fw')
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 0ae83ee01eb..d0f82b5f57f 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -1,19 +1,4 @@
%ul.nav.nav-sidebar
- - if @project.group
- = nav_link do
- = link_to group_path(@project.group), title: 'Go to group', class: 'back-link' do
- = icon('caret-square-o-left fw')
- %span
- Go to group
- - else
- = nav_link do
- = link_to root_path, title: 'Go to dashboard', class: 'back-link' do
- = icon('caret-square-o-left fw')
- %span
- Go to dashboard
-
- %li.separate-item
-
= nav_link(path: 'projects#show', html_options: {class: 'home'}) do
= link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
= icon('bookmark fw')
@@ -67,7 +52,7 @@
%span
Issues
- if @project.default_issues_tracker?
- %span.count.issue_counter= number_with_delimiter(@project.issues.opened.count)
+ %span.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count)
- if project_nav_tab? :merge_requests
= nav_link(controller: :merge_requests) do
@@ -113,7 +98,7 @@
Snippets
- if project_nav_tab? :settings
- = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do
+ = nav_link(html_options: {class: "#{project_tab_class}"}) do
= link_to edit_project_path(@project), title: 'Settings' do
= icon('cogs fw')
%span
diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml
index 37b4d562966..2997f59d946 100644
--- a/app/views/layouts/notify.html.haml
+++ b/app/views/layouts/notify.html.haml
@@ -1,33 +1,9 @@
%html{lang: "en"}
%head
%meta{content: "text/html; charset=utf-8", "http-equiv" => "Content-Type"}
- %title
- GitLab
- :css
- img {
- max-width: 100%;
- height: auto;
- }
- p.details {
- font-style:italic;
- color:#777
- }
- .footer p {
- font-size:small;
- color:#777
- }
- pre.commit-message {
- white-space: pre-wrap;
- }
- .file-stats a {
- text-decoration: none;
- }
- .file-stats .new-file {
- color: #090;
- }
- .file-stats .deleted-file {
- color: #B00;
- }
+ %title
+ GitLab
+ = stylesheet_link_tag 'notify'
%body
%div.content
= yield
diff --git a/app/views/notify/issue_moved_email.html.haml b/app/views/notify/issue_moved_email.html.haml
new file mode 100644
index 00000000000..40f7d61fe19
--- /dev/null
+++ b/app/views/notify/issue_moved_email.html.haml
@@ -0,0 +1,6 @@
+%p
+ Issue was moved to another project.
+%p
+ New issue:
+ = link_to namespace_project_issue_url(@new_project.namespace, @new_project, @new_issue) do
+ = @new_issue.title
diff --git a/app/views/notify/issue_moved_email.text.erb b/app/views/notify/issue_moved_email.text.erb
new file mode 100644
index 00000000000..b3bd43c2055
--- /dev/null
+++ b/app/views/notify/issue_moved_email.text.erb
@@ -0,0 +1,4 @@
+Issue was moved to another project.
+
+New issue location:
+<%= namespace_project_issue_url(@new_project.namespace, @new_project, @new_issue) %>
diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml
index 25e9e8ff008..4dbaa662b66 100644
--- a/app/views/profiles/keys/_key.html.haml
+++ b/app/views/profiles/keys/_key.html.haml
@@ -8,7 +8,7 @@
= key.fingerprint
.pull-right
%span.key-created-at
- created #{time_ago_with_tooltip(key.created_at)} ago
+ created #{time_ago_with_tooltip(key.created_at)}
= link_to path_to_key(key, is_admin), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-transparent prepend-left-10" do
%span.sr-only Remove
= icon('trash')
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index de80abd7f4d..3d15c0d932b 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -56,19 +56,20 @@
.prepend-top-default
= f.submit 'Update settings', class: "btn btn-create"
%hr
- %h5
- Groups (#{@group_members.count})
- %div
- %ul.bordered-list
- - @group_members.each do |group_member|
- - notification = Notification.new(group_member)
- = render 'settings', type: 'group', membership: group_member, notification: notification
- %h5
- Projects (#{@project_members.count})
- %p.account-well
- To specify the notification level per project of a group you belong to, you need to be a member of the project itself, not only its group.
- .append-bottom-default
- %ul.bordered-list
- - @project_members.each do |project_member|
- - notification = Notification.new(project_member)
- = render 'settings', type: 'project', membership: project_member, notification: notification
+.col-lg-9.col-lg-push-3
+ %h5
+ Groups (#{@group_members.count})
+ %div
+ %ul.bordered-list
+ - @group_members.each do |group_member|
+ - notification = Notification.new(group_member)
+ = render 'settings', type: 'group', membership: group_member, notification: notification
+ %h5
+ Projects (#{@project_members.count})
+ %p.account-well
+ To specify the notification level per project of a group you belong to, you need to be a member of the project itself, not only its group.
+ .append-bottom-default
+ %ul.bordered-list
+ - @project_members.each do |project_member|
+ - notification = Notification.new(project_member)
+ = render 'settings', type: 'project', membership: project_member, notification: notification
diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml
index afd4f996b62..44d758dceb3 100644
--- a/app/views/profiles/passwords/edit.html.haml
+++ b/app/views/profiles/passwords/edit.html.haml
@@ -24,12 +24,13 @@
= f.password_field :current_password, required: true, class: 'form-control'
%p.help-block
You must provide your current password in order to change it.
- .form-group
- = f.label :password, 'New password', class: 'label-light'
- = f.password_field :password, required: true, class: 'form-control'
- .form-group
- = f.label :password_confirmation, class: 'label-light'
- = f.password_field :password_confirmation, required: true, class: 'form-control'
- .prepend-top-default.append-bottom-default
- = f.submit 'Save password', class: "btn btn-create append-right-10"
+ .form-group
+ = f.label :password, 'New password', class: 'label-light'
+ = f.password_field :password, required: true, class: 'form-control'
+ .form-group
+ = f.label :password_confirmation, class: 'label-light'
+ = f.password_field :password_confirmation, required: true, class: 'form-control'
+ .prepend-top-default.append-bottom-default
+ = f.submit 'Save password', class: "btn btn-create append-right-10"
+ - unless @user.password_automatically_set?
= link_to "I forgot my password", reset_profile_password_path, method: :put, class: "account-btn-link"
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index cd582ba7060..dcb3be9585d 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -26,7 +26,7 @@
%a.btn.js-choose-user-avatar-button
Browse file...
%span.avatar-file-name.prepend-left-default.js-avatar-filename No file chosen
- = f.file_field :avatar, class: "js-user-avatar-input hidden"
+ = f.file_field :avatar, class: "js-user-avatar-input hidden", accept: "image/*"
.help-block
The maximum file size allowed is 200KB.
- if @user.avatar?
@@ -94,3 +94,25 @@
.prepend-top-default.append-bottom-default
= f.submit 'Update profile settings', class: "btn btn-success"
= link_to "Cancel", user_path(current_user), class: "btn btn-cancel"
+
+.modal.modal-profile-crop
+ .modal-dialog
+ .modal-content
+ .modal-header
+ %button.close{:type => "button", :'data-dismiss' => "modal"}
+ %span
+ &times;
+ %h4.modal-title
+ Position and size your new avatar
+ .modal-body
+ .profile-crop-image-container
+ %img.modal-profile-crop-image
+ .crop-controls
+ .btn-group
+ %button.btn.btn-primary{ data: { method: "zoom", option: "0.1" } }
+ %span.fa.fa-search-plus
+ %button.btn.btn-primary{ data: { method: "zoom", option: "-0.1" } }
+ %span.fa.fa-search-minus
+ .modal-footer
+ %button.btn.btn-primary.js-upload-user-avatar{:type => "button"}
+ Set new profile picture
diff --git a/app/views/projects/_builds_settings.html.haml b/app/views/projects/_builds_settings.html.haml
index 95ab9ecf3e8..9ae6964aaac 100644
--- a/app/views/projects/_builds_settings.html.haml
+++ b/app/views/projects/_builds_settings.html.haml
@@ -1,6 +1,14 @@
%fieldset.builds-feature
%legend
Builds:
+
+ - unless @repository.gitlab_ci_yml
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ %p Builds need to be configured before you can begin using Continuous Integration.
+ = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info'
+ %hr
+
.form-group
.col-sm-offset-2.col-sm-10
%p Get recent application code using the following command:
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index b45df44f270..9b5de17dd3b 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -2,21 +2,21 @@
.project-home-panel.cover-block.clearfix{:class => ("empty-project" if empty_repo)}
.project-identicon-holder
= project_icon(@project, alt: '', class: 'project-avatar avatar s90')
- .project-home-desc
+ .cover-title.project-home-desc
%h1
= @project.name
- %span.visibility-icon.has_tooltip{data: { container: 'body' },
- title: "#{visibility_level_label(@project.visibility_level)} - #{project_visibility_level_description(@project.visibility_level)}"}
+ %span.visibility-icon.has-tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)}
= visibility_level_icon(@project.visibility_level, fw: false)
- - if @project.description.present?
+ - if @project.description.present?
+ .cover-desc.project-home-desc
= markdown(@project.description, pipeline: :description)
- - if forked_from_project = @project.forked_from_project
- %p
- Forked from
- = link_to project_path(forked_from_project) do
- = forked_from_project.namespace.try(:name)
+ - if forked_from_project = @project.forked_from_project
+ .cover-desc
+ Forked from
+ = link_to project_path(forked_from_project) do
+ = forked_from_project.namespace.try(:name)
.cover-controls
- if current_user
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 76a823d3828..57e507e68c8 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -11,7 +11,7 @@
- if branch.name == @repository.root_ref
%span.label.label-primary default
- elsif @repository.merged_to_root_ref? branch.name
- %span.label.label-info.has_tooltip(title="Merged into #{@repository.root_ref}")
+ %span.label.label-info.has-tooltip(title="Merged into #{@repository.root_ref}")
merged
- if @project.protected_branch? branch.name
@@ -30,7 +30,7 @@
Compare
- if can_remove_branch?(@project, branch.name)
- = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has_tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do
+ = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do
= icon("trash-o")
- if branch.name != @repository.root_ref
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 7afea5a5049..88266e21230 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -16,7 +16,7 @@
- else
Name
%b.caret
- %ul.dropdown-menu
+ %ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to namespace_project_branches_path(sort: nil) do
Name
diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml
index 811d304ea75..aa85f495e39 100644
--- a/app/views/projects/builds/index.html.haml
+++ b/app/views/projects/builds/index.html.haml
@@ -27,6 +27,9 @@
= link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project),
data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
+ - unless @repository.gitlab_ci_yml
+ = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info'
+
= link_to ci_lint_path, class: 'btn btn-default' do
= icon('wrench')
%span CI Lint
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 6a60cfeff76..58f43ecb5d5 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -1,4 +1,4 @@
- unless @project.empty_repo?
- if can? current_user, :download_code, @project
- = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has_tooltip', data: {container: "body"}, rel: 'nofollow', title: "Download ZIP" do
+ = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has-tooltip', data: {container: "body"}, rel: 'nofollow', title: "Download ZIP" do
= icon('download')
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index e7c85edff96..1e4c46fca2f 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -3,25 +3,32 @@
%a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"}
= icon('plus')
%ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
- - if can?(current_user, :create_issue, @project)
+ - can_create_issue = can?(current_user, :create_issue, @project)
+ - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
+ - can_create_snippet = can?(current_user, :create_snippet, @project)
+
+ - if can_create_issue
%li
= link_to url_for_new_issue(@project, only_path: true) do
= icon('exclamation-circle fw')
New issue
- - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
+
- if merge_project
%li
= link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project) do
= icon('tasks fw')
New merge request
- - if can?(current_user, :create_snippet, @project)
+
+ - if can_create_snippet
%li
= link_to new_namespace_project_snippet_path(@project.namespace, @project) do
= icon('file-text-o fw')
New snippet
- - if can?(current_user, :push_code, @project)
+ - if can_create_issue || merge_project || can_create_snippet
%li.divider
+
+ - if can?(current_user, :push_code, @project)
%li
= link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master') do
= icon('file fw')
@@ -35,13 +42,11 @@
= icon('tags fw')
New tag
- elsif current_user && current_user.already_forked?(@project)
- %li.divider
%li
= link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master') do
= icon('file fw')
New file
- elsif can?(current_user, :fork_project, @project)
- %li.divider
%li
- continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master'),
notice: edit_in_new_fork_notice,
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index 133531887a2..5fb5fe5af2f 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -1,7 +1,7 @@
- unless @project.empty_repo?
- if current_user && can?(current_user, :fork_project, @project)
- if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
- = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn has_tooltip' do
+ = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn has-tooltip' do
= icon('code-fork fw')
Fork
%div.count-with-arrow
@@ -9,10 +9,10 @@
%span.count
= @project.forks_count
- else
- = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has_tooltip' do
+ = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has-tooltip' do
= icon('code-fork fw')
Fork
- %div.count-with-arrow
+ = link_to namespace_project_forks_path(@project.namespace, @project), class: 'count-with-arrow' do
%span.arrow
%span.count
= @project.forks_count
diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml
index 3e83ec3912f..a3786c35a1f 100644
--- a/app/views/projects/buttons/_notifications.html.haml
+++ b/app/views/projects/buttons/_notifications.html.haml
@@ -14,7 +14,7 @@
= notification_list_item(level, @membership)
- when GroupMember
- .btn.disabled.notifications-btn.has_tooltip{title: "To change the notification level, you need to be a member of the project itself, not only its group."}
+ .btn.disabled.notifications-btn.has-tooltip{title: "To change the notification level, you need to be a member of the project itself, not only its group."}
= icon('bell')
= notification_label(@membership)
= icon('angle-down')
diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml
index 21ba426aaa1..02dbb2985a4 100644
--- a/app/views/projects/buttons/_star.html.haml
+++ b/app/views/projects/buttons/_star.html.haml
@@ -1,5 +1,5 @@
- if current_user
- = link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star has_tooltip', method: :post, remote: true, title: "Star project" do
+ = link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star has-tooltip', method: :post, remote: true, title: "Star project" do
- if current_user.starred?(@project)
= icon('star fw')
%span.starred Unstar
@@ -12,7 +12,7 @@
= @project.star_count
- else
- = link_to new_user_session_path, class: 'btn has_tooltip star-btn', title: 'You must sign in to star a project' do
+ = link_to new_user_session_path, class: 'btn has-tooltip star-btn', title: 'You must sign in to star a project' do
= icon('star fw')
Star
%div.count-with-arrow
diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml
index bac9e244d36..46e4de40042 100644
--- a/app/views/projects/commits/_commit_list.html.haml
+++ b/app/views/projects/commits/_commit_list.html.haml
@@ -5,10 +5,10 @@
.panel-heading
Commits (#{@commits.count})
- if hidden > 0
- %ul.well-list
+ %ul.content-list
- commits.each do |commit|
= render "projects/commits/inline_commit", commit: commit, project: @project
%li.warning-row.unstyled
#{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues.
- else
- %ul.well-list= render commits, project: @project
+ %ul.content-list= render commits, project: @project
diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml
index a7e3c2478c2..64e8da9201d 100644
--- a/app/views/projects/commits/_commits.html.haml
+++ b/app/views/projects/commits/_commits.html.haml
@@ -12,7 +12,7 @@
.light
= pluralize(commits.count, 'commit')
.col-md-10.col-sm-12
- %ul.bordered-list
+ %ul.content-list
= render commits, project: project
%hr.lists-separator
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index 4ab81f3635c..dd590a4b8ec 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -1,7 +1,7 @@
= form_tag namespace_project_compare_index_path(@project.namespace, @project), method: :post, class: 'form-inline js-requires-input' do
.clearfix
- if params[:to] && params[:from]
- = link_to 'switch', {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has_tooltip', title: 'Switch base of comparison'}
+ = link_to 'switch', {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'}
.form-group
.input-group.inline-input-group
%span.input-group-addon from
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 6086ad3661e..2e1a37aa06d 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -20,4 +20,4 @@
- next unless blob
= render 'projects/diffs/file', i: index, project: project,
- diff_file: diff_file, diff_commit: diff_commit, blob: blob
+ diff_file: diff_file, diff_commit: diff_commit, blob: blob, diff_refs: diff_refs
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 3ac058a3bf8..698ed02ea0e 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -28,7 +28,7 @@
.file-actions.hidden-xs
- if blob_text_viewable?(blob)
- = link_to '#', class: 'js-toggle-diff-comments btn active has_tooltip', title: "Toggle comments for this file" do
+ = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: "Toggle comments for this file" do
= icon('comments')
\
@@ -42,13 +42,17 @@
.diff-content.diff-wrap-lines
-# Skipp all non non-supported blobs
- return unless blob.respond_to?('text?')
- - if blob_text_viewable?(blob)
- - if diff_view == 'parallel'
- = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i
- - else
- = render "projects/diffs/text_file", diff_file: diff_file, index: i
- - elsif blob.image?
- - old_file = project.repository.prev_blob_for_diff(diff_commit, diff_file)
- = render "projects/diffs/image", diff_file: diff_file, old_file: old_file, file: blob, index: i
+ - if diff_file.too_large?
+ .nothing-here-block
+ This diff could not be displayed because it is too large.
- else
- .nothing-here-block No preview for this file type
+ - if blob_text_viewable?(blob)
+ - if diff_view == 'parallel'
+ = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i
+ - else
+ = render "projects/diffs/text_file", diff_file: diff_file, index: i
+ - elsif blob.image?
+ - old_file = project.repository.prev_blob_for_diff(diff_commit, diff_file)
+ = render "projects/diffs/image", diff_file: diff_file, old_file: old_file, file: blob, index: i, diff_refs: diff_refs
+ - else
+ .nothing-here-block No preview for this file type
diff --git a/app/views/projects/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml
index 752e92e2e6b..8367112a9cb 100644
--- a/app/views/projects/diffs/_image.html.haml
+++ b/app/views/projects/diffs/_image.html.haml
@@ -1,6 +1,7 @@
- diff = diff_file.diff
- file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path))
-- old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.parent_id, diff.old_path))
+- old_commit_id = diff_refs.first.id
+- old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path))
- if diff.renamed_file || diff.new_file || diff.deleted_file
.image
%span.wrap
@@ -12,7 +13,7 @@
%div.two-up.view
%span.wrap
.frame.deleted
- %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.parent_id, diff.old_path))}
+ %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path))}
%img{src: old_file_raw_path}
%p.image-info.hide
%span.meta-filesize= "#{number_to_human_size old_file.size}"
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
new file mode 100644
index 00000000000..9464c8dc996
--- /dev/null
+++ b/app/views/projects/diffs/_line.html.haml
@@ -0,0 +1,26 @@
+- type = line.type
+%tr.line_holder{id: line_code, class: type}
+ - case type
+ - when 'match'
+ = render "projects/diffs/match_line", {line: line.text,
+ line_old: line.old_pos, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file}
+ - when 'nonewline'
+ %td.old_line.diff-line-num
+ %td.new_line.diff-line-num
+ %td.line_content.match= line.text
+ - else
+ %td.old_line.diff-line-num{class: type}
+ - link_text = raw(type == "new" ? "&nbsp;" : line.old_pos)
+ - if defined?(plain) && plain
+ = link_text
+ - else
+ = link_to link_text, "##{line_code}", id: line_code
+ - if @comments_allowed && can?(current_user, :create_note, @project)
+ = link_to_new_diff_note(line_code)
+ %td.new_line.diff-line-num{class: type, data: {linenumber: line.new_pos}}
+ - link_text = raw(type == "old" ? "&nbsp;" : line.new_pos)
+ - if defined?(plain) && plain
+ = link_text
+ - else
+ = link_to link_text, "##{line_code}", id: line_code
+ %td.line_content{class: "noteable_line #{type} #{line_code}", data: { line_code: line_code }}= diff_line_content(line.text)
diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml
index 9a8208202e4..e7169d7b599 100644
--- a/app/views/projects/diffs/_text_file.html.haml
+++ b/app/views/projects/diffs/_text_file.html.haml
@@ -8,26 +8,9 @@
- last_line = 0
- raw_diff_lines = diff_file.diff_lines.to_a
- diff_file.highlighted_diff_lines.each_with_index do |line, index|
- - type = line.type
- - last_line = line.new_pos
- line_code = generate_line_code(diff_file.file_path, line)
- - line_old = line.old_pos
- %tr.line_holder{ id: line_code, class: "#{type}" }
- - if type == "match"
- = render "projects/diffs/match_line", {line: line.text,
- line_old: line_old, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file}
- - elsif type == 'nonewline'
- %td.old_line.diff-line-num
- %td.new_line.diff-line-num
- %td.line_content.match= line.text
- - else
- %td.old_line.diff-line-num{class: type}
- = link_to raw(type == "new" ? "&nbsp;" : line_old), "##{line_code}", id: line_code
- - if @comments_allowed && can?(current_user, :create_note, @project)
- = link_to_new_diff_note(line_code)
- %td.new_line.diff-line-num{class: type, data: {linenumber: line.new_pos}}
- = link_to raw(type == "old" ? "&nbsp;" : line.new_pos), "##{line_code}", id: line_code
- %td.line_content{class: "noteable_line #{type} #{line_code}", data: { line_code: line_code }}= diff_line_content(line.text)
+ - last_line = line.new_pos
+ = render "projects/diffs/line", {line: line, diff_file: diff_file, line_code: line_code}
- if @reply_allowed
- comments = @line_notes.select { |n| n.line_code == line_code && n.active? }.sort_by(&:created_at)
diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml
index 905f6bbbd48..1fe1d98bf13 100644
--- a/app/views/projects/find_file/show.html.haml
+++ b/app/views/projects/find_file/show.html.haml
@@ -2,7 +2,7 @@
- header_title project_title(@project, "Files", project_files_path(@project))
.file-finder-holder.tree-holder.clearfix
- .gray-content-block.top-block
+ .nav-block
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'find_file', path: @path
%ul.breadcrumb.repo-breadcrumb
diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml
index edabc2d3b44..73a7fc0e1ac 100644
--- a/app/views/projects/forks/new.html.haml
+++ b/app/views/projects/forks/new.html.haml
@@ -12,7 +12,7 @@
.col-md-2.col-sm-3
- if fork = namespace.find_fork_of(@project)
.fork-thumbnail
- = link_to project_path(fork), title: "Visit project fork", class: 'has_tooltip' do
+ = link_to project_path(fork), title: "Visit project fork", class: 'has-tooltip' do
= image_tag namespace_icon(namespace, 100)
.caption
%strong
@@ -22,7 +22,7 @@
- else
.fork-thumbnail
- = link_to namespace_project_forks_path(@project.namespace, @project, namespace_key: namespace.id), title: "Fork here", method: "POST", class: 'has_tooltip' do
+ = link_to namespace_project_forks_path(@project.namespace, @project, namespace_key: namespace.id), title: "Fork here", method: "POST", class: 'has-tooltip' do
= image_tag namespace_icon(namespace, 100)
.caption
%strong
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index a44f34c2a68..4aa92d0b39e 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -3,10 +3,11 @@
.issue-check
= check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
- .issue-title
+ .issue-title.title
%span.issue-title-text
- = link_to_gfm issue.title, issue_path(issue), class: "title"
- %ul.controls.light
+ = confidential_icon(issue)
+ = link_to_gfm issue.title, issue_path(issue)
+ %ul.controls
- if issue.closed?
%li
CLOSED
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index e66e4669d48..6da8e4f33a9 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -1,5 +1,5 @@
- if current_user && can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
.pull-right
- = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), method: :post, class: 'btn', title: @issue.to_branch_name do
+ = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), method: :post, class: 'btn has-tooltip', title: @issue.to_branch_name do
= icon('code-fork')
New Branch
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index fde9304c0f8..efa7642b2dc 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -11,6 +11,8 @@
- if current_user
= link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do
= icon('rss')
+ %span.icon-label
+ Subscribe
= render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project)
- if can? current_user, :create_issue, @project
= link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index c3ee5c80e5f..6fa059cbe68 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -22,6 +22,7 @@
= icon('angle-double-left')
.issue-meta
+ = confidential_icon(@issue)
%strong.identifier
Issue ##{@issue.iid}
%span.creator
@@ -31,9 +32,9 @@
= time_ago_with_tooltip(@issue.created_at)
by
%strong
- = link_to_member(@project, @issue.author, avatar: false, size: 24, mobile_classes: "hidden-xs")
+ = link_to_member(@project, @issue.author, size: 24, mobile_classes: "hidden-xs")
%strong
- = link_to_member(@project, @issue.author, avatar: false, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg",
+ = link_to_member(@project, @issue.author, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg",
by_username: true, avatar: false)
.pull-right.issue-btn-group
@@ -44,7 +45,6 @@
- if can?(current_user, :update_issue, @issue)
= link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
= link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
-
= link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-nr btn-grouped issuable-edit' do
= icon('pencil-square-o')
Edit
diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml
index 4927d239c1e..0612863296a 100644
--- a/app/views/projects/labels/_label.html.haml
+++ b/app/views/projects/labels/_label.html.haml
@@ -8,7 +8,7 @@
%strong.append-right-20
= link_to_label(label) do
- = pluralize label.open_issues_count, 'open issue'
+ = pluralize label.open_issues_count(current_user), 'open issue'
- if current_user
.label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}}
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 18cf3f14f0b..391193eed6c 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -1,8 +1,8 @@
%li{ class: mr_css_classes(merge_request) }
- .merge-request-title
+ .merge-request-title.title
%span.merge-request-title-text
- = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "title"
- %ul.controls.light
+ = link_to_gfm merge_request.title, merge_request_path(merge_request)
+ %ul.controls
- if merge_request.merged?
%li
MERGED
@@ -17,7 +17,7 @@
- if merge_request.open? && merge_request.broken?
%li
- = link_to merge_request_path(merge_request), class: "has_tooltip", title: "Cannot be merged automatically", data: { container: 'body' } do
+ = link_to merge_request_path(merge_request), class: "has-tooltip", title: "Cannot be merged automatically", data: { container: 'body' } do
= icon('exclamation-triangle')
- if merge_request.assignee
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index ee5b9fd95a8..1dd8f721f7e 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -10,7 +10,7 @@
.merge-request{'data-url' => merge_request_path(@merge_request)}
= render "projects/merge_requests/show/mr_title"
- .merge-request-details.issuable-details
+ .merge-request-details.issuable-details{data: {id: @merge_request.project.id}}
= render "projects/merge_requests/show/mr_box"
.append-bottom-default.mr-source-target.prepend-top-default
- if @merge_request.open?
diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml
index c6cbe8589ef..ab4b1f14be5 100644
--- a/app/views/projects/merge_requests/show/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_title.html.haml
@@ -19,9 +19,9 @@
= time_ago_with_tooltip(@merge_request.created_at)
by
%strong
- = link_to_member(@project, @merge_request.author, avatar: false, size: 24, mobile_classes: "hidden-xs")
+ = link_to_member(@project, @merge_request.author, size: 24, mobile_classes: "hidden-xs")
%strong
- = link_to_member(@project, @merge_request.author, avatar: false, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg",
+ = link_to_member(@project, @merge_request.author, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg",
by_username: true, avatar: false)
.issue-btn-group.pull-right
@@ -29,7 +29,7 @@
- if @merge_request.open?
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: 'btn btn-nr btn-grouped btn-close', title: 'Close merge request'
= link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-nr btn-grouped issuable-edit', id: 'edit_merge_request' do
- %i.fa.fa-pencil-square-o
+ = icon('pencil-square-o')
Edit
- if @merge_request.closed?
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'btn btn-nr btn-grouped btn-reopen reopen-mr-link', title: 'Reopen merge request'
diff --git a/app/views/projects/merge_requests/widget/open/_wip.html.haml b/app/views/projects/merge_requests/widget/open/_wip.html.haml
index 0cf16542cc1..c296422a9cf 100644
--- a/app/views/projects/merge_requests/widget/open/_wip.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_wip.html.haml
@@ -1,5 +1,11 @@
%h4
This merge request is currently a Work In Progress
-%p
- When this merge request is ready, remove the "WIP" prefix from the title to allow it to be merged.
+- if can?(current_user, :update_merge_request, @merge_request)
+ %p
+ When this merge request is ready,
+ = link_to remove_wip_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), method: :post do
+ remove the
+ %code WIP:
+ prefix from the title
+ to allow it to be merged.
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index b4597043a27..be63875ab34 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -42,7 +42,7 @@
= preserve do
= markdown @milestone.description
-- if @milestone.complete? && @milestone.active?
+- if @milestone.complete?(current_user) && @milestone.active?
.alert.alert-success.prepend-top-default
%span All issues for this milestone are closed. You may close milestone now.
diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml
index 13e624764d9..2999befffc6 100644
--- a/app/views/projects/notes/_edit_form.html.haml
+++ b/app/views/projects/notes/_edit_form.html.haml
@@ -5,6 +5,6 @@
= render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field'
= render 'projects/notes/hints'
- .note-form-actions
+ .note-form-actions.clearfix
= f.submit 'Save Comment', class: 'btn btn-nr btn-save btn-grouped js-comment-button'
= link_to 'Cancel', '#', class: 'btn btn-nr btn-cancel note-edit-cancel'
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 2cf32e6093d..34fe1743f4b 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -5,28 +5,21 @@
= image_tag avatar_icon(note.author), alt: '', class: 'avatar s40'
.timeline-content
.note-header
+ = link_to_member(note.project, note.author, avatar: false)
+ .inline.note-headline-light
+ = "#{note.author.to_reference} commented"
+ %a{ href: "##{dom_id(note)}" }
+ = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
- if note_editable?(note)
.note-actions
- = link_to '#', title: 'Edit comment', class: 'js-note-edit' do
+ - access = note.project.team.human_max_access(note.author.id)
+ - if access
+ %span.note-role
+ = access
+ = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil-square-o')
-
- = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'js-note-delete danger' do
+ = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
= icon('trash-o')
-
- - unless note.system
- - access = note.project.team.human_max_access(note.author.id)
- - if access
- %span.note-role.label
- = access
-
- = link_to_member(note.project, note.author, avatar: false)
-
- %span.author-username
- = '@' + note.author.username
-
- %span.note-last-update
- %a{name: dom_id(note), href: "##{dom_id(note)}", title: 'Link here'}
- = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note_created_ago')
.note-body{class: note_editable?(note) ? 'js-task-list-container' : ''}
.note-text
= preserve do
diff --git a/app/views/projects/notes/discussions/_active.html.haml b/app/views/projects/notes/discussions/_active.html.haml
index 4f15a99d061..cd8a5f0bd02 100644
--- a/app/views/projects/notes/discussions/_active.html.haml
+++ b/app/views/projects/notes/discussions/_active.html.haml
@@ -1,22 +1,20 @@
- note = discussion_notes.first
.discussion.js-toggle-container{ class: note.discussion_id }
.discussion-header
+ = link_to_member(@project, note.author, avatar: false)
+ .inline.discussion-headline-light
+ = "#{note.author.to_reference} started a discussion"
+ = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do
+ on the diff
.discussion-actions
- = link_to "#", class: "js-toggle-button" do
+ = link_to "#", class: "discussion-action-button discussion-toggle-button js-toggle-button" do
%i.fa.fa-chevron-up
Show/hide discussion
- %div
- = link_to_member(@project, note.author, avatar: false)
- started a discussion
- = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do
- %strong on the diff
.last-update.hide.js-toggle-content
- last_note = discussion_notes.last
last updated by
= link_to_member(@project, last_note.author, avatar: false)
-
- %span.discussion-last-update
- #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')}
+ #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')}
.discussion-body.js-toggle-content
= render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note
diff --git a/app/views/projects/notes/discussions/_commit.html.haml b/app/views/projects/notes/discussions/_commit.html.haml
index 3da2f2060b8..46f2ba4bbcf 100644
--- a/app/views/projects/notes/discussions/_commit.html.haml
+++ b/app/views/projects/notes/discussions/_commit.html.haml
@@ -1,20 +1,22 @@
- note = discussion_notes.first
+- commit = note.noteable
+- commit_description = commit ? 'commit' : 'a deleted commit'
.discussion.js-toggle-container{ class: note.discussion_id }
.discussion-header
+ = link_to_member(@project, note.author, avatar: false)
+ .inline.discussion-headline-light
+ = "#{note.author.to_reference} started a discussion on #{commit_description}"
+ - if commit
+ = link_to(commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace')
.discussion-actions
- = link_to "#", class: "js-toggle-button" do
+ = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do
%i.fa.fa-chevron-up
Show/hide discussion
- %div
- = link_to_member(@project, note.author, avatar: false)
- started a discussion on commit
- = link_to(note.noteable.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace')
.last-update.hide.js-toggle-content
- last_note = discussion_notes.last
last updated by
= link_to_member(@project, last_note.author, avatar: false)
- %span.discussion-last-update
- #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')}
+ #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')}
.discussion-body.js-toggle-content
- if note.for_diff_line?
= render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note
diff --git a/app/views/projects/notes/discussions/_outdated.html.haml b/app/views/projects/notes/discussions/_outdated.html.haml
index 218b0da3977..f8e000b424f 100644
--- a/app/views/projects/notes/discussions/_outdated.html.haml
+++ b/app/views/projects/notes/discussions/_outdated.html.haml
@@ -1,19 +1,18 @@
- note = discussion_notes.first
.discussion.js-toggle-container{ class: note.discussion_id }
.discussion-header
+ = link_to_member(@project, note.author, avatar: false)
+ .inline.discussion-headline-light
+ = "#{note.author.to_reference} started a discussion"
+ on the outdated diff
.discussion-actions
- = link_to "#", class: "js-toggle-button" do
+ = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do
%i.fa.fa-chevron-down
Show/hide discussion
- %div
- = link_to_member(@project, note.author, avatar: false)
- started a discussion on the
- %strong outdated diff
- %div
+ .last-update.hide.js-toggle-content
- last_note = discussion_notes.last
last updated by
= link_to_member(@project, last_note.author, avatar: false)
- %span.discussion-last-update
- #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')}
+ #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')}
.discussion-body.js-toggle-content.hide
= render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note
diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml
index 667057ef2d8..093d1d1bb0f 100644
--- a/app/views/projects/tags/_download.html.haml
+++ b/app/views/projects/tags/_download.html.haml
@@ -6,7 +6,7 @@
%span.caret
%span.sr-only
Select Archive Format
- %ul.col-xs-10.dropdown-menu{ role: 'menu' }
+ %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
%li
= link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do
%i.fa.fa-download
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 399782273d3..dbc35c16feb 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -15,11 +15,11 @@
= render 'projects/tags/download', ref: tag.name, project: @project
- if can?(current_user, :push_code, @project)
- = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn-grouped btn has_tooltip', title: "Edit release notes" do
+ = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn-grouped btn has-tooltip', title: "Edit release notes" do
= icon("pencil")
- if can?(current_user, :admin_project, @project)
- = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has_tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do
+ = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do
= icon("trash-o")
- if commit
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index 8c7f93f93b6..1dc9b799a95 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -5,17 +5,17 @@
.gray-content-block
.pull-right
- if can?(current_user, :push_code, @project)
- = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn-grouped btn has_tooltip', title: 'Edit release notes' do
+ = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn-grouped btn has-tooltip', title: 'Edit release notes' do
= icon("pencil")
- = link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped has_tooltip', title: 'Browse files' do
+ = link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped has-tooltip', title: 'Browse files' do
= icon('files-o')
- = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped has_tooltip', title: 'Browse commits' do
+ = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped has-tooltip', title: 'Browse commits' do
= icon('history')
- if can? current_user, :download_code, @project
= render 'projects/tags/download', ref: @tag.name, project: @project
- if can?(current_user, :admin_project, @project)
.pull-right
- = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped has_tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do
+ = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do
%i.fa.fa-trash-o
.title
%span.item-title= @tag.name
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 3eb626e6dca..1c5f8b3928b 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -15,11 +15,11 @@
- if current_user
%li
- if !on_top_of_branch?
- %span.btn.btn-sm.add-to-tree.disabled.has_tooltip{title: "You can only add files when you are on a branch", data: { container: 'body' }}
+ %span.btn.add-to-tree.disabled.has-tooltip{title: "You can only add files when you are on a branch", data: { container: 'body' }}
= icon('plus')
- else
%span.dropdown
- %a.dropdown-toggle.btn.btn-sm.add-to-tree{href: '#', "data-toggle" => "dropdown"}
+ %a.dropdown-toggle.btn.add-to-tree{href: '#', "data-toggle" => "dropdown"}
= icon('plus')
%ul.dropdown-menu
- if can_edit_tree?
diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml
index 45d700781f3..710f5613c81 100644
--- a/app/views/search/results/_issue.html.haml
+++ b/app/views/search/results/_issue.html.haml
@@ -1,5 +1,6 @@
.search-result-row
%h4
+ = confidential_icon(issue)
= link_to [issue.project.namespace.becomes(Namespace), issue.project, issue] do
%span.term.str-truncated= issue.title
.pull-right ##{issue.iid}
diff --git a/app/views/search/results/_milestone.html.haml b/app/views/search/results/_milestone.html.haml
index e0b18733d74..b31595d8d1c 100644
--- a/app/views/search/results/_milestone.html.haml
+++ b/app/views/search/results/_milestone.html.haml
@@ -6,4 +6,4 @@
- if milestone.description.present?
.description.term
= preserve do
- = search_md_sanitize(markdown(milestone.description)) \ No newline at end of file
+ = search_md_sanitize(markdown(milestone.description))
diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml
index 5fcba2b7e93..9544e3d3e17 100644
--- a/app/views/search/results/_note.html.haml
+++ b/app/views/search/results/_note.html.haml
@@ -1,24 +1,20 @@
- project = note.project
+- note_url = Gitlab::UrlBuilder.new(:note).build(note.id)
+- noteable_identifier = note.noteable.try(:iid) || note.noteable.id
.search-result-row
%h5.note-search-caption.str-truncated
%i.fa.fa-comment
= link_to_member(project, note.author, avatar: false)
commented on
+ = link_to project.name_with_namespace, project
+ &middot;
- if note.for_commit?
- = link_to project do
- = project.name_with_namespace
- &middot;
- = link_to namespace_project_commit_path(project.namespace, project, note.commit_id, anchor: dom_id(note)) do
- Commit #{truncate_sha(note.commit_id)}
+ = link_to "Commit #{truncate_sha(note.commit_id)}", note_url
- else
- = link_to project do
- = project.name_with_namespace
- &middot;
- %span #{note.noteable_type.titleize} ##{note.noteable.iid}
+ %span #{note.noteable_type.titleize} ##{noteable_identifier}
&middot;
- = link_to [project.namespace.becomes(Namespace), project, note.noteable, anchor: dom_id(note)] do
- = note.noteable.title
+ = link_to note.noteable.title, note_url
.note-search-result
.term
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index faf7e49ed29..974751d9970 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -8,11 +8,9 @@
= icon('angle-down')
%ul.dropdown-menu.dropdown-menu-right.clone-options-dropdown
%li
- %a#ssh-selector{href: @project.ssh_url_to_repo}
- SSH
+ = ssh_clone_button(project)
%li
- %a#http-selector{href: @project.http_url_to_repo}
- HTTPS
+ = http_clone_button(project)
= text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true
.input-group-btn
diff --git a/app/views/shared/_group_tips.html.haml b/app/views/shared/_group_tips.html.haml
index e5cf783beb7..46e4340511a 100644
--- a/app/views/shared/_group_tips.html.haml
+++ b/app/views/shared/_group_tips.html.haml
@@ -1,6 +1,5 @@
%ul
%li A group is a collection of several projects
- %li Groups are private by default
%li Members of a group may only view projects they have permission to access
%li Group project URLs are prefixed with the group namespace
%li Existing projects may be moved into a group
diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml
index 8134b15d245..4b47b0291be 100644
--- a/app/views/shared/_label_row.html.haml
+++ b/app/views/shared/_label_row.html.haml
@@ -1,4 +1,4 @@
%span.label-row
- = link_to_label(label)
+ = link_to_label(label, tooltip: false)
%span.prepend-left-10
= markdown(label.description, pipeline: :single_line)
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index fb9a8db0889..40c6eb9be45 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -10,24 +10,28 @@
%i.fa.fa-cogs
= link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-sm btn btn-grouped", title: 'Leave this group' do
- %i.fa.fa-sign-out
+ = icon('sign-out')
.stats
%span
- = icon('home')
+ = icon('bookmark')
= number_with_delimiter(group.projects.count)
%span
= icon('users')
= number_with_delimiter(group.users.count)
+ %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)}
+ = visibility_level_icon(group.visibility_level, fw: false)
+
= image_tag group_icon(group), class: "avatar s40 hidden-xs"
- = link_to group, class: 'group-name title' do
- = group.name
+ .title
+ = link_to group, class: 'group-name' do
+ = group.name
- - if group_member
- as
- %span #{group_member.human_access}
+ - if group_member
+ as
+ %span #{group_member.human_access}
- if group.description.present?
.description
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index dfdc84ba4cc..921eaefd79a 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -7,78 +7,22 @@
class: "check_all_issues left"
.issues-other-filters
.filter-item.inline
- - if params[:author_id]
+ - if params[:author_id].present?
= hidden_field_tag(:author_id, params[:author_id])
- = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author",
- placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id" } })
+ = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit",
+ placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
.filter-item.inline
- - if params[:assignee_id]
+ - if params[:assignee_id].present?
= hidden_field_tag(:assignee_id, params[:assignee_id])
- = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee",
- placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id" } })
+ = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
+ placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
.filter-item.inline.milestone-filter
- - if params[:milestone_title]
- = hidden_field_tag(:milestone_title, params[:milestone_title])
- = dropdown_tag(h(params[:milestone_name] || "Milestone"), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable",
- placeholder: "Search milestones", footer_content: true, data: { show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: (@project.id if @project), milestones: (namespace_project_milestones_path(@project.namespace, @project, :js) if @project) } }) do
- - if @project
- %ul.dropdown-footer-list
- - if can? current_user, :admin_milestone, @project
- %li
- = link_to new_namespace_project_milestone_path(@project.namespace, @project), title: "New Milestone" do
- Create new
- %li
- = link_to namespace_project_milestones_path(@project.namespace, @project) do
- - if can? current_user, :admin_milestone, @project
- Manage milestones
- - else
- View milestones
+ = render "shared/issuable/milestone_dropdown"
.filter-item.inline.labels-filter
- - if params[:label_name]
- = hidden_field_tag(:label_name, params[:label_name])
- .dropdown
- %button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: (@project.id if @project), labels: (namespace_project_labels_path(@project.namespace, @project, :js) if @project)}}
- %span.dropdown-toggle-text
- = h(params[:label_name] || "Label")
- = icon('chevron-down')
- .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
- .dropdown-page-one
- = dropdown_title("Filter by label")
- = dropdown_filter("Search labels")
- = dropdown_content
- - if @project
- = dropdown_footer do
- %ul.dropdown-footer-list
- - if can? current_user, :admin_label, @project
- %li
- %a.dropdown-toggle-page{href: "#"}
- Create new
- %li
- = link_to namespace_project_labels_path(@project.namespace, @project) do
- - if can? current_user, :admin_label, @project
- Manage labels
- - else
- View labels
- - if can? current_user, :admin_label, @project
- .dropdown-page-two
- = dropdown_title("Create new label", back: true)
- = dropdown_content do
- %input#new_label_color{type: "hidden"}
- %input#new_label_name.dropdown-input-field{type: "text", placeholder: "Name new label"}
- .dropdown-label-color-preview.js-dropdown-label-color-preview
- .suggest-colors.suggest-colors-dropdown
- - suggested_colors.each do |color|
- = link_to '#', style: "background-color: #{color}", data: { color: color } do
- &nbsp
- %button.btn.btn-primary.js-new-label-btn{type: "button"}
- Create
- = dropdown_loading
- .dropdown-loading
- = icon('spinner spin')
-
+ = render "shared/issuable/label_dropdown"
.pull-right
= render 'shared/sort_dropdown'
@@ -86,18 +30,17 @@
.issues_bulk_update.hide
= form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do
.filter-item.inline
- = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
+ = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
%ul
%li
%a{href: "#", data: {id: "reopen"}} Open
%li
%a{href: "#", data: {id: "close"}} Closed
.filter-item.inline
- = dropdown_tag("Assignee", options: { toggle_class: "js-user-search", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
+ = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
.filter-item.inline
- = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable",
- placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :js), use_id: true } })
+ = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
= hidden_field_tag 'update[issues_ids]', []
= hidden_field_tag :state_event, params[:state_event]
.filter-item.inline
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index d5a4aad05d9..e2a9e5bfb92 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -13,12 +13,21 @@
- if issuable.is_a?(MergeRequest)
%p.help-block
- - if issuable.work_in_progress?
- Remove the <code>WIP</code> prefix from the title to allow this
- <strong>Work In Progress</strong> merge request to be merged when it's ready.
- - else
- Start the title with <code>[WIP]</code> or <code>WIP:</code> to prevent a
- <strong>Work In Progress</strong> merge request from being merged before it's ready.
+ .js-wip-explanation
+ %a.js-toggle-wip{href: "", tabindex: -1}
+ Remove the
+ %code WIP:
+ prefix from the title
+ to allow this
+ %strong Work In Progress
+ merge request to be merged when it's ready.
+ .js-no-wip-explanation
+ %a.js-toggle-wip{href: "", tabindex: -1}
+ Start the title with
+ %code WIP:
+ to prevent a
+ %strong Work In Progress
+ merge request from being merged before it's ready.
.form-group.detail-page-description
= f.label :description, 'Description', class: 'control-label'
.col-sm-10
@@ -29,6 +38,15 @@
= render 'projects/notes/hints'
.clearfix
.error-alert
+
+- if issuable.is_a?(Issue) && !issuable.project.private?
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :confidential do
+ = f.check_box :confidential
+ This issue is confidential and should only be visible to team members
+
- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
%hr
.form-group
@@ -67,13 +85,26 @@
- if can? current_user, :admin_label, issuable.project
= link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank
+- if issuable.can_move?(current_user)
+ %hr
+ .form-group
+ = label_tag :move_to_project_id, 'Move', class: 'control-label'
+ .col-sm-10
+ - projects = project_options(issuable, current_user, ability: :admin_issue)
+ = select_tag(:move_to_project_id, projects, include_blank: true,
+ class: 'select2', data: { placeholder: 'Select project' })
+ &nbsp;
+ %span{ data: { toggle: 'tooltip', placement: 'auto top' }, style: 'cursor: default',
+ title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' }
+ = icon('question-circle')
+
- if issuable.is_a?(MergeRequest)
%hr
- - if @merge_request.new_record?
- .form-group
- = f.label :source_branch, class: 'control-label'
- .col-sm-10
- = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true })
+ - if @merge_request.new_record?
+ .form-group
+ = f.label :source_branch, class: 'control-label'
+ .col-sm-10
+ = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true })
.form-group
= f.label :target_branch, class: 'control-label'
.col-sm-10
@@ -96,7 +127,12 @@
for this project.
- if issuable.new_record?
- - cancel_project = issuable.source_project
+ = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel'
- else
- - cancel_project = issuable.project
- = link_to 'Cancel', [cancel_project.namespace.becomes(Namespace), cancel_project, issuable], class: 'btn btn-cancel'
+ .pull-right
+ - if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project)
+ = link_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" },
+ method: :delete, class: 'btn btn-grouped' do
+ = icon('trash-o')
+ Delete
+ = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel'
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
new file mode 100644
index 00000000000..fd5e58c1f1f
--- /dev/null
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -0,0 +1,44 @@
+- if params[:label_name].present?
+ = hidden_field_tag(:label_name, params[:label_name])
+.dropdown
+ %button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}}
+ %span.dropdown-toggle-text
+ = h(params[:label_name].presence || "Label")
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
+ .dropdown-page-one
+ = dropdown_title("Filter by label")
+ = dropdown_filter("Search labels")
+ = dropdown_content
+ - if @project
+ = dropdown_footer do
+ %ul.dropdown-footer-list
+ - if can? current_user, :admin_label, @project
+ %li
+ %a.dropdown-toggle-page{href: "#"}
+ Create new
+ %li
+ = link_to namespace_project_labels_path(@project.namespace, @project) do
+ - if can? current_user, :admin_label, @project
+ Manage labels
+ - else
+ View labels
+ - if can? current_user, :admin_label, @project and @project
+ .dropdown-page-two.dropdown-new-label
+ = dropdown_title("Create new label", back: true)
+ = dropdown_content do
+ .dropdown-labels-error.js-label-error
+ %input#new_label_name.dropdown-input-field{type: "text", placeholder: "Name new label"}
+ .suggest-colors.suggest-colors-dropdown
+ - suggested_colors.each do |color|
+ = link_to '#', style: "background-color: #{color}", data: { color: color } do
+ &nbsp
+ .dropdown-label-color-input
+ .dropdown-label-color-preview.js-dropdown-label-color-preview
+ %input#new_label_color.dropdown-input-field{ type: "text" }
+ .clearfix
+ %button.btn.btn-primary.pull-left.js-new-label-btn{type: "button"}
+ Create
+ %button.btn.btn-default.pull-right.js-cancel-label-btn{type: "button"}
+ Cancel
+ = dropdown_loading
diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml
new file mode 100644
index 00000000000..2fcf40ece99
--- /dev/null
+++ b/app/views/shared/issuable/_milestone_dropdown.html.haml
@@ -0,0 +1,16 @@
+- if params[:milestone_title].present?
+ = hidden_field_tag(:milestone_title, params[:milestone_title])
+= dropdown_tag(milestone_dropdown_label(params[:milestone_title]), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable",
+ placeholder: "Search milestones", footer_content: @project.present?, data: { show_no: true, show_any: true, show_upcoming: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
+ - if @project
+ %ul.dropdown-footer-list
+ - if can? current_user, :admin_milestone, @project
+ %li
+ = link_to new_namespace_project_milestone_path(@project.namespace, @project), title: "New Milestone" do
+ Create new
+ %li
+ = link_to namespace_project_milestones_path(@project.namespace, @project) do
+ - if can? current_user, :admin_milestone, @project
+ Manage milestones
+ - else
+ View milestones
diff --git a/app/views/shared/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml
index f1d92ef48b2..33a9a494857 100644
--- a/app/views/shared/issuable/_participants.html.haml
+++ b/app/views/shared/issuable/_participants.html.haml
@@ -1,3 +1,6 @@
+- participants_row = 7
+- participants_size = participants.size
+- participants_extra = participants_size - participants_row
.block.participants
.sidebar-collapsed-icon
= icon('users')
@@ -5,6 +8,13 @@
= participants.count
.title.hide-collapsed
= pluralize participants.count, "participant"
- - participants.each do |participant|
- %span.hide-collapsed
- = link_to_member(@project, participant, name: false, size: 24)
+ .hide-collapsed.participants-list
+ - participants.each do |participant|
+ .participants-author.js-participants-author
+ = link_to_member(@project, participant, name: false, size: 24)
+ - if participants_extra > 0
+ %div.participants-more
+ %a.js-participants-more{href: "#", data: {original_text: "+ #{participants_size - 7} more", less_text: "- show less"}}
+ + #{participants_extra} more
+:javascript
+ IssuableContext.prototype.PARTICIPANTS_ROW_COUNT = #{participants_row};
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 23b1ed1e51b..47e544acf52 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -1,13 +1,12 @@
%aside.right-sidebar{ class: sidebar_gutter_collapsed_class }
.issuable-sidebar
- .block
+ .block.issuable-sidebar-header
%span.issuable-count.hide-collapsed.pull-left
= issuable.iid
of
= issuables_count(issuable)
- %span.pull-right
- %a.gutter-toggle.js-sidebar-toggle{href: '#'}
- = sidebar_gutter_toggle_icon
+ %a.gutter-toggle.pull-right.js-sidebar-toggle{href: '#'}
+ = sidebar_gutter_toggle_icon
.issuable-nav.hide-collapsed.pull-right.btn-group{role: 'group', "aria-label" => '...'}
- if prev_issuable = prev_issuable_for(issuable)
= link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn'
@@ -22,28 +21,33 @@
= form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f|
.block.assignee
- .sidebar-collapsed-icon
+ .sidebar-collapsed-icon.sidebar-collapsed-user{data: {toggle: "tooltip", placement: "left", container: "body"}, title: (issuable.assignee.to_reference if issuable.assignee)}
- if issuable.assignee
- = link_to_member_avatar(issuable.assignee, size: 24)
+ = link_to_member(@project, issuable.assignee, size: 24)
- else
= icon('user')
.title.hide-collapsed
- %label
- Assignee
+ Assignee
+ = icon('spinner spin', class: 'block-loading')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- .pull-right
- = link_to 'Edit', '#', class: 'edit-link'
- .value.hide-collapsed
+ = link_to 'Edit', '#', class: 'edit-link pull-right'
+ .value.bold.hide-collapsed
- if issuable.assignee
- %strong= link_to_member(@project, issuable.assignee, size: 24)
- - if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee)
- %a.pull-right.cannot-be-merged{href: '#', data: {toggle: 'tooltip'}, title: 'Not allowed to merge'}
- = icon('exclamation-triangle')
+ = link_to_member(@project, issuable.assignee, size: 32) do
+ - if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee)
+ %span.pull-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' }
+ = icon('exclamation-triangle')
+ %span.username
+ = issuable.assignee.to_reference
- else
- .light None
+ %span.assign-yourself
+ No assignee -
+ %a.js-assign-yourself{ href: '#' }
+ assign yourself
.selectbox.hide-collapsed
- = users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true, first_user: true)
+ = f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id'
+ = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } })
.block.milestone
.sidebar-collapsed-icon
@@ -54,24 +58,20 @@
- else
No
.title.hide-collapsed
- %label
- Milestone
+ Milestone
+ = icon('spinner spin', class: 'block-loading')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- .pull-right
- = link_to 'Edit', '#', class: 'edit-link'
- .value.hide-collapsed
+ = link_to 'Edit', '#', class: 'edit-link pull-right'
+ .value.bold.hide-collapsed
- if issuable.milestone
- %span.back-to-milestone
- = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do
- %strong
- = icon('clock-o')
- = issuable.milestone.title
+ = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do
+ = issuable.milestone.title
- else
.light None
+
.selectbox.hide-collapsed
- = f.select(:milestone_id, milestone_options(issuable), { include_blank: true }, { class: 'select2 select2-compact js-select2 js-milestone', data: { placeholder: 'Select milestone' }})
- = hidden_field_tag :issuable_context
- = f.submit class: 'btn hide'
+ = f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil
+ = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }})
- if issuable.project.labels.any?
.block.labels
@@ -80,29 +80,51 @@
%span
= issuable.labels.count
.title.hide-collapsed
- %label Labels
+ Labels
+ = icon('spinner spin', class: 'block-loading')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- .pull-right
- = link_to 'Edit', '#', class: 'edit-link'
- .value.issuable-show-labels.hide-collapsed
+ = link_to 'Edit', '#', class: 'edit-link pull-right'
+ .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels.any?) }
- if issuable.labels.any?
- issuable.labels.each do |label|
= link_to_label(label, type: issuable.to_ability_name)
- else
.light None
.selectbox.hide-collapsed
- = f.collection_select :label_ids, issuable.project.labels.all, :id, :name,
- { selected: issuable.label_ids }, multiple: true, class: 'select2 js-select2', data: { placeholder: "Select labels" }
+ - issuable.labels.each do |label|
+ = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
+ .dropdown
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", project_id: (@project.id if @project), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}}
+ %span.dropdown-toggle-text
+ Label
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
+ .dropdown-page-one
+ = dropdown_title("Assign labels")
+ = dropdown_filter("Search labels")
+ = dropdown_content
+ - if @project
+ = dropdown_footer do
+ %ul.dropdown-footer-list
+ - if can? current_user, :admin_label, @project
+ %li
+ %a.dropdown-toggle-page{href: "#"}
+ Create new
+ %li
+ = link_to namespace_project_labels_path(@project.namespace, @project) do
+ - if can? current_user, :admin_label, @project
+ Manage labels
+ - else
+ View labels
= render "shared/issuable/participants", participants: issuable.participants(current_user)
- %hr
- if current_user
- subscribed = issuable.subscribed?(current_user)
.block.light.subscription{data: {url: toggle_subscription_path(issuable)}}
.sidebar-collapsed-icon
= icon('rss')
.title.hide-collapsed
- %label.light Notifications
+ Notifications
- subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
%button.btn.btn-block.btn-gray.subscribe-button.hide-collapsed{:type => 'button'}
%span= subscribed ? 'Unsubscribe' : 'Subscribe'
@@ -124,5 +146,8 @@
= clipboard_button(clipboard_text: project_ref)
:javascript
- new Subscription('.subscription');
- new IssuableContext();
+ new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}');
+ new LabelsSelect();
+ new IssuableContext('#{current_user.to_json(only: [:username, :id, :name])}');
+ new Subscription('.subscription')
+ new Sidebar(); \ No newline at end of file
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index f7c6fc14adf..e1127b2311c 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -10,6 +10,8 @@
%strong #{project.name} &middot;
- elsif show_full_project_name
%strong #{project.name_with_namespace} &middot;
+ - if issuable.is_a?(Issue)
+ = confidential_icon(issuable)
= link_to_gfm issuable.title, [project.namespace.becomes(Namespace), project, issuable], title: issuable.title
%div{class: 'issuable-detail'}
= link_to [project.namespace.becomes(Namespace), project, issuable] do
@@ -21,5 +23,5 @@
- if assignee
= link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: issuable.assignee_id, state: 'all' }),
- class: 'has_tooltip', data: { 'original-title' => "Assigned to #{sanitize(assignee.name)}", container: 'body' } do
+ class: 'has-tooltip', data: { 'original-title' => "Assigned to #{sanitize(assignee.name)}", container: 'body' } do
- image_tag(avatar_icon(issuable.assignee, 16), class: "avatar s16", alt: '')
diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml
index ba27bafd1bc..868b2357003 100644
--- a/app/views/shared/milestones/_labels_tab.html.haml
+++ b/app/views/shared/milestones/_labels_tab.html.haml
@@ -5,7 +5,7 @@
%li
%span.label-row
= link_to milestones_label_path(options) do
- - render_colored_label(label)
+ - render_colored_label(label, tooltip: false)
%span.prepend-left-10
= markdown(label.description, pipeline: :single_line)
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index f01138af3f0..6b25745c554 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -6,10 +6,10 @@
.col-sm-6
%strong= link_to_gfm truncate(milestone.title, length: 100), milestone_path
.col-sm-6
- .pull-right.light #{milestone.percent_complete}% complete
+ .pull-right.light #{milestone.percent_complete(current_user)}% complete
.row
.col-sm-6
- = link_to pluralize(milestone.issues.size, 'Issue'), issues_path
+ = link_to pluralize(milestone.issues_visible_to_user(current_user).size, 'Issue'), issues_path
&middot;
= link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path
.col-sm-6= milestone_progress_bar(milestone)
diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml
index 59d4ae29f79..385c6596606 100644
--- a/app/views/shared/milestones/_summary.html.haml
+++ b/app/views/shared/milestones/_summary.html.haml
@@ -3,15 +3,15 @@
.context.prepend-top-default
.milestone-summary
%h4 Progress
- %strong= milestone.issues.size
+ %strong= milestone.issues_visible_to_user(current_user).size
issues:
%span.milestone-stat
- %strong= milestone.issues.opened.size
+ %strong= milestone.issues_visible_to_user(current_user).opened.size
open and
- %strong= milestone.issues.closed.size
+ %strong= milestone.issues_visible_to_user(current_user).closed.size
closed
%span.milestone-stat
- %strong== #{milestone.percent_complete}%
+ %strong== #{milestone.percent_complete(current_user)}%
complete
%span.milestone-stat
diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml
index 57d7ee85a3b..2b6ce2d7e7a 100644
--- a/app/views/shared/milestones/_tabs.html.haml
+++ b/app/views/shared/milestones/_tabs.html.haml
@@ -2,7 +2,7 @@
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
Issues
- %span.badge= milestone.issues.size
+ %span.badge= milestone.issues_visible_to_user(current_user).size
%li
= link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do
Merge Requests
@@ -21,7 +21,7 @@
.tab-content.milestone-content
.tab-pane.active#tab-issues
- = render 'shared/milestones/issues_tab', issues: milestone.issues, show_project_name: show_project_name, show_full_project_name: show_full_project_name
+ = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user), show_project_name: show_project_name, show_full_project_name: show_full_project_name
.tab-pane#tab-merge-requests
= render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name
.tab-pane#tab-participants
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index 4cf1d948b5b..cab8743a077 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -28,7 +28,7 @@
%h2.title
= markdown escape_once(milestone.title), pipeline: :single_line
-- if milestone.complete? && milestone.active?
+- if milestone.complete?(current_user) && milestone.active?
.alert.alert-success.prepend-top-default
- close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.'
%span All issues for this milestone are closed. #{close_msg}
@@ -47,7 +47,7 @@
- project_name = group ? ms.project.name : ms.project.name_with_namespace
= link_to project_name, namespace_project_milestone_path(ms.project.namespace, ms.project, ms)
%td
- = ms.issues.opened.count
+ = ms.issues_visible_to_user(current_user).opened.count
%td
- if ms.closed?
Closed
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 97cfb76cdb0..53ff8959bc8 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -7,26 +7,11 @@
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
- ci_commit = project.ci_commit(project.commit.sha) if ci && !project.empty_repo? && project.commit
-- cache_key = [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.2']
+- cache_key = [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.3']
- cache_key.push(ci_commit.status) if ci_commit
%li.project-row{ class: css_class }
= cache(cache_key) do
- = link_to project_path(project), class: dom_class(project) do
- - if avatar
- .dash-project-avatar
- - if use_creator_avatar
- = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
- - else
- = project_icon(project, alt: '', class: 'avatar project-avatar s40')
- %span.project-full-name.title
- %span.namespace-name
- - if project.namespace && !skip_namespace
- = project.namespace.human_name
- \/
- %span.project-name.filter-title
- = project.name
-
.controls
- if project.main_language
%span
@@ -42,9 +27,25 @@
%span
= icon('star')
= project.star_count
- %span.visibility-icon.has_tooltip{data: { container: 'body', placement: 'left' },
- title: "#{visibility_level_label(project.visibility_level)} - #{project_visibility_level_description(project.visibility_level)}"}
+ %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project)}
= visibility_level_icon(project.visibility_level, fw: false)
+
+ .title
+ = link_to project_path(project), class: dom_class(project) do
+ - if avatar
+ .dash-project-avatar
+ - if use_creator_avatar
+ = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
+ - else
+ = project_icon(project, alt: '', class: 'avatar project-avatar s40')
+ %span.project-full-name
+ %span.namespace-name
+ - if project.namespace && !skip_namespace
+ = project.namespace.human_name
+ \/
+ %span.project-name.filter-title
+ = project.name
+
- if show_last_commit_as_description
.description
= link_to_gfm project.commit.title, namespace_project_commit_path(project.namespace, project, project.commit),
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index aa5acee9c14..3c445f67236 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -1,5 +1,5 @@
.detail-page-header
- .snippet-box.has_tooltip{class: visibility_level_color(@snippet.visibility_level), title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: 'body' }}
+ .snippet-box.has-tooltip{class: visibility_level_color(@snippet.visibility_level), title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: 'body' }}
= visibility_level_icon(@snippet.visibility_level, fw: false)
= visibility_level_label(@snippet.visibility_level)
%span.identifier
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index a316a085107..c96dfefe17f 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -1,8 +1,8 @@
%li.snippet-row
= image_tag avatar_icon(snippet.author_email), class: "avatar s40 hidden-xs", alt: ''
- .snippet-title
- = link_to reliable_snippet_path(snippet), class: 'title' do
+ .title
+ = link_to reliable_snippet_path(snippet) do
= truncate(snippet.title, length: 60)
- if snippet.private?
%span.label.label-gray
diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml
index 20d2d5f317b..02647229776 100644
--- a/app/views/votes/_votes_block.html.haml
+++ b/app/views/votes/_votes_block.html.haml
@@ -1,6 +1,6 @@
.awards.votes-block
- awards_sort(votable.notes.awards.grouped_awards).each do |emoji, notes|
- %button.btn.award-control.js-emoji-btn.has_tooltip{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user), data: {placement: "top"}}
+ %button.btn.award-control.js-emoji-btn.has-tooltip{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user), data: {placement: "top"}}
= emoji_icon(emoji)
%span.award-control-text.js-counter
= notes.count
diff --git a/app/workers/gitlab_shell_one_shot_worker.rb b/app/workers/gitlab_shell_one_shot_worker.rb
new file mode 100644
index 00000000000..4ddbcf574d5
--- /dev/null
+++ b/app/workers/gitlab_shell_one_shot_worker.rb
@@ -0,0 +1,10 @@
+class GitlabShellOneShotWorker
+ include Sidekiq::Worker
+ include Gitlab::ShellAdapter
+
+ sidekiq_options queue: :gitlab_shell, retry: false
+
+ def perform(action, *arg)
+ gitlab_shell.send(action, *arg)
+ end
+end
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index 55cb6af232e..ccefd0f71a0 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -5,6 +5,9 @@ class ProjectCacheWorker
def perform(project_id)
project = Project.find(project_id)
+
+ return unless project.repository.exists?
+
project.update_repository_size
project.update_commit_count
diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb
index d06e4480292..b51c6a266c9 100644
--- a/app/workers/project_destroy_worker.rb
+++ b/app/workers/project_destroy_worker.rb
@@ -5,7 +5,7 @@ class ProjectDestroyWorker
def perform(project_id, user_id, params)
begin
- project = Project.find(project_id)
+ project = Project.unscoped.find(project_id)
rescue ActiveRecord::RecordNotFound
return
end
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index 21d311579e3..f9e32337983 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -20,14 +20,15 @@ class RepositoryForkWorker
return
end
+ project.repository.after_import
+
unless project.valid_repo?
- logger.error("Project #{id} had an invalid repository after fork")
+ logger.error("Project #{project_id} had an invalid repository after fork")
project.update(import_error: "The forked repository is invalid.")
project.import_fail
return
end
- project.repository.after_import
project.import_finish
end
end
diff --git a/config/application.rb b/config/application.rb
index 2b103c4592d..5a0ac70aa2a 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -49,6 +49,7 @@ module Gitlab
config.assets.paths << Gemojione.index.images_path
config.assets.precompile << "*.png"
config.assets.precompile << "print.css"
+ config.assets.precompile << "notify.css"
# Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0'
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 500b745f55e..fb1c3476f65 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -106,7 +106,7 @@ production: &base
enabled: false
# The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
- # The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
address: "gitlab-incoming+%{key}@gmail.com"
# Email account username
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 626268d7648..2b989015279 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -174,7 +174,6 @@ end
Settings.gitlab['time_zone'] ||= nil
Settings.gitlab['signup_enabled'] ||= true if Settings.gitlab['signup_enabled'].nil?
Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].nil?
-Settings.gitlab['twitter_sharing_enabled'] ||= true if Settings.gitlab['twitter_sharing_enabled'].nil?
Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], [])
Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil?
Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)' if Settings.gitlab['issue_closing_pattern'].nil?
diff --git a/config/initializers/premailer.rb b/config/initializers/premailer.rb
new file mode 100644
index 00000000000..a44316bc3a4
--- /dev/null
+++ b/config/initializers/premailer.rb
@@ -0,0 +1,7 @@
+# See https://github.com/fphilipe/premailer-rails#configuration
+Premailer::Rails.config.merge!(
+ generate_text_part: false,
+ preserve_styles: true,
+ remove_comments: true,
+ remove_ids: true
+)
diff --git a/config/mail_room.yml b/config/mail_room.yml
index aed55f74eab..60257329f3e 100644
--- a/config/mail_room.yml
+++ b/config/mail_room.yml
@@ -17,7 +17,7 @@ if File.exists?(config_file)
config['start_tls'] = false if config['start_tls'].nil?
config['mailbox'] = "inbox" if config['mailbox'].nil?
- if config['enabled'] && config['address'] && config['address'].include?('%{key}')
+ if config['enabled'] && config['address']
redis_url = Gitlab::RedisConfig.new(rails_env).url
%>
-
diff --git a/config/routes.rb b/config/routes.rb
index 2ae282f48a6..6bf22fb4456 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -16,6 +16,18 @@ Rails.application.routes.draw do
end
end
+ # Make the built-in Rails routes available in development, otherwise they'd
+ # get swallowed by the `namespace/project` route matcher below.
+ #
+ # See https://git.io/va79N
+ if Rails.env.development?
+ get '/rails/mailers' => 'rails/mailers#index'
+ get '/rails/mailers/:path' => 'rails/mailers#preview'
+ get '/rails/info/properties' => 'rails/info#properties'
+ get '/rails/info/routes' => 'rails/info#routes'
+ get '/rails/info' => 'rails/info#index'
+ end
+
namespace :ci do
# CI API
Ci::API::API.logger Rails.logger
@@ -354,6 +366,7 @@ Rails.application.routes.draw do
scope module: :dashboard do
resources :milestones, only: [:index, :show]
+ resources :labels, only: [:index]
resources :groups, only: [:index]
resources :snippets, only: [:index]
@@ -611,7 +624,7 @@ Rails.application.routes.draw do
end
end
- resources :merge_requests, constraints: { id: /\d+/ }, except: [:destroy] do
+ resources :merge_requests, constraints: { id: /\d+/ } do
member do
get :commits
get :diffs
@@ -621,6 +634,7 @@ Rails.application.routes.draw do
post :cancel_merge_when_build_succeeds
get :ci_status
post :toggle_subscription
+ post :remove_wip
end
collection do
@@ -681,7 +695,7 @@ Rails.application.routes.draw do
end
end
- resources :issues, constraints: { id: /\d+/ }, except: [:destroy] do
+ resources :issues, constraints: { id: /\d+/ } do
member do
post :toggle_subscription
end
diff --git a/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb b/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb
index 9fa96203ffd..99289166e81 100644
--- a/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb
+++ b/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb
@@ -1,14 +1,18 @@
class ConvertClosedToStateInIssue < ActiveRecord::Migration
+ include Gitlab::Database
+
def up
- Issue.transaction do
- Issue.where(closed: true).update_all(state: :closed)
- Issue.where(closed: false).update_all(state: :opened)
- end
+ execute "UPDATE #{table_name} SET state = 'closed' WHERE closed = #{true_value}"
+ execute "UPDATE #{table_name} SET state = 'opened' WHERE closed = #{false_value}"
end
def down
- Issue.transaction do
- Issue.where(state: :closed).update_all(closed: true)
- end
+ execute "UPDATE #{table_name} SET closed = #{true_value} WHERE state = 'closed'"
+ end
+
+ private
+
+ def table_name
+ Issue.table_name
end
end
diff --git a/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb b/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb
index ebb7ae585e6..bd1e016d679 100644
--- a/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb
+++ b/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb
@@ -1,16 +1,20 @@
class ConvertClosedToStateInMergeRequest < ActiveRecord::Migration
+ include Gitlab::Database
+
def up
- MergeRequest.transaction do
- MergeRequest.where(closed: true, merged: true).update_all(state: :merged)
- MergeRequest.where(closed: true, merged: false).update_all(state: :closed)
- MergeRequest.where(closed: false).update_all(state: :opened)
- end
+ execute "UPDATE #{table_name} SET state = 'merged' WHERE closed = #{true_value} AND merged = #{true_value}"
+ execute "UPDATE #{table_name} SET state = 'closed' WHERE closed = #{true_value} AND merged = #{false_value}"
+ execute "UPDATE #{table_name} SET state = 'opened' WHERE closed = #{false_value}"
end
def down
- MergeRequest.transaction do
- MergeRequest.where(state: :closed).update_all(closed: true)
- MergeRequest.where(state: :merged).update_all(closed: true, merged: true)
- end
+ execute "UPDATE #{table_name} SET closed = #{true_value} WHERE state = 'closed'"
+ execute "UPDATE #{table_name} SET closed = #{true_value}, merged = #{true_value} WHERE state = 'merged'"
+ end
+
+ private
+
+ def table_name
+ MergeRequest.table_name
end
end
diff --git a/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb b/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb
index 1978ea89153..d1174bc3d98 100644
--- a/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb
+++ b/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb
@@ -1,14 +1,18 @@
class ConvertClosedToStateInMilestone < ActiveRecord::Migration
+ include Gitlab::Database
+
def up
- Milestone.transaction do
- Milestone.where(closed: true).update_all(state: :closed)
- Milestone.where(closed: false).update_all(state: :active)
- end
+ execute "UPDATE #{table_name} SET state = 'closed' WHERE closed = #{true_value}"
+ execute "UPDATE #{table_name} SET state = 'active' WHERE closed = #{false_value}"
end
def down
- Milestone.transaction do
- Milestone.where(state: :closed).update_all(closed: true)
- end
+ execute "UPDATE #{table_name} SET closed = #{true_value} WHERE state = 'cloesd'"
+ end
+
+ private
+
+ def table_name
+ Milestone.table_name
end
end
diff --git a/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb b/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb
index b310b35e373..1c758c56ffe 100644
--- a/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb
+++ b/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb
@@ -1,17 +1,19 @@
class ConvertMergeStatusInMergeRequest < ActiveRecord::Migration
def up
- MergeRequest.transaction do
- MergeRequest.where(merge_status: 1).update_all("new_merge_status = 'unchecked'")
- MergeRequest.where(merge_status: 2).update_all("new_merge_status = 'can_be_merged'")
- MergeRequest.where(merge_status: 3).update_all("new_merge_status = 'cannot_be_merged'")
- end
+ execute "UPDATE #{table_name} SET new_merge_status = 'unchecked' WHERE merge_status = 1"
+ execute "UPDATE #{table_name} SET new_merge_status = 'can_be_merged' WHERE merge_status = 2"
+ execute "UPDATE #{table_name} SET new_merge_status = 'cannot_be_merged' WHERE merge_status = 3"
end
def down
- MergeRequest.transaction do
- MergeRequest.where(new_merge_status: :unchecked).update_all("merge_status = 1")
- MergeRequest.where(new_merge_status: :can_be_merged).update_all("merge_status = 2")
- MergeRequest.where(new_merge_status: :cannot_be_merged).update_all("merge_status = 3")
- end
+ execute "UPDATE #{table_name} SET merge_status = 1 WHERE new_merge_status = 'unchecked'"
+ execute "UPDATE #{table_name} SET merge_status = 2 WHERE new_merge_status = 'can_be_merged'"
+ execute "UPDATE #{table_name} SET merge_status = 3 WHERE new_merge_status = 'cannot_be_merged'"
+ end
+
+ private
+
+ def table_name
+ MergeRequest.table_name
end
end
diff --git a/db/migrate/20130419190306_allow_merges_for_forks.rb b/db/migrate/20130419190306_allow_merges_for_forks.rb
index 56ce58a846d..56ea97e8561 100644
--- a/db/migrate/20130419190306_allow_merges_for_forks.rb
+++ b/db/migrate/20130419190306_allow_merges_for_forks.rb
@@ -1,7 +1,7 @@
class AllowMergesForForks < ActiveRecord::Migration
def self.up
add_column :merge_requests, :target_project_id, :integer, :null => true
- MergeRequest.update_all("target_project_id = project_id")
+ execute "UPDATE #{table_name} SET target_project_id = project_id"
change_column :merge_requests, :target_project_id, :integer, :null => false
rename_column :merge_requests, :project_id, :source_project_id
end
@@ -10,4 +10,10 @@ class AllowMergesForForks < ActiveRecord::Migration
remove_column :merge_requests, :target_project_id
rename_column :merge_requests, :source_project_id,:project_id
end
+
+ private
+
+ def table_name
+ MergeRequest.table_name
+ end
end
diff --git a/db/migrate/20160223192159_add_confidential_to_issues.rb b/db/migrate/20160223192159_add_confidential_to_issues.rb
new file mode 100644
index 00000000000..e9d47fd589a
--- /dev/null
+++ b/db/migrate/20160223192159_add_confidential_to_issues.rb
@@ -0,0 +1,6 @@
+class AddConfidentialToIssues < ActiveRecord::Migration
+ def change
+ add_column :issues, :confidential, :boolean, default: false
+ add_index :issues, :confidential
+ end
+end
diff --git a/db/migrate/20160225090018_add_delete_at_to_issues.rb b/db/migrate/20160225090018_add_delete_at_to_issues.rb
new file mode 100644
index 00000000000..3ddbef92978
--- /dev/null
+++ b/db/migrate/20160225090018_add_delete_at_to_issues.rb
@@ -0,0 +1,6 @@
+class AddDeleteAtToIssues < ActiveRecord::Migration
+ def change
+ add_column :issues, :deleted_at, :datetime
+ add_index :issues, :deleted_at
+ end
+end
diff --git a/db/migrate/20160225101956_add_delete_at_to_merge_requests.rb b/db/migrate/20160225101956_add_delete_at_to_merge_requests.rb
new file mode 100644
index 00000000000..9d09105f17d
--- /dev/null
+++ b/db/migrate/20160225101956_add_delete_at_to_merge_requests.rb
@@ -0,0 +1,6 @@
+class AddDeleteAtToMergeRequests < ActiveRecord::Migration
+ def change
+ add_column :merge_requests, :deleted_at, :datetime
+ add_index :merge_requests, :deleted_at
+ end
+end
diff --git a/db/migrate/20160301124843_add_visibility_level_to_groups.rb b/db/migrate/20160301124843_add_visibility_level_to_groups.rb
new file mode 100644
index 00000000000..d1b921bb208
--- /dev/null
+++ b/db/migrate/20160301124843_add_visibility_level_to_groups.rb
@@ -0,0 +1,29 @@
+class AddVisibilityLevelToGroups < ActiveRecord::Migration
+ def up
+ add_column :namespaces, :visibility_level, :integer, null: false, default: Gitlab::VisibilityLevel::PUBLIC
+ add_index :namespaces, :visibility_level
+
+ # Unfortunately, this is needed on top of the `default`, since we don't want the configuration specific
+ # `allowed_visibility_level` to end up in schema.rb
+ if allowed_visibility_level < Gitlab::VisibilityLevel::PUBLIC
+ execute("UPDATE namespaces SET visibility_level = #{allowed_visibility_level}")
+ end
+ end
+
+ def down
+ remove_column :namespaces, :visibility_level
+ end
+
+ private
+
+ def allowed_visibility_level
+ application_settings = select_one("SELECT restricted_visibility_levels FROM application_settings ORDER BY id DESC LIMIT 1")
+ if application_settings
+ restricted_visibility_levels = YAML.safe_load(application_settings["restricted_visibility_levels"]) rescue nil
+ end
+ restricted_visibility_levels ||= []
+
+ allowed_levels = Gitlab::VisibilityLevel.values - restricted_visibility_levels
+ allowed_levels.max
+ end
+end
diff --git a/db/migrate/20160308212903_add_default_group_visibility_to_application_settings.rb b/db/migrate/20160308212903_add_default_group_visibility_to_application_settings.rb
new file mode 100644
index 00000000000..75de5f70fa2
--- /dev/null
+++ b/db/migrate/20160308212903_add_default_group_visibility_to_application_settings.rb
@@ -0,0 +1,29 @@
+# Create visibility level field on DB
+# Sets default_visibility_level to value on settings if not restricted
+# If value is restricted takes higher visibility level allowed
+
+class AddDefaultGroupVisibilityToApplicationSettings < ActiveRecord::Migration
+ def up
+ add_column :application_settings, :default_group_visibility, :integer
+ # Unfortunately, this can't be a `default`, since we don't want the configuration specific
+ # `allowed_visibility_level` to end up in schema.rb
+ execute("UPDATE application_settings SET default_group_visibility = #{allowed_visibility_level}")
+ end
+
+ def down
+ remove_column :application_settings, :default_group_visibility
+ end
+
+ private
+
+ def allowed_visibility_level
+ application_settings = select_one("SELECT restricted_visibility_levels FROM application_settings ORDER BY id DESC LIMIT 1")
+ if application_settings
+ restricted_visibility_levels = YAML.safe_load(application_settings["restricted_visibility_levels"]) rescue nil
+ end
+ restricted_visibility_levels ||= []
+
+ allowed_levels = Gitlab::VisibilityLevel.values - restricted_visibility_levels
+ allowed_levels.max
+ end
+end
diff --git a/db/migrate/20160316192622_change_target_id_to_null_on_todos.rb b/db/migrate/20160316192622_change_target_id_to_null_on_todos.rb
new file mode 100644
index 00000000000..6871b3920df
--- /dev/null
+++ b/db/migrate/20160316192622_change_target_id_to_null_on_todos.rb
@@ -0,0 +1,5 @@
+class ChangeTargetIdToNullOnTodos < ActiveRecord::Migration
+ def change
+ change_column_null :todos, :target_id, true
+ end
+end
diff --git a/db/migrate/20160316204731_add_commit_id_to_todos.rb b/db/migrate/20160316204731_add_commit_id_to_todos.rb
new file mode 100644
index 00000000000..ae19fdd1abd
--- /dev/null
+++ b/db/migrate/20160316204731_add_commit_id_to_todos.rb
@@ -0,0 +1,6 @@
+class AddCommitIdToTodos < ActiveRecord::Migration
+ def change
+ add_column :todos, :commit_id, :string
+ add_index :todos, :commit_id
+ end
+end
diff --git a/db/migrate/20160317092222_add_moved_to_to_issue.rb b/db/migrate/20160317092222_add_moved_to_to_issue.rb
new file mode 100644
index 00000000000..461e7fb3a9b
--- /dev/null
+++ b/db/migrate/20160317092222_add_moved_to_to_issue.rb
@@ -0,0 +1,5 @@
+class AddMovedToToIssue < ActiveRecord::Migration
+ def change
+ add_reference :issues, :moved_to, references: :issues
+ end
+end
diff --git a/db/migrate/20160320204112_index_namespaces_on_visibility_level.rb b/db/migrate/20160320204112_index_namespaces_on_visibility_level.rb
new file mode 100644
index 00000000000..370b339d45c
--- /dev/null
+++ b/db/migrate/20160320204112_index_namespaces_on_visibility_level.rb
@@ -0,0 +1,7 @@
+class IndexNamespacesOnVisibilityLevel < ActiveRecord::Migration
+ def change
+ unless index_exists?(:namespaces, :visibility_level)
+ add_index :namespaces, :visibility_level
+ end
+ end
+end
diff --git a/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb b/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb
new file mode 100644
index 00000000000..1fff9759d1e
--- /dev/null
+++ b/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb
@@ -0,0 +1,17 @@
+class RemoveTodosForDeletedIssues < ActiveRecord::Migration
+ def up
+ execute <<-SQL
+ DELETE FROM todos
+ WHERE todos.target_type = 'Issue'
+ AND NOT EXISTS (
+ SELECT *
+ FROM issues
+ WHERE issues.id = todos.target_id
+ AND issues.deleted_at IS NULL
+ )
+ SQL
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb b/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb
new file mode 100644
index 00000000000..275554e736e
--- /dev/null
+++ b/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb
@@ -0,0 +1,6 @@
+class AddIndexOnPendingDeleteProjects < ActiveRecord::Migration
+ def change
+ add_index :projects, :pending_delete
+ end
+end
+
diff --git a/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb b/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb
new file mode 100644
index 00000000000..54cea964ff2
--- /dev/null
+++ b/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb
@@ -0,0 +1,17 @@
+class RemoveTodosForDeletedMergeRequests < ActiveRecord::Migration
+ def up
+ execute <<-SQL
+ DELETE FROM todos
+ WHERE todos.target_type = 'MergeRequest'
+ AND NOT EXISTS (
+ SELECT *
+ FROM merge_requests
+ WHERE merge_requests.id = todos.target_id
+ AND merge_requests.deleted_at IS NULL
+ )
+ SQL
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb b/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb
new file mode 100644
index 00000000000..0d736e323b6
--- /dev/null
+++ b/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb
@@ -0,0 +1,5 @@
+class RemoveTwitterSharingEnabledFromApplicationSettings < ActiveRecord::Migration
+ def change
+ remove_column :application_settings, :twitter_sharing_enabled, :boolean
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 2f075677b30..e63e22ce864 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: 20160316123110) do
+ActiveRecord::Schema.define(version: 20160331133914) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -77,6 +77,7 @@ ActiveRecord::Schema.define(version: 20160316123110) do
t.boolean "akismet_enabled", default: false
t.string "akismet_api_key"
t.boolean "email_author_in_body", default: false
+ t.integer "default_group_visibility"
end
create_table "audit_events", force: :cascade do |t|
@@ -416,12 +417,17 @@ ActiveRecord::Schema.define(version: 20160316123110) do
t.string "state"
t.integer "iid"
t.integer "updated_by_id"
+ t.integer "moved_to_id"
+ t.boolean "confidential", default: false
+ t.datetime "deleted_at"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree
+ add_index "issues", ["confidential"], name: "index_issues_on_confidential", using: :btree
add_index "issues", ["created_at", "id"], name: "index_issues_on_created_at_and_id", using: :btree
add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree
+ add_index "issues", ["deleted_at"], name: "index_issues_on_deleted_at", using: :btree
add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree
@@ -544,12 +550,14 @@ ActiveRecord::Schema.define(version: 20160316123110) do
t.boolean "merge_when_build_succeeds", default: false, null: false
t.integer "merge_user_id"
t.string "merge_commit_sha"
+ t.datetime "deleted_at"
end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
add_index "merge_requests", ["author_id"], name: "index_merge_requests_on_author_id", using: :btree
add_index "merge_requests", ["created_at", "id"], name: "index_merge_requests_on_created_at_and_id", using: :btree
add_index "merge_requests", ["created_at"], name: "index_merge_requests_on_created_at", using: :btree
+ add_index "merge_requests", ["deleted_at"], name: "index_merge_requests_on_deleted_at", using: :btree
add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree
add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree
@@ -588,6 +596,7 @@ ActiveRecord::Schema.define(version: 20160316123110) do
t.string "description", default: "", null: false
t.string "avatar"
t.boolean "share_with_group_lock", default: false
+ t.integer "visibility_level", default: 20, null: false
end
add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree
@@ -597,6 +606,7 @@ ActiveRecord::Schema.define(version: 20160316123110) do
add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, 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
+ add_index "namespaces", ["visibility_level"], name: "index_namespaces_on_visibility_level", using: :btree
create_table "notes", force: :cascade do |t|
t.text "note"
@@ -735,6 +745,7 @@ ActiveRecord::Schema.define(version: 20160316123110) do
add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree
add_index "projects", ["path"], name: "index_projects_on_path", using: :btree
add_index "projects", ["path"], name: "index_projects_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
+ add_index "projects", ["pending_delete"], name: "index_projects_on_pending_delete", using: :btree
add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree
add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree
add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree
@@ -865,7 +876,7 @@ ActiveRecord::Schema.define(version: 20160316123110) do
create_table "todos", force: :cascade do |t|
t.integer "user_id", null: false
t.integer "project_id", null: false
- t.integer "target_id", null: false
+ t.integer "target_id"
t.string "target_type", null: false
t.integer "author_id"
t.integer "action", null: false
@@ -873,9 +884,11 @@ ActiveRecord::Schema.define(version: 20160316123110) do
t.datetime "created_at"
t.datetime "updated_at"
t.integer "note_id"
+ t.string "commit_id"
end
add_index "todos", ["author_id"], name: "index_todos_on_author_id", using: :btree
+ add_index "todos", ["commit_id"], name: "index_todos_on_commit_id", using: :btree
add_index "todos", ["note_id"], name: "index_todos_on_note_id", using: :btree
add_index "todos", ["project_id"], name: "index_todos_on_project_id", using: :btree
add_index "todos", ["state"], name: "index_todos_on_state", using: :btree
diff --git a/doc/README.md b/doc/README.md
index 08d0a6a5bfb..724c7cca0f1 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -3,7 +3,7 @@
## User documentation
- [API](api/README.md) Automate GitLab via a simple and powerful API.
-- [CI](ci/README.md)
+- [CI](ci/README.md) GitLab Continuous Integration (CI) getting started, .gitlab-ci.yml options, and examples.
- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
- [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).
@@ -19,10 +19,12 @@
## Administrator documentation
+- [Authentication/Authorization](administration/auth/README.md) Configure
+ external authentication with LDAP, SAML, CAS and additional Omniauth providers.
- [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when webhooks aren't enough.
- [Install](install/README.md) Requirements, directory structures and installation from source.
- [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components
-- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, LDAP and Twitter.
+- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter.
- [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages.
- [Libravatar](customization/libravatar.md) Use Libravatar for user avatars.
- [Log system](logs/logs.md) Log system.
@@ -45,4 +47,3 @@
contributing to documentation.
- [Development](development/README.md) Explains the architecture and the guidelines for shell commands.
- [Legal](legal/README.md) Contributor license agreements.
-- [Release](release/README.md) How to make the monthly and security releases.
diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md
new file mode 100644
index 00000000000..07e548aaabe
--- /dev/null
+++ b/doc/administration/auth/README.md
@@ -0,0 +1,11 @@
+# Authentication and Authorization
+
+GitLab integrates with the following external authentication and authorization
+providers.
+
+- [LDAP](ldap.md) Includes Active Directory, Apple Open Directory, Open LDAP,
+ and 389 Server
+- [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google,
+ Bitbucket, Facebook, Shibboleth, Crowd and Azure
+- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider
+- [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS
diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md
new file mode 100644
index 00000000000..237700bbcd9
--- /dev/null
+++ b/doc/administration/auth/ldap.md
@@ -0,0 +1,277 @@
+# LDAP
+
+GitLab integrates with LDAP to support user authentication.
+This integration works with most LDAP-compliant directory
+servers, including Microsoft Active Directory, Apple Open Directory, Open LDAP,
+and 389 Server. GitLab EE includes enhanced integration, including group
+membership syncing.
+
+## Security
+
+GitLab assumes that LDAP users are not able to change their LDAP 'mail', 'email'
+or 'userPrincipalName' attribute. An LDAP user who is allowed to change their
+email on the LDAP server can potentially
+[take over any account](#enabling-ldap-sign-in-for-existing-gitlab-users)
+on your GitLab server.
+
+We recommend against using LDAP integration if your LDAP users are
+allowed to change their 'mail', 'email' or 'userPrincipalName' attribute on
+the LDAP server.
+
+### User deletion
+
+If a user is deleted from the LDAP server, they will be blocked in GitLab, as
+well. Users will be immediately blocked from logging in. However, there is an
+LDAP check cache time (sync time) of one hour (see note). This means users that
+are already logged in or are using Git over SSH will still be able to access
+GitLab for up to one hour. Manually block the user in the GitLab Admin area to
+immediately block all access.
+
+>**Note**: GitLab EE supports a configurable sync time, with a default
+of one hour.
+
+## Configuration
+
+To enable LDAP integration you need to add your LDAP server settings in
+`/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`.
+
+>**Note**: In GitLab EE, you can configure multiple LDAP servers to connect to
+one GitLab server.
+
+Prior to version 7.4, GitLab used a different syntax for configuring
+LDAP integration. The old LDAP integration syntax still works but may be
+removed in a future version. If your `gitlab.rb` or `gitlab.yml` file contains
+LDAP settings in both the old syntax and the new syntax, only the __old__
+syntax will be used by GitLab.
+
+The configuration inside `gitlab_rails['ldap_servers']` below is sensitive to
+incorrect indentation. Be sure to retain the indentation given in the example.
+Copy/paste can sometimes cause problems.
+
+**Omnibus configuration**
+
+```ruby
+gitlab_rails['ldap_enabled'] = true
+gitlab_rails['ldap_servers'] = YAML.load <<-EOS # remember to close this block with 'EOS' below
+main: # 'main' is the GitLab 'provider ID' of this LDAP server
+ ## label
+ #
+ # A human-friendly name for your LDAP server. It is OK to change the label later,
+ # for instance if you find out it is too large to fit on the web page.
+ #
+ # Example: 'Paris' or 'Acme, Ltd.'
+ label: 'LDAP'
+
+ host: '_your_ldap_server'
+ port: 389
+ uid: 'sAMAccountName'
+ method: 'plain' # "tls" or "ssl" or "plain"
+ bind_dn: '_the_full_dn_of_the_user_you_will_bind_with'
+ password: '_the_password_of_the_bind_user'
+
+ # Set a timeout, in seconds, for LDAP queries. This helps avoid blocking
+ # a request if the LDAP server becomes unresponsive.
+ # A value of 0 means there is no timeout.
+ timeout: 10
+
+ # This setting specifies if LDAP server is Active Directory LDAP server.
+ # For non AD servers it skips the AD specific queries.
+ # If your LDAP server is not AD, set this to false.
+ active_directory: true
+
+ # If allow_username_or_email_login is enabled, GitLab will ignore everything
+ # after the first '@' in the LDAP username submitted by the user on login.
+ #
+ # Example:
+ # - the user enters 'jane.doe@example.com' and 'p@ssw0rd' as LDAP credentials;
+ # - GitLab queries the LDAP server with 'jane.doe' and 'p@ssw0rd'.
+ #
+ # If you are using "uid: 'userPrincipalName'" on ActiveDirectory you need to
+ # disable this setting, because the userPrincipalName contains an '@'.
+ allow_username_or_email_login: false
+
+ # To maintain tight control over the number of active users on your GitLab installation,
+ # enable this setting to keep new users blocked until they have been cleared by the admin
+ # (default: false).
+ block_auto_created_users: false
+
+ # Base where we can search for users
+ #
+ # Ex. ou=People,dc=gitlab,dc=example
+ #
+ base: ''
+
+ # Filter LDAP users
+ #
+ # Format: RFC 4515 https://tools.ietf.org/search/rfc4515
+ # Ex. (employeeType=developer)
+ #
+ # Note: GitLab does not support omniauth-ldap's custom filter syntax.
+ #
+ user_filter: ''
+
+ # LDAP attributes that GitLab will use to create an account for the LDAP user.
+ # The specified attribute can either be the attribute name as a string (e.g. 'mail'),
+ # or an array of attribute names to try in order (e.g. ['mail', 'email']).
+ # Note that the user's LDAP login will always be the attribute specified as `uid` above.
+ attributes:
+ # The username will be used in paths for the user's own projects
+ # (like `gitlab.example.com/username/project`) and when mentioning
+ # them in issues, merge request and comments (like `@username`).
+ # If the attribute specified for `username` contains an email address,
+ # the GitLab username will be the part of the email address before the '@'.
+ username: ['uid', 'userid', 'sAMAccountName']
+ email: ['mail', 'email', 'userPrincipalName']
+
+ # If no full name could be found at the attribute specified for `name`,
+ # the full name is determined using the attributes specified for
+ # `first_name` and `last_name`.
+ name: 'cn'
+ first_name: 'givenName'
+ last_name: 'sn'
+
+ ## EE only
+
+ # Base where we can search for groups
+ #
+ # Ex. ou=groups,dc=gitlab,dc=example
+ #
+ group_base: ''
+
+ # The CN of a group containing GitLab administrators
+ #
+ # Ex. administrators
+ #
+ # Note: Not `cn=administrators` or the full DN
+ #
+ admin_group: ''
+
+ # The LDAP attribute containing a user's public SSH key
+ #
+ # Ex. ssh_public_key
+ #
+ sync_ssh_keys: false
+
+# GitLab EE only: add more LDAP servers
+# Choose an ID made of a-z and 0-9 . This ID will be stored in the database
+# so that GitLab can remember which LDAP server a user belongs to.
+# uswest2:
+# label:
+# host:
+# ....
+EOS
+```
+
+**Source configuration**
+
+Use the same format as `gitlab_rails['ldap_servers']` for the contents under
+`servers:` in the example below:
+
+```
+production:
+ # snip...
+ ldap:
+ enabled: false
+ servers:
+ main: # 'main' is the GitLab 'provider ID' of this LDAP server
+ ## label
+ #
+ # A human-friendly name for your LDAP server. It is OK to change the label later,
+ # for instance if you find out it is too large to fit on the web page.
+ #
+ # Example: 'Paris' or 'Acme, Ltd.'
+ label: 'LDAP'
+ # snip...
+```
+
+## Using an LDAP filter to limit access to your GitLab server
+
+If you want to limit all GitLab access to a subset of the LDAP users on your
+LDAP server, the first step should be to narrow the configured `base`. However,
+it is sometimes necessary to filter users further. In this case, you can set up
+an LDAP user filter. The filter must comply with
+[RFC 4515](https://tools.ietf.org/search/rfc4515).
+
+**Omnibus configuration**
+
+```ruby
+gitlab_rails['ldap_servers'] = YAML.load <<-EOS
+main:
+ # snip...
+ user_filter: '(employeeType=developer)'
+EOS
+```
+
+**Source configuration**
+
+```yaml
+production:
+ ldap:
+ servers:
+ main:
+ # snip...
+ user_filter: '(employeeType=developer)'
+```
+
+Tip: If you want to limit access to the nested members of an Active Directory
+group you can use the following syntax:
+
+```
+(memberOf:1.2.840.113556.1.4.1941:=CN=My Group,DC=Example,DC=com)
+```
+
+Please note that GitLab does not support the custom filter syntax used by
+omniauth-ldap.
+
+## Enabling LDAP sign-in for existing GitLab users
+
+When a user signs in to GitLab with LDAP for the first time, and their LDAP
+email address is the primary email address of an existing GitLab user, then
+the LDAP DN will be associated with the existing user. If the LDAP email
+attribute is not found in GitLab's database, a new user is created.
+
+In other words, if an existing GitLab user wants to enable LDAP sign-in for
+themselves, they should check that their GitLab email address matches their
+LDAP email address, and then sign into GitLab via their LDAP credentials.
+
+## Limitations
+
+### TLS Client Authentication
+
+Not implemented by `Net::LDAP`.
+You should disable anonymous LDAP authentication and enable simple or SASL
+authentication. The TLS client authentication setting in your LDAP server cannot
+be mandatory and clients cannot be authenticated with the TLS protocol.
+
+### TLS Server Authentication
+
+Not supported by GitLab's configuration options.
+When setting `method: ssl`, the underlying authentication method used by
+`omniauth-ldap` is `simple_tls`. This method establishes TLS encryption with
+the LDAP server before any LDAP-protocol data is exchanged but no validation of
+the LDAP server's SSL certificate is performed.
+
+## Troubleshooting
+
+### Invalid credentials when logging in
+
+- Make sure the user you are binding with has enough permissions to read the user's
+tree and traverse it.
+- Check that the `user_filter` is not blocking otherwise valid users.
+- Run the following check command to make sure that the LDAP settings are
+ correct and GitLab can see your users:
+
+ ```bash
+ # For Omnibus installations
+ sudo gitlab-rake gitlab:ldap:check
+
+ # For installations from source
+ sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production
+ ```
+
+### Connection Refused
+
+If you are getting 'Connection Refused' errors when trying to connect to the
+LDAP server please double-check the LDAP `port` and `method` settings used by
+GitLab. Common combinations are `method: 'plain'` and `port: 389`, OR
+`method: 'ssl'` and `port: 636`.
diff --git a/doc/api/groups.md b/doc/api/groups.md
index d47e79ba47f..d1b5c9f5f04 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -111,6 +111,7 @@ Parameters:
- `name` (required) - The name of the group
- `path` (required) - The path of the group
- `description` (optional) - The group's description
+- `visibility_level` (optional) - The group's visibility. 0 for private, 10 for internal, 20 for public.
## Transfer project to group
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 9e704648b25..cc6355d34ef 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -237,6 +237,7 @@ POST /projects/:id/issues
| `assignee_id` | integer | no | The ID of a user to assign issue |
| `milestone_id` | integer | no | The ID of a milestone to assign issue |
| `labels` | string | no | Comma-separated label names for an issue |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` |
```bash
curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues?title=Issues%20with%20auth&labels=bug
@@ -326,17 +327,25 @@ Example response:
}
```
-## Delete existing issue (**Deprecated**)
+## Delete an issue
-This call is deprecated and returns a `405 Method Not Allowed` error if called.
-An issue gets now closed and is done by calling
-`PUT /projects/:id/issues/:issue_id` with the parameter `state_event` set to
-`close`. See [edit issue](#edit-issue) for more details.
+Only for admins and project owners. Soft deletes the issue in question.
+If the operation is successful, a status code `200` is returned. In case you cannot
+destroy this issue, or it is not present, code `404` is given.
```
DELETE /projects/:id/issues/:issue_id
```
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of a project's issue |
+
+```bash
+curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85
+```
+
## Comments on issues
Comments are done via the [notes](notes.md) resource.
diff --git a/doc/api/labels.md b/doc/api/labels.md
index 6496ffe9fd1..544e898b6aa 100644
--- a/doc/api/labels.md
+++ b/doc/api/labels.md
@@ -8,9 +8,9 @@ Get all labels for a given project.
GET /projects/:id/labels
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the project |
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | --------------------- |
+| `id` | integer | yes | The ID of the project |
```bash
curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/labels
@@ -22,35 +22,43 @@ Example response:
[
{
"name" : "bug",
- "color" : "#d9534f"
+ "color" : "#d9534f",
+ "description": "Bug reported by user"
},
{
"color" : "#d9534f",
- "name" : "confirmed"
+ "name" : "confirmed",
+ "description": "Confirmed issue"
},
{
"name" : "critical",
- "color" : "#d9534f"
+ "color" : "#d9534f",
+ "description": "Criticalissue. Need fix ASAP"
},
{
"color" : "#428bca",
- "name" : "discussion"
+ "name" : "discussion",
+ "description": "Issue that needs further discussion"
},
{
"name" : "documentation",
- "color" : "#f0ad4e"
+ "color" : "#f0ad4e",
+ "description": "Issue about documentation"
},
{
"color" : "#5cb85c",
- "name" : "enhancement"
+ "name" : "enhancement",
+ "description": "Enhancement proposal"
},
{
"color" : "#428bca",
- "name" : "suggestion"
+ "name" : "suggestion",
+ "description": "Suggestion"
},
{
"color" : "#f0ad4e",
- "name" : "support"
+ "name" : "support",
+ "description": "Support issue"
}
]
```
@@ -66,11 +74,12 @@ and 409 if the label already exists.
POST /projects/:id/labels
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the project |
-| `name` | string | yes | The name of the label |
-| `color` | string | yes | The color of the label in 6-digit hex notation with leading `#` sign |
+| Attribute | Type | Required | Description |
+| ------------- | ------- | -------- | ---------------------------- |
+| `id` | integer | yes | The ID of the project |
+| `name` | string | yes | The name of the label |
+| `color` | string | yes | The color of the label in 6-digit hex notation with leading `#` sign |
+| `description` | string | no | The description of the label |
```bash
curl --data "name=feature&color=#5843AD" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels"
@@ -81,7 +90,8 @@ Example response:
```json
{
"name" : "feature",
- "color" : "#5843AD"
+ "color" : "#5843AD",
+ "description":null
}
```
@@ -97,10 +107,10 @@ In case of an error, an additional error message is returned.
DELETE /projects/:id/labels
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the project |
-| `name` | string | yes | The name of the label |
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | --------------------- |
+| `id` | integer | yes | The ID of the project |
+| `name` | string | yes | The name of the label |
```bash
curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels?name=bug"
@@ -112,6 +122,7 @@ Example response:
{
"title" : "feature",
"color" : "#5843AD",
+ "description": "New feature proposal",
"updated_at" : "2015-11-03T21:22:30.737Z",
"template" : false,
"project_id" : 1,
@@ -133,15 +144,16 @@ In case of an error, an additional error message is returned.
PUT /projects/:id/labels
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the project |
-| `name` | string | yes | The name of the existing label |
-| `new_name` | string | yes if `color` if not provided | The new name of the label |
-| `color` | string | yes if `new_name` is not provided | The new color of the label in 6-digit hex notation with leading `#` sign |
+| Attribute | Type | Required | Description |
+| --------------- | ------- | --------------------------------- | ------------------------------- |
+| `id` | integer | yes | The ID of the project |
+| `name` | string | yes | The name of the existing label |
+| `new_name` | string | yes if `color` if not provided | The new name of the label |
+| `color` | string | yes if `new_name` is not provided | The new color of the label in 6-digit hex notation with leading `#` sign |
+| `description` | string | no | The new description of the label |
```bash
-curl -X PUT --data "name=documentation&new_name=docs&color=#8E44AD" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels"
+curl -X PUT --data "name=documentation&new_name=docs&color=#8E44AD&description=Documentation" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels"
```
Example response:
@@ -149,6 +161,7 @@ Example response:
```json
{
"color" : "#8E44AD",
- "name" : "docs"
+ "name" : "docs",
+ "description": "Documentation"
}
```
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 5c527d55481..b20a6300b7a 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -380,6 +380,25 @@ Parameters:
If the operation is successful, 200 and the updated merge request is returned.
If an error occurs, an error number and a message explaining the reason is returned.
+## Delete a merge request
+
+Only for admins and project owners. Soft deletes the merge request in question.
+If the operation is successful, a status code `200` is returned. In case you cannot
+destroy this merge request, or it is not present, code `404` is given.
+
+```
+DELETE /projects/:id/merge_requests/:merge_request_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `merge_request_id` | integer | yes | The ID of a project's merge request |
+
+```bash
+curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_request/85
+```
+
## Accept MR
Merge changes submitted with MR using this API.
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 3703f4b327a..3a909a2bc87 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -491,6 +491,172 @@ Parameters:
- `id` (required) - The ID of the project to be forked
+### Archive a project
+
+Archives the project if the user is either admin or the project owner of this project. This action is
+idempotent, thus archiving an already archived project will not change the project.
+
+Status code 201 with the project as body is given when successful, in case the user doesn't
+have the proper access rights, code 403 is returned. Status 404 is returned if the project
+doesn't exist, or is hidden to the user.
+
+```
+POST /projects/:id/archive
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the project |
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/archive"
+```
+
+Example response:
+
+```json
+{
+ "id": 3,
+ "description": null,
+ "default_branch": "master",
+ "public": false,
+ "visibility_level": 0,
+ "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
+ "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
+ "web_url": "http://example.com/diaspora/diaspora-project-site",
+ "tag_list": [
+ "example",
+ "disapora project"
+ ],
+ "owner": {
+ "id": 3,
+ "name": "Diaspora",
+ "created_at": "2013-09-30T13: 46: 02Z"
+ },
+ "name": "Diaspora Project Site",
+ "name_with_namespace": "Diaspora / Diaspora Project Site",
+ "path": "diaspora-project-site",
+ "path_with_namespace": "diaspora/diaspora-project-site",
+ "issues_enabled": true,
+ "open_issues_count": 1,
+ "merge_requests_enabled": true,
+ "builds_enabled": true,
+ "wiki_enabled": true,
+ "snippets_enabled": false,
+ "created_at": "2013-09-30T13: 46: 02Z",
+ "last_activity_at": "2013-09-30T13: 46: 02Z",
+ "creator_id": 3,
+ "namespace": {
+ "created_at": "2013-09-30T13: 46: 02Z",
+ "description": "",
+ "id": 3,
+ "name": "Diaspora",
+ "owner_id": 1,
+ "path": "diaspora",
+ "updated_at": "2013-09-30T13: 46: 02Z"
+ },
+ "permissions": {
+ "project_access": {
+ "access_level": 10,
+ "notification_level": 3
+ },
+ "group_access": {
+ "access_level": 50,
+ "notification_level": 3
+ }
+ },
+ "archived": true,
+ "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
+ "shared_runners_enabled": true,
+ "forks_count": 0,
+ "star_count": 0,
+ "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b"
+}
+```
+
+### Unarchive a project
+
+Unarchives the project if the user is either admin or the project owner of this project. This action is
+idempotent, thus unarchiving an non-archived project will not change the project.
+
+Status code 201 with the project as body is given when successful, in case the user doesn't
+have the proper access rights, code 403 is returned. Status 404 is returned if the project
+doesn't exist, or is hidden to the user.
+
+```
+POST /projects/:id/archive
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the project |
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/unarchive"
+```
+
+Example response:
+
+```json
+{
+ "id": 3,
+ "description": null,
+ "default_branch": "master",
+ "public": false,
+ "visibility_level": 0,
+ "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
+ "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
+ "web_url": "http://example.com/diaspora/diaspora-project-site",
+ "tag_list": [
+ "example",
+ "disapora project"
+ ],
+ "owner": {
+ "id": 3,
+ "name": "Diaspora",
+ "created_at": "2013-09-30T13: 46: 02Z"
+ },
+ "name": "Diaspora Project Site",
+ "name_with_namespace": "Diaspora / Diaspora Project Site",
+ "path": "diaspora-project-site",
+ "path_with_namespace": "diaspora/diaspora-project-site",
+ "issues_enabled": true,
+ "open_issues_count": 1,
+ "merge_requests_enabled": true,
+ "builds_enabled": true,
+ "wiki_enabled": true,
+ "snippets_enabled": false,
+ "created_at": "2013-09-30T13: 46: 02Z",
+ "last_activity_at": "2013-09-30T13: 46: 02Z",
+ "creator_id": 3,
+ "namespace": {
+ "created_at": "2013-09-30T13: 46: 02Z",
+ "description": "",
+ "id": 3,
+ "name": "Diaspora",
+ "owner_id": 1,
+ "path": "diaspora",
+ "updated_at": "2013-09-30T13: 46: 02Z"
+ },
+ "permissions": {
+ "project_access": {
+ "access_level": 10,
+ "notification_level": 3
+ },
+ "group_access": {
+ "access_level": 50,
+ "notification_level": 3
+ }
+ },
+ "archived": false,
+ "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
+ "shared_runners_enabled": true,
+ "forks_count": 0,
+ "star_count": 0,
+ "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b"
+}
+```
+
### Remove project
Removes a project including all associated resources (issues, merge requests etc.)
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 001de76c7af..1e745115dc8 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -26,7 +26,6 @@ Example response:
"default_branch_protection" : 2,
"restricted_visibility_levels" : [],
"signin_enabled" : true,
- "twitter_sharing_enabled" : true,
"after_sign_out_path" : null,
"max_attachment_size" : 10,
"user_oauth_applications" : true,
@@ -57,7 +56,6 @@ PUT /application/settings
| `sign_in_text` | string | no | Text on login page |
| `home_page_url` | string | no | Redirect to this URL when not logged in |
| `default_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and masters can push new commits, force push or delete the branch)_, `1` _(partially protected, developers can push new commits, but cannot force push or delete the branch, masters can do anything)_ or `2` _(fully protected, developers cannot push new commits, force push or delete the branch, masters can do anything)_ as a parameter. Default is `1`. |
-| `twitter_sharing_enabled` | boolean | no | Allow users to share project creation on Twitter |
| `restricted_visibility_levels` | array of integers | no | Selected levels cannot be used by non-admin users for projects or snippets. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is null which means there is no restriction. |
| `max_attachment_size` | integer | no | Limit attachment size in MB |
| `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes |
@@ -85,7 +83,6 @@ Example response:
"updated_at": "2015-06-30T13:22:42.210Z",
"home_page_url": "",
"default_branch_protection": 2,
- "twitter_sharing_enabled": true,
"restricted_visibility_levels": [],
"max_attachment_size": 10,
"session_expire_delay": 10080,
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index a9b79bbdb1b..4316f3c1f64 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -279,6 +279,8 @@ job_name:
| Keyword | Required | Description |
|---------------|----------|-------------|
| script | yes | Defines a shell script which is executed by runner |
+| image | no | Use docker image, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) |
+| services | no | Use docker services, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) |
| stage | no | Defines a build stage (default: `test`) |
| type | no | Alias for `stage` |
| only | no | Defines a list of git refs for which build is created |
@@ -329,7 +331,7 @@ There are a few rules that apply to the usage of refs policy:
* `only` and `except` are inclusive. If both `only` and `except` are defined
in a job specification, the ref is filtered by `only` and `except`.
* `only` and `except` allow the use of regular expressions.
-* `only` and `except` allow the use of special keywords: `branches` and `tags`.
+* `only` and `except` allow the use of special keywords: `branches`, `tags`, and `triggers`.
* `only` and `except` allow to specify a repository path to filter jobs for
forks.
@@ -346,6 +348,17 @@ job:
- branches
```
+In this example, `job` will run only for refs that are tagged, or if a build is explicitly requested
+via an API trigger.
+
+```yaml
+job:
+ # use special keywords
+ only:
+ - tags
+ - triggers
+```
+
The repository path can be used to have jobs executed only for the parent
repository and not forks:
diff --git a/doc/development/scss_styleguide.md b/doc/development/scss_styleguide.md
index 6c48c25448b..a79f4073cde 100644
--- a/doc/development/scss_styleguide.md
+++ b/doc/development/scss_styleguide.md
@@ -72,9 +72,9 @@ p { margin: 0; padding: 0; }
### Colors
-HEX (hexadecimal) colors short-form should use shortform where possible, and
-should use lower case letters to differenciate between letters and numbers, e.
-g. `#E3E3E3` vs. `#e3e3e3`.
+HEX (hexadecimal) colors should use shorthand where possible, and should use
+lower case letters to differentiate between letters and numbers, e.g. `#E3E3E3`
+vs. `#e3e3e3`.
```scss
// Bad
@@ -160,6 +160,7 @@ is slightly more performant.
```
### Selectors with a `js-` Prefix
+
Do not use any selector prefixed with `js-` for styling purposes. These
selectors are intended for use only with JavaScript to allow for removal or
renaming without breaking styling.
@@ -187,8 +188,28 @@ CSSComb globally (system-wide). Run it in the GitLab directory with
Note that this won't fix every problem, but it should fix a majority.
+### Ignoring issues
+
+If you want a line or set of lines to be ignored by the linter, you can use
+`// scss-lint:disable RuleName` ([more info][disabling-linters]):
+
+```scss
+// This lint rule is disabled because the class name comes from a gem.
+// scss-lint:disable SelectorFormat
+.ui_charcoal {
+ background-color: #333;
+}
+// scss-lint:enable SelectorFormat
+```
+
+Make sure a comment is added on the line above the `disable` rule, otherwise the
+linter will throw a warning. `DisableLinterReason` is enabled to make sure the
+style guide isn't being ignored, and to communicate to others why the style
+guide is ignored in this instance.
+
[csscomb]: https://github.com/csscomb/csscomb.js
[node]: https://github.com/nodejs/node
[npm]: https://www.npmjs.com/
[scss-lint]: https://github.com/brigade/scss-lint
[scss-lint-documentation]: https://github.com/brigade/scss-lint/blob/master/lib/scss_lint/linter/README.md
+[disabling-linters]: https://github.com/brigade/scss-lint#disabling-linters-via-source
diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md
index 493e1d1b09c..3aa83975ace 100644
--- a/doc/gitlab-basics/README.md
+++ b/doc/gitlab-basics/README.md
@@ -2,26 +2,14 @@
Step-by-step guides on the basics of working with Git and GitLab.
-* [Start using Git on the command line](start-using-git.md)
-
-* [Create and add your SSH Keys](create-your-ssh-keys.md)
-
-* [Command Line basic commands](command-line-commands.md)
-
-* [Basic Git commands](basic-git-commands.md)
-
-* [Create a project](create-project.md)
-
-* [Create a group](create-group.md)
-
-* [Create a branch](create-branch.md)
-
-* [Fork a project](fork-project.md)
-
-* [Add a file](add-file.md)
-
-* [Add an image](add-image.md)
-
-* [Create a Merge Request](add-merge-request.md)
-
-* [Create an Issue](create-issue.md)
+- [Start using Git on the command line](start-using-git.md)
+- [Create and add your SSH Keys](create-your-ssh-keys.md)
+- [Command Line basics](command-line-commands.md)
+- [Create a project](create-project.md)
+- [Create a group](create-group.md)
+- [Create a branch](create-branch.md)
+- [Fork a project](fork-project.md)
+- [Add a file](add-file.md)
+- [Add an image](add-image.md)
+- [Create a Merge Request](add-merge-request.md)
+- [Create an Issue](create-issue.md)
diff --git a/doc/gitlab-basics/basic-git-commands.md b/doc/gitlab-basics/basic-git-commands.md
index 2b5767dd2d3..c2a3415cbc4 100644
--- a/doc/gitlab-basics/basic-git-commands.md
+++ b/doc/gitlab-basics/basic-git-commands.md
@@ -1,59 +1,3 @@
# Basic Git commands
-### Go to the master branch to pull the latest changes from there
-```
-git checkout master
-```
-
-### Download the latest changes in the project
-This is for you to work on an up-to-date copy (it is important to do every time you work on a project), while you setup tracking branches.
-```
-git pull REMOTE NAME-OF-BRANCH -u
-```
-(REMOTE: origin) (NAME-OF-BRANCH: could be "master" or an existing branch)
-
-### Create a branch
-Spaces won't be recognized, so you need to use a hyphen or underscore.
-```
-git checkout -b NAME-OF-BRANCH
-```
-
-### Work on a branch that has already been created
-```
-git checkout NAME-OF-BRANCH
-```
-
-### View the changes you've made
-It's important to be aware of what's happening and what's the status of your changes.
-```
-git status
-```
-
-### Add changes to commit
-You'll see your changes in red when you type "git status".
-```
-git add CHANGES IN RED
-git commit -m "DESCRIBE THE INTENTION OF THE COMMIT"
-```
-
-### Send changes to gitlab.com
-```
-git push REMOTE NAME-OF-BRANCH
-```
-
-### Delete all changes in the Git repository, but leave unstaged things
-```
-git checkout .
-```
-
-### Delete all changes in the Git repository, including untracked files
-```
-git clean -f
-```
-
-### Merge created branch with master branch
-You need to be in the created branch.
-```
-git checkout NAME-OF-BRANCH
-git merge master
-```
+This section is now merged into [Start using Git](start-using-git.md).
diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md
index b2ceda025c0..89ce8bcc3e8 100644
--- a/doc/gitlab-basics/start-using-git.md
+++ b/doc/gitlab-basics/start-using-git.md
@@ -1,6 +1,7 @@
# Start using Git on the command line
-If you want to start using a Git and GitLab, make sure that you have created an account on GitLab.
+If you want to start using a Git and GitLab, make sure that you have created an
+account on GitLab.
## Open a shell
@@ -59,3 +60,63 @@ To view the information that you entered, type:
```
git config --global --list
```
+## Basic Git commands
+
+### Go to the master branch to pull the latest changes from there
+
+```
+git checkout master
+```
+
+### Download the latest changes in the project
+This is for you to work on an up-to-date copy (it is important to do every time you work on a project), while you setup tracking branches.
+```
+git pull REMOTE NAME-OF-BRANCH -u
+```
+(REMOTE: origin) (NAME-OF-BRANCH: could be "master" or an existing branch)
+
+### Create a branch
+Spaces won't be recognized, so you need to use a hyphen or underscore.
+```
+git checkout -b NAME-OF-BRANCH
+```
+
+### Work on a branch that has already been created
+```
+git checkout NAME-OF-BRANCH
+```
+
+### View the changes you've made
+It's important to be aware of what's happening and what's the status of your changes.
+```
+git status
+```
+
+### Add changes to commit
+You'll see your changes in red when you type "git status".
+```
+git add CHANGES IN RED
+git commit -m "DESCRIBE THE INTENTION OF THE COMMIT"
+```
+
+### Send changes to gitlab.com
+```
+git push REMOTE NAME-OF-BRANCH
+```
+
+### Delete all changes in the Git repository, but leave unstaged things
+```
+git checkout .
+```
+
+### Delete all changes in the Git repository, including untracked files
+```
+git clean -f
+```
+
+### Merge created branch with master branch
+You need to be in the created branch.
+```
+git checkout NAME-OF-BRANCH
+git merge master
+```
diff --git a/doc/incoming_email/README.md b/doc/incoming_email/README.md
index 4cfb8402943..5a9a1582877 100644
--- a/doc/incoming_email/README.md
+++ b/doc/incoming_email/README.md
@@ -1,36 +1,99 @@
# Reply by email
-GitLab can be set up to allow users to comment on issues and merge requests by replying to notification emails.
+GitLab can be set up to allow users to comment on issues and merge requests by
+replying to notification emails.
-## Get a mailbox
+## Requirement
-Reply by email requires an IMAP-enabled email account, with a provider or server that supports [email sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing). Sub-addressing is a feature where any email to `user+some_arbitrary_tag@example.com` will end up in the mailbox for `user@example.com`, and is supported by providers such as Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix mail server which you can run on-premises.
+Reply by email requires an IMAP-enabled email account. GitLab allows you to use
+three strategies for this feature:
+- using email sub-addressing
+- using a dedicated email address
+- using a catch-all mailbox
-If you want to use Gmail / Google Apps with Reply by email, make sure you have [IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018) and [allow less secure apps to access the account](https://support.google.com/accounts/answer/6010255).
+### Email sub-addressing
-To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these instructions](./postfix.md).
+**If your provider or server supports email sub-addressing, we recommend using it.**
+
+[Sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing) is
+a feature where any email to `user+some_arbitrary_tag@example.com` will end up
+in the mailbox for `user@example.com`, and is supported by providers such as
+Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix
+mail server which you can run on-premises.
+
+### Dedicated email address
+
+This solution is really simple to set up: you just have to create an email
+address dedicated to receive your users' replies to GitLab notifications.
+
+### Catch-all mailbox
+
+A [catch-all mailbox](https://en.wikipedia.org/wiki/Catch-all) for a domain will
+"catch all" the emails addressed to the domain that do not exist in the mail
+server.
+
+## How it works?
+
+### 1. GitLab sends a notification email
+
+When GitLab sends a notification and Reply by email is enabled, the `Reply-To`
+header is set to the address defined in your GitLab configuration, with the
+`%{key}` placeholder (if present) replaced by a specific "reply key". In
+addition, this "reply key" is also added to the `References` header.
+
+### 2. You reply to the notification email
+
+When you reply to the notification email, your email client will:
+
+- send the email to the `Reply-To` address it got from the notification email
+- set the `In-Reply-To` header to the value of the `Message-ID` header from the
+ notification email
+- set the `References` header to the value of the `Message-ID` plus the value of
+ the notification email's `References` header.
+
+### 3. GitLab receives your reply to the notification email
+
+When GitLab receives your reply, it will look for the "reply key" in the
+following headers, in this order:
+
+1. the `To` header
+1. the `References` header
+
+If it finds a reply key, it will be able to leave your reply as a comment on
+the entity the notification was about (issue, merge request, commit...).
+
+For more details about the `Message-ID`, `In-Reply-To`, and `References headers`,
+please consult [RFC 5322](https://tools.ietf.org/html/rfc5322#section-3.6.4).
## Set it up
+If you want to use Gmail / Google Apps with Reply by email, make sure you have
+[IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018)
+and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255).
+
+To set up a basic Postfix mail server with IMAP access on Ubuntu, follow
+[these instructions](./postfix.md).
+
### Omnibus package installations
-1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the feature and fill in the details for your specific IMAP server and email account:
+1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the
+ feature and fill in the details for your specific IMAP server and email account:
```ruby
# Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com
gitlab_rails['incoming_email_enabled'] = true
-
- # The email address including a placeholder for the key that references the item being replied to.
- # The `%{key}` placeholder is added after the user part, before the `@`.
+
+ # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
gitlab_rails['incoming_email_address'] = "incoming+%{key}@gitlab.example.com"
-
+
# Email account username
# With third party providers, this is usually the full email address.
# With self-hosted email servers, this is usually the user part of the email address.
gitlab_rails['incoming_email_email'] = "incoming"
# Email account password
gitlab_rails['incoming_email_password'] = "[REDACTED]"
-
+
# IMAP server host
gitlab_rails['incoming_email_host'] = "gitlab.example.com"
# IMAP server port
@@ -47,18 +110,18 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
```ruby
# Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
gitlab_rails['incoming_email_enabled'] = true
-
+
# The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
- # The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com"
-
+
# Email account username
# With third party providers, this is usually the full email address.
# With self-hosted email servers, this is usually the user part of the email address.
gitlab_rails['incoming_email_email'] = "gitlab-incoming@gmail.com"
# Email account password
gitlab_rails['incoming_email_password'] = "[REDACTED]"
-
+
# IMAP server host
gitlab_rails['incoming_email_host'] = "imap.gmail.com"
# IMAP server port
@@ -72,8 +135,6 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
gitlab_rails['incoming_email_mailbox_name'] = "inbox"
```
- As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `incoming@gitlab.example.com`/`gitlab-incoming@gmail.com`.
-
1. Reconfigure GitLab and restart mailroom for the changes to take effect:
```sh
@@ -97,7 +158,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
cd /home/git/gitlab
```
-1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and fill in the details for your specific IMAP server and email account:
+1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature
+ and fill in the details for your specific IMAP server and email account:
```sh
sudo editor config/gitlab.yml
@@ -109,7 +171,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
enabled: true
# The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
- # The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
address: "incoming+%{key}@gitlab.example.com"
# Email account username
@@ -138,7 +200,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
enabled: true
# The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
- # The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
address: "gitlab-incoming+%{key}@gmail.com"
# Email account username
@@ -161,8 +223,6 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
mailbox: "inbox"
```
- As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `incoming@gitlab.example.com`/`gitlab-incoming@gmail.com`.
-
1. Enable `mail_room` in the init script at `/etc/default/gitlab`:
```sh
@@ -195,8 +255,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
incoming_email:
enabled: true
- # The email address including a placeholder for the key that references the item being replied to.
- # The `%{key}` placeholder is added after the user part, before the `@`.
+ # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
address: "gitlab-incoming+%{key}@gmail.com"
# Email account username
diff --git a/doc/install/installation.md b/doc/install/installation.md
index c567846f624..bffbc776500 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -348,7 +348,7 @@ GitLab Shell is an SSH access and repository management software developed speci
cd /home/git
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git
cd gitlab-workhorse
- sudo -u git -H git checkout 0.6.5
+ sudo -u git -H git checkout v0.7.1
sudo -u git -H make
### Initialize Database and Activate Advanced Features
diff --git a/doc/integration/github.md b/doc/integration/github.md
index 886784a27c9..1890edd7a4c 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -17,7 +17,7 @@ GitHub will generate an application ID and secret key for you to use.
- Application name: This can be anything. Consider something like "\<Organization\>'s GitLab" or "\<Your Name\>'s GitLab" or something else descriptive.
- Homepage URL: The URL to your GitLab installation. 'https://gitlab.company.com'
- Application description: Fill this in if you wish.
- - Authorization callback URL: 'https://gitlab.company.com/'
+ - Default authorization callback URL is '${YOUR_DOMAIN}/import/github/callback'
1. Select "Register application".
1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot).
diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md
index cf1f98492ea..fb20308c49c 100644
--- a/doc/integration/ldap.md
+++ b/doc/integration/ldap.md
@@ -1,228 +1,3 @@
# GitLab LDAP integration
-GitLab can be configured to allow your users to sign with their LDAP credentials to integrate with e.g. Active Directory.
-
-The first time a user signs in with LDAP credentials, GitLab will create a new GitLab user associated with the LDAP Distinguished Name (DN) of the LDAP user.
-
-GitLab user attributes such as nickname and email will be copied from the LDAP user entry.
-
-## Security
-
-GitLab assumes that LDAP users are not able to change their LDAP 'mail', 'email' or 'userPrincipalName' attribute.
-An LDAP user who is allowed to change their email on the LDAP server can [take over any account](#enabling-ldap-sign-in-for-existing-gitlab-users) on your GitLab server.
-
-We recommend against using GitLab LDAP integration if your LDAP users are allowed to change their 'mail', 'email' or 'userPrincipalName' attribute on the LDAP server.
-
-If a user is deleted from the LDAP server, they will be blocked in GitLab as well.
-Users will be immediately blocked from logging in. However, there is an LDAP check
-cache time of one hour. The means users that are already logged in or are using Git
-over SSH will still be able to access GitLab for up to one hour. Manually block
-the user in the GitLab Admin area to immediately block all access.
-
-## Configuring GitLab for LDAP integration
-
-To enable GitLab LDAP integration you need to add your LDAP server settings in `/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`.
-In GitLab Enterprise Edition you can have multiple LDAP servers connected to one GitLab server.
-
-Please note that before version 7.4, GitLab used a different syntax for configuring LDAP integration.
-The old LDAP integration syntax still works in GitLab 7.4.
-If your `gitlab.rb` or `gitlab.yml` file contains LDAP settings in both the old syntax and the new syntax, only the __old__ syntax will be used by GitLab.
-
-```ruby
-# For omnibus packages
-gitlab_rails['ldap_enabled'] = true
-gitlab_rails['ldap_servers'] = YAML.load <<-EOS # remember to close this block with 'EOS' below
-main: # 'main' is the GitLab 'provider ID' of this LDAP server
- ## label
- #
- # A human-friendly name for your LDAP server. It is OK to change the label later,
- # for instance if you find out it is too large to fit on the web page.
- #
- # Example: 'Paris' or 'Acme, Ltd.'
- label: 'LDAP'
-
- host: '_your_ldap_server'
- port: 389
- uid: 'sAMAccountName'
- method: 'plain' # "tls" or "ssl" or "plain"
- bind_dn: '_the_full_dn_of_the_user_you_will_bind_with'
- password: '_the_password_of_the_bind_user'
-
- # Set a timeout, in seconds, for LDAP queries. This helps avoid blocking
- # a request if the LDAP server becomes unresponsive.
- # A value of 0 means there is no timeout.
- timeout: 10
-
- # This setting specifies if LDAP server is Active Directory LDAP server.
- # For non AD servers it skips the AD specific queries.
- # If your LDAP server is not AD, set this to false.
- active_directory: true
-
- # If allow_username_or_email_login is enabled, GitLab will ignore everything
- # after the first '@' in the LDAP username submitted by the user on login.
- #
- # Example:
- # - the user enters 'jane.doe@example.com' and 'p@ssw0rd' as LDAP credentials;
- # - GitLab queries the LDAP server with 'jane.doe' and 'p@ssw0rd'.
- #
- # If you are using "uid: 'userPrincipalName'" on ActiveDirectory you need to
- # disable this setting, because the userPrincipalName contains an '@'.
- allow_username_or_email_login: false
-
- # To maintain tight control over the number of active users on your GitLab installation,
- # enable this setting to keep new users blocked until they have been cleared by the admin
- # (default: false).
- block_auto_created_users: false
-
- # Base where we can search for users
- #
- # Ex. ou=People,dc=gitlab,dc=example
- #
- base: ''
-
- # Filter LDAP users
- #
- # Format: RFC 4515 https://tools.ietf.org/search/rfc4515
- # Ex. (employeeType=developer)
- #
- # Note: GitLab does not support omniauth-ldap's custom filter syntax.
- #
- user_filter: ''
-
- # LDAP attributes that GitLab will use to create an account for the LDAP user.
- # The specified attribute can either be the attribute name as a string (e.g. 'mail'),
- # or an array of attribute names to try in order (e.g. ['mail', 'email']).
- # Note that the user's LDAP login will always be the attribute specified as `uid` above.
- attributes:
- # The username will be used in paths for the user's own projects
- # (like `gitlab.example.com/username/project`) and when mentioning
- # them in issues, merge request and comments (like `@username`).
- # If the attribute specified for `username` contains an email address,
- # the GitLab username will be the part of the email address before the '@'.
- username: ['uid', 'userid', 'sAMAccountName']
- email: ['mail', 'email', 'userPrincipalName']
-
- # If no full name could be found at the attribute specified for `name`,
- # the full name is determined using the attributes specified for
- # `first_name` and `last_name`.
- name: 'cn'
- first_name: 'givenName'
- last_name: 'sn'
-
-# GitLab EE only: add more LDAP servers
-# Choose an ID made of a-z and 0-9 . This ID will be stored in the database
-# so that GitLab can remember which LDAP server a user belongs to.
-# uswest2:
-# label:
-# host:
-# ....
-EOS
-```
-
-If you are getting 'Connection Refused' errors when trying to connect to the LDAP server please double-check the LDAP `port` and `method` settings used by GitLab.
-Common combinations are `method: 'plain'` and `port: 389`, OR `method: 'ssl'` and `port: 636`.
-
-If you are using a GitLab installation from source you can find the LDAP settings in `/home/git/gitlab/config/gitlab.yml`:
-
-```
-production:
- # snip...
- ldap:
- enabled: false
- servers:
- main: # 'main' is the GitLab 'provider ID' of this LDAP server
- ## label
- #
- # A human-friendly name for your LDAP server. It is OK to change the label later,
- # for instance if you find out it is too large to fit on the web page.
- #
- # Example: 'Paris' or 'Acme, Ltd.'
- label: 'LDAP'
- # snip...
-```
-
-## Enabling LDAP sign-in for existing GitLab users
-
-When a user signs in to GitLab with LDAP for the first time, and their LDAP email address is the primary email address of an existing GitLab user, then the LDAP DN will be associated with the existing user.
-
-If the LDAP email attribute is not found in GitLab's database, a new user is created.
-
-In other words, if an existing GitLab user wants to enable LDAP sign-in for themselves, they should check that their GitLab email address matches their LDAP email address, and then sign into GitLab via their LDAP credentials.
-
-GitLab recognizes the following LDAP attributes as email addresses: `mail`, `email` and `userPrincipalName`.
-
-If multiple LDAP email attributes are present, e.g. `mail: foo@bar.com` and `email: foo@example.com`, then the first attribute found wins -- in this case `foo@bar.com`.
-
-## Using an LDAP filter to limit access to your GitLab server
-
-If you want to limit all GitLab access to a subset of the LDAP users on your LDAP server you can set up an LDAP user filter.
-The filter must comply with [RFC 4515](https://tools.ietf.org/search/rfc4515).
-
-```ruby
-# For omnibus packages; new LDAP server syntax
-gitlab_rails['ldap_servers'] = YAML.load <<-EOS
-main:
- # snip...
- user_filter: '(employeeType=developer)'
-EOS
-```
-
-```yaml
-# For installations from source; new LDAP server syntax
-production:
- ldap:
- servers:
- main:
- # snip...
- user_filter: '(employeeType=developer)'
-```
-
-Tip: if you want to limit access to the nested members of an Active Directory group you can use the following syntax:
-
-```
-(memberOf:1.2.840.113556.1.4.1941:=CN=My Group,DC=Example,DC=com)
-```
-
-Please note that GitLab does not support the custom filter syntax used by omniauth-ldap.
-
-## Limitations
-
-GitLab's LDAP client is based on [omniauth-ldap](https://gitlab.com/gitlab-org/omniauth-ldap)
-which encapsulates Ruby's `Net::LDAP` class. It provides a pure-Ruby implementation
-of the LDAP client protocol. As a result, GitLab is limited by `omniauth-ldap` and may impact your LDAP
-server settings.
-
-### TLS Client Authentication
-Not implemented by `Net::LDAP`.
-So you should disable anonymous LDAP authentication and enable simple or SASL
-authentication. TLS client authentication setting in your LDAP server cannot be
-mandatory and clients cannot be authenticated with the TLS protocol.
-
-### TLS Server Authentication
-Not supported by GitLab's configuration options.
-When setting `method: ssl`, the underlying authentication method used by
-`omniauth-ldap` is `simple_tls`. This method establishes TLS encryption with
-the LDAP server before any LDAP-protocol data is exchanged but no validation of
-the LDAP server's SSL certificate is performed.
-
-## Troubleshooting
-
-### Invalid credentials when logging in
-
-Make sure the user you are binding with has enough permissions to read the user's
-tree and traverse it.
-
-Also make sure that the `user_filter` is not blocking otherwise valid users.
-
-To make sure that the LDAP settings are correct and GitLab can see your users,
-execute the following command:
-
-
-```bash
-# For Omnibus installations
-sudo gitlab-rake gitlab:ldap:check
-
-# For installations from source
-sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production
-```
-
+This document was moved under [`administration/auth/ldap`](administration/auth/ldap.md).
diff --git a/doc/intro/README.md b/doc/intro/README.md
new file mode 100644
index 00000000000..fecbbe6317b
--- /dev/null
+++ b/doc/intro/README.md
@@ -0,0 +1,41 @@
+# Get started with GitLab
+
+## Organize
+
+Create projects and groups.
+
+- [Create a new project](../gitlab-basics/create-project.md)
+- [Create a new group](../gitlab-basics/create-group.md)
+
+## Prioritize
+
+Create issues, labels, milestones, cast your vote, and review issues.
+
+- [Create a new issue](../gitlab-basics/create-issue.md)
+- [Assign labels to issues](../workflow/labels.md)
+- [Use milestones as an overview of your project's tracker](../workflow/milestones.md)
+- [Use voting to express your like/dislike to issues and merge requests](../workflow/award_emoji.md)
+
+## Collaborate
+
+Create merge requests and review code.
+
+- [Fork a project and contribute to it](../workflow/forking_workflow.md)
+- [Create a new merge request](../gitlab-basics/add-merge-request.md)
+- [Automatically close issues from merge requests](../customization/issue_closing.md)
+- [Automatically merge when your builds succeed](../workflow/merge_when_build_succeeds.md)
+- [Revert any commit](../workflow/revert_changes.md)
+
+## Test and Deploy
+
+Use the built-in continuous integration in GitLab.
+
+- [Get started with GitLab CI](../ci/quick_start/README.md)
+
+## Install and Update
+
+Install and update your GitLab installation.
+
+- [Install GitLab](https://about.gitlab.com/installation/)
+- [Update GitLab](https://about.gitlab.com/update/)
+- [Explore Omnibus GitLab configuration options](http://doc.gitlab.com/omnibus/settings/configuration.html)
diff --git a/doc/monitoring/performance/grafana_configuration.md b/doc/monitoring/performance/grafana_configuration.md
new file mode 100644
index 00000000000..416c9870aa0
--- /dev/null
+++ b/doc/monitoring/performance/grafana_configuration.md
@@ -0,0 +1,118 @@
+# Grafana Configuration
+
+[Grafana](http://grafana.org/) is a tool that allows you to visualize time
+series metrics through graphs and dashboards. It supports several backend
+data stores, including InfluxDB. GitLab writes performance data to InfluxDB
+and Grafana will allow you to query InfluxDB to display useful graphs.
+
+For the easiest installation and configuration, install Grafana on the same
+server as InfluxDB. For larger installations, you may want to split out these
+services.
+
+## Installation
+
+Grafana supplies package repositories (Yum/Apt) for easy installation.
+See [Grafana installation documentation](http://docs.grafana.org/installation/)
+for detailed steps.
+
+> **Note**: Before starting Grafana for the first time, set the admin user
+and password in `/etc/grafana/grafana.ini`. Otherwise, the default password
+will be `admin`.
+
+## Configuration
+
+Login as the admin user. Expand the menu by clicking the Grafana logo in the
+top left corner. Choose 'Data Sources' from the menu. Then, click 'Add new'
+in the top bar.
+
+![Grafana empty data source page](img/grafana_data_source_empty.png)
+
+Fill in the configuration details for the InfluxDB data source. Save and
+Test Connection to ensure the configuration is correct.
+
+- **Name**: InfluxDB
+- **Default**: Checked
+- **Type**: InfluxDB 0.9.x (Even if you're using InfluxDB 0.10.x)
+- **Url**: https://localhost:8086 (Or the remote URL if you've installed InfluxDB
+on a separate server)
+- **Access**: proxy
+- **Database**: gitlab
+- **User**: admin (Or the username configured when setting up InfluxDB)
+- **Password**: The password configured when you set up InfluxDB
+
+![Grafana data source configurations](img/grafana_data_source_configuration.png)
+
+## Apply retention policies and create continuous queries
+
+If you intend to import the GitLab provided Grafana dashboards, you will need
+to copy and run a set of queries against InfluxDB to create the needed data
+sets.
+
+On the InfluxDB server, run the following command, substituting your InfluxDB
+user and password:
+
+```bash
+influxdb --username admin -password super_secret
+```
+
+This will drop you in to an InfluxDB interactive session. Copy the entire
+contents below and paste it in to the interactive session:
+
+```
+CREATE RETENTION POLICY gitlab_30d ON gitlab DURATION 30d REPLICATION 1 DEFAULT
+CREATE RETENTION POLICY seven_days ON gitlab DURATION 7d REPLICATION 1
+CREATE CONTINUOUS QUERY rails_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean", percentile(sql_duration, 95.000) AS "sql_duration_95th", percentile(sql_duration, 99.000) AS "sql_duration_99th", mean(sql_duration) AS "sql_duration_mean", percentile(view_duration, 95.000) AS "view_duration_95th", percentile(view_duration, 99.000) AS "view_duration_99th", mean(view_duration) AS "view_duration_mean" INTO gitlab.seven_days.rails_transaction_timings FROM gitlab.gitlab_30d.rails_transactions GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY sidekiq_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean", percentile(sql_duration, 95.000) AS "sql_duration_95th", percentile(sql_duration, 99.000) AS "sql_duration_99th", mean(sql_duration) AS "sql_duration_mean", percentile(view_duration, 95.000) AS "view_duration_95th", percentile(view_duration, 99.000) AS "view_duration_99th", mean(view_duration) AS "view_duration_mean" INTO gitlab.seven_days.sidekiq_transaction_timings FROM gitlab.gitlab_30d.sidekiq_transactions GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY rails_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.rails_transaction_counts FROM gitlab.gitlab_30d.rails_transactions GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY sidekiq_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.sidekiq_transaction_counts FROM gitlab.gitlab_30d.sidekiq_transactions GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY rails_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.rails_method_call_timings FROM gitlab.gitlab_30d.rails_method_calls GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY sidekiq_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.sidekiq_method_call_timings FROM gitlab.gitlab_30d.sidekiq_method_calls GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY rails_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.rails_method_call_timings_per_method FROM gitlab.gitlab_30d.rails_method_calls GROUP BY time(1m), method END
+CREATE CONTINUOUS QUERY sidekiq_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.sidekiq_method_call_timings_per_method FROM gitlab.gitlab_30d.sidekiq_method_calls GROUP BY time(1m), method END
+CREATE CONTINUOUS QUERY rails_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.rails_memory_usage_per_minute FROM gitlab.gitlab_30d.rails_memory_usage GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY sidekiq_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.sidekiq_memory_usage_per_minute FROM gitlab.gitlab_30d.sidekiq_memory_usage GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY sidekiq_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.sidekiq_file_descriptors_per_minute FROM gitlab.gitlab_30d.sidekiq_file_descriptors GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY rails_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.rails_file_descriptors_per_minute FROM gitlab.gitlab_30d.rails_file_descriptors GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY rails_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.rails_gc_counts_per_minute FROM gitlab.gitlab_30d.rails_gc_statistics GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY sidekiq_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.sidekiq_gc_counts_per_minute FROM gitlab.gitlab_30d.sidekiq_gc_statistics GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY rails_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.rails_gc_timings_per_minute FROM gitlab.gitlab_30d.rails_gc_statistics GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY sidekiq_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.sidekiq_gc_timings_per_minute FROM gitlab.gitlab_30d.sidekiq_gc_statistics GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY rails_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.rails_gc_major_minor_per_minute FROM gitlab.gitlab_30d.rails_gc_statistics GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY sidekiq_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.sidekiq_gc_major_minor_per_minute FROM gitlab.gitlab_30d.sidekiq_gc_statistics GROUP BY time(1m) END
+```
+
+## Import Dashboards
+
+You can now import a set of default dashboards that will give you a good
+start on displaying useful information. GitLab has published a set of default
+[Grafana dashboards][grafana-dashboards] to get you started. Clone the
+repository or download a zip/tarball, then follow these steps to import each
+JSON file.
+
+Open the dashboard dropdown menu and click 'Import'
+
+![Grafana dashboard dropdown](/img/grafana_dashboard_dropdown.png)
+
+Click 'Choose file' and browse to the location where you downloaded or cloned
+the dashboard repository. Pick one of the JSON files to import.
+
+![Grafana dashboard import](/img/grafana_dashboard_import.png)
+
+Once the dashboard is imported, be sure to click save icon in the top bar. If
+you do not save the dashboard after importing it will be removed when you
+navigate away.
+
+![Grafana save icon](/img/grafana_save_icon.png)
+
+Repeat this process for each dashboard you wish to import.
+
+[grafana-dashboards]: https://gitlab.com/gitlab-org/grafana-dashboards
+
+---
+
+Read more on:
+
+- [Introduction to GitLab Performance Monitoring](introduction.md)
+- [GitLab Configuration](gitlab_configuration.md)
+- [InfluxDB Installation/Configuration](influxdb_configuration.md)
+- [InfluxDB Schema](influxdb_schema.md)
diff --git a/doc/monitoring/performance/img/grafana_dashboard_dropdown.png b/doc/monitoring/performance/img/grafana_dashboard_dropdown.png
new file mode 100644
index 00000000000..b4448c7a09f
--- /dev/null
+++ b/doc/monitoring/performance/img/grafana_dashboard_dropdown.png
Binary files differ
diff --git a/doc/monitoring/performance/img/grafana_dashboard_import.png b/doc/monitoring/performance/img/grafana_dashboard_import.png
new file mode 100644
index 00000000000..5a2d3c0937a
--- /dev/null
+++ b/doc/monitoring/performance/img/grafana_dashboard_import.png
Binary files differ
diff --git a/doc/monitoring/performance/img/grafana_data_source_configuration.png b/doc/monitoring/performance/img/grafana_data_source_configuration.png
new file mode 100644
index 00000000000..7e2e111f570
--- /dev/null
+++ b/doc/monitoring/performance/img/grafana_data_source_configuration.png
Binary files differ
diff --git a/doc/monitoring/performance/img/grafana_data_source_empty.png b/doc/monitoring/performance/img/grafana_data_source_empty.png
new file mode 100644
index 00000000000..11e27571e64
--- /dev/null
+++ b/doc/monitoring/performance/img/grafana_data_source_empty.png
Binary files differ
diff --git a/doc/monitoring/performance/img/grafana_save_icon.png b/doc/monitoring/performance/img/grafana_save_icon.png
new file mode 100644
index 00000000000..3d4265bee8e
--- /dev/null
+++ b/doc/monitoring/performance/img/grafana_save_icon.png
Binary files differ
diff --git a/doc/monitoring/performance/introduction.md b/doc/monitoring/performance/introduction.md
index f2460d31302..79904916b7e 100644
--- a/doc/monitoring/performance/introduction.md
+++ b/doc/monitoring/performance/introduction.md
@@ -8,8 +8,9 @@ Apart from this introduction, you are advised to read through the following
documents in order to understand and properly configure GitLab Performance Monitoring:
- [GitLab Configuration](gitlab_configuration.md)
-- [InfluxDB Configuration](influxdb_configuration.md)
+- [InfluxDB Install/Configuration](influxdb_configuration.md)
- [InfluxDB Schema](influxdb_schema.md)
+- [Grafana Install/Configuration](grafana_configuration.md)
## Introduction to GitLab Performance Monitoring
diff --git a/doc/public_access/public_access.md b/doc/public_access/public_access.md
index 6e22ea7b72a..20aa90f0d69 100644
--- a/doc/public_access/public_access.md
+++ b/doc/public_access/public_access.md
@@ -35,6 +35,21 @@ the repository.
1. Go to your project's **Settings**
1. Change "Visibility Level" to either Public, Internal or Private
+## Visibility of groups
+
+>**Note:**
+[Starting with][3323] GitLab 8.6, the group visibility has changed and can be
+configured the same way as projects. In previous versions, a group's page was
+always visible to all users.
+
+Like with projects, the visibility of a group can be set to dictate whether
+anonymous users, all signed in users, or only explicit group members can view
+it. The restriction for visibility levels on the application setting level also
+applies to groups, so if that's set to internal, the explore page will be empty
+for anonymous users. The group page now has a visibility level icon.
+
+[3323]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3323
+
## Visibility of users
The public page of a user, located at `/u/username`, is always visible whether
@@ -43,14 +58,6 @@ you are logged in or not.
When visiting the public page of a user, you can only see the projects which
you are privileged to.
-## Visibility of groups
-
-The public page of a group, located at `/groups/groupname`, is always visible
-to everyone.
-
-Logged out users will be able to see the description and the avatar of the
-group as well as all public projects belonging to that group.
-
## Restricting the use of public or internal projects
In the Admin area under **Settings** (`/admin/application_settings`), you can
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index f6d1234ac4a..4329ac30a1c 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -249,6 +249,9 @@ reconfigure` after changing `gitlab-secrets.json`.
### Installation from source
```
+# Stop processes that are connected to the database
+sudo service gitlab stop
+
bundle exec rake gitlab:backup:restore RAILS_ENV=production
```
diff --git a/doc/release/README.md b/doc/release/README.md
deleted file mode 100644
index 52eca7c02a6..00000000000
--- a/doc/release/README.md
+++ /dev/null
@@ -1,10 +0,0 @@
-## Release cycle
-
-Since 2011 a minor or major version of GitLab is released on the 22nd of every month. Patch and security releases are published when needed. New features are detailed on the [blog](https://about.gitlab.com/blog/) and in the [changelog](CHANGELOG). Features that will likely be in the next releases can be found on the [direction page](https://about.gitlab.com/direction/).
-
-## Release process documentation
-
-- [Monthly release](monthly.md), every month on the 22nd.
-- [Patch release](patch.md), if there are serious regressions.
-- [Security](security.md), for security problems.
-- [Master](master.md), update process for the master branch.
diff --git a/doc/release/howto_rc1.md b/doc/release/howto_rc1.md
deleted file mode 100644
index 07c703142d4..00000000000
--- a/doc/release/howto_rc1.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# How to create RC1
-
-The RC1 release comes with the task to update the installation and upgrade docs. Be mindful that there might already be merge requests for this on GitLab or GitHub.
-
-### 1. Update the installation guide
-
-1. Check if it references the correct branch `x-x-stable` (doesn't exist yet, but that is okay)
-1. Check the [GitLab Shell version](/lib/tasks/gitlab/check.rake#L782)
-1. Check the [Git version](/lib/tasks/gitlab/check.rake#L794)
-1. There might be other changes. Ask around.
-
-### 2. Create update guides
-
-[Follow this guide](howto_update_guides.md) to create update guides.
-
-### 3. Code quality indicators
-
-Make sure the code quality indicators are green / good.
-
-- [![Build status](http://ci.gitlab.org/projects/1/status.png?ref=master)](http://ci.gitlab.org/projects/1?ref=master) on ci.gitlab.org (master branch)
-
-- [![Build Status](https://semaphoreapp.com/api/v1/projects/2f1a5809-418b-4cc2-a1f4-819607579fe7/243338/badge.png)](https://semaphoreapp.com/gitlabhq/gitlabhq) (master branch)
-
-- [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.png)](https://codeclimate.com/github/gitlabhq/gitlabhq)
-
-- [![Dependency Status](https://gemnasium.com/gitlabhq/gitlabhq.png)](https://gemnasium.com/gitlabhq/gitlabhq) this button can be yellow (small updates are available) but must not be red (a security fix or an important update is available)
-
-- [![Coverage Status](https://coveralls.io/repos/gitlabhq/gitlabhq/badge.png?branch=master)](https://coveralls.io/r/gitlabhq/gitlabhq)
-
-### 4. Run release tool
-
-**Make sure EE `master` has latest changes from CE `master`**
-
-Get release tools
-
-```
-git clone git@dev.gitlab.org:gitlab/release-tools.git
-cd release-tools
-```
-
-Release candidate creates stable branch from master.
-So we need to sync master branch between all CE, EE and CI remotes.
-
-```
-bundle exec rake sync
-```
-
-Create release candidate and stable branch:
-
-```
-bundle exec rake release["x.x.0.rc1"]
-```
-
-Now developers can use master for merging new features.
-So you should use stable branch for future code changes related to release.
diff --git a/doc/release/howto_update_guides.md b/doc/release/howto_update_guides.md
deleted file mode 100644
index 23d0959c33d..00000000000
--- a/doc/release/howto_update_guides.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# Create update guides
-
-1. Create: CE update guide from previous version. Like `7.3-to-7.4.md`
-1. Create: CE to EE update guide in EE repository for latest version.
-1. Update: `6.x-or-7.x-to-7.x.md` to latest version.
-1. Create: CI update guide from previous version
-
-It's best to copy paste the previous guide and make changes where necessary.
-The typical steps are listed below with any points you should specifically look at.
-
-#### 0. Any major changes?
-
-List any major changes here, so the user is aware of them before starting to upgrade. For instance:
-
-- Database updates
-- Web server changes
-- File structure changes
-
-#### 1. Stop server
-
-#### 2. Make backup
-
-#### 3. Do users need to update dependencies like `git`?
-
-- Check if the [GitLab Shell version](/lib/tasks/gitlab/check.rake#L782) changed since the last release.
-
-- Check if the [Git version](/lib/tasks/gitlab/check.rake#L794) changed since the last release.
-
-#### 4. Get latest code
-
-#### 5. Does GitLab shell need to be updated?
-
-#### 6. Install libs, migrations, etc.
-
-#### 7. Any config files updated since last release?
-
-Check if any of these changed since last release:
-
-- [lib/support/nginx/gitlab](/lib/support/nginx/gitlab)
-- [lib/support/nginx/gitlab-ssl](/lib/support/nginx/gitlab-ssl)
-- <https://gitlab.com/gitlab-org/gitlab-shell/commits/master/config.yml.example>
-- [config/gitlab.yml.example](/config/gitlab.yml.example)
-- [config/unicorn.rb.example](/config/unicorn.rb.example)
-- [config/database.yml.mysql](/config/database.yml.mysql)
-- [config/database.yml.postgresql](/config/database.yml.postgresql)
-- [config/initializers/rack_attack.rb.example](/config/initializers/rack_attack.rb.example)
-- [config/resque.yml.example](/config/resque.yml.example)
-
-#### 8. Need to update init script?
-
-Check if the `init.d/gitlab` script changed since last release: [lib/support/init.d/gitlab](/lib/support/init.d/gitlab)
-
-#### 9. Start application
-
-#### 10. Check application status
diff --git a/doc/release/master.md b/doc/release/master.md
deleted file mode 100644
index 9163e652003..00000000000
--- a/doc/release/master.md
+++ /dev/null
@@ -1,62 +0,0 @@
-# How to push GitLab CE master branch to all remotes.
-
-The source code of GitLab is available on multiple servers (with GitLab.com as the canonical source).
-Synchronization between the repo's is done by the lead developer if there is no rush.
-This happens a few times per workday on average.
-If somebody else with access to all repo's wants to do it the instructions are below.
-This is just to distribute changes, not to make them.
-
-## Add this to `.bashrc` or [your dotfiles](https://github.com/dosire/dotfiles/commit/52803ce3ac60d57632164b7713ff0041e86fa26c)
-
-```bash
-gpa ()
-{
- git push origin ${1:-master} && git push gh ${1:-master} && git push gl ${1:-master}
-}
-```
-
-## Then add remotes to your local repo
-
-```bash
-cd my-gitlab-ce-repo
-
-git remote add origin git@dev.gitlab.org:gitlab/gitlabhq.git
-git remote add gh git@github.com:gitlabhq/gitlabhq.git
-git remote add gl git@gitlab.com:gitlab-org/gitlab-ce.git
-```
-
-## Push to all remotes
-
-```bash
-gpa
-```
-
-# Yanking packages from packages.gitlab.com
-
-In case something went wrong with the release and there is a need to remove the packages you can yank the packages by following the
-procedure described in [package cloud documentation](https://packagecloud.io/docs#yank_pkg).
-
-You need to have:
-
-1. `package_cloud` gem installed (sudo gem install package_cloud)
-1. Email and password for packages.gitlab.com
-1. Make sure that you are supplying the url to packages.gitlab.com (default is packagecloud.io)
-
-Example of yanking a package:
-
-```bash
-package_cloud yank --url https://packages.gitlab.com gitlab/gitlab-ce/el/6 gitlab-ce-7.10.2~omnibus-1.x86_64.rpm
-```
-
-If you are attempting this for the first time the output will look something like:
-
-```bash
-Looking for repository at gitlab/gitlab-ce... No config file exists at /Users/marin/.packagecloud. Login to create one.
-Email:
-marin@gitlab.com
-Password:
-
-Got your token. Writing a config file to /Users/marin/.packagecloud... success!
-success!
-Attempting to yank package at gitlab/gitlab-ce/el/6/gitlab-ce-7.10.2~omnibus-1.x86_64.rpm...done!
-```
diff --git a/doc/release/monthly.md b/doc/release/monthly.md
deleted file mode 100644
index 907c19e65a0..00000000000
--- a/doc/release/monthly.md
+++ /dev/null
@@ -1,245 +0,0 @@
-# Monthly Release
-
-NOTE: This is a guide used by the GitLab the company to release GitLab.
-As an end user you do not need to use this guide.
-
-The process starts 7 working days before the release.
-The release manager doesn't have to perform all the work but must ensure someone is assigned.
-The current release manager must schedule the appointment of the next release manager.
-The new release manager should create overall issue to track the progress.
-The release manager should be the only person pushing/merging commits to the x-y-stable branches.
-
-## Release Manager
-
-A release manager is selected that coordinates all releases the coming month,
-including the patch releases for previous releases.
-The release manager has to make sure all the steps below are done and delegated where necessary.
-This person should also make sure this document is kept up to date and issues are created and updated.
-
-## Take vacations into account
-
-The time is measured in weekdays to compensate for weekends.
-Do everything on time to prevent problems due to rush jobs or too little testing time.
-Make sure that you take into account any vacations of maintainers.
-If the release is falling behind immediately warn the team.
-
-## Create an overall issue and follow it
-
-Create an issue in the GitLab CE project. Name it "Release x.x" and tag it with
-the `release` label for easier searching. Replace the dates with actual dates
-based on the number of workdays before the release. All steps from issue
-template are explained below:
-
-```
-### Xth: (7 working days before the 22nd)
-
-- [ ] Triage the [Omnibus milestone]
-
-### Xth: (6 working days before the 22nd)
-
-- [ ] Determine QA person and notify this person
-- [ ] Check the tasks in [how to rc1 guide](https://dev.gitlab.org/gitlab/gitlabhq/blob/master/doc/release/howto_rc1.md) and delegate tasks if necessary
-- [ ] Merge CE `master` into EE `master` via merge request (#LINK)
-- [ ] Create CE and EE RC1 versions (#LINK)
-- [ ] Build RC1 packages
-
-### Xth: (5 working days before the 22nd)
-
-- [ ] Do QA and fix anything coming out of it (#LINK)
-- [ ] Close the [Omnibus milestone]
-- [ ] Prepare the [blog post]
-
-### Xth: (4 working days before the 22nd)
-
-- [ ] Update GitLab.com with RC1
-- [ ] Create the regression issue in the CE issue tracker:
-
- ```
- This is a meta issue to index possible regressions in this monthly release
- and any patch versions.
-
- Please do not raise or discuss issues directly in this issue but link to
- issues that might warrant a patch release. If there is a Merge Request
- that fixes the issue, please link to that as well.
-
- Please only post one regression issue and/or merge request per comment.
- Comments will be updated by the release manager as they are addressed.
- ```
-
-- [ ] Tweet about RC1 release:
-
- ```
- GitLab x.y.0.rc1 is available: https://packages.gitlab.com/gitlab/unstable
- Use at your own risk. Please link regressions issues from
- LINK_TO_REGRESSION_ISSUE
- ```
-
-### Xth: (3 working days before the 22nd)
-
-- [ ] Merge `x-y-stable` into `x-y-stable-ee`
-- [ ] Check that everyone is mentioned on the [blog post] using `@all`
-
-### Xth: (2 working days before the 22nd)
-
-- [ ] Check that MVP is added to the [MVP page]
-
-### Xth: (1 working day before the 22nd)
-
-- [ ] Merge `x-y-stable` into `x-y-stable-ee`
-- [ ] Create CE and EE release candidates
-- [ ] Create Omnibus tags and build packages for the latest release candidates
-- [ ] Update GitLab.com with the latest RC
-
-### 22nd before 1200 CET:
-
-Release before 1200 CET / 2AM PST, to make sure the majority of our users
-get the new version on the 22nd and there is sufficient time in the European
-workday to quickly fix any issues.
-
-- [ ] Merge `x-y-stable` into `x-y-stable-ee`
-- [ ] Create the 'x.y.0' tag with the [release tools](https://dev.gitlab.org/gitlab/release-tools)
-- [ ] Create the 'x.y.0' version on version.gitlab.com
-- [ ] Try to do before 1100 CET: Create and push Omnibus tags for x.y.0 (will auto-release the packages)
-- [ ] Try to do before 1200 CET: Publish the release [blog post]
-- [ ] Tweet about the release
-- [ ] Schedule a second Tweet of the release announcement with the same text at 1800 CET / 8AM PST
-
-[Omnibus milestone]: LINK_TO_OMNIBUS_MILESTONE
-[blog post]: LINK_TO_WIP_BLOG_POST
-[MVP page]: https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/source/mvp/index.html
-```
-
-- - -
-
-## Update changelog
-
-Any changes not yet added to the changelog are added by lead developer and in that merge request the complete team is
-asked if there is anything missing.
-
-There are three changelogs that need to be updated: CE, EE and CI.
-
-## Create RC1 (CE, EE, CI)
-
-[Follow this How-to guide](howto_rc1.md) to create RC1.
-
-## Prepare CHANGELOG for next release
-
-Once the stable branches have been created, update the CHANGELOG in `master` with the upcoming version, usually X.X.X.pre.
-
-On creating the stable branches, notify the core team and developers.
-
-## QA
-
-Create issue on dev.gitlab.org `gitlab` repository, named "GitLab X.X QA" in order to keep track of the progress.
-
-Use the omnibus packages created for RC1 of Enterprise Edition using [this guide](https://dev.gitlab.org/gitlab/gitlab-ee/blob/master/doc/release/manual_testing.md).
-
-**NOTE** Upgrader can only be tested when tags are pushed to all repositories. Do not forget to confirm it is working before releasing. Note that in the issue.
-
-#### Fix anything coming out of the QA
-
-Create an issue with description of a problem, if it is quick fix fix it yourself otherwise contact the team for advice.
-
-**NOTE** If there is a problem that cannot be fixed in a timely manner, reverting the feature is an option! If the feature is reverted,
-create an issue about it in order to discuss the next steps after the release.
-
-## Update GitLab.com with RC1
-
-Use the omnibus EE packages created for RC1.
-If there are big database migrations consider testing them with the production db on a VM.
-Try to deploy in the morning.
-It is important to do this as soon as possible, so we can catch any errors before we release the full version.
-
-## Create a regressions issue
-
-On [the GitLab CE issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues/) create an issue titled "GitLab X.X regressions" add the following text:
-
-This is a meta issue to discuss possible regressions in this monthly release and any patch versions.
-Please do not raise issues directly in this issue but link to issues that might warrant a patch release.
-The decision to create a patch release or not is with the release manager who is assigned to this issue.
-The release manager will comment here about the plans for patch releases.
-
-Assign the issue to the release manager and at mention all members of GitLab core team. If there are any known bugs in the release add them immediately.
-
-## Tweet about RC1
-
-Tweet about the RC release:
-
-> GitLab x.x.0.rc1 is out. This release candidate is only suitable for testing. Please link regressions issues from LINK_TO_REGRESSION_ISSUE
-
-## Prepare the blog post
-
-1. The blog post template for this release should already exist and might have comments that were added during the month.
-1. Fill out as much of the blog post template as you can.
-1. Make sure the blog post contains information about the GitLab CI release.
-1. Check the changelog of CE and EE for important changes.
-1. Also check the CI changelog
-1. Add a proposed tweet text to the blog post WIP MR description.
-1. Create a WIP MR for the blog post
-1. Make sure merge request title starts with `WIP` so it can not be accidentally merged until ready.
-1. Ask Dmitriy (or a team member with OS X) to add screenshots to the WIP MR.
-1. Decide with core team who will be the MVP user.
-1. Create WIP MR for adding MVP to MVP page on website
-1. Add a note if there are security fixes: This release fixes an important security issue and we advise everyone to upgrade as soon as possible.
-1. Create a merge request on [GitLab.com](https://gitlab.com/gitlab-com/www-gitlab-com/tree/master)
-1. Assign to one reviewer who will fix spelling issues by editing the branch (either with a git client or by using the online editor)
-1. Comment to the reviewer: '@person Please mention the whole team as soon as you are done (3 workdays before release at the latest)'
-1. Create a new merge request with complete copy of the [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md) for the next release using the branch name `release-x-x-x`.
-
-## Create CE, EE, CI stable versions
-
-Get release tools
-
-```
-git clone git@dev.gitlab.org:gitlab/release-tools.git
-cd release-tools
-```
-
-Bump version, create release tag and push to remotes:
-
-```
-bundle exec rake release["x.x.0"]
-```
-
-This will create correct version and tag and push to all CE, EE and CI remotes.
-
-Update [installation.md](/doc/install/installation.md) to the newest version in master.
-
-
-## Create Omnibus tags and build packages
-
-Follow the [release doc in the Omnibus repository](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/release.md).
-This can happen before tagging because Omnibus uses tags in its own repo and SHA1's to refer to the GitLab codebase.
-
-## Update GitLab.com with the stable version
-
-- Deploy the package (should not need downtime because of the small difference with RC1)
-- Deploy the package for gitlab.com/ci
-
-## Release CE, EE and CI
-
-__1. Publish packages for new release__
-
-Update `downloads/index.html` and `downloads/archive/index.html` in `www-gitlab-com` repository.
-
-__2. Publish blog for new release__
-
-Doublecheck the everyone has been mentioned in the blog post.
-Merge the [blog merge request](#1-prepare-the-blog-post) in `www-gitlab-com` repository.
-
-__3. Tweet to blog__
-
-Send out a tweet to share the good news with the world.
-List the most important features and link to the blog post.
-
-Proposed tweet "Release of GitLab X.X & CI Y.Y! FEATURE, FEATURE and FEATURE &lt;link-to-blog-post&gt; #gitlab"
-
-Consider creating a post on Hacker News.
-
-## Release new AMIs
-
-[Follow this guide](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md)
-
-## Create a WIP blogpost for the next release
-
-Create a WIP blogpost using [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md).
diff --git a/doc/release/patch.md b/doc/release/patch.md
deleted file mode 100644
index 1c921439156..00000000000
--- a/doc/release/patch.md
+++ /dev/null
@@ -1,81 +0,0 @@
-# Things to do when doing a patch release
-
-NOTE: This is a guide for GitLab developers. If you are trying to install GitLab
-see the latest stable [installation guide](install/installation.md) and if you
-are trying to upgrade, see the [upgrade guides](update).
-
-## When to do a patch release
-
-Patch releases are done as-needed in order to fix regressions in the current
-major release that cannot or should not wait until the next major release.
-What's included and when to release is at the discretion of the release manager.
-
-## Release Procedure
-
-### Create a patch issue
-
-Create an issue in the GitLab CE project. Name it "Release x.y.z", tag it with
-the `release` label, and assign it to the milestone of the corresponding major
-release.
-
-Use the following template:
-
-```
-- Picked into respective `stable` branches:
-- [ ] Merge `x-y-stable` into `x-y-stable-ee`
-- [ ] release-tools: `x.y.z`
-- omnibus-gitlab
- - [ ] `x.y.z+ee.0`
- - [ ] `x.y.z+ce.0`
-- [ ] Deploy
-- [ ] Add patch notice to [x.y regressions]()
-- [ ] [Blog post]()
-- [ ] [Tweet]()
-- [ ] Add entry to version.gitlab.com
-```
-
-Update the issue with links to merge requests that need to be/have been picked
-into the `stable` branches.
-
-### Preparation
-
-1. Verify that the issue can be reproduced
-1. Note in the 'GitLab X.X regressions' that you will create a patch
-1. Fix the issue on a feature branch, do this on the private GitLab development server
-1. If it is a security issue, then assign it to the release manager and apply a 'security' label
-1. Consider creating and testing workarounds
-1. After the branch is merged into master, cherry pick the commit(s) into the current stable branch
-1. Make sure that the build has passed and all tests are passing
-1. In a separate commit in the master branch update the CHANGELOG
-1. For EE, update the CHANGELOG-EE if it is EE specific fix. Otherwise, merge the stable CE branch and add to CHANGELOG-EE "Merge community edition changes for version X.X.X"
-1. Merge CE stable branch into EE stable branch
-
-### Bump version
-
-Get release tools
-
-```
-git clone git@dev.gitlab.org:gitlab/release-tools.git
-cd release-tools
-```
-
-Bump all versions in stable branch, even if the changes affect only EE, CE, or CI. Since all the versions are synced now,
-it doesn't make sense to say upgrade CE to 7.2, EE to 7.3 and CI to 7.1.
-
-Create release tag and push to remotes:
-
-```
-bundle exec rake release["x.x.x"]
-```
-
-## Release
-
-1. [Build new packages with the latest version](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/release.md)
-1. Apply the patch to GitLab.com and the private GitLab development server
-1. Apply the patch to ci.gitLab.com and the private GitLab CI development server
-1. Create and publish a blog post, see [patch release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/patch_release_blog_template.md)
-1. Send tweets about the release from `@gitlab`, tweet should include the most important feature that the release is addressing and link to the blog post
-1. Note in the 'GitLab X.X regressions' issue that the patch was published (CE only)
-1. Create the 'x.y.0' version on version.gitlab.com
-1. [Create new AMIs](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md)
-1. Create a new patch release issue for the next potential release
diff --git a/doc/release/security.md b/doc/release/security.md
deleted file mode 100644
index 118c016ba4f..00000000000
--- a/doc/release/security.md
+++ /dev/null
@@ -1,76 +0,0 @@
-# Things to do when doing an out-of-bound security release
-
-NOTE: This is a guide for GitLab developers. If you are trying to install GitLab see the latest stable [installation guide](install/installation.md) and if you are trying to upgrade, see the [upgrade guides](update).
-
-## When to do a security release
-
-Do a security release when there is a critical issue that needs to be addresses before the next monthly release. Otherwise include it in the monthly release and note there was a security fix in the release announcement.
-
-## Security vulnerability disclosure
-
-Please report suspected security vulnerabilities in private to <support@gitlab.com>, also see the [disclosure section on the GitLab.com website](https://about.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities.
-
-## Release Procedure
-
-1. Verify that the issue can be reproduced
-1. Acknowledge the issue to the researcher that disclosed it
-1. Inform the release manager that there needs to be a security release
-1. Do the steps from [patch release document](../release/patch.md), starting with "Create an issue on private GitLab development server"
-1. The MR with the security fix should get a 'security' label and be assigned to the release manager
-1. Build the package for GitLab.com and do a deploy
-1. Build the package for ci.gitLab.com and do a deploy
-1. [Create new AMIs](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md)
-1. Create feature branches for the blog post on GitLab.com and link them from the code branch
-1. Merge and publish the blog posts
-1. Send tweets about the release from `@gitlabhq`
-1. Send out an email to [the community google mailing list](https://groups.google.com/forum/#!forum/gitlabhq)
-1. Post a signed copy of our complete announcement to [oss-security](http://www.openwall.com/lists/oss-security/) and request a CVE number. CVE is only needed for bugs that allow someone to own the server (Remote Code Execution) or access to code of projects they are not a member of.
-1. Add the security researcher to the [Security Researcher Acknowledgments list](https://about.gitlab.com/vulnerability-acknowledgements/)
-1. Thank the security researcher in an email for their cooperation
-1. Update the blog post and the CHANGELOG when we receive the CVE number
-
-The timing of the code merge into master should be coordinated in advance.
-
-After the merge we strive to publish the announcements within 60 minutes.
-
-## Blog post template
-
-XXX Security Advisory for GitLab
-
-A recently discovered critical vulnerability in GitLab allows [unauthenticated API access|remote code execution|unauthorized access to repositories|XXX|PICKSOMETHING]. All users should update GitLab and gitlab-shell immediately. We [have|haven't|XXX|PICKSOMETHING|] heard of this vulnerability being actively exploited.
-
-### Version affected
-
-GitLab Community Edition XXX and lower
-
-GitLab Enterprise Edition XXX and lower
-
-### Fixed versions
-
-GitLab Community Edition XXX and up
-
-GitLab Enterprise Edition XXX and up
-
-### Impact
-
-On GitLab installations which use MySQL as their database backend it is possible for an attacker to assume the identity of any existing GitLab user in certain API calls. This attack can be performed by [unauthenticated|authenticated|XXX|PICKSOMETHING] users.
-
-### Workarounds
-
-If you are unable to upgrade you should apply the following patch and restart GitLab.
-
-XXX
-
-### Credit
-
-We want to thank XXX of XXX for the responsible disclosure of this vulnerability.
-
-## Email template
-
-We just announced a security advisory for GitLab at XXX
-
-Please contact us at support@gitlab.com if you have any questions.
-
-## Tweet template
-
-We just announced a security advisory for GitLab at XXX
diff --git a/doc/update/8.2-to-8.3.md b/doc/update/8.2-to-8.3.md
index 2ca4e1f3770..9f5c6c4dc84 100644
--- a/doc/update/8.2-to-8.3.md
+++ b/doc/update/8.2-to-8.3.md
@@ -1,5 +1,14 @@
# From 8.2 to 8.3
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
**NOTE:** GitLab 8.0 introduced several significant changes related to
installation and configuration which *are not duplicated here*. Be sure you're
already running a working version of at least 8.0 before proceeding with this
diff --git a/doc/update/8.3-to-8.4.md b/doc/update/8.3-to-8.4.md
index 269deec7a9c..9f6517d9487 100644
--- a/doc/update/8.3-to-8.4.md
+++ b/doc/update/8.3-to-8.4.md
@@ -1,5 +1,14 @@
# From 8.3 to 8.4
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
### 1. Stop server
sudo service gitlab stop
diff --git a/doc/update/8.4-to-8.5.md b/doc/update/8.4-to-8.5.md
index 0a9cb5683e7..0cb137a03cc 100644
--- a/doc/update/8.4-to-8.5.md
+++ b/doc/update/8.4-to-8.5.md
@@ -1,5 +1,14 @@
# From 8.4 to 8.5
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
### 1. Stop server
sudo service gitlab stop
diff --git a/doc/update/8.5-to-8.6.md b/doc/update/8.5-to-8.6.md
index 024f6e8a433..b9abcbd2c12 100644
--- a/doc/update/8.5-to-8.6.md
+++ b/doc/update/8.5-to-8.6.md
@@ -1,5 +1,14 @@
# From 8.5 to 8.6
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
### 1. Stop server
sudo service gitlab stop
@@ -49,11 +58,30 @@ GitLab 8.1.
```bash
cd /home/git/gitlab-workhorse
sudo -u git -H git fetch --all
-sudo -u git -H git checkout 0.6.5
+sudo -u git -H git checkout v0.7.1
sudo -u git -H make
```
-### 6. Install libs, migrations, etc.
+### 6. Updates for PostgreSQL Users
+
+Starting with 8.6 users using GitLab in combination with PostgreSQL are required
+to have the `pg_trgm` extension enabled for all GitLab databases. If you're
+using GitLab's Omnibus packages there's nothing you'll need to do manually as
+this extension is enabled automatically. Users who install GitLab without using
+Omnibus (e.g. by building from source) have to enable this extension manually.
+To enable this extension run the following SQL command as a PostgreSQL super
+user for _every_ GitLab database:
+
+```sql
+CREATE EXTENSION IF NOT EXISTS pg_trgm;
+```
+
+Certain operating systems might require the installation of extra packages for
+this extension to be available. For example, users using Ubuntu will have to
+install the `postgresql-contrib` package in order for this extension to be
+available.
+
+### 7. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
@@ -75,7 +103,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
```
-### 7. Update configuration files
+### 8. Update configuration files
#### New configuration options for `gitlab.yml`
@@ -111,25 +139,6 @@ Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
-### 8. Updates for PostgreSQL Users
-
-Starting with 8.6 users using GitLab in combination with PostgreSQL are required
-to have the `pg_trgm` extension enabled for all GitLab databases. If you're
-using GitLab's Omnibus packages there's nothing you'll need to do manually as
-this extension is enabled automatically. Users who install GitLab without using
-Omnibus (e.g. by building from source) have to enable this extension manually.
-To enable this extension run the following SQL command as a PostgreSQL super
-user for _every_ GitLab database:
-
-```sql
-CREATE EXTENSION IF NOT EXISTS pg_trgm;
-```
-
-Certain operating systems might require the installation of extra packages for
-this extension to be available. For example, users using Ubuntu will have to
-install the `postgresql-contrib` package in order for this extension to be
-available.
-
### 9. Start application
sudo service gitlab start
diff --git a/doc/update/README.md b/doc/update/README.md
index 109d5de3fa2..0241f036830 100644
--- a/doc/update/README.md
+++ b/doc/update/README.md
@@ -15,3 +15,4 @@ Depending on the installation method and your GitLab version, there are multiple
- [MySQL to PostgreSQL](mysql_to_postgresql.md) guides you through migrating your database from MySQL to PostgreSQL.
- [MySQL installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/database_mysql.md) contains additional information about configuring GitLab to work with a MySQL database.
+- [Restoring from backup after a failed upgrade](restore_after_failure.md)
diff --git a/doc/update/restore_after_failure.md b/doc/update/restore_after_failure.md
new file mode 100644
index 00000000000..01c52aae7f5
--- /dev/null
+++ b/doc/update/restore_after_failure.md
@@ -0,0 +1,83 @@
+# Restoring from backup after a failed upgrade
+
+Upgrades are usually smooth and restoring from backup is a rare occurrence.
+However, it's important to know how to recover when problems do arise.
+
+## Roll back to an earlier version and restore a backup
+
+In some cases after a failed upgrade, the fastest solution is to roll back to
+the previous version you were using.
+
+First, roll back the code or package. For source installations this involves
+checking out the older version (branch or tag). For Omnibus installations this
+means installing the older .deb or .rpm package. Then, restore from a backup.
+Follow the instructions in the
+[Backup and Restore](../raketasks/backup_restore.md#restore-a-previously-created-backup)
+documentation.
+
+## Potential problems on the next upgrade
+
+When a rollback is necessary it can produce problems on subsequent upgrade
+attempts. This is because some tables may have been added during the failed
+upgrade. If these tables are still present after you restore from the
+older backup it can lead to migration failures on future upgrades.
+
+Starting in GitLab 8.6 we drop all tables prior to importing the backup to
+prevent this problem. If you've restored a backup to a version prior to 8.6 you
+may need to manually correct the problem next time you upgrade.
+
+Example error:
+
+```
+== 20151103134857 CreateLfsObjects: migrating =================================
+-- create_table(:lfs_objects)
+rake aborted!
+StandardError: An error has occurred, this and all later migrations canceled:
+
+PG::DuplicateTable: ERROR: relation "lfs_objects" already exists
+```
+
+Copy the version from the error. In this case the version number is
+`20151103134857`.
+
+>**WARNING:** Use the following steps only if you are certain this is what you
+need to do.
+
+### GitLab 8.6+
+
+Pass the version to a database rake task to manually mark the migration as
+complete.
+
+```
+# Source install
+sudo -u git -H bundle exec rake gitlab:db:mark_migration_complete[20151103134857] RAILS_ENV=production
+
+# Omnibus install
+sudo gitlab-rake gitlab:db:mark_migration_complete[20151103134857]
+```
+
+Once the migration is successfully marked, run the rake `db:migrate` task again.
+You will likely have to repeat this process several times until all failed
+migrations are marked complete.
+
+### GitLab < 8.6
+
+```
+# Source install
+sudo -u git -H bundle exec rails console production
+
+# Omnibus install
+sudo gitlab-rails console
+```
+
+At the Rails console, type the following commands:
+
+```
+ActiveRecord::Base.connection.execute("INSERT INTO schema_migrations (version) VALUES('20151103134857')")
+exit
+```
+
+Once the migration is successfully marked, run the rake `db:migrate` task again.
+You will likely have to repeat this process several times until all failed
+migrations are marked complete.
+
diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md
index afdf1a682e2..22e207b6d32 100644
--- a/doc/web_hooks/web_hooks.md
+++ b/doc/web_hooks/web_hooks.md
@@ -58,13 +58,13 @@ X-Gitlab-Event: Push Hook
"path_with_namespace":"mike/diaspora",
"default_branch":"master",
"homepage":"http://example.com/mike/diaspora",
- "url":"git@example.com:mike/diasporadiaspora.git",
+ "url":"git@example.com:mike/diaspora.git",
"ssh_url":"git@example.com:mike/diaspora.git",
"http_url":"http://example.com/mike/diaspora.git"
},
"repository":{
"name": "Diaspora",
- "url": "git@example.com:mike/diasporadiaspora.git",
+ "url": "git@example.com:mike/diaspora.git",
"description": "",
"homepage": "http://example.com/mike/diaspora",
"git_http_url":"http://example.com/mike/diaspora.git",
@@ -113,7 +113,6 @@ Triggered when you create (or delete) tags to the repository.
X-Gitlab-Event: Tag Push Hook
```
-
**Request body:**
```json
@@ -143,7 +142,7 @@ X-Gitlab-Event: Tag Push Hook
"http_url":"http://example.com/jsmith/example.git"
},
"repository":{
- "name": "jsmith",
+ "name": "Example",
"url": "ssh://git@example.com/jsmith/example.git",
"description": "",
"homepage": "http://example.com/jsmith/example",
@@ -478,7 +477,7 @@ X-Gitlab-Event: Note Hook
},
"repository":{
"name":"diaspora",
- "url":"git@example.com:mike/diasporadiaspora.git",
+ "url":"git@example.com:mike/diaspora.git",
"description":"",
"homepage":"http://example.com/mike/diaspora"
},
diff --git a/doc/workflow/award_emoji.md b/doc/workflow/award_emoji.md
new file mode 100644
index 00000000000..70b35c58be6
--- /dev/null
+++ b/doc/workflow/award_emoji.md
@@ -0,0 +1,48 @@
+# Award emojis
+
+>**Note:**
+This feature was [introduced][1825] in GitLab 8.2.
+
+When you're collaborating online, you get fewer opportunities for high-fives
+and thumbs-ups. In order to make virtual celebrations easier, you can now vote
+on issues and merge requests using emoji!
+
+![Award emoji](img/award_emoji_select.png)
+
+This makes it much easier to give and receive feedback, without a long comment
+thread. Any comment that contains only the thumbs up or down emojis is
+converted to a vote and depicted in the emoji area.
+
+You can then use that functionality to sort issues and merge requests based on
+popularity.
+
+## Sort issues and merge requests on vote count
+
+>**Note:**
+This feature was [introduced][2871] in GitLab 8.5.
+
+You can quickly sort the issues or merge requests by the number of votes they
+have received. The sort option can be found in the right dropdown menu.
+
+![Votes sort options](img/award_emoji_votes_sort_options.png)
+
+---
+
+Sort by most popular issues/merge requests.
+
+![Votes sort by most popular](img/award_emoji_votes_most_popular.png)
+
+---
+
+Sort by least popular issues/merge requests.
+
+![Votes sort by least popular](img/award_emoji_votes_least_popular.png)
+
+---
+
+The number of upvotes and downvotes is not summed up. That means that an issue
+with 18 upvotes and 5 downvotes is considered more popular than an issue with
+17 upvotes and no downvotes.
+
+[2871]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2781
+[1825]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1825
diff --git a/doc/workflow/img/award_emoji_select.png b/doc/workflow/img/award_emoji_select.png
new file mode 100644
index 00000000000..fffdfedda5d
--- /dev/null
+++ b/doc/workflow/img/award_emoji_select.png
Binary files differ
diff --git a/doc/workflow/img/award_emoji_votes_least_popular.png b/doc/workflow/img/award_emoji_votes_least_popular.png
new file mode 100644
index 00000000000..2ef5be7154f
--- /dev/null
+++ b/doc/workflow/img/award_emoji_votes_least_popular.png
Binary files differ
diff --git a/doc/workflow/img/award_emoji_votes_most_popular.png b/doc/workflow/img/award_emoji_votes_most_popular.png
new file mode 100644
index 00000000000..5b089730d93
--- /dev/null
+++ b/doc/workflow/img/award_emoji_votes_most_popular.png
Binary files differ
diff --git a/doc/workflow/img/award_emoji_votes_sort_options.png b/doc/workflow/img/award_emoji_votes_sort_options.png
new file mode 100644
index 00000000000..9bbf3f82a0b
--- /dev/null
+++ b/doc/workflow/img/award_emoji_votes_sort_options.png
Binary files differ
diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md
index 36cb9da2380..9dc1e9b47e3 100644
--- a/doc/workflow/lfs/lfs_administration.md
+++ b/doc/workflow/lfs/lfs_administration.md
@@ -23,6 +23,10 @@ In `/etc/gitlab/gitlab.rb`:
```ruby
gitlab_rails['lfs_enabled'] = false
+
+# Optionally, change the storage path location. Defaults to
+# `#{gitlab_rails['shared_path']}/lfs-objects`. Which evaluates to
+# `/var/opt/gitlab/gitlab-rails/shared/lfs-objects` by default.
gitlab_rails['lfs_storage_path'] = "/mnt/storage/lfs-objects"
```
diff --git a/features/dashboard/todos.feature b/features/dashboard/todos.feature
index 1e7b1b50d64..8677b450813 100644
--- a/features/dashboard/todos.feature
+++ b/features/dashboard/todos.feature
@@ -36,3 +36,8 @@ Feature: Dashboard Todos
Scenario: I filter by action
Given I filter by "Mentioned"
Then I should not see todos related to "Assignments" in the list
+
+ @javascript
+ Scenario: I click on a todo row
+ Given I click on the todo
+ Then I should be directed to the corresponding page
diff --git a/features/groups.feature b/features/groups.feature
index 419a5d3963d..49e939807b5 100644
--- a/features/groups.feature
+++ b/features/groups.feature
@@ -7,10 +7,6 @@ Feature: Groups
When I visit group "NonExistentGroup" page
Then page status code should be 404
- Scenario: I should have back to group button
- When I visit group "Owned" page
- Then I should see back to dashboard button
-
@javascript
Scenario: I should see group "Owned" dashboard list
When I visit group "Owned" page
diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature
index ff21c7d1b83..de7e2b37725 100644
--- a/features/project/issues/issues.feature
+++ b/features/project/issues/issues.feature
@@ -160,6 +160,7 @@ Feature: Project Issues
Scenario: Issues on empty project
Given empty project "Empty Project"
+ And I have an ssh key
When I visit empty project page
And I see empty project details with ssh clone info
When I visit empty project's issues page
diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature
index 74685d24a7d..823658b4f24 100644
--- a/features/project/merge_requests.feature
+++ b/features/project/merge_requests.feature
@@ -325,3 +325,11 @@ Feature: Project Merge Requests
When I click the "Target branch" dropdown
And I select a new target branch
Then I should see new target branch changes
+
+ @javascript
+ Scenario: I can close merge request after commenting
+ Given I visit merge request page "Bug NS-04"
+ And I leave a comment like "XML attached"
+ Then I should see comment "XML attached"
+ And I click link "Close"
+ Then I should see closed merge request "Bug NS-04"
diff --git a/features/project/project.feature b/features/project/project.feature
index f1f3ed26065..aa22401c88e 100644
--- a/features/project/project.feature
+++ b/features/project/project.feature
@@ -18,15 +18,6 @@ Feature: Project
Then I should see the default project avatar
And I should not see the "Remove avatar" button
- Scenario: I should have back to group button
- And project "Shop" belongs to group
- And I visit project "Shop" page
- Then I should see back to group button
-
- Scenario: I should have back to group button
- And I visit project "Shop" page
- Then I should see back to dashboard button
-
Scenario: I should have readme on page
And I visit project "Shop" page
Then I should see project "Shop" README
diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb
index f4a56865532..e21af72a777 100644
--- a/features/steps/dashboard/issues.rb
+++ b/features/steps/dashboard/issues.rb
@@ -42,11 +42,10 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
end
step 'I click "All" link' do
- find('.js-author-search').click
- find('.dropdown-menu-user-full-name', match: :first).click
-
- find('.js-assignee-search').click
- find('.dropdown-menu-user-full-name', match: :first).click
+ find(".js-author-search").click
+ find(".dropdown-menu-author li a", match: :first).click
+ find(".js-assignee-search").click
+ find(".dropdown-menu-assignee li a", match: :first).click
end
def should_see(issue)
diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb
index 9722a5a848c..30b21b93ac7 100644
--- a/features/steps/dashboard/todos.rb
+++ b/features/steps/dashboard/todos.rb
@@ -41,7 +41,6 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
click_link 'Done'
end
- expect(page).to have_content 'Todo was successfully marked as done.'
expect(page).to have_content 'To do 3'
expect(page).to have_content 'Done 1'
should_not_see_todo "John Doe assigned you merge request !#{merge_request.iid}"
@@ -89,6 +88,14 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
should_not_see_todo "John Doe assigned you issue ##{issue.iid}"
end
+ step 'I click on the todo' do
+ find('.todo:nth-child(1)').click
+ end
+
+ step 'I should be directed to the corresponding page' do
+ page.should have_css('.identifier', text: 'Merge Request !1')
+ end
+
def should_see_todo(position, title, body, pending = true)
page.within(".todo:nth-child(#{position})") do
expect(page).to have_content title
diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb
index a167d259837..b6ce5bc9cec 100644
--- a/features/steps/group/milestones.rb
+++ b/features/steps/group/milestones.rb
@@ -5,7 +5,9 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
include SharedUser
step 'I click on group milestones' do
- click_link 'Milestones'
+ page.within '.nav-secondary' do
+ click_link("Milestones")
+ end
end
step 'I should see group milestones index page has no milestones' do
diff --git a/features/steps/groups.rb b/features/steps/groups.rb
index 7a6ae15ffa5..483370f41c6 100644
--- a/features/steps/groups.rb
+++ b/features/steps/groups.rb
@@ -4,10 +4,6 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
include SharedGroup
include SharedUser
- step 'I should see back to dashboard button' do
- expect(page).to have_content 'Go to dashboard'
- end
-
step 'I should see group "Owned"' do
expect(page).to have_content '@owned'
end
@@ -35,7 +31,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
end
step 'I should see projects activity feed' do
- expect(page).to have_content 'closed issue'
+ expect(page).to have_content 'joined project'
end
step 'I should see issues from group "Owned" assigned to me' do
diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb
index 19d81453d8c..4584fc4d754 100644
--- a/features/steps/project/active_tab.rb
+++ b/features/steps/project/active_tab.rb
@@ -82,7 +82,9 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
# Sub Tabs: Issues
step 'I click the "Milestones" tab' do
- click_link('Milestones')
+ page.within '.nav-secondary' do
+ click_link('Milestones')
+ end
end
step 'I click the "Labels" tab' do
diff --git a/features/steps/project/create.rb b/features/steps/project/create.rb
index 8a0e8fc2b6c..422b151eaa2 100644
--- a/features/steps/project/create.rb
+++ b/features/steps/project/create.rb
@@ -27,7 +27,7 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps
step 'I click on HTTP' do
find('#clone-dropdown').click
- find('#http-selector').click
+ find('.http-selector').click
end
step 'Remote url should update to http link' do
@@ -36,7 +36,7 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps
step 'If I click on SSH' do
find('#clone-dropdown').click
- find('#ssh-selector').click
+ find('.ssh-selector').click
end
step 'Remote url should update to ssh link' do
diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb
index 527f7853da9..d9b16afa9b8 100644
--- a/features/steps/project/fork.rb
+++ b/features/steps/project/fork.rb
@@ -36,7 +36,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
end
step 'I goto the Merge Requests page' do
- page.within '.page-sidebar-expanded' do
+ page.within '.nav-secondary' do
click_link "Merge Requests"
end
end
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index 8c31fa890b2..aff5ca676be 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -5,6 +5,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
include SharedNote
include SharedPaths
include SharedMarkdown
+ include SharedUser
step 'I should see "Release 0.4" in issues' do
expect(page).to have_content "Release 0.4"
@@ -240,7 +241,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'empty project "Empty Project"' do
- create :empty_project, name: 'Empty Project', namespace: @user.namespace
+ create :project_empty_repo, name: 'Empty Project', namespace: @user.namespace
end
When 'I visit empty project page' do
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index 91fe19dd477..a4f02b590ea 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -326,7 +326,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I should see a discussion has started on diff' do
page.within(".notes .discussion") do
- page.should have_content "#{current_user.name} started a discussion"
+ page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion"
page.should have_content sample_commit.line_code_path
page.should have_content "Line is wrong"
end
@@ -334,7 +334,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I should see a discussion by user "John Doe" has started on diff' do
page.within(".notes .discussion") do
- page.should have_content "#{user_exists("John Doe").name} started a discussion"
+ page.should have_content "#{user_exists("John Doe").name} #{user_exists("John Doe").to_reference} started a discussion"
page.should have_content sample_commit.line_code_path
page.should have_content "Line is wrong"
end
@@ -350,7 +350,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I should see a discussion has started on commit diff' do
page.within(".notes .discussion") do
- page.should have_content "#{current_user.name} started a discussion on commit"
+ page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion on commit"
page.should have_content sample_commit.line_code_path
page.should have_content "Line is wrong"
end
@@ -358,7 +358,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I should see a discussion has started on commit' do
page.within(".notes .discussion") do
- page.should have_content "#{current_user.name} started a discussion on commit"
+ page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion on commit"
page.should have_content "One comment to rule them all"
end
end
diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb
index ef185861e00..8f1d4a223a9 100644
--- a/features/steps/project/project.rb
+++ b/features/steps/project/project.rb
@@ -114,7 +114,9 @@ class Spinach::Features::Project < Spinach::FeatureSteps
end
step 'I should not see "Snippets" button' do
- expect(page).not_to have_link 'Snippets'
+ page.within '.nav-secondary' do
+ expect(page).not_to have_link 'Snippets'
+ end
end
step 'project "Shop" belongs to group' do
@@ -123,14 +125,6 @@ class Spinach::Features::Project < Spinach::FeatureSteps
@project.save!
end
- step 'I should see back to dashboard button' do
- expect(page).to have_content 'Go to dashboard'
- end
-
- step 'I should see back to group button' do
- expect(page).to have_content 'Go to group'
- end
-
step 'I click notifications drop down button' do
click_link 'notifications-button'
end
diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb
index 4fc2ece79ff..fa7d24ce611 100644
--- a/features/steps/shared/project_tab.rb
+++ b/features/steps/shared/project_tab.rb
@@ -41,7 +41,7 @@ module SharedProjectTab
end
step 'the active main tab should be Settings' do
- page.within '.nav-sidebar' do
+ page.within '.nav-secondary' do
expect(page).to have_content('Go to project')
end
end
diff --git a/fixtures/emojis/digests.json b/fixtures/emojis/digests.json
new file mode 100644
index 00000000000..18d6e93e0f4
--- /dev/null
+++ b/fixtures/emojis/digests.json
@@ -0,0 +1,8597 @@
+[
+ {
+ "name": "100",
+ "unicode": "1F4AF",
+ "digest": "6d57c7cc93335f853e1a5670233f121bc94730dbd82b2b3c5c5a509e092ef0fd"
+ },
+ {
+ "name": "1234",
+ "unicode": "1F522",
+ "digest": "727763fd9f18fd5df59e9f78e678ea4ec753e674d70f15d4e77c7802067d660b"
+ },
+ {
+ "name": "8ball",
+ "unicode": "1F3B1",
+ "digest": "1aecf21951452ba24e921ec71b3d313b7ddc2e185b0339c9e0eebc85be4f031d"
+ },
+ {
+ "name": "a",
+ "unicode": "1F170",
+ "digest": "2272113a5bcb7faf8db7c1bd35df576d32f2f7cbd881463934ad3382eb87c723"
+ },
+ {
+ "name": "ab",
+ "unicode": "1F18E",
+ "digest": "6f8a237751fdc84db4121f408272d9a23258515449610e4c6c54f50f6e995627"
+ },
+ {
+ "name": "abc",
+ "unicode": "1F524",
+ "digest": "652a2381a7b587d8a52d5178e2d7d6c8600b33d36160fa69677943da374105bc"
+ },
+ {
+ "name": "abcd",
+ "unicode": "1F521",
+ "digest": "35ade4fd3d75294ebb72c24490aa32745604edc6cabe095b90634cd3ce78c07b"
+ },
+ {
+ "name": "accept",
+ "unicode": "1F251",
+ "digest": "8212ed158cc447c92813273fc915e84d3d5c4c48d1b38e498c088bad27ab8145"
+ },
+ {
+ "name": "aerial_tramway",
+ "unicode": "1F6A1",
+ "digest": "8039d7f67e6e5b211066cab6cf2142afc3aca5c830a357369362c9b484029563"
+ },
+ {
+ "name": "airplane",
+ "unicode": "2708",
+ "digest": "18f4dfac323555d8cdabb79148874c0185ce98e1a08e69414d236b23e502a854"
+ },
+ {
+ "name": "airplane_arriving",
+ "unicode": "1F6EC",
+ "digest": "9a1c81d97512e5d0e3acec40290d00f616ec182140909859e366a734b9f840bb"
+ },
+ {
+ "name": "airplane_departure",
+ "unicode": "1F6EB",
+ "digest": "e3c5ff4038db998c1897cb237d0b865da0bc60331c758f204e45a979d5fab445"
+ },
+ {
+ "name": "airplane_northeast",
+ "unicode": "1F6EA",
+ "digest": "fdddc2cd3618ec6661612581b8b93553cb086b0bb197e96aedf1bee8055e7bb4"
+ },
+ {
+ "name": "airplane_small",
+ "unicode": "1F6E9",
+ "digest": "f98b44422d6bf505b50330805ecf68013d035341f0b6487c3c05ad913eb5abd3"
+ },
+ {
+ "name": "airplane_small_up",
+ "unicode": "1F6E8",
+ "digest": "029752b29a757c087dec60f45ea242e974fc181129e20390d5d4a2f90442091a"
+ },
+ {
+ "name": "airplane_up",
+ "unicode": "1F6E7",
+ "digest": "ec45d4dbfce1f75dc59339417b1dcf5f1e1359cd9d04ff233babf359a3330e77"
+ },
+ {
+ "name": "alarm_clock",
+ "unicode": "23F0",
+ "digest": "84ddd7b3b857c165410b7b44863e5354ca0f3591c3bfe56231f12c9f7531a96f"
+ },
+ {
+ "name": "alembic",
+ "unicode": "2697",
+ "digest": "45698914a21683f06931d807af171bcb6984e5ebce66012bba71b467565bd69d"
+ },
+ {
+ "name": "alien",
+ "unicode": "1F47D",
+ "digest": "94dbe4e90614c654145aba93610c43e3ab86df8ca07391bd4e56383f9329c008"
+ },
+ {
+ "name": "ambulance",
+ "unicode": "1F691",
+ "digest": "82ef36bcd13c88a4b2397c918b8048adc6bf045ed2532ff568e0dfd1b1b29c3c"
+ },
+ {
+ "name": "amphora",
+ "unicode": "1F3FA",
+ "digest": "d3758d88aa1fc3be01894102f57479d3a49790510d38ad3d06a2774962010608"
+ },
+ {
+ "name": "anchor",
+ "unicode": "2693",
+ "digest": "27c6034f769d9f020362fc5b227b9279651cc940861e727d1f6ccd59af98f851"
+ },
+ {
+ "name": "angel",
+ "unicode": "1F47C",
+ "digest": "c1b8ad2adc7686e7fbbe4ec357071e7228a5e0762e001bb589e2f97ff258d5c7"
+ },
+ {
+ "name": "angel_tone1",
+ "unicode": "1F47C-1F3FB",
+ "digest": "90b701c43311b1096c4a012d9905a186f1a16829ea2707921a8418c28617d751"
+ },
+ {
+ "name": "angel_tone2",
+ "unicode": "1F47C-1F3FC",
+ "digest": "d6bcaf1b76e25d486d4ab9b159cf727782d508543d1ae27c8d2c12d2f13d6eb0"
+ },
+ {
+ "name": "angel_tone3",
+ "unicode": "1F47C-1F3FD",
+ "digest": "3069285e6218c8083cb0085aa10017bcdea033e321d97ba339a84892074b903a"
+ },
+ {
+ "name": "angel_tone4",
+ "unicode": "1F47C-1F3FE",
+ "digest": "dbb87019752d9caa94ce086858c1e3225b62e221ad599f5106548fda2456fc2b"
+ },
+ {
+ "name": "angel_tone5",
+ "unicode": "1F47C-1F3FF",
+ "digest": "f77703df97720c27a128b5f3c0948b9e04a6b6b81ea5306468154f9bf56225db"
+ },
+ {
+ "name": "anger",
+ "unicode": "1F4A2",
+ "digest": "2253b7ff0894f247bc6f04d841a748c56d6c94684880c13df42387691ff20e75"
+ },
+ {
+ "name": "anger_left",
+ "unicode": "1F5EE",
+ "digest": "f2711991e8b386b2d5b12f296ce20a9b4b00ef91d6d67af2cf4e06abf2faa1dc"
+ },
+ {
+ "name": "anger_right",
+ "unicode": "1F5EF",
+ "digest": "24b572d64c519251a3ae8844e8d66fd6955752aff99aebe7dc20179505a466c4"
+ },
+ {
+ "name": "angry",
+ "unicode": "1F620",
+ "digest": "c4188ba70df99d8ccef5706d711176725d3dd50d62f065a177d68d85c7828107"
+ },
+ {
+ "name": "anguished",
+ "unicode": "1F627",
+ "digest": "9c2347308133ae50dc04da62042fff847f4c477b2956b8aa976f0413899e38bc"
+ },
+ {
+ "name": "ant",
+ "unicode": "1F41C",
+ "digest": "d2af2ed1cfe15d649aa329d965764a1e8726941d833841781a5b66d7dd0b0921"
+ },
+ {
+ "name": "apple",
+ "unicode": "1F34E",
+ "digest": "a9babee24f454934a5e1fb8d781cbce354dfd88e8a8e01f02e8b30071fd40460"
+ },
+ {
+ "name": "aquarius",
+ "unicode": "2652",
+ "digest": "1a168c252678847d1f9ef450887489e3bdc207ecae4b6fb05e92295ff861ae2c"
+ },
+ {
+ "name": "aries",
+ "unicode": "2648",
+ "digest": "bde262a8795e12f8b0ebb3f0f8c3a56104062fcee8d5d678cf4bb445a7daf698"
+ },
+ {
+ "name": "arrow_backward",
+ "unicode": "25C0",
+ "digest": "ddae36d1febf5c246e51d599e2898a8aa30cd47f88b5bcb469e3ca9d22538b97"
+ },
+ {
+ "name": "arrow_double_down",
+ "unicode": "23EC",
+ "digest": "906f42b5f788128ed90d2d162cf03e6e595a50ad05e0aa5f64e925637379d0cd"
+ },
+ {
+ "name": "arrow_double_up",
+ "unicode": "23EB",
+ "digest": "2129a57402980de6fc6f59ad8354525c2dbcd66d1b78f4de091181ddc81e0693"
+ },
+ {
+ "name": "arrow_down",
+ "unicode": "2B07",
+ "digest": "370e4f41565d5dab245c20e45c502505a56d26c2392283781b841eb3e905edb2"
+ },
+ {
+ "name": "arrow_down_small",
+ "unicode": "1F53D",
+ "digest": "98a2b183f2daec425160bbfce1d2b940b8baa0d5032fdacfa9453e39bed5651b"
+ },
+ {
+ "name": "arrow_forward",
+ "unicode": "25B6",
+ "digest": "348627b8e0f55cf1e9ab19c9de1d170371b2c4cb4dda9a2aa8e0c558db08b18a"
+ },
+ {
+ "name": "arrow_heading_down",
+ "unicode": "2935",
+ "digest": "96c64953fc3134711247bef320f252c48993ebc90494925b7fee42ffce2a2ec2"
+ },
+ {
+ "name": "arrow_heading_up",
+ "unicode": "2934",
+ "digest": "94f94e74176cc050703b3584f3f700debf86e4e61b893a441825a21fa3f8ce74"
+ },
+ {
+ "name": "arrow_left",
+ "unicode": "2B05",
+ "digest": "4553be62a63d7550deac4f7dbeffce6006f769ae6cddfb8c795671672011ba0b"
+ },
+ {
+ "name": "arrow_lower_left",
+ "unicode": "2199",
+ "digest": "10f83c252110d705cdcfebc35a70c341ad288730d0c0729479e3a96e263d5120"
+ },
+ {
+ "name": "arrow_lower_right",
+ "unicode": "2198",
+ "digest": "ee33abd4c96c19e9b80a2fc1500ba8ecaa6668c49310cc816a496e8c61af3850"
+ },
+ {
+ "name": "arrow_right",
+ "unicode": "27A1",
+ "digest": "2611e9138a2651916f414015d0287f5f0af266514d96a42915d32b04fb652a90"
+ },
+ {
+ "name": "arrow_right_hook",
+ "unicode": "21AA",
+ "digest": "628b06384a2963a4fe81e9fbf4e22511f697878d9b9db7d2fc98f8aadbe8f4f9"
+ },
+ {
+ "name": "arrow_up",
+ "unicode": "2B06",
+ "digest": "c09e5f41c01028b45707c525d30d3d6731ec57b7447f0d7ba4ad6c1404449e5c"
+ },
+ {
+ "name": "arrow_up_down",
+ "unicode": "2195",
+ "digest": "e7fd92d24a01702f76c7fcc0de998bc81fbfb93711d076984f6da91d1dccd84c"
+ },
+ {
+ "name": "arrow_up_small",
+ "unicode": "1F53C",
+ "digest": "bc48dad74bc1d0c5579cbf5e3d005314b0d21bc5b5ebbba2b05136e33f49296d"
+ },
+ {
+ "name": "arrow_upper_left",
+ "unicode": "2196",
+ "digest": "792a9709f03843024e53d201cb4769c59b656c3bf0dff2306e8e605493a66b93"
+ },
+ {
+ "name": "arrow_upper_right",
+ "unicode": "2197",
+ "digest": "ee934b0c9cff270efd30a6cafc15253d405efd2c93b4785ac2ed4ea6420266a6"
+ },
+ {
+ "name": "arrows_clockwise",
+ "unicode": "1F503",
+ "digest": "914f4120513730d7a19c9f8c4e59223a90568de0b25a225b712b31fa9697ef4f"
+ },
+ {
+ "name": "arrows_counterclockwise",
+ "unicode": "1F504",
+ "digest": "86d87597e4e3db6dbba9907ee82412db0cbab1ea875bd0be6505dd886dc19b90"
+ },
+ {
+ "name": "art",
+ "unicode": "1F3A8",
+ "digest": "dfc6b0da780199df86507d65b0499ba1706c266ae7badcb0e7fb5b85af7c9578"
+ },
+ {
+ "name": "articulated_lorry",
+ "unicode": "1F69B",
+ "digest": "4c4de240ebd175f7b53453eda4e51f2e57d0db2a98d317f804116e14e47cff1d"
+ },
+ {
+ "name": "ascending_notes",
+ "unicode": "1F39C",
+ "digest": "33432042771d456338dda5d98e49322d3600f2cc9049963480c7c38d9de1ef0a"
+ },
+ {
+ "name": "asterisk",
+ "unicode": "002A-20E3",
+ "digest": "0b7f27f545b616677c83d40ff957337477b2881459b4d3c839ae55e23797419f"
+ },
+ {
+ "name": "astonished",
+ "unicode": "1F632",
+ "digest": "58632b97e274ade5183752db2b3c5c4fe29effcd5a9720a8d01fa809b97023dc"
+ },
+ {
+ "name": "athletic_shoe",
+ "unicode": "1F45F",
+ "digest": "1fc55d85a4d6751f9e60467801b051d2fb3341bdcc33b8d3695d5143359edb43"
+ },
+ {
+ "name": "atm",
+ "unicode": "1F3E7",
+ "digest": "bf827ef6c349f5b6912d821457975a4720d1750529d907e94ece429b7a388d7e"
+ },
+ {
+ "name": "atom",
+ "unicode": "269B",
+ "digest": "cbce1725602efbb77a935cfae5407e4d75489ee988910296c7f6140665afc669"
+ },
+ {
+ "name": "b",
+ "unicode": "1F171",
+ "digest": "9116256b3189977e37f6da7ddedf82bb29b0358829a4e8718fd59e51d9b86b3c"
+ },
+ {
+ "name": "baby",
+ "unicode": "1F476",
+ "digest": "66596bea11015154e0b1752b85f349f4286c6643ee6f51ee5e60e0d625c4ae9a"
+ },
+ {
+ "name": "baby_bottle",
+ "unicode": "1F37C",
+ "digest": "ed42994b4a539b8bfeccde0f3c7e9c7f54d6696ff48ce7e48171bbab51002348"
+ },
+ {
+ "name": "baby_chick",
+ "unicode": "1F424",
+ "digest": "ea2cfa0e5c2cbff5fffdb52cc04dfe7872834bd7cfeaa45e0541b8faffcbd0e9"
+ },
+ {
+ "name": "baby_symbol",
+ "unicode": "1F6BC",
+ "digest": "65df04dff8739b86f7663ae9c0648927341f360a986655e109721b0e16013b75"
+ },
+ {
+ "name": "baby_tone1",
+ "unicode": "1F476-1F3FB",
+ "digest": "bc747527a2d723cf99ef3fc2539c19d29634c92ff417736982d3bf87d65d06eb"
+ },
+ {
+ "name": "baby_tone2",
+ "unicode": "1F476-1F3FC",
+ "digest": "b82bba7a666b7d070751726e54acc7fb8f96e2dfc09e9610d61cfd20947aef9c"
+ },
+ {
+ "name": "baby_tone3",
+ "unicode": "1F476-1F3FD",
+ "digest": "7f45dfd4ea2ae8515d419ffa13e7ee5c625b024b4e521ace5344c414bb929da0"
+ },
+ {
+ "name": "baby_tone4",
+ "unicode": "1F476-1F3FE",
+ "digest": "80b1854626616f15426649cc6415e4911a55c8f761422fe48a08af9e8ac6a7cb"
+ },
+ {
+ "name": "baby_tone5",
+ "unicode": "1F476-1F3FF",
+ "digest": "9f890804d19a61bee76a29644c818045dd96cf69d67cfbca2d11f4ad376b27da"
+ },
+ {
+ "name": "back",
+ "unicode": "1F519",
+ "digest": "1dc73947b8f56e033777ca3f747407923bd16b07e53a6c78b09950ca474b7e7a"
+ },
+ {
+ "name": "badminton",
+ "unicode": "1F3F8",
+ "digest": "3f95180c1175d0248ebf4b8650cf86566c39e0486d828078244080194c14d4fe"
+ },
+ {
+ "name": "baggage_claim",
+ "unicode": "1F6C4",
+ "digest": "7c1a69511aa2a93984d601da4d1cef1cb4cefbbf127b1486278da8c01345bbf3"
+ },
+ {
+ "name": "balloon",
+ "unicode": "1F388",
+ "digest": "a10c2b0865179cdbdef339494ec9b2a109451a356e53738d6a9dd43232500956"
+ },
+ {
+ "name": "ballot_box",
+ "unicode": "1F5F3",
+ "digest": "0455ea75612efe78354315b4c345953d2d559bb471d5b01c1adc1d6b74ed693a"
+ },
+ {
+ "name": "ballot_box_check",
+ "unicode": "1F5F9",
+ "digest": "fc3ba16c009d963a4a0ea20a348ac98eee3c4c18c481df19a5ada0d1de7fcc15"
+ },
+ {
+ "name": "ballot_box_with_check",
+ "unicode": "2611",
+ "digest": "5f5cec7fe462557d31e8d2b836534c1e76d546cc0061236fa2af3667972b84aa"
+ },
+ {
+ "name": "ballot_box_x",
+ "unicode": "1F5F5",
+ "digest": "861dcfc2361298262587b5d0e163fed96a55c44636361f5b4a9ab1d6502b8928"
+ },
+ {
+ "name": "ballot_x",
+ "unicode": "1F5F4",
+ "digest": "0b73b89847eb82bcad5664644c8af237e0aef6c3d8c94b7a5df94e05d0ebf4e1"
+ },
+ {
+ "name": "bamboo",
+ "unicode": "1F38D",
+ "digest": "feb0cf2f1012a1c0649b8c66f7e96e2d8bcdefe879c5a52dab3e25c51009e3b2"
+ },
+ {
+ "name": "banana",
+ "unicode": "1F34C",
+ "digest": "aa9a1e6db00efa94a7f414c570eff7fc29011be64031a24d03b7f37b617cfd2d"
+ },
+ {
+ "name": "bangbang",
+ "unicode": "203C",
+ "digest": "bdd350766ccd1c0138f6294f7ebfa3e9867b02bda40a743f7062e52c68358765"
+ },
+ {
+ "name": "bank",
+ "unicode": "1F3E6",
+ "digest": "c9648c93049cf8e7884242e58ae3145383d2e5034c9090e0d34c53f5bbce397f"
+ },
+ {
+ "name": "bar_chart",
+ "unicode": "1F4CA",
+ "digest": "942277f72a5b754b13454dab62c85b1ff3447544f38ec76a285f3be32f6f5d12"
+ },
+ {
+ "name": "barber",
+ "unicode": "1F488",
+ "digest": "e1526eea685aafc56fb83d07f8ff63c9967600e447b0e5f831a17d6153f2062d"
+ },
+ {
+ "name": "baseball",
+ "unicode": "26BE",
+ "digest": "3d028b16a898f3a15874bc9d3891f9fbf59ea1c226c5c774eddb58a712c489ae"
+ },
+ {
+ "name": "basketball",
+ "unicode": "1F3C0",
+ "digest": "b2f5a3904d505db066337a24fc840ef75b49ef4c5f152227d8e632ff82285b12"
+ },
+ {
+ "name": "basketball_player",
+ "unicode": "26F9",
+ "digest": "e94beb69f631667479a80095bf313ceb3aa109d6ebb80f182722360a6d2a214e"
+ },
+ {
+ "name": "basketball_player_tone1",
+ "unicode": "26F9-1F3FB",
+ "digest": "6fc77cf2f26ee18e9a3faea500d4277839f77633f31ee618a68c301f1ad32d90"
+ },
+ {
+ "name": "basketball_player_tone2",
+ "unicode": "26F9-1F3FC",
+ "digest": "6ee9060c24d92708e12a854fb0bdf5c717c90b8c0350d8aa40c278b41bfa12fc"
+ },
+ {
+ "name": "basketball_player_tone3",
+ "unicode": "26F9-1F3FD",
+ "digest": "752e90dbfa7c7a9ae3f37de924e22f3c3d5a7e54dd41c8e8eb99cabb0dad73cf"
+ },
+ {
+ "name": "basketball_player_tone4",
+ "unicode": "26F9-1F3FE",
+ "digest": "38bedc3074e6243454d568d9b665f5764f1a3d983875651ce7a1cdb53da9f6c8"
+ },
+ {
+ "name": "basketball_player_tone5",
+ "unicode": "26F9-1F3FF",
+ "digest": "25ee1e84670d3db96d3ad098c859abd6b3448f55f668ce0c195ee2337a215de7"
+ },
+ {
+ "name": "bath",
+ "unicode": "1F6C0",
+ "digest": "ae6301a6354630cd9dc06a5137f23f826d019c8298b2b012b6ff31b773a910b6"
+ },
+ {
+ "name": "bath_tone1",
+ "unicode": "1F6C0-1F3FB",
+ "digest": "fce7ae2e7ef3f7f44f36c2ad49348b4cf7fce0b0c17e1a90a1e85734cee95b2a"
+ },
+ {
+ "name": "bath_tone2",
+ "unicode": "1F6C0-1F3FC",
+ "digest": "4d1c9444f16467488fe939fdad279d6855d28be564e5dcc1990451c4b9ae8c95"
+ },
+ {
+ "name": "bath_tone3",
+ "unicode": "1F6C0-1F3FD",
+ "digest": "9a59a4360effb48af4cbb1a953655ef61e69375407038b4d0bd8068fbaf3cc16"
+ },
+ {
+ "name": "bath_tone4",
+ "unicode": "1F6C0-1F3FE",
+ "digest": "01aafa8a53a08018b9fbf28ec6b3b918d6bd0dee7a891196f32f81f60d114f0e"
+ },
+ {
+ "name": "bath_tone5",
+ "unicode": "1F6C0-1F3FF",
+ "digest": "2733e81ccaee21231c2e47e3310b431e9bd784bf34f0db609f8eadcee359500d"
+ },
+ {
+ "name": "bathtub",
+ "unicode": "1F6C1",
+ "digest": "9515e3bb9ab41350305e64fc6877aae82d51e1ba8ce8b2b4b8ffaeda960820cd"
+ },
+ {
+ "name": "battery",
+ "unicode": "1F50B",
+ "digest": "7d4d475c1d5b1be55c319953e3363ff864fe4fcd921a8aa649b9a547c0894deb"
+ },
+ {
+ "name": "beach",
+ "unicode": "1F3D6",
+ "digest": "52855d75cfa4476ccc23c58b4afcb76ee48abb22a9a6081210c8accefdf33099"
+ },
+ {
+ "name": "beach_umbrella",
+ "unicode": "26F1",
+ "digest": "cefe8e195d21d3e0769d3bfe15170db9e57c86db9d31cacb19fcdc8d2191b661"
+ },
+ {
+ "name": "bear",
+ "unicode": "1F43B",
+ "digest": "b5ac126875c20c82b9e3140b143233944a2e4132d781d0b575e83673988523cb"
+ },
+ {
+ "name": "bed",
+ "unicode": "1F6CF",
+ "digest": "1919245d7a76799aad0533eb72db2cbaa1f32ee8231a0c1989d3f233f2d42370"
+ },
+ {
+ "name": "bee",
+ "unicode": "1F41D",
+ "digest": "69ada63403c8dabae39c63ba143143aeb59b66faae6aa82d8342337925a9e6b5"
+ },
+ {
+ "name": "beer",
+ "unicode": "1F37A",
+ "digest": "b71dd6efdb4ce7d9d71fdbf82a2ccf83841fb0cceb119ee7da1e575d3bfa853c"
+ },
+ {
+ "name": "beers",
+ "unicode": "1F37B",
+ "digest": "994108cebfe0c614c05967af4e3864d8adbbfcf7cccef1cbd42a47b7dfabf80c"
+ },
+ {
+ "name": "beetle",
+ "unicode": "1F41E",
+ "digest": "ec351ce238a81711eef00e5be1de2e198423cf524b60e531d435902b44420edc"
+ },
+ {
+ "name": "beginner",
+ "unicode": "1F530",
+ "digest": "13288d9fc221dc02f4181b998104e13c3c5c98d3c4e650186bef59a46d39f6f0"
+ },
+ {
+ "name": "bell",
+ "unicode": "1F514",
+ "digest": "784b9a82814ce14a264e54b3a8f8e706f3c7b763646d9f8174c4aa84ad41ef09"
+ },
+ {
+ "name": "bellhop",
+ "unicode": "1F6CE",
+ "digest": "c15455f1b52ac26404b5c13a0e1070212ed1830026422873f4f6335e26e31259"
+ },
+ {
+ "name": "bento",
+ "unicode": "1F371",
+ "digest": "d59314b17a8646d4a78fefb7b79f289f33d4aaea893fed4cad0b890df63395e7"
+ },
+ {
+ "name": "bicyclist",
+ "unicode": "1F6B4",
+ "digest": "e7359d615d40325bb08a145cfebde2ecef448deeb21695a34b55d3ccb971447f"
+ },
+ {
+ "name": "bicyclist_tone1",
+ "unicode": "1F6B4-1F3FB",
+ "digest": "e45808faa32f4ffb881d3569c0b8e2c69d4a64665f4d1fae24d7a1e5f1d3ea4b"
+ },
+ {
+ "name": "bicyclist_tone2",
+ "unicode": "1F6B4-1F3FC",
+ "digest": "92a3494270d1da6a117e92402c7898d4a7fffbe3d6143fb9ae445c4827c0c8a4"
+ },
+ {
+ "name": "bicyclist_tone3",
+ "unicode": "1F6B4-1F3FD",
+ "digest": "6fdf1db2bbd08d06b643b08f0f29daeaa20e0b8c8abec21132191f435cc05e42"
+ },
+ {
+ "name": "bicyclist_tone4",
+ "unicode": "1F6B4-1F3FE",
+ "digest": "d9c27848e1bcc8197c858e1ef12a537f4ed6c77fb211b6731388dc88c2bb7a61"
+ },
+ {
+ "name": "bicyclist_tone5",
+ "unicode": "1F6B4-1F3FF",
+ "digest": "4892af1a8a0229a813d7b8e3d88481c2365e3e1a5ce2e0e27ce432c5336da810"
+ },
+ {
+ "name": "bike",
+ "unicode": "1F6B2",
+ "digest": "e726f97b5432f46ed51328c0930d1d63b3a2d7b67c5c2303a5ca997083cfcac1"
+ },
+ {
+ "name": "bikini",
+ "unicode": "1F459",
+ "digest": "7612fcb72c005ae7172260825f588d6995f2bc919cb3d283dd4591f6872a1855"
+ },
+ {
+ "name": "biohazard",
+ "unicode": "2623",
+ "digest": "81f8309318051255ed4dc18855a3cd3f8657a6f3b2d368caa531a57ce0e34235"
+ },
+ {
+ "name": "bird",
+ "unicode": "1F426",
+ "digest": "3f219e5aa18e2f1febfd368ec133786cd2eab357db79984cb8ba07fed0eec7cd"
+ },
+ {
+ "name": "birthday",
+ "unicode": "1F382",
+ "digest": "9eb1adb0170ab851042cb3da8b64f02f4e4b63e7a07db405b55b50f5bbd3cacf"
+ },
+ {
+ "name": "black_circle",
+ "unicode": "26AB",
+ "digest": "c2ba672994ad0f99d7fdc449f3fee45a2dca68a58f9fe95825b38465a30ef44e"
+ },
+ {
+ "name": "black_joker",
+ "unicode": "1F0CF",
+ "digest": "1eb85b8e2b93dec221a97a1c309dee3683408f6166e1a1a1bd83cf2f64f007dd"
+ },
+ {
+ "name": "black_large_square",
+ "unicode": "2B1B",
+ "digest": "0ff2112227c38ed8c30b0bddf2300e87d2a244cd7fe81886a1cb1a287a7e8bb6"
+ },
+ {
+ "name": "black_medium_small_square",
+ "unicode": "25FE",
+ "digest": "f1010aa694084ad4655a9d4ce5a1711eaab21029e31bf8798253f0ad644e8abb"
+ },
+ {
+ "name": "black_medium_square",
+ "unicode": "25FC",
+ "digest": "06bf48ffbc84e71bbb90aa0f6c3f9f53533c6fd063ff168cefdb0a050dcf8302"
+ },
+ {
+ "name": "black_nib",
+ "unicode": "2712",
+ "digest": "c1361df4a5ae9f2ed121d26928021e96c6865331861e1960700d39cb1bd49355"
+ },
+ {
+ "name": "black_small_square",
+ "unicode": "25AA",
+ "digest": "d430ec419869fa1b5ba980ddeecb4c5ad5050a2b3421e45048cc184a6fc46899"
+ },
+ {
+ "name": "black_square_button",
+ "unicode": "1F532",
+ "digest": "85b6587b6b2c3544ddb7bc07207b0740e437744ba134835836153899ae396135"
+ },
+ {
+ "name": "blossom",
+ "unicode": "1F33C",
+ "digest": "029bbe385e07e2017dd918d685e107678c9c0e919a3bd1521b7a0d7c9172da05"
+ },
+ {
+ "name": "blowfish",
+ "unicode": "1F421",
+ "digest": "b5ee9f6ffabb74e3024067f016d17a631ee98536cb9c7269d55fa867f95a54fb"
+ },
+ {
+ "name": "blue_book",
+ "unicode": "1F4D8",
+ "digest": "6fbf227fb9facc1957bb9dfb31749cbfe66c3afe8081347f2471fd64ef2e6b3a"
+ },
+ {
+ "name": "blue_car",
+ "unicode": "1F699",
+ "digest": "e61ef2299d11fc01e9d6c496d188a7211633946706f6e771c412368346ca16f4"
+ },
+ {
+ "name": "blue_heart",
+ "unicode": "1F499",
+ "digest": "1af8d04173e0a984360786f6031220000dd548b8c912a68fd51f2ba490a9e16a"
+ },
+ {
+ "name": "blush",
+ "unicode": "1F60A",
+ "digest": "d615cda0f7c185ed8a92008204043ef769f3b7fb5424d595aeaaf3827bcdbd73"
+ },
+ {
+ "name": "boar",
+ "unicode": "1F417",
+ "digest": "c23a06db0337597e361ae581eacd4faf9926c6b7db0510d3599eb2e2a73315cb"
+ },
+ {
+ "name": "bomb",
+ "unicode": "1F4A3",
+ "digest": "0099e7435eba35f4f3ad273993293693a8b5cd110567c95ed83e5b4e2d0978ff"
+ },
+ {
+ "name": "book",
+ "unicode": "1F4D6",
+ "digest": "152408f2ff9949b7cbe57f623e4f875aa8dd0b02317e03cc914e1ea3712b3fc7"
+ },
+ {
+ "name": "book2",
+ "unicode": "1F56E",
+ "digest": "26d6b66a1957e7750b3e22eb2e46d0cc85932977bbb81d3d8482ec1ec58ee12b"
+ },
+ {
+ "name": "bookmark",
+ "unicode": "1F516",
+ "digest": "a2e0c6f5466c1b2fc148b20f6afcf4a878f4df55b0181f61fffa3ff727dcb251"
+ },
+ {
+ "name": "bookmark_tabs",
+ "unicode": "1F4D1",
+ "digest": "16135d62ff440722bd1ce8f84219be6a5eb3120a1597bfda4aeed4a2d9e7d7b2"
+ },
+ {
+ "name": "books",
+ "unicode": "1F4DA",
+ "digest": "ba019e4174639440caec424b30dfa016fe71a6f7436fe63025a2e3609ebfc012"
+ },
+ {
+ "name": "boom",
+ "unicode": "1F4A5",
+ "digest": "ec26246935c99749950612d69c06435ccdc126f14426a48a7599c5b6b91d9d58"
+ },
+ {
+ "name": "boot",
+ "unicode": "1F462",
+ "digest": "7ed639d52e285b0f46064dd4e1f4a8fb5814e1b2dc47c6f93cb349a6ac7ea97a"
+ },
+ {
+ "name": "bouquet",
+ "unicode": "1F490",
+ "digest": "b699f13af218560344f3571436f87b6f8c5c9f0fa0308836937667241b3fc7aa"
+ },
+ {
+ "name": "bouquet2",
+ "unicode": "1F395",
+ "digest": "1643ec51ff26fc1ac0c67859e202386398650bf2a996c82b68e1b73fa52abf7d"
+ },
+ {
+ "name": "bow",
+ "unicode": "1F647",
+ "digest": "5e260c38cfc80cd2f20ef78d982126dbf90934f7afa12c96d0b7b413beb6d4e0"
+ },
+ {
+ "name": "bow_and_arrow",
+ "unicode": "1F3F9",
+ "digest": "1c23469256331ea4ff03c036f89f0e63ad3228c51faecba50129da99b7eaddf3"
+ },
+ {
+ "name": "bow_tone1",
+ "unicode": "1F647-1F3FB",
+ "digest": "d3ec7ef70b355ba310d6fae7130a4e4cd11526b6e219474b5678a2b3ba1077f0"
+ },
+ {
+ "name": "bow_tone2",
+ "unicode": "1F647-1F3FC",
+ "digest": "c2905c0feba15fbc533cc6b36038eeda30f729182aa544f1d9164f5ccfed64d5"
+ },
+ {
+ "name": "bow_tone3",
+ "unicode": "1F647-1F3FD",
+ "digest": "298fc646d96c307eaa137c80b403d8355539ed8af13d3954a4ccacef67d341fa"
+ },
+ {
+ "name": "bow_tone4",
+ "unicode": "1F647-1F3FE",
+ "digest": "27db8401aa62a2544b24ff839b332958b5e8c3ab3fd7a289d3c62c654705da60"
+ },
+ {
+ "name": "bow_tone5",
+ "unicode": "1F647-1F3FF",
+ "digest": "168cdf834edb54723cf1c32311d4117c288132c5f76d6c415726c7484158c52a"
+ },
+ {
+ "name": "bowling",
+ "unicode": "1F3B3",
+ "digest": "0e888bcd1a5cc1ea7b07cea255ccb04dcdc87b0337b74cdc96a708aad7975768"
+ },
+ {
+ "name": "boy",
+ "unicode": "1F466",
+ "digest": "f349ab3e1015b4ccda5faab6a355f9c38e36e7c1cd667084563a14a2b11036ea"
+ },
+ {
+ "name": "boy_tone1",
+ "unicode": "1F466-1F3FB",
+ "digest": "4d04a5e45c9f9749de580321a212e14304b4ffcd229fa971fb59d97e6124262f"
+ },
+ {
+ "name": "boy_tone2",
+ "unicode": "1F466-1F3FC",
+ "digest": "0c9d6b6b1b3da68b9ef1f0f01efa4d170a48cfc66de4f577f8669c160b81cc97"
+ },
+ {
+ "name": "boy_tone3",
+ "unicode": "1F466-1F3FD",
+ "digest": "7dbecace78edb2aceffce6cb4d49ca132b93d80c26a8f1526a18832a2f23454a"
+ },
+ {
+ "name": "boy_tone4",
+ "unicode": "1F466-1F3FE",
+ "digest": "49f9c633afa8ff81068c78717e0012f8936fb3dcdb8b57342410f57f0635ae7c"
+ },
+ {
+ "name": "boy_tone5",
+ "unicode": "1F466-1F3FF",
+ "digest": "17e2ec379c7b542e6c2c5deef992af5f1fbaa3e288d1f71c8c984fb91a698cd4"
+ },
+ {
+ "name": "boys_symbol",
+ "unicode": "1F6C9",
+ "digest": "47fadbcb876ca436264ce2f3ebd1472bd68f55cc2b4833bf054335be9dc7a0f2"
+ },
+ {
+ "name": "bread",
+ "unicode": "1F35E",
+ "digest": "43697495538bfed11ed75213af8b1bdc14ef359d9b472cd7f9130fcb0a198680"
+ },
+ {
+ "name": "bride_with_veil",
+ "unicode": "1F470",
+ "digest": "37e75fbb2b0d06c900d51269b99107c60b61453dbf218b54df3011a455cd6dc3"
+ },
+ {
+ "name": "bride_with_veil_tone1",
+ "unicode": "1F470-1F3FB",
+ "digest": "44072e54e0618d2675a5bfd6572108590e51e8e733381e091e8754ee96c2cf20"
+ },
+ {
+ "name": "bride_with_veil_tone2",
+ "unicode": "1F470-1F3FC",
+ "digest": "f0acd961e108db9d9dd5d1b06e708b2eb6a7ef7235d6c8678b9319077faf4fa8"
+ },
+ {
+ "name": "bride_with_veil_tone3",
+ "unicode": "1F470-1F3FD",
+ "digest": "3f7adddb41ead3cd07098799ab2a5b8e8842344307d9045264403fb685f20555"
+ },
+ {
+ "name": "bride_with_veil_tone4",
+ "unicode": "1F470-1F3FE",
+ "digest": "5f7199fd99319651f3a7b3553cc5387c59b65cac1eb020441e19b5c12c807dc7"
+ },
+ {
+ "name": "bride_with_veil_tone5",
+ "unicode": "1F470-1F3FF",
+ "digest": "4b1f6c33dd72a3a11c764bb00e7be7441b39c7af78aae52141276a279d63ab78"
+ },
+ {
+ "name": "bridge_at_night",
+ "unicode": "1F309",
+ "digest": "f81cc36de8edbdf3fe4d55932d5c6c8ad429487ec1f7af044611b6dc950ee09c"
+ },
+ {
+ "name": "briefcase",
+ "unicode": "1F4BC",
+ "digest": "a3c3e802191f3e131683dac1fcd81e294dea72af8e65c94972990924c79c5619"
+ },
+ {
+ "name": "broken_heart",
+ "unicode": "1F494",
+ "digest": "4dee349274c2ea44d1c0395cbd39356b88897b0c45040aa40d8cb2607ee67420"
+ },
+ {
+ "name": "bug",
+ "unicode": "1F41B",
+ "digest": "bac4660ee8dcbef0023691804ee3fad3ea3d4bac20d847a5913cee6e7dca826c"
+ },
+ {
+ "name": "bulb",
+ "unicode": "1F4A1",
+ "digest": "af5394230f95781c7eb8054b1a13732a6e6170318599c79e9ca2a816a5b821a2"
+ },
+ {
+ "name": "bullettrain_front",
+ "unicode": "1F685",
+ "digest": "59afcd289500bd4148b1b91f560a5ce8ac9e1b52eddb8fec857ff5d171f017fb"
+ },
+ {
+ "name": "bullettrain_side",
+ "unicode": "1F684",
+ "digest": "79ff8f579081a2f1c3b05311a18ca432adb026a7860875cea4a5460e49b2a474"
+ },
+ {
+ "name": "bullhorn",
+ "unicode": "1F56B",
+ "digest": "a4ca5cbfe299e8ccd148d17055d2d395cf8515e416bf771044c9a670509a8254"
+ },
+ {
+ "name": "bullhorn_waves",
+ "unicode": "1F56C",
+ "digest": "92493636cf086205d1e12cc19e613b84152ef10b8cd0215619a0fc813bfc9a7c"
+ },
+ {
+ "name": "burrito",
+ "unicode": "1F32F",
+ "digest": "4babb1af1136ab2334d26495b0be779d0bcc9516fd956fc07ffde427d11122f0"
+ },
+ {
+ "name": "bus",
+ "unicode": "1F68C",
+ "digest": "476e7a5e92f64038e5012205395efead51f1c10b3edb25380f38da97e2412edd"
+ },
+ {
+ "name": "busstop",
+ "unicode": "1F68F",
+ "digest": "3bcf82872ab6abb0278238c71bd004a40c46696bdda05f54c153d45d6fe88f15"
+ },
+ {
+ "name": "bust_in_silhouette",
+ "unicode": "1F464",
+ "digest": "2230844993ab011fe2756a1aa3873ff7d5f7d888bddec408ba0b32e4f6003570"
+ },
+ {
+ "name": "busts_in_silhouette",
+ "unicode": "1F465",
+ "digest": "d1c3cb6d437616834425a53621c0bc0a6b368d745dd9da2300a3db4543d57660"
+ },
+ {
+ "name": "cactus",
+ "unicode": "1F335",
+ "digest": "e87588e6548d201db903dc0523b3ccc83c6b559981d743eae1504ce668cd8be4"
+ },
+ {
+ "name": "cake",
+ "unicode": "1F370",
+ "digest": "3947783d128018f5e396602d0492cb5c31e8e8df98af01eda7cade71aea8d989"
+ },
+ {
+ "name": "calculator",
+ "unicode": "1F5A9",
+ "digest": "01b47b5c69c12b65fa4f4c0d580f2a98280d6116f4ad2cf8be378759008bcc3c"
+ },
+ {
+ "name": "calendar",
+ "unicode": "1F4C6",
+ "digest": "00bb700dd88efbc43bc64263491cdf77965130b1dc23f31e682905c3dfe4040c"
+ },
+ {
+ "name": "calendar_spiral",
+ "unicode": "1F5D3",
+ "digest": "1dd5da98bb435c0c3f632bc0a5c9fdde694de7aee752bf4bb85def086e788a2a"
+ },
+ {
+ "name": "calling",
+ "unicode": "1F4F2",
+ "digest": "2375828085f2efd17b8a5ebb3cfec1e420190913328a7a0dd9ff0f67c7249ffb"
+ },
+ {
+ "name": "camel",
+ "unicode": "1F42B",
+ "digest": "9ff789ab50b51cd9e7fdc7fbe8d6f913fda95dfd425949f97974548652a53ce1"
+ },
+ {
+ "name": "camera",
+ "unicode": "1F4F7",
+ "digest": "d95192b9ba0f566d8874099125def031e15297d1306989ea9b6a49f7b9b56661"
+ },
+ {
+ "name": "camera_with_flash",
+ "unicode": "1F4F8",
+ "digest": "4db6fb3fdb9a004537dff97f4197c7ed87c9c978ba9ac562ed8bb7c1fa260d38"
+ },
+ {
+ "name": "camping",
+ "unicode": "1F3D5",
+ "digest": "f0855dc78bf6f3d06b3c2fc19180c8ff23d9e22871658fcc26a8fde08d328a0a"
+ },
+ {
+ "name": "cancellation_x",
+ "unicode": "1F5D9",
+ "digest": "cea2f7a48543207615ee06755ded62c2a95a7eaf7d7b68a3fc25e74d94e2c92c"
+ },
+ {
+ "name": "cancer",
+ "unicode": "264B",
+ "digest": "b990f85e9f62017d99526244eaef5c5e56f8808698011e85d44de1d2ed87f1a2"
+ },
+ {
+ "name": "candle",
+ "unicode": "1F56F",
+ "digest": "5eefd555951e65298583009a307acc6fb6d02c88325ef3adf231717e75e5a333"
+ },
+ {
+ "name": "candy",
+ "unicode": "1F36C",
+ "digest": "f14203c408173fbb94b4ee69d6de67226a17dc51b0cbd776f62623ee03fd2eb3"
+ },
+ {
+ "name": "capital_abcd",
+ "unicode": "1F520",
+ "digest": "2a7cc876218b8c244b9802448ee25ce5004671a4f00ea950a636d8c3b766dbef"
+ },
+ {
+ "name": "capricorn",
+ "unicode": "2651",
+ "digest": "03a5fd064c10f47c7fd0ae318c573bb559c269b1b2d61b45aa5b8ce9b5fbd9df"
+ },
+ {
+ "name": "card_box",
+ "unicode": "1F5C3",
+ "digest": "7d760ae1d44e6f4b2aac00895ca86b5743f8b5ca157ec2bd21ce2665e50ad23a"
+ },
+ {
+ "name": "card_index",
+ "unicode": "1F4C7",
+ "digest": "150950903eccb468981c58b87ed7c1ba44e17f52627d695f660ce96b3d9d6e8e"
+ },
+ {
+ "name": "carousel_horse",
+ "unicode": "1F3A0",
+ "digest": "d6862085550fa139a147dceb1b2b9f950a08dcd01cecd8b8697f9c7992ca054e"
+ },
+ {
+ "name": "cartridge",
+ "unicode": "1F5AD",
+ "digest": "0b1625eea118060b51a70905c1eb3313ed632e989f70943eca16aa29fe8a34f2"
+ },
+ {
+ "name": "cat",
+ "unicode": "1F431",
+ "digest": "002208c0c9165971853ee05cd05513175a913376a462a345a939d73401c6acb7"
+ },
+ {
+ "name": "cat2",
+ "unicode": "1F408",
+ "digest": "fbdb726cc035f83784dcfe2d9adb85f8aeec429064aed5c5ca0b8be406068aa5"
+ },
+ {
+ "name": "cd",
+ "unicode": "1F4BF",
+ "digest": "bd4d4eef2cc0b1e4ee1f5280f922743e76f27d35836987801b2b48969eac17d8"
+ },
+ {
+ "name": "celtic_cross",
+ "unicode": "1F548",
+ "digest": "187aac988d7e02085a15f31c4cc0ff25127be5b088e354e65c7b1152bffb40ff"
+ },
+ {
+ "name": "chains",
+ "unicode": "26D3",
+ "digest": "a6a915d9c361e1564e13cf2d33ad5df3d684aa349b8dc5909e6343d67401beb9"
+ },
+ {
+ "name": "champagne",
+ "unicode": "1F37E",
+ "digest": "77395d3afe5cc10bfdc381120bae2ae4aefdaa96c529536413873a696c5fa713"
+ },
+ {
+ "name": "chart",
+ "unicode": "1F4B9",
+ "digest": "9fd5f8cd99988bbe0fabc89a0b23e28d1468641d2f9468e82b7148a1948d8236"
+ },
+ {
+ "name": "chart_with_downwards_trend",
+ "unicode": "1F4C9",
+ "digest": "6fe456d76c0a996c12049057b5d60129098a9deddfa2d133cff5c4400e4595a0"
+ },
+ {
+ "name": "chart_with_upwards_trend",
+ "unicode": "1F4C8",
+ "digest": "e83cc4cf4228bd77e030a19755b11cf75cf671f40973c23e240afa54d9de478e"
+ },
+ {
+ "name": "checkered_flag",
+ "unicode": "1F3C1",
+ "digest": "77501c2c66af31f72f5c05f21e87598cd59740b5cfc02926c66dc755bab3c3cf"
+ },
+ {
+ "name": "cheese",
+ "unicode": "1F9C0",
+ "digest": "5897036ba97b557868bb314fcee83b9d8a609c8447b270a0b3d34a29ce7496d1"
+ },
+ {
+ "name": "cherries",
+ "unicode": "1F352",
+ "digest": "5a0ba73039e4b56e3d16a1c70ad992f41af7a16f6d5ba4b5337bdf338276f0ff"
+ },
+ {
+ "name": "cherry_blossom",
+ "unicode": "1F338",
+ "digest": "b40533225291f539ffe97e4ab1d70d07e179b2f9345b2814355164d0407cf3bf"
+ },
+ {
+ "name": "chestnut",
+ "unicode": "1F330",
+ "digest": "6a2a37899d28326daf36965b343b2646492c2c0cee8871321cc17315d6252a9a"
+ },
+ {
+ "name": "chicken",
+ "unicode": "1F414",
+ "digest": "13d770684a11ea10c0ae7570a98c5dfafd4bfb78ac3f72f46729aef9060b85c0"
+ },
+ {
+ "name": "children_crossing",
+ "unicode": "1F6B8",
+ "digest": "654d2502c1edc57c5ab4237df76db3121f6b8735eb13d30bffd305605a083445"
+ },
+ {
+ "name": "chipmunk",
+ "unicode": "1F43F",
+ "digest": "1ae3c838450afcbbe8a96992481dde252e343ab83546d0789ebed81a78ca9188"
+ },
+ {
+ "name": "chocolate_bar",
+ "unicode": "1F36B",
+ "digest": "2486b7265048eb2294d6be0a0a8a4d6067df95721ace9d131d8f715a27ba8cf0"
+ },
+ {
+ "name": "christmas_tree",
+ "unicode": "1F384",
+ "digest": "454c08870eaa84283c19731ed3b10c4868d2e2f0cc44f2feba0de9ba4cc9c4e1"
+ },
+ {
+ "name": "church",
+ "unicode": "26EA",
+ "digest": "b62e838ffb0dfefeced1707359437b6815e0721783b549212282e08617402f6f"
+ },
+ {
+ "name": "cinema",
+ "unicode": "1F3A6",
+ "digest": "6df56f6a0008d0352740d1e045ffdb702e80c2a6d88b6db1a8bcd27eb3c12dcc"
+ },
+ {
+ "name": "circus_tent",
+ "unicode": "1F3AA",
+ "digest": "f8b7a7f4cf4f9efd20423acc30abb3a28e2a5183b3e39f5cc88e7e0ed7757d64"
+ },
+ {
+ "name": "city_dusk",
+ "unicode": "1F306",
+ "digest": "8779066dc9386d05c951b1df1753983c2937a5f3b84d5fc09ed0b172d4ef914e"
+ },
+ {
+ "name": "city_sunset",
+ "unicode": "1F307",
+ "digest": "c2530d12204eb518c5a3c8d7deba11170b1412fdf406aea05a69d4c026210d1b"
+ },
+ {
+ "name": "cityscape",
+ "unicode": "1F3D9",
+ "digest": "15251a708d50fc721bd67d8abb2a517c0bade196df3b736e21d79191d749241f"
+ },
+ {
+ "name": "cl",
+ "unicode": "1F191",
+ "digest": "104591d8e7b980cf38dcf8326d36c845384b7a4e6d94c49f36e9946484712a95"
+ },
+ {
+ "name": "clap",
+ "unicode": "1F44F",
+ "digest": "ed6ef8bb78ca1fa295b87222c440c6d5ba4f154f2752bf0d428941260d66aaac"
+ },
+ {
+ "name": "clap_tone1",
+ "unicode": "1F44F-1F3FB",
+ "digest": "57a1fd1fa2578c30b8a47abb84e81af5f5bbc6c301a5daf0c53d4d07b017e777"
+ },
+ {
+ "name": "clap_tone2",
+ "unicode": "1F44F-1F3FC",
+ "digest": "2ad4dcd513e55486f21151bf3792e1febf116574d238545b07b4290901430fdd"
+ },
+ {
+ "name": "clap_tone3",
+ "unicode": "1F44F-1F3FD",
+ "digest": "2d8c705d4fcc162fb65cd51e2c6683f1129ebc72fba13343533f64ede1c62687"
+ },
+ {
+ "name": "clap_tone4",
+ "unicode": "1F44F-1F3FE",
+ "digest": "40ffd41b2b4f59d0040e9d20497e57c4e47f18aeae43fcae02be5c2f50069102"
+ },
+ {
+ "name": "clap_tone5",
+ "unicode": "1F44F-1F3FF",
+ "digest": "be55df1ac7600ba086c2ef6ea223ebc62271fa47876c53ade1a1c0151fdc994c"
+ },
+ {
+ "name": "clapper",
+ "unicode": "1F3AC",
+ "digest": "a8748398f56fd2c1e6e87fe0c77edec444df7c7dd462d43dbcea6d8de97c81c5"
+ },
+ {
+ "name": "classical_building",
+ "unicode": "1F3DB",
+ "digest": "6a607b0666141b51d6e944b04f3f6188a5c026396e6105f1d2a5e6b6350cd66b"
+ },
+ {
+ "name": "clipboard",
+ "unicode": "1F4CB",
+ "digest": "4ca1a0b864a962b111d6bdb65373b779f3fff571ffd32d029666f9b708e1ab73"
+ },
+ {
+ "name": "clock",
+ "unicode": "1F570",
+ "digest": "c48314ccde8bf01acc2b1bc9a6b5aa7d796fc0c8769f80398bc74545fcef31ed"
+ },
+ {
+ "name": "clock1",
+ "unicode": "1F550",
+ "digest": "c0550fa0c385920cbdb775bdaaa5e812097a484c4a32e35ebbafe3a364a4a438"
+ },
+ {
+ "name": "clock10",
+ "unicode": "1F559",
+ "digest": "25651ac5520505f326457364428de3679cc22ca57278d4c54cc4b60420fa7b74"
+ },
+ {
+ "name": "clock1030",
+ "unicode": "1F565",
+ "digest": "dbf682bac968fc5a3959af2b96eaaa5ee78306f6341c43c1345b94bc561a3d04"
+ },
+ {
+ "name": "clock11",
+ "unicode": "1F55A",
+ "digest": "333732dd6c3184f257964bcf5a20a6111f9adb04560b5d12dc613636e846df5b"
+ },
+ {
+ "name": "clock1130",
+ "unicode": "1F566",
+ "digest": "005999cb37998adea1645d7df63b2705a42db3b4f1a734891d79af3e833764ff"
+ },
+ {
+ "name": "clock12",
+ "unicode": "1F55B",
+ "digest": "6690e591bec1751e1c5472e0bf52f66779b2113e5b8c6c578e65dbb83d091b16"
+ },
+ {
+ "name": "clock1230",
+ "unicode": "1F567",
+ "digest": "549f3921bcff7f330c5a41e6756d8c15601f1f8278b35b369148771c60be2a6f"
+ },
+ {
+ "name": "clock130",
+ "unicode": "1F55C",
+ "digest": "9332ef07a9dde8ccaa1e58a3e97edee0601a1152fc6d351b782816c838d2a408"
+ },
+ {
+ "name": "clock2",
+ "unicode": "1F551",
+ "digest": "9d1ec8fbdae627880e1c067c10d6a40f1e4494a246c77224b3cd7b287554c4b4"
+ },
+ {
+ "name": "clock230",
+ "unicode": "1F55D",
+ "digest": "3578a39c28695d4e617a648a1eb44e0bb5a8a11dcbe04fa2eb2aea0a60589067"
+ },
+ {
+ "name": "clock3",
+ "unicode": "1F552",
+ "digest": "c2e2a27301b6ac27dc359be590448eb1e65fe87211f1af30a473d8bde4f3db47"
+ },
+ {
+ "name": "clock330",
+ "unicode": "1F55E",
+ "digest": "7a77cf8cf9a98f4767a2dca1d3795be45938eee185db81120d85cedebe128899"
+ },
+ {
+ "name": "clock4",
+ "unicode": "1F553",
+ "digest": "0945c4199400d546350cfff25bc9e9160789d1cf9890b3318bdc462ac6cc9782"
+ },
+ {
+ "name": "clock430",
+ "unicode": "1F55F",
+ "digest": "9fdb6f1fa076c4c6a395dbf6db27499ee447b3558f3aa64d913686c360e428a8"
+ },
+ {
+ "name": "clock5",
+ "unicode": "1F554",
+ "digest": "855b3500eb6d20bb6e51d3a6c9d1a5131c06404c6c149841c7cca52201036428"
+ },
+ {
+ "name": "clock530",
+ "unicode": "1F560",
+ "digest": "a6ebd9f884d45a1f43650351a1f1da9724bc044d7da2f6d99ffb3d1fa0c31c5d"
+ },
+ {
+ "name": "clock6",
+ "unicode": "1F555",
+ "digest": "e38f9fc4f87f12ee602dcf2285d59dbc343fc0fc37662992cfe9866c20f58e87"
+ },
+ {
+ "name": "clock630",
+ "unicode": "1F561",
+ "digest": "735954a650791fc38c845c43998023e652d36e55534850e43952878b8804b2f1"
+ },
+ {
+ "name": "clock7",
+ "unicode": "1F556",
+ "digest": "2c4244ec4019e9624e6ea5a751bb735ab87bead33b1ea160265c81bba3c2f736"
+ },
+ {
+ "name": "clock730",
+ "unicode": "1F562",
+ "digest": "0bcf20e30be1bb23394696770301867e307f8e5014e0ed7d75ed96efe34d625d"
+ },
+ {
+ "name": "clock8",
+ "unicode": "1F557",
+ "digest": "af454047a1765ef1c8355969302a826d4c47f5c61a6ec47fdec3510a8003b0d8"
+ },
+ {
+ "name": "clock830",
+ "unicode": "1F563",
+ "digest": "e48b81dac055dc6d5f7832cf34368329c573d03b35bfe076fed1c6e6d48a82e7"
+ },
+ {
+ "name": "clock9",
+ "unicode": "1F558",
+ "digest": "f2a3d1bc029dc0e6406cdaa96542e77503e4cfb79d99c69cb454b8cf635a73fc"
+ },
+ {
+ "name": "clock930",
+ "unicode": "1F564",
+ "digest": "bb1b2b83052e8e6fb97c48c13bce0d950907e044eb2dabf21d7fed321f75110b"
+ },
+ {
+ "name": "clockwise_arrows",
+ "unicode": "1F5D8",
+ "digest": "67027b7e1a4d800a3ce7d731c21c098d1109d217159a27665eebb7e080fc2622"
+ },
+ {
+ "name": "closed_book",
+ "unicode": "1F4D5",
+ "digest": "afd6dae5fa0f59330fc2adb922e92b3410a33a80a2667651718c7dac588010bc"
+ },
+ {
+ "name": "closed_lock_with_key",
+ "unicode": "1F510",
+ "digest": "d0ed5c00f939111ce86f9c741b733b22e04ebbd871aa33da3eb0f46a6f38b707"
+ },
+ {
+ "name": "closed_umbrella",
+ "unicode": "1F302",
+ "digest": "3ef08b299f9170007a5433fe82d0953bf0f75b6685d0ce58972f9af032dc471a"
+ },
+ {
+ "name": "cloud",
+ "unicode": "2601",
+ "digest": "d1e7932551e85c6e86bfb3b41f0c936a6d0953bf9f9119b8cca3eaed22ac0c01"
+ },
+ {
+ "name": "cloud_lightning",
+ "unicode": "1F329",
+ "digest": "fc9c85cc95f9c456635692c974f72b6d40e14943824b8129a21c47265c3416f4"
+ },
+ {
+ "name": "cloud_rain",
+ "unicode": "1F327",
+ "digest": "f4406e62ed98f6141ab70736f6d5c540023e805396db0346ee6b7082c3f5e8e2"
+ },
+ {
+ "name": "cloud_snow",
+ "unicode": "1F328",
+ "digest": "948990cd13dd927917208c026089519fcf8e258a8a284684ace67c9a2f9a8149"
+ },
+ {
+ "name": "cloud_tornado",
+ "unicode": "1F32A",
+ "digest": "44753516d0bd05d47cfa6eb922aba570ba6a87f805f325772b2cff071460ead1"
+ },
+ {
+ "name": "clubs",
+ "unicode": "2663",
+ "digest": "5fd19fadd3b0887a6a59819ffbbe33a061055c043200700c31be30e14a5d36d5"
+ },
+ {
+ "name": "cocktail",
+ "unicode": "1F378",
+ "digest": "cf096ebe15b4053702d490cd96f04d565b4993529bcd6d8d50cb821200d1cd92"
+ },
+ {
+ "name": "coffee",
+ "unicode": "2615",
+ "digest": "6ea6128e353d9f74aee99caaaaa30c53f996fb242bf3bffb0fa92e6b4d373e57"
+ },
+ {
+ "name": "coffin",
+ "unicode": "26B0",
+ "digest": "b59772d7aa262c4d7433f9cdf76d50011f4c63421b730c8ab4a08675f730c39f"
+ },
+ {
+ "name": "cold_sweat",
+ "unicode": "1F630",
+ "digest": "f0d0057bf01db8d930f6e4632c5bf8d0b1bc709bcfb6463a1f1973b5f1d70a83"
+ },
+ {
+ "name": "comet",
+ "unicode": "2604",
+ "digest": "00252ec55d1846d95c8d4c704b35251232d9810029fc215a7da08262dd1f3541"
+ },
+ {
+ "name": "compression",
+ "unicode": "1F5DC",
+ "digest": "432fbe66e5e3c38ebfeb4eb03465667a1e1be868b4afe510ec95eadda6481bde"
+ },
+ {
+ "name": "computer",
+ "unicode": "1F4BB",
+ "digest": "99777be010488867c7872b2e235be7c35b1a6f28d92baa921b61ced5491c0257"
+ },
+ {
+ "name": "computer_old",
+ "unicode": "1F5B3",
+ "digest": "b27c30d74f205a8a3bd00a55ca17da7cf6ae3b65ae33e949755a4c6bd69a9fd3"
+ },
+ {
+ "name": "confetti_ball",
+ "unicode": "1F38A",
+ "digest": "e77d0c0970d3d12e123e548639fc0fa3ce41668667e4be55baefc09dfaa22cb0"
+ },
+ {
+ "name": "confounded",
+ "unicode": "1F616",
+ "digest": "0f51db64149151d3d7ae5dce08c9af3d064123524fa36fe1f51a78cbd966b6ea"
+ },
+ {
+ "name": "confused",
+ "unicode": "1F615",
+ "digest": "ed23587432c1be98356156784ca4fe0b374b7b3b371660d45cfb0a1efd44e322"
+ },
+ {
+ "name": "congratulations",
+ "unicode": "3297",
+ "digest": "2a46d640bf24fd4dc7649baf4b28c4adb30eda8d24d70eda07036c85b48195e0"
+ },
+ {
+ "name": "construction",
+ "unicode": "1F6A7",
+ "digest": "73fac9fb5eb91954b0f998f9d05fb953241eed988c134fa42477393159fa34fa"
+ },
+ {
+ "name": "construction_site",
+ "unicode": "1F3D7",
+ "digest": "0ff52e6adf1927d356b27be5fef6bad2ad842be05e3a0bd16a17efe78e5676d9"
+ },
+ {
+ "name": "construction_worker",
+ "unicode": "1F477",
+ "digest": "2be436fa7ad0a31e328fc6f776044bd1eec35c99541ced891792e3bef738d0a0"
+ },
+ {
+ "name": "construction_worker_tone1",
+ "unicode": "1F477-1F3FB",
+ "digest": "172cebc84f91237a85292c5ab0a105cc3abbb96e7423c4ae81feffd00bdb3b26"
+ },
+ {
+ "name": "construction_worker_tone2",
+ "unicode": "1F477-1F3FC",
+ "digest": "3e9b96ddfd639eefda99ad3a0ad26a28a0f2c8be72988c2bdbd648e6104638b6"
+ },
+ {
+ "name": "construction_worker_tone3",
+ "unicode": "1F477-1F3FD",
+ "digest": "11f83c565168dce5ac2387b873769d85ec4087171d6e92fc766c209ea06cd4f3"
+ },
+ {
+ "name": "construction_worker_tone4",
+ "unicode": "1F477-1F3FE",
+ "digest": "09e320e78e3a2940f0c5a0ef9a235ab72c51e053fd8ff433843fdb62571c8e70"
+ },
+ {
+ "name": "construction_worker_tone5",
+ "unicode": "1F477-1F3FF",
+ "digest": "7ac2a1a0038e7aefea889380be604a98255823587e90799165f7db39dd03a0cc"
+ },
+ {
+ "name": "control_knobs",
+ "unicode": "1F39B",
+ "digest": "9f10e578b410ff6aa7cc7fe806a0f1181893765303c0ca3867b652f1392a8a22"
+ },
+ {
+ "name": "contruction_site",
+ "unicode": "1F3D7",
+ "digest": "0ff52e6adf1927d356b27be5fef6bad2ad842be05e3a0bd16a17efe78e5676d9"
+ },
+ {
+ "name": "convenience_store",
+ "unicode": "1F3EA",
+ "digest": "1ff4351e4a4503f58ed5d35074a2112c681337e35ffe55332187481685573606"
+ },
+ {
+ "name": "cookie",
+ "unicode": "1F36A",
+ "digest": "5c78ce2e721b0a3767d6ce0b59c1e88fdf94a7edc94e98c4d6b7aadb5b2aeea7"
+ },
+ {
+ "name": "cool",
+ "unicode": "1F192",
+ "digest": "54a96697a5070388ce8364a5ee2e0d78a53acc8b4f6755b1359fd67252cc41e8"
+ },
+ {
+ "name": "cop",
+ "unicode": "1F46E",
+ "digest": "16bee252c2a133bcf57f6d7b8372a61364744a2f662acb90e2005732555135fa"
+ },
+ {
+ "name": "cop_tone1",
+ "unicode": "1F46E-1F3FB",
+ "digest": "2fc52f3ed735e327d12dadb15f9feb7b7f720fc6857b551548a2a84809053817"
+ },
+ {
+ "name": "cop_tone2",
+ "unicode": "1F46E-1F3FC",
+ "digest": "6208f3174ced4f07ba3820ba838b247d7438d69d86eb04927333e7436e56af7e"
+ },
+ {
+ "name": "cop_tone3",
+ "unicode": "1F46E-1F3FD",
+ "digest": "2427d30bdfe127be4d8c3870472cae191eece142c784a5c2809df938f43e7c53"
+ },
+ {
+ "name": "cop_tone4",
+ "unicode": "1F46E-1F3FE",
+ "digest": "6e73f8abdf816f3cb2728b971a5a8d006a236c1d71b2ee1788ab60329f406323"
+ },
+ {
+ "name": "cop_tone5",
+ "unicode": "1F46E-1F3FF",
+ "digest": "4b146465cc95ade7e9ca722e31a1b06311214dae8f7f4d95c6329d56c45b451f"
+ },
+ {
+ "name": "copyright",
+ "unicode": "00A9",
+ "digest": "8143583821085dfc8ac21079fe220288ba3a3b6ca3014dc5dc98b18da77589c1"
+ },
+ {
+ "name": "corn",
+ "unicode": "1F33D",
+ "digest": "0160502226b5f9af81763545f288dbbb20632039d7509f347c751cfdb49dc5b5"
+ },
+ {
+ "name": "couch",
+ "unicode": "1F6CB",
+ "digest": "a93fffed194b404200495abda8772bb35539cfc8499eb0a9bf09c508afad6676"
+ },
+ {
+ "name": "couple",
+ "unicode": "1F46B",
+ "digest": "97fe611a613216a1788f9bd88a9deb4714ee123a66b5fd3d0ac916fbb4da7304"
+ },
+ {
+ "name": "couple_mm",
+ "unicode": "1F468-2764-1F468",
+ "digest": "3ae6fbf3ba168256ea85c756ac1e7b83fdb8b780d33f06128ed80706ff627eea"
+ },
+ {
+ "name": "couple_with_heart",
+ "unicode": "1F491",
+ "digest": "d9701173a5e8dff052ab6a15a42494dbb61dc7146d3734c82916abc9c05f76db"
+ },
+ {
+ "name": "couple_ww",
+ "unicode": "1F469-2764-1F469",
+ "digest": "d2a2ec29c1a1234ea0aa1d9fc6707cf8be8bb36ea8b92523ffa1c3071bcf0b06"
+ },
+ {
+ "name": "couplekiss",
+ "unicode": "1F48F",
+ "digest": "e722730de82397da7c8f88d79319b391e8f01fbe4a9133850cc92ad34e77bd82"
+ },
+ {
+ "name": "cow",
+ "unicode": "1F42E",
+ "digest": "dcc1efef2f02588806a156ed43da959c587d4c576ff6badec77f820ed3ba507f"
+ },
+ {
+ "name": "cow2",
+ "unicode": "1F404",
+ "digest": "dcf59f92fd0a37b2ca720bcda606defa4357b58d8f4ad15c1288ad8d814b2bc7"
+ },
+ {
+ "name": "crab",
+ "unicode": "1F980",
+ "digest": "59d34a4e92326ebeab188d9e33b25c20f4d54d187c274713fa3256b03b9e662a"
+ },
+ {
+ "name": "crayon",
+ "unicode": "1F58D",
+ "digest": "0f3351c2e68a8d47d27b45a9901be6160de0f9a291bd8680df84d0fc679bcb31"
+ },
+ {
+ "name": "credit_card",
+ "unicode": "1F4B3",
+ "digest": "708c0e7008e06e5d1b3b4e68a7e0ada9f4ae22ab6c28285d81a340f913fd9a84"
+ },
+ {
+ "name": "crescent_moon",
+ "unicode": "1F319",
+ "digest": "0959f838a410e8bfeebf00aa9658df56e515dbd2361142021071e17244662bfc"
+ },
+ {
+ "name": "cricket",
+ "unicode": "1F3CF",
+ "digest": "00eb11254e887c71db5e8945ad211e9e0280f1e02f4b77a4799b64bba2bbe9b3"
+ },
+ {
+ "name": "crocodile",
+ "unicode": "1F40A",
+ "digest": "99abcb42264d40d2450aaca8c3759a019bfd600a311cf3027243f1ca200d4639"
+ },
+ {
+ "name": "cross",
+ "unicode": "271D",
+ "digest": "a6e3c345cf6aa2ce690b66454066b53ef5b1dab2ed635e21f1586b1dffc5df42"
+ },
+ {
+ "name": "cross_heavy",
+ "unicode": "1F547",
+ "digest": "2e37c26b9bad0beb019c7f3e7a3892352d0ad9ca1b90c4333d42e8d56680be70"
+ },
+ {
+ "name": "cross_white",
+ "unicode": "1F546",
+ "digest": "3452e667010d7e49a51d7e1f4ba8ed4f303e33ed43255a051e9a18832a1efba6"
+ },
+ {
+ "name": "crossbones",
+ "unicode": "1F571",
+ "digest": "f5e7ce293c1a3282711073e68f033a3876e8428d1218cb2f8294630f9124e584"
+ },
+ {
+ "name": "crossed_flags",
+ "unicode": "1F38C",
+ "digest": "d4da057db289bec83f0106a94c89bd0cd9b52c7c7f8bc69bc8cbce480d53e12b"
+ },
+ {
+ "name": "crossed_swords",
+ "unicode": "2694",
+ "digest": "f159978583fa77c73ba6de85d35c4195cbd55963e537bd2bfd8f98ab8ff3559a"
+ },
+ {
+ "name": "crown",
+ "unicode": "1F451",
+ "digest": "e6fe2a28b7d80749ca121cabbe89321dcecdd760a122e73fb1562ea9bb40e90d"
+ },
+ {
+ "name": "cruise_ship",
+ "unicode": "1F6F3",
+ "digest": "90519c46ddfb63e71bc76661953da9041e5f0b97e9f8a7a8696518b4d529f3dd"
+ },
+ {
+ "name": "cry",
+ "unicode": "1F622",
+ "digest": "2d6a096796222c29b050f74db6b5aff9b9f61390c5eb56e45d1801918751002f"
+ },
+ {
+ "name": "crying_cat_face",
+ "unicode": "1F63F",
+ "digest": "df057d4e3e5c5c87caedf87ea3a6f936811b93f228f46bb7018d2bb5afaa6d35"
+ },
+ {
+ "name": "crystal_ball",
+ "unicode": "1F52E",
+ "digest": "7de438f88134c32c4db67d705e5fecf2a6187a87f56ebbb5bcc5ba09626e2935"
+ },
+ {
+ "name": "cupid",
+ "unicode": "1F498",
+ "digest": "7cb3f7d1ddf9678982197ef0e65735fb465ae8e3652d611f37d3bcccf4d7e2c1"
+ },
+ {
+ "name": "curly_loop",
+ "unicode": "27B0",
+ "digest": "881a43ae406cb74b2ef136bf970db9928bcdc3bbbb7393e90d2c597fe1dd9a96"
+ },
+ {
+ "name": "currency_exchange",
+ "unicode": "1F4B1",
+ "digest": "c4d76e9e61fac8d3c0cb9e07f1fbf1a7fcac6f4d4c78776ff7f04fc9391ce689"
+ },
+ {
+ "name": "curry",
+ "unicode": "1F35B",
+ "digest": "ebe41ee864c873e3a371888c0087b11dbcb124335812895002ed81fe2b6ba571"
+ },
+ {
+ "name": "custard",
+ "unicode": "1F36E",
+ "digest": "afc192f405c30e2d529ec0f4b31a7faf474bcd01fded5294dc38880b8bb22155"
+ },
+ {
+ "name": "customs",
+ "unicode": "1F6C3",
+ "digest": "5abb98151a79cebc1032c0ea149617093e42f41e50574a790a91074cabaa4c3a"
+ },
+ {
+ "name": "cyclone",
+ "unicode": "1F300",
+ "digest": "ae77e15bf2f312f03dbc5c7813d304005bbb549953482db9beb91810c585dc0e"
+ },
+ {
+ "name": "dagger",
+ "unicode": "1F5E1",
+ "digest": "377060a7ce930566a4732b361be98e8a193a546846dfbba2a00abeeef41d1976"
+ },
+ {
+ "name": "dancer",
+ "unicode": "1F483",
+ "digest": "e050db55afbb968e02219a58c7e82b824848d299a4df64f0d08d4e1872816203"
+ },
+ {
+ "name": "dancer_tone1",
+ "unicode": "1F483-1F3FB",
+ "digest": "350f6b2e4589fdd436173163035621b8da0bd49c7b9ec9f39593aae5e0ed0641"
+ },
+ {
+ "name": "dancer_tone2",
+ "unicode": "1F483-1F3FC",
+ "digest": "a9efc84ec80582f286147ca34162a27fd5989f4030084acdbc309d4368660f5b"
+ },
+ {
+ "name": "dancer_tone3",
+ "unicode": "1F483-1F3FD",
+ "digest": "ef187f44278fdb8605c80f5cf199e0b3de8a49085dada2e215bb91e1d7d3be5d"
+ },
+ {
+ "name": "dancer_tone4",
+ "unicode": "1F483-1F3FE",
+ "digest": "5195bc352dc9d24cc5505a167c756038e55c05048c61799ea1bfdf2debe44ac2"
+ },
+ {
+ "name": "dancer_tone5",
+ "unicode": "1F483-1F3FF",
+ "digest": "55cb7eee9fa11a16a3932800a19e334546f7396df6aadde22e58fe3185926b16"
+ },
+ {
+ "name": "dancers",
+ "unicode": "1F46F",
+ "digest": "39e7dfd9dafeee20f2968960b1179ee4bf3f2b63a3035fc1944024d0ae8b5de1"
+ },
+ {
+ "name": "dango",
+ "unicode": "1F361",
+ "digest": "2a1b50abe5dc72335344878d9b701028ccad651964d9e3affeedbf3c2bfd652a"
+ },
+ {
+ "name": "dark_sunglasses",
+ "unicode": "1F576",
+ "digest": "6bb1e911a93d5eb0581d3ce8f8929125d3d8fc04e086f3263cfd25af1348ce6c"
+ },
+ {
+ "name": "dart",
+ "unicode": "1F3AF",
+ "digest": "6f28741543a4c1eead21856128ffea1fcf772954fe6af40844dfde47f092ed32"
+ },
+ {
+ "name": "dash",
+ "unicode": "1F4A8",
+ "digest": "25aef37611f1c2f2e96518bf8aeba80580dca9634c8505d390c147388adf6746"
+ },
+ {
+ "name": "date",
+ "unicode": "1F4C5",
+ "digest": "de591b8fad608be761b839beefe9e4c2316320bcf0c44c543a1bc4b89923d938"
+ },
+ {
+ "name": "deciduous_tree",
+ "unicode": "1F333",
+ "digest": "ff31a52096ac1eae770f7f71b6d802198add2c8b4d9d7c9327071b6d6ab86c7b"
+ },
+ {
+ "name": "department_store",
+ "unicode": "1F3EC",
+ "digest": "c1e200d5fdd792121acabdb17bbcfe8e28a63757cfd895c72d4909f14de95ac2"
+ },
+ {
+ "name": "descending_notes",
+ "unicode": "1F39D",
+ "digest": "f09c6a2e094b13bf91cc07b7b776e43348ccef9f91247ca36cc02e7d91098af0"
+ },
+ {
+ "name": "desert",
+ "unicode": "1F3DC",
+ "digest": "e45815250bfc5411de516f87efa218874bcda4b0420b4c17182efc22ba0ce80d"
+ },
+ {
+ "name": "desktop",
+ "unicode": "1F5A5",
+ "digest": "ba46323e695918e7253f1013cb991efb09790581c74c07c38bc5e10a20b8e8de"
+ },
+ {
+ "name": "desktop_window",
+ "unicode": "1F5D4",
+ "digest": "d5b6c4a847e2a96f97f50fd353a22cb121915cb1d7bbc0f02df38769819b6b7e"
+ },
+ {
+ "name": "diamond_shape_with_a_dot_inside",
+ "unicode": "1F4A0",
+ "digest": "4e0e6364b8682dec9a9e20676161c9c9c0faf0a5fdd5402ca2668b18f2bb850a"
+ },
+ {
+ "name": "diamonds",
+ "unicode": "2666",
+ "digest": "42b13b2ed8e5fc63fbe81263c06cc203ba18a45ed5cc2a4fdbf617d219a0d3b4"
+ },
+ {
+ "name": "disappointed",
+ "unicode": "1F61E",
+ "digest": "7f1a619fef84960a9f312d17a58aa58105a4f20a4072efb10227892ab22475d8"
+ },
+ {
+ "name": "disappointed_relieved",
+ "unicode": "1F625",
+ "digest": "a389f5e0a4b619dbc406217967fb1f8f3d0e49b3f790e554ae0ececadbf98967"
+ },
+ {
+ "name": "dividers",
+ "unicode": "1F5C2",
+ "digest": "bf4c303452a4c0b4986925041dbec5b7e478060d560630b7c5bc2f997fcad668"
+ },
+ {
+ "name": "dizzy",
+ "unicode": "1F4AB",
+ "digest": "d6fba9b906f0eabd46686e416273a2ca6634249374385f2abf7ed284f0eef995"
+ },
+ {
+ "name": "dizzy_face",
+ "unicode": "1F635",
+ "digest": "b55e20c1551a2912bb5ec64a66c788c9d6f21594cc1da66032188f3814b03f40"
+ },
+ {
+ "name": "do_not_litter",
+ "unicode": "1F6AF",
+ "digest": "126f8c4085e0a8de8241f211f96c3f42c3e3400ea7d8fdf79a14443c3eceb972"
+ },
+ {
+ "name": "document",
+ "unicode": "1F5CE",
+ "digest": "2cbca96cc69306a10f1a9b6505723e027239439d899f6b395dc43f3c37d2d777"
+ },
+ {
+ "name": "document_text",
+ "unicode": "1F5B9",
+ "digest": "29407b12409c9673f3d89ef1f86ee50cbc7ed53b1870e33b4a29bbc609017f72"
+ },
+ {
+ "name": "dog",
+ "unicode": "1F436",
+ "digest": "c7b729de8a0967b1f38c3fa5ded94e77e329588caeaaf43abfd1090f420e62bf"
+ },
+ {
+ "name": "dog2",
+ "unicode": "1F415",
+ "digest": "e1897ca60bb3d2662cbe7933352e2b9c50739adf5901d3328797bf399575b97a"
+ },
+ {
+ "name": "dollar",
+ "unicode": "1F4B5",
+ "digest": "7db1e57f799439df1295d42b5249393f1e8cacc8df54caf30499c967a7282742"
+ },
+ {
+ "name": "dolls",
+ "unicode": "1F38E",
+ "digest": "398e7ff5780328700aadded7ce8c50757b1096af5cec66cc4d813a6714686b6d"
+ },
+ {
+ "name": "dolphin",
+ "unicode": "1F42C",
+ "digest": "27385af08848d93acdd13f72751074c2cbccb5ab3c6047e334598af74ed4862d"
+ },
+ {
+ "name": "door",
+ "unicode": "1F6AA",
+ "digest": "3365d7834086328ecbf1da0037f1cf1d0eb49534e173f7962a9e8f4b2ab87e26"
+ },
+ {
+ "name": "doughnut",
+ "unicode": "1F369",
+ "digest": "b4b99fdfe8d07b49cbdd78f8c57e4424819a4ffc8a3ba4867da44cbb3b3a5cca"
+ },
+ {
+ "name": "dove",
+ "unicode": "1F54A",
+ "digest": "4e2e9c47e5632efe6ccf945d61dbc2f1155a2e52905e17f307b502a2c951bdb8"
+ },
+ {
+ "name": "dragon",
+ "unicode": "1F409",
+ "digest": "d7d016568b54d67017681a075fb799d4a2a790ecfa2946d02dbcee629eb4975d"
+ },
+ {
+ "name": "dragon_face",
+ "unicode": "1F432",
+ "digest": "4d0025f1df63b62448477a8f08a50704e15caafb10fea476b529113f41797ab9"
+ },
+ {
+ "name": "dress",
+ "unicode": "1F457",
+ "digest": "02d56ed227280eaf5ad92830ee304afb81f74bb5a13c855397bcd04dd7fa51fb"
+ },
+ {
+ "name": "dromedary_camel",
+ "unicode": "1F42A",
+ "digest": "5afe8a0b73f9f4560264020b1e02a566149dbc38c15a00d2fb5cd90b32d09a75"
+ },
+ {
+ "name": "droplet",
+ "unicode": "1F4A7",
+ "digest": "a92c419792cbd3ba90ed21547362134cfac3e17a5304ee4e3872c9f7b561f834"
+ },
+ {
+ "name": "dvd",
+ "unicode": "1F4C0",
+ "digest": "1ba23e2f01ced5e192e4c1d2f766d9bce400470e81c81410139fd3c0739422df"
+ },
+ {
+ "name": "e-mail",
+ "unicode": "1F4E7",
+ "digest": "12135310cfedc091d120426f5b132df82b538c5fcad458bf6b21588f353c3adb"
+ },
+ {
+ "name": "ear",
+ "unicode": "1F442",
+ "digest": "70ba1103a34e68590d91a3b6f8acdbad3b1c65e46e31e26ee1cb855c1e21095e"
+ },
+ {
+ "name": "ear_of_rice",
+ "unicode": "1F33E",
+ "digest": "ddd5f3cc83dbdafd9115861eecd0128e52165bb1dd0049df06ffc564b650d384"
+ },
+ {
+ "name": "ear_tone1",
+ "unicode": "1F442-1F3FB",
+ "digest": "72977be94f5d287a09d175f98fba8b7955ae13aa12ce8e029c0ca875c02ee820"
+ },
+ {
+ "name": "ear_tone2",
+ "unicode": "1F442-1F3FC",
+ "digest": "5ff2e46cb3be7f13b8b94092246b58dab4c2a9ee2a5a46e0b84cf35a6928141f"
+ },
+ {
+ "name": "ear_tone3",
+ "unicode": "1F442-1F3FD",
+ "digest": "19b523f5ada2acaea94b922059c458a3303f4da1dd4c197cf25d31a0e6ecc4b2"
+ },
+ {
+ "name": "ear_tone4",
+ "unicode": "1F442-1F3FE",
+ "digest": "6a5cca9f49c539ef7d0883a2f39652f33ee2d3b25dca0234e4ba027ebbb2b466"
+ },
+ {
+ "name": "ear_tone5",
+ "unicode": "1F442-1F3FF",
+ "digest": "a0a56e8abd36e9be6e2448bcee6f56ecb8bf62d728b19ab6e8f9c6338e226b67"
+ },
+ {
+ "name": "earth_africa",
+ "unicode": "1F30D",
+ "digest": "d4921b543d7cf0c7344fa50c5e4d5a76c208d900be852adc1ee82ed4e8861a39"
+ },
+ {
+ "name": "earth_americas",
+ "unicode": "1F30E",
+ "digest": "61691e6aa9b8d90fc7f75fbc6cc7add5c36022d38f3e05c9d7c54dc44cf865bb"
+ },
+ {
+ "name": "earth_asia",
+ "unicode": "1F30F",
+ "digest": "262904cb552c7f5cf828a11071b3d430a74824b7464e8759ef93ee23b1705767"
+ },
+ {
+ "name": "egg",
+ "unicode": "1F373",
+ "digest": "a7dd617cad489c481ffd14937d9ed491cdd5756903e00473f42600c2fbefb600"
+ },
+ {
+ "name": "eggplant",
+ "unicode": "1F346",
+ "digest": "e5402e8ae5b7f9699ed86b97c242f7939d5731c5a364a2d5b9d04ea5d293cda1"
+ },
+ {
+ "name": "eight",
+ "unicode": "0038-20E3",
+ "digest": "34e293d3228e4643a0132d592f96db91b651fe6ced056ac3c8a3fd49c5ed3416"
+ },
+ {
+ "name": "eight_pointed_black_star",
+ "unicode": "2734",
+ "digest": "c3c2da75731a9a0f4f0a8d1f9cffef75c35e19b7f5d4081da33ac12b46be5fc2"
+ },
+ {
+ "name": "eight_spoked_asterisk",
+ "unicode": "2733",
+ "digest": "cc69618c1074d2b00e6f2c49df5e2c5ff6f4c0fae305505eb8c9daa65a0ea340"
+ },
+ {
+ "name": "electric_plug",
+ "unicode": "1F50C",
+ "digest": "732e1d1675233a0b4643cb73d0c352f8a5a56a11ee90d26627ad1e43c2e4a8e5"
+ },
+ {
+ "name": "elephant",
+ "unicode": "1F418",
+ "digest": "08df3910c4d5d8f49a72c47dd938195e495bde8fd8b3e7b17098a2c1afc41634"
+ },
+ {
+ "name": "end",
+ "unicode": "1F51A",
+ "digest": "05844ab9dcb43deff86f04617af6ea09215595de1415dcfaae018bced57938fe"
+ },
+ {
+ "name": "envelope",
+ "unicode": "2709",
+ "digest": "aad272511d0db910437ba25cf1fb9c806d47aad92a232edb87055916daf4676a"
+ },
+ {
+ "name": "envelope_back",
+ "unicode": "1F582",
+ "digest": "bc60b6d375feee00758a94a05b42eeb165f4084b20eb3e6012b72faa221f7e75"
+ },
+ {
+ "name": "envelope_flying",
+ "unicode": "1F585",
+ "digest": "9d6b6ca4c08006062a6f11948de3e15b13cf5c458967e39a9358665a8e13e214"
+ },
+ {
+ "name": "envelope_stamped",
+ "unicode": "1F583",
+ "digest": "f6102aea7283ddc136bfeb09589573420b9279105045fc6b965c1633c1297468"
+ },
+ {
+ "name": "envelope_stamped_pen",
+ "unicode": "1F586",
+ "digest": "80ea471318d1e04f8e525ff236b3cd4a4c864e66c6246b6aad77d92f56895f33"
+ },
+ {
+ "name": "envelope_with_arrow",
+ "unicode": "1F4E9",
+ "digest": "c1ba19b5e7cf64c547ac46eee139e6af70700d49ab511a96e6828c30feb116bc"
+ },
+ {
+ "name": "euro",
+ "unicode": "1F4B6",
+ "digest": "f571952583ffecfa5777065e4d1b680c423d25bc80e567a48fb5d7a1c1b5e735"
+ },
+ {
+ "name": "european_castle",
+ "unicode": "1F3F0",
+ "digest": "db82e383975d079a7bb006e7868035088d75c33bd4031cf8466b71089b65426f"
+ },
+ {
+ "name": "european_post_office",
+ "unicode": "1F3E4",
+ "digest": "d9b38e0f0ca3ad8895b40c767bdbb2b142ccaf03a86c2f275f57a31ed478801a"
+ },
+ {
+ "name": "evergreen_tree",
+ "unicode": "1F332",
+ "digest": "60d8b2d86b20255341f7ecad6d0f178ba9db5fa6b3de92f1b439cdb19f2fc0b1"
+ },
+ {
+ "name": "exclamation",
+ "unicode": "2757",
+ "digest": "cd900ecf82de2b26f0d7783dac4b3232ae94d2cddad5bfacea2eaf65b7ac0a09"
+ },
+ {
+ "name": "expressionless",
+ "unicode": "1F611",
+ "digest": "2ec9466b2d629907ce4c3e24e57f7ee556d2258ff011d972e14d0ae969a40c51"
+ },
+ {
+ "name": "eye",
+ "unicode": "1F441",
+ "digest": "790841e8fce647173eec3c5019440ad9c7e916c535f92acb3132bd92df148cad"
+ },
+ {
+ "name": "eye_in_speech_bubble",
+ "unicode": "1F441-1F5E8",
+ "digest": "bcde5a89a7653bff302685d9d632dd2723796a7ac73125fb7b9493d1ca848e0a"
+ },
+ {
+ "name": "eyeglasses",
+ "unicode": "1F453",
+ "digest": "fd140bef19c420bafe59368d35dd58a58a53e7145b104bae94be10f90679213b"
+ },
+ {
+ "name": "eyes",
+ "unicode": "1F440",
+ "digest": "57ed1f87ebe2485ea32ea69abdb8c5f7ccdcc149b33e74230d801f0883c68c5d"
+ },
+ {
+ "name": "factory",
+ "unicode": "1F3ED",
+ "digest": "6e6b35ae013e5dd26852c9a95d05c39e89c1c1950a33f47e7b951c34af18f37c"
+ },
+ {
+ "name": "fallen_leaf",
+ "unicode": "1F342",
+ "digest": "28ba8628065ffa973b525dd1455691c828d49c2b8c814af387880c13f6707f7e"
+ },
+ {
+ "name": "family",
+ "unicode": "1F46A",
+ "digest": "b5307f86e54cfea581e8406f4b95c801e250a893a9d208cc9a69a6d910b90932"
+ },
+ {
+ "name": "family_mmb",
+ "unicode": "1F468-1F468-1F466",
+ "digest": "49a753c3fcd4420800dd1cda585dae6bfa81615ad4862b477246456f86dc9e82"
+ },
+ {
+ "name": "family_mmbb",
+ "unicode": "1F468-1F468-1F466-1F466",
+ "digest": "882a3a0048efd666b0ab3a07b9f08041aa3a2acdab02664d0feff30bbfa70d68"
+ },
+ {
+ "name": "family_mmg",
+ "unicode": "1F468-1F468-1F467",
+ "digest": "45dd75c19d260a658c8ac93cf878976b96d2000f0efc9c59e72dacc80afb08fa"
+ },
+ {
+ "name": "family_mmgb",
+ "unicode": "1F468-1F468-1F467-1F466",
+ "digest": "910f44a348a951d36ee1f1484d237085bec5083c3875a4d908831dfc64530eaf"
+ },
+ {
+ "name": "family_mmgg",
+ "unicode": "1F468-1F468-1F467-1F467",
+ "digest": "012e75ad0d1b16c2ce63bf80a1ebfb1fc194229cfaf1241039599b82832f6aee"
+ },
+ {
+ "name": "family_mwbb",
+ "unicode": "1F468-1F469-1F466-1F466",
+ "digest": "049a32f61c54f093d2124e25f8b2ec7eac13161e2f2ebf6dc067797698cbe831"
+ },
+ {
+ "name": "family_mwg",
+ "unicode": "1F468-1F469-1F467",
+ "digest": "ba32c637caba634bda99ccba2a1a2a4b6f33aaaed933c30c7d5a51e8de1790d0"
+ },
+ {
+ "name": "family_mwgb",
+ "unicode": "1F468-1F469-1F467-1F466",
+ "digest": "198faba987f45429329b93bbce4f111329f284558bf0eecfa1424186b5f009fe"
+ },
+ {
+ "name": "family_mwgg",
+ "unicode": "1F468-1F469-1F467-1F467",
+ "digest": "3fa2e57cba314dcff04cf8186914823e1e081aabf34fa7437b05c58015df400c"
+ },
+ {
+ "name": "family_wwb",
+ "unicode": "1F469-1F469-1F466",
+ "digest": "b9592fc110a25a478569075deaa520308ef74579cd47aa44df9836599d68143f"
+ },
+ {
+ "name": "family_wwbb",
+ "unicode": "1F469-1F469-1F466-1F466",
+ "digest": "88f398997835fcf5153f17f6baf0deeb2a9c25ce2f8422192c18ac23e90b3193"
+ },
+ {
+ "name": "family_wwg",
+ "unicode": "1F469-1F469-1F467",
+ "digest": "c8d859d3c957fe0d535efccde295fe99bab76e3d28ab5a49c8e736608461cb2e"
+ },
+ {
+ "name": "family_wwgb",
+ "unicode": "1F469-1F469-1F467-1F466",
+ "digest": "006506e4a3d0c82642a0c8481ce95e5e3b969e20fe2def0a16dd686afddbc705"
+ },
+ {
+ "name": "family_wwgg",
+ "unicode": "1F469-1F469-1F467-1F467",
+ "digest": "2553f0deab133aad09b99411d9dd68b56fede30f55ee1f354358767765e36673"
+ },
+ {
+ "name": "fast_forward",
+ "unicode": "23E9",
+ "digest": "1baaed10969b60c083da754ee056bb71df36182cc65af40640acfb76f6b39200"
+ },
+ {
+ "name": "fax",
+ "unicode": "1F4E0",
+ "digest": "b0a392192d03bd5d1ad5ee8eea933cf64725b1776819537bbed27561d78192e7"
+ },
+ {
+ "name": "fearful",
+ "unicode": "1F628",
+ "digest": "7c4cc4de3357c2a6d6e779342b09dabb3ef832a32f2778a0ba074b446f588e8f"
+ },
+ {
+ "name": "feet",
+ "unicode": "1F43E",
+ "digest": "cae13fb54ec64dbcf86ea25bebe2b79877e2d4f5d810b867f095f1d3dfc7f144"
+ },
+ {
+ "name": "ferris_wheel",
+ "unicode": "1F3A1",
+ "digest": "a710a8a0fb039d953313b75330db37e3228d856593547b1f04dc83c00168b987"
+ },
+ {
+ "name": "ferry",
+ "unicode": "26F4",
+ "digest": "21ea239b5adb68dc1ce6c5a1993b0a0b835ef6cc7a0a27cb890838d8475504f6"
+ },
+ {
+ "name": "field_hockey",
+ "unicode": "1F3D1",
+ "digest": "1e46c7f0b5b79c90a5d211ea14cd7e358b1a26a3c8294439253f2b08d0e5c92e"
+ },
+ {
+ "name": "file_cabinet",
+ "unicode": "1F5C4",
+ "digest": "c0b7bdab6c98909eb0fbf1ac89da0008bb00ddb1cb57fe64b4a5ac993eeb18c9"
+ },
+ {
+ "name": "file_folder",
+ "unicode": "1F4C1",
+ "digest": "d98f93c6d7283df0c45f08d3d31ecf5b91b6db1b735959f19e42bfada500a0d1"
+ },
+ {
+ "name": "film_frames",
+ "unicode": "1F39E",
+ "digest": "754a0a60e978f8299a0c4f8959e1f9260f01683e15ae943db430036f01a79b18"
+ },
+ {
+ "name": "finger_pointing_down",
+ "unicode": "1F597",
+ "digest": "0c542ac3141e8f2e74767acd0eb399c2d68c779cb78bf16d437ad3b1f8134ad9"
+ },
+ {
+ "name": "finger_pointing_down2",
+ "unicode": "1F59F",
+ "digest": "c5b128a232cbf518544802a2ae1459368274297163721fa05d0103cf95b2b1ee"
+ },
+ {
+ "name": "finger_pointing_left",
+ "unicode": "1F598",
+ "digest": "d178ece691e2091be08db77fda9cf05462934628557358a8cb6222587b291f7e"
+ },
+ {
+ "name": "finger_pointing_right",
+ "unicode": "1F599",
+ "digest": "a412a47544d8f401f9181f8826c5fa3d6b42a1d76f6926963c2d9cd2a01be06d"
+ },
+ {
+ "name": "finger_pointing_up",
+ "unicode": "1F59E",
+ "digest": "32c2ccab52aa318a47c816d1bcf9c076e667c9ef3e64ce37d7ba7e827238690d"
+ },
+ {
+ "name": "fire",
+ "unicode": "1F525",
+ "digest": "b44311874681135acbb5e7226febe4365c732da3a9617f10d7074a3b1ade1641"
+ },
+ {
+ "name": "fire_engine",
+ "unicode": "1F692",
+ "digest": "3ae03fa34a7088ada95458eb4ee3e97691b3489149f6bbc168086f0483ed3bb2"
+ },
+ {
+ "name": "fire_engine_oncoming",
+ "unicode": "1F6F1",
+ "digest": "e2482c450136d373f74dfafddf502e0b675eb5d2e1e1c645f163db0e4d15fbb6"
+ },
+ {
+ "name": "fireworks",
+ "unicode": "1F386",
+ "digest": "3dee83a27c406960253ca1460eb88a599c7b81506051b69605a421b17fe8282c"
+ },
+ {
+ "name": "first_quarter_moon",
+ "unicode": "1F313",
+ "digest": "8fa066362d77bd889090bbe0904ca47f34704e29781c67133c6eaa521c3e1972"
+ },
+ {
+ "name": "first_quarter_moon_with_face",
+ "unicode": "1F31B",
+ "digest": "8877edb366f8eaa00fd83200acf5a17c3b84d246a250519d565dda3aea866ec3"
+ },
+ {
+ "name": "fish",
+ "unicode": "1F41F",
+ "digest": "9ce742108794cc15e59f7719623ae938efbd8155c93ad72585a32f4e32ea9414"
+ },
+ {
+ "name": "fish_cake",
+ "unicode": "1F365",
+ "digest": "1b5b14509287e30da9b8d7abcec376b247f9095aea4bf3fc320349f061a4c321"
+ },
+ {
+ "name": "fishing_pole_and_fish",
+ "unicode": "1F3A3",
+ "digest": "35db56776db1fcec7c8479922d57d54da2577cfe44a894bfd78c51c950c450fb"
+ },
+ {
+ "name": "fist",
+ "unicode": "270A",
+ "digest": "6b80ac2e4d8b830ae06f7c1626d456460094e4ba20c20fb82dabb6b3d2ce7605"
+ },
+ {
+ "name": "fist_tone1",
+ "unicode": "270A-1F3FB",
+ "digest": "d7c79f4f988dd68f064baa5a3a568ab299f8d409db45c8463f39b80e5dd6081f"
+ },
+ {
+ "name": "fist_tone2",
+ "unicode": "270A-1F3FC",
+ "digest": "d1108194e2d962f9ccd00131876d769a8e003117a460d18b2ccbf93e0a0ea346"
+ },
+ {
+ "name": "fist_tone3",
+ "unicode": "270A-1F3FD",
+ "digest": "12f5644b632c95a5c2e41cc9af299e286e266db8b3860091ef5be5f0c4ccc026"
+ },
+ {
+ "name": "fist_tone4",
+ "unicode": "270A-1F3FE",
+ "digest": "521a3ac573381f3bc37a08ddd2d122767aaa0b6b7a38050d3671a12343351816"
+ },
+ {
+ "name": "fist_tone5",
+ "unicode": "270A-1F3FF",
+ "digest": "604e5a234da1b9160e506b3c9026faf9e04268fced7b44baa1ef5e3d4efa83a4"
+ },
+ {
+ "name": "five",
+ "unicode": "0035-20E3",
+ "digest": "0cbd6cd11eb6c2d67749112750d125f4f0a07b53bb7bfb1de0986d943ea9d632"
+ },
+ {
+ "name": "flag_ac",
+ "unicode": "1F1E6-1F1E8",
+ "digest": "d9db1edeb709824a1083c2bba79ca5f683ed0edded35918bb167d1ee7396c8da"
+ },
+ {
+ "name": "flag_ad",
+ "unicode": "1F1E6-1F1E9",
+ "digest": "04a8c1745d9b8b20e903302379f2557e8082f72e33878db4cb2cd6b33eb97952"
+ },
+ {
+ "name": "flag_ae",
+ "unicode": "1F1E6-1F1EA",
+ "digest": "868324ac2e7bea1547f5de95f39633b77b8d62f3b3433b3d1a4ee96d169a09cd"
+ },
+ {
+ "name": "flag_af",
+ "unicode": "1F1E6-1F1EB",
+ "digest": "9a94458519e9db5d6cf1557e54fdf62d7e48aaf7de25744a093ec8f284656226"
+ },
+ {
+ "name": "flag_ag",
+ "unicode": "1F1E6-1F1EC",
+ "digest": "ea59fabc2bd9024df06a59a34412f52bebfeb03eb6abd73d8fe153e3a68e28f4"
+ },
+ {
+ "name": "flag_ai",
+ "unicode": "1F1E6-1F1EE",
+ "digest": "75676ded736ad2ebb921e9fd8ebfef49819a35c3dcf005bbc3b7e8c6e75178f2"
+ },
+ {
+ "name": "flag_al",
+ "unicode": "1F1E6-1F1F1",
+ "digest": "77b835dcff399b609e2479cbf10f08344c8fc277370ba8e4540165ca15563847"
+ },
+ {
+ "name": "flag_am",
+ "unicode": "1F1E6-1F1F2",
+ "digest": "3b820c628dd5a93137f7288a43553778f60b0beea4c0a239d063893c0723e73d"
+ },
+ {
+ "name": "flag_ao",
+ "unicode": "1F1E6-1F1F4",
+ "digest": "d26439d4ecbe8b67bb1ae9753454505358ebb6b802624f19800471e53ee27187"
+ },
+ {
+ "name": "flag_aq",
+ "unicode": "1F1E6-1F1F6",
+ "digest": "6b0b4e800d88ab289ae4b6d449bfa115e92543958b477d13ad348468a74e4616"
+ },
+ {
+ "name": "flag_ar",
+ "unicode": "1F1E6-1F1F7",
+ "digest": "ca76db601dd3f5794f1caace8ab5641fe3786b86e4ae030706162f0ce07d27b3"
+ },
+ {
+ "name": "flag_as",
+ "unicode": "1F1E6-1F1F8",
+ "digest": "170e1dde0e3fd2e0f2149de5cc8845e15580cc0412e81a643d61bd387de16141"
+ },
+ {
+ "name": "flag_at",
+ "unicode": "1F1E6-1F1F9",
+ "digest": "0ab3675a16b4988e87c81e87453c160d6616c7be76247f54c471dc63aa8b42ba"
+ },
+ {
+ "name": "flag_au",
+ "unicode": "1F1E6-1F1FA",
+ "digest": "b6f17d3dfd3547c069a0b6cddd4cf44fb8ce1d1d300e24284fb292ac142537e3"
+ },
+ {
+ "name": "flag_aw",
+ "unicode": "1F1E6-1F1FC",
+ "digest": "7857bc907f04dfb7ccc4401c05034ad8afb6383a022db77973cfcafa4d6c16c8"
+ },
+ {
+ "name": "flag_ax",
+ "unicode": "1F1E6-1F1FD",
+ "digest": "ab8f1fd4af7c220a54d478cec5a9f7f3beb5fc83439c448f3ac9848af8391ac1"
+ },
+ {
+ "name": "flag_az",
+ "unicode": "1F1E6-1F1FF",
+ "digest": "187cc7b6d39800c5910a34409db1e6b1d8aac808c72a93e922a419d9b054fd0b"
+ },
+ {
+ "name": "flag_ba",
+ "unicode": "1F1E7-1F1E6",
+ "digest": "cd22c744213087384cf79ed314742026787212c9ceb6999ed166534670f7864a"
+ },
+ {
+ "name": "flag_bb",
+ "unicode": "1F1E7-1F1E7",
+ "digest": "44ff0a48ac2d2180374baa58b1b7c64f26d0d151a48811eb08ffa20758104512"
+ },
+ {
+ "name": "flag_bd",
+ "unicode": "1F1E7-1F1E9",
+ "digest": "c18793d2b963458607a0bab94c57e62c8278fce870e96fd8dda78067a8fbde18"
+ },
+ {
+ "name": "flag_be",
+ "unicode": "1F1E7-1F1EA",
+ "digest": "6e6ccfca064a43b93c8acc04a9425f95af204198022ca20b9ee6c491e99ad950"
+ },
+ {
+ "name": "flag_bf",
+ "unicode": "1F1E7-1F1EB",
+ "digest": "d69c0394a1c7cb6323f54f024b7d740c728f229ca5e1b54ac374d5024f5470a5"
+ },
+ {
+ "name": "flag_bg",
+ "unicode": "1F1E7-1F1EC",
+ "digest": "413a270caf4a9155e84bdba6c9512277f5642246f6ba8d701383a5eeb02f7e95"
+ },
+ {
+ "name": "flag_bh",
+ "unicode": "1F1E7-1F1ED",
+ "digest": "9243ed65d7f24c824c2a3207335a2d4ad25251258547c16d0b7b7cbb9df6f8de"
+ },
+ {
+ "name": "flag_bi",
+ "unicode": "1F1E7-1F1EE",
+ "digest": "63056519030524b2d2dcd47448267d817205dbd6b98075c97f011a8f1d4d1a4b"
+ },
+ {
+ "name": "flag_bj",
+ "unicode": "1F1E7-1F1EF",
+ "digest": "93b245eed85d22260d27d1a8c77f51fb3439309e09b2aeca6cd504dbea77b509"
+ },
+ {
+ "name": "flag_bl",
+ "unicode": "1F1E7-1F1F1",
+ "digest": "5e1e478deaf02bbaa26595e4cefc5f5c9bec6105ce521b7b9ab4fa5e7a452c14"
+ },
+ {
+ "name": "flag_black",
+ "unicode": "1F3F4",
+ "digest": "df131e5c28e9f51dea53fe7f33551f91d420f7d686b7a62980f0154c6b5357a6"
+ },
+ {
+ "name": "flag_bm",
+ "unicode": "1F1E7-1F1F2",
+ "digest": "9dcd9e60faebe7f93eb19157e99f2ad654a8145c61738de96e6ecd11a246764a"
+ },
+ {
+ "name": "flag_bn",
+ "unicode": "1F1E7-1F1F3",
+ "digest": "078af6ca481a77871ba005e251a46ce63951c27b1b0cd33b9c1d0d31d349bc1a"
+ },
+ {
+ "name": "flag_bo",
+ "unicode": "1F1E7-1F1F4",
+ "digest": "92516d04e922a3bcbabe2e7619194bc972c09ba97576e8155f9829c397a71d8c"
+ },
+ {
+ "name": "flag_bq",
+ "unicode": "1F1E7-1F1F6",
+ "digest": "7832df5267a2bb8dddb83aeb11162ce79aeebdb718f2ac0e54adcf3d87936171"
+ },
+ {
+ "name": "flag_br",
+ "unicode": "1F1E7-1F1F7",
+ "digest": "aabcc1c082124045ed214f7d9778d8e2ed791ebb8433defea91db458658abeec"
+ },
+ {
+ "name": "flag_bs",
+ "unicode": "1F1E7-1F1F8",
+ "digest": "f628f39003608e181696634929522884165e27ccef55270293f92eeef991635f"
+ },
+ {
+ "name": "flag_bt",
+ "unicode": "1F1E7-1F1F9",
+ "digest": "af24a8ab34815da04c3e5af49a47449e0de93b068957cbda695816d0f830ca12"
+ },
+ {
+ "name": "flag_bv",
+ "unicode": "1F1E7-1F1FB",
+ "digest": "ff0037f6eed95d4bb5f2b502902360e1ff41426e2896daf3e0730cef1f8f7e41"
+ },
+ {
+ "name": "flag_bw",
+ "unicode": "1F1E7-1F1FC",
+ "digest": "3e3241ecb97946cc3e467b083d113a57dd305595e1512d4da18cc403e8689c1d"
+ },
+ {
+ "name": "flag_by",
+ "unicode": "1F1E7-1F1FE",
+ "digest": "bdd21885c6fac475241884a44149b887297772e17617ee59dd9fe8518d52cf3d"
+ },
+ {
+ "name": "flag_bz",
+ "unicode": "1F1E7-1F1FF",
+ "digest": "21c16e1da641af004576000bf1db44b9a1e0fccfddc775e96022721c2f18eeea"
+ },
+ {
+ "name": "flag_ca",
+ "unicode": "1F1E8-1F1E6",
+ "digest": "0d00e459084d58d3ea9c60488a9e51bf45f71b77f1600f190225d5ca6ca6c796"
+ },
+ {
+ "name": "flag_cc",
+ "unicode": "1F1E8-1F1E8",
+ "digest": "86ab27164603ef0f1f83fe898eda6fbb7bc5709f2518f5577f00817860806a7b"
+ },
+ {
+ "name": "flag_cd",
+ "unicode": "1F1E8-1F1E9",
+ "digest": "fdc2796530ada4bd0bae37ace4bbe707b321b287dcd64568f8e01d3a9df56066"
+ },
+ {
+ "name": "flag_cf",
+ "unicode": "1F1E8-1F1EB",
+ "digest": "5943bec02bede0931e21e7c34a68f375499f60a34883cc1edf2f21e9834b15ce"
+ },
+ {
+ "name": "flag_cg",
+ "unicode": "1F1E8-1F1EC",
+ "digest": "54498482e2772371e148e05cfb7c5eb55f6a22cd528662abdea10bad47d157da"
+ },
+ {
+ "name": "flag_ch",
+ "unicode": "1F1E8-1F1ED",
+ "digest": "53d6d35aeeebb0b4b1ad858dc3691e649ac73d30b3be76f96d5fe9605fa99386"
+ },
+ {
+ "name": "flag_ci",
+ "unicode": "1F1E8-1F1EE",
+ "digest": "3a173a3058a5c0174dc88750852cafec264e901ce82a6c69db122c8c0ea71a3a"
+ },
+ {
+ "name": "flag_ck",
+ "unicode": "1F1E8-1F1F0",
+ "digest": "42f395ff53c618b72b8a224cd4343d1a32f5ad82ced56bf590170a5ff0d5134c"
+ },
+ {
+ "name": "flag_cl",
+ "unicode": "1F1E8-1F1F1",
+ "digest": "9d6255feb690596904d800e72d5acdb5cda941c5a741b031ea39a3c7650ac46f"
+ },
+ {
+ "name": "flag_cm",
+ "unicode": "1F1E8-1F1F2",
+ "digest": "ffc99d14e0a8b46a980331090ed9f36f31a87f1b0f8dd8c09007a31c6127c69e"
+ },
+ {
+ "name": "flag_cn",
+ "unicode": "1F1E8-1F1F3",
+ "digest": "869a98c52bdc33591f87e2aab6cb4f13e98bb19136250ff25805d0312a8b7c8a"
+ },
+ {
+ "name": "flag_co",
+ "unicode": "1F1E8-1F1F4",
+ "digest": "6aa458440eb2500ad307fea40fd8f1171a1506a6e32af144a4fd51545bb56151"
+ },
+ {
+ "name": "flag_cp",
+ "unicode": "1F1E8-1F1F5",
+ "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee"
+ },
+ {
+ "name": "flag_cr",
+ "unicode": "1F1E8-1F1F7",
+ "digest": "0f3b54d8330c5bb136647547dafc598bda755697cfd6b7d872a2443ba7b5cad4"
+ },
+ {
+ "name": "flag_cu",
+ "unicode": "1F1E8-1F1FA",
+ "digest": "69bc973002475bb3d9b54cb0ba9ec9cb85f144c1cf54689da0ee8f414ebb0d83"
+ },
+ {
+ "name": "flag_cv",
+ "unicode": "1F1E8-1F1FB",
+ "digest": "af2e135cf3c1b03a5937c068a75061b5cd332e95902fd0f8dffb2ac2dc89692a"
+ },
+ {
+ "name": "flag_cw",
+ "unicode": "1F1E8-1F1FC",
+ "digest": "df4b2228a82f766c5c64c13c1388482a68549e59dd843671ee0eb43506e33411"
+ },
+ {
+ "name": "flag_cx",
+ "unicode": "1F1E8-1F1FD",
+ "digest": "db12e513345a7be53954167d359ede0b3effbfb292508ee4d726123e3a8f83d7"
+ },
+ {
+ "name": "flag_cy",
+ "unicode": "1F1E8-1F1FE",
+ "digest": "0cea41d4820746e2c6eb408f7ec7419afba9f7396401d92e6c1d77382f721d0b"
+ },
+ {
+ "name": "flag_cz",
+ "unicode": "1F1E8-1F1FF",
+ "digest": "a1c2405916963be306f761539123486a2845af53716c9dfe94ad5420e14d36c4"
+ },
+ {
+ "name": "flag_de",
+ "unicode": "1F1E9-1F1EA",
+ "digest": "74a80b64437bc4e31bdd7cbb753ecd2d719bf34c506cbac535db83a644174cce"
+ },
+ {
+ "name": "flag_dg",
+ "unicode": "1F1E9-1F1EC",
+ "digest": "13cb5ea872f94a9c3fb579cef417e2d1ed38e8cbe95059576380cacd59bc4b9d"
+ },
+ {
+ "name": "flag_dj",
+ "unicode": "1F1E9-1F1EF",
+ "digest": "5b479654c28d3eeb70055c5e25dc46ccaba9eeea7537cc45ca9dbb8186b743b6"
+ },
+ {
+ "name": "flag_dk",
+ "unicode": "1F1E9-1F1F0",
+ "digest": "dee7fa9644a9b447417518a353e7edcbb37b2af8bc7d13a6ed71d7210c43ca3c"
+ },
+ {
+ "name": "flag_dm",
+ "unicode": "1F1E9-1F1F2",
+ "digest": "2e339190a8a0a238140f42e329f6646af5be75763a787ea268488a2e0440dc4c"
+ },
+ {
+ "name": "flag_do",
+ "unicode": "1F1E9-1F1F4",
+ "digest": "be5dafcd32d7197a96d37299a91835a8009299452f05a66d91c5fdec17448230"
+ },
+ {
+ "name": "flag_dz",
+ "unicode": "1F1E9-1F1FF",
+ "digest": "cf525d56bac45fe689f92d441274fc0ecbed4f95591d2c066598f72b1ee8d618"
+ },
+ {
+ "name": "flag_ea",
+ "unicode": "1F1EA-1F1E6",
+ "digest": "1acb13950f7c3692f9a36e618d8ec10a73ead5d7fa80fb52b6b2a18e3d456002"
+ },
+ {
+ "name": "flag_ec",
+ "unicode": "1F1EA-1F1E8",
+ "digest": "4d9d35450efc6026651ccc2278e70fb90b001ca5e5eecd31361b1e4e23253dbd"
+ },
+ {
+ "name": "flag_ee",
+ "unicode": "1F1EA-1F1EA",
+ "digest": "86ec7b2f618fe71dddec3d5a621b56b878d683780f1e0ad446f965326d42df48"
+ },
+ {
+ "name": "flag_eg",
+ "unicode": "1F1EA-1F1EC",
+ "digest": "f06d36a6fec15af4c1a76de30e8469847dde2728bb5a48956b4e466098b778a4"
+ },
+ {
+ "name": "flag_eh",
+ "unicode": "1F1EA-1F1ED",
+ "digest": "eb63f5b92c62c98dc008dfa7ad8830aa17fa23964f812a28055bd8b6f5960c5b"
+ },
+ {
+ "name": "flag_er",
+ "unicode": "1F1EA-1F1F7",
+ "digest": "e901195f7b37b22a6872d36713de0ec176f6424c209e261e5c849ce318c772f6"
+ },
+ {
+ "name": "flag_es",
+ "unicode": "1F1EA-1F1F8",
+ "digest": "27ab5cc6c2e9f26ccdfa632887533eebcd9b514f80cec9e721cf8e5e2544339c"
+ },
+ {
+ "name": "flag_et",
+ "unicode": "1F1EA-1F1F9",
+ "digest": "6cdb3718c9b3ec713258dd36781db58b7da53f3017445056c1a76233e3b4a7de"
+ },
+ {
+ "name": "flag_eu",
+ "unicode": "1F1EA-1F1FA",
+ "digest": "363f60e8a747166d5cec8d70bfdf266411eec2ff07933b6187975075caadfd74"
+ },
+ {
+ "name": "flag_fi",
+ "unicode": "1F1EB-1F1EE",
+ "digest": "1a1959cb551a0e8bdaee8c04657fb7387a4d83173f7759f89468da12e1818a9e"
+ },
+ {
+ "name": "flag_fj",
+ "unicode": "1F1EB-1F1EF",
+ "digest": "f26dc36ea9c1f32d9bb54874ea384e7118b6e2585be69245fdd73acd8304ae78"
+ },
+ {
+ "name": "flag_fk",
+ "unicode": "1F1EB-1F1F0",
+ "digest": "0479e233499b704f91a9b13d083e66296efe2f28ed917ab1496b223bfb09adb8"
+ },
+ {
+ "name": "flag_fm",
+ "unicode": "1F1EB-1F1F2",
+ "digest": "142ea7b4b4a7004329925b495da43ab82351cbaac383c8da6e614b39ba58d05e"
+ },
+ {
+ "name": "flag_fo",
+ "unicode": "1F1EB-1F1F4",
+ "digest": "f1c800d4f4d39e2aead9a11ed500f16108d6bc48bd24bd2a1af7b966d8e76752"
+ },
+ {
+ "name": "flag_fr",
+ "unicode": "1F1EB-1F1F7",
+ "digest": "6f52f36b5199c65ab1cad13ff4e77d2d8b48a8ff79b92166976674ffdc7829ee"
+ },
+ {
+ "name": "flag_ga",
+ "unicode": "1F1EC-1F1E6",
+ "digest": "50a0d5a07466e419b74a4d532738f7958de9baa37df6191be4f3755dccc3b326"
+ },
+ {
+ "name": "flag_gb",
+ "unicode": "1F1EC-1F1E7",
+ "digest": "220f7da6d5a231b766c79f2e1b7d3fdb74ec0c0c17558cc00a8a8ccdf2afc2e0"
+ },
+ {
+ "name": "flag_gd",
+ "unicode": "1F1EC-1F1E9",
+ "digest": "3e162b0d13f4ceea7f663b1d425f13863d104e80df75a640f526e276bcd04081"
+ },
+ {
+ "name": "flag_ge",
+ "unicode": "1F1EC-1F1EA",
+ "digest": "35897f8254675d2efe9e3070c88af9ef214f08440e6ee75ebe81d28cdb57ea2b"
+ },
+ {
+ "name": "flag_gf",
+ "unicode": "1F1EC-1F1EB",
+ "digest": "3a34df321635f71a0f2cc4e1eda58d85c29230c77456362345196351bf56533d"
+ },
+ {
+ "name": "flag_gg",
+ "unicode": "1F1EC-1F1EC",
+ "digest": "c972f8d190b4e9ca8890df41503d202ffd73981833d3f3750f563302167bcd66"
+ },
+ {
+ "name": "flag_gh",
+ "unicode": "1F1EC-1F1ED",
+ "digest": "9c3d3569bd411389fa0af7c6938d4325cedeb9c0e8f059dc1d5a74c6b8d6d01b"
+ },
+ {
+ "name": "flag_gi",
+ "unicode": "1F1EC-1F1EE",
+ "digest": "ede638bc6fedc30a01821025d87ec19297500da9c04a7a155984fca186118649"
+ },
+ {
+ "name": "flag_gl",
+ "unicode": "1F1EC-1F1F1",
+ "digest": "a2ce3371eff1da8331671925f707232aa593ac7400d59555c9ca689729ce24ec"
+ },
+ {
+ "name": "flag_gm",
+ "unicode": "1F1EC-1F1F2",
+ "digest": "932bf6eb75ddd4278268dd2f09d8fffcfef89f8fd6b6e86a08a414cd3ceec94d"
+ },
+ {
+ "name": "flag_gn",
+ "unicode": "1F1EC-1F1F3",
+ "digest": "ebf543713895adaa09d64897f24bd461191191b8fcbbcede52bdaf4bd2dc67a8"
+ },
+ {
+ "name": "flag_gp",
+ "unicode": "1F1EC-1F1F5",
+ "digest": "2e6c48d80c571b34f31fa9b3622dcc51e1707c0118e991e9c177742ff02a8a96"
+ },
+ {
+ "name": "flag_gq",
+ "unicode": "1F1EC-1F1F6",
+ "digest": "b0f5810180d12fc48faf75e73f882dc59072d7bf957f8455bf7e1e336539dc41"
+ },
+ {
+ "name": "flag_gr",
+ "unicode": "1F1EC-1F1F7",
+ "digest": "8d60d6f8910f5179d851dbea0798b56a492c6be85f3d55e1a1126cd1d6663a3b"
+ },
+ {
+ "name": "flag_gs",
+ "unicode": "1F1EC-1F1F8",
+ "digest": "7b07915af0e2364ebc386a162d44846f3a7986fdd24e20ad2bc56d64a103fe9c"
+ },
+ {
+ "name": "flag_gt",
+ "unicode": "1F1EC-1F1F9",
+ "digest": "0c78108ede45bf34917b409a0867f5ec8253c74b694beda083f3e8d04d7a10d8"
+ },
+ {
+ "name": "flag_gu",
+ "unicode": "1F1EC-1F1FA",
+ "digest": "909f1bc98fa1507adb787eb3875503b21ea937d6ae8bb152153916c2da5e13bb"
+ },
+ {
+ "name": "flag_gw",
+ "unicode": "1F1EC-1F1FC",
+ "digest": "f5f34410c7b22d5ed9994b47d0e7a9d9a6a1f05c4d3142f7fef3e4409725f5e6"
+ },
+ {
+ "name": "flag_gy",
+ "unicode": "1F1EC-1F1FE",
+ "digest": "4939cf52ab34a924a31032b42668960a2c7d8d4f998b16b065c247110df334be"
+ },
+ {
+ "name": "flag_hk",
+ "unicode": "1F1ED-1F1F0",
+ "digest": "bde0916df6d62f6b1cf8f85a8a39526c97fc6ef6fedb0b0cae2adb127a08eafe"
+ },
+ {
+ "name": "flag_hm",
+ "unicode": "1F1ED-1F1F2",
+ "digest": "603e6c9bff9a0dc941970a313fe98fbf53ff5a57028f1a2766420be4211711cc"
+ },
+ {
+ "name": "flag_hn",
+ "unicode": "1F1ED-1F1F3",
+ "digest": "2953ad0909bc32c02615f6ad5a4e5f331ba794a41632b1f0fc366e1c640cc2b9"
+ },
+ {
+ "name": "flag_hr",
+ "unicode": "1F1ED-1F1F7",
+ "digest": "41c9ffc4f0faaa2d77e5cffb781329e7d2489ce879bd8eb9c503621e834abc50"
+ },
+ {
+ "name": "flag_ht",
+ "unicode": "1F1ED-1F1F9",
+ "digest": "6a56c3d71b4f858e1774aa2134a9f5584087fec968e9ee8bb1046d2ec93bf059"
+ },
+ {
+ "name": "flag_hu",
+ "unicode": "1F1ED-1F1FA",
+ "digest": "72f5809818d4cab8c0cee73df7f67b820fb8471eea4199911a5917ac099795e8"
+ },
+ {
+ "name": "flag_ic",
+ "unicode": "1F1EE-1F1E8",
+ "digest": "7e2a7667fcd05f927af47e64c5790c104a9956dd9f1a45f03cb0fdcc85d866d3"
+ },
+ {
+ "name": "flag_id",
+ "unicode": "1F1EE-1F1E9",
+ "digest": "4721f616fae2e443e52f1e9cc96e4835bddca16a2d75d7d5afea57cdee866b7f"
+ },
+ {
+ "name": "flag_ie",
+ "unicode": "1F1EE-1F1EA",
+ "digest": "84b19833e6c9fb43187f8a28d85045a3df58816f20a07edab90474323174b1f3"
+ },
+ {
+ "name": "flag_il",
+ "unicode": "1F1EE-1F1F1",
+ "digest": "c99d4bd8c2541cf3a7392c4faf4477d96bc47065dd1423b9e06450483e69b34f"
+ },
+ {
+ "name": "flag_im",
+ "unicode": "1F1EE-1F1F2",
+ "digest": "5eeb12c0315b527ce61649a38b64d76af726a73b2d381d1a1ddd1366bafb1bfc"
+ },
+ {
+ "name": "flag_in",
+ "unicode": "1F1EE-1F1F3",
+ "digest": "ecc3cfcff3368fe0875a51a8be9f4dfd449a187e5beb41a2b34241736247f73b"
+ },
+ {
+ "name": "flag_io",
+ "unicode": "1F1EE-1F1F4",
+ "digest": "26243d60e04ba3bc9eb8f008bfc77b2a64bcf1a3d0073eb0449a8c8121618c9c"
+ },
+ {
+ "name": "flag_iq",
+ "unicode": "1F1EE-1F1F6",
+ "digest": "a1fb5e59575081920b3be5290f654d57a9be099deb56d4ed69eba81a2b531cb3"
+ },
+ {
+ "name": "flag_ir",
+ "unicode": "1F1EE-1F1F7",
+ "digest": "ab89488b934af1d4bdae7ed16dfc74fffe658bb8e95d5161b48cdd06de44ae85"
+ },
+ {
+ "name": "flag_is",
+ "unicode": "1F1EE-1F1F8",
+ "digest": "55db1fc9e6c56d4c9bcb9a46e5e4300cf2a0c32fa91dc24b487a1d56c8097268"
+ },
+ {
+ "name": "flag_it",
+ "unicode": "1F1EE-1F1F9",
+ "digest": "36fc993fb00ab607578a4d0e573e988e17b9459a68a000a48de905a8238589d0"
+ },
+ {
+ "name": "flag_je",
+ "unicode": "1F1EF-1F1EA",
+ "digest": "c608dbfd1259330e2f8c40dc5d12ffd0489396f4fc5f3ca57bcb2f0d9d05c20c"
+ },
+ {
+ "name": "flag_jm",
+ "unicode": "1F1EF-1F1F2",
+ "digest": "a8224b68b2d324f848d75e4376875ef76a8174e6ba32790d9ca622fe1eabfd5f"
+ },
+ {
+ "name": "flag_jo",
+ "unicode": "1F1EF-1F1F4",
+ "digest": "2403563dc2ab4ed0e7e3a0761cc09f96801550bba6b177b54d651d8804ad987d"
+ },
+ {
+ "name": "flag_jp",
+ "unicode": "1F1EF-1F1F5",
+ "digest": "aea8eebd0a0139818cb7629d9c9a8e55160b458eb8ffeee2f36c5cff4b507fd3"
+ },
+ {
+ "name": "flag_ke",
+ "unicode": "1F1F0-1F1EA",
+ "digest": "9c8365f74858743bcdce4a9cf6a6f4110faf2dc6433e5dc7d98c24bb3b32a36d"
+ },
+ {
+ "name": "flag_kg",
+ "unicode": "1F1F0-1F1EC",
+ "digest": "0c72bdb1d64b1e3be3d9516a50655a6162d8501851d2cf2fadb8c6ef7740df4e"
+ },
+ {
+ "name": "flag_kh",
+ "unicode": "1F1F0-1F1ED",
+ "digest": "49e41e488732d789e395091e144cd6215c6818ba2073e5e22ea21203a737d03c"
+ },
+ {
+ "name": "flag_ki",
+ "unicode": "1F1F0-1F1EE",
+ "digest": "9d7f168adbcf5f4cfe28470addfdb0a8b231438d593edb70f633981bfa4c7638"
+ },
+ {
+ "name": "flag_km",
+ "unicode": "1F1F0-1F1F2",
+ "digest": "9318c28957fa7a19eba5ec452c1cbce01a5a83d41d29d081614d3abb0585d478"
+ },
+ {
+ "name": "flag_kn",
+ "unicode": "1F1F0-1F1F3",
+ "digest": "eac7e7d0f023dee5c0c8559bc2c9a96273adda54ce47598025120b30d8d6ebc1"
+ },
+ {
+ "name": "flag_kp",
+ "unicode": "1F1F0-1F1F5",
+ "digest": "d4d53db6f8363174de6db864c056267ba8a7d7e87b5527f2f42bb9b8ac3f362b"
+ },
+ {
+ "name": "flag_kr",
+ "unicode": "1F1F0-1F1F7",
+ "digest": "5c7e61ab4a2aae70cbe51f0ca4718516002bc943b35d870bd853a0c98c4e0ed5"
+ },
+ {
+ "name": "flag_kw",
+ "unicode": "1F1F0-1F1FC",
+ "digest": "5d229cd99d25f4285bd30d98cfcc3cd8346648897476e2905a1811ceeef48d37"
+ },
+ {
+ "name": "flag_ky",
+ "unicode": "1F1F0-1F1FE",
+ "digest": "9ce3d8dfc273d3a400960876c434b702f93df92c6c00682dbed2ec8e3966d8a8"
+ },
+ {
+ "name": "flag_kz",
+ "unicode": "1F1F0-1F1FF",
+ "digest": "a6f0be0a767fa4824495d568d9fc2bd8d4c1a26f363873d3b65362e9383e2a50"
+ },
+ {
+ "name": "flag_la",
+ "unicode": "1F1F1-1F1E6",
+ "digest": "ab2ae96da87f7b53ab212f8dcd897a591cff9ea6666270097a8e739ee0b8f8cb"
+ },
+ {
+ "name": "flag_lb",
+ "unicode": "1F1F1-1F1E7",
+ "digest": "0c3fcab22e9fae1c78658290aff97de785d0b6adb5e3702d00073ce774b7ed54"
+ },
+ {
+ "name": "flag_lc",
+ "unicode": "1F1F1-1F1E8",
+ "digest": "e154b0b3a1635a36e0d9ad518c0ea12259320e5f1ebbda982248486492065d28"
+ },
+ {
+ "name": "flag_li",
+ "unicode": "1F1F1-1F1EE",
+ "digest": "bbc393a89e73cc8c29a0a9297428d07aa1d4717ea9b7d4dd9d69f21ac7d0605d"
+ },
+ {
+ "name": "flag_lk",
+ "unicode": "1F1F1-1F1F0",
+ "digest": "376bd501d113a844971ca1006ab31aa086cd55d74842ea5f3dedaba997b58693"
+ },
+ {
+ "name": "flag_lr",
+ "unicode": "1F1F1-1F1F7",
+ "digest": "9a6ebe1c9d9a53079ee77292a5ad0965f96409b0417f92876a1c3bd463d6a9bc"
+ },
+ {
+ "name": "flag_ls",
+ "unicode": "1F1F1-1F1F8",
+ "digest": "e2f4b05414f6e0c3d629a92b0534d4145475f0214a83a62c902fe0884c833c89"
+ },
+ {
+ "name": "flag_lt",
+ "unicode": "1F1F1-1F1F9",
+ "digest": "d5e2f8b2ffa820a33ea6d612fccd61e32467d25154342f5be134d3520e48387f"
+ },
+ {
+ "name": "flag_lu",
+ "unicode": "1F1F1-1F1FA",
+ "digest": "f43277103292195b51981d08e2dde68eab660a65c7875f510e09a8b2370f1b5c"
+ },
+ {
+ "name": "flag_lv",
+ "unicode": "1F1F1-1F1FB",
+ "digest": "e1288ac5c80d6e9d577d652e34be247ca39bf9d3d7cfc8a6cae13c1f9ac9dc47"
+ },
+ {
+ "name": "flag_ly",
+ "unicode": "1F1F1-1F1FE",
+ "digest": "5122294b769a174e3b6e3d238bb846b3e760929f5bb3c1a708d8a429f3f32f68"
+ },
+ {
+ "name": "flag_ma",
+ "unicode": "1F1F2-1F1E6",
+ "digest": "615a6447ff284de7689b4fd7b04fdda308f65dbbec958cfb96d2977514981d16"
+ },
+ {
+ "name": "flag_mc",
+ "unicode": "1F1F2-1F1E8",
+ "digest": "08b48b28938acbfc0fbc15c25ee14dbad7164c5165d03df2eee370755ee7b4cf"
+ },
+ {
+ "name": "flag_md",
+ "unicode": "1F1F2-1F1E9",
+ "digest": "93d61de68f821e1e08b30e63d91e8b4a657766475128538894cf9da9a3b4e3c0"
+ },
+ {
+ "name": "flag_me",
+ "unicode": "1F1F2-1F1EA",
+ "digest": "ee55c0eb78241aec2baf1822a47fa46d63209ceae3db7617ae886b823ae229ff"
+ },
+ {
+ "name": "flag_mf",
+ "unicode": "1F1F2-1F1EB",
+ "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee"
+ },
+ {
+ "name": "flag_mg",
+ "unicode": "1F1F2-1F1EC",
+ "digest": "86ec8140e2c4854f52cff74757baf0cbb75a4aacca8be6af8c8f9c939a7b866c"
+ },
+ {
+ "name": "flag_mh",
+ "unicode": "1F1F2-1F1ED",
+ "digest": "8311ea3422c9d5e94b55e19b03bedd6fe6e2a191b7657e15ac75a48932958a5b"
+ },
+ {
+ "name": "flag_mk",
+ "unicode": "1F1F2-1F1F0",
+ "digest": "5c6f504f88c5a875c06ac8b26fa6e81a9d79c42a1c7d1fad9a5d4c8ad06ca502"
+ },
+ {
+ "name": "flag_ml",
+ "unicode": "1F1F2-1F1F1",
+ "digest": "d08a4973db40cf28e58ca3c80e8bd4e50d68ba1080b31917aeefdb0e210b5c50"
+ },
+ {
+ "name": "flag_mm",
+ "unicode": "1F1F2-1F1F2",
+ "digest": "5e95089514ca09bb93afb481b317477c9d053adcf450e0b711d78ed1078c7470"
+ },
+ {
+ "name": "flag_mn",
+ "unicode": "1F1F2-1F1F3",
+ "digest": "7a0ca72715dd2a36eeeed2f8c888497cb752f0000af8f07d6930743caf6e4273"
+ },
+ {
+ "name": "flag_mo",
+ "unicode": "1F1F2-1F1F4",
+ "digest": "d2c7c2191bc1bc83d85f2270968cb4de5cf26a11f70e166a8b32c108287ef729"
+ },
+ {
+ "name": "flag_mp",
+ "unicode": "1F1F2-1F1F5",
+ "digest": "89ad06121fd7981338fe188464491bea371f85125bfb4fc01fb5cad606613b1e"
+ },
+ {
+ "name": "flag_mq",
+ "unicode": "1F1F2-1F1F6",
+ "digest": "98176f3af823b26a3657a17c5073ee22367898b40bd3973de76329aa87ca5a2e"
+ },
+ {
+ "name": "flag_mr",
+ "unicode": "1F1F2-1F1F7",
+ "digest": "cc3e705ad84f83fe2d544385c39564743024dab26595d62469b35fdb791f6015"
+ },
+ {
+ "name": "flag_ms",
+ "unicode": "1F1F2-1F1F8",
+ "digest": "465e3d5700b557f2589bd6e34a0c6b12c634a6ed4dcfbee3c1c841c5de3413f0"
+ },
+ {
+ "name": "flag_mt",
+ "unicode": "1F1F2-1F1F9",
+ "digest": "e610ba22d8d8ad750ed10dff8e1b4d89bc34f066c3424bfa77dbdc1a5d79743a"
+ },
+ {
+ "name": "flag_mu",
+ "unicode": "1F1F2-1F1FA",
+ "digest": "3daf015d3b95218677dafbb282b7804686aa68875a6bd1d70c165b7b149e19cb"
+ },
+ {
+ "name": "flag_mv",
+ "unicode": "1F1F2-1F1FB",
+ "digest": "d30e4bfd04f08177de92f3c175600aaafa89b9668bbe2b83f35f07a74382065c"
+ },
+ {
+ "name": "flag_mw",
+ "unicode": "1F1F2-1F1FC",
+ "digest": "f364b1c8bfda3f86b5e26422eedc571ba11e312dcc634197631a6840cb22aede"
+ },
+ {
+ "name": "flag_mx",
+ "unicode": "1F1F2-1F1FD",
+ "digest": "eafb02ec0be9cefab7cef7c426c7d860d98e4947f4da04054154dc86d8f487c4"
+ },
+ {
+ "name": "flag_my",
+ "unicode": "1F1F2-1F1FE",
+ "digest": "9a690b357bc6b970781bd122c1e546ade3ccb73d930c2af1008b82027e36c7cf"
+ },
+ {
+ "name": "flag_mz",
+ "unicode": "1F1F2-1F1FF",
+ "digest": "36d0548ebfef9e0443ec1d0597ebfa6e95c25b997381f30c8c74008820743bb9"
+ },
+ {
+ "name": "flag_na",
+ "unicode": "1F1F3-1F1E6",
+ "digest": "4989dc9452b0bdfa101cfd3b7c83ef1195a7e45128b9ed00193fe712a6d02fca"
+ },
+ {
+ "name": "flag_nc",
+ "unicode": "1F1F3-1F1E8",
+ "digest": "7fc9d865eebf729d5496c4cd7576476ec599f65b379d4a6df66b4e399553c2eb"
+ },
+ {
+ "name": "flag_ne",
+ "unicode": "1F1F3-1F1EA",
+ "digest": "d3f10fb44ec44a04112bc66d05f0a44c6ec46dae73cfd3fe26cdc8b32ec06713"
+ },
+ {
+ "name": "flag_nf",
+ "unicode": "1F1F3-1F1EB",
+ "digest": "d390e0d52215a025380af221ba9e955e5886edbb4c9f4b124f2fb60a8e019e42"
+ },
+ {
+ "name": "flag_ng",
+ "unicode": "1F1F3-1F1EC",
+ "digest": "e69d1bb8f1db4a0c295c90dda23d8f97c2dea59f9a2da2ecb0e9a1dc4dbea101"
+ },
+ {
+ "name": "flag_ni",
+ "unicode": "1F1F3-1F1EE",
+ "digest": "dbaccc942637469b0ee75bd5f956958c3c5a89d8f69b69c96f02ab6594124894"
+ },
+ {
+ "name": "flag_nl",
+ "unicode": "1F1F3-1F1F1",
+ "digest": "bda2eb0315763c3c19d37c664dab1ee4280f20888a0ca57677fd33cfa4240910"
+ },
+ {
+ "name": "flag_no",
+ "unicode": "1F1F3-1F1F4",
+ "digest": "42b49dec756a220781ea271ca8fbcaba524dc3b38d5d8f999bfaa40ef9ebd302"
+ },
+ {
+ "name": "flag_np",
+ "unicode": "1F1F3-1F1F5",
+ "digest": "b5259257db079235310d5d9537d2b5b61ae0326bc8920ba13084b009844e2957"
+ },
+ {
+ "name": "flag_nr",
+ "unicode": "1F1F3-1F1F7",
+ "digest": "1bd7d1fe2c3a5e98cfd4dff6e8d6dd6d3c74f0051ad615587d77d2291a9784cc"
+ },
+ {
+ "name": "flag_nu",
+ "unicode": "1F1F3-1F1FA",
+ "digest": "e2a7a398e07d2232147cc0917d72d18b519246d3d314e9f6f03dcf98d312d4ce"
+ },
+ {
+ "name": "flag_nz",
+ "unicode": "1F1F3-1F1FF",
+ "digest": "ce8b1cb87dae3a3ec865575b57a0b4987a7f4bd3f170e7b210dd764fc2588cd4"
+ },
+ {
+ "name": "flag_om",
+ "unicode": "1F1F4-1F1F2",
+ "digest": "29da72505a276a8a372a00c197388ebc5098c221cab26b3ff755bd62b10f740f"
+ },
+ {
+ "name": "flag_pa",
+ "unicode": "1F1F5-1F1E6",
+ "digest": "180b673c9aceea43a8b55823a82d80600257e4982d0757d129860e3d8a14f458"
+ },
+ {
+ "name": "flag_pe",
+ "unicode": "1F1F5-1F1EA",
+ "digest": "b61823ea2cd91e371e40832df5764558b81d44fac41030827a3f6d2564643c00"
+ },
+ {
+ "name": "flag_pf",
+ "unicode": "1F1F5-1F1EB",
+ "digest": "e560421911f4af90c73a0dbdf8f42e69316003799304c9394fb127e3b83326fa"
+ },
+ {
+ "name": "flag_pg",
+ "unicode": "1F1F5-1F1EC",
+ "digest": "880e87db2ce0eac38db037683a5db46fd6ce30623cf56ae4a93a747103570044"
+ },
+ {
+ "name": "flag_ph",
+ "unicode": "1F1F5-1F1ED",
+ "digest": "49aae2f56bfd1385741dc76857aa1f1459778b2d39a1c955e469c5367585bfd5"
+ },
+ {
+ "name": "flag_pk",
+ "unicode": "1F1F5-1F1F0",
+ "digest": "64379dbfc932df3a07935b5cfa11ca151f761d3728939e982604e12c663cd646"
+ },
+ {
+ "name": "flag_pl",
+ "unicode": "1F1F5-1F1F1",
+ "digest": "3b688b074c2735d3dea0b7ab74b80eba243ce50cb05d68e585c9d701c1f14617"
+ },
+ {
+ "name": "flag_pm",
+ "unicode": "1F1F5-1F1F2",
+ "digest": "a13a69ee3131501dd8138173cfb669a35ee8039d84aa665e69dd7f0d0aa3e717"
+ },
+ {
+ "name": "flag_pn",
+ "unicode": "1F1F5-1F1F3",
+ "digest": "d7ae3985cf66024e4a3001e79a8efbb3e75571f2b0abbd0fb87fc1efc795a2b3"
+ },
+ {
+ "name": "flag_pr",
+ "unicode": "1F1F5-1F1F7",
+ "digest": "4910dc984bc908158506b770f28af56150cbb4509a4291947dfa2479b9e4b308"
+ },
+ {
+ "name": "flag_ps",
+ "unicode": "1F1F5-1F1F8",
+ "digest": "b2bca7619fced25de94d7bd398537857460348a552e7d73d189aef3f428e6a13"
+ },
+ {
+ "name": "flag_pt",
+ "unicode": "1F1F5-1F1F9",
+ "digest": "177282613b4b8b4d9551f1da6a1c3f66f1b96cf67c71c7d164213b26b3237395"
+ },
+ {
+ "name": "flag_pw",
+ "unicode": "1F1F5-1F1FC",
+ "digest": "2ff42a14bdc7df76b5f989dca381f94765032b26ae47d47b97844abde458cefe"
+ },
+ {
+ "name": "flag_py",
+ "unicode": "1F1F5-1F1FE",
+ "digest": "80169b69a46c4c67d0090dc2c6bf05d1a14f133ac7ae56f811547e8e8f70d81b"
+ },
+ {
+ "name": "flag_qa",
+ "unicode": "1F1F6-1F1E6",
+ "digest": "589b44b975aa97426afb8db7f8b355491fca246b693903485824bf0f5a6953a2"
+ },
+ {
+ "name": "flag_re",
+ "unicode": "1F1F7-1F1EA",
+ "digest": "77d242261742831a142c9ec74cd17d76b1e6d1af751ff3c6a356646744bc798a"
+ },
+ {
+ "name": "flag_ro",
+ "unicode": "1F1F7-1F1F4",
+ "digest": "d7d17026ea81f27456983722540f9a23343a3a1b22e7697c4fba118ce8b4719e"
+ },
+ {
+ "name": "flag_rs",
+ "unicode": "1F1F7-1F1F8",
+ "digest": "e466a18cc0368e623d3fe33a036c1e88db91ae24f7510e17caacc85c41f1bac8"
+ },
+ {
+ "name": "flag_ru",
+ "unicode": "1F1F7-1F1FA",
+ "digest": "86bf53a62dfc4c434d910f43df70f430fc67c0070fe3fc466c4fbfd6a5d8e646"
+ },
+ {
+ "name": "flag_rw",
+ "unicode": "1F1F7-1F1FC",
+ "digest": "38ec5a01896c9747a8dbf865d5e8584770e587253b7af3d3b9c36cd993f67518"
+ },
+ {
+ "name": "flag_sa",
+ "unicode": "1F1F8-1F1E6",
+ "digest": "a44d0b145f2a0b68eace24ecfd27519e9525ec764836728bc9c1fe96ccb811a0"
+ },
+ {
+ "name": "flag_sb",
+ "unicode": "1F1F8-1F1E7",
+ "digest": "8ffa24c5cb92be4dbe43f6cd85b61b9608a3101bd78ebccff4fe99c209b3e241"
+ },
+ {
+ "name": "flag_sc",
+ "unicode": "1F1F8-1F1E8",
+ "digest": "227d090ac2cbf317e594567b6114b5063a13cfe33abf990d37b200debcfadabb"
+ },
+ {
+ "name": "flag_sd",
+ "unicode": "1F1F8-1F1E9",
+ "digest": "350f3332e8ea1138e54facc870dd0fea5f2ab7d3fd4baa02ed8627ae79642f6c"
+ },
+ {
+ "name": "flag_se",
+ "unicode": "1F1F8-1F1EA",
+ "digest": "c1b09f36c263727de83b54376f05e083a17a61941af9a1640b826629256a280d"
+ },
+ {
+ "name": "flag_sg",
+ "unicode": "1F1F8-1F1EC",
+ "digest": "e6fc26920dfc07e4fd3c8d897de9c607e0bf48a3b64a13630c858d707a8e7660"
+ },
+ {
+ "name": "flag_sh",
+ "unicode": "1F1F8-1F1ED",
+ "digest": "f2c22ab0eb49e3104c35f1c0268b1e63c3a67f41b0cfa9861b189525988e53b6"
+ },
+ {
+ "name": "flag_si",
+ "unicode": "1F1F8-1F1EE",
+ "digest": "1ef0b10e498f71591322f9d8ec122d39838f479370cf7ee922560986ef6c4f2e"
+ },
+ {
+ "name": "flag_sj",
+ "unicode": "1F1F8-1F1EF",
+ "digest": "ce913b007f84a9cba2add8d754aa791901624c60e4200de426dfa25271cb0f78"
+ },
+ {
+ "name": "flag_sk",
+ "unicode": "1F1F8-1F1F0",
+ "digest": "d8f8fc4024c82f906effe98facbef9d543fb3708b1134dc502c74dc4a442b30a"
+ },
+ {
+ "name": "flag_sl",
+ "unicode": "1F1F8-1F1F1",
+ "digest": "dd7fd0452498d8d1c894cf0d5a662ddff9c5bcc02148bdc3dc7e6f25d0bb586e"
+ },
+ {
+ "name": "flag_sm",
+ "unicode": "1F1F8-1F1F2",
+ "digest": "2b499606aee2b5cbf4037338753c80a4c8f75f4abcef2c8657bd9337e602bbd3"
+ },
+ {
+ "name": "flag_sn",
+ "unicode": "1F1F8-1F1F3",
+ "digest": "03b46a9d8b129da13f60c23b820b04fba52050ca58a41b859ad57d5c3cc2515d"
+ },
+ {
+ "name": "flag_so",
+ "unicode": "1F1F8-1F1F4",
+ "digest": "ea416b6a05ddc5b16291ebe5101735360b08c834d55ac82c663ac1dd3e459048"
+ },
+ {
+ "name": "flag_sr",
+ "unicode": "1F1F8-1F1F7",
+ "digest": "012179fbcbcb7343e7b09d33e283fb63c7964a6eca35ccb9407d468e495a9874"
+ },
+ {
+ "name": "flag_ss",
+ "unicode": "1F1F8-1F1F8",
+ "digest": "6723150482c640643c9dd7e33ea749f4a8b46aceacbd4f5e11aa33b3ee13aab7"
+ },
+ {
+ "name": "flag_st",
+ "unicode": "1F1F8-1F1F9",
+ "digest": "0947fcec2e3cb1b0e9943c3d00891e8ee226e8d0532e9b1fe807ddf2e8fbc49d"
+ },
+ {
+ "name": "flag_sv",
+ "unicode": "1F1F8-1F1FB",
+ "digest": "ce7e583db833c4b10e2f7a2d09b97bb522c02e96ea0b3f3a48a955f7d8f970d8"
+ },
+ {
+ "name": "flag_sx",
+ "unicode": "1F1F8-1F1FD",
+ "digest": "c01fb238c7ba439f24a5ef821b6457f2a0fd0b99a1b2d02395bed87f0a4a88e5"
+ },
+ {
+ "name": "flag_sy",
+ "unicode": "1F1F8-1F1FE",
+ "digest": "a77d87ef98c96140c59998d10d94837e2a056dd3ac5c7522e89e5c62eac69e69"
+ },
+ {
+ "name": "flag_sz",
+ "unicode": "1F1F8-1F1FF",
+ "digest": "2904ad01040a9107ad556ec4c2561781d96746005cca250babb1127b8ba21050"
+ },
+ {
+ "name": "flag_ta",
+ "unicode": "1F1F9-1F1E6",
+ "digest": "eda84db90e1a8854e8ff3c15b3b38ee65f7d6532b76970a6fbac304c30d8c959"
+ },
+ {
+ "name": "flag_tc",
+ "unicode": "1F1F9-1F1E8",
+ "digest": "4628fdf6dc598a2846beefe97f7d4c6812f4961394cec132924b44bbe79b3322"
+ },
+ {
+ "name": "flag_td",
+ "unicode": "1F1F9-1F1E9",
+ "digest": "125ff31e4285cb2a5493a52a2703ebe8e7138b918ec4dae3d0f8693632372df6"
+ },
+ {
+ "name": "flag_tf",
+ "unicode": "1F1F9-1F1EB",
+ "digest": "489d591e11764ac341f2234020f7879db782b8f673fc9aae425fd713e4082334"
+ },
+ {
+ "name": "flag_tg",
+ "unicode": "1F1F9-1F1EC",
+ "digest": "4ceedfcfcc22cd14d9add9d86d6748447995f19f7095fa4be883e21eb1aa86bc"
+ },
+ {
+ "name": "flag_th",
+ "unicode": "1F1F9-1F1ED",
+ "digest": "2798cc660af1c5dc4891c30aded3a53d7cfa0af128cc495df8141907b165902d"
+ },
+ {
+ "name": "flag_tj",
+ "unicode": "1F1F9-1F1EF",
+ "digest": "0483506fc5b5f2d4fc18ea3cd2f8a5da985d68fe4bf90bd3fd05e67e38f32398"
+ },
+ {
+ "name": "flag_tk",
+ "unicode": "1F1F9-1F1F0",
+ "digest": "d5d4a8c6ce3207731b7c154a9d8d8fa2af055a48f03b3cbbcfd3317d3b8a75f2"
+ },
+ {
+ "name": "flag_tl",
+ "unicode": "1F1F9-1F1F1",
+ "digest": "7a2ba8f91a6b627c60c88244223a9b9d0c12707f50b174f9c2eca07dd3440df7"
+ },
+ {
+ "name": "flag_tm",
+ "unicode": "1F1F9-1F1F2",
+ "digest": "adcf5f23adcf983ce626b44559482f8728251eab34b3ff5d8b125112f3a1010f"
+ },
+ {
+ "name": "flag_tn",
+ "unicode": "1F1F9-1F1F3",
+ "digest": "5ee690ee1f3c3c0cba9b36efdef902894ec59cefbc60c4baa341efd3d7bb9ba2"
+ },
+ {
+ "name": "flag_to",
+ "unicode": "1F1F9-1F1F4",
+ "digest": "cde8672ca25b0e3a423865283fab9bc3ab10f472e04979b3b2f8032b71e96300"
+ },
+ {
+ "name": "flag_tr",
+ "unicode": "1F1F9-1F1F7",
+ "digest": "3d83c03ed084cfc81fa633310382acd7213e1eaa19d0ed97d142e7824032b55d"
+ },
+ {
+ "name": "flag_tt",
+ "unicode": "1F1F9-1F1F9",
+ "digest": "d66d272ac27e2b398289d6b60128ccd3508aeb1f4a00a3920c5e6a21bfe357ed"
+ },
+ {
+ "name": "flag_tv",
+ "unicode": "1F1F9-1F1FB",
+ "digest": "8716527383854cf1569f737d0f0f9ad77b46747255f24e02f5b2fbc850c2e35c"
+ },
+ {
+ "name": "flag_tw",
+ "unicode": "1F1F9-1F1FC",
+ "digest": "fb17b97e18e4423c5f60d60ec3ec60b917be579fc4dd9b5b23236786dcb35108"
+ },
+ {
+ "name": "flag_tz",
+ "unicode": "1F1F9-1F1FF",
+ "digest": "a8a8cf57ae5227cb54620bf31d2d6e154d2067d6d049b8db64bc4e538222948b"
+ },
+ {
+ "name": "flag_ua",
+ "unicode": "1F1FA-1F1E6",
+ "digest": "03aca4b3ffd60d944a5793eb7530f8d8ae527782f642f6606194e46ee314b12c"
+ },
+ {
+ "name": "flag_ug",
+ "unicode": "1F1FA-1F1EC",
+ "digest": "70226a1585e88390b3b815b8b79a0ddb36d2961c6b465c4ff72aa444abfe982e"
+ },
+ {
+ "name": "flag_um",
+ "unicode": "1F1FA-1F1F2",
+ "digest": "aa83bf051149acf907140a860de5de1700710e4164ae5549ad1040b24d0a142b"
+ },
+ {
+ "name": "flag_us",
+ "unicode": "1F1FA-1F1F8",
+ "digest": "32ba2aa09a30514247e91d60762791b582f547a37d9151f98b700dff50f355ea"
+ },
+ {
+ "name": "flag_uy",
+ "unicode": "1F1FA-1F1FE",
+ "digest": "0e01b3f1df4bdf6d616dacc9c5825151b941bf074be750e8b24a07ea5d5bcacb"
+ },
+ {
+ "name": "flag_uz",
+ "unicode": "1F1FA-1F1FF",
+ "digest": "903029ce83812a2134f24b65db35b183443a440ea5fecaa6ef7dcaaf65b2519c"
+ },
+ {
+ "name": "flag_va",
+ "unicode": "1F1FB-1F1E6",
+ "digest": "fd3c1c5d0ac030e838f807288912c98a3e258f87901e252e46942a4dab9f8cb7"
+ },
+ {
+ "name": "flag_vc",
+ "unicode": "1F1FB-1F1E8",
+ "digest": "7cd554ea8ca817b5366701160274587ab44167ae5a89c430bbaf237ea18b7421"
+ },
+ {
+ "name": "flag_ve",
+ "unicode": "1F1FB-1F1EA",
+ "digest": "72930094fb088c1facabea07616035ec4771374358a90c3045219d087b350dd8"
+ },
+ {
+ "name": "flag_vg",
+ "unicode": "1F1FB-1F1EC",
+ "digest": "78a59afd368b7a8312bfdb2f49927ff09e6b8f46aab0136c0453e3319e81df49"
+ },
+ {
+ "name": "flag_vi",
+ "unicode": "1F1FB-1F1EE",
+ "digest": "e070879f9605a9bae66bb84f2abf5a40c8b264baee65cd4f7a6720b826739f29"
+ },
+ {
+ "name": "flag_vn",
+ "unicode": "1F1FB-1F1F3",
+ "digest": "100ddf06e0f239b170f4d6cb459450bf4945281ee818f7d3c061828b80562219"
+ },
+ {
+ "name": "flag_vu",
+ "unicode": "1F1FB-1F1FA",
+ "digest": "59fc9d16818295bba4f7f551598f85378cd07f2bd7e31a4eef2589aaa3847563"
+ },
+ {
+ "name": "flag_wf",
+ "unicode": "1F1FC-1F1EB",
+ "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee"
+ },
+ {
+ "name": "flag_white",
+ "unicode": "1F3F3",
+ "digest": "96307e3a28e92d1e7147a06f154ffc291ee3cd1765cf8b7bfb06412294112559"
+ },
+ {
+ "name": "flag_ws",
+ "unicode": "1F1FC-1F1F8",
+ "digest": "0c95271d0f4b23f0d215ee0fba05cf08ecb70665d4c028e17463ecda2754b164"
+ },
+ {
+ "name": "flag_xk",
+ "unicode": "1F1FD-1F1F0",
+ "digest": "713aa7d228e96f4a06d58d1fb8c2a55296c3e56842f8177ca936f3e09f50da1e"
+ },
+ {
+ "name": "flag_ye",
+ "unicode": "1F1FE-1F1EA",
+ "digest": "3bb65bae9c913357bcae8b8b5878efc9e194ca308442ab69639c29716b49f078"
+ },
+ {
+ "name": "flag_yt",
+ "unicode": "1F1FE-1F1F9",
+ "digest": "f86c86f4c194610a3af78971fcf221ad97b9499d08f6d64476e417a2f52a611e"
+ },
+ {
+ "name": "flag_za",
+ "unicode": "1F1FF-1F1E6",
+ "digest": "4dd4fa49a01fdcfc7c1c099a7869e0e9acba83a6a3debf6c8505ada4c796b872"
+ },
+ {
+ "name": "flag_zm",
+ "unicode": "1F1FF-1F1F2",
+ "digest": "ab6790d89875447de3d1c7f4713b102761bc3e9afdd714b818689e175ca03011"
+ },
+ {
+ "name": "flag_zw",
+ "unicode": "1F1FF-1F1FC",
+ "digest": "9d39b934fe922174b2250f2cd1b174a548d2904091d3298f35b7cc59fbceb181"
+ },
+ {
+ "name": "flags",
+ "unicode": "1F38F",
+ "digest": "c3f4a66786e524a5562919afcba9486113091ed205f1342e91d2f6439845ad61"
+ },
+ {
+ "name": "flashlight",
+ "unicode": "1F526",
+ "digest": "5f641b8fd1c7f1dcd43ec3b1ef78d14ef9929d723789c5567aca8b95d3d39803"
+ },
+ {
+ "name": "fleur-de-lis",
+ "unicode": "269C",
+ "digest": "d6ddeeea355ed55103b7fc65ac1ee0dbaa79d01e0d136b265363a6b92284c073"
+ },
+ {
+ "name": "flip_phone",
+ "unicode": "1F581",
+ "digest": "be59efba4bc0759af5a726c06619090ef5071bf2541611d71691dedecee6c697"
+ },
+ {
+ "name": "floppy_black",
+ "unicode": "1F5AA",
+ "digest": "9022f51bb09c5130c6d46bb2accb159bed6f54d6fbffda6ecad62965ebc958ea"
+ },
+ {
+ "name": "floppy_disk",
+ "unicode": "1F4BE",
+ "digest": "e987961ca516032a90942ef6c398836f2da68a5981714bd172acfe7b0e369d0a"
+ },
+ {
+ "name": "floppy_white",
+ "unicode": "1F5AB",
+ "digest": "ec79c400117c4506ef8cf3eebef6c42dd37e60b3079d3e98b6ccd06e517e2af0"
+ },
+ {
+ "name": "flower_playing_cards",
+ "unicode": "1F3B4",
+ "digest": "451f361050b96ba9ed8dc5b64c8a90c1316fd9b83fb818152881a54e100eea6c"
+ },
+ {
+ "name": "flushed",
+ "unicode": "1F633",
+ "digest": "39cf51f9dec2a910c66ecd39a7bd616fea09d67e81801e57e84f03ed1e917750"
+ },
+ {
+ "name": "fog",
+ "unicode": "1F32B",
+ "digest": "da6fdb9b682ed9a3368adcd7531f1a29e22755a620e3cca163fc3f33a6a78107"
+ },
+ {
+ "name": "foggy",
+ "unicode": "1F301",
+ "digest": "b599f3178db289c6e30017f3f0a9d30b00a75417057c7a10c0c9eedac78edbf1"
+ },
+ {
+ "name": "folder",
+ "unicode": "1F5C0",
+ "digest": "8932141321911032ce8469ba85fe309b78384545c3b9946978b383670b956644"
+ },
+ {
+ "name": "folder_open",
+ "unicode": "1F5C1",
+ "digest": "74f3b484771c3d6ef61cf003de25c1a59b875afa46c057b5b1d92d9f99460685"
+ },
+ {
+ "name": "football",
+ "unicode": "1F3C8",
+ "digest": "834fe5f431d6aa8ef1186aa79e71f813393535d273483b6af4cc4bdb8380e5b4"
+ },
+ {
+ "name": "footprints",
+ "unicode": "1F463",
+ "digest": "60dc938f6769ea21b05b5afcc481d3ddacf1f565e04f33310b271d5422e7ceb9"
+ },
+ {
+ "name": "fork_and_knife",
+ "unicode": "1F374",
+ "digest": "7e07c9dc555d172fa2eaa41cefd8d46d9624be0137aff196dd003a8a82610ec3"
+ },
+ {
+ "name": "fork_knife_plate",
+ "unicode": "1F37D",
+ "digest": "b4081b9edea6cdab5112fdd17535051ba17710953013f5020c7c40f84a1e3247"
+ },
+ {
+ "name": "fountain",
+ "unicode": "26F2",
+ "digest": "0acdca5e8f6d745a8d582d96012ec8fc55b9f5447e657ebfd998a4e332d99322"
+ },
+ {
+ "name": "four",
+ "unicode": "0034-20E3",
+ "digest": "36bd4ea6e2ae689835a79f8e60466eccd62fce7e91e84ed768cffd87dac628dd"
+ },
+ {
+ "name": "four_leaf_clover",
+ "unicode": "1F340",
+ "digest": "12ee2343df25bbd9077fdc12314c1edb51c0cdb556af7e22590e8a578ef57f17"
+ },
+ {
+ "name": "frame_photo",
+ "unicode": "1F5BC",
+ "digest": "6ff21063063989c6ae7dd69f4d6a781c676f9dba380d8e6f1dbac5d53b24f349"
+ },
+ {
+ "name": "frame_tiles",
+ "unicode": "1F5BD",
+ "digest": "34a5bb044b4b3ad94b116ad106f7b6747fb8612dc0e9f8ccd4313c2920508df0"
+ },
+ {
+ "name": "frame_x",
+ "unicode": "1F5BE",
+ "digest": "2e427688fd70361c8c59787d0722ad68abe1c3f968258ee99c0c77ce4b8a8e15"
+ },
+ {
+ "name": "free",
+ "unicode": "1F193",
+ "digest": "c1d9172a656717f78d941303c5da8790c6cd9827838d8f7dc3719afb53bcab80"
+ },
+ {
+ "name": "fried_shrimp",
+ "unicode": "1F364",
+ "digest": "c0c19e95f2c38f6cf870920bf3c2d4d69c36ea6e7dc9a5c45c3e8b285269d40a"
+ },
+ {
+ "name": "fries",
+ "unicode": "1F35F",
+ "digest": "0f546534684de29d319cbcbab4162acb321c4f8f3202fe17d69e1894ab7c8195"
+ },
+ {
+ "name": "frog",
+ "unicode": "1F438",
+ "digest": "6a417757fa6ee39e7a277cbd53c690ff88af0b1d76728d56f9bc645cb628aeb7"
+ },
+ {
+ "name": "frowning",
+ "unicode": "1F626",
+ "digest": "fb39f5c2aea98054adb02a3a0ac34a2e38d83f32cd590e9d2449e06a9702f2f5"
+ },
+ {
+ "name": "frowning2",
+ "unicode": "2639",
+ "digest": "7bb6c682a6c9f98bf3a5ae986e317fd26d1af497c857500deec2f06b6a3af5da"
+ },
+ {
+ "name": "fuelpump",
+ "unicode": "26FD",
+ "digest": "9cbb2646c93b255bd3de87dc01aa1193ab96e39a3013975d250472ab8aae61d6"
+ },
+ {
+ "name": "full_moon",
+ "unicode": "1F315",
+ "digest": "0b4f08ef2089397ead034b444a60e6e9810073454581b52a46b2369e3b9cd5f9"
+ },
+ {
+ "name": "full_moon_with_face",
+ "unicode": "1F31D",
+ "digest": "a371cb9e1f28a7db739dd058234642a2e333dff4b6df9882df85a6d984e4b5e8"
+ },
+ {
+ "name": "game_die",
+ "unicode": "1F3B2",
+ "digest": "6584909a4348c350c04417421b63eace1245087f7d239051b30a0cd37fe929f9"
+ },
+ {
+ "name": "gear",
+ "unicode": "2699",
+ "digest": "b0ff5fd007daa366a9eecb7422dbeb8a973e123a04267b88fef96c7453238294"
+ },
+ {
+ "name": "gem",
+ "unicode": "1F48E",
+ "digest": "d75d854f35975e4e291c3b9fcaf8437467f6d7eb27b29e2d7c0f0038fc666fe2"
+ },
+ {
+ "name": "gemini",
+ "unicode": "264A",
+ "digest": "392abe62872736a0bf92979a8c25a814985d0ff0a08dc7ab2a5c058aeda7e685"
+ },
+ {
+ "name": "ghost",
+ "unicode": "1F47B",
+ "digest": "f084b14483476e2d07563840f8c33b46da9c17f791da07fde3acffeb77342947"
+ },
+ {
+ "name": "gift",
+ "unicode": "1F381",
+ "digest": "c9a2ae6ea05c02e78e9567dcbd971701a2f869eb46c62d85cef23d0834388d8c"
+ },
+ {
+ "name": "gift_heart",
+ "unicode": "1F49D",
+ "digest": "e0c5aacf1ce89117d86b148f10a02dc18fe0cd22a75fbf6f0f88f2fad3ca80fe"
+ },
+ {
+ "name": "girl",
+ "unicode": "1F467",
+ "digest": "0758cbc4cbc7d72d6df8f66fc3a6b2b283c6634b053e59d61c6cac44cf8bffda"
+ },
+ {
+ "name": "girl_tone1",
+ "unicode": "1F467-1F3FB",
+ "digest": "7afdece55cb64e8056e2202de8c17b66ddb616f224ac374ec9a160d06b3138cc"
+ },
+ {
+ "name": "girl_tone2",
+ "unicode": "1F467-1F3FC",
+ "digest": "c160aa65fee70ad52930d01246ac9f282ff6abf1d93c5cc5b299fc257ee81db1"
+ },
+ {
+ "name": "girl_tone3",
+ "unicode": "1F467-1F3FD",
+ "digest": "b8a5687cd637855a41b8c7dc686f0e69fda379875408cd269f1b330a805c72f4"
+ },
+ {
+ "name": "girl_tone4",
+ "unicode": "1F467-1F3FE",
+ "digest": "a9cf743936b733634f323790a1abe3a410601b6841484baebea484b392f4e98e"
+ },
+ {
+ "name": "girl_tone5",
+ "unicode": "1F467-1F3FF",
+ "digest": "c902170e67b81eee35eeefb6a5c62c6109cb423dcae88d4e036ddd50b240c072"
+ },
+ {
+ "name": "girls_symbol",
+ "unicode": "1F6CA",
+ "digest": "2c55aee81defd7a1620ffeaad8d9bcc1835f19237c72c79633aec45671ddb9ff"
+ },
+ {
+ "name": "globe_with_meridians",
+ "unicode": "1F310",
+ "digest": "945646de3d8f057760fe374494a253d9a6aa8a132309154b0a5bdbffb5b20c3f"
+ },
+ {
+ "name": "goat",
+ "unicode": "1F410",
+ "digest": "f99cbc6755d119cb5c1dce08cabd20871f98d009bb773da4a146dae60476a235"
+ },
+ {
+ "name": "golf",
+ "unicode": "26F3",
+ "digest": "74a7876d185f8ff6a6533e4db2e1eb787119b2f8d8b07c36d99ec3163fb48485"
+ },
+ {
+ "name": "golfer",
+ "unicode": "1F3CC",
+ "digest": "6458295a5e4a6e4323c32a7f1f7182fb2d3918083839efc380d995860ce360b1"
+ },
+ {
+ "name": "grapes",
+ "unicode": "1F347",
+ "digest": "7f6873d65180ab476f49d207ac2d1f7dbaf6c8b0b561d50b64325e192cf97a86"
+ },
+ {
+ "name": "green_apple",
+ "unicode": "1F34F",
+ "digest": "effc3fe60f2ab704a034c794bfccfa023b41332f8f16ca44cc8ea41698f03873"
+ },
+ {
+ "name": "green_book",
+ "unicode": "1F4D7",
+ "digest": "6652c4d2ccfa4a287a5d45007bd06cadc16d34b0a1ca4b6b13b46f976c8d8319"
+ },
+ {
+ "name": "green_heart",
+ "unicode": "1F49A",
+ "digest": "f4bcb660a1d3cf3692238359d8b9de9a725a9af81f166253e487d61b8ccf9d86"
+ },
+ {
+ "name": "grey_exclamation",
+ "unicode": "2755",
+ "digest": "ac8cdab7496d133e7bc9475f2fdb0cf59b3ccba20f2f156c8b693e72b5948078"
+ },
+ {
+ "name": "grey_question",
+ "unicode": "2754",
+ "digest": "c173e1b2a16ab62b0abd7a58deb7a6df709b072d30d001627b92d0123a3a3e4a"
+ },
+ {
+ "name": "grimacing",
+ "unicode": "1F62C",
+ "digest": "8c54b73f5d2c1c6347e2c0ab01616519e0fb34490daa9c36664d442c6851c57e"
+ },
+ {
+ "name": "grin",
+ "unicode": "1F601",
+ "digest": "916eabdabd8b7ca698e638bbbd14affff97464ec11a3b59c0cb96cd7705600d8"
+ },
+ {
+ "name": "grinning",
+ "unicode": "1F600",
+ "digest": "3d8665c03f272ca3063e96145989926355a7ac315ed1a032d30fcefa6f0c3923"
+ },
+ {
+ "name": "guardsman",
+ "unicode": "1F482",
+ "digest": "ebbd29fa138005232d64fca4a8ec015d097fa14e6ded57b35ac257b4570b3c36"
+ },
+ {
+ "name": "guardsman_tone1",
+ "unicode": "1F482-1F3FB",
+ "digest": "b6082c8fee5dbc3ce2540f3939d5e344b5366c9f07827345facaba438e7017ff"
+ },
+ {
+ "name": "guardsman_tone2",
+ "unicode": "1F482-1F3FC",
+ "digest": "2b813afe1c2bbdaf9a47493393a0e6c400a16e453ed25a9a9c0035197927b56e"
+ },
+ {
+ "name": "guardsman_tone3",
+ "unicode": "1F482-1F3FD",
+ "digest": "49b2fa1ad0bc50a5ef6d73fb140aa1876506b9ebb9d45782ccb8dbb6818f8dde"
+ },
+ {
+ "name": "guardsman_tone4",
+ "unicode": "1F482-1F3FE",
+ "digest": "a584e1e3a8ad7be4871a6bdb7996d4f649abeaa77eb5d1cae998058d8b23ca0f"
+ },
+ {
+ "name": "guardsman_tone5",
+ "unicode": "1F482-1F3FF",
+ "digest": "e853b67ee13fda99e98f47083529ca80c404df1b19352c78b9c69850eb8f2c76"
+ },
+ {
+ "name": "guitar",
+ "unicode": "1F3B8",
+ "digest": "8c041b961649cc5917f56f2fb543f9a5280724647ed2fc67bc94a05eff9da805"
+ },
+ {
+ "name": "gun",
+ "unicode": "1F52B",
+ "digest": "d7f5aa657cc0ba04d878511820632b89c305a9b4d6c4a4b90ff691dad9906607"
+ },
+ {
+ "name": "haircut",
+ "unicode": "1F487",
+ "digest": "369dbab1b138c31d3eca04c950fdab4ec9f085272268c241f100d44e7b0f229e"
+ },
+ {
+ "name": "haircut_tone1",
+ "unicode": "1F487-1F3FB",
+ "digest": "c56f32d7c1d8a92d22429133f87f31a159818939cfdc570cb48b6d243cc58cf2"
+ },
+ {
+ "name": "haircut_tone2",
+ "unicode": "1F487-1F3FC",
+ "digest": "e916e040ffb8e869e930d1256343af2ad2bbaa683f01a11564d0777019944bec"
+ },
+ {
+ "name": "haircut_tone3",
+ "unicode": "1F487-1F3FD",
+ "digest": "f07cdfbea964ac42a9a050f832107ef0f2fa8115b27689f93d1be954de07b7c1"
+ },
+ {
+ "name": "haircut_tone4",
+ "unicode": "1F487-1F3FE",
+ "digest": "32ec7f5e999f7c43676768c8320ffaa346c713d340a94b948b1f564b345a2d11"
+ },
+ {
+ "name": "haircut_tone5",
+ "unicode": "1F487-1F3FF",
+ "digest": "5aad997d09e7975700927906d41a10bae774356ccddbe5197980bde670272262"
+ },
+ {
+ "name": "hamburger",
+ "unicode": "1F354",
+ "digest": "24ebae9a69cf283ab198499cb38d0cdcd82bac74c8e8d1e769ad78eb320a4294"
+ },
+ {
+ "name": "hammer",
+ "unicode": "1F528",
+ "digest": "a43a66b0efdc4cd2c84fd0ccc2cb8e9ede1f89c5d62eefa6ae521d3aed9d81b3"
+ },
+ {
+ "name": "hammer_pick",
+ "unicode": "2692",
+ "digest": "2e4fe33406ca03fbb0df1596d63e903d8ee6bd78ecc3ec38a67dd2cecbc584e2"
+ },
+ {
+ "name": "hamster",
+ "unicode": "1F439",
+ "digest": "f47da088ff5792532a382b6e3a47d2dd7c5e6fc19abd5ff6c5ba3ce420b4192e"
+ },
+ {
+ "name": "hand_splayed",
+ "unicode": "1F590",
+ "digest": "a43e52f7cdec5e9d51497888b0988d7bbd42846ad7e492b196293fbce576d197"
+ },
+ {
+ "name": "hand_splayed_reverse",
+ "unicode": "1F591",
+ "digest": "ff0af0fe9def7388adca6836e5958492282b1afae99f1b6e1e65d11ba68b96db"
+ },
+ {
+ "name": "hand_splayed_tone1",
+ "unicode": "1F590-1F3FB",
+ "digest": "73cceec7117280d330f8a149979190f0f355dd8d0a92821be89fb70344bb8dfe"
+ },
+ {
+ "name": "hand_splayed_tone2",
+ "unicode": "1F590-1F3FC",
+ "digest": "b06fac698128f4c3a7b8ea56e8bc4de088bb5461aa0f9c84553f16b43d347145"
+ },
+ {
+ "name": "hand_splayed_tone3",
+ "unicode": "1F590-1F3FD",
+ "digest": "a94ee9a2f8cdec6d2f7dd6887d1c7b8e064fcad63030c2c7c001742d72b5603e"
+ },
+ {
+ "name": "hand_splayed_tone4",
+ "unicode": "1F590-1F3FE",
+ "digest": "501792b4126c6f32e755accee0fc8b4d1915e1d36c4ceaa40f3bd0066efe76c3"
+ },
+ {
+ "name": "hand_splayed_tone5",
+ "unicode": "1F590-1F3FF",
+ "digest": "22ed533d587cf44f286e2d6ad77be20b4b5f133c422af4ca51e9af86a75002d8"
+ },
+ {
+ "name": "hand_victory",
+ "unicode": "1F594",
+ "digest": "2d512ced4e8a438f2a346aed67310d3080f9828c748ade1be95943c32ba1c735"
+ },
+ {
+ "name": "handbag",
+ "unicode": "1F45C",
+ "digest": "f1e2822c67f659b52c76821dd9db001332215a8566fc1846c89b6019c9758038"
+ },
+ {
+ "name": "hard_disk",
+ "unicode": "1F5B4",
+ "digest": "df8549d4281f5ae70fb6792a02c078e651764b0276aa43b7407236bd38fc21b4"
+ },
+ {
+ "name": "hash",
+ "unicode": "0023-20E3",
+ "digest": "5bd5c7180485fa71accdec5378bdc196ce0602f594f91e4eadc1e7514d5d0f90"
+ },
+ {
+ "name": "hatched_chick",
+ "unicode": "1F425",
+ "digest": "7995c3eb503a8b9662694eba80a9b551216473a31928091e35cd6ebc21cee083"
+ },
+ {
+ "name": "hatching_chick",
+ "unicode": "1F423",
+ "digest": "22905b42fa65dbc9aad8940d2db13691cacc62014f54e0960978ee0002178e1b"
+ },
+ {
+ "name": "head_bandage",
+ "unicode": "1F915",
+ "digest": "d690b740ff4f58e89dfc764c6411a4e84cfedffd7694eb5efa839a642dbabd08"
+ },
+ {
+ "name": "headphones",
+ "unicode": "1F3A7",
+ "digest": "219da138032c01c97a94f02b211049418191a3beb3d159804b9033f5916fd3c8"
+ },
+ {
+ "name": "hear_no_evil",
+ "unicode": "1F649",
+ "digest": "8120060238eaca645809dd113862a144f10395afcb3837ab60c0f04009b49a2f"
+ },
+ {
+ "name": "heart",
+ "unicode": "2764",
+ "digest": "a646a25a36f431cadc7e56afd1a4d1b7cbae5292a25d7783bd31462d0d3d719b"
+ },
+ {
+ "name": "heart_decoration",
+ "unicode": "1F49F",
+ "digest": "a83989669347c98cb74065d4f0befedbc37f82c91214e773245cb6810ab359b4"
+ },
+ {
+ "name": "heart_exclamation",
+ "unicode": "2763",
+ "digest": "9751c89dcf10805f2011949ff3ddcb6bcb13de8c32ae5de9e03955e8a4235df2"
+ },
+ {
+ "name": "heart_eyes",
+ "unicode": "1F60D",
+ "digest": "335ea73efca4824e623a5a51ccdb494c8b1f5f10b4139b39b250a2a771876b0d"
+ },
+ {
+ "name": "heart_eyes_cat",
+ "unicode": "1F63B",
+ "digest": "9346b85afb80f7b498cc255426ea15a287f81d8fb3c26dab61337635f439d3ce"
+ },
+ {
+ "name": "heart_tip",
+ "unicode": "1F394",
+ "digest": "2178829e2c85accda55d2f685544587f6de5c8398a127ae1e08ff1c4ab282204"
+ },
+ {
+ "name": "heartbeat",
+ "unicode": "1F493",
+ "digest": "cd6921ce55c155873220a09416d695c4bcca1556007066d6d185e93d6561e825"
+ },
+ {
+ "name": "heartpulse",
+ "unicode": "1F497",
+ "digest": "f869357b9e678d9671ec38c569fc88efec48006c159b69297277cee795dc4dc9"
+ },
+ {
+ "name": "hearts",
+ "unicode": "2665",
+ "digest": "17dc9b2941561f58ca0f04d0754b1eff3490b63b17241580b3d4aa4638fa85e8"
+ },
+ {
+ "name": "heavy_check_mark",
+ "unicode": "2714",
+ "digest": "b5fa24f6e0f1dcbd6278e9125154522f2efd79e6dd0836ccb792a1f3aeeff2b2"
+ },
+ {
+ "name": "heavy_division_sign",
+ "unicode": "2797",
+ "digest": "59a6983d788f347c64eecb3df6f7d3b36779d92df6cc811820993ff9e18d77e1"
+ },
+ {
+ "name": "heavy_dollar_sign",
+ "unicode": "1F4B2",
+ "digest": "d2e89c54b3fdeda4d1fd4d29454b69dcf750181110894e6e71a40df99c95bfe8"
+ },
+ {
+ "name": "heavy_minus_sign",
+ "unicode": "2796",
+ "digest": "dd5ab3722fe49cfdbc5e1fbab5b342dc960de7b412d4fba59d66e06ce3dc3bcd"
+ },
+ {
+ "name": "heavy_multiplication_x",
+ "unicode": "2716",
+ "digest": "7d77742f91377785675802f40bd8dde9bd1feeb513735760a58ea9bee8a65d44"
+ },
+ {
+ "name": "heavy_plus_sign",
+ "unicode": "2795",
+ "digest": "9aa9dcdbba120a4b485c21f67589609b789c6e3edf08479ff8268fa0db973ad7"
+ },
+ {
+ "name": "helicopter",
+ "unicode": "1F681",
+ "digest": "b259ea8d2bdca36766075894da650b1d3ff4c8602259cd0d30cb8214cd585340"
+ },
+ {
+ "name": "helmet_with_cross",
+ "unicode": "26D1",
+ "digest": "affbe9dd87b87ff9235b4858c59c2a73e9ed30dd5221e5b666b8d7747378a9c4"
+ },
+ {
+ "name": "herb",
+ "unicode": "1F33F",
+ "digest": "3c452106b1966f643751bf161fa7d1762a33e6fff381b2109bb53b55c4fdd129"
+ },
+ {
+ "name": "hibiscus",
+ "unicode": "1F33A",
+ "digest": "268963a1f3cdad9050d9ae31c558e010f33812e3b09bbf9088ba876c033d8b2f"
+ },
+ {
+ "name": "high_brightness",
+ "unicode": "1F506",
+ "digest": "d607f6269d95dd16c2a7932e49ac09e44f4c19e0a34f6c0f21ecb945a2316361"
+ },
+ {
+ "name": "high_heel",
+ "unicode": "1F460",
+ "digest": "5c320d5954bf4f4dacacddd562c1598ab101731077a6656ac5d2bfd41405483e"
+ },
+ {
+ "name": "hockey",
+ "unicode": "1F3D2",
+ "digest": "008904c1b8db139215492a6d96c09f2c3eeda769f858a9bbae13f8c54d439d0e"
+ },
+ {
+ "name": "hole",
+ "unicode": "1F573",
+ "digest": "36bbafa5e89b1410ec74919aaf60b09ac3525a421cb5b475b9bb2f20357db8de"
+ },
+ {
+ "name": "homes",
+ "unicode": "1F3D8",
+ "digest": "9980d6dd6cbd23b820747ecac4cb10974dd24b0c94b4acfe21fa87793ad065c9"
+ },
+ {
+ "name": "honey_pot",
+ "unicode": "1F36F",
+ "digest": "94cb1624491076b5cb145e7a309f91a7be3d4c0bed712af6a51d641eb73edee7"
+ },
+ {
+ "name": "horse",
+ "unicode": "1F434",
+ "digest": "624ad9dc9ed7af3f6e1a2f9d4ed483702ae64ed5fbcf5e9918af6bfef24e76f9"
+ },
+ {
+ "name": "horse_racing",
+ "unicode": "1F3C7",
+ "digest": "c2702b7225e9839a789dda7c43f0cc86dced2b4d5d3787116106396633362de6"
+ },
+ {
+ "name": "horse_racing_tone1",
+ "unicode": "1F3C7-1F3FB",
+ "digest": "a7ed284f9d5cd8a4fe4a09cb91c3f99e5db99c7e31c5f525c14de97b06857d92"
+ },
+ {
+ "name": "horse_racing_tone2",
+ "unicode": "1F3C7-1F3FC",
+ "digest": "20b4d61b21ee6ba860b029f0ad0e38f5ecb6dd2c774f7b7801fba07ed33f96be"
+ },
+ {
+ "name": "horse_racing_tone3",
+ "unicode": "1F3C7-1F3FD",
+ "digest": "dd65f7bb96ee44507d26e524202d567d2d7679d571245299a2a84f68bd5def4c"
+ },
+ {
+ "name": "horse_racing_tone4",
+ "unicode": "1F3C7-1F3FE",
+ "digest": "36afaad218a4c820b19c7c9bbbc187119d47b41273d8f48ab14cc3e32dd7c21f"
+ },
+ {
+ "name": "horse_racing_tone5",
+ "unicode": "1F3C7-1F3FF",
+ "digest": "2e0efd501a4471428533ce7909972a49ff045369261c27e4abb97ee2aede2f47"
+ },
+ {
+ "name": "hospital",
+ "unicode": "1F3E5",
+ "digest": "df5c774fa36b2601e6960a7b81cdfac71c1d2d71f04dea88068d1c9043e313bb"
+ },
+ {
+ "name": "hot_pepper",
+ "unicode": "1F336",
+ "digest": "62e4dade3c793f6d83530bd1f60f3e3e26c1e10a41786c3a15f5aec0ff2b8e76"
+ },
+ {
+ "name": "hotdog",
+ "unicode": "1F32D",
+ "digest": "58b829e26b5c4642942898d9c7873cb08e048fd7deaacba8292899d5d895cb2b"
+ },
+ {
+ "name": "hotel",
+ "unicode": "1F3E8",
+ "digest": "428120a35b38a217901e10d704751eb8fdbc9f805e6eccd8aab070f4311b2085"
+ },
+ {
+ "name": "hotsprings",
+ "unicode": "2668",
+ "digest": "df4f946218445f97a6f28c6abe4c1d1dac56ff97a8cd81df59f1b3c320e0092f"
+ },
+ {
+ "name": "hourglass",
+ "unicode": "231B",
+ "digest": "07aece9413e6898717b4f0757e073d7a593f3e8044c56855127033b796207ccb"
+ },
+ {
+ "name": "hourglass_flowing_sand",
+ "unicode": "23F3",
+ "digest": "92dbc68e9d16fb9f706236367e1882f0d2b6817b83ca490820a000021f2c6483"
+ },
+ {
+ "name": "house",
+ "unicode": "1F3E0",
+ "digest": "a6221fc84a9b0e11ae71bfa1e0020982b55ff8c89a374a6d755dba710b4e058c"
+ },
+ {
+ "name": "house_abandoned",
+ "unicode": "1F3DA",
+ "digest": "e404631e3a296bdeae3de7510da8934c32327bc0fa0f7ae4e676b61932165668"
+ },
+ {
+ "name": "house_with_garden",
+ "unicode": "1F3E1",
+ "digest": "22d0d911da96b7ae3bf6692d3cf3590afbca959fc99c13e7a088f7194f43a35d"
+ },
+ {
+ "name": "hugging",
+ "unicode": "1F917",
+ "digest": "68ed6c4e0eae9071cf67770a39e07a2290b4f7763170f765b3cd3ac67ae43240"
+ },
+ {
+ "name": "hushed",
+ "unicode": "1F62F",
+ "digest": "69faa8e0b170ee8cf41977ca4a5154406360ed9699d5c62ecdaa01f50e8e4276"
+ },
+ {
+ "name": "ice_cream",
+ "unicode": "1F368",
+ "digest": "d48ec98a8789148b96c30f19595201a0f85ed899659d97d1d3596091162909ff"
+ },
+ {
+ "name": "ice_skate",
+ "unicode": "26F8",
+ "digest": "6fb044d9fbe62605f6728062c35c345ddd3ae4cc51203c925b0e69f1b3ef2dbf"
+ },
+ {
+ "name": "icecream",
+ "unicode": "1F366",
+ "digest": "abd5774157575dd304dc1a393244757853972c863861a654ca29b2d528e48b28"
+ },
+ {
+ "name": "id",
+ "unicode": "1F194",
+ "digest": "860ffb36d37d84e2c1cf0ab991b95c1cf73e458bef0e4d85bb0c1e26115cb2d1"
+ },
+ {
+ "name": "ideograph_advantage",
+ "unicode": "1F250",
+ "digest": "37892a5642cd49ef7828646f36f48b5a83dc02437624c05da428579256118030"
+ },
+ {
+ "name": "imp",
+ "unicode": "1F47F",
+ "digest": "f8c93d03bd9f1d5ef86738541e11695d6811bf6fef06759eba98321b6d038814"
+ },
+ {
+ "name": "inbox_tray",
+ "unicode": "1F4E5",
+ "digest": "066a2d75633eb50329496f6866b5b0645c2e48135a03118f1bf53244f8529043"
+ },
+ {
+ "name": "incoming_envelope",
+ "unicode": "1F4E8",
+ "digest": "ef6e5c5aa679d174181dae77113717f26e295778dde1e2c3bdf1d64de8a4af8c"
+ },
+ {
+ "name": "info",
+ "unicode": "1F6C8",
+ "digest": "59c35e77d5ee663c5d56f7d8af845ce8aeb9935e526ae4a06e02ae70e71212ca"
+ },
+ {
+ "name": "information_desk_person",
+ "unicode": "1F481",
+ "digest": "acae6d272e348aee87dd60360f16ac58cea7cb4e1ea962cc1655005c7f4aed27"
+ },
+ {
+ "name": "information_desk_person_tone1",
+ "unicode": "1F481-1F3FB",
+ "digest": "709ebb0481ca981d76ece2d4fc68db693ddf18b9c1aaa0b6ac5d3c42e71bf07f"
+ },
+ {
+ "name": "information_desk_person_tone2",
+ "unicode": "1F481-1F3FC",
+ "digest": "d5bc3563bc721d66b73850db93ac827be3715e7ca6420dc0051396ffe26bef47"
+ },
+ {
+ "name": "information_desk_person_tone3",
+ "unicode": "1F481-1F3FD",
+ "digest": "af67fd4ef2fc402bec2d446b2e8ff5e9f636b5a9bbb6639587cdb88bd780d265"
+ },
+ {
+ "name": "information_desk_person_tone4",
+ "unicode": "1F481-1F3FE",
+ "digest": "fd3174d1adfe13e8c0d6b6ae9c3a26ea35bb40f98f0728f91d1798809a74933b"
+ },
+ {
+ "name": "information_desk_person_tone5",
+ "unicode": "1F481-1F3FF",
+ "digest": "4b773c443830a02de8b4d6471077b5d1387b560b537cabba7cdc667110cbde69"
+ },
+ {
+ "name": "information_source",
+ "unicode": "2139",
+ "digest": "50cd8bf46d20b7c18d5f00a69fc79452aa32934245ba8d0929e51632d73876bd"
+ },
+ {
+ "name": "innocent",
+ "unicode": "1F607",
+ "digest": "a3510fd51c17093ebe2371cfde7611aa44aed2d120a0e5500cfaae0f1d3486a4"
+ },
+ {
+ "name": "interrobang",
+ "unicode": "2049",
+ "digest": "1f843ff672486154f9f3df549bb1b528a5eac8d15264f447649ba57f45ee4d00"
+ },
+ {
+ "name": "iphone",
+ "unicode": "1F4F1",
+ "digest": "be6f96c02ddae557f700fd20fe7b3f94c9e1c928acb82b2b8b214d231273fece"
+ },
+ {
+ "name": "island",
+ "unicode": "1F3DD",
+ "digest": "17f02b309b62ed9542b1d8943168302846040e420f413e56d799bb5fba7064fa"
+ },
+ {
+ "name": "izakaya_lantern",
+ "unicode": "1F3EE",
+ "digest": "ddb20f475aa119c3a64a55dff40f7a9dbc3a14f7ffc6cfbac89210c652f10d02"
+ },
+ {
+ "name": "jack_o_lantern",
+ "unicode": "1F383",
+ "digest": "62a701ac472619bcb3859e0d9a61b98c7f5c32150d2d04ca8c3e8fc3bec4dbd5"
+ },
+ {
+ "name": "japan",
+ "unicode": "1F5FE",
+ "digest": "2535300fff2b2e4b75fc73c187be6c0ea4bc4753e443db498ea55e268e627ab7"
+ },
+ {
+ "name": "japanese_castle",
+ "unicode": "1F3EF",
+ "digest": "70645aa05599e23a9ac4327e4a2e78bffe7ea06c38ec1935c15ae420619c5c1c"
+ },
+ {
+ "name": "japanese_goblin",
+ "unicode": "1F47A",
+ "digest": "59b6901dc6eedc6509c25b4eef6702bf461ded06c5ff12fe2a02a5b3301577c0"
+ },
+ {
+ "name": "japanese_ogre",
+ "unicode": "1F479",
+ "digest": "dab7e68cd4cbf99c13d64792c7104c4f0a846bc63aa12950fa8fab028dca301d"
+ },
+ {
+ "name": "jeans",
+ "unicode": "1F456",
+ "digest": "ddd032ac77cdfe49152a0e0a0eaaaea9f183590fb1f493ec30e9e39f679e3914"
+ },
+ {
+ "name": "jet_up",
+ "unicode": "1F6E6",
+ "digest": "3708e5e034b1c64d1268d66527e13c369aa0f8903bce9172bef773b2d1940948"
+ },
+ {
+ "name": "joy",
+ "unicode": "1F602",
+ "digest": "f90cfbcb14f906f8d786b61f022c978f381fc99ca422805f605631314e101805"
+ },
+ {
+ "name": "joy_cat",
+ "unicode": "1F639",
+ "digest": "6ca24a94490de66d1ca2cbc080bcd805f54ca295051d8e6588cae3fe6658c80a"
+ },
+ {
+ "name": "joystick",
+ "unicode": "1F579",
+ "digest": "ec172df88ef8e8a5512d6d906c13296875b7057ed0cca79f4ac8cddd9e1de34b"
+ },
+ {
+ "name": "kaaba",
+ "unicode": "1F54B",
+ "digest": "30f1a27a148399bbb811586eff795eff858701c42055c23e4d5bef7ae77f5f32"
+ },
+ {
+ "name": "key",
+ "unicode": "1F511",
+ "digest": "c68ed648350d3976c8d27a709020c8873ecf553929e66453acff96231684a1a2"
+ },
+ {
+ "name": "key2",
+ "unicode": "1F5DD",
+ "digest": "87a7d42531d7a11dcb11b0d6d1be611ee8cec35b5d22226a8ac6083fedef4f5d"
+ },
+ {
+ "name": "keyboard",
+ "unicode": "1F5AE",
+ "digest": "3b254cbf19946df3af05e501d11653d89fcda91684b7248d86186f842b83bf16"
+ },
+ {
+ "name": "keyboard_mouse",
+ "unicode": "1F5A6",
+ "digest": "95b523e55d8afeaeb06442bbe20e47f49643bb0c32d89a8cdbbccdead20532b3"
+ },
+ {
+ "name": "keyboard_with_jacks",
+ "unicode": "1F398",
+ "digest": "e29a0d0b8018d13458469edca13c60a882a2817957c1aa11b050684c995a47ee"
+ },
+ {
+ "name": "keycap_ten",
+ "unicode": "1F51F",
+ "digest": "7593aa7ffe7192a2e35c6ccec76522f6243777783c9152c7c03419835ea58c03"
+ },
+ {
+ "name": "kimono",
+ "unicode": "1F458",
+ "digest": "e92bea044fe013f1993c2229d86e9cca9d43f14aab00564ce6ff559bdc5ce93a"
+ },
+ {
+ "name": "kiss",
+ "unicode": "1F48B",
+ "digest": "c060eb09af2a0d0f77d307b995c15719b0e59c9162a490b8a553fac9b779c8f0"
+ },
+ {
+ "name": "kiss_mm",
+ "unicode": "1F468-2764-1F48B-1F468",
+ "digest": "381364ad988ec07cc3708fd60f71838092224009088fff587069b4e8ab01ee63"
+ },
+ {
+ "name": "kiss_ww",
+ "unicode": "1F469-2764-1F48B-1F469",
+ "digest": "7705ca707b73f44c856ea324bdfe30ed05244c8d192d1111f6e1d62ab3f2f8a5"
+ },
+ {
+ "name": "kissing",
+ "unicode": "1F617",
+ "digest": "3142617e8b9488689bd9efc67c0e4cc71a1870df8ffc308f949eedc5c3684051"
+ },
+ {
+ "name": "kissing_cat",
+ "unicode": "1F63D",
+ "digest": "ed26cee8c438ba41365b55c48457cdad3e8d43bf90db3128ac5b277718b82ed3"
+ },
+ {
+ "name": "kissing_closed_eyes",
+ "unicode": "1F61A",
+ "digest": "22d3369d21b4c2cb4c0c2cab9551cd848dd4f9adecfa64977d3f1a80fc0c8b53"
+ },
+ {
+ "name": "kissing_heart",
+ "unicode": "1F618",
+ "digest": "1f089b07447bdcc1baada6a2a9607d4ef4f2de9a6093fcab47a553a64b9acb76"
+ },
+ {
+ "name": "kissing_smiling_eyes",
+ "unicode": "1F619",
+ "digest": "e37d282861669adfa3953b9af833acfab7d55e787621d4318d77de7e3529d5c5"
+ },
+ {
+ "name": "knife",
+ "unicode": "1F52A",
+ "digest": "3fef068a6ada61630dc868e47d25e0e0550b44bc7cf530afe88ca63dc7ab2a39"
+ },
+ {
+ "name": "koala",
+ "unicode": "1F428",
+ "digest": "fe020ab9048f3c2a881474f8b1335db6bfaf37d115ff9b2d264f668d136122dd"
+ },
+ {
+ "name": "koko",
+ "unicode": "1F201",
+ "digest": "734a5cb296826a598e02be3f4ec22f318633ede2ce274914586256421e2df97b"
+ },
+ {
+ "name": "label",
+ "unicode": "1F3F7",
+ "digest": "9fe8195c3efab4d905b1cfcba0ae58cda12496030b0908de8076ff5e6777742e"
+ },
+ {
+ "name": "large_blue_circle",
+ "unicode": "1F535",
+ "digest": "ba4d0f84a9c2be9a65b25c8cfa78f30d4856d021b1853154dd1d2fd0c5bcfb6a"
+ },
+ {
+ "name": "large_blue_diamond",
+ "unicode": "1F537",
+ "digest": "d5aa5e315126859c10c83507be6b9e11cbf423f7a27145de089468cff9b94a94"
+ },
+ {
+ "name": "large_orange_diamond",
+ "unicode": "1F536",
+ "digest": "108600badd0ef267842325c0fbf326cb3504306332c64f6f5694de2b54c9438a"
+ },
+ {
+ "name": "last_quarter_moon",
+ "unicode": "1F317",
+ "digest": "68315b85bc1cb17bb82629bd1a6024a5124f3641b9878a732a8aad016c587546"
+ },
+ {
+ "name": "last_quarter_moon_with_face",
+ "unicode": "1F31C",
+ "digest": "146a419109b7f662bf87cf9de299e47d025a8758c8970b7dabf3483e1956b559"
+ },
+ {
+ "name": "laughing",
+ "unicode": "1F606",
+ "digest": "f22d3be77f1daf058d04c3cbc1fd7f76b4dc069d2d300b45e63e768b08d269c5"
+ },
+ {
+ "name": "leaves",
+ "unicode": "1F343",
+ "digest": "f65e2db125564eb04fc427a49fff175d6e2dae847bd12314d5e6a131610d5ccd"
+ },
+ {
+ "name": "ledger",
+ "unicode": "1F4D2",
+ "digest": "62df1772cec10c035ae0646e6cca4ba7d75b10636a520d091c5b42c2dc36b742"
+ },
+ {
+ "name": "left_luggage",
+ "unicode": "1F6C5",
+ "digest": "62292758715115e55ab6239805b7f99b7b35bdfa8d40da07fe391424f1f083d8"
+ },
+ {
+ "name": "left_receiver",
+ "unicode": "1F57B",
+ "digest": "8052e44951afee04c87296128744b5019ec783c9ed1a231f659af6c8ddaa50f3"
+ },
+ {
+ "name": "left_right_arrow",
+ "unicode": "2194",
+ "digest": "28a6945972451b1f4dadec5c55310b8868ffd9f3b0a07803287bc4e07a56e7d4"
+ },
+ {
+ "name": "leftwards_arrow_with_hook",
+ "unicode": "21A9",
+ "digest": "d672afc39fd50f78d7370be243173fe76ba50292f0c401305b562898939a8b7f"
+ },
+ {
+ "name": "lemon",
+ "unicode": "1F34B",
+ "digest": "e0e293a8b8c1b3c87534f5e05cf006671eb3c6d52b4d17d40f2e23bce215a8be"
+ },
+ {
+ "name": "leo",
+ "unicode": "264C",
+ "digest": "b0fd4e5f4637de530b62323521c6edcd80312d67ea4043eedd959acb6763474a"
+ },
+ {
+ "name": "leopard",
+ "unicode": "1F406",
+ "digest": "ede891be8484a17e6277431c64ec1bfd6b742544a41947ebc85005bc2d558bb1"
+ },
+ {
+ "name": "level_slider",
+ "unicode": "1F39A",
+ "digest": "49777cf160d9130d723e3bfef765c3de54033e6b059000fb0e22fb559b5ed190"
+ },
+ {
+ "name": "levitate",
+ "unicode": "1F574",
+ "digest": "3e4e9a5ac6a8dbd7909c58a9d915f16f1a0fc59cc019714ae5935f18e4704044"
+ },
+ {
+ "name": "libra",
+ "unicode": "264E",
+ "digest": "ec8e2e7a735abc9f2bddb115fc0e09f4bdc7a164679e2b57d127f58eee1155c2"
+ },
+ {
+ "name": "lifter",
+ "unicode": "1F3CB",
+ "digest": "f64db037fd21e5918e5de35d6a561ef4b44668e307ed351338de00fcf3e771e3"
+ },
+ {
+ "name": "lifter_tone1",
+ "unicode": "1F3CB-1F3FB",
+ "digest": "f9e0d161b12c4908ac3409b11c1a77ee38f33ba018f12416545876214bfb7c01"
+ },
+ {
+ "name": "lifter_tone2",
+ "unicode": "1F3CB-1F3FC",
+ "digest": "631eb6ed5bd147dc6f1f8b94149abe44d62a0f78e7809e37a4bfe127c40ed98f"
+ },
+ {
+ "name": "lifter_tone3",
+ "unicode": "1F3CB-1F3FD",
+ "digest": "406b5707a47d9066f016acf0b64fa695e3505acc2453758a0428de21efd7eb6d"
+ },
+ {
+ "name": "lifter_tone4",
+ "unicode": "1F3CB-1F3FE",
+ "digest": "d917164ed8c4bb1ffcc887ca256ec329e7fa1b9516eaf8c159f8b43fdb071ed6"
+ },
+ {
+ "name": "lifter_tone5",
+ "unicode": "1F3CB-1F3FF",
+ "digest": "f79ea93e8a40b3c895b693bf49eb4ce6e7b3f4413595e5881ea44839fd7fe8e5"
+ },
+ {
+ "name": "light_check_mark",
+ "unicode": "1F5F8",
+ "digest": "7842b0df8c2b6703bed0cce5d2790d394eec7120b2a245a76f375528f2729a7b"
+ },
+ {
+ "name": "light_rail",
+ "unicode": "1F688",
+ "digest": "7c2be55456f1332e849ff6699a26dda2e1641c280f45c9ec88dedf6d9b7b7fe2"
+ },
+ {
+ "name": "link",
+ "unicode": "1F517",
+ "digest": "cc4873f8a612dd721dddcd507a4430b4fb6c4abc15a8848456f0ffd97811b163"
+ },
+ {
+ "name": "lion_face",
+ "unicode": "1F981",
+ "digest": "935b1076815f51fafcd860a395d0a03c536acfcea61ffcf542a377da046fa7d9"
+ },
+ {
+ "name": "lips",
+ "unicode": "1F444",
+ "digest": "e3bc20f9e210fa1711271234fe61bf1c9ddf36dd6ffc5b832c6c3a769a1e59a8"
+ },
+ {
+ "name": "lips2",
+ "unicode": "1F5E2",
+ "digest": "c6ba915982ac47d8aaf14ad3605949df95588acfb4e147bf608f8c1714cdf19b"
+ },
+ {
+ "name": "lipstick",
+ "unicode": "1F484",
+ "digest": "335b912e163020df3d6d9f0a19a55d6547bd59b471c5a3e374c2968e49911ccc"
+ },
+ {
+ "name": "lock",
+ "unicode": "1F512",
+ "digest": "c20eacfb8ccd9bb85919a837c0d4650ee608edb48c85bff46945f613e95d7038"
+ },
+ {
+ "name": "lock_with_ink_pen",
+ "unicode": "1F50F",
+ "digest": "5cab25cea08e22d9c3f5de16de6d0ab658ca15cc93d7830f29b0f3e9348ec45f"
+ },
+ {
+ "name": "lollipop",
+ "unicode": "1F36D",
+ "digest": "33d2334a00bf0e15869ccc75fadc36f27f89abf0525bb71f859aad9e1dc4ad66"
+ },
+ {
+ "name": "loop",
+ "unicode": "27BF",
+ "digest": "fa1174ddc44e317d0796e07868c7ac8ac9c9274fbc8a6c3d0ec78d543c3c6bf0"
+ },
+ {
+ "name": "loud_sound",
+ "unicode": "1F50A",
+ "digest": "fb70229e13b690ffc1031d2e631123f8c908035a15218c297c1c4a3ff3624aa0"
+ },
+ {
+ "name": "loudspeaker",
+ "unicode": "1F4E2",
+ "digest": "e2d6cf9ec6412ee62f3128a1afd8c63ec74755c4833f01a4f99722407fe154d6"
+ },
+ {
+ "name": "love_hotel",
+ "unicode": "1F3E9",
+ "digest": "184670ebc4045043a7b18d576da3255d216551da522a11cde7df34524e9c7d50"
+ },
+ {
+ "name": "love_letter",
+ "unicode": "1F48C",
+ "digest": "9a4c52e2622fc7d364995ebc93ca530d972134621d117b72053a659dffc90ffc"
+ },
+ {
+ "name": "low_brightness",
+ "unicode": "1F505",
+ "digest": "c177b7fa9fdbef959cc47e7d16becd71117470b767a81ed6d15f80f464776c02"
+ },
+ {
+ "name": "m",
+ "unicode": "24C2",
+ "digest": "2eaf011e74d69613923dad424daaec4c13b592388dbcc5757b645bc058eedecb"
+ },
+ {
+ "name": "mag",
+ "unicode": "1F50D",
+ "digest": "029427bd73d2c79fffc5194ded01f6011952ec0124b7634c6230e0afa7ad7c95"
+ },
+ {
+ "name": "mag_right",
+ "unicode": "1F50E",
+ "digest": "f99de50bb59ec3bf1d4ccb8584ca09d4a7ceb5bf9f600ea8d3f84930efbf01b8"
+ },
+ {
+ "name": "mahjong",
+ "unicode": "1F004",
+ "digest": "da5d1fa980c38e092d414516161ca26046aa65ace3261999ea750f72e676ac6e"
+ },
+ {
+ "name": "mailbox",
+ "unicode": "1F4EB",
+ "digest": "14217df8f39a95fc0a0c527f97db1ca8564764034e921614decc5be705629352"
+ },
+ {
+ "name": "mailbox_closed",
+ "unicode": "1F4EA",
+ "digest": "e0c7beb205ec548a66d8afc7f103b64c6c79c08417ab550f19c36cc6d1a62bc4"
+ },
+ {
+ "name": "mailbox_with_mail",
+ "unicode": "1F4EC",
+ "digest": "6d381c0c4181be628d9409df1d85f8a9438c21ef5b92d82ef8ae1ff0079236de"
+ },
+ {
+ "name": "mailbox_with_no_mail",
+ "unicode": "1F4ED",
+ "digest": "74843d5ea9e03b48323f2252bdd000585f549b7fffe1fe181a25c38b99b5e23d"
+ },
+ {
+ "name": "man",
+ "unicode": "1F468",
+ "digest": "0275935258b4c832c3fcb06531d3e6972e2c3d46bab2973004750a9f00bd4cb6"
+ },
+ {
+ "name": "man_tone1",
+ "unicode": "1F468-1F3FB",
+ "digest": "1f6603d040f4a025f49d384170dd16b8da169663fc3282af1dc8710d9c1a7adf"
+ },
+ {
+ "name": "man_tone2",
+ "unicode": "1F468-1F3FC",
+ "digest": "d65bb03071b483946c69c61769d19b29a2af76fa7e43020e55f0bbc046492221"
+ },
+ {
+ "name": "man_tone3",
+ "unicode": "1F468-1F3FD",
+ "digest": "9af8ede7211b19a7dc0c60db083dd2bdc4897dda4d71e57feadf2e39d847f060"
+ },
+ {
+ "name": "man_tone4",
+ "unicode": "1F468-1F3FE",
+ "digest": "6555de60976aafeb024db78addb44eab2a412dd7277013f44d06757d03b6a252"
+ },
+ {
+ "name": "man_tone5",
+ "unicode": "1F468-1F3FF",
+ "digest": "b58b97a28a6adc1777acc05194cd917c730f90e37441124c384ded12e9a7d2a4"
+ },
+ {
+ "name": "man_with_gua_pi_mao",
+ "unicode": "1F472",
+ "digest": "88663173a6ccbebec5e24883c90d965447e022c6688773273110fe544d5b1607"
+ },
+ {
+ "name": "man_with_gua_pi_mao_tone1",
+ "unicode": "1F472-1F3FB",
+ "digest": "3c8bad3923a619f888e14544d357499a26a517e8fbe7a51027117b960c9eb842"
+ },
+ {
+ "name": "man_with_gua_pi_mao_tone2",
+ "unicode": "1F472-1F3FC",
+ "digest": "da125a3310fab19c9282497d53e2fc71ad07920ce60a0ef52dcdb31500023f09"
+ },
+ {
+ "name": "man_with_gua_pi_mao_tone3",
+ "unicode": "1F472-1F3FD",
+ "digest": "1d5842558847367966bf3ea473ff80fe744359bc5d969f4cc06cf2e452ed2fb6"
+ },
+ {
+ "name": "man_with_gua_pi_mao_tone4",
+ "unicode": "1F472-1F3FE",
+ "digest": "92be490f3ba602a43e2be8160d8bfd8a0691b2f81fe017b06df10f476a89ffab"
+ },
+ {
+ "name": "man_with_gua_pi_mao_tone5",
+ "unicode": "1F472-1F3FF",
+ "digest": "669f6b31bc7a8bf50b169d0600f14e00addaeb24144a1bace8b94950372839b0"
+ },
+ {
+ "name": "man_with_turban",
+ "unicode": "1F473",
+ "digest": "87d30d35ba40ee39c2df8ce19d975ce34a9c54688bafeac7377d7d481e55f1a4"
+ },
+ {
+ "name": "man_with_turban_tone1",
+ "unicode": "1F473-1F3FB",
+ "digest": "33b8b8154e0691e2ad66177dbf1e0101411fd8b3a16bf4e54c36d4a874f2a275"
+ },
+ {
+ "name": "man_with_turban_tone2",
+ "unicode": "1F473-1F3FC",
+ "digest": "1a6b83faa8d6e6a7d12a04898a6f22243287330a1faa081d2626b17dfb07174d"
+ },
+ {
+ "name": "man_with_turban_tone3",
+ "unicode": "1F473-1F3FD",
+ "digest": "5d43da5109e688ff8ca0675f33ebbaf930e206f1f01e3ee773f2844663fe572b"
+ },
+ {
+ "name": "man_with_turban_tone4",
+ "unicode": "1F473-1F3FE",
+ "digest": "bfaf7293c5ea75d0ecdc6fe5afe8f48e7b29b2e0df06ef974d3e1732f5db5dd4"
+ },
+ {
+ "name": "man_with_turban_tone5",
+ "unicode": "1F473-1F3FF",
+ "digest": "fba2404dd3d7eab5268519894cc0b386e1b17fdf14a04760c346014aa0e25acd"
+ },
+ {
+ "name": "mans_shoe",
+ "unicode": "1F45E",
+ "digest": "45dc13ac44c922b4c4b8ecb2e1a870a78e09d53da86843431ab0e9ec96ebcd97"
+ },
+ {
+ "name": "map",
+ "unicode": "1F5FA",
+ "digest": "f56116d09996d6d08fb5cdfb46622b545253f2649008170fc2011a9713fa875b"
+ },
+ {
+ "name": "maple_leaf",
+ "unicode": "1F341",
+ "digest": "40c5ee93396301911391cf6e70454b6fa8020fe5c85d3364136bcedb5d052cdb"
+ },
+ {
+ "name": "mask",
+ "unicode": "1F637",
+ "digest": "e0301cd27eb8c74c9772ff05b880215fc031ac1ae7f3177cd24ba0acb43b3834"
+ },
+ {
+ "name": "massage",
+ "unicode": "1F486",
+ "digest": "856d0fb1144ee91c58dfad74f9a2cababf6bae4b3ceba2a95c03ecd44ae3aa21"
+ },
+ {
+ "name": "massage_tone1",
+ "unicode": "1F486-1F3FB",
+ "digest": "fd53b06eb0967303c0914ebb79fd872900ec0f71b2852c7238517e192e5023e1"
+ },
+ {
+ "name": "massage_tone2",
+ "unicode": "1F486-1F3FC",
+ "digest": "7ef57359a339ae1ca4488f9a6195a352e74daf5b67d8e1ae1e91fe866921c40c"
+ },
+ {
+ "name": "massage_tone3",
+ "unicode": "1F486-1F3FD",
+ "digest": "e4fb643b6242bedb395e503ae337a88b2a255b5fda88b4aaa93396f948614a6e"
+ },
+ {
+ "name": "massage_tone4",
+ "unicode": "1F486-1F3FE",
+ "digest": "94f007c2daf9455fa8d2b10cc7ccff7db9bc9daf835ef5c3699be091938db833"
+ },
+ {
+ "name": "massage_tone5",
+ "unicode": "1F486-1F3FF",
+ "digest": "d18e800b728bf45b500f492062dc81312ca1ad7b1a0277a3d5bc150e4632ea1c"
+ },
+ {
+ "name": "meat_on_bone",
+ "unicode": "1F356",
+ "digest": "674a2a58e174b7681eef3b6c5b39c098ed9374cc610d037166c0092ee5269a97"
+ },
+ {
+ "name": "medal",
+ "unicode": "1F3C5",
+ "digest": "270d438b6e2155e944dc734ea3e4d02409e51f59db2db636398fbf96e5edb0e6"
+ },
+ {
+ "name": "mega",
+ "unicode": "1F4E3",
+ "digest": "540ab4fd5bab041a681749b85e6de598ebcbfc4fbf5c3cdbd9ca1e8256191733"
+ },
+ {
+ "name": "melon",
+ "unicode": "1F348",
+ "digest": "39dd0ecb23e2d3da6cbb7309333fed5d7e2cb38c0afc526ade78520eca11b5f4"
+ },
+ {
+ "name": "menorah",
+ "unicode": "1F54E",
+ "digest": "5f81bc2e5a34bf76481d2958fdb0b4e4540c599aa837a6453609a39023885d8c"
+ },
+ {
+ "name": "mens",
+ "unicode": "1F6B9",
+ "digest": "5ed56cff80e8ee7ed581f2a2e365915db5cb29df89e850e0add0b68db4b0c788"
+ },
+ {
+ "name": "metal",
+ "unicode": "1F918",
+ "digest": "45e5fac0b9b019cf217dcfd1380cafb0d03063454612178278dac1ca5f8476a6"
+ },
+ {
+ "name": "metal_tone1",
+ "unicode": "1F918-1F3FB",
+ "digest": "9b3596fe7c063df838f0a43fb680ce10fb88e2b73c5c3324abfa357a224c17aa"
+ },
+ {
+ "name": "metal_tone2",
+ "unicode": "1F918-1F3FC",
+ "digest": "e15a4898a0efca4354ac48d6b01ff0618ce8b110b1246a4f5d78e19b54658be6"
+ },
+ {
+ "name": "metal_tone3",
+ "unicode": "1F918-1F3FD",
+ "digest": "c159e8179cb1907c246b432d87c5253b914fd7cebb6ac05292c4e38eff4815b0"
+ },
+ {
+ "name": "metal_tone4",
+ "unicode": "1F918-1F3FE",
+ "digest": "a8a43a88028c97074321e3da56df1045db41ede58bf286c21d7ae90f222f2011"
+ },
+ {
+ "name": "metal_tone5",
+ "unicode": "1F918-1F3FF",
+ "digest": "e6611e826e867e2c73a8cadb138e4aa6365e3583dd229ff24b3e8f161904bf56"
+ },
+ {
+ "name": "metro",
+ "unicode": "1F687",
+ "digest": "532378cf385f9a7fafe2f5c8203e675be6d38798871f4c8e2c50498a1529f956"
+ },
+ {
+ "name": "microphone",
+ "unicode": "1F3A4",
+ "digest": "46da2b94e4dc233f640249103f09ec915aaa812cce90afe68fedb6774a27ad4b"
+ },
+ {
+ "name": "microphone2",
+ "unicode": "1F399",
+ "digest": "f9df32cd207808f67a895d3460a215d1ecc42e377907bcd64731c02b697d4f32"
+ },
+ {
+ "name": "microscope",
+ "unicode": "1F52C",
+ "digest": "79918f5fe0a39f31f270a481f4c6e00ea49fc09d64b1ae78770971293c2b1ed8"
+ },
+ {
+ "name": "middle_finger",
+ "unicode": "1F595",
+ "digest": "c6320b236a4a9593aeade511b52dd3114207e947458cb3b818c78737a505fdf6"
+ },
+ {
+ "name": "middle_finger_tone1",
+ "unicode": "1F595-1F3FB",
+ "digest": "93c7aa994856185519d576cb779bdcff3a33f7077eef98e70968125f92f02448"
+ },
+ {
+ "name": "middle_finger_tone2",
+ "unicode": "1F595-1F3FC",
+ "digest": "a0de802294717b80e08d9d30f5fd64eacb90b5b3b9d7a0c27d6226a22822597f"
+ },
+ {
+ "name": "middle_finger_tone3",
+ "unicode": "1F595-1F3FD",
+ "digest": "8bbbab07c838257416bbf8377904362c07019fca9d5abf9fd048ccf6370178da"
+ },
+ {
+ "name": "middle_finger_tone4",
+ "unicode": "1F595-1F3FE",
+ "digest": "d9eed8db540fdb669c6ae5ef168b77659660589f5ddd9b66062274d335a3ef04"
+ },
+ {
+ "name": "middle_finger_tone5",
+ "unicode": "1F595-1F3FF",
+ "digest": "0519c3298040e57db202294476df239edb9b23b44848bab296bc45eda7cf8664"
+ },
+ {
+ "name": "military_medal",
+ "unicode": "1F396",
+ "digest": "bd1da0004768f404c6bb4db85d4b748f766a77ab3edb74e709d0c0064509a043"
+ },
+ {
+ "name": "milky_way",
+ "unicode": "1F30C",
+ "digest": "598b4e641c1081bb03ce38a29f9711fc8616373216a833e4daa14fbe97a358f5"
+ },
+ {
+ "name": "minibus",
+ "unicode": "1F690",
+ "digest": "3d15791ca96349c3abb5bd5d1014b6b33b984db19609f56f5fd1e8d2fc551809"
+ },
+ {
+ "name": "minidisc",
+ "unicode": "1F4BD",
+ "digest": "83c4bfda4e0a80785fa1c3f2bbf3c15aca2bda8ea3727ce78bc4236e1e377a36"
+ },
+ {
+ "name": "mobile_phone_off",
+ "unicode": "1F4F4",
+ "digest": "cfe6dfd766b9e0b4768df25d6e943c9abc0e910ff5e5c7a8a0f425c786bbab8d"
+ },
+ {
+ "name": "money_mouth",
+ "unicode": "1F911",
+ "digest": "3ac2f9b5409e1426eef6966938ca04cf78aeffefd43f44b6c86af4af7836e22f"
+ },
+ {
+ "name": "money_with_wings",
+ "unicode": "1F4B8",
+ "digest": "f7f1fa502d2f6804169869aeb5ca7f0ea64bc2d6a0204f08875d65da4f8cb332"
+ },
+ {
+ "name": "moneybag",
+ "unicode": "1F4B0",
+ "digest": "442db49cda27360d2eb781489c9879730a6094c3267bb0a0a8687d84f8fed078"
+ },
+ {
+ "name": "monkey",
+ "unicode": "1F412",
+ "digest": "3141c971aacbadaba21f970a515e192740212be2a49fa1f5eb0fc4dc576e209f"
+ },
+ {
+ "name": "monkey_face",
+ "unicode": "1F435",
+ "digest": "e2397431d2befe44bf5298fa81d865d80722bf954113bceacc2aa98b84d856e2"
+ },
+ {
+ "name": "monorail",
+ "unicode": "1F69D",
+ "digest": "b546153200d6fbe8d65b1b34f62ff4a19b1b6a159eb1b536c5c2ecb56dab0ec9"
+ },
+ {
+ "name": "mood_bubble",
+ "unicode": "1F5F0",
+ "digest": "1df7061217e478d43ab9a87d4f351c4ca56705acd6b4e0b0bedfdece77635f1b"
+ },
+ {
+ "name": "mood_bubble_lightning",
+ "unicode": "1F5F1",
+ "digest": "4af3e4e53eaa328b0d20542ab31705a74bf9fd368cd0673b706838ce1681d3c9"
+ },
+ {
+ "name": "mood_lightning",
+ "unicode": "1F5F2",
+ "digest": "6784635e81ec722fd50a1c2a23b0f9679e4bf1b5ae2b5a01eeb995bc1f7a426f"
+ },
+ {
+ "name": "mortar_board",
+ "unicode": "1F393",
+ "digest": "cb59edb08f75c374088b65284e4d0f77b9bc9573de3e6a5127f865431011e54c"
+ },
+ {
+ "name": "mosque",
+ "unicode": "1F54C",
+ "digest": "a08ddb74342dea8f79063db6f98ba03eb08fe99481de8ce9123827ca7f17c7f3"
+ },
+ {
+ "name": "motorboat",
+ "unicode": "1F6E5",
+ "digest": "9dbea67bbe2e95dcc68c049a58f87390a44350b32308342615d75214af3d1cef"
+ },
+ {
+ "name": "motorcycle",
+ "unicode": "1F3CD",
+ "digest": "8429fb6dfeb873abdffcc179c32d4f23e91c9e6b27b06cd204fd2e83cc11189e"
+ },
+ {
+ "name": "motorway",
+ "unicode": "1F6E3",
+ "digest": "fc05a36c917637c135b0a60db8afcd58cee2b335070fe3888697f8026c9d11a5"
+ },
+ {
+ "name": "mount_fuji",
+ "unicode": "1F5FB",
+ "digest": "22bfffef033637b3c9b2fe7e539c74a659d2a49e594d2b33be894da00654d059"
+ },
+ {
+ "name": "mountain",
+ "unicode": "26F0",
+ "digest": "486cf4e9d5f3913d138fdb7878fe869b39caa3fca53876365957a89dc8f7edb8"
+ },
+ {
+ "name": "mountain_bicyclist",
+ "unicode": "1F6B5",
+ "digest": "b547b96951b6837df8ae3be1e846f15e7e2ac06d976e1fe7f1442dcc5d3a0942"
+ },
+ {
+ "name": "mountain_bicyclist_tone1",
+ "unicode": "1F6B5-1F3FB",
+ "digest": "68ce0d55163c7b89ee1d87b752ece127bb25ca9deb3421b31df549a00ac5f69d"
+ },
+ {
+ "name": "mountain_bicyclist_tone2",
+ "unicode": "1F6B5-1F3FC",
+ "digest": "5bfa82180bfb8bc4444cf301688aff02884895574a7ba66b398aaf20bde0f101"
+ },
+ {
+ "name": "mountain_bicyclist_tone3",
+ "unicode": "1F6B5-1F3FD",
+ "digest": "33cb64a792123b81a05080465a0ea1035a2cdfdab01c71f5f725a5f92251c3e8"
+ },
+ {
+ "name": "mountain_bicyclist_tone4",
+ "unicode": "1F6B5-1F3FE",
+ "digest": "9c3fa4e65dcb0ad69b963292e77c7a75853ae3c1d18a90670f81ffb65b5d020c"
+ },
+ {
+ "name": "mountain_bicyclist_tone5",
+ "unicode": "1F6B5-1F3FF",
+ "digest": "871de9e3fddb49b305e5f91000143878b0288c107a125c4e60acf2b6cf8b7f3f"
+ },
+ {
+ "name": "mountain_cableway",
+ "unicode": "1F6A0",
+ "digest": "f248ed5bf864f4a81e365b30d2825d2e6fc15a200c4ccf69e9f797341529f955"
+ },
+ {
+ "name": "mountain_railway",
+ "unicode": "1F69E",
+ "digest": "7dd08745ab56c95c3dfcebcca517ff231cef61b670cedf9d7c53f3244c34e30b"
+ },
+ {
+ "name": "mountain_snow",
+ "unicode": "1F3D4",
+ "digest": "9939aade3d4d972ba3af16fcc6cc2454978f5426e4c92838734a44db065ce0ff"
+ },
+ {
+ "name": "mouse",
+ "unicode": "1F42D",
+ "digest": "fb20b3a82f407a6316bbbac68d58018c3d5b93a9a6ae968f44ace18d1c5698d9"
+ },
+ {
+ "name": "mouse2",
+ "unicode": "1F401",
+ "digest": "87be4099523ec32440e6d091f1193a8ed90730b9fbecaafed4912585bfe7818c"
+ },
+ {
+ "name": "mouse_one",
+ "unicode": "1F5AF",
+ "digest": "e0d2055ccba489d24e0c0b6d2f22793efe48a734b0fd50f5af88f721b40665c0"
+ },
+ {
+ "name": "mouse_three_button",
+ "unicode": "1F5B1",
+ "digest": "6a5629fee01145211cc8f4e8f59c5f1e61affed38c650502213d76c7d8861b01"
+ },
+ {
+ "name": "movie_camera",
+ "unicode": "1F3A5",
+ "digest": "d6633b89a637b64d617c3032eed74bb82d3fa732dd9975486b2b5841b473808a"
+ },
+ {
+ "name": "moyai",
+ "unicode": "1F5FF",
+ "digest": "bf948c26cd98e2f5e48da363f2924a9d7c217232115a00cec372d0d5293402a8"
+ },
+ {
+ "name": "muscle",
+ "unicode": "1F4AA",
+ "digest": "c85147efb786bdea3e7d53e2edf6b827280cd9fa881661a6102a614bf5b3579f"
+ },
+ {
+ "name": "muscle_tone1",
+ "unicode": "1F4AA-1F3FB",
+ "digest": "38d071df2b25031b61f3605b03c34d2e5d3e35d29f3c4aada14be37e19750eb8"
+ },
+ {
+ "name": "muscle_tone2",
+ "unicode": "1F4AA-1F3FC",
+ "digest": "dcf11b76c8ffb58dc7e4f9ecd32a4c291d9772d51df2853d41081e041e7e0876"
+ },
+ {
+ "name": "muscle_tone3",
+ "unicode": "1F4AA-1F3FD",
+ "digest": "a3d5f8f2dbfc28f9713ee657428ea3292c47d0b22f11a51c13594be22b0f5204"
+ },
+ {
+ "name": "muscle_tone4",
+ "unicode": "1F4AA-1F3FE",
+ "digest": "eb220fc19be58d16cacc6b721e1011078b03256c0245756f251a4c2bcf50586c"
+ },
+ {
+ "name": "muscle_tone5",
+ "unicode": "1F4AA-1F3FF",
+ "digest": "4e18708cbd61eaad288f913c86ad2d45108dd4484bc35879c5dcdd075eeb09fd"
+ },
+ {
+ "name": "mushroom",
+ "unicode": "1F344",
+ "digest": "a2b252cd759244409d9a8066470059948e2c50b8cc86b59821c1c86b5190f640"
+ },
+ {
+ "name": "musical_keyboard",
+ "unicode": "1F3B9",
+ "digest": "dcb3e84d27bfe373e5ea7ede457908de52002f0fd6105e9f3f5525c54d2a43dd"
+ },
+ {
+ "name": "musical_note",
+ "unicode": "1F3B5",
+ "digest": "76a0f598f8e251a9dab44f2e14f2b7a6fb0c0c351e0f37862c8c99d380f1c261"
+ },
+ {
+ "name": "musical_score",
+ "unicode": "1F3BC",
+ "digest": "a132c6b35236005b45c830a42fa97b454d3061c14991c6320f34807f10ba6a4a"
+ },
+ {
+ "name": "mute",
+ "unicode": "1F507",
+ "digest": "73a99b7f9e00f92cab78cd304dee4e893a112c3a6f2285c13d44916ea547458e"
+ },
+ {
+ "name": "nail_care",
+ "unicode": "1F485",
+ "digest": "62f721d3610d1647dba4b3f53cd4f2bc4180dae298314c2cca2a6a8ab1664525"
+ },
+ {
+ "name": "nail_care_tone1",
+ "unicode": "1F485-1F3FB",
+ "digest": "11b82ed2e6b6619c9b74702fdacfb0ddc91310191c8b89f355c7c69a72673f8f"
+ },
+ {
+ "name": "nail_care_tone2",
+ "unicode": "1F485-1F3FC",
+ "digest": "5195c76bccb9149d9080347d785dae2cce947bada5b198fae8c23e42f5553154"
+ },
+ {
+ "name": "nail_care_tone3",
+ "unicode": "1F485-1F3FD",
+ "digest": "50eab0bf825c5e00db07a3f5ad26b1bb221f54efb5c55549f392b2f5aec09e5a"
+ },
+ {
+ "name": "nail_care_tone4",
+ "unicode": "1F485-1F3FE",
+ "digest": "d05a9ccfad02191c89e4cbd00aa48fdaf908c0de6681f4a587d500be448e528f"
+ },
+ {
+ "name": "nail_care_tone5",
+ "unicode": "1F485-1F3FF",
+ "digest": "62466354dcf6717a8b9e942ca2c5ad15a26aa815c213e3b01faba9a2e302ecdd"
+ },
+ {
+ "name": "name_badge",
+ "unicode": "1F4DB",
+ "digest": "0a1cb0f7d489d3356a4d3e01f9faf78449d82d8ec4595c8639a55c3606c97c40"
+ },
+ {
+ "name": "necktie",
+ "unicode": "1F454",
+ "digest": "029e1140391ef559a9316021c2db94f05653751fdf9d8f366446467a70fee6df"
+ },
+ {
+ "name": "negative_squared_cross_mark",
+ "unicode": "274E",
+ "digest": "0ba0e705fdeac99edd712db31a8846320b9d2cf53c9cb4d4bcfd22ba4e1488ea"
+ },
+ {
+ "name": "nerd",
+ "unicode": "1F913",
+ "digest": "94efd551700aae8909b8dd7a78a54a33e070d24b2e0a10534353645084614e98"
+ },
+ {
+ "name": "network",
+ "unicode": "1F5A7",
+ "digest": "1dbaa54deeb2328fd8a3f044e450c97ac3ff39627c598bb2f4312d677482ee06"
+ },
+ {
+ "name": "neutral_face",
+ "unicode": "1F610",
+ "digest": "df01da8501e1f588049c8ed66e504e9abcce83f74ce5790f4d3dc547408f77ee"
+ },
+ {
+ "name": "new",
+ "unicode": "1F195",
+ "digest": "24e80abd29750d8b297335cdd4751b6250bb820560cf0392a6cc8783d34db63a"
+ },
+ {
+ "name": "new_moon",
+ "unicode": "1F311",
+ "digest": "2d697e431eac53d6e1ea367b5da03c15fc535cd7e8c214f801fe595b768a8e11"
+ },
+ {
+ "name": "new_moon_with_face",
+ "unicode": "1F31A",
+ "digest": "ea469a4668ded071f35e5898ae229fdb5d02b0730ce233169b83e22f81292baa"
+ },
+ {
+ "name": "newspaper",
+ "unicode": "1F4F0",
+ "digest": "0aaf6747a43fb60cd15e6e64ca0eccaade331b376c6fe6712fd5e8294e9868cc"
+ },
+ {
+ "name": "newspaper2",
+ "unicode": "1F5DE",
+ "digest": "0ca6b5850091f23295c970815a8e64a52e3c3dae492029ecb1e0726c2693f9bf"
+ },
+ {
+ "name": "ng",
+ "unicode": "1F196",
+ "digest": "4994c9b795033ed788e98c4af571a1dffe28c0a1479e3b42dcae21bb08381b5f"
+ },
+ {
+ "name": "night_with_stars",
+ "unicode": "1F303",
+ "digest": "56bb4a59a897c1836ee1a49cc99f468891b790b0f8bce203c201c13bb7b8ae9a"
+ },
+ {
+ "name": "nine",
+ "unicode": "0039-20E3",
+ "digest": "7e3644a98cb6417a351530c9ce6b368e637a22c847a8c04133897dc1c5d7419f"
+ },
+ {
+ "name": "no_bell",
+ "unicode": "1F515",
+ "digest": "f4fb42836132000101624fecef8b9358736a0fc76beae460e6986aaa479204fd"
+ },
+ {
+ "name": "no_bicycles",
+ "unicode": "1F6B3",
+ "digest": "b3c258bea7d6988640e3348598c03c97632ca00a11cbf0352995b801ff4a296b"
+ },
+ {
+ "name": "no_entry",
+ "unicode": "26D4",
+ "digest": "ac807d54092efdc3aea417790a7d0c50b59800c9ea49b37f1aec6d2e453c5f6d"
+ },
+ {
+ "name": "no_entry_sign",
+ "unicode": "1F6AB",
+ "digest": "5a17d677ec1c7595a7970a1cbe0d20909341b30d3ab31471ced590f51fff1ff7"
+ },
+ {
+ "name": "no_good",
+ "unicode": "1F645",
+ "digest": "8ce921e5e13e1203cf43fdc3e7c5ec1fb2a1f9ff79f21539cff542c80af2e5fe"
+ },
+ {
+ "name": "no_good_tone1",
+ "unicode": "1F645-1F3FB",
+ "digest": "aab4d354aaac06e8348eb354487c6381e475b44651cb2716660904a36c47a1b6"
+ },
+ {
+ "name": "no_good_tone2",
+ "unicode": "1F645-1F3FC",
+ "digest": "8fb66b1a7b8f72062794281294515d47471a8c59de300b99d656c3412ca19d64"
+ },
+ {
+ "name": "no_good_tone3",
+ "unicode": "1F645-1F3FD",
+ "digest": "aeecf73fb9dca24b4002db2802fc9b5a483644c49f834c19f143d4e56ec46c1a"
+ },
+ {
+ "name": "no_good_tone4",
+ "unicode": "1F645-1F3FE",
+ "digest": "fadeb23307d5ccabbf08c848cf81c66c05b152aa32b85f86061caf14760f8eb9"
+ },
+ {
+ "name": "no_good_tone5",
+ "unicode": "1F645-1F3FF",
+ "digest": "cf26d5d6463d0febf4e1f08e343308742ffe0811cfc30c459b87d4cc812f5d04"
+ },
+ {
+ "name": "no_mobile_phones",
+ "unicode": "1F4F5",
+ "digest": "3b4ead88beca33f1e303d0a45268849be7aaaff7830b761732c7a5afc5a2de3a"
+ },
+ {
+ "name": "no_mouth",
+ "unicode": "1F636",
+ "digest": "2af81a3e07a8b7827a1e58f6f5036ccff2f6e7b0027a4f934c9fa34c6a780963"
+ },
+ {
+ "name": "no_pedestrians",
+ "unicode": "1F6B7",
+ "digest": "9f9ed90bb8f9964fa8cb0048fc092ecc0612a1994c98d19ef0b5a58607888476"
+ },
+ {
+ "name": "no_smoking",
+ "unicode": "1F6AD",
+ "digest": "fb90290ff5c917b7307a97c8ba793d20c61042525cf2f7bfd4cd2a7878aeefa5"
+ },
+ {
+ "name": "non-potable_water",
+ "unicode": "1F6B1",
+ "digest": "c4ddca2ab1a97260e9b2c2aa33fb03455c0e8174541c3a9416fc44143a3ee567"
+ },
+ {
+ "name": "nose",
+ "unicode": "1F443",
+ "digest": "308e28b15b7f734f6f184ae367789d7cf258656b24861cf8d5935127d810aa3f"
+ },
+ {
+ "name": "nose_tone1",
+ "unicode": "1F443-1F3FB",
+ "digest": "392d24b38ac3edc2d7b83945a60bbe9115a6a97658d8af35281a7cbef79449e8"
+ },
+ {
+ "name": "nose_tone2",
+ "unicode": "1F443-1F3FC",
+ "digest": "409a790339c405770492e49fdb0b5ba34087c27e2f9018ecd845ab078e61476a"
+ },
+ {
+ "name": "nose_tone3",
+ "unicode": "1F443-1F3FD",
+ "digest": "92b52b479a935f31e460257d809c531edad1a6bb4583ad18233b12c4e45202fe"
+ },
+ {
+ "name": "nose_tone4",
+ "unicode": "1F443-1F3FE",
+ "digest": "78ad2e857792e86cded6ba5620f634f7d1f79a92c82c266e48fab9bd73df3688"
+ },
+ {
+ "name": "nose_tone5",
+ "unicode": "1F443-1F3FF",
+ "digest": "dbef6813c1965d3e93f70f33f118f9950130af21c622cea97ea215a36b4fa73f"
+ },
+ {
+ "name": "note",
+ "unicode": "1F5C9",
+ "digest": "073660fdaa02ecf98d04f61f8d65d6cc447ccae3825fccaff19a2c99ebba52af"
+ },
+ {
+ "name": "note_empty",
+ "unicode": "1F5C6",
+ "digest": "06b56eeaca6349bbcf1020bea98f937450a7e086db65cd5d7497748e0fb607be"
+ },
+ {
+ "name": "notebook",
+ "unicode": "1F4D3",
+ "digest": "64bd4a3e7ca7b22fc704c7b7bd4d13540c16bc69b9d8dd76e69e6ad573ab3823"
+ },
+ {
+ "name": "notebook_with_decorative_cover",
+ "unicode": "1F4D4",
+ "digest": "4b45f28fbde1be5c214a6bc2413abc91db02bccd86f74c21b7f4a4da8b75a46f"
+ },
+ {
+ "name": "notepad",
+ "unicode": "1F5CA",
+ "digest": "85069e2d13540886457368a57295072aec44c7137d9223bfcf908ce1f0e5124e"
+ },
+ {
+ "name": "notepad_empty",
+ "unicode": "1F5C7",
+ "digest": "8be5053e74c13d8220917c5aee1f4afdecb001612886438f283b0c2a0fecf6af"
+ },
+ {
+ "name": "notepad_spiral",
+ "unicode": "1F5D2",
+ "digest": "c181b6c1cc6063ec1848e46cbbf1d8b890c53b59cdc5218311ce06889570e727"
+ },
+ {
+ "name": "notes",
+ "unicode": "1F3B6",
+ "digest": "bf3868386e17eac40ac7fbabea027042027ff061daafe406c869cdd8ce94641d"
+ },
+ {
+ "name": "nut_and_bolt",
+ "unicode": "1F529",
+ "digest": "fdb9d7408202fad7a52ff21608042c08c3b0beb195999fff233df36a29dc9e96"
+ },
+ {
+ "name": "o",
+ "unicode": "2B55",
+ "digest": "8e119dba4130bd33b3ee5c862fb4fa5a691173911ffee51cb9359fee3398e330"
+ },
+ {
+ "name": "o2",
+ "unicode": "1F17E",
+ "digest": "00d751124c25633611055bd61e74fc3f3d1779f0d09e1e707837686f613367b4"
+ },
+ {
+ "name": "ocean",
+ "unicode": "1F30A",
+ "digest": "9b1fbfd2a64f417d0c2cb91085b29a12d14e15844bc21798bdee938bb7bf6222"
+ },
+ {
+ "name": "octopus",
+ "unicode": "1F419",
+ "digest": "3fdfbc02f47ad434bdeb7f3a15cd4e8f8118ee1cd754627e358f1c2f4616f5e3"
+ },
+ {
+ "name": "oden",
+ "unicode": "1F362",
+ "digest": "afed1c5166943e5803602ffacc67652e3b29ee4222a6c36aba2daf88bd21ad3c"
+ },
+ {
+ "name": "office",
+ "unicode": "1F3E2",
+ "digest": "dc1836ef152d88fd628df18db770594f5dbc8d7f20d6ce982588b25b78b19c92"
+ },
+ {
+ "name": "oil",
+ "unicode": "1F6E2",
+ "digest": "f8b7626cb09e229203105b9c8c7f3fbb38c0650021092fc50115ad517248644a"
+ },
+ {
+ "name": "ok",
+ "unicode": "1F197",
+ "digest": "6b05bbab4a7104541c2f4bce553884d17ae0ad07589b19d6b53b6949c14f2269"
+ },
+ {
+ "name": "ok_hand",
+ "unicode": "1F44C",
+ "digest": "9981f32ef200b011a10f6bfa2066c41b6b5e7bcd6c3c21647980b640bc1fa93b"
+ },
+ {
+ "name": "ok_hand_tone1",
+ "unicode": "1F44C-1F3FB",
+ "digest": "e5933a9b64b03ce0634f15f02ff7b6424530dbdc0e283461e0c9992d0c2ca2ad"
+ },
+ {
+ "name": "ok_hand_tone2",
+ "unicode": "1F44C-1F3FC",
+ "digest": "4c04741c9f2c8731da8df3015e9aae00061a01848c2d22aab1e9853c271deed3"
+ },
+ {
+ "name": "ok_hand_tone3",
+ "unicode": "1F44C-1F3FD",
+ "digest": "216dc5a72f9e34bbb7b39f680c388bd5b52abf9b41b843342e53e285b7933076"
+ },
+ {
+ "name": "ok_hand_tone4",
+ "unicode": "1F44C-1F3FE",
+ "digest": "7139de7ec9d5a962cf87b9fbbeef3a53aa482bb840ab3b64d8d0da81bdc19886"
+ },
+ {
+ "name": "ok_hand_tone5",
+ "unicode": "1F44C-1F3FF",
+ "digest": "e18b0a1bc5d970cc63466bd6da6e9f855db37d1eada3230d19f600c1f5a402a3"
+ },
+ {
+ "name": "ok_woman",
+ "unicode": "1F646",
+ "digest": "3b2fa732d9c9addb056f136192428e99d805d4cb1c7dab724fd552c7e93197e4"
+ },
+ {
+ "name": "ok_woman_tone1",
+ "unicode": "1F646-1F3FB",
+ "digest": "017aca3797701b043a44f22e67dcad8b531a3ca14e629ae0d2fbc601ed3e49cb"
+ },
+ {
+ "name": "ok_woman_tone2",
+ "unicode": "1F646-1F3FC",
+ "digest": "036bed032bc5a616668775cda0d5640c810e2836aa28009c8e8bf2b487259c59"
+ },
+ {
+ "name": "ok_woman_tone3",
+ "unicode": "1F646-1F3FD",
+ "digest": "d9a4414caddda43d1a36828cfbecce5f2b7e5c1b67b4a47991b2ae0a34cf7ab7"
+ },
+ {
+ "name": "ok_woman_tone4",
+ "unicode": "1F646-1F3FE",
+ "digest": "942e1b9aa495c4c4de0804e4d4348422201299d649e5d65829ba4a308880df1c"
+ },
+ {
+ "name": "ok_woman_tone5",
+ "unicode": "1F646-1F3FF",
+ "digest": "e8d0fb5b999d5d63404493aa505b5af2260c76001023431d5e788773d0a9e2de"
+ },
+ {
+ "name": "older_man",
+ "unicode": "1F474",
+ "digest": "620f763325827acbeb9d57798ef55d87827d0dfc77b84d942e25bc5057f2cbfe"
+ },
+ {
+ "name": "older_man_tone1",
+ "unicode": "1F474-1F3FB",
+ "digest": "e0f35c12362eae503d1c30a345c3a4978196d351d8a1eb9d5f107c60ea4bbf52"
+ },
+ {
+ "name": "older_man_tone2",
+ "unicode": "1F474-1F3FC",
+ "digest": "671766ce9fa47c3fa009d4f138344c87d73032a1c38e48614c663f8ea5d0f673"
+ },
+ {
+ "name": "older_man_tone3",
+ "unicode": "1F474-1F3FD",
+ "digest": "6ff4885ef8c416b8970780a691fef74c8d89421ab11e0aa8c522c33e1c67fbe8"
+ },
+ {
+ "name": "older_man_tone4",
+ "unicode": "1F474-1F3FE",
+ "digest": "0ae7d4e316dcd4d27a5a6cdaabab88a4f992bd1b75f6ceaeb5b906ed1eb5269c"
+ },
+ {
+ "name": "older_man_tone5",
+ "unicode": "1F474-1F3FF",
+ "digest": "abe2757bd5e35f30d2a6daec09637ea5382a46d14d239b77282e9bf874229b57"
+ },
+ {
+ "name": "older_woman",
+ "unicode": "1F475",
+ "digest": "3ed599443eed25399aac999fc234c9e97f8fb6ec567e37a553c26e01021b097c"
+ },
+ {
+ "name": "older_woman_tone1",
+ "unicode": "1F475-1F3FB",
+ "digest": "7421c5dba67cfd1eeabb2fa8faf4aa0d615d23f191cf7d7c0ad9c1fa884edfda"
+ },
+ {
+ "name": "older_woman_tone2",
+ "unicode": "1F475-1F3FC",
+ "digest": "65edeef25648ac7f8be535df06af1286441691fa15176e99a6e83fc779aa2cde"
+ },
+ {
+ "name": "older_woman_tone3",
+ "unicode": "1F475-1F3FD",
+ "digest": "5d27bbcc5796227a9caec1c7612d3f691055655b96f7303e420839463d76c269"
+ },
+ {
+ "name": "older_woman_tone4",
+ "unicode": "1F475-1F3FE",
+ "digest": "75b858e910175fc0233503d672120fd43ac035ba3fd2052fbb44df39f6e3695c"
+ },
+ {
+ "name": "older_woman_tone5",
+ "unicode": "1F475-1F3FF",
+ "digest": "9da1cf10a605c470877d7f4a840f99344b1ec2e7b1ec7db61e930cde77025e3b"
+ },
+ {
+ "name": "om_symbol",
+ "unicode": "1F549",
+ "digest": "c8c1c9d445b1fc50a627b71bee21fba978e04532e4685ec032a0174f51fc12bb"
+ },
+ {
+ "name": "on",
+ "unicode": "1F51B",
+ "digest": "08e1159a68d3334a87ffa75b9e70826cb557d0f73a2c1d08f4c3d60476ecacc8"
+ },
+ {
+ "name": "oncoming_automobile",
+ "unicode": "1F698",
+ "digest": "6bff7f40fe223df6d16c7512532b8aa6f83e8c13e1007b63eb9aabf774c1a322"
+ },
+ {
+ "name": "oncoming_bus",
+ "unicode": "1F68D",
+ "digest": "127a357fcd96ce4b9ab11c3dba95d8ff811bab193dd8ba38efb7067a44752ce8"
+ },
+ {
+ "name": "oncoming_police_car",
+ "unicode": "1F694",
+ "digest": "57cb70e05e70c1f68ab42259f307ed9782c2b9d6e35d2dff2895aa23d7eb6b04"
+ },
+ {
+ "name": "oncoming_taxi",
+ "unicode": "1F696",
+ "digest": "174967ae4c3d5881d2408c71c020f704e933190af4caef5d2908e9ac382f35ea"
+ },
+ {
+ "name": "one",
+ "unicode": "0031-20E3",
+ "digest": "113b9d87c3e37c9c54e49cecccbfc40c15fb97fd03a51505df85e48b78702b2b"
+ },
+ {
+ "name": "open_file_folder",
+ "unicode": "1F4C2",
+ "digest": "def93715203aed464211798d773732895a19389a94a2e7ed43e7f229b2aab7da"
+ },
+ {
+ "name": "open_hands",
+ "unicode": "1F450",
+ "digest": "7c60a37ae11727c998908199b8709e52593b931843aef942f37b306b1edca12a"
+ },
+ {
+ "name": "open_hands_tone1",
+ "unicode": "1F450-1F3FB",
+ "digest": "09ffa9b3f28fc56a71e4e711bdfc87ce1a56721229377e71f1c00224523f8b9b"
+ },
+ {
+ "name": "open_hands_tone2",
+ "unicode": "1F450-1F3FC",
+ "digest": "21ecaba9f086bcb7eb07c17c2b2621bcd1ca28c57f79032d5e0eba356494cc85"
+ },
+ {
+ "name": "open_hands_tone3",
+ "unicode": "1F450-1F3FD",
+ "digest": "c7dbb8c44f78f7793b202ec215fee42b7e1e555d659fbf402383500217b89656"
+ },
+ {
+ "name": "open_hands_tone4",
+ "unicode": "1F450-1F3FE",
+ "digest": "867451d42492ab2277687447f421f744530b9ea057312326353fec39c94b18fd"
+ },
+ {
+ "name": "open_hands_tone5",
+ "unicode": "1F450-1F3FF",
+ "digest": "56335506cf68e29150cb68d7ebbb4a92aed390018966669a8144d20ae0d6cfe3"
+ },
+ {
+ "name": "open_mouth",
+ "unicode": "1F62E",
+ "digest": "f05fdf998e8b5c0b00ebd8b5ab17a67f5c0a45275f31a201af74e8ab0c2f7ba9"
+ },
+ {
+ "name": "ophiuchus",
+ "unicode": "26CE",
+ "digest": "98c61bb0c36d60c476d42d5e074297662e8d141dcab7004a5bd63c359eed3b84"
+ },
+ {
+ "name": "optical_disk",
+ "unicode": "1F5B8",
+ "digest": "df8c10028d29d65f144a6b789d1c3294e7b3293554c4c30d28d72dc7ba8d9a5d"
+ },
+ {
+ "name": "orange_book",
+ "unicode": "1F4D9",
+ "digest": "86d150ea3d62183ab7dfe2851cf7f4d1ae769b7ecbb1987b0f463e639e429598"
+ },
+ {
+ "name": "orthodox_cross",
+ "unicode": "2626",
+ "digest": "9c861285ca6d699cd2c72b6df44ec2b1e64138152f19c66e32df1ce770ff2e83"
+ },
+ {
+ "name": "outbox_tray",
+ "unicode": "1F4E4",
+ "digest": "b6a6015d5d7d528af485de23ff4518dc35408def1cc49bc6c9b01d880d613985"
+ },
+ {
+ "name": "ox",
+ "unicode": "1F402",
+ "digest": "cbcfe5c8c4d6b939e24e18e610785f171bb9410441e02c2eeb1bceb0a6246daf"
+ },
+ {
+ "name": "package",
+ "unicode": "1F4E6",
+ "digest": "4023cffce85384217a73609f457aec013876e689c44bcfff0bcc35f3e4e1ab00"
+ },
+ {
+ "name": "page",
+ "unicode": "1F5CF",
+ "digest": "cc745056525f59d9128d1d03b14770376bb09ab64b8ef4ac994ab7f38efd4783"
+ },
+ {
+ "name": "page_facing_up",
+ "unicode": "1F4C4",
+ "digest": "71a0872bf1b13c58746f9b41655227c75be107ab6083c0dce13cb16444af22e7"
+ },
+ {
+ "name": "page_with_curl",
+ "unicode": "1F4C3",
+ "digest": "cb4210464faea946c7b07db7067c7fc98920f778cf57721388f5362942ba3029"
+ },
+ {
+ "name": "pager",
+ "unicode": "1F4DF",
+ "digest": "209dbdc19aa650ecacc0569e17a9123c9a1e39df59c9b4120f3b0888b63cd6f1"
+ },
+ {
+ "name": "pages",
+ "unicode": "1F5D0",
+ "digest": "05bd47b78f089389356d9d839c736843f56b959ab4277056606ffcbb013390bc"
+ },
+ {
+ "name": "paintbrush",
+ "unicode": "1F58C",
+ "digest": "73eb33184f5f495d6c2699fafc1a8680069f82a70fbe519290c3a2ce30d1aee9"
+ },
+ {
+ "name": "palm_tree",
+ "unicode": "1F334",
+ "digest": "1589ff4b1b87296edc0118e4aa67b3b504ed85a5b8d47e7d0c3e309d0bbf8cd6"
+ },
+ {
+ "name": "panda_face",
+ "unicode": "1F43C",
+ "digest": "050ee87892f56ff485f460bc6c3846d98a0ca7083d2cf0b8ab24772b672273f2"
+ },
+ {
+ "name": "paperclip",
+ "unicode": "1F4CE",
+ "digest": "1463607a59345973f009fa53a719e2264b95743560adb99737bef29b1d133a95"
+ },
+ {
+ "name": "paperclips",
+ "unicode": "1F587",
+ "digest": "7071e031f4a100c3cb3573fbfa375360043f0276289a0818f2ffaf71b3580040"
+ },
+ {
+ "name": "park",
+ "unicode": "1F3DE",
+ "digest": "d257f0f1b1a0134573f80ba1a5f522a91c320ee7f93a1cb64877c077e7e19b50"
+ },
+ {
+ "name": "parking",
+ "unicode": "1F17F",
+ "digest": "e1d2cfd1c57ea85003ca4df066cbba4e506bf6c4d6c790e27b2f78ad8443fabf"
+ },
+ {
+ "name": "part_alternation_mark",
+ "unicode": "303D",
+ "digest": "b3cc2e803b255e858417345ba6ba52a1c22f511b483fec11b5d68c4432f759b6"
+ },
+ {
+ "name": "partly_sunny",
+ "unicode": "26C5",
+ "digest": "484990f5e1a3b14c731e7bd4b0b4a1c10cd5fb54ac7cf2751f40c8bf59d7e2b4"
+ },
+ {
+ "name": "passport_control",
+ "unicode": "1F6C2",
+ "digest": "224e8ef60d4d6587721727555de324948fb5b6c1cb5cc4b546960983d1ec85c4"
+ },
+ {
+ "name": "pause_button",
+ "unicode": "23F8",
+ "digest": "edd605ffaa39a7905ed0958b7cc69f00f5b271e579198d2df1746ad1b3648272"
+ },
+ {
+ "name": "peace",
+ "unicode": "262E",
+ "digest": "e0ee8a5c9fb18d5db6841b21527ed8fd955abdff9ffdb7b2684dca22107015fc"
+ },
+ {
+ "name": "peach",
+ "unicode": "1F351",
+ "digest": "a3f4fd5ff02e0a03104ab54456ee1a7521858ee68443856ee10e0972e5b6aaa5"
+ },
+ {
+ "name": "pear",
+ "unicode": "1F350",
+ "digest": "7a7a72568d53677cd1fff4d9e58e63327a742fa16d22a2bef03b4a6fa378d3b3"
+ },
+ {
+ "name": "pen_ballpoint",
+ "unicode": "1F58A",
+ "digest": "6becdc6f622c774bb09b7e7592bba2123ecccc9de32a35f0b18b50d7d54109cb"
+ },
+ {
+ "name": "pen_fountain",
+ "unicode": "1F58B",
+ "digest": "8c78cf0c2bd1d5e309d2d3356ff207e3fc76ca18dd6b90762cb62f6afbc95c6a"
+ },
+ {
+ "name": "pencil",
+ "unicode": "1F4DD",
+ "digest": "62b7ee5d9352114d09ee6f2c9a4c5e8b79f775a6c509e82ddfcdd61e13716249"
+ },
+ {
+ "name": "pencil2",
+ "unicode": "270F",
+ "digest": "aa2c572772187fee1f9125bb0950f5ce8a61f7dd2647258c40b4077ee5feb498"
+ },
+ {
+ "name": "pencil3",
+ "unicode": "1F589",
+ "digest": "52c1ba1228917eb491ac1745a495e0fdafba6b985a81caba250f71d1f94c725c"
+ },
+ {
+ "name": "penguin",
+ "unicode": "1F427",
+ "digest": "095de34b3f6a2521a342c21f5f2551a0092bf47429801c15b7bbf0913924f412"
+ },
+ {
+ "name": "pennant_black",
+ "unicode": "1F3F2",
+ "digest": "cd3c33bfc3c7fbe84b98d2d481d56a7bf5488ff94afadd8b5a0e454768b80269"
+ },
+ {
+ "name": "pennant_white",
+ "unicode": "1F3F1",
+ "digest": "818b1be73540f2cfeb1c514e1ee75d18715af317f0db817d9ae081b9ea33d4b0"
+ },
+ {
+ "name": "pensive",
+ "unicode": "1F614",
+ "digest": "2d9e7f1eed14dcc86674cec78e992567a40d0f223fc67d722b91eebcd1251269"
+ },
+ {
+ "name": "performing_arts",
+ "unicode": "1F3AD",
+ "digest": "a202755bab6427433975589bb8b63e61e5d7f55c6242676d8000e91eedabc55e"
+ },
+ {
+ "name": "persevere",
+ "unicode": "1F623",
+ "digest": "686ef3fc70ce8294d02a764ebd75b69f25cca6bff6b92e7905130366d22f6d8a"
+ },
+ {
+ "name": "person_frowning",
+ "unicode": "1F64D",
+ "digest": "16e8fbf22c0b4c237d0d45202fa32d1ebd04760a5b6975c9c9b477321ccb0e12"
+ },
+ {
+ "name": "person_frowning_tone1",
+ "unicode": "1F64D-1F3FB",
+ "digest": "a143b865976ce3cf307db854cfd1ca58c3832df0eee5e9b0ab307cf4f24ba3db"
+ },
+ {
+ "name": "person_frowning_tone2",
+ "unicode": "1F64D-1F3FC",
+ "digest": "4e7050d8a38019ba2293f66b9930e6a7e35dacf3b3bc9431edb586a0d9ea8054"
+ },
+ {
+ "name": "person_frowning_tone3",
+ "unicode": "1F64D-1F3FD",
+ "digest": "0750015d3ac1b5954d31e36cd59c70b6ed9f4df698082484b7ac59eb0b9964b0"
+ },
+ {
+ "name": "person_frowning_tone4",
+ "unicode": "1F64D-1F3FE",
+ "digest": "18d6cc92d0990624218d38d6eeed60bccb371d0fc9f1c889e9476b3b0c44b5e8"
+ },
+ {
+ "name": "person_frowning_tone5",
+ "unicode": "1F64D-1F3FF",
+ "digest": "4a898199cbaf083d37511f51d8a1d2560b7a20c62a1b09087831da7010fbd093"
+ },
+ {
+ "name": "person_with_blond_hair",
+ "unicode": "1F471",
+ "digest": "67d95a0801c65f62db55fa80ab35dec65c239601a44bf5f5902e4645f126770e"
+ },
+ {
+ "name": "person_with_blond_hair_tone1",
+ "unicode": "1F471-1F3FB",
+ "digest": "e79717bfe30a26eafc082a75fa7547d8f2ad3c123fb2d75a95e75f0ce7ecbd0c"
+ },
+ {
+ "name": "person_with_blond_hair_tone2",
+ "unicode": "1F471-1F3FC",
+ "digest": "c4a1961c292149ab6e1fd54a7894398599bf855de97a05ee4e836a86a400deb3"
+ },
+ {
+ "name": "person_with_blond_hair_tone3",
+ "unicode": "1F471-1F3FD",
+ "digest": "e2707d0cf778bee5b72d861ec76430eb1cf9f9820f066ee6327574d5697f445e"
+ },
+ {
+ "name": "person_with_blond_hair_tone4",
+ "unicode": "1F471-1F3FE",
+ "digest": "94da43f0b12ef4a98dabec096ff1184b0a9b5b6ee55824d257e5112cc7e88730"
+ },
+ {
+ "name": "person_with_blond_hair_tone5",
+ "unicode": "1F471-1F3FF",
+ "digest": "9e096a210ea720d32bc6a7005cd77f8b314ccf817fc3060da2e1796de39e9d60"
+ },
+ {
+ "name": "person_with_pouting_face",
+ "unicode": "1F64E",
+ "digest": "8c3199a422250d2db9a163156191ed2c6697d7f31699e2efe19e05ca26e5d225"
+ },
+ {
+ "name": "person_with_pouting_face_tone1",
+ "unicode": "1F64E-1F3FB",
+ "digest": "3e1f09bbf607381c992739ea92dd35cbd26b1bbc705a7d21b7c3156f50e9d8b3"
+ },
+ {
+ "name": "person_with_pouting_face_tone2",
+ "unicode": "1F64E-1F3FC",
+ "digest": "b5fc1cf3fdc5ff01105ee2452db90baa6a52c1e42f3795b2836c3e35197ece1f"
+ },
+ {
+ "name": "person_with_pouting_face_tone3",
+ "unicode": "1F64E-1F3FD",
+ "digest": "e8ec2539c458a8283c8c1050634c432b6363f3e64b68ba4c977994782f09b564"
+ },
+ {
+ "name": "person_with_pouting_face_tone4",
+ "unicode": "1F64E-1F3FE",
+ "digest": "5cab7a29699decd45682583446c2bf56ddcd69cd16e14db661b526a4076dfa17"
+ },
+ {
+ "name": "person_with_pouting_face_tone5",
+ "unicode": "1F64E-1F3FF",
+ "digest": "3caebd3626fd77d849859d1c99a747f80a2b59bfa5c1854494f1ce0485539a94"
+ },
+ {
+ "name": "pick",
+ "unicode": "26CF",
+ "digest": "24a3e8f592435b97272e6d134ea5503dce3012811659c4aadbad4e45d9fba679"
+ },
+ {
+ "name": "pig",
+ "unicode": "1F437",
+ "digest": "50b55fc74e8f6c89c6e04609381c99a660748908f0ef015f5da37089678ad0c3"
+ },
+ {
+ "name": "pig2",
+ "unicode": "1F416",
+ "digest": "e8189fb678608e8b9d69e11d2566f9a4765cbdff99ec8e66df30c7a2dabf742f"
+ },
+ {
+ "name": "pig_nose",
+ "unicode": "1F43D",
+ "digest": "7e299cb49a771884f5065c68733a5a1fe354a54cff009127230177f1717af4a5"
+ },
+ {
+ "name": "pill",
+ "unicode": "1F48A",
+ "digest": "53ae3379cc6721744979122569f157a5a13aa6b48e081a89f17b2d90134efe9e"
+ },
+ {
+ "name": "pineapple",
+ "unicode": "1F34D",
+ "digest": "ceda8ffa4a41594f28a4e69d03f8a1daeb2ba20740f0b8c56447cae833eea035"
+ },
+ {
+ "name": "ping_pong",
+ "unicode": "1F3D3",
+ "digest": "dd2a84716c93410a285ff759bfbc2dc31a10f90b203c7a657b908e5949e89a39"
+ },
+ {
+ "name": "piracy",
+ "unicode": "1F572",
+ "digest": "f42955ba75c598392e5e258be49968d858c876e0d6e7aa9dc795f7e8cff42be9"
+ },
+ {
+ "name": "pisces",
+ "unicode": "2653",
+ "digest": "75f11b9a094196b54a242420362fa7c0aeba7cfc497b187e1aaaba96d93684a7"
+ },
+ {
+ "name": "pizza",
+ "unicode": "1F355",
+ "digest": "ac94ae1c034f7b854ce2a483e1c219d101a84336f5065342f4824ff32ba705c4"
+ },
+ {
+ "name": "place_of_worship",
+ "unicode": "1F6D0",
+ "digest": "4fabc307b7e35f94288f6d53985485662a4814b11a9a382f0a3873d41b1290d3"
+ },
+ {
+ "name": "play_pause",
+ "unicode": "23EF",
+ "digest": "d69e8cdec33447283cf65d343b986115e27681d781b721db7894e5c587ca18ad"
+ },
+ {
+ "name": "point_down",
+ "unicode": "1F447",
+ "digest": "685f46a643be7f3033896e59a822f87d61ce50db6969bcdbacc743215a96bb7a"
+ },
+ {
+ "name": "point_down_tone1",
+ "unicode": "1F447-1F3FB",
+ "digest": "d3dd2608fe17d5649c960fcf8dbdb68466908d80fa349b7947b457da2a27ebb1"
+ },
+ {
+ "name": "point_down_tone2",
+ "unicode": "1F447-1F3FC",
+ "digest": "67ab236a14f6d63abcdb26433a66a183d223186c21ebc9f978fab50165ebe271"
+ },
+ {
+ "name": "point_down_tone3",
+ "unicode": "1F447-1F3FD",
+ "digest": "c8a2368f2cedb5bbb5cc0195b97fbf3787747637bf6e77bdc9a4edf4a3f22a04"
+ },
+ {
+ "name": "point_down_tone4",
+ "unicode": "1F447-1F3FE",
+ "digest": "6a92eab3bc8f950fa423e690f54a352887bda92f01e91c62eb3f3a9544c10cd8"
+ },
+ {
+ "name": "point_down_tone5",
+ "unicode": "1F447-1F3FF",
+ "digest": "6ad329f156414f421d6f8cf5e2a68d34b7a41f90d80e8e66b15bcbd3788126c7"
+ },
+ {
+ "name": "point_left",
+ "unicode": "1F448",
+ "digest": "cb520d6bba4c2b3bd7911315c9efce3261d048ff090437d7e24c9c5a7255043e"
+ },
+ {
+ "name": "point_left_tone1",
+ "unicode": "1F448-1F3FB",
+ "digest": "81813901bdaa8d261277f79aff9e9a21beb80a5855899941820b25f70786ec21"
+ },
+ {
+ "name": "point_left_tone2",
+ "unicode": "1F448-1F3FC",
+ "digest": "ecdc3dea0d644290aa7e0dab758c215822482a482ba35d825a33152453593c1e"
+ },
+ {
+ "name": "point_left_tone3",
+ "unicode": "1F448-1F3FD",
+ "digest": "84e73b6a37755016271c255eba164f349dbd2a2badf5d9ac1c6f4cbfcae589f0"
+ },
+ {
+ "name": "point_left_tone4",
+ "unicode": "1F448-1F3FE",
+ "digest": "d16800499b6c6ede94256796b1de8a8f723879f636849856b3bd8b7a092b5576"
+ },
+ {
+ "name": "point_left_tone5",
+ "unicode": "1F448-1F3FF",
+ "digest": "18b7108066cebf2d4090f29e595a2f01db94bd210f3b1d61dc269ec249a749b9"
+ },
+ {
+ "name": "point_right",
+ "unicode": "1F449",
+ "digest": "866180bf31e92de32aba336d5b5ce81773a29cdaadada1d93c944cf9ad9783bc"
+ },
+ {
+ "name": "point_right_tone1",
+ "unicode": "1F449-1F3FB",
+ "digest": "ebe2e4bf6bd46a5798b9a845a4ed055911c4fe58dbeacc4d39d6ea63e28e7cc9"
+ },
+ {
+ "name": "point_right_tone2",
+ "unicode": "1F449-1F3FC",
+ "digest": "b638662a67b1c6adde4f5abc789aae010b178404cdd1b71fcc982cdf8307c655"
+ },
+ {
+ "name": "point_right_tone3",
+ "unicode": "1F449-1F3FD",
+ "digest": "32c6ca2f992416ab2c36672dfbc1c0de8f102c77a13496dd8d63736a7b0261d2"
+ },
+ {
+ "name": "point_right_tone4",
+ "unicode": "1F449-1F3FE",
+ "digest": "89bd6828e9b82408a3829d49fa43332e2599f7d10bc6e5b14b750ef03267b173"
+ },
+ {
+ "name": "point_right_tone5",
+ "unicode": "1F449-1F3FF",
+ "digest": "390525048a12b0efa22de550c800e439b0deaad03f1f31155d179aef093354af"
+ },
+ {
+ "name": "point_up",
+ "unicode": "261D",
+ "digest": "31b5ca1303c1afabe1db322b24f73b23f3568c87a364f61c82f6e0c858c090e9"
+ },
+ {
+ "name": "point_up_2",
+ "unicode": "1F446",
+ "digest": "55c237054aa347c9847f3f3f577eb755db55dfcf793aa7de0f8f868574d70e8f"
+ },
+ {
+ "name": "point_up_2_tone1",
+ "unicode": "1F446-1F3FB",
+ "digest": "dc07e7732d973de96ae3b08b14c19e20b6c1aea7f5a30e7198679b750422e914"
+ },
+ {
+ "name": "point_up_2_tone2",
+ "unicode": "1F446-1F3FC",
+ "digest": "af2211fc4a1bd51d1e76f7bc43a6fa87bdd24e4295c52fdbdb01c1ca670a6cd7"
+ },
+ {
+ "name": "point_up_2_tone3",
+ "unicode": "1F446-1F3FD",
+ "digest": "917701169b3fb3e1b6e14a68e9572b25998ef2e38abac9ad8cf30100f8ea0dac"
+ },
+ {
+ "name": "point_up_2_tone4",
+ "unicode": "1F446-1F3FE",
+ "digest": "20843904764c6c3e55792cce0c55c76f72b97788c5229cad655ebf1f2873b439"
+ },
+ {
+ "name": "point_up_2_tone5",
+ "unicode": "1F446-1F3FF",
+ "digest": "1d0cca546027c717da50f90da65757af46fe7cd4e397da9b8e203446f707208d"
+ },
+ {
+ "name": "point_up_tone1",
+ "unicode": "261D-1F3FB",
+ "digest": "5ede60379dee23166c6b834d73da8b55268e330f67058843b8a3705dca6ed71a"
+ },
+ {
+ "name": "point_up_tone2",
+ "unicode": "261D-1F3FC",
+ "digest": "c94a15ef848d410aa5d32b8d0e453b59682fde6f39e6705cbb81cf0829833a81"
+ },
+ {
+ "name": "point_up_tone3",
+ "unicode": "261D-1F3FD",
+ "digest": "d319ce72876d97a3b1d4bc7c0679e546a983f02145d723a0da5ed0b73a51cfe7"
+ },
+ {
+ "name": "point_up_tone4",
+ "unicode": "261D-1F3FE",
+ "digest": "9171a27f86f27fd144347a17153fb56e30bd32e67a8f10f8c1f32a40cad4e009"
+ },
+ {
+ "name": "point_up_tone5",
+ "unicode": "261D-1F3FF",
+ "digest": "a894f87da4c3d33d5e6e74d003a33ec60c453db6507fe05d22235f807ead27d6"
+ },
+ {
+ "name": "police_car",
+ "unicode": "1F693",
+ "digest": "7999869cb75be404fc34942b6f9d8e84fa7e259aa892a1e8e1652a5f02cceea6"
+ },
+ {
+ "name": "poodle",
+ "unicode": "1F429",
+ "digest": "8a568d8688bf19b440b7c1b49fcfe6672b8f75af0031d89ab6212623430acadb"
+ },
+ {
+ "name": "poop",
+ "unicode": "1F4A9",
+ "digest": "140ce75a015ede5e764873e0ae9a56e7b2af333eddca0fe2796b14545c620258"
+ },
+ {
+ "name": "popcorn",
+ "unicode": "1F37F",
+ "digest": "12264cb16fca9317e3ba8d5924a2c8f15f790e36d2f29e7b12aaaf77e1beb73d"
+ },
+ {
+ "name": "post_office",
+ "unicode": "1F3E3",
+ "digest": "5e2d896cd646a2eecd5596af9e44ca1fa2745de5cedaf0f6d193b8243201c6cc"
+ },
+ {
+ "name": "postal_horn",
+ "unicode": "1F4EF",
+ "digest": "339aa61fa1567a1d159bb8204d15db889fbb6cc1106f6e1991b4a184d1bc1fc7"
+ },
+ {
+ "name": "postbox",
+ "unicode": "1F4EE",
+ "digest": "ef1a6543fccb9f1009cc3782c51883e51167721a0b49e8ba21e8e6049b216906"
+ },
+ {
+ "name": "potable_water",
+ "unicode": "1F6B0",
+ "digest": "4a2379835660dfa8b6780d662a10d1effab710f471eb9b5e6ade4772ba7e5aeb"
+ },
+ {
+ "name": "pouch",
+ "unicode": "1F45D",
+ "digest": "cbd47ec1a65f5c642773d8ea2e7e57f7041a2d7ed9df05fbdd7bc8743c6dece6"
+ },
+ {
+ "name": "poultry_leg",
+ "unicode": "1F357",
+ "digest": "d416e9464bd58073bd3e32eb06c0da96905609f47b9d667acdc0810e94237584"
+ },
+ {
+ "name": "pound",
+ "unicode": "1F4B7",
+ "digest": "1ac491bb8a91613b2b1faaac4e7b4bc794d2abef69ac79de17d54c824c3ef826"
+ },
+ {
+ "name": "pouting_cat",
+ "unicode": "1F63E",
+ "digest": "ba28d75401d5bb98773acd35aaf173356bae4d5a5520a226559478138364ebdf"
+ },
+ {
+ "name": "pray",
+ "unicode": "1F64F",
+ "digest": "fb0df9c1566014bd2df2a1afd59366b896f20c03ca3516e02e4be44ea556c8ea"
+ },
+ {
+ "name": "pray_tone1",
+ "unicode": "1F64F-1F3FB",
+ "digest": "c6d8cb46e65ad13a92e85f97e018176fd89513f23e899e15d1ad09e3b4009f4b"
+ },
+ {
+ "name": "pray_tone2",
+ "unicode": "1F64F-1F3FC",
+ "digest": "2cd68cbe1ba3254f173ec8136af79cae64873bd0f20480158c3e6babd5a1a442"
+ },
+ {
+ "name": "pray_tone3",
+ "unicode": "1F64F-1F3FD",
+ "digest": "d2e81863f74a87b96335fb108e7b206f28ed18185362ab4d42a3b0523801398b"
+ },
+ {
+ "name": "pray_tone4",
+ "unicode": "1F64F-1F3FE",
+ "digest": "ad1b91254b101d872325c325ebd1f2a6257cfe22e83de88e29dd16ffac191979"
+ },
+ {
+ "name": "pray_tone5",
+ "unicode": "1F64F-1F3FF",
+ "digest": "23f40a11321decbdc6a1d274b9ad571041d261d364d13d1063c306e73ad52254"
+ },
+ {
+ "name": "prayer_beads",
+ "unicode": "1F4FF",
+ "digest": "cb6f8700154f75749cf2642a25c03e255dc18428baf8b57f6bd807c92b83e28d"
+ },
+ {
+ "name": "princess",
+ "unicode": "1F478",
+ "digest": "47b93eb52d757c3c000d9760391ecb942776d883b28050d833fa11612483d8ee"
+ },
+ {
+ "name": "princess_tone1",
+ "unicode": "1F478-1F3FB",
+ "digest": "1e4073c2abdf51a61a1a85a3e063541fe96e9b9ec36ec6f7fb9c98deeb230869"
+ },
+ {
+ "name": "princess_tone2",
+ "unicode": "1F478-1F3FC",
+ "digest": "6a0a5dc447cd887798f908c15972e7a12d28d81f168b92bcb105786ac253bea0"
+ },
+ {
+ "name": "princess_tone3",
+ "unicode": "1F478-1F3FD",
+ "digest": "2f08d22fdfc7a7d66fcd87ae716b811f43077f5bb17fef87f5b7e2aa93700d70"
+ },
+ {
+ "name": "princess_tone4",
+ "unicode": "1F478-1F3FE",
+ "digest": "02129211bf7bf7ff6de35913b7069aee151532d878b8c4f7e24c012e5b09d4b4"
+ },
+ {
+ "name": "princess_tone5",
+ "unicode": "1F478-1F3FF",
+ "digest": "d676f103600b69dbfdb469469a77b9d561ec460ff862befa58ab30ddc909c9f7"
+ },
+ {
+ "name": "printer",
+ "unicode": "1F5A8",
+ "digest": "c44402c87071f8d31d3997abab53ab9f8f7c11434e747380928814ceb6b0a417"
+ },
+ {
+ "name": "prohibited",
+ "unicode": "1F6C7",
+ "digest": "bc6cdea2269a0ec39576d98dc4cda2bd9efa4dc330dde870148c6a85ad9cc63f"
+ },
+ {
+ "name": "projector",
+ "unicode": "1F4FD",
+ "digest": "fc361282f367926254c08150b02cb8fda7fa8d2c9c939d9360c78bf19a4f982e"
+ },
+ {
+ "name": "punch",
+ "unicode": "1F44A",
+ "digest": "5759db1d7093744c74b840bbb4761fb025d6633f8fa539bcb35dcf54fc05ceb6"
+ },
+ {
+ "name": "punch_tone1",
+ "unicode": "1F44A-1F3FB",
+ "digest": "793b3fa2a43c23b2c1e1b48b86ae35e8c4024cd065fac0a0a5ada87cb78d6de3"
+ },
+ {
+ "name": "punch_tone2",
+ "unicode": "1F44A-1F3FC",
+ "digest": "6fc2467e99982ab00b0c352c6f7793d34faf17b16a0312082c9bd1f0709e3938"
+ },
+ {
+ "name": "punch_tone3",
+ "unicode": "1F44A-1F3FD",
+ "digest": "bf747b29952550c5b4d3807b9ed85b5e5d4bcc3265b0e214791f7db547f861fb"
+ },
+ {
+ "name": "punch_tone4",
+ "unicode": "1F44A-1F3FE",
+ "digest": "3b6c0ccb682552f32d6744c438e3af04a1732c67a74bcafb14c723cf526fed87"
+ },
+ {
+ "name": "punch_tone5",
+ "unicode": "1F44A-1F3FF",
+ "digest": "945bae1aa3587cd1dc57d1ec4da18c67a59e0e7150dcc8735e5357b4ea1234ac"
+ },
+ {
+ "name": "purple_heart",
+ "unicode": "1F49C",
+ "digest": "e0eb886e74f22d40d059ff3a089d472af53c6c53de380f428cca140dfd046345"
+ },
+ {
+ "name": "purse",
+ "unicode": "1F45B",
+ "digest": "67d82ff9a4d76148b9d98538d4b786f880058a556e650ec3f93e1632aa42aaa7"
+ },
+ {
+ "name": "pushpin",
+ "unicode": "1F4CC",
+ "digest": "c4de129d5d8744caffeb2f499fcc0bc6b551843938f8166ffecd0de00bda66e3"
+ },
+ {
+ "name": "pushpin_black",
+ "unicode": "1F588",
+ "digest": "80ebac74edb9e8e1f8a219b32a676d318ed73b359cd8193b91b493d775307f63"
+ },
+ {
+ "name": "put_litter_in_its_place",
+ "unicode": "1F6AE",
+ "digest": "b26d3b68bd62d30ecfe75cfaf309a7a0f91e92db0aa18b0b97b97baf0609d4e6"
+ },
+ {
+ "name": "question",
+ "unicode": "2753",
+ "digest": "258e3169bae177fb0f01ed5f9b933f7f02dd2673e12a316af44a0c3729a78a2c"
+ },
+ {
+ "name": "rabbit",
+ "unicode": "1F430",
+ "digest": "9817a7454aeda77d28f63eb13c0dc0a6d9e6c9abe3dcf538b4b3477e494cddb6"
+ },
+ {
+ "name": "rabbit2",
+ "unicode": "1F407",
+ "digest": "67ba57a31b0768a2118faabdcb088f96f1441e1132397f65b6937d523ff7dabb"
+ },
+ {
+ "name": "race_car",
+ "unicode": "1F3CE",
+ "digest": "2e9828e3884c79ad7e9e1173d3470790f3f56cfa08ef4e38deff45db0728c66c"
+ },
+ {
+ "name": "racehorse",
+ "unicode": "1F40E",
+ "digest": "36aa3c7123ee7e15600657166032b21b8edeb192cf6d3ada39b5c65001f7fc40"
+ },
+ {
+ "name": "radio",
+ "unicode": "1F4FB",
+ "digest": "b1403f9a883405b909208f52c9474c2d3923681ea0b02609a6e9dc12460319a5"
+ },
+ {
+ "name": "radio_button",
+ "unicode": "1F518",
+ "digest": "9bcdac17b3620331a32f9bb876812231a701eb5a7f696e7d875f877ab92159fc"
+ },
+ {
+ "name": "radioactive",
+ "unicode": "2622",
+ "digest": "5ad8e8594617c0153672a76421deb836e05c6098020c33af3f975f8fcfe216e4"
+ },
+ {
+ "name": "rage",
+ "unicode": "1F621",
+ "digest": "02ac70551fc51478884c133b29539cae58b463c760db38c0aeec1bdf5b282312"
+ },
+ {
+ "name": "railway_car",
+ "unicode": "1F683",
+ "digest": "8490e2ecf94c7c1d1e22fea0d80cc18a49648741009e51984f583b17bbd022e2"
+ },
+ {
+ "name": "railway_track",
+ "unicode": "1F6E4",
+ "digest": "63ee881cc775d5b2711082b6c96ab44d5204c5d390afd6d8ee97e52aeeaa5e5e"
+ },
+ {
+ "name": "rainbow",
+ "unicode": "1F308",
+ "digest": "bbd8ecc8d0737948969a3539d2d202e599404e509f1a21bdbb0a0c41c2540522"
+ },
+ {
+ "name": "raised_hand",
+ "unicode": "270B",
+ "digest": "4192881a0d613b4fcb19b1c2d8b83aadee6f0b12170721c8dd7b1ccef6540199"
+ },
+ {
+ "name": "raised_hand_tone1",
+ "unicode": "270B-1F3FB",
+ "digest": "df2e046c99dceb9184c50a777b403d72bfb25ff473d6a4e20bb9a731db64ed8d"
+ },
+ {
+ "name": "raised_hand_tone2",
+ "unicode": "270B-1F3FC",
+ "digest": "ed179299a1c397cd51cf6067d6795d71a3831d35e1ec9eacbf0286c8992c1e7a"
+ },
+ {
+ "name": "raised_hand_tone3",
+ "unicode": "270B-1F3FD",
+ "digest": "cacbd0ddef65bc01a41bd921ea159f8cd89050309b10f15780d6199f79434a54"
+ },
+ {
+ "name": "raised_hand_tone4",
+ "unicode": "270B-1F3FE",
+ "digest": "04c934c7a55b83bcfa7f3880fc1f6aa0f188090c37b9670e6775a512a1cf59e9"
+ },
+ {
+ "name": "raised_hand_tone5",
+ "unicode": "270B-1F3FF",
+ "digest": "da0c4283b7b19861237c023234c6db28045b8f5a5971acb015733e08e2940e86"
+ },
+ {
+ "name": "raised_hands",
+ "unicode": "1F64C",
+ "digest": "308e475f38558e73bd66e28693d77478caa5bca4360cffaffc2a97b5858c56ba"
+ },
+ {
+ "name": "raised_hands_tone1",
+ "unicode": "1F64C-1F3FB",
+ "digest": "e39b9bc49dccc127e44f543e98961fcf5bcd44d6e216741bcd10ec3667263c84"
+ },
+ {
+ "name": "raised_hands_tone2",
+ "unicode": "1F64C-1F3FC",
+ "digest": "f376ab13071ffdc11888ec221ef5b4de546ca0f60bd9ae30bf3da4066c220462"
+ },
+ {
+ "name": "raised_hands_tone3",
+ "unicode": "1F64C-1F3FD",
+ "digest": "67694325a43e629c00fa9bd2ff7e19f84f216b2855ae2cf097762dfa7aca25e6"
+ },
+ {
+ "name": "raised_hands_tone4",
+ "unicode": "1F64C-1F3FE",
+ "digest": "a2254fe75a0770708916a4ddd5db4420221c6ea9db9f74068d14eadfc0f3772c"
+ },
+ {
+ "name": "raised_hands_tone5",
+ "unicode": "1F64C-1F3FF",
+ "digest": "bd7c9897cefb454ccdc46027bf56d6587565bdd345d7d0f081b7b671a53f6c99"
+ },
+ {
+ "name": "raising_hand",
+ "unicode": "1F64B",
+ "digest": "d57178fc77e9fa140682634da35f9ab12a65d9b4c506b7cd8a9697f1b5910bdb"
+ },
+ {
+ "name": "raising_hand_tone1",
+ "unicode": "1F64B-1F3FB",
+ "digest": "f46b34361ef79743f3187d6860182bbe1ae411031db7fe5c0f7292fa472b9c16"
+ },
+ {
+ "name": "raising_hand_tone2",
+ "unicode": "1F64B-1F3FC",
+ "digest": "20b85a2ebca150b2020a04b41d34884c78c22f42c251e2b9d23fd3724574143b"
+ },
+ {
+ "name": "raising_hand_tone3",
+ "unicode": "1F64B-1F3FD",
+ "digest": "5e0401b528c2b8edff766d39cdcedbe9abebe4c940df7a36ace61f59c08d508a"
+ },
+ {
+ "name": "raising_hand_tone4",
+ "unicode": "1F64B-1F3FE",
+ "digest": "e4f5624264269ad09cde207cd7d4eb0fd46de816880daeec457ac8cd51cc1b7b"
+ },
+ {
+ "name": "raising_hand_tone5",
+ "unicode": "1F64B-1F3FF",
+ "digest": "eb34b6c037bee5bbc4222f6aab421aa785f527ebf1b5e971769e5102244d60e1"
+ },
+ {
+ "name": "ram",
+ "unicode": "1F40F",
+ "digest": "b71950d7a286a4c4909c5ec7c35211c2a5c20b6bad341bd863c6a85c4bcf9c80"
+ },
+ {
+ "name": "ramen",
+ "unicode": "1F35C",
+ "digest": "7dd185b24852b577913edc78647cd53b27d42e225fde29aa2f3aba25c980b5c4"
+ },
+ {
+ "name": "rat",
+ "unicode": "1F400",
+ "digest": "7a10d9ba5ee1010d421d9cf73d7966507302a69617a32fe9f1a00d57a31f7bd7"
+ },
+ {
+ "name": "record_button",
+ "unicode": "23FA",
+ "digest": "c2ba672994ad0f99d7fdc449f3fee45a2dca68a58f9fe95825b38465a30ef44e"
+ },
+ {
+ "name": "recycle",
+ "unicode": "267B",
+ "digest": "74a54ed62a40dfbdcace1f08b085658a77d45c62570273927ad270bf9a8a2f4d"
+ },
+ {
+ "name": "red_car",
+ "unicode": "1F697",
+ "digest": "558730d6418aa5d85b73af58c8041efd12cff906e26ea47c50963f66d33d6eb8"
+ },
+ {
+ "name": "red_circle",
+ "unicode": "1F534",
+ "digest": "9dcf0132f6f2cc81702f0e3b15b37984e8439796705bf98f68ba449b3dfa5307"
+ },
+ {
+ "name": "registered",
+ "unicode": "00AE",
+ "digest": "ed924107384461aabb4924c401c6c087ffa047bc2ef735823e7c2be67804707c"
+ },
+ {
+ "name": "relaxed",
+ "unicode": "263A",
+ "digest": "65072f7b9bfaaa92b8a0ed012dffe2cfd2efa3748264aaf450aa31ba6bd44045"
+ },
+ {
+ "name": "relieved",
+ "unicode": "1F60C",
+ "digest": "1f2c7ae6a9d74a112de89403be6eca3d8155d70395e7fce51032fc961f235c7d"
+ },
+ {
+ "name": "reminder_ribbon",
+ "unicode": "1F397",
+ "digest": "e4a2afc7dce40589657f7043ba8acc9638fd4117252278233ea89f84cddad387"
+ },
+ {
+ "name": "repeat",
+ "unicode": "1F501",
+ "digest": "27b6dad9215e58e24c607a39dbf398ecf66ccb692c81e08eb2f5f4912db30522"
+ },
+ {
+ "name": "repeat_one",
+ "unicode": "1F502",
+ "digest": "052d13f2b08eaf70b31252aa78f95d06fbe22c58945c19381b13cbeb1c855651"
+ },
+ {
+ "name": "restroom",
+ "unicode": "1F6BB",
+ "digest": "b77fbc4247c241362e5ef9e6eb58b1b437aa9d16b65886cec0c55ceb55c1440e"
+ },
+ {
+ "name": "revolving_hearts",
+ "unicode": "1F49E",
+ "digest": "2b8925d3e78df2dba8534252fe60bf03285346f6b3697be7668bd568e6d85931"
+ },
+ {
+ "name": "rewind",
+ "unicode": "23EA",
+ "digest": "91a95b26d12ca76111556096f4d96484c9f1d7e1b20ccff5a3291b36e529a6d1"
+ },
+ {
+ "name": "ribbon",
+ "unicode": "1F380",
+ "digest": "9c0296d8c2baa84c99347c431bf79b288d98b5f17b1ce7605ad7ce1da265d5aa"
+ },
+ {
+ "name": "rice",
+ "unicode": "1F35A",
+ "digest": "e34849496a79e71ae4700df94f2a54895bf6de758a92edeae33fe78295a3ba21"
+ },
+ {
+ "name": "rice_ball",
+ "unicode": "1F359",
+ "digest": "52df5da8b0edbdeb56d66e0f30ad4549abdd81c064f7269d920dcac66a3df2e4"
+ },
+ {
+ "name": "rice_cracker",
+ "unicode": "1F358",
+ "digest": "d55f8f9d807f4619eb243c510938067a7417a64bd9435b05dfeb2a36fdb2b6a0"
+ },
+ {
+ "name": "rice_scene",
+ "unicode": "1F391",
+ "digest": "482d854d8d30edfc1ecd48a4ce476e6498606321405bf5a0b4ff74489a092af8"
+ },
+ {
+ "name": "right_speaker",
+ "unicode": "1F568",
+ "digest": "d268bb84be863c0884620dfc6d2a764b0c7466d2f9810549b138e21ac70add4e"
+ },
+ {
+ "name": "right_speaker_one",
+ "unicode": "1F569",
+ "digest": "5b92daa87bdf6ee15e798bec382a2ee885f4e6e77a68a3f626adcfe4c782b375"
+ },
+ {
+ "name": "right_speaker_three",
+ "unicode": "1F56A",
+ "digest": "4d00b720a65bd0f4c3682b290b1976ec2388d6ae61225398f4e70556ae9e5f80"
+ },
+ {
+ "name": "ring",
+ "unicode": "1F48D",
+ "digest": "ae2a93e7895b9b89f5a39f01d356ffed988f219ef8b658a56c55285826a4533b"
+ },
+ {
+ "name": "ringing_bell",
+ "unicode": "1F56D",
+ "digest": "d71ab7fa937fc4af507b5b07ea58a4f31e875d9e8304ef2b850d7cebe0e9cd66"
+ },
+ {
+ "name": "robot",
+ "unicode": "1F916",
+ "digest": "cc0e363774b86e21a5b2cea7f7af85bca9e92c124ebcd39c6067c125048baa60"
+ },
+ {
+ "name": "rocket",
+ "unicode": "1F680",
+ "digest": "65d8bd005ceac41904237b7a8c5f55f16713a55d971522f0bbe63a1d548e515d"
+ },
+ {
+ "name": "roller_coaster",
+ "unicode": "1F3A2",
+ "digest": "907baab1f3d7becf3f8a3b1264642b395bd73b4af49e23058b3abb5c69e9106a"
+ },
+ {
+ "name": "rolling_eyes",
+ "unicode": "1F644",
+ "digest": "f596f203030b6c9bd743848512aa3fc7919447020d35ae5c2bf13ccb16fa2dbe"
+ },
+ {
+ "name": "rooster",
+ "unicode": "1F413",
+ "digest": "6cefdaa45631ed8c9480e15f578c793d95af81b42687164fd7900eee325ccf07"
+ },
+ {
+ "name": "rose",
+ "unicode": "1F339",
+ "digest": "584909a4a2ece625c688f8479a39692bb8e816b692e6eb7dfd40cb045259b1b2"
+ },
+ {
+ "name": "rosette",
+ "unicode": "1F3F5",
+ "digest": "0ce3b85ca05124ab99d57ebc9aa17bb246ee614d2fcda1ef62bf42ac7e616148"
+ },
+ {
+ "name": "rosette_black",
+ "unicode": "1F3F6",
+ "digest": "ae8675891c88f9d98463d35178445950c39b0deb0f0e8b3f341228a6e0d0e477"
+ },
+ {
+ "name": "rotating_light",
+ "unicode": "1F6A8",
+ "digest": "369e069e0bfecc7413e75f4015e9c1de527a33c7cce3f6c2b4adb60a0d9d338c"
+ },
+ {
+ "name": "round_pushpin",
+ "unicode": "1F4CD",
+ "digest": "1bc5fe5a90a6e56ea00246f1b008a0e0cce0d77c226dc0300bf9a2804b543877"
+ },
+ {
+ "name": "rowboat",
+ "unicode": "1F6A3",
+ "digest": "c10e09bf8be8b1a8ef3113edd9327126d6a4644f3bc81c7ada2922851e4d1cfb"
+ },
+ {
+ "name": "rowboat_tone1",
+ "unicode": "1F6A3-1F3FB",
+ "digest": "a84fc1b30d1a284dcd3899dc4de8f11e7b65c258528eb41c7dbf8f82425fee12"
+ },
+ {
+ "name": "rowboat_tone2",
+ "unicode": "1F6A3-1F3FC",
+ "digest": "85f001430a2ad607a15901f7c2dcf8381471f42d6cc0775e76a2ff1f457151c1"
+ },
+ {
+ "name": "rowboat_tone3",
+ "unicode": "1F6A3-1F3FD",
+ "digest": "adf8b1e45a46a13f3db40c29df0312216558e9d0c615aa46a8e913cee5003a81"
+ },
+ {
+ "name": "rowboat_tone4",
+ "unicode": "1F6A3-1F3FE",
+ "digest": "05482749ec40bdf02e53fc42d316c51f4f3ed643f21e8fc16b81930e4a884bda"
+ },
+ {
+ "name": "rowboat_tone5",
+ "unicode": "1F6A3-1F3FF",
+ "digest": "d4bb337d948996d4a23d87f99988f02fc207815b862082ffd2eef5f0c1016aa9"
+ },
+ {
+ "name": "rugby_football",
+ "unicode": "1F3C9",
+ "digest": "e14aebbded78d4a5e9b4028f79a8ca840d02798c6758cb9e926e992e2a35a4f3"
+ },
+ {
+ "name": "runner",
+ "unicode": "1F3C3",
+ "digest": "58a884f06d37b0ce78197bebcd3f0e102dd90022ebd86ec70a2ef5a5cdf9683b"
+ },
+ {
+ "name": "runner_tone1",
+ "unicode": "1F3C3-1F3FB",
+ "digest": "65f1633d1517803de23686d2dbcc75a5787874266db4981138ccdbe4badc773c"
+ },
+ {
+ "name": "runner_tone2",
+ "unicode": "1F3C3-1F3FC",
+ "digest": "2bc81f3fb77445cdc75c34806ab0ce912bacfe47f63b5d2011a4f5d370cf7064"
+ },
+ {
+ "name": "runner_tone3",
+ "unicode": "1F3C3-1F3FD",
+ "digest": "beaf5f254cba2991fdd0c38ce2ddd1b4c1110e15b2b7bc026d32f162e295c4ef"
+ },
+ {
+ "name": "runner_tone4",
+ "unicode": "1F3C3-1F3FE",
+ "digest": "21d531ba9b3d13747ad636b8f7a6f184c974bf61d9f529975a64f9629263c407"
+ },
+ {
+ "name": "runner_tone5",
+ "unicode": "1F3C3-1F3FF",
+ "digest": "b02a5bcc58cc45f8219262ec44c77764172fd8f2624d9122ded4a5a5db04c0ed"
+ },
+ {
+ "name": "running_shirt_with_sash",
+ "unicode": "1F3BD",
+ "digest": "431bed35f4a55175bf99af769e74a81e8650c6ab34af6ecddaa1417ff7e437e6"
+ },
+ {
+ "name": "sa",
+ "unicode": "1F202",
+ "digest": "a47a480631f874e8a2cd69b5d513f90a1e81a96bfa2f6025bf244a82baca3656"
+ },
+ {
+ "name": "sagittarius",
+ "unicode": "2650",
+ "digest": "14871e6681c35e4a63a0b19613f77b3674d00cb78d06975e02ca29e61b5cea8c"
+ },
+ {
+ "name": "sailboat",
+ "unicode": "26F5",
+ "digest": "6f742dde6c180a174b771aa3942b558e98a3dc1eb212dd31add86c5fa5620865"
+ },
+ {
+ "name": "sake",
+ "unicode": "1F376",
+ "digest": "aa1392790c805950779dde7778292c937f8c1aaecb522876171d5ee542ec51f8"
+ },
+ {
+ "name": "sandal",
+ "unicode": "1F461",
+ "digest": "14f1e9003a6acd90a55f23c48ed87a758fca586f2e0b0edc4dc9d1deef9eb067"
+ },
+ {
+ "name": "santa",
+ "unicode": "1F385",
+ "digest": "12feddd84eb49ce30ae68d4f93d66e2c0dd11297a4d1275c9a50d4f35bea83a9"
+ },
+ {
+ "name": "santa_tone1",
+ "unicode": "1F385-1F3FB",
+ "digest": "a75813770efe27d5b4c80ad892d0c796d88d1a0dbb1bd02d5f68882d7abad479"
+ },
+ {
+ "name": "santa_tone2",
+ "unicode": "1F385-1F3FC",
+ "digest": "90f8072fdde5f4a275cbd1902d6c94689d453b1bee0336213dc9d6f7e1d038e1"
+ },
+ {
+ "name": "santa_tone3",
+ "unicode": "1F385-1F3FD",
+ "digest": "0973053e7b77d268080126a50b95b45429630e5d49f62210e7b71840794c7dc5"
+ },
+ {
+ "name": "santa_tone4",
+ "unicode": "1F385-1F3FE",
+ "digest": "5cd49c0d199a42846b400b3c1244d448ed6fe5ce993d379817cb2a5f7c0b609b"
+ },
+ {
+ "name": "santa_tone5",
+ "unicode": "1F385-1F3FF",
+ "digest": "a54c36dfa99b39549fb1d3dd7f0021a7aee28112960172ed466dacc67961c525"
+ },
+ {
+ "name": "satellite",
+ "unicode": "1F4E1",
+ "digest": "3b9797c8161526edce0bd8e9b8563055166f9307761c367ab3e2ad7645b6dee0"
+ },
+ {
+ "name": "satellite_orbital",
+ "unicode": "1F6F0",
+ "digest": "104b135e3736a4bcfd51a42dadb53bf3e00d7f85d77a94bcb86c6704fbfacd01"
+ },
+ {
+ "name": "saxophone",
+ "unicode": "1F3B7",
+ "digest": "1090da174ce8aa4f7d35025f65d5ac235e09310abde998d2a725ef3a989a2b75"
+ },
+ {
+ "name": "scales",
+ "unicode": "2696",
+ "digest": "b2984caa182b691a33650344708f47c61d6d319fd067760d7594c2ef60c1e27b"
+ },
+ {
+ "name": "school",
+ "unicode": "1F3EB",
+ "digest": "caf35260dc465a833521e4a0034201978fed41bbf72cd770756b3340c60e8a0c"
+ },
+ {
+ "name": "school_satchel",
+ "unicode": "1F392",
+ "digest": "a89a2cc46d24d57c2d6b95ed7a56ed829ae2f97b9e6201b2d5adc78c2b78518b"
+ },
+ {
+ "name": "scissors",
+ "unicode": "2702",
+ "digest": "a4e91127ac83acf5ebc64fbeca768cbbf24f2f0a484861c9c8104bee377b97ae"
+ },
+ {
+ "name": "scorpion",
+ "unicode": "1F982",
+ "digest": "a090a96731bc1171b054b51abec4c9b36faa62708fd51ac48277ccf5e55d9d12"
+ },
+ {
+ "name": "scorpius",
+ "unicode": "264F",
+ "digest": "1ad9bc1030a8f58f3f3223bac52c954cc7a0350805a9df7a42a26972c3b74728"
+ },
+ {
+ "name": "scream",
+ "unicode": "1F631",
+ "digest": "75d613786737ee9c0a74da7394b9ae190eacc7182164627ad8205ac64e4cc09a"
+ },
+ {
+ "name": "scream_cat",
+ "unicode": "1F640",
+ "digest": "eee04ff27c2c6b57d698cb87b0af8064ba8313ffc13aa090e38cd5aa8c3d2f76"
+ },
+ {
+ "name": "scroll",
+ "unicode": "1F4DC",
+ "digest": "b8205847649e3ce6b946f1d1da972ed015adde3841c62971b8169235f4b41c1f"
+ },
+ {
+ "name": "seat",
+ "unicode": "1F4BA",
+ "digest": "054c4db0bc8939e9dd951a3f73e9ae4b3c31652784f4d304b509c2bd32f98e31"
+ },
+ {
+ "name": "secret",
+ "unicode": "3299",
+ "digest": "77daef6e5c91d55228781ddec954a7089d1851297ec81daef6e813cd22915b5e"
+ },
+ {
+ "name": "see_no_evil",
+ "unicode": "1F648",
+ "digest": "aa5883fe605aeaa172d16640b8347580f9cb7d85a596da1b13955f27b0b79297"
+ },
+ {
+ "name": "seedling",
+ "unicode": "1F331",
+ "digest": "a75ec929402de1e653fd6bc89e5be2f92fe5fe52f39e4b6c290eae3c59172b56"
+ },
+ {
+ "name": "seven",
+ "unicode": "0037-20E3",
+ "digest": "c6a34020f6bb25871164fad44302a45c5bffced87f51dfbb816c2985ad7f6a1c"
+ },
+ {
+ "name": "shamrock",
+ "unicode": "2618",
+ "digest": "530e6b987ecb9bcbf0d6e0e11bd075e7949873c784da4f9e1e1b47efd37e5058"
+ },
+ {
+ "name": "shaved_ice",
+ "unicode": "1F367",
+ "digest": "fc22c3568f6be56771e83fd0e67b7eb3750041304d5d4979d3ec417f5201230e"
+ },
+ {
+ "name": "sheep",
+ "unicode": "1F411",
+ "digest": "3e3656b82784164ca02c5d775db7245260f0119d2c1d35ba552a6dc75ef02544"
+ },
+ {
+ "name": "shell",
+ "unicode": "1F41A",
+ "digest": "ff2f4f574b61bffd85c63bc2315c80d3cbcaba37a7c15a1f00783d312bd441d4"
+ },
+ {
+ "name": "shield",
+ "unicode": "1F6E1",
+ "digest": "062aec4a325da7b637c5710846c7e7319229be49b7e59f50428442a7ef725d60"
+ },
+ {
+ "name": "shinto_shrine",
+ "unicode": "26E9",
+ "digest": "9768fe94142a7dc169703d3707b203f285a546455e29fe2bbf185d44f160d6d0"
+ },
+ {
+ "name": "ship",
+ "unicode": "1F6A2",
+ "digest": "f8d5b0c8ec66287b732d9171ac1913be02efb656de11501213a207d8a6c801e1"
+ },
+ {
+ "name": "shirt",
+ "unicode": "1F455",
+ "digest": "e2e72c323f3bfaea02e8cf52201aa144dc56ec0f25ec97d5f04ee6c2ee99104e"
+ },
+ {
+ "name": "shopping_bags",
+ "unicode": "1F6CD",
+ "digest": "0194ba540c47e4fc6403be2df68f785d56810efc2dc011dfbf700f3778cb704a"
+ },
+ {
+ "name": "shower",
+ "unicode": "1F6BF",
+ "digest": "c945120182392510348de9a957c2b77a4645d118691298a2ad660dafa62a859c"
+ },
+ {
+ "name": "signal_strength",
+ "unicode": "1F4F6",
+ "digest": "7876ed9d602e1be746ca0629f072d85668d1f9715e9135745e803bdf89819a3c"
+ },
+ {
+ "name": "six",
+ "unicode": "0036-20E3",
+ "digest": "b409f23b73e46393c7a814442816b5880c38ef12a7feb5505e71276c195e8ca9"
+ },
+ {
+ "name": "six_pointed_star",
+ "unicode": "1F52F",
+ "digest": "4bc294dcbf4185250873b52b2fb5453fb7d80df912db929add6e4b7efc066363"
+ },
+ {
+ "name": "ski",
+ "unicode": "1F3BF",
+ "digest": "7ee81a2e2f7ff4e32dbf3d64b034e7542ec0c86d32e25eb125052e674943d75f"
+ },
+ {
+ "name": "skier",
+ "unicode": "26F7",
+ "digest": "49df9a4206ae0c7c2dbfc8a8b13fd3e14e6f7e750bd5a8581ab6a1626d4c165e"
+ },
+ {
+ "name": "skull",
+ "unicode": "1F480",
+ "digest": "dfd169764b192ac7c6e5101277dd9f1e010e86bdd32ad37e00ed4499fc0a5dd6"
+ },
+ {
+ "name": "skull_crossbones",
+ "unicode": "2620",
+ "digest": "e2acf0f36b6a6800c1829a1c6551b5d0eb6dcdef4b7f02070cf69570aeab608c"
+ },
+ {
+ "name": "sleeping",
+ "unicode": "1F634",
+ "digest": "4ead95079b1a542eedd0e5a0e93fddb318a002bdaffaa2fe5d8d7f20bf8143ed"
+ },
+ {
+ "name": "sleeping_accommodation",
+ "unicode": "1F6CC",
+ "digest": "10ee8cd925a75d7977b7cf004e08b5a8147b509ee4281e879a8b57c4a7c2cb04"
+ },
+ {
+ "name": "sleepy",
+ "unicode": "1F62A",
+ "digest": "dea3b246bb8af1b28e200358e3d5d59c8bba1813f35a7f4a57ec568ef43591db"
+ },
+ {
+ "name": "slight_frown",
+ "unicode": "1F641",
+ "digest": "3ae82b38b58ffa50eddebd87153428d880ca181f4f4178a9ca3bd813ea15ccbc"
+ },
+ {
+ "name": "slight_smile",
+ "unicode": "1F642",
+ "digest": "5eee09f634a4e2031927d008a6530a258a00e611ead0c386dd5b7ebb5e75a306"
+ },
+ {
+ "name": "slot_machine",
+ "unicode": "1F3B0",
+ "digest": "9d516b389299431b608c89d3f02ac68d28cb8df2a780f2048923bbcfbb49f416"
+ },
+ {
+ "name": "small_blue_diamond",
+ "unicode": "1F539",
+ "digest": "97389e82755dc43015089dee635072357ec347f0117b2d3e9b006c46514948ee"
+ },
+ {
+ "name": "small_orange_diamond",
+ "unicode": "1F538",
+ "digest": "67442d3b707501b7768f606115688373d13617ecf0b3b03ace0f1a6d38f66ddf"
+ },
+ {
+ "name": "small_red_triangle",
+ "unicode": "1F53A",
+ "digest": "e0a556a3dd5bbf0290ed7c00eb6f6307dc2ea98d1fb3111fd85a7f46242a3638"
+ },
+ {
+ "name": "small_red_triangle_down",
+ "unicode": "1F53B",
+ "digest": "7a11dcb8a517df220493d471759e4f4bca0db3769e2d942bbf596a88a3e57f72"
+ },
+ {
+ "name": "smile",
+ "unicode": "1F604",
+ "digest": "46a7c3545b0038dfce6825d97544f6665f28512ad05c404d668e32ac599c7ecb"
+ },
+ {
+ "name": "smile_cat",
+ "unicode": "1F638",
+ "digest": "c1db961f0fa261532b842816aca7ea7f6d8b461c7e930a1a1c91f96efd9db515"
+ },
+ {
+ "name": "smiley",
+ "unicode": "1F603",
+ "digest": "deeaaee64ebdd9fc0bcb719db75c3f7e0c33ddbcc97f6cd51f9f84377a4368ce"
+ },
+ {
+ "name": "smiley_cat",
+ "unicode": "1F63A",
+ "digest": "85ad852cb3881c4b754af172fdfc6231af42578033ea9f2981ceae944c41e72f"
+ },
+ {
+ "name": "smiling_imp",
+ "unicode": "1F608",
+ "digest": "e777bdf186d89921df106d23bf002967b69afffd7e981b3cbb19f89630a06e87"
+ },
+ {
+ "name": "smirk",
+ "unicode": "1F60F",
+ "digest": "2e7fddd8bed33ef4b7d8c13320302b87a28203e576ef87bd43716952cf0b5ace"
+ },
+ {
+ "name": "smirk_cat",
+ "unicode": "1F63C",
+ "digest": "9ca0721f4c18592b4b809ade8f716b95fa30cd31dd87d1e41db29a319becd705"
+ },
+ {
+ "name": "smoking",
+ "unicode": "1F6AC",
+ "digest": "3d14b3f0c57eb7a6a31ff371b0a454986533b79dbbeac78a76e4063478911b8d"
+ },
+ {
+ "name": "snail",
+ "unicode": "1F40C",
+ "digest": "57d946c7ec84dfad71bc4f7a042927ec5712aef50c66d21af892b6c8a7faf5e1"
+ },
+ {
+ "name": "snake",
+ "unicode": "1F40D",
+ "digest": "d084da540162288721364992f3b8059cbf2efd9f5b48f49a196ddbe23a073870"
+ },
+ {
+ "name": "snowboarder",
+ "unicode": "1F3C2",
+ "digest": "de9e1767526de606f4908743af94cc17e89fdb0a2a44167d3d021ef09d033ab9"
+ },
+ {
+ "name": "snowflake",
+ "unicode": "2744",
+ "digest": "e476863ccd7d7b549c6191fb25c121c6a467b4baef4683b7dc3e0a793c2e5d76"
+ },
+ {
+ "name": "snowman",
+ "unicode": "26C4",
+ "digest": "792946b8446f2243d11b89d07c73a774be3abd36573f3918640b1ba8714270b5"
+ },
+ {
+ "name": "snowman2",
+ "unicode": "2603",
+ "digest": "571acabaa4d55782c4529b762423a7e34cb1fb6bb7852cbd013e2e846d8311d1"
+ },
+ {
+ "name": "sob",
+ "unicode": "1F62D",
+ "digest": "562f02ab584bcbcf9ba73cf7fa7d7129965266abd28db2c73913b8c42f2f5aca"
+ },
+ {
+ "name": "soccer",
+ "unicode": "26BD",
+ "digest": "5fd0d534659b63dc862c65a80561b255bece0b76708fe8ecbae8e01b08d8cad0"
+ },
+ {
+ "name": "soon",
+ "unicode": "1F51C",
+ "digest": "d2a1ab16a4056d80c827ea23f9332bb73235fc841b857cbf545062ff8aeed81d"
+ },
+ {
+ "name": "sos",
+ "unicode": "1F198",
+ "digest": "fadfe8337e133a6f05d205d0807f288e5c230db04cb09f3547ce0cb73cfcf48a"
+ },
+ {
+ "name": "sound",
+ "unicode": "1F509",
+ "digest": "c0074b338fd461f1f9d1143b7f9b3781ddb3fd501ea79b2410630433a8e87b83"
+ },
+ {
+ "name": "space_invader",
+ "unicode": "1F47E",
+ "digest": "d264390004bd28d664dfda0069104be6db32ce477e23a95ac595bac2e29fd4e7"
+ },
+ {
+ "name": "spades",
+ "unicode": "2660",
+ "digest": "d1ad99a4fc20dfea881a9062a9f2109e483dbb5dea3b29e9653cb27ec57b4800"
+ },
+ {
+ "name": "spaghetti",
+ "unicode": "1F35D",
+ "digest": "ac63f9ad143e236ce6068098e5330a333ade9cddfb3dd6b1457ea47ce9dcf7e9"
+ },
+ {
+ "name": "sparkle",
+ "unicode": "2747",
+ "digest": "95b8f4f1bb6080cd1d7bd333c4724dbba43ed196dce72a2bbaab46c4a1bc0e48"
+ },
+ {
+ "name": "sparkler",
+ "unicode": "1F387",
+ "digest": "3a296e4d0081ad1a566e111d218e352e1439bba9fd04e8a1eb9a8e36bd438cb7"
+ },
+ {
+ "name": "sparkles",
+ "unicode": "2728",
+ "digest": "5ab280ea10c30e0e0b5a26ef52b8f47ad44a983330f7ef62ac0c0888752bbdb6"
+ },
+ {
+ "name": "sparkling_heart",
+ "unicode": "1F496",
+ "digest": "f145dab6b597c07e5a851176fabaf56dd857209645483d1acc1490d12c969113"
+ },
+ {
+ "name": "speak_no_evil",
+ "unicode": "1F64A",
+ "digest": "6eae2d066d39c4ba81e58a8327ed875c68bc9b1297c18dc0f5243e477a81040f"
+ },
+ {
+ "name": "speaker",
+ "unicode": "1F508",
+ "digest": "ea59c5a9d994808ff7937c300303e644b5f1ad41097e82f9e73ea6e1c718936c"
+ },
+ {
+ "name": "speaking_head",
+ "unicode": "1F5E3",
+ "digest": "d92cfe1200887300b2f05f9576448a2f2a79d0accd51f323a65ce3db0aa5639b"
+ },
+ {
+ "name": "speech_balloon",
+ "unicode": "1F4AC",
+ "digest": "5dccfda46fc984583bc9eaece66e7e884f2a9eb12a69dbd3493035e3c862edd0"
+ },
+ {
+ "name": "speech_left",
+ "unicode": "1F5E8",
+ "digest": "478b0b07460a9f54b7d0050f886da59fde5e428daa11e899fc31477fda1707ed"
+ },
+ {
+ "name": "speech_right",
+ "unicode": "1F5E9",
+ "digest": "8439b13779163c15e678a78b08ebeeb7d131632df21d2a7868de7fed38ca9d8a"
+ },
+ {
+ "name": "speech_three",
+ "unicode": "1F5EB",
+ "digest": "55a934f3659b6e75fdce0d0c4e2ea56dd34a43892c85a6666bd1882a0bfb92a9"
+ },
+ {
+ "name": "speech_two",
+ "unicode": "1F5EA",
+ "digest": "0563ef0591da243673cf877462acc5d8e1d980a56e81668ac627de74d0c33983"
+ },
+ {
+ "name": "speedboat",
+ "unicode": "1F6A4",
+ "digest": "553a288ab8eeb3dee7b9d1c92eba38016caef7658beaa828136ba1d6ba8ed08a"
+ },
+ {
+ "name": "spider",
+ "unicode": "1F577",
+ "digest": "519f7243b5574102ce3f8953e5480812830a1feb32ae51e8573724c864338481"
+ },
+ {
+ "name": "spider_web",
+ "unicode": "1F578",
+ "digest": "42959fae08a2162d6ee8c8706f823c5932f3801bc90da30d2ca9a48c3ff25572"
+ },
+ {
+ "name": "spy",
+ "unicode": "1F575",
+ "digest": "eaa570a36d83119d0a596228e74affe84d7355714ff6901d88a89410d26dec2a"
+ },
+ {
+ "name": "spy_tone1",
+ "unicode": "1F575-1F3FB",
+ "digest": "abdc066d4cad6a17047faf7806c45feb43ae1e2056cf500536f08f4173dbfa94"
+ },
+ {
+ "name": "spy_tone2",
+ "unicode": "1F575-1F3FC",
+ "digest": "72a3313ef12364105e764cc3deabd47eb6bd086f261c435682ae1cd29dc8230b"
+ },
+ {
+ "name": "spy_tone3",
+ "unicode": "1F575-1F3FD",
+ "digest": "2a1108d3d2e778f88aa5b3ae36705c877b84d0bf6b421409582ba748aeb2aee7"
+ },
+ {
+ "name": "spy_tone4",
+ "unicode": "1F575-1F3FE",
+ "digest": "1d4fe62912384bc0d687bcf4565752caf0ed6146c903a156d1c6ba6ea239b154"
+ },
+ {
+ "name": "spy_tone5",
+ "unicode": "1F575-1F3FF",
+ "digest": "69c1baac73783edb9e2d0c951f922dc7dddac34d0a9c818fee8d1021bc17db0d"
+ },
+ {
+ "name": "stadium",
+ "unicode": "1F3DF",
+ "digest": "4356db5d2cdef8c40830638debaf1f50831130c12ae8d8dc3d9a6bd28fdaa1f7"
+ },
+ {
+ "name": "star",
+ "unicode": "2B50",
+ "digest": "13240b8fada84e7555892996e9f9652503bf9b9a002056c2bae428d543abe2da"
+ },
+ {
+ "name": "star2",
+ "unicode": "1F31F",
+ "digest": "9b56c7548f6a222499d4e848576ea25eab837db72b207ebf8a62a451b35f758f"
+ },
+ {
+ "name": "star_and_crescent",
+ "unicode": "262A",
+ "digest": "10b8a0771e415aa6610fa62185137aa1836c2bb3e82f1a3f601470e94f784923"
+ },
+ {
+ "name": "star_of_david",
+ "unicode": "2721",
+ "digest": "5bc4d1038b8316281e01a9c575ded7ede0fc24c7593db5b5d36ca2e188aa5614"
+ },
+ {
+ "name": "stars",
+ "unicode": "1F320",
+ "digest": "23605eafc949feead3eca145a7ff5ee3b211a8bfd95621bd35dd05df532b97c6"
+ },
+ {
+ "name": "station",
+ "unicode": "1F689",
+ "digest": "c346f12fff64161041af8492550c3541a6304e53f30288224ddd0c6fe08c4d6b"
+ },
+ {
+ "name": "statue_of_liberty",
+ "unicode": "1F5FD",
+ "digest": "56fa27ab059a9fd1f53aec47d9108277a3bf04a73186f36297cd1207c832ee31"
+ },
+ {
+ "name": "steam_locomotive",
+ "unicode": "1F682",
+ "digest": "d0ec2eb3d761ab6157e17eab1b8b4dec3a69f9becc4251592cbb67d71825e661"
+ },
+ {
+ "name": "stereo",
+ "unicode": "1F4FE",
+ "digest": "1ce1f9a83867514b8351ad4fd80c46bba04ad67dfb9874e63d7296e1a21161a5"
+ },
+ {
+ "name": "stew",
+ "unicode": "1F372",
+ "digest": "12e6e4bf48a7296700e07a053d831dd67b70c308ca9522ca96e933a4d1ef6c5e"
+ },
+ {
+ "name": "stock_chart",
+ "unicode": "1F5E0",
+ "digest": "4a0fbf54d19b0b5626f91c932a24e6ac12a65b4fc276d852ff4356c8c579d28a"
+ },
+ {
+ "name": "stop_button",
+ "unicode": "23F9",
+ "digest": "57310962c7738a7da4f2a62cbd5e0b26d7aec357978267a0d8ca8e6cbd7ffb02"
+ },
+ {
+ "name": "stopwatch",
+ "unicode": "23F1",
+ "digest": "c8e69c24f9da98dcb41c9c6355922d08a702f12a35667fbc5beb3f659430333d"
+ },
+ {
+ "name": "straight_ruler",
+ "unicode": "1F4CF",
+ "digest": "55ff7182a3696461df52e3000708083f803bc8bf0f3c25dacb34175cc104b51d"
+ },
+ {
+ "name": "strawberry",
+ "unicode": "1F353",
+ "digest": "fd501e1fefb70242ac7c4dc30ad3d8c3ae200b263a832daedaa984906114afaf"
+ },
+ {
+ "name": "stuck_out_tongue",
+ "unicode": "1F61B",
+ "digest": "1b49956cec511ee382177d95da77c8b6a9214a02c86bf7c6c6fd6cc9df3e9331"
+ },
+ {
+ "name": "stuck_out_tongue_closed_eyes",
+ "unicode": "1F61D",
+ "digest": "60a4d5d92550c6ad4db901d42c9f6434fe94fa3ddb353b6019a93d374d9485e9"
+ },
+ {
+ "name": "stuck_out_tongue_winking_eye",
+ "unicode": "1F61C",
+ "digest": "d9c15ad1c4782a0391a79aeda2745127527385b0b5fc01c8d96c3f3b637a74ae"
+ },
+ {
+ "name": "sun_with_face",
+ "unicode": "1F31E",
+ "digest": "56b14e92f68f8701fdc42763e1f4695ed352845f22bd5d412f827e5cf98dd83b"
+ },
+ {
+ "name": "sunflower",
+ "unicode": "1F33B",
+ "digest": "817dea222a75bb6492c32b4b144d07f48295d7dd113e21760f90b18277612ebb"
+ },
+ {
+ "name": "sunglasses",
+ "unicode": "1F60E",
+ "digest": "16003cc5256397389889f52e0a5e14daea8d8c72f2ea660b8174529868cba9cd"
+ },
+ {
+ "name": "sunny",
+ "unicode": "2600",
+ "digest": "f68a774b7d574fc711111e17368b57c40d973d263c7e857544a09051d4592ab9"
+ },
+ {
+ "name": "sunrise",
+ "unicode": "1F305",
+ "digest": "ce06a9321bc04605538a59f9fca8536d6209d7ded03120e5d2a0be955bb17ddf"
+ },
+ {
+ "name": "sunrise_over_mountains",
+ "unicode": "1F304",
+ "digest": "286244ac2bec8c5c41cf8c7c439702fa525c57fab623f7f9bd7687db0adf75b2"
+ },
+ {
+ "name": "surfer",
+ "unicode": "1F3C4",
+ "digest": "d17c7ea185ca5ef5a2950ef126ee14103bf7769acb419a20d08cc023f619e459"
+ },
+ {
+ "name": "surfer_tone1",
+ "unicode": "1F3C4-1F3FB",
+ "digest": "af66f2f26071b3ba8d7c795139055a58a857212f8cb1f51a507242ad7d2c49c7"
+ },
+ {
+ "name": "surfer_tone2",
+ "unicode": "1F3C4-1F3FC",
+ "digest": "7a34e8b1fdad0a89bbb10333d241583ef018517fdd90f171ad7121de53776a3f"
+ },
+ {
+ "name": "surfer_tone3",
+ "unicode": "1F3C4-1F3FD",
+ "digest": "b2f4cbd59a0aa93c7ee2bbb14ce55c8306dc25884377982a5f132ce6c074fa1d"
+ },
+ {
+ "name": "surfer_tone4",
+ "unicode": "1F3C4-1F3FE",
+ "digest": "b16a02cfcc3606524cca9408e69c654fb83a162eaec8faae8dfd8ec67fe391c5"
+ },
+ {
+ "name": "surfer_tone5",
+ "unicode": "1F3C4-1F3FF",
+ "digest": "b9a156e1aa57544b703db4e4a7773e244a3139e82c2c808c2e5a804fb524f512"
+ },
+ {
+ "name": "sushi",
+ "unicode": "1F363",
+ "digest": "d2709b51ee92997c7fafa1b1517259cb896819c8dc9ba98ae26e1d44ec810d4f"
+ },
+ {
+ "name": "suspension_railway",
+ "unicode": "1F69F",
+ "digest": "48903e103ef00a068b0100b28319b1e41c6a4485cb564f0ca59422ec9d3b259c"
+ },
+ {
+ "name": "sweat",
+ "unicode": "1F613",
+ "digest": "8d684fa882bcbf07f4e91ea02a48cd61f22e7aa206162b8352c26fc19361ed4e"
+ },
+ {
+ "name": "sweat_drops",
+ "unicode": "1F4A6",
+ "digest": "fca48e255dff08dab97ef98b75c67f7504a13be8b90afac88b69a7b7e887e445"
+ },
+ {
+ "name": "sweat_smile",
+ "unicode": "1F605",
+ "digest": "0c8156554eec2396b5fee908da46484945db980d2ebc6dee57b4069a86826182"
+ },
+ {
+ "name": "sweet_potato",
+ "unicode": "1F360",
+ "digest": "3ce74ea9bc14906a3d29a9592c0657aee8f7961d406992752f7580b16ca6bdd0"
+ },
+ {
+ "name": "swimmer",
+ "unicode": "1F3CA",
+ "digest": "05f3aa8544e3b15837bb06ae47344633b3e60d64c572dc6638c4cee19d6e5506"
+ },
+ {
+ "name": "swimmer_tone1",
+ "unicode": "1F3CA-1F3FB",
+ "digest": "85a266a9131f6a1b37e758305ca43ffb46e3e07b0a465c5faefbdb5e5adeb7a4"
+ },
+ {
+ "name": "swimmer_tone2",
+ "unicode": "1F3CA-1F3FC",
+ "digest": "f2afdc4d05a2694e663a420d5ad82bd48c92aedc4137d0fd3725bf08c41bd12a"
+ },
+ {
+ "name": "swimmer_tone3",
+ "unicode": "1F3CA-1F3FD",
+ "digest": "b87ecc38fb9e8eeeef8b120164d758d3f6a68a407053b03261354fd7f90f43b6"
+ },
+ {
+ "name": "swimmer_tone4",
+ "unicode": "1F3CA-1F3FE",
+ "digest": "a08629cf3484953b851b357c6a04891fb97ac15e70c376bbb82af47479835e1c"
+ },
+ {
+ "name": "swimmer_tone5",
+ "unicode": "1F3CA-1F3FF",
+ "digest": "21d83f66b2ef3e348f9e14ec108b9a90262d9934039ebd573471d2bdcde68974"
+ },
+ {
+ "name": "symbols",
+ "unicode": "1F523",
+ "digest": "f33c3ce58374e23b8957c759016fdb5c56ef7fe812bd4e693ae8ff7574cf6bbf"
+ },
+ {
+ "name": "synagogue",
+ "unicode": "1F54D",
+ "digest": "b13402c3c5793ebf924335a87a9f69befb7a6c152fc2a288261b2c2d49842eb6"
+ },
+ {
+ "name": "syringe",
+ "unicode": "1F489",
+ "digest": "39e5e7530255ccf2ff35ec5c653568c8645a4711170c573117f796ea3438c44a"
+ },
+ {
+ "name": "taco",
+ "unicode": "1F32E",
+ "digest": "6b004ce7129e00abcc10278bba1b9c3d5ac71888b99bf353f9878d8e494e3e0d"
+ },
+ {
+ "name": "tada",
+ "unicode": "1F389",
+ "digest": "956a180a1f18e3a1252761e5b3713324f63975ee1fe32168b59b60aa4dd8b72b"
+ },
+ {
+ "name": "tanabata_tree",
+ "unicode": "1F38B",
+ "digest": "d074457ba347687bfc8397ec62edee6325c411356216e7d43acd3f60628a0bb8"
+ },
+ {
+ "name": "tangerine",
+ "unicode": "1F34A",
+ "digest": "1b46bb690458914220cba18c43d7ae0f6914adfee6dba7cf2bb58ed4e1854ad8"
+ },
+ {
+ "name": "taurus",
+ "unicode": "2649",
+ "digest": "ea87fb3baa32605107d63b60847e4873ad9e21b7e7b652e3721cde777168670d"
+ },
+ {
+ "name": "taxi",
+ "unicode": "1F695",
+ "digest": "f44249c643a96d924e1eb35f67a133f3ca61128e610a880afaa09a73c7bcaf9d"
+ },
+ {
+ "name": "tea",
+ "unicode": "1F375",
+ "digest": "56ab8c291de8320c5b339e1cfbe972696e4ea31c592cefa240eda9a3abdf4fa3"
+ },
+ {
+ "name": "telephone",
+ "unicode": "260E",
+ "digest": "609104588e00039199a2fef3190ee6a7be5fca7cb09b36ffe5a7d800aac69d8d"
+ },
+ {
+ "name": "telephone_black",
+ "unicode": "1F57F",
+ "digest": "c3a42a653a91d90c6b668f678419d5438f2e546050914b841623e57107e805db"
+ },
+ {
+ "name": "telephone_receiver",
+ "unicode": "1F4DE",
+ "digest": "e3bf6034de6cf2160893ba4990eba198185a6a3f9cd5767a63b048e41c297640"
+ },
+ {
+ "name": "telephone_white",
+ "unicode": "1F57E",
+ "digest": "62a7e0e50c53e9f85eba51a92882e6064be05997910d3f7700e1e957dbaf0581"
+ },
+ {
+ "name": "telescope",
+ "unicode": "1F52D",
+ "digest": "abe0aca5f2c78105b0e9e4c8ee7a40adcd9bb013e7c49d568076459bade73556"
+ },
+ {
+ "name": "ten",
+ "unicode": "1F51F",
+ "digest": "7593aa7ffe7192a2e35c6ccec76522f6243777783c9152c7c03419835ea58c03"
+ },
+ {
+ "name": "tennis",
+ "unicode": "1F3BE",
+ "digest": "0a5fad3f7f35da0f37761e2279c148dbe154fa14c0e2a0749209b8b2b213a388"
+ },
+ {
+ "name": "tent",
+ "unicode": "26FA",
+ "digest": "7ddf437d8d186e4e3c3e818d137518d590fa06098813c7fe20e1f2a9704feab2"
+ },
+ {
+ "name": "thermometer",
+ "unicode": "1F321",
+ "digest": "597d1714442698a22187fee4d57a2580322f7206c7d51e4519023824598ec08f"
+ },
+ {
+ "name": "thermometer_face",
+ "unicode": "1F912",
+ "digest": "f19c489d89dd2d39770a6c8725a20f3e98f9e5216774af60c0665fd6a03a7687"
+ },
+ {
+ "name": "thinking",
+ "unicode": "1F914",
+ "digest": "f64a9a18dca4c502b46f933838753a818b604a9d0268aa32eda26cbd31abc58c"
+ },
+ {
+ "name": "thought_balloon",
+ "unicode": "1F4AD",
+ "digest": "76c8513191641f0a79e878ccc0d83c4576984609810633f596db2f64cc684b7d"
+ },
+ {
+ "name": "thought_left",
+ "unicode": "1F5EC",
+ "digest": "4fd591bf4318df73d1b17f434a449d8e95f49cca53a3d8f4d1ca983f3809ef46"
+ },
+ {
+ "name": "thought_right",
+ "unicode": "1F5ED",
+ "digest": "0e8c0ce26e2d0e30894f5394b0736456e8268f775e0e7eda4c7dc3c2ff9231ae"
+ },
+ {
+ "name": "three",
+ "unicode": "0033-20E3",
+ "digest": "ca0147a8f67cea3bc2516fa8deef4325188359559786c94ff0b27f90eef04b88"
+ },
+ {
+ "name": "thumbs_down_reverse",
+ "unicode": "1F593",
+ "digest": "a8b561e389bc4e4b07fba70994f6445e5ddc6afe68922fcb6e9e7282d19ad958"
+ },
+ {
+ "name": "thumbs_up_reverse",
+ "unicode": "1F592",
+ "digest": "b6e52715c5ce590bfd08f6e05058ec3765ea2da341b11f9825d100608b173837"
+ },
+ {
+ "name": "thumbsdown",
+ "unicode": "1F44E",
+ "digest": "a98f742c9773e0d95c0de5e1c10d1ab373fa761378a205f27d095e85debe69a3"
+ },
+ {
+ "name": "thumbsdown_tone1",
+ "unicode": "1F44E-1F3FB",
+ "digest": "5d0a7c63d52eafe6267c552168c5557a66622009d565c3cf7b5378c1f6e84bce"
+ },
+ {
+ "name": "thumbsdown_tone2",
+ "unicode": "1F44E-1F3FC",
+ "digest": "ca5c15dc516660b2989a1c717bf3745fdfb6964c7acf3b938285ff6c7caf2ca2"
+ },
+ {
+ "name": "thumbsdown_tone3",
+ "unicode": "1F44E-1F3FD",
+ "digest": "05740e3568795270674dac9134198bf75b1b778c11daa71649c88c231859ec16"
+ },
+ {
+ "name": "thumbsdown_tone4",
+ "unicode": "1F44E-1F3FE",
+ "digest": "5ee93bcc2f515806462a7b303064beade2b22a3f43a8162e39fd65d15d772e27"
+ },
+ {
+ "name": "thumbsdown_tone5",
+ "unicode": "1F44E-1F3FF",
+ "digest": "5c9ef8d53cf6f755668ab6dabfbfcdfd4b95fd59db3b3dd60290efefe9c33994"
+ },
+ {
+ "name": "thumbsup",
+ "unicode": "1F44D",
+ "digest": "28b31df963773ba42a1a089f43cd89d0ce1ab0981e5410f41242e9a125fc1aee"
+ },
+ {
+ "name": "thumbsup_tone1",
+ "unicode": "1F44D-1F3FB",
+ "digest": "f6365942738d2128b6959d6672b3d295757dc8240703cb84a2b014ad78d67de3"
+ },
+ {
+ "name": "thumbsup_tone2",
+ "unicode": "1F44D-1F3FC",
+ "digest": "771d30146e4dc947a69057b05d32c765c8457ab02b5342889c5489acf27ef356"
+ },
+ {
+ "name": "thumbsup_tone3",
+ "unicode": "1F44D-1F3FD",
+ "digest": "0bb7bbfb654c6139260e1786e7ffa5a33f31e19410c1d4d15737fdf5dd4c721d"
+ },
+ {
+ "name": "thumbsup_tone4",
+ "unicode": "1F44D-1F3FE",
+ "digest": "df0927c5342f0075fbf4ea83b724e6f70c0466c54769c9ce4a5c2deb602b28aa"
+ },
+ {
+ "name": "thumbsup_tone5",
+ "unicode": "1F44D-1F3FF",
+ "digest": "0683ae08c50aaf186c6406680a60617679c7b4bccd0817f24b15911dbb06866f"
+ },
+ {
+ "name": "thunder_cloud_rain",
+ "unicode": "26C8",
+ "digest": "dd836f06b41a10d6ed9bcbdae291d2886847ff66dc3ede2427382e469f60674c"
+ },
+ {
+ "name": "ticket",
+ "unicode": "1F3AB",
+ "digest": "a7654a5529535120da3c377e72cd1f7997bdc2dabf1d44b584f7df7852b158f9"
+ },
+ {
+ "name": "tickets",
+ "unicode": "1F39F",
+ "digest": "ccafcc9583a84e847ff1eaa3d53187c5ab150a7d27c6a19363e59b9bc046b567"
+ },
+ {
+ "name": "tiger",
+ "unicode": "1F42F",
+ "digest": "9ebe3117f5f1b589ff8164f8d87dcc275923e0db87121d2cee0fdb9b56dfc4ac"
+ },
+ {
+ "name": "tiger2",
+ "unicode": "1F405",
+ "digest": "212c95dc60d52420a6320917fe3fdd0683b4edc1a2a2c4a1c60920d1f90f4bc3"
+ },
+ {
+ "name": "timer",
+ "unicode": "23F2",
+ "digest": "c48199312ed42ff53a33bb2791db19e2e2521223cd49d8f758ea95b9b379c5ff"
+ },
+ {
+ "name": "tired_face",
+ "unicode": "1F62B",
+ "digest": "ad687a956388ec53ca1e301a0abe2f1e2cfb9f73cd543dd61a21c7335a42e332"
+ },
+ {
+ "name": "tm",
+ "unicode": "2122",
+ "digest": "1156c8b0af40b336bbb6534b3302ac63eab009c4cd0476adcf1fc4669f04b647"
+ },
+ {
+ "name": "toilet",
+ "unicode": "1F6BD",
+ "digest": "a4a24529c21e00e0861f4160c771f0e90aae8f6aee7550ad30d3dbb3fabbd4be"
+ },
+ {
+ "name": "tokyo_tower",
+ "unicode": "1F5FC",
+ "digest": "6324f154f5f5c722044129e5bca03484aca1439911585e42c1c181ffa30b480c"
+ },
+ {
+ "name": "tomato",
+ "unicode": "1F345",
+ "digest": "41bb6de095b27815eacb74a70aea8f7d4fe1ff947182b112001dd47ae7e45fbb"
+ },
+ {
+ "name": "tone1",
+ "unicode": "1F3FB",
+ "digest": "5c62003a098b774c068be45d658db3c0dd38483c0871f7c8ae293bc1222c4f0c"
+ },
+ {
+ "name": "tone2",
+ "unicode": "1F3FC",
+ "digest": "3c636ecbc4e58c7a360f2338daaf44e7da598fd07e0ba1514bb5c0f83fc8819f"
+ },
+ {
+ "name": "tone3",
+ "unicode": "1F3FD",
+ "digest": "398a1e5441b64c9c2d033bbc01d7a8d90b4db30ea9f30e28f0a9120c72a48df8"
+ },
+ {
+ "name": "tone4",
+ "unicode": "1F3FE",
+ "digest": "ff4a12195aeb7494c785b81266efad8cd60c8022c407a0fc032a02e8b83216b3"
+ },
+ {
+ "name": "tone5",
+ "unicode": "1F3FF",
+ "digest": "9e9f0125b5d57011b7456c84719e6be6cf71d06c1b198081d0937c0979164a81"
+ },
+ {
+ "name": "tongue",
+ "unicode": "1F445",
+ "digest": "bf9dd7c65a8dc5d77eb013658a0a12a13f7b224a784e65e203d9584bb6b41427"
+ },
+ {
+ "name": "tools",
+ "unicode": "1F6E0",
+ "digest": "9b0a36dfdb475621d326359662b22cbdb80563c4f476aa5e7d7c00cdba605bd9"
+ },
+ {
+ "name": "top",
+ "unicode": "1F51D",
+ "digest": "d645030099aeb433307569e8e1c4342c1c411a8fefe50fdca7a3207a1a0db671"
+ },
+ {
+ "name": "tophat",
+ "unicode": "1F3A9",
+ "digest": "1082fb2ee2e98fe65d21081b74ca59b07adef85043e2d36f25cac69db2d31fd3"
+ },
+ {
+ "name": "track_next",
+ "unicode": "23ED",
+ "digest": "d5415ed140933f345fea8023a3d8fca30dcfcf7d19d9dc9771fa2cae9df62a3b"
+ },
+ {
+ "name": "track_previous",
+ "unicode": "23EE",
+ "digest": "97ff4a59a236e5cf506fa3577b20715b3b0197e0f343a50615b36185d5b835f1"
+ },
+ {
+ "name": "trackball",
+ "unicode": "1F5B2",
+ "digest": "8332503454ce42059d720c285fe2b15eb0562a0a4b234dccb0f3159bb30a91aa"
+ },
+ {
+ "name": "tractor",
+ "unicode": "1F69C",
+ "digest": "a41d304c41a85d966f6a7c301735fdbe2ae41f4471dd7dcd72023046ca2546d0"
+ },
+ {
+ "name": "traffic_light",
+ "unicode": "1F6A5",
+ "digest": "005f68d028fec8d9ae389cc2b23e1343a82c028eb32820d5e56f5c84eba315d1"
+ },
+ {
+ "name": "train",
+ "unicode": "1F68B",
+ "digest": "bf32893b7b9ecd248e8afe840624061746ac6ceb741e3e861ebfa46014f4bed4"
+ },
+ {
+ "name": "train2",
+ "unicode": "1F686",
+ "digest": "08a9732453a0b4f68dd2d3d3879f04ee538f65897913b5a5157c0585132a374a"
+ },
+ {
+ "name": "train_diesel",
+ "unicode": "1F6F2",
+ "digest": "621bb967cd93fa9f8fd4b155965cc7572d3f91f88d94938ba10c8626718b623c"
+ },
+ {
+ "name": "tram",
+ "unicode": "1F68A",
+ "digest": "5a86d31f7ab677d967fecd75babc900b5169766d0228961912314c4c4d1d64ee"
+ },
+ {
+ "name": "triangle_round",
+ "unicode": "1F6C6",
+ "digest": "e24bb39ecfaaa746b03dc8418697d09ef327d5b077db39014f39d5fb87e23bd5"
+ },
+ {
+ "name": "triangular_flag_on_post",
+ "unicode": "1F6A9",
+ "digest": "d824c973d84cd62c845d64e546de87b094fda8f9972b6a33acd75e1a5ac19f75"
+ },
+ {
+ "name": "triangular_ruler",
+ "unicode": "1F4D0",
+ "digest": "5576802d8bcb8836f473d9c7641ff666250c23c8476c676b253e577695025959"
+ },
+ {
+ "name": "trident",
+ "unicode": "1F531",
+ "digest": "70c1e8254da5b0e4552673b487503a20feeb249484d4596836b75de70220be82"
+ },
+ {
+ "name": "triumph",
+ "unicode": "1F624",
+ "digest": "b09262121b0d3d9d017ded22d0fbb1acaa6ee8c9d38e9ac34292b390d97408fe"
+ },
+ {
+ "name": "trolleybus",
+ "unicode": "1F68E",
+ "digest": "5af943836cc30c3b79160c70b6488c984fa63c104dce08c436597a93d30ff6f4"
+ },
+ {
+ "name": "trophy",
+ "unicode": "1F3C6",
+ "digest": "c249938815042716db2b39cdece6715fabf9e56ed583270c451925e6c91f9191"
+ },
+ {
+ "name": "tropical_drink",
+ "unicode": "1F379",
+ "digest": "352d903e813a27d2a74803322539b50a50aec0ca2ed7ab4a92ec480b1c226cb6"
+ },
+ {
+ "name": "tropical_fish",
+ "unicode": "1F420",
+ "digest": "13a104ca9c326238ab8d85b60759629b4efaa836946fbe58d78d779443475f7b"
+ },
+ {
+ "name": "truck",
+ "unicode": "1F69A",
+ "digest": "13d381d6b43b42350a1e24c02296904b8fdc38c1bf0939fc7037850127e91f21"
+ },
+ {
+ "name": "trumpet",
+ "unicode": "1F3BA",
+ "digest": "df7fb48920ac0919ee2d7b30102016479f747a5d4dd25b3e18d9f17121d232d1"
+ },
+ {
+ "name": "tulip",
+ "unicode": "1F337",
+ "digest": "519a84336464b5dc8db57eecef3e5b8ed82ccfdaa0ed0fa9ef7bcf0e8acea1f8"
+ },
+ {
+ "name": "turkey",
+ "unicode": "1F983",
+ "digest": "e87bff52ad3e301dc62f6832b8a6fcaf99db260a96263e4203a55ce3abda8cf8"
+ },
+ {
+ "name": "turned_ok_hand",
+ "unicode": "1F58F",
+ "digest": "8a6c5b7d4c737866e7e32c6d9f7f447a48a0ac57a8909d43f87367d4a9b59246"
+ },
+ {
+ "name": "turtle",
+ "unicode": "1F422",
+ "digest": "388b3e75b931638a09f65b842d26e2cc87b200ba782dec871f84cddd71aaeaf3"
+ },
+ {
+ "name": "tv",
+ "unicode": "1F4FA",
+ "digest": "dba03be6482d6291599c7393b0f749c0de5c873d45c96a20ccc53b3e104a6a24"
+ },
+ {
+ "name": "twisted_rightwards_arrows",
+ "unicode": "1F500",
+ "digest": "5fcad0247576e10e683f353008749975e9371a4f66c0901a73c3a0c7803c63c7"
+ },
+ {
+ "name": "two",
+ "unicode": "0032-20E3",
+ "digest": "20ad722532a5073fff8aef0a5e890421da0ae97f0723a8a2cc503c13d24ba597"
+ },
+ {
+ "name": "two_hearts",
+ "unicode": "1F495",
+ "digest": "160cb11e3ed2ae1b20957d445c6c4b4bd604d067294818dfeeefba4562425eb9"
+ },
+ {
+ "name": "two_men_holding_hands",
+ "unicode": "1F46C",
+ "digest": "923734704e544f7484fdb424bfe26f51ee07754db712cd151f8fbe955023a1ab"
+ },
+ {
+ "name": "two_women_holding_hands",
+ "unicode": "1F46D",
+ "digest": "58a40e7819cab3589ac81bb4fdc485b7196ee355544b54c6b00169028c260130"
+ },
+ {
+ "name": "u5272",
+ "unicode": "1F239",
+ "digest": "b7e8ad52629a1f1fca77a5c9a51da87ce2b9a81f6af9bcbe9bec9552d398e9bf"
+ },
+ {
+ "name": "u5408",
+ "unicode": "1F234",
+ "digest": "f359799d206cff6aae3af26eb8ad153abd38e817d4c70b2e5e5e8cf2f46e645e"
+ },
+ {
+ "name": "u55b6",
+ "unicode": "1F23A",
+ "digest": "c40293bea0f148e76ca5152e830b1b474380fe259180fbf74fece1ccc9afd8a3"
+ },
+ {
+ "name": "u6307",
+ "unicode": "1F22F",
+ "digest": "45449f7ae29da9e507c19d0f2b22f17f7cbd763f2ec87eb893be5bae49c7f78e"
+ },
+ {
+ "name": "u6708",
+ "unicode": "1F237",
+ "digest": "b897ead8c952013975ce6f381cdb8c584ebe4015311ef87f2a332c8a9e155d75"
+ },
+ {
+ "name": "u6709",
+ "unicode": "1F236",
+ "digest": "8b2f792abc1313a1a58f2fb8b37ad68a964004c962535f7739131257b1331a05"
+ },
+ {
+ "name": "u6e80",
+ "unicode": "1F235",
+ "digest": "fd982a56d4c492e63526b427bb948d7f155b0d5c414a68c7177698a71e72269b"
+ },
+ {
+ "name": "u7121",
+ "unicode": "1F21A",
+ "digest": "334f87a5254b58503d9f7a8ecc3d971a99839ec9c22c443469d72caca1750a48"
+ },
+ {
+ "name": "u7533",
+ "unicode": "1F238",
+ "digest": "3c8e743ae9960e43b9fa0cc698018fcb2a52ae34d143f0561298191f9def019c"
+ },
+ {
+ "name": "u7981",
+ "unicode": "1F232",
+ "digest": "a08bf39be3a54c076de79478c09b79c5c4d221853722870dd6e81abb78a4b64a"
+ },
+ {
+ "name": "u7a7a",
+ "unicode": "1F233",
+ "digest": "5dfb74a534a6490df989f84eac271c79d52f29313b6d43662dd0ff029794367c"
+ },
+ {
+ "name": "umbrella",
+ "unicode": "2614",
+ "digest": "ff1191f6c11b82f5337f78aadb58af50c69abaf676a384b0473bf49004e4018f"
+ },
+ {
+ "name": "umbrella2",
+ "unicode": "2602",
+ "digest": "aa7db9d6ed42dff847a8e5ee48a8eeff7a6e7f30de155a28951407f5aaa3dae2"
+ },
+ {
+ "name": "unamused",
+ "unicode": "1F612",
+ "digest": "efbbcaee6f3178afe509d74d13243ec6befe3112620a01e5079171eac4b32417"
+ },
+ {
+ "name": "underage",
+ "unicode": "1F51E",
+ "digest": "ae9a300fa400a57b7216a0a040fb8a5f02236fbceeeceed58bfd953c87ad51fe"
+ },
+ {
+ "name": "unicorn",
+ "unicode": "1F984",
+ "digest": "1b1e9c209dabe619db76fd346c3fb51b28ace0e4102697fe0973fe2d46aa9f08"
+ },
+ {
+ "name": "unlock",
+ "unicode": "1F513",
+ "digest": "63dbef0855399254ae01cf4ef0676adebc1432ae1ee260b569c23ae8152deaf8"
+ },
+ {
+ "name": "up",
+ "unicode": "1F199",
+ "digest": "902a3ecbcd73099a28476b49bc9e7b06da6cc002ee584e0501e5b625fb515088"
+ },
+ {
+ "name": "upside_down",
+ "unicode": "1F643",
+ "digest": "763fe2baf07a9b04f96958adf38a43c7dd2bc70d57398f49604307bd835cbb53"
+ },
+ {
+ "name": "urn",
+ "unicode": "26B1",
+ "digest": "dbfd5b90709d1b812d2fff71a5cfa10f84a4579866c2d7cd0e80759a22b2ba0e"
+ },
+ {
+ "name": "v",
+ "unicode": "270C",
+ "digest": "df85ad1a3ff365c3232a010701c9b25cd824d19fa2511422dee60ac231f457e3"
+ },
+ {
+ "name": "v_tone1",
+ "unicode": "270C-1F3FB",
+ "digest": "ce45db8de862b6f37d9208920d7c7c19335fac2cbff59b52be1ccbc01e3249da"
+ },
+ {
+ "name": "v_tone2",
+ "unicode": "270C-1F3FC",
+ "digest": "9036c8d793b02b4d2e6a4752b8ec319ec50efd6fcd6feef7b0671a63e5659acc"
+ },
+ {
+ "name": "v_tone3",
+ "unicode": "270C-1F3FD",
+ "digest": "a94b95f7656d62b442c99f2643b96b0c6114683401a94cdda68405c37efecc4c"
+ },
+ {
+ "name": "v_tone4",
+ "unicode": "270C-1F3FE",
+ "digest": "5c75f74993856f2faeeaee68df7689056e60d30e8c573039db8303167f7d0a80"
+ },
+ {
+ "name": "v_tone5",
+ "unicode": "270C-1F3FF",
+ "digest": "bb899672adb3c11f65983fbf9581de7f0a1bbac86fde146e799cea1126fe241e"
+ },
+ {
+ "name": "vertical_traffic_light",
+ "unicode": "1F6A6",
+ "digest": "36296e03620f16d35e5cec195cd97f5b358dfdedcd43bc1b3f7988ff7e85ab47"
+ },
+ {
+ "name": "vhs",
+ "unicode": "1F4FC",
+ "digest": "f4be55f4c23a85e0caacbf569742c117c8fd52c189465a6560cbd2f8873ad74f"
+ },
+ {
+ "name": "vibration_mode",
+ "unicode": "1F4F3",
+ "digest": "b9b8dfa3160c22f78b7d627cb52636d81ca6230a196cee5e94028e32e06b9a98"
+ },
+ {
+ "name": "video_camera",
+ "unicode": "1F4F9",
+ "digest": "3bfaa24e5fb00145e3e4dd07ecf569dabbb3f211551e46085ef23cf23002cfc3"
+ },
+ {
+ "name": "video_game",
+ "unicode": "1F3AE",
+ "digest": "4dcbd76030e37d0f7429852991a5f3f126cbdedfc124ecad0ba29d227375f6e2"
+ },
+ {
+ "name": "violin",
+ "unicode": "1F3BB",
+ "digest": "8ab7adc6e1e934f9e05009cd0a6d4da3136092c8f11c0606b91914be182206f5"
+ },
+ {
+ "name": "virgo",
+ "unicode": "264D",
+ "digest": "aaa19752756d0cac949445de1d2b8bf1f75a071368ae0acf5002f4acdc34826f"
+ },
+ {
+ "name": "volcano",
+ "unicode": "1F30B",
+ "digest": "86c17d61d66bfa868c02f1d31daca22f077c096368ef53cd9bfb9914a2f0b273"
+ },
+ {
+ "name": "volleyball",
+ "unicode": "1F3D0",
+ "digest": "b505684b13f814fbc08dc8ff652849328f46068276e0a24ae1961e2aff15868f"
+ },
+ {
+ "name": "vs",
+ "unicode": "1F19A",
+ "digest": "e31bd8b48b88c21d717964d1360a7751684dd1e0b63fdd655f1a9ec10a952dfb"
+ },
+ {
+ "name": "vulcan",
+ "unicode": "1F596",
+ "digest": "ca800fce797e652c5f47bf44992e8fbe19554688a36423fdf7c29ca6defae1e0"
+ },
+ {
+ "name": "vulcan_tone1",
+ "unicode": "1F596-1F3FB",
+ "digest": "84bafdaca43426b053f5caa4e868ca109d99113a28ea9799db09d3c5d5f645c8"
+ },
+ {
+ "name": "vulcan_tone2",
+ "unicode": "1F596-1F3FC",
+ "digest": "e7cedf63ead957ee5c287e4cb0828ba70673e17b604f92b529875c32d094e7e3"
+ },
+ {
+ "name": "vulcan_tone3",
+ "unicode": "1F596-1F3FD",
+ "digest": "e124fef20f289921553274cf834f6dcc1a012889d30d9874dc5ad01afb8235b8"
+ },
+ {
+ "name": "vulcan_tone4",
+ "unicode": "1F596-1F3FE",
+ "digest": "ea2115f549e4680467521bbf362b229f4a8f0fdadbfaf231378d801f9b369f08"
+ },
+ {
+ "name": "vulcan_tone5",
+ "unicode": "1F596-1F3FF",
+ "digest": "1b322e1252491f35ae02f0b279b6529dad867f2a6b3c2c3e77f981bed07e447d"
+ },
+ {
+ "name": "walking",
+ "unicode": "1F6B6",
+ "digest": "8ec0b2207d4368422261bc58944c17dff2554b2356becfb18f21dd87425cd67b"
+ },
+ {
+ "name": "walking_tone1",
+ "unicode": "1F6B6-1F3FB",
+ "digest": "9ee2224226326833fb0c9598c737fbd2f6bca1c81f082537e9f22ea1de4ff48e"
+ },
+ {
+ "name": "walking_tone2",
+ "unicode": "1F6B6-1F3FC",
+ "digest": "4855d521e937d10d58eeb2bbada493699e31e1098128f81a9e3303bcf3edeb49"
+ },
+ {
+ "name": "walking_tone3",
+ "unicode": "1F6B6-1F3FD",
+ "digest": "82669cf7167054a3615add01059f87dbb809edac3889ee171d5994de90448000"
+ },
+ {
+ "name": "walking_tone4",
+ "unicode": "1F6B6-1F3FE",
+ "digest": "c11f03aa96248272f831f68b93c5b21b2ecbffeb1b4c1c13373bf539ee7db8f8"
+ },
+ {
+ "name": "walking_tone5",
+ "unicode": "1F6B6-1F3FF",
+ "digest": "18238ee121a64211f6bcdbd475cee4ad6debe2bf421daba53d125aa005c26d10"
+ },
+ {
+ "name": "waning_crescent_moon",
+ "unicode": "1F318",
+ "digest": "96ef03ff85247877255a5ca3e8a8bb63f7d41f66531e8db61cbcd863e3ad7355"
+ },
+ {
+ "name": "waning_gibbous_moon",
+ "unicode": "1F316",
+ "digest": "994223113ad151e6b42ee317a10dad18f86759a308e61ab88eeb10ab780aae67"
+ },
+ {
+ "name": "warning",
+ "unicode": "26A0",
+ "digest": "a702e51efd1a3ab425eada008ccf694f38a71db14bb710edacc2e206d61f5ca3"
+ },
+ {
+ "name": "wastebasket",
+ "unicode": "1F5D1",
+ "digest": "afecb31aaf5078298ab9f7c5da29a49ce0cdefe477ee50889be9c0e43ccf1799"
+ },
+ {
+ "name": "watch",
+ "unicode": "231A",
+ "digest": "410334c87b8552f601f4ea1b7e36582a8b22f11b804d5ab1008d4af2b5a0cbe6"
+ },
+ {
+ "name": "water_buffalo",
+ "unicode": "1F403",
+ "digest": "d1becfaea464372c46e5442c6030ea355806ce5864c2435c123a9bb3a2c3c5eb"
+ },
+ {
+ "name": "watermelon",
+ "unicode": "1F349",
+ "digest": "88dd78812520c44080c79fe8cb1825bc713e5155da2ce8c73286333749e7035e"
+ },
+ {
+ "name": "wave",
+ "unicode": "1F44B",
+ "digest": "5103c49914ff1a2d76a1ab6db2530ddd9f48b98b708ab15292ceadf28873c939"
+ },
+ {
+ "name": "wave_tone1",
+ "unicode": "1F44B-1F3FB",
+ "digest": "ef2d79f377d09dedd1e900b2f4e4a2412bf562cd88484f71c52d465053f8aae9"
+ },
+ {
+ "name": "wave_tone2",
+ "unicode": "1F44B-1F3FC",
+ "digest": "d323e6e2e9ce035bc11b98226d46ab393dfdf3909d99e7a828b51950e6574656"
+ },
+ {
+ "name": "wave_tone3",
+ "unicode": "1F44B-1F3FD",
+ "digest": "8a8a386d53252455c20d6b235c462fd9cb3b20c9c19c67e67b3dece4621b5cf6"
+ },
+ {
+ "name": "wave_tone4",
+ "unicode": "1F44B-1F3FE",
+ "digest": "a8281c2ab9cf6e2b3d3cad24707fe412ec2398195530b716a2617477416c0432"
+ },
+ {
+ "name": "wave_tone5",
+ "unicode": "1F44B-1F3FF",
+ "digest": "5ccbee95bfc180580c8a02b88146110c4d132b8ea618dd6a58f03c1db921d58d"
+ },
+ {
+ "name": "wavy_dash",
+ "unicode": "3030",
+ "digest": "b5b67fc12938801a98ff22b6f7b566c603f58c183737fa740a500724879f0e99"
+ },
+ {
+ "name": "waxing_crescent_moon",
+ "unicode": "1F312",
+ "digest": "20446122d170b18f88ea71524f6747d42b97f9d765c52e676e5163fee58ec379"
+ },
+ {
+ "name": "waxing_gibbous_moon",
+ "unicode": "1F314",
+ "digest": "4324e43d4d45e6333f7379c9feb8efd3093d76f3920d7dc5ad3c615e76104998"
+ },
+ {
+ "name": "wc",
+ "unicode": "1F6BE",
+ "digest": "cb7c5d35bf11149d12cda2c0897cb6038e043127055bbe2e8e33c9b422d6d8fc"
+ },
+ {
+ "name": "weary",
+ "unicode": "1F629",
+ "digest": "29a291033a1b67eda3710dffae42d63fcfa663e37dab728c236172f3e877fe8f"
+ },
+ {
+ "name": "wedding",
+ "unicode": "1F492",
+ "digest": "6c7d874f464c9c76b0d767135aa40ced94089b5f71d373098b47488d7f3ef7c4"
+ },
+ {
+ "name": "whale",
+ "unicode": "1F433",
+ "digest": "94168acda6ba502b64ea50ff4aaafb7e6258d7c6806e91f090c8a3c46edc5b6d"
+ },
+ {
+ "name": "whale2",
+ "unicode": "1F40B",
+ "digest": "e1cde2308bd510b2449c96e88ffec796856f98b19ceedc1cd7e9ea009dae1417"
+ },
+ {
+ "name": "wheel_of_dharma",
+ "unicode": "2638",
+ "digest": "bbd6927697c22a1c3e56fd0c9933d9e00dbf120505fe48d02cb486bcd67a8b2c"
+ },
+ {
+ "name": "wheelchair",
+ "unicode": "267F",
+ "digest": "513f759acf528f6a7e39d9de1d171c3faebe645c9cf3bd86b185123016beef95"
+ },
+ {
+ "name": "white_check_mark",
+ "unicode": "2705",
+ "digest": "a0b3bf7c4fb131e7a9fab5169ea4094e2665e02cedaa091f0d6e78609b2f17ed"
+ },
+ {
+ "name": "white_circle",
+ "unicode": "26AA",
+ "digest": "2e7323fa4d1e3929e529d49210a0b82a043eae4f7c95128ec86b98c46fdb0e7c"
+ },
+ {
+ "name": "white_flower",
+ "unicode": "1F4AE",
+ "digest": "a3efea4950e09994f5e9d3d16f0728969238302304a6cce90b293c56e9a3e20c"
+ },
+ {
+ "name": "white_large_square",
+ "unicode": "2B1C",
+ "digest": "99c4442a65f2e3c568f45aed9e74590206c517a716557f4d741d967c9f42ed40"
+ },
+ {
+ "name": "white_medium_small_square",
+ "unicode": "25FD",
+ "digest": "a1edfeb4e540dcc020ba5dde19f7a18d90966788baa5382a22a0f9038d593f01"
+ },
+ {
+ "name": "white_medium_square",
+ "unicode": "25FB",
+ "digest": "794c2339ca71bb6d65ac488fb7b5dc4f0a2412f30890d2c4ece53cdbf52ba78b"
+ },
+ {
+ "name": "white_small_square",
+ "unicode": "25AB",
+ "digest": "9c4c308070a0c4524993cc36feaa778aad8f0df9f209b82d28b1f3811c441bc4"
+ },
+ {
+ "name": "white_square_button",
+ "unicode": "1F533",
+ "digest": "f46e18c7250c874d1b4d6117eda741d86a081352e76f3d019dd64af2669fa4bb"
+ },
+ {
+ "name": "white_sun_cloud",
+ "unicode": "1F325",
+ "digest": "d8ce416e6bdb0e59e06e2fceac3177dbe59fefc248fd8c6d76b80d1418141070"
+ },
+ {
+ "name": "white_sun_rain_cloud",
+ "unicode": "1F326",
+ "digest": "d2b132518261864ac4a95707eaeea335dd8351ed2b8ef4e2272ced456e309bf1"
+ },
+ {
+ "name": "white_sun_small_cloud",
+ "unicode": "1F324",
+ "digest": "b86a72f1cdb4d24fd3ab180aae9db012ca51fc01f3786aab596c2e330066b185"
+ },
+ {
+ "name": "wind_blowing_face",
+ "unicode": "1F32C",
+ "digest": "20bdeb8e39dc637792ac9fbee031c5791889f3126e83556ba51f98809c19763c"
+ },
+ {
+ "name": "wind_chime",
+ "unicode": "1F390",
+ "digest": "1fc26f33ce13b6a969bb76e914de054ec5d1c7c4cd1dc5ee8fea5f3149f794d8"
+ },
+ {
+ "name": "wine_glass",
+ "unicode": "1F377",
+ "digest": "7dfcf9c5195a20fd2745b19e102910392b0fc8f1650b98ab81957807841935e0"
+ },
+ {
+ "name": "wink",
+ "unicode": "1F609",
+ "digest": "404ac6c920414ca35894da1d97b3b2fabe92bd09569274eb5798fbb297129036"
+ },
+ {
+ "name": "wolf",
+ "unicode": "1F43A",
+ "digest": "ebadd7766c4a314b4027c32435a2f5727a6283123dfb8834e10251cbfc07ca2f"
+ },
+ {
+ "name": "woman",
+ "unicode": "1F469",
+ "digest": "9f0dbb5d1e0db4f008141582dcb6413f5aebaa13e191349c976a435b2bee0956"
+ },
+ {
+ "name": "woman_tone1",
+ "unicode": "1F469-1F3FB",
+ "digest": "c1f2a503481fdd96cfbfa7d556500f8e0da0cea1c72ed1078ecbb6962221c22a"
+ },
+ {
+ "name": "woman_tone2",
+ "unicode": "1F469-1F3FC",
+ "digest": "bf78b3a8f7424037069f8ac337e154ef185f55026c71a6cf6dbe15eb42ef9813"
+ },
+ {
+ "name": "woman_tone3",
+ "unicode": "1F469-1F3FD",
+ "digest": "4ccd70a2052b932b3395ac0a957c05815327dc8082fd461abcd797411db8ce05"
+ },
+ {
+ "name": "woman_tone4",
+ "unicode": "1F469-1F3FE",
+ "digest": "71b5efc4a410102e60048ca05f87587384a6db309f3be94109a4f92ea97072dc"
+ },
+ {
+ "name": "woman_tone5",
+ "unicode": "1F469-1F3FF",
+ "digest": "91a1cd015731f4db501c276a8236eb0665e4dc7aa1891e2a67b8d3e543fbea9c"
+ },
+ {
+ "name": "womans_clothes",
+ "unicode": "1F45A",
+ "digest": "599332c0b863a40fd0c319e4e0f52ae847326a96d180c288e0466b3ac308a27e"
+ },
+ {
+ "name": "womans_hat",
+ "unicode": "1F452",
+ "digest": "231ff55c3fa56d8fb5731fe41f547e67ffacfdde82286f45d4ca65a2d2821239"
+ },
+ {
+ "name": "womens",
+ "unicode": "1F6BA",
+ "digest": "f971429456b543804412490af2e27e0b14d0d536a156db898bce67b136e1b563"
+ },
+ {
+ "name": "worried",
+ "unicode": "1F61F",
+ "digest": "e017f636e79b9301f3a06471a5f3513ba7dbb9b97938de1140c1df4c32fd8844"
+ },
+ {
+ "name": "wrench",
+ "unicode": "1F527",
+ "digest": "c9ded4f7f496bad8691677226310bbd31bb485722ea479bc7a68a2b4ef9d55d9"
+ },
+ {
+ "name": "writing_hand",
+ "unicode": "1F58E",
+ "digest": "c4fc18ece6778339ebe14438aaf570e22385c3010c2d341824fa72ac6068cfeb"
+ },
+ {
+ "name": "writing_hand_tone1",
+ "unicode": "270D-1F3FB",
+ "digest": "38e64e6dca4847a12aef8a117c113b2025d841501c4bc8188c57d0c8a4f1e34d"
+ },
+ {
+ "name": "writing_hand_tone2",
+ "unicode": "270D-1F3FC",
+ "digest": "2b2d0ac2701ae707c31d9c85feb2e3700e11398701e2b0519338897817d53baf"
+ },
+ {
+ "name": "writing_hand_tone3",
+ "unicode": "270D-1F3FD",
+ "digest": "85d67f90ff8bd2e7157f28fd857e6730b660a7eb82eb5350f57671f728ce725b"
+ },
+ {
+ "name": "writing_hand_tone4",
+ "unicode": "270D-1F3FE",
+ "digest": "056c05c201b3d0972433f00910967ad7334e37726e2956fee053ec2e1a9153c7"
+ },
+ {
+ "name": "writing_hand_tone5",
+ "unicode": "270D-1F3FF",
+ "digest": "95c59157d301ee08990e4302fd9bdd7953e1d1abed09636d0837d84e44f53ba6"
+ },
+ {
+ "name": "x",
+ "unicode": "274C",
+ "digest": "1d256b0015b9cbdeaa4558f9241782c89d86c79a42e507621f7949c56a90b6c0"
+ },
+ {
+ "name": "yellow_heart",
+ "unicode": "1F49B",
+ "digest": "e869a80266b4379a8d82988fef25e187632bfb076ae619f576e416906cd688a7"
+ },
+ {
+ "name": "yen",
+ "unicode": "1F4B4",
+ "digest": "8f3d801c687e585e4497123c5c91a8b0c558578deec6a8c1591b25e64a3a8992"
+ },
+ {
+ "name": "yin_yang",
+ "unicode": "262F",
+ "digest": "e8ea4c686518ad6165e15ed67b529f2f1e20d648aa2ecb7e9bff5a6067dd3fea"
+ },
+ {
+ "name": "yum",
+ "unicode": "1F60B",
+ "digest": "d9c97bbf6bdb6e39977437680f0b37c9335306c51e01114056ae1d4c9c85b0e0"
+ },
+ {
+ "name": "zap",
+ "unicode": "26A1",
+ "digest": "37588734c7fe330ae35e6ee99e7cf4183e8fe1bc01f6bbbc6293b21076a338cb"
+ },
+ {
+ "name": "zero",
+ "unicode": "0030-20E3",
+ "digest": "519c927db8264d5379ab2c6a18656ea6dd1ceb2afc92eb48563bf86af4697571"
+ },
+ {
+ "name": "zipper_mouth",
+ "unicode": "1F910",
+ "digest": "8396249161b6d865861b56aabd17cae2c821b0d814f4249bf8cab0bb21fa8ee9"
+ },
+ {
+ "name": "zzz",
+ "unicode": "1F4A4",
+ "digest": "f07c56d2d55c0a886c26a8e3d49a9adeab54cc1a0c0354ea8d3bf23aaed3176d"
+ }
+] \ No newline at end of file
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 71197205f34..340fc5452ab 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -8,7 +8,7 @@ module API
expose :id, :state, :avatar_url
expose :web_url do |user, options|
- Gitlab::Application.routes.url_helpers.user_url(user)
+ Gitlab::Routing.url_helpers.user_url(user)
end
end
@@ -85,11 +85,11 @@ module API
end
class Group < Grape::Entity
- expose :id, :name, :path, :description
+ expose :id, :name, :path, :description, :visibility_level
expose :avatar_url
expose :web_url do |group, options|
- Gitlab::Application.routes.url_helpers.group_url(group)
+ Gitlab::Routing.url_helpers.group_url(group)
end
end
@@ -292,7 +292,7 @@ module API
end
class Label < Grape::Entity
- expose :name, :color
+ expose :name, :color, :description
end
class Compare < Grape::Entity
@@ -334,12 +334,12 @@ module API
expose :updated_at
expose :home_page_url
expose :default_branch_protection
- expose :twitter_sharing_enabled
expose :restricted_visibility_levels
expose :max_attachment_size
expose :session_expire_delay
expose :default_project_visibility
expose :default_snippet_visibility
+ expose :default_group_visibility
expose :restricted_signup_domains
expose :user_oauth_applications
expose :after_sign_out_path
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 1a14d870a4a..c165de21a75 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -31,7 +31,7 @@ module API
authorize! :create_group, current_user
required_attributes! [:name, :path]
- attrs = attributes_for_keys [:name, :path, :description]
+ attrs = attributes_for_keys [:name, :path, :description, :visibility_level]
@group = Group.new(attrs)
if @group.save
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index a72044e8058..4921ae99e78 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -118,9 +118,7 @@ module API
end
def authorize!(action, subject)
- unless abilities.allowed?(current_user, action, subject)
- forbidden!
- end
+ forbidden! unless abilities.allowed?(current_user, action, subject)
end
def authorize_push_project
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 252744515da..1fee1dee1a6 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -82,7 +82,7 @@ module API
# GET /projects/:id/issues?milestone=1.0.0&state=closed
# GET /issues?iid=42
get ":id/issues" do
- issues = user_project.issues
+ issues = user_project.issues.visible_to_user(current_user)
issues = filter_issues_state(issues, params[:state]) unless params[:state].nil?
issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil?
@@ -104,23 +104,28 @@ module API
# GET /projects/:id/issues/:issue_id
get ":id/issues/:issue_id" do
@issue = user_project.issues.find(params[:issue_id])
+ not_found! unless can?(current_user, :read_issue, @issue)
present @issue, with: Entities::Issue
end
# Create a new project issue
#
# Parameters:
- # id (required) - The ID of a project
- # title (required) - The title of an issue
- # description (optional) - The description of an issue
- # assignee_id (optional) - The ID of a user to assign issue
+ # id (required) - The ID of a project
+ # title (required) - The title of an issue
+ # description (optional) - The description of an issue
+ # assignee_id (optional) - The ID of a user to assign issue
# milestone_id (optional) - The ID of a milestone to assign issue
- # labels (optional) - The labels of an issue
+ # labels (optional) - The labels of an issue
+ # created_at (optional) - The date
# Example Request:
# POST /projects/:id/issues
post ":id/issues" do
required_attributes! [:title]
- attrs = attributes_for_keys [:title, :description, :assignee_id, :milestone_id]
+
+ keys = [:title, :description, :assignee_id, :milestone_id]
+ keys << :created_at if current_user.admin? || user_project.owner == current_user
+ attrs = attributes_for_keys(keys)
# Validate label names in advance
if (errors = validate_label_params(params)).any?
@@ -190,7 +195,7 @@ module API
end
end
- # Delete a project issue (deprecated)
+ # Delete a project issue
#
# Parameters:
# id (required) - The ID of a project
@@ -198,7 +203,10 @@ module API
# Example Request:
# DELETE /projects/:id/issues/:issue_id
delete ":id/issues/:issue_id" do
- not_allowed!
+ issue = user_project.issues.find_by(id: params[:issue_id])
+
+ authorize!(:destroy_issue, issue)
+ issue.destroy
end
end
end
diff --git a/lib/api/labels.rb b/lib/api/labels.rb
index 78ca58ad0d1..4af6bef0fa7 100644
--- a/lib/api/labels.rb
+++ b/lib/api/labels.rb
@@ -17,17 +17,18 @@ module API
# Creates a new label
#
# Parameters:
- # id (required) - The ID of a project
- # name (required) - The name of the label to be deleted
- # color (required) - Color of the label given in 6-digit hex
- # notation with leading '#' sign (e.g. #FFAABB)
+ # id (required) - The ID of a project
+ # name (required) - The name of the label to be created
+ # color (required) - Color of the label given in 6-digit hex
+ # notation with leading '#' sign (e.g. #FFAABB)
+ # description (optional) - The description of label to be created
# Example Request:
# POST /projects/:id/labels
post ':id/labels' do
authorize! :admin_label, user_project
required_attributes! [:name, :color]
- attrs = attributes_for_keys [:name, :color]
+ attrs = attributes_for_keys [:name, :color, :description]
label = user_project.find_label(attrs[:name])
conflict!('Label already exists') if label
@@ -62,11 +63,12 @@ module API
# Updates an existing label. At least one optional parameter is required.
#
# Parameters:
- # id (required) - The ID of a project
- # name (required) - The name of the label to be deleted
- # new_name (optional) - The new name of the label
- # color (optional) - Color of the label given in 6-digit hex
- # notation with leading '#' sign (e.g. #FFAABB)
+ # id (required) - The ID of a project
+ # name (required) - The name of the label to be deleted
+ # new_name (optional) - The new name of the label
+ # color (optional) - Color of the label given in 6-digit hex
+ # notation with leading '#' sign (e.g. #FFAABB)
+ # description (optional) - The description of label to be created
# Example Request:
# PUT /projects/:id/labels
put ':id/labels' do
@@ -76,7 +78,7 @@ module API
label = user_project.find_label(params[:name])
not_found!('Label not found') unless label
- attrs = attributes_for_keys [:new_name, :color]
+ attrs = attributes_for_keys [:new_name, :color, :description]
if attrs.empty?
render_api_error!('Required parameters "new_name" or "color" ' \
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index c5e5d57ed4d..93052fba06b 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -100,6 +100,18 @@ module API
end
end
+ # Delete a MR
+ #
+ # Parameters:
+ # id (required) - The ID of the project
+ # merge_request_id (required) - The MR id
+ delete ":id/merge_requests/:merge_request_id" do
+ merge_request = user_project.merge_requests.find_by(id: params[:merge_request_id])
+
+ authorize!(:destroy_merge_request, merge_request)
+ merge_request.destroy
+ end
+
# Routing "merge_request/:merge_request_id/..." is DEPRECATED and WILL BE REMOVED in version 9.0
# Use "merge_requests/:merge_request_id/..." instead.
#
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 6fcb5261e40..24b31005475 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -244,6 +244,34 @@ module API
end
end
+ # Archive project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # Example Request:
+ # PUT /projects/:id/archive
+ post ':id/archive' do
+ authorize!(:archive_project, user_project)
+
+ user_project.archive!
+
+ present user_project, with: Entities::Project
+ end
+
+ # Unarchive project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # Example Request:
+ # PUT /projects/:id/unarchive
+ post ':id/unarchive' do
+ authorize!(:archive_project, user_project)
+
+ user_project.unarchive!
+
+ present user_project, with: Entities::Project
+ end
+
# Remove project
#
# Parameters:
diff --git a/lib/award_emoji.rb b/lib/award_emoji.rb
index 783fcfb61ad..4fc3443ac68 100644
--- a/lib/award_emoji.rb
+++ b/lib/award_emoji.rb
@@ -48,4 +48,23 @@ class AwardEmoji
JSON.parse(File.read(json_path))
end
end
+
+ # Returns an Array of Emoji names and their asset URLs.
+ def self.urls
+ @urls ||= begin
+ path = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
+ prefix = Gitlab::Application.config.assets.prefix
+ digest = Gitlab::Application.config.assets.digest
+
+ JSON.parse(File.read(path)).map do |hash|
+ if digest
+ fname = "#{hash['unicode']}-#{hash['digest']}"
+ else
+ fname = hash['unicode']
+ end
+
+ { name: hash['name'], path: "#{prefix}/#{fname}.png" }
+ end
+ end
+ end
end
diff --git a/lib/banzai/filter.rb b/lib/banzai/filter.rb
index 905c4c0144e..3eb544dfef9 100644
--- a/lib/banzai/filter.rb
+++ b/lib/banzai/filter.rb
@@ -1,5 +1,3 @@
-require 'active_support/core_ext/string/output_safety'
-
module Banzai
module Filter
def self.[](name)
diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb
index 856f56fb175..fac7dad3243 100644
--- a/lib/banzai/filter/autolink_filter.rb
+++ b/lib/banzai/filter/autolink_filter.rb
@@ -1,4 +1,3 @@
-require 'html/pipeline/filter'
require 'uri'
module Banzai
diff --git a/lib/banzai/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb
index 470727ee312..b469ea0f626 100644
--- a/lib/banzai/filter/commit_range_reference_filter.rb
+++ b/lib/banzai/filter/commit_range_reference_filter.rb
@@ -43,7 +43,7 @@ module Banzai
end
def url_for_object(range, project)
- h = Gitlab::Application.routes.url_helpers
+ h = Gitlab::Routing.url_helpers
h.namespace_project_compare_url(project.namespace, project,
range.to_param.merge(only_path: context[:only_path]))
end
diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb
index 713a56ba949..bd88207326c 100644
--- a/lib/banzai/filter/commit_reference_filter.rb
+++ b/lib/banzai/filter/commit_reference_filter.rb
@@ -37,7 +37,7 @@ module Banzai
end
def url_for_object(commit, project)
- h = Gitlab::Application.routes.url_helpers
+ h = Gitlab::Routing.url_helpers
h.namespace_project_commit_url(project.namespace, project, commit,
only_path: context[:only_path])
end
diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb
index 207437ba7cf..d25de900674 100644
--- a/lib/banzai/filter/emoji_filter.rb
+++ b/lib/banzai/filter/emoji_filter.rb
@@ -1,7 +1,3 @@
-require 'action_controller'
-require 'gitlab_emoji'
-require 'html/pipeline/filter'
-
module Banzai
module Filter
# HTML filter that replaces :emoji: with images.
diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb
index 8d368f3b9e7..d179bea181e 100644
--- a/lib/banzai/filter/external_link_filter.rb
+++ b/lib/banzai/filter/external_link_filter.rb
@@ -1,5 +1,3 @@
-require 'html/pipeline/filter'
-
module Banzai
module Filter
# HTML Filter to add a `rel="nofollow"` attribute to external links
diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb
index f31f921903b..7ce26db1b90 100644
--- a/lib/banzai/filter/gollum_tags_filter.rb
+++ b/lib/banzai/filter/gollum_tags_filter.rb
@@ -1,6 +1,3 @@
-require 'banzai'
-require 'html/pipeline/filter'
-
module Banzai
module Filter
# HTML Filter for parsing Gollum's tags in HTML. It's only parses the
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
index 9f08aa36e8b..2732e0b5145 100644
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ b/lib/banzai/filter/issue_reference_filter.rb
@@ -9,6 +9,11 @@ module Banzai
Issue
end
+ def self.user_can_see_reference?(user, node, context)
+ issue = Issue.find(node.attr('data-issue')) rescue nil
+ Ability.abilities.allowed?(user, :read_issue, issue)
+ end
+
def find_object(project, id)
project.get_issue(id)
end
diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb
index 8147e5ed3c7..a2987850d03 100644
--- a/lib/banzai/filter/label_reference_filter.rb
+++ b/lib/banzai/filter/label_reference_filter.rb
@@ -31,7 +31,7 @@ module Banzai
end
def url_for_object(label, project)
- h = Gitlab::Application.routes.url_helpers
+ h = Gitlab::Routing.url_helpers
h.namespace_project_issues_url(project.namespace, project, label_name: label.name,
only_path: context[:only_path])
end
diff --git a/lib/banzai/filter/markdown_filter.rb b/lib/banzai/filter/markdown_filter.rb
index 0659fed1419..9b209533a89 100644
--- a/lib/banzai/filter/markdown_filter.rb
+++ b/lib/banzai/filter/markdown_filter.rb
@@ -1,5 +1,3 @@
-require 'html/pipeline/filter'
-
module Banzai
module Filter
class MarkdownFilter < HTML::Pipeline::TextFilter
diff --git a/lib/banzai/filter/merge_request_reference_filter.rb b/lib/banzai/filter/merge_request_reference_filter.rb
index 57c71708992..cad38a51851 100644
--- a/lib/banzai/filter/merge_request_reference_filter.rb
+++ b/lib/banzai/filter/merge_request_reference_filter.rb
@@ -14,7 +14,7 @@ module Banzai
end
def url_for_object(mr, project)
- h = Gitlab::Application.routes.url_helpers
+ h = Gitlab::Routing.url_helpers
h.namespace_project_merge_request_url(project.namespace, project, mr,
only_path: context[:only_path])
end
diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb
index e88b27c1fae..4cb82178024 100644
--- a/lib/banzai/filter/milestone_reference_filter.rb
+++ b/lib/banzai/filter/milestone_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# HTML filter that replaces milestone references with links.
@@ -13,7 +11,7 @@ module Banzai
end
def url_for_object(issue, project)
- h = Gitlab::Application.routes.url_helpers
+ h = Gitlab::Routing.url_helpers
h.namespace_project_milestone_url(project.namespace, project, milestone,
only_path: context[:only_path])
end
diff --git a/lib/banzai/filter/redactor_filter.rb b/lib/banzai/filter/redactor_filter.rb
index 7141ed7c9bd..e589b5df6ec 100644
--- a/lib/banzai/filter/redactor_filter.rb
+++ b/lib/banzai/filter/redactor_filter.rb
@@ -1,5 +1,3 @@
-require 'html/pipeline/filter'
-
module Banzai
module Filter
# HTML filter that removes references to records that the current user does
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index 3637b1bac94..a3326ae042c 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -1,6 +1,3 @@
-require 'active_support/core_ext/string/output_safety'
-require 'html/pipeline/filter'
-
module Banzai
module Filter
# Base class for GitLab Flavored Markdown reference filters.
@@ -47,6 +44,7 @@ module Banzai
# Returns a String
def data_attribute(attributes = {})
attributes[:reference_filter] = self.class.name.demodulize
+ attributes.delete(:original) if context[:no_original_data]
attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") }.join(" ")
end
diff --git a/lib/banzai/filter/reference_gatherer_filter.rb b/lib/banzai/filter/reference_gatherer_filter.rb
index 86d484feb90..96fdb06304e 100644
--- a/lib/banzai/filter/reference_gatherer_filter.rb
+++ b/lib/banzai/filter/reference_gatherer_filter.rb
@@ -1,5 +1,3 @@
-require 'html/pipeline/filter'
-
module Banzai
module Filter
# HTML filter that gathers all referenced records that the current user has
diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb
index 41380627d39..ea21c7b041c 100644
--- a/lib/banzai/filter/relative_link_filter.rb
+++ b/lib/banzai/filter/relative_link_filter.rb
@@ -1,4 +1,3 @@
-require 'html/pipeline/filter'
require 'uri'
module Banzai
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index e8011519608..42dbab9d27e 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -1,6 +1,3 @@
-require 'html/pipeline/filter'
-require 'html/pipeline/sanitization_filter'
-
module Banzai
module Filter
# Sanitize HTML
diff --git a/lib/banzai/filter/snippet_reference_filter.rb b/lib/banzai/filter/snippet_reference_filter.rb
index c870a42f741..d507eb5ebe1 100644
--- a/lib/banzai/filter/snippet_reference_filter.rb
+++ b/lib/banzai/filter/snippet_reference_filter.rb
@@ -14,7 +14,7 @@ module Banzai
end
def url_for_object(snippet, project)
- h = Gitlab::Application.routes.url_helpers
+ h = Gitlab::Routing.url_helpers
h.namespace_project_snippet_url(project.namespace, project, snippet,
only_path: context[:only_path])
end
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index 8c5855e5ffc..62a79c62e20 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -1,4 +1,3 @@
-require 'html/pipeline/filter'
require 'rouge/plugins/redcarpet'
module Banzai
diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb
index 4056dcd6d64..a4eda6fdf76 100644
--- a/lib/banzai/filter/table_of_contents_filter.rb
+++ b/lib/banzai/filter/table_of_contents_filter.rb
@@ -1,5 +1,3 @@
-require 'html/pipeline/filter'
-
module Banzai
module Filter
# HTML filter that adds an anchor child element to all Headers in a
diff --git a/lib/banzai/filter/upload_link_filter.rb b/lib/banzai/filter/upload_link_filter.rb
index f642aee0967..7edfe5ade2d 100644
--- a/lib/banzai/filter/upload_link_filter.rb
+++ b/lib/banzai/filter/upload_link_filter.rb
@@ -1,4 +1,3 @@
-require 'html/pipeline/filter'
require 'uri'
module Banzai
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index 24f16f8b547..989fa64e078 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -90,7 +90,7 @@ module Banzai
private
def urls
- Gitlab::Application.routes.url_helpers
+ Gitlab::Routing.url_helpers
end
def link_class
diff --git a/lib/banzai/filter/yaml_front_matter_filter.rb b/lib/banzai/filter/yaml_front_matter_filter.rb
index e4e2f3f228d..58e3e81209e 100644
--- a/lib/banzai/filter/yaml_front_matter_filter.rb
+++ b/lib/banzai/filter/yaml_front_matter_filter.rb
@@ -1,6 +1,3 @@
-require 'html/pipeline/filter'
-require 'yaml'
-
module Banzai
module Filter
class YamlFrontMatterFilter < HTML::Pipeline::Filter
diff --git a/lib/banzai/pipeline/base_pipeline.rb b/lib/banzai/pipeline/base_pipeline.rb
index f60966c3c0f..321fd5bbe14 100644
--- a/lib/banzai/pipeline/base_pipeline.rb
+++ b/lib/banzai/pipeline/base_pipeline.rb
@@ -1,5 +1,3 @@
-require 'html/pipeline'
-
module Banzai
module Pipeline
class BasePipeline
diff --git a/lib/banzai/pipeline/wiki_pipeline.rb b/lib/banzai/pipeline/wiki_pipeline.rb
index 9b4ff0f0f80..0b5a9e0b2b8 100644
--- a/lib/banzai/pipeline/wiki_pipeline.rb
+++ b/lib/banzai/pipeline/wiki_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class WikiPipeline < FullPipeline
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index c89e1b51019..b7209c14148 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -26,8 +26,8 @@ module Ci
validate!
end
- def builds_for_stage_and_ref(stage, ref, tag = false)
- builds.select{|build| build[:stage] == stage && process?(build[:only], build[:except], ref, tag)}
+ def builds_for_stage_and_ref(stage, ref, tag = false, trigger_request = nil)
+ builds.select{|build| build[:stage] == stage && process?(build[:only], build[:except], ref, tag, trigger_request)}
end
def builds
@@ -242,9 +242,9 @@ module Ci
stage_index = stages.index(job[:stage])
job[:dependencies].each do |dependency|
- raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency]
+ raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency.to_sym]
- unless stages.index(@jobs[dependency][:stage]) < stage_index
+ unless stages.index(@jobs[dependency.to_sym][:stage]) < stage_index
raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages"
end
end
@@ -266,29 +266,30 @@ module Ci
value.in?([true, false])
end
- def process?(only_params, except_params, ref, tag)
+ def process?(only_params, except_params, ref, tag, trigger_request)
if only_params.present?
- return false unless matching?(only_params, ref, tag)
+ return false unless matching?(only_params, ref, tag, trigger_request)
end
if except_params.present?
- return false if matching?(except_params, ref, tag)
+ return false if matching?(except_params, ref, tag, trigger_request)
end
true
end
- def matching?(patterns, ref, tag)
+ def matching?(patterns, ref, tag, trigger_request)
patterns.any? do |pattern|
- match_ref?(pattern, ref, tag)
+ match_ref?(pattern, ref, tag, trigger_request)
end
end
- def match_ref?(pattern, ref, tag)
+ def match_ref?(pattern, ref, tag, trigger_request)
pattern, path = pattern.split('@', 2)
return false if path && path != self.path
return true if tag && pattern == 'tags'
return true if !tag && pattern == 'branches'
+ return true if trigger_request.present? && pattern == 'triggers'
if pattern.first == "/" && pattern.last == "/"
Regexp.new(pattern[1...-1]) =~ ref
diff --git a/lib/gitlab/badge/build.rb b/lib/gitlab/badge/build.rb
new file mode 100644
index 00000000000..28a2391dbf8
--- /dev/null
+++ b/lib/gitlab/badge/build.rb
@@ -0,0 +1,24 @@
+module Gitlab
+ module Badge
+ ##
+ # Build badge
+ #
+ class Build
+ def initialize(project, ref)
+ @image = ::Ci::ImageForBuildService.new.execute(project, ref: ref)
+ end
+
+ def to_s
+ @image[:name].sub(/\.svg$/, '')
+ end
+
+ def type
+ 'image/svg+xml'
+ end
+
+ def data
+ File.read(@image[:path])
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 761b63e98f6..1acc22fe5bf 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -21,7 +21,6 @@ module Gitlab
default_branch_protection: Settings.gitlab['default_branch_protection'],
signup_enabled: Settings.gitlab['signup_enabled'],
signin_enabled: Settings.gitlab['signin_enabled'],
- twitter_sharing_enabled: Settings.gitlab['twitter_sharing_enabled'],
gravatar_enabled: Settings.gravatar['enabled'],
sign_in_text: Settings.extra['sign_in_text'],
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index faa2830c16e..d2e85cabf72 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -24,6 +24,10 @@ module Gitlab
@lines ||= parser.parse(raw_diff.each_line).to_a
end
+ def too_large?
+ diff.too_large?
+ end
+
def highlighted_diff_lines
Gitlab::Diff::Highlight.new(self).highlight
end
diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb
index 41f0edcaf7e..8f9be6cd9a3 100644
--- a/lib/gitlab/email/message/repository_push.rb
+++ b/lib/gitlab/email/message/repository_push.rb
@@ -5,7 +5,7 @@ module Gitlab
attr_accessor :recipient
attr_reader :author_id, :ref, :action
- include Gitlab::Application.routes.url_helpers
+ include Gitlab::Routing.url_helpers
delegate :namespace, :name_with_namespace, to: :project, prefix: :project
delegate :name, to: :author, prefix: :author
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index 2ca21af5bc8..97ef9851d71 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -45,12 +45,12 @@ module Gitlab
note = create_note(reply)
unless note.persisted?
- message = "The comment could not be created for the following reasons:"
+ msg = "The comment could not be created for the following reasons:"
note.errors.full_messages.each do |error|
- message << "\n\n- #{error}"
+ msg << "\n\n- #{error}"
end
- raise InvalidNoteError, message
+ raise InvalidNoteError, msg
end
end
@@ -63,9 +63,24 @@ module Gitlab
end
def reply_key
- reply_key = nil
+ key_from_to_header || key_from_additional_headers
+ end
+
+ def key_from_to_header
+ key = nil
message.to.each do |address|
- reply_key = Gitlab::IncomingEmail.key_from_address(address)
+ key = Gitlab::IncomingEmail.key_from_address(address)
+ break if key
+ end
+
+ key
+ end
+
+ def key_from_additional_headers
+ reply_key = nil
+
+ Array(message.references).each do |message_id|
+ reply_key = Gitlab::IncomingEmail.key_from_fallback_reply_message_id(message_id)
break if reply_key
end
diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb
index 2ef50286b1d..c73eca832d7 100644
--- a/lib/gitlab/exclusive_lease.rb
+++ b/lib/gitlab/exclusive_lease.rb
@@ -15,6 +15,25 @@ module Gitlab
# seconds then two overlapping operations may hold a lease for the same
# key at the same time.
#
+ # This class has no 'cancel' method. I originally decided against adding
+ # it because it would add complexity and a false sense of security. The
+ # complexity: instead of setting '1' we would have to set a UUID, and to
+ # delete it we would have to execute Lua on the Redis server to only
+ # delete the key if the value was our own UUID. Otherwise there is a
+ # chance that when you intend to cancel your lease you actually delete
+ # someone else's. The false sense of security: you cannot design your
+ # system to rely too much on the lease being cancelled after use because
+ # the calling (Ruby) process may crash or be killed. You _cannot_ count
+ # on begin/ensure blocks to cancel a lease, because the 'ensure' does
+ # not always run. Think of 'kill -9' from the Unicorn master for
+ # instance.
+ #
+ # If you find that leases are getting in your way, ask yourself: would
+ # it be enough to lower the lease timeout? Another thing that might be
+ # appropriate is to only use a lease for bulk/automated operations, and
+ # to ignore the lease when you get a single 'manual' user request (a
+ # button click).
+ #
class ExclusiveLease
def initialize(key, timeout:)
@key, @timeout = key, timeout
@@ -27,6 +46,8 @@ module Gitlab
!!redis.set(redis_key, '1', nx: true, ex: @timeout)
end
+ # No #cancel method. See comments above!
+
private
def redis
diff --git a/lib/gitlab/fogbugz_import/client.rb b/lib/gitlab/fogbugz_import/client.rb
index 431d50882fd..2152182b37f 100644
--- a/lib/gitlab/fogbugz_import/client.rb
+++ b/lib/gitlab/fogbugz_import/client.rb
@@ -26,7 +26,7 @@ module Gitlab
def user_map
users = {}
res = @api.command(:listPeople)
- res['people']['person'].each do |user|
+ [res['people']['person']].flatten.each do |user|
users[user['ixPerson']] = { name: user['sFullName'], email: user['sEmail'] }
end
users
diff --git a/lib/gitlab/gfm/reference_rewriter.rb b/lib/gitlab/gfm/reference_rewriter.rb
new file mode 100644
index 00000000000..78d7a4f27cf
--- /dev/null
+++ b/lib/gitlab/gfm/reference_rewriter.rb
@@ -0,0 +1,84 @@
+module Gitlab
+ module Gfm
+ ##
+ # Class that unfolds local references in text.
+ #
+ # The initializer takes text in Markdown and project this text is valid
+ # in context of.
+ #
+ # `unfold` method tries to find all local references and unfold each of
+ # those local references to cross reference format, assuming that the
+ # argument passed to this method is a project that references will be
+ # viewed from (see `Referable#to_reference method).
+ #
+ # Examples:
+ #
+ # 'Hello, this issue is related to #123 and
+ # other issues labeled with ~"label"', will be converted to:
+ #
+ # 'Hello, this issue is related to gitlab-org/gitlab-ce#123 and
+ # other issue labeled with gitlab-org/gitlab-ce~"label"'.
+ #
+ # It does respect markdown lexical rules, so text in code block will not be
+ # replaced, see another example:
+ #
+ # 'Merge request for issue #1234, see also link:
+ # http://gitlab.com/some/link/#1234, and code `puts #1234`' =>
+ #
+ # 'Merge request for issue gitlab-org/gitlab-ce#1234, se also link:
+ # http://gitlab.com/some/link/#1234, and code `puts #1234`'
+ #
+ class ReferenceRewriter
+ def initialize(text, source_project, current_user)
+ @text = text
+ @source_project = source_project
+ @current_user = current_user
+ @original_html = markdown(text)
+ @pattern = Gitlab::ReferenceExtractor.references_pattern
+ end
+
+ def rewrite(target_project)
+ return @text unless needs_rewrite?
+
+ @text.gsub(@pattern) do |reference|
+ unfold_reference(reference, Regexp.last_match, target_project)
+ end
+ end
+
+ def needs_rewrite?
+ @text =~ @pattern
+ end
+
+ private
+
+ def unfold_reference(reference, match, target_project)
+ before = @text[0...match.begin(0)]
+ after = @text[match.end(0)..-1]
+
+ referable = find_referable(reference)
+ return reference unless referable
+
+ cross_reference = referable.to_reference(target_project)
+ return reference if reference == cross_reference
+
+ new_text = before + cross_reference + after
+ substitution_valid?(new_text) ? cross_reference : reference
+ end
+
+ def find_referable(reference)
+ extractor = Gitlab::ReferenceExtractor.new(@source_project,
+ @current_user)
+ extractor.analyze(reference)
+ extractor.all.first
+ end
+
+ def substitution_valid?(substituted)
+ @original_html == markdown(substituted)
+ end
+
+ def markdown(text)
+ Banzai.render(text, project: @source_project, no_original_data: true)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gfm/uploads_rewriter.rb b/lib/gitlab/gfm/uploads_rewriter.rb
new file mode 100644
index 00000000000..abc8c8c55e6
--- /dev/null
+++ b/lib/gitlab/gfm/uploads_rewriter.rb
@@ -0,0 +1,51 @@
+module Gitlab
+ module Gfm
+ ##
+ # Class that rewrites markdown links for uploads
+ #
+ # Using a pattern defined in `FileUploader` it copies files to a new
+ # project and rewrites all links to uploads in in a given text.
+ #
+ #
+ class UploadsRewriter
+ def initialize(text, source_project, _current_user)
+ @text = text
+ @source_project = source_project
+ @pattern = FileUploader::MARKDOWN_PATTERN
+ end
+
+ def rewrite(target_project)
+ return @text unless needs_rewrite?
+
+ @text.gsub(@pattern) do |markdown|
+ file = find_file(@source_project, $~[:secret], $~[:file])
+ return markdown unless file.try(:exists?)
+
+ new_uploader = FileUploader.new(target_project)
+ new_uploader.store!(file)
+ new_uploader.to_markdown
+ end
+ end
+
+ def needs_rewrite?
+ files.any?
+ end
+
+ def files
+ referenced_files = @text.scan(@pattern).map do
+ find_file(@source_project, $~[:secret], $~[:file])
+ end
+
+ referenced_files.compact.select(&:exists?)
+ end
+
+ private
+
+ def find_file(project, secret, file)
+ uploader = FileUploader.new(project, secret)
+ uploader.retrieve_from_store!(file)
+ uploader.file
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb
index 9068d79c95e..8ce9d32abe0 100644
--- a/lib/gitlab/incoming_email.rb
+++ b/lib/gitlab/incoming_email.rb
@@ -1,13 +1,10 @@
module Gitlab
module IncomingEmail
class << self
- def enabled?
- config.enabled && address_formatted_correctly?
- end
+ FALLBACK_REPLY_MESSAGE_ID_REGEX = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\Z/.freeze
- def address_formatted_correctly?
- config.address &&
- config.address.include?("%{key}")
+ def enabled?
+ config.enabled && config.address
end
def reply_address(key)
@@ -24,6 +21,13 @@ module Gitlab
match[1]
end
+ def key_from_fallback_reply_message_id(message_id)
+ match = message_id.match(FALLBACK_REPLY_MESSAGE_ID_REGEX)
+ return unless match
+
+ match[1]
+ end
+
def config
Gitlab.config.incoming_email
end
diff --git a/lib/gitlab/note_data_builder.rb b/lib/gitlab/note_data_builder.rb
index 71cf6a0d886..18523e0aefe 100644
--- a/lib/gitlab/note_data_builder.rb
+++ b/lib/gitlab/note_data_builder.rb
@@ -41,7 +41,7 @@ module Gitlab
data[:issue] = note.noteable.hook_attrs
elsif note.for_merge_request?
data[:merge_request] = note.noteable.hook_attrs
- elsif note.for_project_snippet?
+ elsif note.for_snippet?
data[:snippet] = note.noteable.hook_attrs
end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 0607a8b9592..71c5b6801fb 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -2,7 +2,8 @@ module Gitlab
class ProjectSearchResults < SearchResults
attr_reader :project, :repository_ref
- def initialize(project, query, repository_ref = nil)
+ def initialize(current_user, project, query, repository_ref = nil)
+ @current_user = current_user
@project = project
@repository_ref = if repository_ref.present?
repository_ref
diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb
index 4d830aa45e1..13c4d64c99b 100644
--- a/lib/gitlab/reference_extractor.rb
+++ b/lib/gitlab/reference_extractor.rb
@@ -1,6 +1,7 @@
module Gitlab
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor < Banzai::ReferenceExtractor
+ REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range)
attr_accessor :project, :current_user, :author
def initialize(project, current_user = nil, author = nil)
@@ -17,7 +18,7 @@ module Gitlab
super(text, context.merge(project: project))
end
- %i(user label milestone merge_request snippet commit commit_range).each do |type|
+ REFERABLES.each do |type|
define_method("#{type}s") do
@references[type] ||= references(type, reference_context)
end
@@ -31,6 +32,21 @@ module Gitlab
end
end
+ def all
+ REFERABLES.each { |referable| send(referable.to_s.pluralize) }
+ @references.values.flatten
+ end
+
+ def self.references_pattern
+ return @pattern if @pattern
+
+ patterns = REFERABLES.map do |ref|
+ ref.to_s.classify.constantize.try(:reference_pattern)
+ end
+
+ @pattern = Regexp.union(patterns.compact)
+ end
+
private
def reference_context
diff --git a/lib/gitlab/routing.rb b/lib/gitlab/routing.rb
new file mode 100644
index 00000000000..5132177de51
--- /dev/null
+++ b/lib/gitlab/routing.rb
@@ -0,0 +1,13 @@
+module Gitlab
+ module Routing
+ # Returns the URL helpers Module.
+ #
+ # This method caches the output as Rails' "url_helpers" method creates an
+ # anonymous module every time it's called.
+ #
+ # Returns a Module.
+ def self.url_helpers
+ @url_helpers ||= Gitlab::Application.routes.url_helpers
+ end
+ end
+end
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index f13528a2eea..f8ab2b1f09e 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -1,12 +1,13 @@
module Gitlab
class SearchResults
- attr_reader :query
+ attr_reader :current_user, :query
# Limit search results by passed projects
# It allows us to search only for projects user has access to
attr_reader :limit_projects
- def initialize(limit_projects, query)
+ def initialize(current_user, limit_projects, query)
+ @current_user = current_user
@limit_projects = limit_projects || Project.all
@query = Shellwords.shellescape(query) if query.present?
end
@@ -58,7 +59,7 @@ module Gitlab
end
def issues
- issues = Issue.where(project_id: project_ids_relation)
+ issues = Issue.visible_to_user(current_user).where(project_id: project_ids_relation)
if query =~ /#(\d+)\z/
issues = issues.where(iid: $1)
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index 6f0d02cafd1..f301d42939d 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -1,7 +1,8 @@
module Gitlab
class UrlBuilder
- include Gitlab::Application.routes.url_helpers
+ include Gitlab::Routing.url_helpers
include GitlabRoutingHelper
+ include ActionView::RecordIdentifier
def initialize(type)
@type = type
@@ -37,19 +38,16 @@ module Gitlab
namespace_project_commit_url(namespace_id: note.project.namespace,
id: note.commit_id,
project_id: note.project,
- anchor: "note_#{note.id}")
+ anchor: dom_id(note))
elsif note.for_issue?
issue = Issue.find(note.noteable_id)
- issue_url(issue,
- anchor: "note_#{note.id}")
+ issue_url(issue, anchor: dom_id(note))
elsif note.for_merge_request?
merge_request = MergeRequest.find(note.noteable_id)
- merge_request_url(merge_request,
- anchor: "note_#{note.id}")
- elsif note.for_project_snippet?
+ merge_request_url(merge_request, anchor: dom_id(note))
+ elsif note.for_snippet?
snippet = Snippet.find(note.noteable_id)
- project_snippet_url(snippet,
- anchor: "note_#{note.id}")
+ project_snippet_url(snippet, anchor: dom_id(note))
end
end
end
diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb
index 3160a3c7582..a1ee1cba216 100644
--- a/lib/gitlab/visibility_level.rb
+++ b/lib/gitlab/visibility_level.rb
@@ -6,6 +6,14 @@
module Gitlab
module VisibilityLevel
extend CurrentSettings
+ extend ActiveSupport::Concern
+
+ included do
+ scope :public_only, -> { where(visibility_level: PUBLIC) }
+ scope :public_and_internal_only, -> { where(visibility_level: [PUBLIC, INTERNAL] ) }
+
+ scope :public_to_user, -> (user) { user && !user.external ? public_and_internal_only : public_only }
+ end
PRIVATE = 0 unless const_defined?(:PRIVATE)
INTERNAL = 10 unless const_defined?(:INTERNAL)
@@ -48,10 +56,6 @@ module Gitlab
options.has_value?(level)
end
- def allowed_fork_levels(origin_level)
- [PRIVATE, INTERNAL, PUBLIC].select{ |level| level <= origin_level }
- end
-
def level_name(level)
level_name = 'Unknown'
options.each do |name, lvl|
diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake
index cfaf4a129b1..7ec00a898fd 100644
--- a/lib/tasks/gemojione.rake
+++ b/lib/tasks/gemojione.rake
@@ -1,19 +1,39 @@
-# This task will generate a standard and Retina sprite of all of the current
-# Gemojione Emojis, with the accompanying SCSS map.
-#
-# It will not appear in `rake -T` output, and the dependent gems are not
-# included in the Gemfile by default, because this task will only be needed
-# occasionally, such as when new Emojis are added to Gemojione.
-
-begin
- require 'sprite_factory'
- require 'rmagick'
-rescue LoadError
- # noop
-end
-
namespace :gemojione do
+ desc 'Generates Emoji SHA256 digests'
+ task digests: :environment do
+ require 'digest/sha2'
+ require 'json'
+
+ dir = Gemojione.index.images_path
+
+ digests = AwardEmoji.emojis.map do |name, emoji_hash|
+ fpath = File.join(dir, "#{emoji_hash['unicode']}.png")
+ digest = Digest::SHA256.file(fpath).hexdigest
+
+ { name: name, unicode: emoji_hash['unicode'], digest: digest }
+ end
+
+ out = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
+
+ File.open(out, 'w') do |handle|
+ handle.write(JSON.pretty_generate(digests))
+ end
+ end
+
+ # This task will generate a standard and Retina sprite of all of the current
+ # Gemojione Emojis, with the accompanying SCSS map.
+ #
+ # It will not appear in `rake -T` output, and the dependent gems are not
+ # included in the Gemfile by default, because this task will only be needed
+ # occasionally, such as when new Emojis are added to Gemojione.
task sprite: :environment do
+ begin
+ require 'sprite_factory'
+ require 'rmagick'
+ rescue LoadError
+ # noop
+ end
+
check_requirements!
SIZE = 20
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index cb4abe13799..402bb338f27 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -22,7 +22,7 @@ namespace :gitlab do
end
# Restore backup of GitLab system
- desc "GitLab | Restore a previously created backup"
+ desc 'GitLab | Restore a previously created backup'
task restore: :environment do
warn_user_is_not_gitlab
configure_cron_mode
@@ -30,13 +30,31 @@ namespace :gitlab do
backup = Backup::Manager.new
backup.unpack
- Rake::Task["gitlab:backup:db:restore"].invoke unless backup.skipped?("db")
- Rake::Task["gitlab:backup:repo:restore"].invoke unless backup.skipped?("repositories")
- Rake::Task["gitlab:backup:uploads:restore"].invoke unless backup.skipped?("uploads")
- Rake::Task["gitlab:backup:builds:restore"].invoke unless backup.skipped?("builds")
- Rake::Task["gitlab:backup:artifacts:restore"].invoke unless backup.skipped?("artifacts")
- Rake::Task["gitlab:backup:lfs:restore"].invoke unless backup.skipped?("lfs")
- Rake::Task["gitlab:shell:setup"].invoke
+ unless backup.skipped?('db')
+ unless ENV['force'] == 'yes'
+ warning = warning = <<-MSG.strip_heredoc
+ Before restoring the database we recommend removing all existing
+ tables to avoid future upgrade problems. Be aware that if you have
+ custom tables in the GitLab database these tables and all data will be
+ removed.
+ MSG
+ ask_to_continue
+ puts 'Removing all tables. Press `Ctrl-C` within 5 seconds to abort'.yellow
+ sleep(5)
+ end
+ # Drop all tables Load the schema to ensure we don't have any newer tables
+ # hanging out from a failed upgrade
+ $progress.puts 'Cleaning the database ... '.blue
+ Rake::Task['gitlab:db:drop_tables'].invoke
+ $progress.puts 'done'.green
+ Rake::Task['gitlab:backup:db:restore'].invoke
+ end
+ Rake::Task['gitlab:backup:repo:restore'].invoke unless backup.skipped?('repositories')
+ Rake::Task['gitlab:backup:uploads:restore'].invoke unless backup.skipped?('uploads')
+ Rake::Task['gitlab:backup:builds:restore'].invoke unless backup.skipped?('builds')
+ Rake::Task['gitlab:backup:artifacts:restore'].invoke unless backup.skipped?('artifacts')
+ Rake::Task['gitlab:backup:lfs:restore'].invoke unless backup.skipped?('lfs')
+ Rake::Task['gitlab:shell:setup'].invoke
backup.cleanup
end
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 27ed57efe55..effb8eb6001 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -623,7 +623,6 @@ namespace :gitlab do
start_checking "Reply by email"
if Gitlab.config.incoming_email.enabled
- check_address_formatted_correctly
check_imap_authentication
if Rails.env.production?
@@ -643,20 +642,6 @@ namespace :gitlab do
# Checks
########################
- def check_address_formatted_correctly
- print "Address formatted correctly? ... "
-
- if Gitlab::IncomingEmail.address_formatted_correctly?
- puts "yes".green
- else
- puts "no".red
- try_fixing_it(
- "Make sure that the address in config/gitlab.yml includes the '%{key}' placeholder."
- )
- fix_and_rerun
- end
- end
-
def check_initd_configured_correctly
print "Init.d configured correctly? ... "
diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake
new file mode 100644
index 00000000000..4921c6e0bcf
--- /dev/null
+++ b/lib/tasks/gitlab/db.rake
@@ -0,0 +1,35 @@
+namespace :gitlab do
+ namespace :db do
+ desc 'GitLab | Manually insert schema migration version'
+ task :mark_migration_complete, [:version] => :environment do |_, args|
+ unless args[:version]
+ puts "Must specify a migration version as an argument".red
+ exit 1
+ end
+
+ version = args[:version].to_i
+ if version == 0
+ puts "Version '#{args[:version]}' must be a non-zero integer".red
+ exit 1
+ end
+
+ sql = "INSERT INTO schema_migrations (version) VALUES (#{version})"
+ begin
+ ActiveRecord::Base.connection.execute(sql)
+ puts "Successfully marked '#{version}' as complete".green
+ rescue ActiveRecord::RecordNotUnique
+ puts "Migration version '#{version}' is already marked complete".yellow
+ end
+ end
+
+ desc 'Drop all tables'
+ task :drop_tables => :environment do
+ connection = ActiveRecord::Base.connection
+ tables = connection.tables
+ tables.delete 'schema_migrations'
+ # Truncate schema_migrations to ensure migrations re-run
+ connection.execute('TRUNCATE schema_migrations')
+ tables.each { |t| connection.execute("DROP TABLE #{t}") }
+ end
+ end
+end
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index 5b1f65d7aff..9ef8ba1b097 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -1,15 +1,14 @@
require 'spec_helper'
describe Admin::UsersController do
- let(:admin) { create(:admin) }
+ let(:user) { create(:user) }
before do
- sign_in(admin)
+ sign_in(create(:admin))
end
describe 'DELETE #user with projects' do
- let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:empty_project, namespace: user.namespace) }
before do
project.team << [user, :developer]
@@ -23,8 +22,6 @@ describe Admin::UsersController do
end
describe 'PUT block/:id' do
- let(:user) { create(:user) }
-
it 'blocks user' do
put :block, id: user.username
user.reload
@@ -50,8 +47,6 @@ describe Admin::UsersController do
end
context 'manually blocked users' do
- let(:user) { create(:user) }
-
before do
user.block
end
@@ -66,8 +61,6 @@ describe Admin::UsersController do
end
describe 'PUT unlock/:id' do
- let(:user) { create(:user) }
-
before do
request.env["HTTP_REFERER"] = "/"
user.lock_access!
@@ -95,8 +88,6 @@ describe Admin::UsersController do
end
describe 'PATCH disable_two_factor' do
- let(:user) { create(:user) }
-
it 'disables 2FA for the user' do
expect(user).to receive(:disable_two_factor!)
allow(subject).to receive(:user).and_return(user)
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 55851befc8c..186239d3096 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -30,44 +30,4 @@ describe ApplicationController do
controller.send(:check_password_expiration)
end
end
-
- describe 'check labels authorization' do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
- let(:controller) { ApplicationController.new }
-
- before do
- project.team << [user, :guest]
- allow(controller).to receive(:current_user).and_return(user)
- allow(controller).to receive(:project).and_return(project)
- end
-
- it 'should succeed if issues and MRs are enabled' do
- project.issues_enabled = true
- project.merge_requests_enabled = true
- controller.send(:authorize_read_label!)
- expect(response.status).to eq(200)
- end
-
- it 'should succeed if issues are enabled, MRs are disabled' do
- project.issues_enabled = true
- project.merge_requests_enabled = false
- controller.send(:authorize_read_label!)
- expect(response.status).to eq(200)
- end
-
- it 'should succeed if issues are disabled, MRs are enabled' do
- project.issues_enabled = false
- project.merge_requests_enabled = true
- controller.send(:authorize_read_label!)
- expect(response.status).to eq(200)
- end
-
- it 'should fail if issues and MRs are disabled' do
- project.issues_enabled = false
- project.merge_requests_enabled = false
- expect(controller).to receive(:access_denied!)
- controller.send(:authorize_read_label!)
- end
- end
end
diff --git a/spec/controllers/ci/projects_controller_spec.rb b/spec/controllers/ci/projects_controller_spec.rb
index db0748f323f..5022a3e2c80 100644
--- a/spec/controllers/ci/projects_controller_spec.rb
+++ b/spec/controllers/ci/projects_controller_spec.rb
@@ -5,6 +5,27 @@ describe Ci::ProjectsController do
let!(:project) { create(:project, visibility, ci_id: 1) }
let(:ci_id) { project.ci_id }
+ describe '#index' do
+ context 'user signed in' do
+ before do
+ sign_in(create(:user))
+ get(:index)
+ end
+
+ it 'redirects to /' do
+ expect(response).to redirect_to(root_path)
+ end
+ end
+
+ context 'user not signed in' do
+ before { get(:index) }
+
+ it 'redirects to sign in page' do
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
##
# Specs for *deprecated* CI badge
#
diff --git a/spec/controllers/groups/avatars_controller_spec.rb b/spec/controllers/groups/avatars_controller_spec.rb
index 3dac134a731..91d639218e5 100644
--- a/spec/controllers/groups/avatars_controller_spec.rb
+++ b/spec/controllers/groups/avatars_controller_spec.rb
@@ -2,9 +2,10 @@ require 'spec_helper'
describe Groups::AvatarsController do
let(:user) { create(:user) }
- let(:group) { create(:group, owner: user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
+ let(:group) { create(:group, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
before do
+ group.add_owner(user)
sign_in(user)
end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 938e97298b6..465531b2b36 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -1,10 +1,15 @@
require 'rails_helper'
describe GroupsController do
- describe 'GET index' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
+ let!(:group_member) { create(:group_member, group: group, user: user) }
+
+ describe 'GET #index' do
context 'as a user' do
it 'redirects to Groups Dashboard' do
- sign_in(create(:user))
+ sign_in(user)
get :index
@@ -20,4 +25,54 @@ describe GroupsController do
end
end
end
+
+ describe 'GET #issues' do
+ let(:issue_1) { create(:issue, project: project) }
+ let(:issue_2) { create(:issue, project: project) }
+
+ before do
+ create_list(:upvote_note, 3, project: project, noteable: issue_2)
+ create_list(:upvote_note, 2, project: project, noteable: issue_1)
+ create_list(:downvote_note, 2, project: project, noteable: issue_2)
+
+ sign_in(user)
+ end
+
+ context 'sorting by votes' do
+ it 'sorts most popular issues' do
+ get :issues, id: group.to_param, sort: 'upvotes_desc'
+ expect(assigns(:issues)).to eq [issue_2, issue_1]
+ end
+
+ it 'sorts least popular issues' do
+ get :issues, id: group.to_param, sort: 'downvotes_desc'
+ expect(assigns(:issues)).to eq [issue_2, issue_1]
+ end
+ end
+ end
+
+ describe 'GET #merge_requests' do
+ let(:merge_request_1) { create(:merge_request, source_project: project) }
+ let(:merge_request_2) { create(:merge_request, :simple, source_project: project) }
+
+ before do
+ create_list(:upvote_note, 3, project: project, noteable: merge_request_2)
+ create_list(:upvote_note, 2, project: project, noteable: merge_request_1)
+ create_list(:downvote_note, 2, project: project, noteable: merge_request_2)
+
+ sign_in(user)
+ end
+
+ context 'sorting by votes' do
+ it 'sorts most popular merge requests' do
+ get :merge_requests, id: group.to_param, sort: 'upvotes_desc'
+ expect(assigns(:merge_requests)).to eq [merge_request_2, merge_request_1]
+ end
+
+ it 'sorts least popular merge requests' do
+ get :merge_requests, id: group.to_param, sort: 'downvotes_desc'
+ expect(assigns(:merge_requests)).to eq [merge_request_2, merge_request_1]
+ end
+ end
+ end
end
diff --git a/spec/controllers/namespaces_controller_spec.rb b/spec/controllers/namespaces_controller_spec.rb
index 77436958711..27e9afe582e 100644
--- a/spec/controllers/namespaces_controller_spec.rb
+++ b/spec/controllers/namespaces_controller_spec.rb
@@ -15,14 +15,9 @@ describe NamespacesController do
end
context "when the namespace belongs to a group" do
- let!(:group) { create(:group) }
- let!(:project) { create(:project, namespace: group) }
-
- context "when the group has public projects" do
- before do
- project.update_attribute(:visibility_level, Project::PUBLIC)
- end
+ let!(:group) { create(:group) }
+ context "when the group is public" do
context "when not signed in" do
it "redirects to the group's page" do
get :show, id: group.path
@@ -44,27 +39,31 @@ describe NamespacesController do
end
end
- context "when the project doesn't have public projects" do
+ context "when the group is private" do
+ before do
+ group.update_attribute(:visibility_level, Group::PRIVATE)
+ end
+
context "when not signed in" do
- it "does not redirect to the sign in page" do
+ it "redirects to the sign in page" do
get :show, id: group.path
- expect(response).not_to redirect_to(new_user_session_path)
+ expect(response).to redirect_to(new_user_session_path)
end
end
+
context "when signed in" do
before do
sign_in(user)
end
- context "when the user has access to the project" do
+ context "when the user has access to the group" do
before do
- project.team << [user, :master]
+ group.add_developer(user)
end
context "when the user is blocked" do
before do
user.block
- project.team << [user, :master]
end
it "redirects to the sign in page" do
@@ -83,11 +82,11 @@ describe NamespacesController do
end
end
- context "when the user doesn't have access to the project" do
- it "redirects to the group's page" do
+ context "when the user doesn't have access to the group" do
+ it "responds with status 404" do
get :show, id: group.path
- expect(response).to redirect_to(group_path(group))
+ expect(response.status).to eq(404)
end
end
end
diff --git a/spec/controllers/projects/avatars_controller_spec.rb b/spec/controllers/projects/avatars_controller_spec.rb
index e79b46a3504..4d724ca9ed0 100644
--- a/spec/controllers/projects/avatars_controller_spec.rb
+++ b/spec/controllers/projects/avatars_controller_spec.rb
@@ -6,7 +6,7 @@ describe Projects::AvatarsController do
before do
sign_in(user)
- project.team << [user, :developer]
+ project.team << [user, :master]
controller.instance_variable_set(:@project, project)
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 76d56bc989d..d6e4cd71ce6 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -1,16 +1,16 @@
require('spec_helper')
describe Projects::IssuesController do
- let(:project) { create(:project) }
+ let(:project) { create(:project_empty_repo) }
let(:user) { create(:user) }
- let(:issue) { create(:issue, project: project) }
-
- before do
- sign_in(user)
- project.team << [user, :developer]
- end
+ let(:issue) { create(:issue, project: project) }
describe "GET #index" do
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
it "returns index" do
get :index, namespace_id: project.namespace.path, project_id: project.path
@@ -38,6 +38,177 @@ describe Projects::IssuesController do
get :index, namespace_id: project.namespace.path, project_id: project.path
expect(response.status).to eq(404)
end
+ end
+
+ describe 'Confidential Issues' do
+ let(:project) { create(:project_empty_repo, :public) }
+ let(:assignee) { create(:assignee) }
+ let(:author) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:admin) { create(:admin) }
+ let!(:issue) { create(:issue, project: project) }
+ let!(:unescaped_parameter_value) { create(:issue, :confidential, project: project, author: author) }
+ let!(:request_forgery_timing_attack) { create(:issue, :confidential, project: project, assignee: assignee) }
+
+ describe 'GET #index' do
+ it 'should not list confidential issues for guests' do
+ sign_out(:user)
+ get_issues
+
+ expect(assigns(:issues)).to eq [issue]
+ end
+
+ it 'should not list confidential issues for non project members' do
+ sign_in(non_member)
+ get_issues
+
+ expect(assigns(:issues)).to eq [issue]
+ end
+
+ it 'should list confidential issues for author' do
+ sign_in(author)
+ get_issues
+
+ expect(assigns(:issues)).to include unescaped_parameter_value
+ expect(assigns(:issues)).not_to include request_forgery_timing_attack
+ end
+
+ it 'should list confidential issues for assignee' do
+ sign_in(assignee)
+ get_issues
+
+ expect(assigns(:issues)).not_to include unescaped_parameter_value
+ expect(assigns(:issues)).to include request_forgery_timing_attack
+ end
+
+ it 'should list confidential issues for project members' do
+ sign_in(member)
+ project.team << [member, :developer]
+
+ get_issues
+
+ expect(assigns(:issues)).to include unescaped_parameter_value
+ expect(assigns(:issues)).to include request_forgery_timing_attack
+ end
+
+ it 'should list confidential issues for admin' do
+ sign_in(admin)
+ get_issues
+
+ expect(assigns(:issues)).to include unescaped_parameter_value
+ expect(assigns(:issues)).to include request_forgery_timing_attack
+ end
+
+ def get_issues
+ get :index,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param
+ end
+ end
+
+ shared_examples_for 'restricted action' do |http_status|
+ it 'returns 404 for guests' do
+ sign_out :user
+ go(id: unescaped_parameter_value.to_param)
+
+ expect(response).to have_http_status :not_found
+ end
+
+ it 'returns 404 for non project members' do
+ sign_in(non_member)
+ go(id: unescaped_parameter_value.to_param)
+
+ expect(response).to have_http_status :not_found
+ end
+
+ it "returns #{http_status[:success]} for author" do
+ sign_in(author)
+ go(id: unescaped_parameter_value.to_param)
+
+ expect(response).to have_http_status http_status[:success]
+ end
+
+ it "returns #{http_status[:success]} for assignee" do
+ sign_in(assignee)
+ go(id: request_forgery_timing_attack.to_param)
+
+ expect(response).to have_http_status http_status[:success]
+ end
+
+ it "returns #{http_status[:success]} for project members" do
+ sign_in(member)
+ project.team << [member, :developer]
+ go(id: unescaped_parameter_value.to_param)
+
+ expect(response).to have_http_status http_status[:success]
+ end
+
+ it "returns #{http_status[:success]} for admin" do
+ sign_in(admin)
+ go(id: unescaped_parameter_value.to_param)
+
+ expect(response).to have_http_status http_status[:success]
+ end
+ end
+
+ describe 'GET #show' do
+ it_behaves_like 'restricted action', success: 200
+
+ def go(id:)
+ get :show,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: id
+ end
+ end
+
+ describe 'GET #edit' do
+ it_behaves_like 'restricted action', success: 200
+ def go(id:)
+ get :edit,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: id
+ end
+ end
+
+ describe 'PUT #update' do
+ it_behaves_like 'restricted action', success: 302
+
+ def go(id:)
+ put :update,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: id,
+ issue: { title: 'New title' }
+ end
+ end
+ end
+
+ describe "DELETE #destroy" do
+ context "when the user is a developer" do
+ before { sign_in(user) }
+ it "rejects a developer to destroy an issue" do
+ delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context "when the user is owner" do
+ let(:owner) { create(:user) }
+ let(:namespace) { create(:namespace, owner: owner) }
+ let(:project) { create(:project, namespace: namespace) }
+
+ before { sign_in(owner) }
+
+ it "deletes the issue" do
+ delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid
+
+ expect(response.status).to eq(302)
+ expect(controller).to set_flash[:notice].to(/The issue was successfully deleted\./).now
+ end
+ end
end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index e82fe26c7a6..75e6b6f45a7 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -63,7 +63,7 @@ describe Projects::MergeRequestsController do
id: merge_request.iid,
format: format)
- expect(response.body).to eq((merge_request.send(:"to_#{format}",user)).to_s)
+ expect(response.body).to eq((merge_request.send(:"to_#{format}")).to_s)
end
it "should not escape Html" do
@@ -157,6 +157,29 @@ describe Projects::MergeRequestsController do
end
end
+ describe "DELETE #destroy" do
+ it "denies access to users unless they're admin or project owner" do
+ delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid
+
+ expect(response.status).to eq(404)
+ end
+
+ context "when the user is owner" do
+ let(:owner) { create(:user) }
+ let(:namespace) { create(:namespace, owner: owner) }
+ let(:project) { create(:project, namespace: namespace) }
+
+ before { sign_in owner }
+
+ it "deletes the merge request" do
+ delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid
+
+ expect(response.status).to eq(302)
+ expect(controller).to set_flash[:notice].to(/The merge request was successfully deleted\./).now
+ end
+ end
+ end
+
describe 'GET diffs' do
def go(format: 'html')
get :diffs,
diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb
new file mode 100644
index 00000000000..0f32a30f18b
--- /dev/null
+++ b/spec/controllers/projects/snippets_controller_spec.rb
@@ -0,0 +1,107 @@
+require 'spec_helper'
+
+describe Projects::SnippetsController do
+ let(:project) { create(:project_empty_repo, :public, snippets_enabled: true) }
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ project.team << [user2, :master]
+ end
+
+ describe 'GET #index' do
+ context 'when the project snippet is private' do
+ let!(:project_snippet) { create(:project_snippet, :private, project: project, author: user) }
+
+ context 'when anonymous' do
+ it 'does not include the private snippet' do
+ get :index, namespace_id: project.namespace.path, project_id: project.path
+
+ expect(assigns(:snippets)).not_to include(project_snippet)
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context 'when signed in as the author' do
+ before { sign_in(user) }
+
+ it 'renders the snippet' do
+ get :index, namespace_id: project.namespace.path, project_id: project.path
+
+ expect(assigns(:snippets)).to include(project_snippet)
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context 'when signed in as a project member' do
+ before { sign_in(user2) }
+
+ it 'renders the snippet' do
+ get :index, namespace_id: project.namespace.path, project_id: project.path
+
+ expect(assigns(:snippets)).to include(project_snippet)
+ expect(response.status).to eq(200)
+ end
+ end
+ end
+ end
+
+ %w[show raw].each do |action|
+ describe "GET ##{action}" do
+ context 'when the project snippet is private' do
+ let(:project_snippet) { create(:project_snippet, :private, project: project, author: user) }
+
+ context 'when anonymous' do
+ it 'responds with status 404' do
+ get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'when signed in as the author' do
+ before { sign_in(user) }
+
+ it 'renders the snippet' do
+ get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param
+
+ expect(assigns(:snippet)).to eq(project_snippet)
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context 'when signed in as a project member' do
+ before { sign_in(user2) }
+
+ it 'renders the snippet' do
+ get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param
+
+ expect(assigns(:snippet)).to eq(project_snippet)
+ expect(response.status).to eq(200)
+ end
+ end
+ end
+
+ context 'when the project snippet does not exist' do
+ context 'when anonymous' do
+ it 'responds with status 404' do
+ get action, namespace_id: project.namespace.path, project_id: project.path, id: 42
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'when signed in' do
+ before { sign_in(user) }
+
+ it 'responds with status 404' do
+ get action, namespace_id: project.namespace.path, project_id: project.path, id: 42
+
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/root_controller_spec.rb b/spec/controllers/root_controller_spec.rb
index 5a104ae7c99..b14d275f7fa 100644
--- a/spec/controllers/root_controller_spec.rb
+++ b/spec/controllers/root_controller_spec.rb
@@ -43,6 +43,28 @@ describe RootController do
end
end
+ context 'who has customized their dashboard setting for groups' do
+ before do
+ user.update_attribute(:dashboard, 'groups')
+ end
+
+ it 'redirects to their group list' do
+ get :index
+ expect(response).to redirect_to dashboard_groups_path
+ end
+ end
+
+ context 'who has customized their dashboard setting for todos' do
+ before do
+ user.update_attribute(:dashboard, 'todos')
+ end
+
+ it 'redirects to their todo list' do
+ get :index
+ expect(response).to redirect_to dashboard_todos_path
+ end
+ end
+
context 'who uses the default dashboard setting' do
it 'renders the default dashboard' do
get :index
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index af5d043cf02..73858e6f063 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -30,7 +30,7 @@ describe UploadsController do
end
end
end
-
+
context "when not signed in" do
it "responds with status 200" do
get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png"
@@ -126,14 +126,9 @@ describe UploadsController do
end
context "when viewing a group avatar" do
- let!(:group) { create(:group, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
- let!(:project) { create(:project, namespace: group) }
-
- context "when the group has public projects" do
- before do
- project.update_attribute(:visibility_level, Project::PUBLIC)
- end
+ let!(:group) { create(:group, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
+ context "when the group is public" do
context "when not signed in" do
it "responds with status 200" do
get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
@@ -155,7 +150,11 @@ describe UploadsController do
end
end
- context "when the project doesn't have public projects" do
+ context "when the group is private" do
+ before do
+ group.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
+ end
+
context "when signed in" do
before do
sign_in(user)
@@ -163,13 +162,12 @@ describe UploadsController do
context "when the user has access to the project" do
before do
- project.team << [user, :master]
+ group.add_developer(user)
end
context "when the user is blocked" do
before do
user.block
- project.team << [user, :master]
end
it "redirects to the sign in page" do
diff --git a/spec/factories/broadcast_messages.rb b/spec/factories/broadcast_messages.rb
index 373ca75467e..c80e7366551 100644
--- a/spec/factories/broadcast_messages.rb
+++ b/spec/factories/broadcast_messages.rb
@@ -15,7 +15,7 @@
FactoryGirl.define do
factory :broadcast_message do
message "MyText"
- starts_at Date.today
+ starts_at Date.yesterday
ends_at Date.tomorrow
trait :expired do
diff --git a/spec/factories/file_uploader.rb b/spec/factories/file_uploader.rb
new file mode 100644
index 00000000000..1b36e21f2b0
--- /dev/null
+++ b/spec/factories/file_uploader.rb
@@ -0,0 +1,20 @@
+FactoryGirl.define do
+ factory :file_uploader do
+ project
+ secret nil
+
+ transient do
+ fixture { 'rails_sample.jpg' }
+ path { File.join(Rails.root, 'spec/fixtures', fixture) }
+ file { Rack::Test::UploadedFile.new(path) }
+ end
+
+ after(:build) do |uploader, evaluator|
+ uploader.store!(evaluator.file)
+ end
+
+ initialize_with do
+ new(project, secret)
+ end
+ end
+end
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index 4a3a155d7ff..2d47a6f6c4c 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -3,5 +3,17 @@ FactoryGirl.define do
sequence(:name) { |n| "group#{n}" }
path { name.downcase.gsub(/\s/, '_') }
type 'Group'
+
+ trait :public do
+ visibility_level Gitlab::VisibilityLevel::PUBLIC
+ end
+
+ trait :internal do
+ visibility_level Gitlab::VisibilityLevel::INTERNAL
+ end
+
+ trait :private do
+ visibility_level Gitlab::VisibilityLevel::PRIVATE
+ end
end
end
diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb
index 722095de590..e72aa9479b7 100644
--- a/spec/factories/issues.rb
+++ b/spec/factories/issues.rb
@@ -4,6 +4,10 @@ FactoryGirl.define do
author
project
+ trait :confidential do
+ confidential true
+ end
+
trait :closed do
state :closed
end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index a9df5fa1d3a..e281e2f227b 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -51,6 +51,11 @@ FactoryGirl.define do
trait :with_diffs do
end
+ trait :without_diffs do
+ source_branch "improve/awesome"
+ target_branch "master"
+ end
+
trait :conflict do
source_branch "feature_conflict"
target_branch "feature"
diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb
index bd85b1d798a..7ae06c27840 100644
--- a/spec/factories/todos.rb
+++ b/spec/factories/todos.rb
@@ -5,14 +5,15 @@
# id :integer not null, primary key
# user_id :integer not null
# project_id :integer not null
-# target_id :integer not null
+# target_id :integer
# target_type :string not null
# author_id :integer
-# note_id :integer
# action :integer not null
# state :string not null
# created_at :datetime
# updated_at :datetime
+# note_id :integer
+# commit_id :string
#
FactoryGirl.define do
@@ -30,5 +31,10 @@ FactoryGirl.define do
trait :mentioned do
action { Todo::MENTIONED }
end
+
+ trait :on_commit do
+ commit_id RepoHelpers.sample_commit.id
+ target_type "Commit"
+ end
end
end
diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb
index 457859dedaf..62de081661d 100644
--- a/spec/factories_spec.rb
+++ b/spec/factories_spec.rb
@@ -1,9 +1,17 @@
require 'spec_helper'
-FactoryGirl.factories.map(&:name).each do |factory_name|
- describe "#{factory_name} factory" do
- it 'should be valid' do
- expect(build(factory_name)).to be_valid
+describe 'factories' do
+ FactoryGirl.factories.each do |factory|
+ describe "#{factory.name} factory" do
+ let(:entity) { build(factory.name) }
+
+ it 'does not raise error when created 'do
+ expect { entity }.to_not raise_error
+ end
+
+ it 'should be valid', if: factory.build_class < ActiveRecord::Base do
+ expect(entity).to be_valid
+ end
end
end
end
diff --git a/spec/features/dashboard_milestones_spec.rb b/spec/features/dashboard_milestones_spec.rb
new file mode 100644
index 00000000000..f32fddbc9fa
--- /dev/null
+++ b/spec/features/dashboard_milestones_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+feature 'Dashboard > Milestones', feature: true do
+ describe 'as anonymous user' do
+ before do
+ visit dashboard_milestones_path
+ end
+
+ it 'is redirected to sign-in page' do
+ expect(current_path).to eq new_user_session_path
+ end
+ end
+
+ describe 'as logged-in user' do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, namespace: user.namespace) }
+ let!(:milestone) { create(:milestone, project: project) }
+ before do
+ project.team << [user, :master]
+ login_with(user)
+ visit dashboard_milestones_path
+ end
+
+ it 'sees milestones' do
+ expect(current_path).to eq dashboard_milestones_path
+ expect(page).to have_content(milestone.title)
+ end
+ end
+end
diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb
index f6e33f651c4..99445185893 100644
--- a/spec/features/issues/filter_by_milestone_spec.rb
+++ b/spec/features/issues/filter_by_milestone_spec.rb
@@ -11,7 +11,41 @@ feature 'Issue filtering by Milestone', feature: true do
visit_issues(project)
filter_by_milestone(Milestone::None.title)
- expect(page).to have_css('.issue .title', count: 1)
+ expect(page).to have_css('.issue', count: 1)
+ end
+
+ context 'filters by upcoming milestone', js: true do
+ it 'should not show issues with no expiry' do
+ create(:issue, project: project)
+ create(:issue, project: project, milestone: milestone)
+
+ visit_issues(project)
+ filter_by_milestone(Milestone::Upcoming.title)
+
+ expect(page).to have_css('.issue', count: 0)
+ end
+
+ it 'should show issues in future' do
+ milestone = create(:milestone, project: project, due_date: Date.tomorrow)
+ create(:issue, project: project)
+ create(:issue, project: project, milestone: milestone)
+
+ visit_issues(project)
+ filter_by_milestone(Milestone::Upcoming.title)
+
+ expect(page).to have_css('.issue', count: 1)
+ end
+
+ it 'should not show issues in past' do
+ milestone = create(:milestone, project: project, due_date: Date.yesterday)
+ create(:issue, project: project)
+ create(:issue, project: project, milestone: milestone)
+
+ visit_issues(project)
+ filter_by_milestone(Milestone::Upcoming.title)
+
+ expect(page).to have_css('.issue', count: 0)
+ end
end
scenario 'filters by a specific Milestone', js: true do
@@ -21,7 +55,7 @@ feature 'Issue filtering by Milestone', feature: true do
visit_issues(project)
filter_by_milestone(milestone.title)
- expect(page).to have_css('.issue .title', count: 1)
+ expect(page).to have_css('.issue', count: 1)
end
def visit_issues(project)
@@ -30,8 +64,6 @@ feature 'Issue filtering by Milestone', feature: true do
def filter_by_milestone(title)
find(".js-milestone-select").click
- sleep 0.5
- find(".milestone-filter a", text: title).click
- sleep 1
+ find(".milestone-filter .dropdown-content a", text: title).click
end
end
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
new file mode 100644
index 00000000000..6fda0c31866
--- /dev/null
+++ b/spec/features/issues/move_spec.rb
@@ -0,0 +1,87 @@
+require 'rails_helper'
+
+feature 'issue move to another project' do
+ let(:user) { create(:user) }
+ let(:old_project) { create(:project) }
+ let(:text) { 'Some issue description' }
+
+ let(:issue) do
+ create(:issue, description: text, project: old_project, author: user)
+ end
+
+ background { login_as(user) }
+
+ context 'user does not have permission to move issue' do
+ background do
+ old_project.team << [user, :guest]
+
+ edit_issue(issue)
+ end
+
+ scenario 'moving issue to another project not allowed' do
+ expect(page).to have_no_select('move_to_project_id')
+ end
+ end
+
+ context 'user has permission to move issue' do
+ let!(:mr) { create(:merge_request, source_project: old_project) }
+ let(:new_project) { create(:project) }
+ let(:text) { 'Text with !1' }
+ let(:cross_reference) { old_project.to_reference }
+
+ background do
+ old_project.team << [user, :reporter]
+ new_project.team << [user, :reporter]
+
+ edit_issue(issue)
+ end
+
+ scenario 'moving issue to another project' do
+ select(new_project.name_with_namespace, from: 'move_to_project_id')
+ click_button('Save changes')
+
+ expect(current_url).to include project_path(new_project)
+
+ page.within('.issue') do
+ expect(page).to have_content("Text with #{cross_reference}!1")
+ expect(page).to have_content("Moved from #{cross_reference}#1")
+ expect(page).to have_content(issue.title)
+ end
+ end
+
+ context 'projects user does not have permission to move issue to exist' do
+ let!(:private_project) { create(:project, :private) }
+ let(:another_project) { create(:project) }
+ background { another_project.team << [user, :guest] }
+
+ scenario 'browsing projects in projects select' do
+ options = [ '', 'No project', new_project.name_with_namespace ]
+ expect(page).to have_select('move_to_project_id', options: options)
+ end
+ end
+
+ context 'issue has been already moved' do
+ let(:new_issue) { create(:issue, project: new_project) }
+ let(:issue) do
+ create(:issue, project: old_project, author: user, moved_to: new_issue)
+ end
+
+ scenario 'user wants to move issue that has already been moved' do
+ expect(page).to have_no_select('move_to_project_id')
+ end
+ end
+ end
+
+ def edit_issue(issue)
+ visit issue_path(issue)
+ page.within('.issuable-header') { click_link 'Edit' }
+ end
+
+ def issue_path(issue)
+ namespace_project_issue_path(issue.project.namespace, issue.project, issue)
+ end
+
+ def project_path(project)
+ namespace_project_path(new_project.namespace, new_project)
+ end
+end
diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb
index 1f3bd915f48..9219b767547 100644
--- a/spec/features/issues/new_branch_button_spec.rb
+++ b/spec/features/issues/new_branch_button_spec.rb
@@ -24,7 +24,7 @@ feature 'Start new branch from an issue', feature: true do
end
let(:referenced_mr) do
create(:merge_request, :simple, source_project: project, target_project: project,
- description: "Fixes ##{issue.iid}")
+ description: "Fixes ##{issue.iid}", author: user)
end
before do
diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb
new file mode 100644
index 00000000000..3eb903a93fe
--- /dev/null
+++ b/spec/features/issues/update_issues_spec.rb
@@ -0,0 +1,117 @@
+require 'rails_helper'
+
+feature 'Multiple issue updating from issues#index', feature: true do
+ let!(:project) { create(:project) }
+ let!(:issue) { create(:issue, project: project) }
+ let!(:user) { create(:user)}
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ context 'status', js: true do
+ it 'should be set to closed' do
+ visit namespace_project_issues_path(project.namespace, project)
+
+ find('#check_all_issues').click
+ find('.js-issue-status').click
+
+ find('.dropdown-menu-status a', text: 'Closed').click
+ click_update_issues_button
+ expect(page).to have_selector('.issue', count: 0)
+ end
+
+ it 'should be set to open' do
+ create_closed
+ visit namespace_project_issues_path(project.namespace, project)
+
+ find('.issues-state-filters a', text: 'Closed').click
+
+ find('#check_all_issues').click
+ find('.js-issue-status').click
+
+ find('.dropdown-menu-status a', text: 'Open').click
+ click_update_issues_button
+ expect(page).to have_selector('.issue', count: 0)
+ end
+ end
+
+ context 'assignee', js: true do
+ it 'should update to current user' do
+ visit namespace_project_issues_path(project.namespace, project)
+
+ find('#check_all_issues').click
+ find('.js-update-assignee').click
+
+ find('.dropdown-menu-user-link', text: user.username).click
+ click_update_issues_button
+
+ page.within('.issue .controls') do
+ expect(find('.author_link')["data-original-title"]).to have_content(user.name)
+ end
+ end
+
+ it 'should update to unassigned' do
+ create_assigned
+ visit namespace_project_issues_path(project.namespace, project)
+
+ find('#check_all_issues').click
+ find('.js-update-assignee').click
+
+ click_link 'Unassigned'
+ click_update_issues_button
+
+ within first('.issue .controls') do
+ expect(page).to have_no_selector('.author_link')
+ end
+ end
+ end
+
+ context 'milestone', js: true do
+ let(:milestone) { create(:milestone, project: project) }
+
+ it 'should update milestone' do
+ visit namespace_project_issues_path(project.namespace, project)
+
+ find('#check_all_issues').click
+ find('.issues_bulk_update .js-milestone-select').click
+
+ find('.dropdown-menu-milestone a', text: milestone.title).click
+ click_update_issues_button
+
+ expect(find('.issue')).to have_content milestone.title
+ end
+
+ it 'should set to no milestone' do
+ create_with_milestone
+ visit namespace_project_issues_path(project.namespace, project)
+
+ expect(first('.issue')).to have_content milestone.title
+
+ find('#check_all_issues').click
+ find('.issues_bulk_update .js-milestone-select').click
+
+ find('.dropdown-menu-milestone a', text: "No Milestone").click
+ click_update_issues_button
+
+ expect(first('.issue')).to_not have_content milestone.title
+ end
+ end
+
+ def create_closed
+ create(:issue, project: project, state: :closed)
+ end
+
+ def create_assigned
+ create(:issue, project: project, assignee: user)
+ end
+
+ def create_with_milestone
+ create(:issue, project: project, milestone: milestone)
+ end
+
+ def click_update_issues_button
+ find('.update_selected_issues').click
+ end
+end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index e844e681ebf..db46657c36a 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -34,20 +34,7 @@ describe 'Issues', feature: true do
fill_in 'issue_title', with: 'bug 345'
fill_in 'issue_description', with: 'bug description'
end
-
- it 'does not change issue count' do
- expect { click_button 'Save changes' }.to_not change { Issue.count }
- end
-
- it 'should update issue fields' do
- click_button 'Save changes'
-
- expect(page).to have_content @user.name
- expect(page).to have_content 'bug 345'
- expect(page).to have_content project.name
- end
end
-
end
describe 'Editing issue assignee' do
@@ -70,7 +57,7 @@ describe 'Issues', feature: true do
click_button 'Save changes'
page.within('.assignee') do
- expect(page).to have_content 'None'
+ expect(page).to have_content 'No assignee - assign yourself'
end
expect(issue.reload.assignee).to be_nil
@@ -198,20 +185,26 @@ describe 'Issues', feature: true do
end
describe 'update assignee from issue#show' do
- let(:issue) { create(:issue, project: project, author: @user) }
+ let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
context 'by autorized user' do
- it 'with dropdown menu' do
+ it 'allows user to select unassigned', js: true do
visit namespace_project_issue_path(project.namespace, project, issue)
- find('.issuable-sidebar #issue_assignee_id').
- set project.team.members.first.id
- click_button 'Update Issue'
+ page.within('.assignee') do
+ expect(page).to have_content "#{@user.name}"
+ end
+
+ find('.block.assignee .edit-link').click
+ sleep 2 # wait for ajax stuff to complete
+ first('.dropdown-menu-user-link').click
+ sleep 2
+ page.within('.assignee') do
+ expect(page).to have_content 'No assignee'
+ end
- expect(page).to have_content 'Assignee'
- has_select?('issue_assignee_id',
- selected: project.team.members.first.name)
+ expect(issue.reload.assignee).to be_nil
end
end
@@ -221,8 +214,6 @@ describe 'Issues', feature: true do
before :each do
project.team << [[guest], :guest]
- issue.assignee = @user
- issue.save
end
it 'shows assignee text', js: true do
@@ -241,20 +232,23 @@ describe 'Issues', feature: true do
context 'by authorized user' do
- it 'with dropdown menu' do
- visit namespace_project_issue_path(project.namespace, project, issue)
- find('.issuable-sidebar').
- select(milestone.title, from: 'issue_milestone_id')
- click_button 'Update Issue'
+ it 'allows user to select unassigned', js: true do
+ visit namespace_project_issue_path(project.namespace, project, issue)
- expect(page).to have_content "Milestone changed to #{milestone.title}"
+ page.within('.milestone') do
+ expect(page).to have_content "None"
+ end
+ find('.block.milestone .edit-link').click
+ sleep 2 # wait for ajax stuff to complete
+ first('.dropdown-content li').click
+ sleep 2
page.within('.milestone') do
- expect(page).to have_content milestone.title
+ expect(page).to have_content 'None'
end
- has_select?('issue_assignee_id', selected: milestone.title)
+ expect(issue.reload.milestone).to be_nil
end
end
@@ -283,25 +277,6 @@ describe 'Issues', feature: true do
issue.assignee = user2
issue.save
end
-
- it 'allows user to remove assignee', js: true do
- visit namespace_project_issue_path(project.namespace, project, issue)
-
- page.within('.assignee') do
- expect(page).to have_content user2.name
- end
-
- find('.assignee .edit-link').click
- sleep 2 # wait for ajax stuff to complete
- first('.user-result').click
-
- page.within('.assignee') do
- expect(page).to have_content 'None'
- end
-
- sleep 2 # wait for ajax stuff to complete
- expect(issue.reload.assignee).to be_nil
- end
end
end
diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb
index 1b2fd1bab10..c57ab5f3b03 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -11,7 +11,41 @@ feature 'Merge Request filtering by Milestone', feature: true do
visit_merge_requests(project)
filter_by_milestone(Milestone::None.title)
- expect(page).to have_css('.merge-request-title', count: 1)
+ expect(page).to have_css('.merge-request', count: 1)
+ end
+
+ context 'filters by upcoming milestone', js: true do
+ it 'should not show issues with no expiry' do
+ create(:merge_request, :with_diffs, source_project: project)
+ create(:merge_request, :simple, source_project: project, milestone: milestone)
+
+ visit_merge_requests(project)
+ filter_by_milestone(Milestone::Upcoming.title)
+
+ expect(page).to have_css('.merge-request', count: 0)
+ end
+
+ it 'should show issues in future' do
+ milestone = create(:milestone, project: project, due_date: Date.tomorrow)
+ create(:merge_request, :with_diffs, source_project: project)
+ create(:merge_request, :simple, source_project: project, milestone: milestone)
+
+ visit_merge_requests(project)
+ filter_by_milestone(Milestone::Upcoming.title)
+
+ expect(page).to have_css('.merge-request', count: 1)
+ end
+
+ it 'should not show issues in past' do
+ milestone = create(:milestone, project: project, due_date: Date.yesterday)
+ create(:merge_request, :with_diffs, source_project: project)
+ create(:merge_request, :simple, source_project: project, milestone: milestone)
+
+ visit_merge_requests(project)
+ filter_by_milestone(Milestone::Upcoming.title)
+
+ expect(page).to have_css('.merge-request', count: 0)
+ end
end
scenario 'filters by a specific Milestone', js: true do
@@ -21,7 +55,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
visit_merge_requests(project)
filter_by_milestone(milestone.title)
- expect(page).to have_css('.merge-request-title', count: 1)
+ expect(page).to have_css('.merge-request', count: 1)
end
def visit_merge_requests(project)
@@ -30,8 +64,6 @@ feature 'Merge Request filtering by Milestone', feature: true do
def filter_by_milestone(title)
find(".js-milestone-select").click
- sleep 0.5
find(".milestone-filter a", text: title).click
- sleep 1
end
end
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index 84c036e59c0..3e6289a46b1 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -1,19 +1,46 @@
require 'spec_helper'
describe "Search", feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+
before do
- login_as :user
- @project = create(:project, namespace: @user.namespace)
- @project.team << [@user, :reporter]
+ login_with(user)
+ project.team << [user, :reporter]
visit search_path
+ end
- page.within '.search-holder' do
- fill_in "search", with: @project.name[0..3]
- click_button "Search"
+ describe 'searching for Projects' do
+ it 'finds a project' do
+ page.within '.search-holder' do
+ fill_in "search", with: project.name[0..3]
+ click_button "Search"
+ end
+
+ expect(page).to have_content project.name
end
end
- it "should show project in search results" do
- expect(page).to have_content @project.name
+ context 'search for comments' do
+ it 'finds a snippet' do
+ snippet = create(:project_snippet, :private, project: project, author: user, title: 'Some title')
+ note = create(:note,
+ noteable: snippet,
+ author: user,
+ note: 'Supercalifragilisticexpialidocious',
+ project: project)
+ # Must visit project dashboard since global search won't search
+ # everything (e.g. comments, snippets, etc.)
+ visit namespace_project_path(project.namespace, project)
+
+ page.within '.search' do
+ fill_in 'search', with: note.note
+ click_button 'Go'
+ end
+
+ click_link 'Comments'
+
+ expect(page).to have_link(snippet.title)
+ end
end
end
diff --git a/spec/features/security/group/internal_access_spec.rb b/spec/features/security/group/internal_access_spec.rb
new file mode 100644
index 00000000000..71b783b7276
--- /dev/null
+++ b/spec/features/security/group/internal_access_spec.rb
@@ -0,0 +1,109 @@
+require 'rails_helper'
+
+describe 'Internal Group access', feature: true do
+ include AccessMatchers
+
+ let(:group) { create(:group, :internal) }
+ let(:project) { create(:project, :internal, group: group) }
+
+ let(:owner) { create(:user) }
+ let(:master) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:guest) { create(:user) }
+
+ let(:project_guest) { create(:user) }
+
+ before do
+ group.add_owner(owner)
+ group.add_master(master)
+ group.add_developer(developer)
+ group.add_reporter(reporter)
+ group.add_guest(guest)
+
+ project.team << [project_guest, :guest]
+ end
+
+ describe "Group should be internal" do
+ describe '#internal?' do
+ subject { group.internal? }
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe 'GET /groups/:path' do
+ subject { group_path(group) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for project_guest }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe 'GET /groups/:path/issues' do
+ subject { issues_group_path(group) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for project_guest }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe 'GET /groups/:path/merge_requests' do
+ subject { merge_requests_group_path(group) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for project_guest }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+
+ describe 'GET /groups/:path/group_members' do
+ subject { group_group_members_path(group) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for project_guest }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe 'GET /groups/:path/edit' do
+ subject { edit_group_path(group) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_denied_for master }
+ it { is_expected.to be_denied_for developer }
+ it { is_expected.to be_denied_for reporter }
+ it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_denied_for project_guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :visitor }
+ it { is_expected.to be_denied_for :external }
+ end
+end
diff --git a/spec/features/security/group/private_access_spec.rb b/spec/features/security/group/private_access_spec.rb
new file mode 100644
index 00000000000..cc9aee802f9
--- /dev/null
+++ b/spec/features/security/group/private_access_spec.rb
@@ -0,0 +1,109 @@
+require 'rails_helper'
+
+describe 'Private Group access', feature: true do
+ include AccessMatchers
+
+ let(:group) { create(:group, :private) }
+ let(:project) { create(:project, :private, group: group) }
+
+ let(:owner) { create(:user) }
+ let(:master) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:guest) { create(:user) }
+
+ let(:project_guest) { create(:user) }
+
+ before do
+ group.add_owner(owner)
+ group.add_master(master)
+ group.add_developer(developer)
+ group.add_reporter(reporter)
+ group.add_guest(guest)
+
+ project.team << [project_guest, :guest]
+ end
+
+ describe "Group should be private" do
+ describe '#private?' do
+ subject { group.private? }
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe 'GET /groups/:path' do
+ subject { group_path(group) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for project_guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe 'GET /groups/:path/issues' do
+ subject { issues_group_path(group) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for project_guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe 'GET /groups/:path/merge_requests' do
+ subject { merge_requests_group_path(group) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for project_guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+
+ describe 'GET /groups/:path/group_members' do
+ subject { group_group_members_path(group) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for project_guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe 'GET /groups/:path/edit' do
+ subject { edit_group_path(group) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_denied_for master }
+ it { is_expected.to be_denied_for developer }
+ it { is_expected.to be_denied_for reporter }
+ it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_denied_for project_guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :visitor }
+ it { is_expected.to be_denied_for :external }
+ end
+end
diff --git a/spec/features/security/group/public_access_spec.rb b/spec/features/security/group/public_access_spec.rb
new file mode 100644
index 00000000000..db986683dbe
--- /dev/null
+++ b/spec/features/security/group/public_access_spec.rb
@@ -0,0 +1,109 @@
+require 'rails_helper'
+
+describe 'Public Group access', feature: true do
+ include AccessMatchers
+
+ let(:group) { create(:group, :public) }
+ let(:project) { create(:project, :public, group: group) }
+
+ let(:owner) { create(:user) }
+ let(:master) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:guest) { create(:user) }
+
+ let(:project_guest) { create(:user) }
+
+ before do
+ group.add_owner(owner)
+ group.add_master(master)
+ group.add_developer(developer)
+ group.add_reporter(reporter)
+ group.add_guest(guest)
+
+ project.team << [project_guest, :guest]
+ end
+
+ describe "Group should be public" do
+ describe '#public?' do
+ subject { group.public? }
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe 'GET /groups/:path' do
+ subject { group_path(group) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for project_guest }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_allowed_for :external }
+ it { is_expected.to be_allowed_for :visitor }
+ end
+
+ describe 'GET /groups/:path/issues' do
+ subject { issues_group_path(group) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for project_guest }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_allowed_for :external }
+ it { is_expected.to be_allowed_for :visitor }
+ end
+
+ describe 'GET /groups/:path/merge_requests' do
+ subject { merge_requests_group_path(group) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for project_guest }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_allowed_for :external }
+ it { is_expected.to be_allowed_for :visitor }
+ end
+
+
+ describe 'GET /groups/:path/group_members' do
+ subject { group_group_members_path(group) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for project_guest }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_allowed_for :external }
+ it { is_expected.to be_allowed_for :visitor }
+ end
+
+ describe 'GET /groups/:path/edit' do
+ subject { edit_group_path(group) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_denied_for master }
+ it { is_expected.to be_denied_for developer }
+ it { is_expected.to be_denied_for reporter }
+ it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_denied_for project_guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :visitor }
+ it { is_expected.to be_denied_for :external }
+ end
+end
diff --git a/spec/features/security/group_access_spec.rb b/spec/features/security/group_access_spec.rb
deleted file mode 100644
index 65f8073c693..00000000000
--- a/spec/features/security/group_access_spec.rb
+++ /dev/null
@@ -1,284 +0,0 @@
-require 'rails_helper'
-
-describe 'Group access', feature: true do
- include AccessMatchers
-
- def group
- @group ||= create(:group)
- end
-
- def create_project(access_level)
- if access_level == :mixed
- create(:empty_project, :public, group: group)
- create(:empty_project, :internal, group: group)
- else
- create(:empty_project, access_level, group: group)
- end
- end
-
- def group_member(access_level, grp = group())
- level = Object.const_get("Gitlab::Access::#{access_level.upcase}")
-
- create(:user).tap do |user|
- grp.add_user(user, level)
- end
- end
-
- describe 'GET /groups/new' do
- subject { new_group_path }
-
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :visitor }
- end
-
- describe 'GET /groups/:path' do
- subject { group_path(group) }
-
- context 'with public projects' do
- let!(:project) { create_project(:public) }
-
- it { is_expected.to be_allowed_for group_member(:owner) }
- it { is_expected.to be_allowed_for group_member(:master) }
- it { is_expected.to be_allowed_for group_member(:reporter) }
- it { is_expected.to be_allowed_for group_member(:guest) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :visitor }
- end
-
- context 'with mixed projects' do
- let!(:project) { create_project(:mixed) }
-
- it { is_expected.to be_allowed_for group_member(:owner) }
- it { is_expected.to be_allowed_for group_member(:master) }
- it { is_expected.to be_allowed_for group_member(:reporter) }
- it { is_expected.to be_allowed_for group_member(:guest) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :visitor }
- end
-
- context 'with internal projects' do
- let!(:project) { create_project(:internal) }
-
- it { is_expected.to be_allowed_for group_member(:owner) }
- it { is_expected.to be_allowed_for group_member(:master) }
- it { is_expected.to be_allowed_for group_member(:reporter) }
- it { is_expected.to be_allowed_for group_member(:guest) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :visitor }
- end
-
- context 'with no projects' do
- it { is_expected.to be_allowed_for group_member(:owner) }
- it { is_expected.to be_allowed_for group_member(:master) }
- it { is_expected.to be_allowed_for group_member(:reporter) }
- it { is_expected.to be_allowed_for group_member(:guest) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :visitor }
- end
- end
-
- describe 'GET /groups/:path/issues' do
- subject { issues_group_path(group) }
-
- context 'with public projects' do
- let!(:project) { create_project(:public) }
-
- it { is_expected.to be_allowed_for group_member(:owner) }
- it { is_expected.to be_allowed_for group_member(:master) }
- it { is_expected.to be_allowed_for group_member(:reporter) }
- it { is_expected.to be_allowed_for group_member(:guest) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :visitor }
- end
-
- context 'with mixed projects' do
- let!(:project) { create_project(:mixed) }
-
- it { is_expected.to be_allowed_for group_member(:owner) }
- it { is_expected.to be_allowed_for group_member(:master) }
- it { is_expected.to be_allowed_for group_member(:reporter) }
- it { is_expected.to be_allowed_for group_member(:guest) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :visitor }
- end
-
- context 'with internal projects' do
- let!(:project) { create_project(:internal) }
-
- it { is_expected.to be_allowed_for group_member(:owner) }
- it { is_expected.to be_allowed_for group_member(:master) }
- it { is_expected.to be_allowed_for group_member(:reporter) }
- it { is_expected.to be_allowed_for group_member(:guest) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :visitor }
- end
-
- context 'with no projects' do
- it { is_expected.to be_allowed_for group_member(:owner) }
- it { is_expected.to be_allowed_for group_member(:master) }
- it { is_expected.to be_allowed_for group_member(:reporter) }
- it { is_expected.to be_allowed_for group_member(:guest) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :visitor }
- end
- end
-
- describe 'GET /groups/:path/merge_requests' do
- subject { merge_requests_group_path(group) }
-
- context 'with public projects' do
- let!(:project) { create_project(:public) }
-
- it { is_expected.to be_allowed_for group_member(:owner) }
- it { is_expected.to be_allowed_for group_member(:master) }
- it { is_expected.to be_allowed_for group_member(:reporter) }
- it { is_expected.to be_allowed_for group_member(:guest) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :visitor }
- end
-
- context 'with mixed projects' do
- let!(:project) { create_project(:mixed) }
-
- it { is_expected.to be_allowed_for group_member(:owner) }
- it { is_expected.to be_allowed_for group_member(:master) }
- it { is_expected.to be_allowed_for group_member(:reporter) }
- it { is_expected.to be_allowed_for group_member(:guest) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :visitor }
- end
-
- context 'with internal projects' do
- let!(:project) { create_project(:internal) }
-
- it { is_expected.to be_allowed_for group_member(:owner) }
- it { is_expected.to be_allowed_for group_member(:master) }
- it { is_expected.to be_allowed_for group_member(:reporter) }
- it { is_expected.to be_allowed_for group_member(:guest) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :visitor }
- end
-
- context 'with no projects' do
- it { is_expected.to be_allowed_for group_member(:owner) }
- it { is_expected.to be_allowed_for group_member(:master) }
- it { is_expected.to be_allowed_for group_member(:reporter) }
- it { is_expected.to be_allowed_for group_member(:guest) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :visitor }
- end
- end
-
- describe 'GET /groups/:path/group_members' do
- subject { group_group_members_path(group) }
-
- context 'with public projects' do
- let!(:project) { create_project(:public) }
-
- it { is_expected.to be_allowed_for group_member(:owner) }
- it { is_expected.to be_allowed_for group_member(:master) }
- it { is_expected.to be_allowed_for group_member(:reporter) }
- it { is_expected.to be_allowed_for group_member(:guest) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :visitor }
- end
-
- context 'with mixed projects' do
- let!(:project) { create_project(:mixed) }
-
- it { is_expected.to be_allowed_for group_member(:owner) }
- it { is_expected.to be_allowed_for group_member(:master) }
- it { is_expected.to be_allowed_for group_member(:reporter) }
- it { is_expected.to be_allowed_for group_member(:guest) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :visitor }
- end
-
- context 'with internal projects' do
- let!(:project) { create_project(:internal) }
-
- it { is_expected.to be_allowed_for group_member(:owner) }
- it { is_expected.to be_allowed_for group_member(:master) }
- it { is_expected.to be_allowed_for group_member(:reporter) }
- it { is_expected.to be_allowed_for group_member(:guest) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :visitor }
- end
-
- context 'with no projects' do
- it { is_expected.to be_allowed_for group_member(:owner) }
- it { is_expected.to be_allowed_for group_member(:master) }
- it { is_expected.to be_allowed_for group_member(:reporter) }
- it { is_expected.to be_allowed_for group_member(:guest) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :visitor }
- end
- end
-
- describe 'GET /groups/:path/edit' do
- subject { edit_group_path(group) }
-
- context 'with public projects' do
- let!(:project) { create_project(:public) }
-
- it { is_expected.to be_allowed_for group_member(:owner) }
- it { is_expected.to be_denied_for group_member(:master) }
- it { is_expected.to be_denied_for group_member(:reporter) }
- it { is_expected.to be_denied_for group_member(:guest) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :visitor }
- end
-
- context 'with mixed projects' do
- let!(:project) { create_project(:mixed) }
-
- it { is_expected.to be_allowed_for group_member(:owner) }
- it { is_expected.to be_denied_for group_member(:master) }
- it { is_expected.to be_denied_for group_member(:reporter) }
- it { is_expected.to be_denied_for group_member(:guest) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :visitor }
- end
-
- context 'with internal projects' do
- let!(:project) { create_project(:internal) }
-
- it { is_expected.to be_allowed_for group_member(:owner) }
- it { is_expected.to be_denied_for group_member(:master) }
- it { is_expected.to be_denied_for group_member(:reporter) }
- it { is_expected.to be_denied_for group_member(:guest) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :visitor }
- end
-
- context 'with no projects' do
- it { is_expected.to be_allowed_for group_member(:owner) }
- it { is_expected.to be_denied_for group_member(:master) }
- it { is_expected.to be_denied_for group_member(:reporter) }
- it { is_expected.to be_denied_for group_member(:guest) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :visitor }
- end
- end
-end
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index f88c591d897..79d5bf4cf06 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -5,25 +5,22 @@ describe "Internal Project Access", feature: true do
let(:project) { create(:project, :internal) }
- let(:master) { create(:user) }
- let(:guest) { create(:user) }
- let(:reporter) { create(:user) }
- let(:external_team_member) { create(:user, external: true) }
+ let(:owner) { project.owner }
+ let(:master) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:guest) { create(:user) }
before do
- # full access
project.team << [master, :master]
- project.team << [external_team_member, :master]
-
- # readonly
+ project.team << [developer, :developer]
project.team << [reporter, :reporter]
+ project.team << [guest, :guest]
end
describe "Project should be internal" do
- subject { project }
-
describe '#internal?' do
- subject { super().internal? }
+ subject { project.internal? }
it { is_expected.to be_truthy }
end
end
@@ -31,78 +28,84 @@ describe "Internal Project Access", feature: true do
describe "GET /:project_path" do
subject { namespace_project_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/tree/master" do
subject { namespace_project_tree_path(project.namespace, project, project.repository.root_ref) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/commits/master" do
subject { namespace_project_commits_path(project.namespace, project, project.repository.root_ref, limit: 1) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/commit/:sha" do
subject { namespace_project_commit_path(project.namespace, project, project.repository.commit) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/compare" do
subject { namespace_project_compare_index_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/project_members" do
subject { namespace_project_project_members_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_denied_for developer }
it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -110,52 +113,56 @@ describe "Internal Project Access", feature: true do
let(:commit) { project.repository.commit }
subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore')) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/edit" do
subject { edit_namespace_project_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_denied_for developer }
it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/deploy_keys" do
subject { namespace_project_deploy_keys_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_denied_for developer }
it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/issues" do
subject { namespace_project_issues_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -163,65 +170,70 @@ describe "Internal Project Access", feature: true do
let(:issue) { create(:issue, project: project) }
subject { edit_namespace_project_issue_path(project.namespace, project, issue) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/snippets" do
subject { namespace_project_snippets_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/snippets/new" do
subject { new_namespace_project_snippet_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/merge_requests" do
subject { namespace_project_merge_requests_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/merge_requests/new" do
subject { new_namespace_project_merge_request_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -233,13 +245,14 @@ describe "Internal Project Access", feature: true do
allow_any_instance_of(Project).to receive(:branches).and_return([])
end
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -251,26 +264,28 @@ describe "Internal Project Access", feature: true do
allow_any_instance_of(Project).to receive(:tags).and_return([])
end
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/hooks" do
subject { namespace_project_hooks_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_denied_for developer }
it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
end
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index 19f287ce7a4..0a89193eb67 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -3,27 +3,24 @@ require 'spec_helper'
describe "Private Project Access", feature: true do
include AccessMatchers
- let(:project) { create(:project) }
+ let(:project) { create(:project, :private) }
- let(:master) { create(:user) }
- let(:guest) { create(:user) }
- let(:reporter) { create(:user) }
- let(:external_team_member) { create(:user, external: true) }
+ let(:owner) { project.owner }
+ let(:master) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:guest) { create(:user) }
before do
- # full access
project.team << [master, :master]
- project.team << [external_team_member, :master]
-
- # readonly
+ project.team << [developer, :developer]
project.team << [reporter, :reporter]
+ project.team << [guest, :guest]
end
describe "Project should be private" do
- subject { project }
-
describe '#private?' do
- subject { super().private? }
+ subject { project.private? }
it { is_expected.to be_truthy }
end
end
@@ -31,77 +28,84 @@ describe "Private Project Access", feature: true do
describe "GET /:project_path" do
subject { namespace_project_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_allowed_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/tree/master" do
subject { namespace_project_tree_path(project.namespace, project, project.repository.root_ref) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/commits/master" do
subject { namespace_project_commits_path(project.namespace, project, project.repository.root_ref, limit: 1) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/commit/:sha" do
subject { namespace_project_commit_path(project.namespace, project, project.repository.commit) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
- it { is_expected.to be_allowed_for external_team_member }
+ it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/compare" do
subject { namespace_project_compare_index_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/project_members" do
subject { namespace_project_project_members_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_denied_for developer }
it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -109,52 +113,56 @@ describe "Private Project Access", feature: true do
let(:commit) { project.repository.commit }
subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore'))}
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/edit" do
subject { edit_namespace_project_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_denied_for developer }
it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/deploy_keys" do
subject { namespace_project_deploy_keys_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_denied_for developer }
it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/issues" do
subject { namespace_project_issues_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_allowed_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -162,39 +170,42 @@ describe "Private Project Access", feature: true do
let(:issue) { create(:issue, project: project) }
subject { edit_namespace_project_issue_path(project.namespace, project, issue) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/snippets" do
subject { namespace_project_snippets_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_allowed_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/merge_requests" do
subject { namespace_project_merge_requests_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_allowed_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -206,13 +217,14 @@ describe "Private Project Access", feature: true do
allow_any_instance_of(Project).to receive(:branches).and_return([])
end
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -224,26 +236,28 @@ describe "Private Project Access", feature: true do
allow_any_instance_of(Project).to receive(:tags).and_return([])
end
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/hooks" do
subject { namespace_project_hooks_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_denied_for developer }
it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
- it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
end
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index 4e135076367..40daac89d40 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -3,29 +3,24 @@ require 'spec_helper'
describe "Public Project Access", feature: true do
include AccessMatchers
- let(:project) { create(:project) }
+ let(:project) { create(:project, :public) }
- let(:master) { create(:user) }
- let(:guest) { create(:user) }
- let(:reporter) { create(:user) }
+ let(:owner) { project.owner }
+ let(:master) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:guest) { create(:user) }
before do
- # public project
- project.visibility_level = Gitlab::VisibilityLevel::PUBLIC
- project.save!
-
- # full access
project.team << [master, :master]
-
- # readonly
+ project.team << [developer, :developer]
project.team << [reporter, :reporter]
+ project.team << [guest, :guest]
end
describe "Project should be public" do
- subject { project }
-
describe '#public?' do
- subject { super().public? }
+ subject { project.public? }
it { is_expected.to be_truthy }
end
end
@@ -33,9 +28,11 @@ describe "Public Project Access", feature: true do
describe "GET /:project_path" do
subject { namespace_project_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_allowed_for :external }
@@ -45,9 +42,11 @@ describe "Public Project Access", feature: true do
describe "GET /:project_path/tree/master" do
subject { namespace_project_tree_path(project.namespace, project, project.repository.root_ref) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_allowed_for :external }
@@ -57,9 +56,11 @@ describe "Public Project Access", feature: true do
describe "GET /:project_path/commits/master" do
subject { namespace_project_commits_path(project.namespace, project, project.repository.root_ref, limit: 1) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_allowed_for :external }
@@ -69,9 +70,11 @@ describe "Public Project Access", feature: true do
describe "GET /:project_path/commit/:sha" do
subject { namespace_project_commit_path(project.namespace, project, project.repository.commit) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_allowed_for :external }
@@ -81,9 +84,11 @@ describe "Public Project Access", feature: true do
describe "GET /:project_path/compare" do
subject { namespace_project_compare_index_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_allowed_for :external }
@@ -93,9 +98,11 @@ describe "Public Project Access", feature: true do
describe "GET /:project_path/project_members" do
subject { namespace_project_project_members_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_denied_for developer }
it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
@@ -108,9 +115,11 @@ describe "Public Project Access", feature: true do
context "when allowed for public" do
before { project.update(public_builds: true) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_allowed_for :external }
@@ -120,9 +129,11 @@ describe "Public Project Access", feature: true do
context "when disallowed for public" do
before { project.update(public_builds: false) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
@@ -138,9 +149,11 @@ describe "Public Project Access", feature: true do
context "when allowed for public" do
before { project.update(public_builds: true) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_allowed_for :external }
@@ -150,9 +163,11 @@ describe "Public Project Access", feature: true do
context "when disallowed for public" do
before { project.update(public_builds: false) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
@@ -165,9 +180,11 @@ describe "Public Project Access", feature: true do
subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore')) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_allowed_for :visitor }
@@ -176,9 +193,11 @@ describe "Public Project Access", feature: true do
describe "GET /:project_path/edit" do
subject { edit_namespace_project_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_denied_for developer }
it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
@@ -188,9 +207,11 @@ describe "Public Project Access", feature: true do
describe "GET /:project_path/deploy_keys" do
subject { namespace_project_deploy_keys_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_denied_for developer }
it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
@@ -200,9 +221,11 @@ describe "Public Project Access", feature: true do
describe "GET /:project_path/issues" do
subject { namespace_project_issues_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_allowed_for :external }
@@ -213,9 +236,11 @@ describe "Public Project Access", feature: true do
let(:issue) { create(:issue, project: project) }
subject { edit_namespace_project_issue_path(project.namespace, project, issue) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
@@ -225,9 +250,11 @@ describe "Public Project Access", feature: true do
describe "GET /:project_path/snippets" do
subject { namespace_project_snippets_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_allowed_for :external }
@@ -237,9 +264,11 @@ describe "Public Project Access", feature: true do
describe "GET /:project_path/snippets/new" do
subject { new_namespace_project_snippet_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
@@ -249,9 +278,11 @@ describe "Public Project Access", feature: true do
describe "GET /:project_path/merge_requests" do
subject { namespace_project_merge_requests_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_allowed_for :external }
@@ -261,9 +292,11 @@ describe "Public Project Access", feature: true do
describe "GET /:project_path/merge_requests/new" do
subject { new_namespace_project_merge_request_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
@@ -278,9 +311,11 @@ describe "Public Project Access", feature: true do
allow_any_instance_of(Project).to receive(:branches).and_return([])
end
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_allowed_for :external }
@@ -295,9 +330,11 @@ describe "Public Project Access", feature: true do
allow_any_instance_of(Project).to receive(:tags).and_return([])
end
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_allowed_for :external }
@@ -307,9 +344,11 @@ describe "Public Project Access", feature: true do
describe "GET /:project_path/hooks" do
subject { namespace_project_hooks_path(project.namespace, project) }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_denied_for developer }
it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
diff --git a/spec/features/security/project/snippet/internal_access_spec.rb b/spec/features/security/project/snippet/internal_access_spec.rb
new file mode 100644
index 00000000000..db53a9cec97
--- /dev/null
+++ b/spec/features/security/project/snippet/internal_access_spec.rb
@@ -0,0 +1,78 @@
+require 'spec_helper'
+
+describe "Internal Project Snippets Access", feature: true do
+ include AccessMatchers
+
+ let(:project) { create(:project, :internal) }
+
+ let(:owner) { project.owner }
+ let(:master) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:guest) { create(:user) }
+ let(:internal_snippet) { create(:project_snippet, :internal, project: project, author: owner) }
+ let(:private_snippet) { create(:project_snippet, :private, project: project, author: owner) }
+
+ before do
+ project.team << [master, :master]
+ project.team << [developer, :developer]
+ project.team << [reporter, :reporter]
+ project.team << [guest, :guest]
+ end
+
+ describe "GET /:project_path/snippets" do
+ subject { namespace_project_snippets_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe "GET /:project_path/snippets/new" do
+ subject { new_namespace_project_snippet_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe "GET /:project_path/snippets/:id for an internal snippet" do
+ subject { namespace_project_snippet_path(project.namespace, project, internal_snippet) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe "GET /:project_path/snippets/:id for a private snippet" do
+ subject { namespace_project_snippet_path(project.namespace, project, private_snippet) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+end
diff --git a/spec/features/security/project/snippet/private_access_spec.rb b/spec/features/security/project/snippet/private_access_spec.rb
new file mode 100644
index 00000000000..d23d645c8e5
--- /dev/null
+++ b/spec/features/security/project/snippet/private_access_spec.rb
@@ -0,0 +1,63 @@
+require 'spec_helper'
+
+describe "Private Project Snippets Access", feature: true do
+ include AccessMatchers
+
+ let(:project) { create(:project, :private) }
+
+ let(:owner) { project.owner }
+ let(:master) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:guest) { create(:user) }
+ let(:private_snippet) { create(:project_snippet, :private, project: project, author: owner) }
+
+ before do
+ project.team << [master, :master]
+ project.team << [developer, :developer]
+ project.team << [reporter, :reporter]
+ project.team << [guest, :guest]
+ end
+
+ describe "GET /:project_path/snippets" do
+ subject { namespace_project_snippets_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe "GET /:project_path/snippets/new" do
+ subject { new_namespace_project_snippet_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe "GET /:project_path/snippets/:id for a private snippet" do
+ subject { namespace_project_snippet_path(project.namespace, project, private_snippet) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+end
diff --git a/spec/features/security/project/snippet/public_access_spec.rb b/spec/features/security/project/snippet/public_access_spec.rb
new file mode 100644
index 00000000000..e3665b6116a
--- /dev/null
+++ b/spec/features/security/project/snippet/public_access_spec.rb
@@ -0,0 +1,93 @@
+require 'spec_helper'
+
+describe "Public Project Snippets Access", feature: true do
+ include AccessMatchers
+
+ let(:project) { create(:project, :public) }
+
+ let(:owner) { project.owner }
+ let(:master) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:guest) { create(:user) }
+ let(:public_snippet) { create(:project_snippet, :public, project: project, author: owner) }
+ let(:internal_snippet) { create(:project_snippet, :internal, project: project, author: owner) }
+ let(:private_snippet) { create(:project_snippet, :private, project: project, author: owner) }
+
+ before do
+ project.team << [master, :master]
+ project.team << [developer, :developer]
+ project.team << [reporter, :reporter]
+ project.team << [guest, :guest]
+ end
+
+ describe "GET /:project_path/snippets" do
+ subject { namespace_project_snippets_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_allowed_for :external }
+ it { is_expected.to be_allowed_for :visitor }
+ end
+
+ describe "GET /:project_path/snippets/new" do
+ subject { new_namespace_project_snippet_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe "GET /:project_path/snippets/:id for a public snippet" do
+ subject { namespace_project_snippet_path(project.namespace, project, public_snippet) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_allowed_for :external }
+ it { is_expected.to be_allowed_for :visitor }
+ end
+
+ describe "GET /:project_path/snippets/:id for an internal snippet" do
+ subject { namespace_project_snippet_path(project.namespace, project, internal_snippet) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe "GET /:project_path/snippets/:id for a private snippet" do
+ subject { namespace_project_snippet_path(project.namespace, project, private_snippet) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+end
diff --git a/spec/finders/group_projects_finder_spec.rb b/spec/finders/group_projects_finder_spec.rb
new file mode 100644
index 00000000000..fdd3849816f
--- /dev/null
+++ b/spec/finders/group_projects_finder_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe GroupProjectsFinder do
+ let(:group) { create(:group) }
+ let(:current_user) { create(:user) }
+
+ let(:finder) { described_class.new(source_user) }
+
+ let!(:public_project) { create(:project, :public, group: group, path: '1') }
+ let!(:private_project) { create(:project, :private, group: group, path: '2') }
+ let!(:shared_project_1) { create(:project, :public, path: '3') }
+ let!(:shared_project_2) { create(:project, :private, path: '4') }
+ let!(:shared_project_3) { create(:project, :internal, path: '5') }
+
+
+ before do
+ shared_project_1.project_group_links.create(group_access: Gitlab::Access::MASTER, group: group)
+ shared_project_2.project_group_links.create(group_access: Gitlab::Access::MASTER, group: group)
+ shared_project_3.project_group_links.create(group_access: Gitlab::Access::MASTER, group: group)
+ end
+
+
+ describe 'with a group member current user' do
+ before { group.add_user(current_user, Gitlab::Access::MASTER) }
+
+ context "only shared" do
+ subject { described_class.new(group, only_shared: true).execute(current_user) }
+ it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1]) }
+ end
+
+ context "only owned" do
+ subject { described_class.new(group, only_owned: true).execute(current_user) }
+ it { is_expected.to eq([private_project, public_project]) }
+ end
+
+ context "all" do
+ subject { described_class.new(group).execute(current_user) }
+ it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1, private_project, public_project]) }
+ end
+ end
+
+ describe 'without group member current_user' do
+ before { shared_project_2.team << [current_user, Gitlab::Access::MASTER] }
+
+ context "only shared" do
+ context "without external user" do
+ subject { described_class.new(group, only_shared: true).execute(current_user) }
+ it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1]) }
+ end
+
+ context "with external user" do
+ before { current_user.update_attributes(external: true) }
+ subject { described_class.new(group, only_shared: true).execute(current_user) }
+ it { is_expected.to eq([shared_project_2, shared_project_1]) }
+ end
+ end
+
+ context "only owned" do
+ context "without external user" do
+ before { private_project.team << [current_user, Gitlab::Access::MASTER] }
+ subject { described_class.new(group, only_owned: true).execute(current_user) }
+ it { is_expected.to eq([private_project, public_project]) }
+ end
+
+ context "with external user" do
+ before { current_user.update_attributes(external: true) }
+ subject { described_class.new(group, only_owned: true).execute(current_user) }
+ it { is_expected.to eq([public_project]) }
+ end
+
+ context "all" do
+ subject { described_class.new(group).execute(current_user) }
+ it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1, public_project]) }
+ end
+ end
+ end
+
+ describe "no user" do
+ context "only shared" do
+ subject { described_class.new(group, only_shared: true).execute(current_user) }
+ it { is_expected.to eq([shared_project_3, shared_project_1]) }
+ end
+
+ context "only owned" do
+ subject { described_class.new(group, only_owned: true).execute(current_user) }
+ it { is_expected.to eq([public_project]) }
+ end
+ end
+end
diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb
new file mode 100644
index 00000000000..d5d111e8d15
--- /dev/null
+++ b/spec/finders/groups_finder_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe GroupsFinder do
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let!(:private_group) { create(:group, :private) }
+ let!(:internal_group) { create(:group, :internal) }
+ let!(:public_group) { create(:group, :public) }
+ let(:finder) { described_class.new }
+
+ describe 'execute' do
+ describe 'without a user' do
+ subject { finder.execute }
+
+ it { is_expected.to eq([public_group]) }
+ end
+
+ describe 'with a user' do
+ subject { finder.execute(user) }
+
+ context 'normal user' do
+ it { is_expected.to eq([public_group, internal_group]) }
+ end
+
+ context 'external user' do
+ let(:user) { create(:user, external: true) }
+
+ it { is_expected.to eq([public_group]) }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/finders/joined_groups_finder_spec.rb b/spec/finders/joined_groups_finder_spec.rb
new file mode 100644
index 00000000000..f90a8e007c8
--- /dev/null
+++ b/spec/finders/joined_groups_finder_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe JoinedGroupsFinder do
+ describe '#execute' do
+ let!(:profile_owner) { create(:user) }
+ let!(:profile_visitor) { create(:user) }
+
+ let!(:private_group) { create(:group, :private) }
+ let!(:private_group_2) { create(:group, :private) }
+ let!(:internal_group) { create(:group, :internal) }
+ let!(:internal_group_2) { create(:group, :internal) }
+ let!(:public_group) { create(:group, :public) }
+ let!(:public_group_2) { create(:group, :public) }
+ let!(:finder) { described_class.new(profile_owner) }
+
+ context 'without a user' do
+ before do
+ public_group.add_master(profile_owner)
+ end
+
+ it 'only shows public groups from profile owner' do
+ expect(finder.execute).to eq([public_group])
+ end
+ end
+
+ context "with a user" do
+ before do
+ private_group.add_master(profile_owner)
+ internal_group.add_master(profile_owner)
+ public_group.add_master(profile_owner)
+ end
+
+ context "when the profile visitor is in the private group" do
+ before do
+ private_group.add_developer(profile_visitor)
+ end
+
+ it 'only shows groups where both users are authorized to see' do
+ expect(finder.execute(profile_visitor)).to eq([public_group, internal_group, private_group])
+ end
+ end
+
+ context 'if profile visitor is in one of the private group projects' do
+ before do
+ project = create(:project, :private, group: private_group, name: 'B', path: 'B')
+ project.team.add_user(profile_visitor, Gitlab::Access::DEVELOPER)
+ end
+
+ it 'shows group' do
+ expect(finder.execute(profile_visitor)).to eq([public_group, internal_group, private_group])
+ end
+ end
+
+ context 'external users' do
+ before do
+ profile_visitor.update_attributes(external: true)
+ end
+
+ context 'if not a member' do
+ it "does not show internal groups" do
+ expect(finder.execute(profile_visitor)).to eq([public_group])
+ end
+ end
+
+ context "if authorized" do
+ before do
+ internal_group.add_master(profile_visitor)
+ end
+
+ it "shows internal groups if authorized" do
+ expect(finder.execute(profile_visitor)).to eq([public_group, internal_group])
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/finders/personal_projects_finder_spec.rb b/spec/finders/personal_projects_finder_spec.rb
index 38817add456..a4681fe59d8 100644
--- a/spec/finders/personal_projects_finder_spec.rb
+++ b/spec/finders/personal_projects_finder_spec.rb
@@ -1,19 +1,17 @@
require 'spec_helper'
describe PersonalProjectsFinder do
- let(:source_user) { create(:user) }
- let(:current_user) { create(:user) }
+ let(:source_user) { create(:user) }
+ let(:current_user) { create(:user) }
+ let(:finder) { described_class.new(source_user) }
+ let!(:public_project) { create(:project, :public, namespace: source_user.namespace) }
- let(:finder) { described_class.new(source_user) }
-
- let!(:public_project) do
- create(:project, :public, namespace: source_user.namespace, name: 'A',
- path: 'A')
+ let!(:private_project) do
+ create(:project, :private, namespace: source_user.namespace, path: 'mepmep')
end
- let!(:private_project) do
- create(:project, :private, namespace: source_user.namespace, name: 'B',
- path: 'B')
+ let!(:internal_project) do
+ create(:project, :internal, namespace: source_user.namespace, path: 'C')
end
before do
@@ -29,6 +27,14 @@ describe PersonalProjectsFinder do
describe 'with a current user' do
subject { finder.execute(current_user) }
- it { is_expected.to eq([private_project, public_project]) }
+ context 'normal user' do
+ it { is_expected.to eq([internal_project, private_project, public_project]) }
+ end
+
+ context 'external' do
+ before { current_user.update_attributes(external: true) }
+
+ it { is_expected.to eq([private_project, public_project]) }
+ end
end
end
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index fae0da9d898..0a1cc3b3df7 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe ProjectsFinder do
describe '#execute' do
let(:user) { create(:user) }
- let(:group) { create(:group) }
+ let(:group) { create(:group, :public) }
let!(:private_project) do
create(:project, :private, name: 'A', path: 'A')
diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb
index 7fdc5e5d7aa..810016c9658 100644
--- a/spec/finders/snippets_finder_spec.rb
+++ b/spec/finders/snippets_finder_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe SnippetsFinder do
let(:user) { create :user }
let(:user1) { create :user }
- let(:group) { create :group }
+ let(:group) { create :group, :public }
let(:project1) { create(:empty_project, :public, group: group) }
let(:project2) { create(:empty_project, :private, group: group) }
diff --git a/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references.eml b/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references.eml
new file mode 100644
index 00000000000..39d5cefbc2a
--- /dev/null
+++ b/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references.eml
@@ -0,0 +1,42 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: reply@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+In-Reply-To: <issue_1@localhost>
+References: <issue_1@localhost> <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>
+Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+I could not disagree more. I am obviously biased but adventure time is the
+greatest show ever created. Everyone should watch it.
+
+- Jake out
+
+
+On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta
+<reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo> wrote:
+>
+>
+>
+> eviltrout posted in 'Adventure Time Sux' on Discourse Meta:
+>
+> ---
+> hey guys everyone knows adventure time sucks!
+>
+> ---
+> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3
+>
+> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences).
+>
diff --git a/spec/fixtures/emails/valid_reply.eml b/spec/fixtures/emails/valid_reply.eml
index 1e696389954..980e10a8812 100644
--- a/spec/fixtures/emails/valid_reply.eml
+++ b/spec/fixtures/emails/valid_reply.eml
@@ -7,6 +7,8 @@ Date: Thu, 13 Jun 2013 17:03:48 -0400
From: Jake the Dog <jake@adventuretime.ooo>
To: reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo
Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+In-Reply-To: <issue_1@localhost>
+References: <issue_1@localhost> <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>
Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
Mime-Version: 1.0
Content-Type: text/plain;
@@ -37,4 +39,4 @@ On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta
> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3
>
> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences).
-> \ No newline at end of file
+>
diff --git a/spec/helpers/groups_helper.rb b/spec/helpers/groups_helper_spec.rb
index 4ea90a80a92..4ea90a80a92 100644
--- a/spec/helpers/groups_helper.rb
+++ b/spec/helpers/groups_helper_spec.rb
diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb
index 4f129eca183..39042ff7e91 100644
--- a/spec/helpers/labels_helper_spec.rb
+++ b/spec/helpers/labels_helper_spec.rb
@@ -11,7 +11,7 @@ describe LabelsHelper do
end
it 'uses the instance variable' do
- expect(link_to_label(label)).to match %r{<a href="/#{@project.to_reference}/issues\?label_name=#{label.name}">.*</a>}
+ expect(link_to_label(label)).to match %r{<a href="/#{@project.to_reference}/issues\?label_name=#{label.name}"><span class="[\w\s\-]*has-tooltip".*</span></a>}
end
end
@@ -39,6 +39,14 @@ describe LabelsHelper do
end
end
+ context 'with a tooltip argument' do
+ context 'set to false' do
+ it 'does not include the has-tooltip class' do
+ expect(link_to_label(label, tooltip: false)).not_to match %r{has-tooltip}
+ end
+ end
+ end
+
context 'with block' do
it 'passes the block to link_to' do
link = link_to_label(label) { 'Foo' }
@@ -49,7 +57,7 @@ describe LabelsHelper do
context 'without block' do
it 'uses render_colored_label as the link content' do
expect(self).to receive(:render_colored_label).
- with(label).and_return('Foo')
+ with(label, tooltip: true).and_return('Foo')
expect(link_to_label(label)).to match('Foo')
end
end
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index e5df59c4fba..2f9291afc3f 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -19,7 +19,9 @@ describe PreferencesHelper do
['Your Projects (default)', 'projects'],
['Starred Projects', 'stars'],
["Your Projects' Activity", 'project_activity'],
- ["Starred Projects' Activity", 'starred_project_activity']
+ ["Starred Projects' Activity", 'starred_project_activity'],
+ ["Your Groups", 'groups'],
+ ["Your Todos", 'todos']
]
end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 53207767581..c258cfebd73 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -11,16 +11,8 @@ describe ProjectsHelper do
describe "can_change_visibility_level?" do
let(:project) { create(:project) }
-
- let(:fork_project) do
- fork_project = create(:forked_project_with_submodules)
- fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
- fork_project.save
-
- fork_project
- end
-
let(:user) { create(:user) }
+ let(:fork_project) { Projects::ForkService.new(project, user).execute }
it "returns false if there are no appropriate permissions" do
allow(helper).to receive(:can?) { false }
@@ -94,4 +86,23 @@ describe ProjectsHelper do
end
end
end
+
+ describe 'default_clone_protocol' do
+ describe 'using HTTP' do
+ it 'returns HTTP' do
+ expect(helper).to receive(:current_user).and_return(nil)
+
+ expect(helper.send(:default_clone_protocol)).to eq('http')
+ end
+ end
+
+ describe 'using HTTPS' do
+ it 'returns HTTPS' do
+ allow(Gitlab.config.gitlab).to receive(:protocol).and_return('https')
+ expect(helper).to receive(:current_user).and_return(nil)
+
+ expect(helper.send(:default_clone_protocol)).to eq('https')
+ end
+ end
+ end
end
diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb
index cd7596a763d..ff98249570d 100644
--- a/spec/helpers/visibility_level_helper_spec.rb
+++ b/spec/helpers/visibility_level_helper_spec.rb
@@ -8,6 +8,7 @@ describe VisibilityLevelHelper do
end
let(:project) { build(:project) }
+ let(:group) { build(:group) }
let(:personal_snippet) { build(:personal_snippet) }
let(:project_snippet) { build(:project_snippet) }
@@ -19,6 +20,13 @@ describe VisibilityLevelHelper do
end
end
+ context 'used with a Group' do
+ it 'delegates groups to #group_visibility_level_description' do
+ expect(visibility_level_description(Gitlab::VisibilityLevel::PRIVATE, group))
+ .to match /group/i
+ end
+ end
+
context 'called with a Snippet' do
it 'delegates snippets to #snippet_visibility_level_description' do
expect(visibility_level_description(Gitlab::VisibilityLevel::INTERNAL, project_snippet))
@@ -58,13 +66,8 @@ describe VisibilityLevelHelper do
describe "skip_level?" do
describe "forks" do
- let(:project) { create(:project, :internal) }
- let(:fork_project) { create(:forked_project_with_submodules) }
-
- before do
- fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
- fork_project.save
- end
+ let(:project) { create(:project, :internal) }
+ let(:fork_project) { create(:project, forked_from_project: project) }
it "skips levels" do
expect(skip_level?(fork_project, Gitlab::VisibilityLevel::PUBLIC)).to be_truthy
diff --git a/spec/lib/award_emoji_spec.rb b/spec/lib/award_emoji_spec.rb
new file mode 100644
index 00000000000..330678f7f16
--- /dev/null
+++ b/spec/lib/award_emoji_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe AwardEmoji do
+ describe '.urls' do
+ subject { AwardEmoji.urls }
+
+ it { is_expected.to be_an_instance_of(Array) }
+ it { is_expected.to_not be_empty }
+
+ context 'every Hash in the Array' do
+ it 'has the correct keys and values' do
+ subject.each do |hash|
+ expect(hash[:name]).to be_an_instance_of(String)
+ expect(hash[:path]).to be_an_instance_of(String)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb
index e2d21f53b7e..94468abcbb3 100644
--- a/spec/lib/banzai/filter/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb
@@ -56,7 +56,7 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
describe 'label span element' do
it 'includes default classes' do
doc = reference_filter("Label #{reference}")
- expect(doc.css('a span').first.attr('class')).to eq 'label color-label'
+ expect(doc.css('a span').first.attr('class')).to eq 'label color-label has-tooltip'
end
it 'includes a style attribute' do
diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb
index e9bb388e361..c2c2fd0eb6a 100644
--- a/spec/lib/banzai/filter/redactor_filter_spec.rb
+++ b/spec/lib/banzai/filter/redactor_filter_spec.rb
@@ -44,12 +44,82 @@ describe Banzai::Filter::RedactorFilter, lib: true do
end
end
- context "for user references" do
+ context 'with data-issue' do
+ context 'for confidential issues' do
+ it 'removes references for non project members' do
+ non_member = create(:user)
+ project = create(:empty_project, :public)
+ issue = create(:issue, :confidential, project: project)
+
+ link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ doc = filter(link, current_user: non_member)
+
+ expect(doc.css('a').length).to eq 0
+ end
+
+ it 'allows references for author' do
+ author = create(:user)
+ project = create(:empty_project, :public)
+ issue = create(:issue, :confidential, project: project, author: author)
+
+ link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ doc = filter(link, current_user: author)
+
+ expect(doc.css('a').length).to eq 1
+ end
+
+ it 'allows references for assignee' do
+ assignee = create(:user)
+ project = create(:empty_project, :public)
+ issue = create(:issue, :confidential, project: project, assignee: assignee)
+
+ link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ doc = filter(link, current_user: assignee)
+ expect(doc.css('a').length).to eq 1
+ end
+
+ it 'allows references for project members' do
+ member = create(:user)
+ project = create(:empty_project, :public)
+ project.team << [member, :developer]
+ issue = create(:issue, :confidential, project: project)
+
+ link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ doc = filter(link, current_user: member)
+
+ expect(doc.css('a').length).to eq 1
+ end
+
+ it 'allows references for admin' do
+ admin = create(:admin)
+ project = create(:empty_project, :public)
+ issue = create(:issue, :confidential, project: project)
+
+ link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ doc = filter(link, current_user: admin)
+
+ expect(doc.css('a').length).to eq 1
+ end
+ end
+
+ it 'allows references for non confidential issues' do
+ user = create(:user)
+ project = create(:empty_project, :public)
+ issue = create(:issue, project: project)
+
+ link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ doc = filter(link, current_user: user)
+
+ expect(doc.css('a').length).to eq 1
+ end
+ end
+
+ context "for user references" do
context 'with data-group' do
it 'removes unpermitted Group references' do
user = create(:user)
- group = create(:group)
+ group = create(:group, :private)
link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter')
doc = filter(link, current_user: user)
@@ -59,7 +129,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
it 'allows permitted Group references' do
user = create(:user)
- group = create(:group)
+ group = create(:group, :private)
group.add_developer(user)
link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter')
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index fab6412d29f..dcb8a3451bd 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -97,6 +97,28 @@ module Ci
expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
end
+ it "returns builds if only has a triggers keyword specified and a trigger is provided" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: ["triggers"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, true).size).to eq(1)
+ end
+
+ it "does not return builds if only has a triggers keyword specified and no trigger is provided" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: ["triggers"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
+ end
+
it "returns builds if only has current repository path" do
config = YAML.dump({
before_script: ["pwd"],
@@ -203,6 +225,28 @@ module Ci
expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
end
+ it "does not return builds if except has a triggers keyword specified and a trigger is provided" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, except: ["triggers"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, true).size).to eq(0)
+ end
+
+ it "returns builds if except has a triggers keyword specified and no trigger is provided" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, except: ["triggers"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ end
+
it "does not return builds if except has current repository path" do
config = YAML.dump({
before_script: ["pwd"],
@@ -448,19 +492,25 @@ module Ci
end
context 'dependencies to builds' do
+ let(:dependencies) { ['build1', 'build2'] }
+
+ it { expect { subject }.to_not raise_error }
+ end
+
+ context 'dependencies to builds defined as symbols' do
let(:dependencies) { [:build1, :build2] }
it { expect { subject }.to_not raise_error }
end
context 'undefined dependency' do
- let(:dependencies) { [:undefined] }
+ let(:dependencies) { ['undefined'] }
it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: undefined dependency: undefined') }
end
context 'dependencies to deploy' do
- let(:dependencies) { [:deploy] }
+ let(:dependencies) { ['deploy'] }
it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: dependency deploy is not defined in prior stages') }
end
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index f38fadda9ba..566035c60d0 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe ExtractsPath, lib: true do
include ExtractsPath
include RepoHelpers
- include Gitlab::Application.routes.url_helpers
+ include Gitlab::Routing.url_helpers
let(:project) { double('project') }
diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb
new file mode 100644
index 00000000000..b78c2b6224f
--- /dev/null
+++ b/spec/lib/gitlab/badge/build_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+describe Gitlab::Badge::Build do
+ let(:project) { create(:project) }
+ let(:sha) { project.commit.sha }
+ let(:badge) { described_class.new(project, 'master') }
+
+ describe '#type' do
+ subject { badge.type }
+ it { is_expected.to eq 'image/svg+xml' }
+ end
+
+ context 'build exists' do
+ let(:ci_commit) { create(:ci_commit, project: project, sha: sha) }
+ let!(:build) { create(:ci_build, commit: ci_commit) }
+
+
+ context 'build success' do
+ before { build.success! }
+
+ describe '#to_s' do
+ subject { badge.to_s }
+ it { is_expected.to eq 'build-success' }
+ end
+
+ describe '#data' do
+ let(:data) { badge.data }
+
+ it 'contains infromation about success' do
+ expect(status_node(data, 'success')).to be_truthy
+ end
+ end
+ end
+
+ context 'build failed' do
+ before { build.drop! }
+
+ describe '#to_s' do
+ subject { badge.to_s }
+ it { is_expected.to eq 'build-failed' }
+ end
+
+ describe '#data' do
+ let(:data) { badge.data }
+
+ it 'contains infromation about failure' do
+ expect(status_node(data, 'failed')).to be_truthy
+ end
+ end
+ end
+ end
+
+ context 'build does not exist' do
+ describe '#to_s' do
+ subject { badge.to_s }
+ it { is_expected.to eq 'build-unknown' }
+ end
+
+ describe '#data' do
+ let(:data) { badge.data }
+
+ it 'contains infromation about unknown build' do
+ expect(status_node(data, 'unknown')).to be_truthy
+ end
+ end
+ end
+
+ def status_node(data, status)
+ xml = Nokogiri::XML.parse(data)
+ xml.at(%Q{text:contains("#{status}")})
+ end
+end
diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb
index 04cf11fc6f1..a1f51429a79 100644
--- a/spec/lib/gitlab/closing_issue_extractor_spec.rb
+++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb
@@ -11,6 +11,7 @@ describe Gitlab::ClosingIssueExtractor, lib: true do
subject { described_class.new(project, project.creator) }
before do
+ project.team << [project.creator, :developer]
project2.team << [project.creator, :master]
end
@@ -235,6 +236,6 @@ describe Gitlab::ClosingIssueExtractor, lib: true do
end
def urls
- Gitlab::Application.routes.url_helpers
+ Gitlab::Routing.url_helpers
end
end
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 0d9694f2c13..a0cbef6e6a4 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -18,4 +18,18 @@ describe Gitlab::Diff::File, lib: true do
describe :mode_changed? do
it { expect(diff_file.mode_changed?).to be_falsey }
end
+
+ describe '#too_large?' do
+ it 'returns true for a file that is too large' do
+ expect(diff).to receive(:too_large?).and_return(true)
+
+ expect(diff_file.too_large?).to eq(true)
+ end
+
+ it 'returns false for a file that is small enough' do
+ expect(diff).to receive(:too_large?).and_return(false)
+
+ expect(diff_file.too_large?).to eq(false)
+ end
+ end
end
diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb
index abe179cd4af..36267faeb93 100644
--- a/spec/lib/gitlab/email/receiver_spec.rb
+++ b/spec/lib/gitlab/email/receiver_spec.rb
@@ -3,6 +3,7 @@ require "spec_helper"
describe Gitlab::Email::Receiver, lib: true do
before do
stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo")
+ stub_config_setting(host: 'localhost')
end
let(:reply_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" }
@@ -137,5 +138,27 @@ describe Gitlab::Email::Receiver, lib: true do
expect(note.note).to include(markdown)
end
+
+ context 'when sub-addressing is not supported' do
+ before do
+ stub_incoming_email_setting(enabled: true, address: nil)
+ end
+
+ shared_examples 'an email that contains a reply key' do |header|
+ it "fetches the reply key from the #{header} header and creates a comment" do
+ expect { receiver.execute }.to change { noteable.notes.count }.by(1)
+ note = noteable.notes.last
+
+ expect(note.author).to eq(sent_notification.recipient)
+ expect(note.note).to include('I could not disagree more.')
+ end
+ end
+
+ context 'reply key is in the References header' do
+ let(:email_raw) { fixture_file('emails/reply_without_subaddressing_and_key_inside_references.eml') }
+
+ it_behaves_like 'an email that contains a reply key', 'References'
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/fogbugz_import/client_spec.rb b/spec/lib/gitlab/fogbugz_import/client_spec.rb
new file mode 100644
index 00000000000..2dc71be0254
--- /dev/null
+++ b/spec/lib/gitlab/fogbugz_import/client_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe Gitlab::FogbugzImport::Client, lib: true do
+
+ let(:client) { described_class.new(uri: '', token: '') }
+ let(:one_user) { { 'people' => { 'person' => { "ixPerson" => "2", "sFullName" => "James" } } } }
+ let(:two_users) { { 'people' => { 'person' => [one_user, { "ixPerson" => "3" }] } } }
+
+ it 'retrieves user_map with one user' do
+ stub_api(one_user)
+
+ expect(client.user_map.count).to eq(1)
+ end
+
+ it 'retrieves user_map with two users' do
+ stub_api(two_users)
+
+ expect(client.user_map.count).to eq(2)
+ end
+
+ def stub_api(users)
+ allow_any_instance_of(::Fogbugz::Interface).to receive(:command).with(:listPeople).and_return(users)
+ end
+end
diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
new file mode 100644
index 00000000000..0a7ca3ec848
--- /dev/null
+++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe Gitlab::Gfm::ReferenceRewriter do
+ let(:text) { 'some text' }
+ let(:old_project) { create(:project) }
+ let(:new_project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before { old_project.team << [user, :guest] }
+
+ describe '#rewrite' do
+ subject do
+ described_class.new(text, old_project, user).rewrite(new_project)
+ end
+
+ context 'multiple issues and merge requests referenced' do
+ let!(:issue_first) { create(:issue, project: old_project) }
+ let!(:issue_second) { create(:issue, project: old_project) }
+ let!(:merge_request) { create(:merge_request, source_project: old_project) }
+
+ context 'plain text description' do
+ let(:text) { 'Description that references #1, #2 and !1' }
+
+ it { is_expected.to include issue_first.to_reference(new_project) }
+ it { is_expected.to include issue_second.to_reference(new_project) }
+ it { is_expected.to include merge_request.to_reference(new_project) }
+ end
+
+ context 'description with ignored elements' do
+ let(:text) do
+ "Hi. This references #1, but not `#2`\n" +
+ '<pre>and not !1</pre>'
+ end
+
+ it { is_expected.to include issue_first.to_reference(new_project) }
+ it { is_expected.to_not include issue_second.to_reference(new_project) }
+ it { is_expected.to_not include merge_request.to_reference(new_project) }
+ end
+
+ context 'description ambigous elements' do
+ context 'url' do
+ let(:url) { 'http://gitlab.com/#1' }
+ let(:text) { "This references #1, but not #{url}" }
+
+ it { is_expected.to include url }
+ end
+
+ context 'code' do
+ let(:text) { "#1, but not `[#1]`" }
+ it { is_expected.to eq "#{issue_first.to_reference(new_project)}, but not `[#1]`" }
+ end
+
+ context 'code reverse' do
+ let(:text) { "not `#1`, but #1" }
+ it { is_expected.to eq "not `#1`, but #{issue_first.to_reference(new_project)}" }
+ end
+
+ context 'code in random order' do
+ let(:text) { "#1, `#1`, #1, `#1`" }
+ let(:ref) { issue_first.to_reference(new_project) }
+
+ it { is_expected.to eq "#{ref}, `#1`, #{ref}, `#1`" }
+ end
+
+ context 'description with labels' do
+ let!(:label) { create(:label, id: 123, name: 'test', project: old_project) }
+ let(:project_ref) { old_project.to_reference }
+
+ context 'label referenced by id' do
+ let(:text) { '#1 and ~123' }
+ it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~123} }
+ end
+
+ context 'label referenced by text' do
+ let(:text) { '#1 and ~"test"' }
+ it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~123} }
+ end
+ end
+ end
+
+ context 'reference contains milestone' do
+ let(:milestone) { create(:milestone) }
+ let(:text) { "milestone ref: #{milestone.to_reference}" }
+
+ it { is_expected.to eq text }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
new file mode 100644
index 00000000000..eda956e6f0a
--- /dev/null
+++ b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe Gitlab::Gfm::UploadsRewriter do
+ let(:user) { create(:user) }
+ let(:old_project) { create(:project) }
+ let(:new_project) { create(:project) }
+ let(:rewriter) { described_class.new(text, old_project, user) }
+
+ context 'text contains links to uploads' do
+ let(:image_uploader) do
+ build(:file_uploader, project: old_project)
+ end
+
+ let(:zip_uploader) do
+ build(:file_uploader, project: old_project,
+ fixture: 'ci_build_artifacts.zip')
+ end
+
+ let(:text) do
+ "Text and #{image_uploader.to_markdown} and #{zip_uploader.to_markdown}"
+ end
+
+ describe '#rewrite' do
+ let!(:new_text) { rewriter.rewrite(new_project) }
+
+ let(:old_files) { [image_uploader, zip_uploader].map(&:file) }
+ let(:new_files) do
+ described_class.new(new_text, new_project, user).files
+ end
+
+ let(:old_paths) { old_files.map(&:path) }
+ let(:new_paths) { new_files.map(&:path) }
+
+ it 'rewrites content' do
+ expect(new_text).to_not eq text
+ expect(new_text.length).to eq text.length
+ end
+
+ it 'copies files' do
+ expect(new_files).to all(exist)
+ expect(old_paths).to_not match_array new_paths
+ expect(old_paths).to all(include(old_project.path_with_namespace))
+ expect(new_paths).to all(include(new_project.path_with_namespace))
+ end
+
+ it 'does not remove old files' do
+ expect(old_files).to all(exist)
+ end
+
+ it 'generates a new secret for each file' do
+ expect(new_paths).to_not include image_uploader.secret
+ expect(new_paths).to_not include zip_uploader.secret
+ end
+ end
+
+ describe '#needs_rewrite?' do
+ subject { rewriter.needs_rewrite? }
+ it { is_expected.to eq true }
+ end
+
+ describe '#files' do
+ subject { rewriter.files }
+ it { is_expected.to be_an(Array) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb
index bcdba8d4c12..afb3e26f8fb 100644
--- a/spec/lib/gitlab/incoming_email_spec.rb
+++ b/spec/lib/gitlab/incoming_email_spec.rb
@@ -7,24 +7,8 @@ describe Gitlab::IncomingEmail, lib: true do
stub_incoming_email_setting(enabled: true)
end
- context "when the address is valid" do
- before do
- stub_incoming_email_setting(address: "replies+%{key}@example.com")
- end
-
- it "returns true" do
- expect(described_class.enabled?).to be_truthy
- end
- end
-
- context "when the address is invalid" do
- before do
- stub_incoming_email_setting(address: "replies@example.com")
- end
-
- it "returns false" do
- expect(described_class.enabled?).to be_falsey
- end
+ it 'returns true' do
+ expect(described_class.enabled?).to be_truthy
end
end
@@ -58,4 +42,10 @@ describe Gitlab::IncomingEmail, lib: true do
expect(described_class.key_from_address("replies+key@example.com")).to eq("key")
end
end
+
+ context 'self.key_from_fallback_reply_message_id' do
+ it 'returns reply key' do
+ expect(described_class.key_from_fallback_reply_message_id('reply-key@localhost')).to eq('key')
+ end
+ end
end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 09adbc07dcb..db0ff95b4f5 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -1,11 +1,12 @@
require 'spec_helper'
describe Gitlab::ProjectSearchResults, lib: true do
+ let(:user) { create(:user) }
let(:project) { create(:project) }
let(:query) { 'hello world' }
describe 'initialize with empty ref' do
- let(:results) { Gitlab::ProjectSearchResults.new(project, query, '') }
+ let(:results) { Gitlab::ProjectSearchResults.new(user, project, query, '') }
it { expect(results.project).to eq(project) }
it { expect(results.repository_ref).to be_nil }
@@ -14,10 +15,74 @@ describe Gitlab::ProjectSearchResults, lib: true do
describe 'initialize with ref' do
let(:ref) { 'refs/heads/test' }
- let(:results) { Gitlab::ProjectSearchResults.new(project, query, ref) }
+ let(:results) { Gitlab::ProjectSearchResults.new(user, project, query, ref) }
it { expect(results.project).to eq(project) }
it { expect(results.repository_ref).to eq(ref) }
it { expect(results.query).to eq('hello world') }
end
+
+ describe 'confidential issues' do
+ let(:query) { 'issue' }
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:admin) { create(:admin) }
+ let!(:issue) { create(:issue, project: project, title: 'Issue 1') }
+ let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) }
+ let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) }
+
+ it 'should not list project confidential issues for non project members' do
+ results = described_class.new(non_member, project, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).not_to include security_issue_1
+ expect(issues).not_to include security_issue_2
+ expect(results.issues_count).to eq 1
+ end
+
+ it 'should list project confidential issues for author' do
+ results = described_class.new(author, project, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).to include security_issue_1
+ expect(issues).not_to include security_issue_2
+ expect(results.issues_count).to eq 2
+ end
+
+ it 'should list project confidential issues for assignee' do
+ results = described_class.new(assignee, project.id, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).not_to include security_issue_1
+ expect(issues).to include security_issue_2
+ expect(results.issues_count).to eq 2
+ end
+
+ it 'should list project confidential issues for project members' do
+ project.team << [member, :developer]
+
+ results = described_class.new(member, project, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).to include security_issue_1
+ expect(issues).to include security_issue_2
+ expect(results.issues_count).to eq 3
+ end
+
+ it 'should list all project issues for admin' do
+ results = described_class.new(admin, project, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).to include security_issue_1
+ expect(issues).to include security_issue_2
+ expect(results.issues_count).to eq 3
+ end
+ end
end
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index 7d963795e17..7c617723e6d 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe Gitlab::ReferenceExtractor, lib: true do
let(:project) { create(:project) }
+
subject { Gitlab::ReferenceExtractor.new(project, project.creator) }
it 'accesses valid user objects' do
@@ -41,6 +42,7 @@ describe Gitlab::ReferenceExtractor, lib: true do
end
it 'accesses valid issue objects' do
+ project.team << [project.creator, :developer]
@i0 = create(:issue, project: project)
@i1 = create(:issue, project: project)
@@ -122,4 +124,24 @@ describe Gitlab::ReferenceExtractor, lib: true do
expect(extracted).to match_array([issue])
end
end
+
+ describe '#all' do
+ let(:issue) { create(:issue, project: project) }
+ let(:label) { create(:label, project: project) }
+ let(:text) { "Ref. #{issue.to_reference} and #{label.to_reference}" }
+
+ before do
+ project.team << [project.creator, :developer]
+ subject.analyze(text)
+ end
+
+ it 'returns all referables' do
+ expect(subject.all).to match_array([issue, label])
+ end
+ end
+
+ describe '.references_pattern' do
+ subject { described_class.references_pattern }
+ it { is_expected.to be_kind_of Regexp }
+ end
end
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index bb18f417858..f4afe597e8d 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe Gitlab::SearchResults do
+ let(:user) { create(:user) }
let!(:project) { create(:project, name: 'foo') }
let!(:issue) { create(:issue, project: project, title: 'foo') }
@@ -9,7 +10,7 @@ describe Gitlab::SearchResults do
end
let!(:milestone) { create(:milestone, project: project, title: 'foo') }
- let(:results) { described_class.new(Project.all, 'foo') }
+ let(:results) { described_class.new(user, Project.all, 'foo') }
describe '#total_count' do
it 'returns the total amount of search hits' do
@@ -52,4 +53,92 @@ describe Gitlab::SearchResults do
expect(results.empty?).to eq(false)
end
end
+
+ describe 'confidential issues' do
+ let(:project_1) { create(:empty_project) }
+ let(:project_2) { create(:empty_project) }
+ let(:project_3) { create(:empty_project) }
+ let(:project_4) { create(:empty_project) }
+ let(:query) { 'issue' }
+ let(:limit_projects) { Project.where(id: [project_1.id, project_2.id, project_3.id]) }
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:admin) { create(:admin) }
+ let!(:issue) { create(:issue, project: project_1, title: 'Issue 1') }
+ let!(:security_issue_1) { create(:issue, :confidential, project: project_1, title: 'Security issue 1', author: author) }
+ let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project_1, assignee: assignee) }
+ let!(:security_issue_3) { create(:issue, :confidential, project: project_2, title: 'Security issue 3', author: author) }
+ let!(:security_issue_4) { create(:issue, :confidential, project: project_3, title: 'Security issue 4', assignee: assignee) }
+ let!(:security_issue_5) { create(:issue, :confidential, project: project_4, title: 'Security issue 5') }
+
+ it 'should not list confidential issues for non project members' do
+ results = described_class.new(non_member, limit_projects, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).not_to include security_issue_1
+ expect(issues).not_to include security_issue_2
+ expect(issues).not_to include security_issue_3
+ expect(issues).not_to include security_issue_4
+ expect(issues).not_to include security_issue_5
+ expect(results.issues_count).to eq 1
+ end
+
+ it 'should list confidential issues for author' do
+ results = described_class.new(author, limit_projects, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).to include security_issue_1
+ expect(issues).not_to include security_issue_2
+ expect(issues).to include security_issue_3
+ expect(issues).not_to include security_issue_4
+ expect(issues).not_to include security_issue_5
+ expect(results.issues_count).to eq 3
+ end
+
+ it 'should list confidential issues for assignee' do
+ results = described_class.new(assignee, limit_projects, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).not_to include security_issue_1
+ expect(issues).to include security_issue_2
+ expect(issues).not_to include security_issue_3
+ expect(issues).to include security_issue_4
+ expect(issues).not_to include security_issue_5
+ expect(results.issues_count).to eq 3
+ end
+
+ it 'should list confidential issues for project members' do
+ project_1.team << [member, :developer]
+ project_2.team << [member, :developer]
+
+ results = described_class.new(member, limit_projects, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).to include security_issue_1
+ expect(issues).to include security_issue_2
+ expect(issues).to include security_issue_3
+ expect(issues).not_to include security_issue_4
+ expect(issues).not_to include security_issue_5
+ expect(results.issues_count).to eq 4
+ end
+
+ it 'should list all issues for admin' do
+ results = described_class.new(admin, limit_projects, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).to include security_issue_1
+ expect(issues).to include security_issue_2
+ expect(issues).to include security_issue_3
+ expect(issues).to include security_issue_4
+ expect(issues).not_to include security_issue_5
+ expect(results.issues_count).to eq 5
+ end
+ end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index f910424d85b..631b5094f42 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -35,7 +35,9 @@ describe Notify do
subject { Notify.new_issue_email(issue.assignee_id, issue.id) }
it_behaves_like 'an assignee email'
- it_behaves_like 'an email starting a new thread', 'issue'
+ it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
+ let(:model) { issue }
+ end
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread'
@@ -73,9 +75,11 @@ describe Notify do
subject { Notify.reassigned_issue_email(recipient.id, issue.id, previous_assignee.id, current_user.id) }
it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread', 'issue'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { issue }
+ end
it_behaves_like 'it should show Gmail Actions View Issue link'
- it_behaves_like "an unsubscribeable thread"
+ it_behaves_like 'an unsubscribeable thread'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -104,7 +108,9 @@ describe Notify do
subject { Notify.relabeled_issue_email(recipient.id, issue.id, %w[foo bar baz], current_user.id) }
it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread', 'issue'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { issue }
+ end
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with a labels subscriptions link in its footer'
@@ -132,7 +138,9 @@ describe Notify do
let(:status) { 'closed' }
subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) }
- it_behaves_like 'an answer to an existing thread', 'issue'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { issue }
+ end
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread'
@@ -158,6 +166,35 @@ describe Notify do
is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/
end
end
+
+ describe 'moved to another project' do
+ let(:new_issue) { create(:issue) }
+ subject { Notify.issue_moved_email(recipient, issue, new_issue, current_user) }
+
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { issue }
+ end
+ it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'an unsubscribeable thread'
+
+ it 'contains description about action taken' do
+ is_expected.to have_body_text 'Issue was moved to another project'
+ end
+
+ it 'has the correct subject' do
+ is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/i
+ end
+
+ it 'contains link to new issue' do
+ new_issue_url = namespace_project_issue_path(new_issue.project.namespace,
+ new_issue.project, new_issue)
+ is_expected.to have_body_text new_issue_url
+ end
+
+ it 'contains a link to the original issue' do
+ is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/
+ end
+ end
end
context 'for merge requests' do
@@ -169,9 +206,11 @@ describe Notify do
subject { Notify.new_merge_request_email(merge_request.assignee_id, merge_request.id) }
it_behaves_like 'an assignee email'
- it_behaves_like 'an email starting a new thread', 'merge_request'
+ it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
+ let(:model) { merge_request }
+ end
it_behaves_like 'it should show Gmail Actions View Merge request link'
- it_behaves_like "an unsubscribeable thread"
+ it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do
is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
@@ -189,10 +228,6 @@ describe Notify do
is_expected.to have_body_text /#{merge_request.target_branch}/
end
- it 'has the correct message-id set' do
- is_expected.to have_header 'Message-ID', "<merge_request_#{merge_request.id}@#{Gitlab.config.gitlab.host}>"
- end
-
context 'when enabled email_author_in_body' do
before do
allow(current_application_settings).to receive(:email_author_in_body).and_return(true)
@@ -220,7 +255,9 @@ describe Notify do
subject { Notify.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) }
it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread', 'merge_request'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { merge_request }
+ end
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like "an unsubscribeable thread"
@@ -251,7 +288,9 @@ describe Notify do
subject { Notify.relabeled_merge_request_email(recipient.id, merge_request.id, %w[foo bar baz], current_user.id) }
it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread', 'merge_request'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { merge_request }
+ end
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with a labels subscriptions link in its footer'
@@ -279,9 +318,11 @@ describe Notify do
let(:status) { 'reopened' }
subject { Notify.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) }
- it_behaves_like 'an answer to an existing thread', 'merge_request'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { merge_request }
+ end
it_behaves_like 'it should show Gmail Actions View Merge request link'
- it_behaves_like "an unsubscribeable thread"
+ it_behaves_like 'an unsubscribeable thread'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -310,9 +351,11 @@ describe Notify do
subject { Notify.merged_merge_request_email(recipient.id, merge_request.id, merge_author.id) }
it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread', 'merge_request'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { merge_request }
+ end
it_behaves_like 'it should show Gmail Actions View Merge request link'
- it_behaves_like "an unsubscribeable thread"
+ it_behaves_like 'an unsubscribeable thread'
it 'is sent as the merge author' do
sender = subject.header[:from].addrs[0]
@@ -429,9 +472,11 @@ describe Notify do
subject { Notify.note_commit_email(recipient.id, note.id) }
it_behaves_like 'a note email'
- it_behaves_like 'an answer to an existing thread', 'commit'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { commit }
+ end
it_behaves_like 'it should show Gmail Actions View Commit link'
- it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'a user cannot unsubscribe through footer link'
it 'has the correct subject' do
is_expected.to have_subject /#{commit.title} \(#{commit.short_id}\)/
@@ -450,7 +495,9 @@ describe Notify do
subject { Notify.note_merge_request_email(recipient.id, note.id) }
it_behaves_like 'a note email'
- it_behaves_like 'an answer to an existing thread', 'merge_request'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { merge_request }
+ end
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread'
@@ -471,7 +518,9 @@ describe Notify do
subject { Notify.note_issue_email(recipient.id, note.id) }
it_behaves_like 'a note email'
- it_behaves_like 'an answer to an existing thread', 'issue'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { issue }
+ end
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread'
diff --git a/spec/mailers/shared/notify.rb b/spec/mailers/shared/notify.rb
index 6019af544d3..56a6dbf96f9 100644
--- a/spec/mailers/shared/notify.rb
+++ b/spec/mailers/shared/notify.rb
@@ -10,6 +10,13 @@ shared_context 'gitlab email notification' do
ActionMailer::Base.deliveries.clear
email = recipient.emails.create(email: "notifications@example.com")
recipient.update_attribute(:notification_email, email.email)
+ stub_incoming_email_setting(enabled: true, address: "reply+%{key}@#{Gitlab.config.gitlab.host}")
+ end
+end
+
+shared_context 'reply-by-email is enabled with incoming address without %{key}' do
+ before do
+ stub_incoming_email_setting(enabled: true, address: "reply@#{Gitlab.config.gitlab.host}")
end
end
@@ -46,25 +53,76 @@ shared_examples 'an email with X-GitLab headers containing project details' do
end
end
-shared_examples 'an email starting a new thread' do |message_id_prefix|
- include_examples 'an email with X-GitLab headers containing project details'
+shared_examples 'a new thread email with reply-by-email enabled' do
+ let(:regex) { /\A<reply\-(.*)@#{Gitlab.config.gitlab.host}>\Z/ }
+
+ it 'has a Message-ID header' do
+ is_expected.to have_header 'Message-ID', "<#{model.class.model_name.singular_route_key}_#{model.id}@#{Gitlab.config.gitlab.host}>"
+ end
- it 'has a discussion identifier' do
- is_expected.to have_header 'Message-ID', /<#{message_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/
+ it 'has a References header' do
+ is_expected.to have_header 'References', regex
end
end
-shared_examples 'an answer to an existing thread' do |thread_id_prefix|
+shared_examples 'a thread answer email with reply-by-email enabled' do
include_examples 'an email with X-GitLab headers containing project details'
+ let(:regex) { /\A<#{model.class.model_name.singular_route_key}_#{model.id}@#{Gitlab.config.gitlab.host}> <reply\-(.*)@#{Gitlab.config.gitlab.host}>\Z/ }
+
+ it 'has a Message-ID header' do
+ is_expected.to have_header 'Message-ID', /\A<(.*)@#{Gitlab.config.gitlab.host}>\Z/
+ end
+
+ it 'has a In-Reply-To header' do
+ is_expected.to have_header 'In-Reply-To', "<#{model.class.model_name.singular_route_key}_#{model.id}@#{Gitlab.config.gitlab.host}>"
+ end
+
+ it 'has a References header' do
+ is_expected.to have_header 'References', regex
+ end
it 'has a subject that begins with Re: ' do
is_expected.to have_subject /^Re: /
end
+end
+
+shared_examples 'an email starting a new thread with reply-by-email enabled' do
+ include_examples 'an email with X-GitLab headers containing project details'
+ include_examples 'a new thread email with reply-by-email enabled'
+
+ context 'when reply-by-email is enabled with incoming address with %{key}' do
+ it 'has a Reply-To header' do
+ is_expected.to have_header 'Reply-To', /<reply+(.*)@#{Gitlab.config.gitlab.host}>\Z/
+ end
+ end
+
+ context 'when reply-by-email is enabled with incoming address without %{key}' do
+ include_context 'reply-by-email is enabled with incoming address without %{key}'
+ include_examples 'a new thread email with reply-by-email enabled'
+
+ it 'has a Reply-To header' do
+ is_expected.to have_header 'Reply-To', /<reply@#{Gitlab.config.gitlab.host}>\Z/
+ end
+ end
+end
+
+shared_examples 'an answer to an existing thread with reply-by-email enabled' do
+ include_examples 'an email with X-GitLab headers containing project details'
+ include_examples 'a thread answer email with reply-by-email enabled'
+
+ context 'when reply-by-email is enabled with incoming address with %{key}' do
+ it 'has a Reply-To header' do
+ is_expected.to have_header 'Reply-To', /<reply+(.*)@#{Gitlab.config.gitlab.host}>\Z/
+ end
+ end
+
+ context 'when reply-by-email is enabled with incoming address without %{key}' do
+ include_context 'reply-by-email is enabled with incoming address without %{key}'
+ include_examples 'a thread answer email with reply-by-email enabled'
- it 'has headers that reference an existing thread' do
- is_expected.to have_header 'Message-ID', /<(.*)@#{Gitlab.config.gitlab.host}>/
- is_expected.to have_header 'References', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/
- is_expected.to have_header 'In-Reply-To', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/
+ it 'has a Reply-To header' do
+ is_expected.to have_header 'Reply-To', /<reply@#{Gitlab.config.gitlab.host}>\Z/
+ end
end
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index b1764d7ac09..520cf1b75de 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -12,7 +12,6 @@
# updated_at :datetime
# home_page_url :string(255)
# default_branch_protection :integer default(2)
-# twitter_sharing_enabled :boolean default(TRUE)
# restricted_visibility_levels :text
# version_check_enabled :boolean default(TRUE)
# max_attachment_size :integer default(10), not null
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 253902512c3..0e9111c8029 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -86,10 +86,21 @@ eos
let(:issue) { create :issue, project: project }
let(:other_project) { create :project, :public }
let(:other_issue) { create :issue, project: other_project }
+ let(:commiter) { create :user }
+
+ before do
+ project.team << [commiter, :developer]
+ other_project.team << [commiter, :developer]
+ end
it 'detects issues that this commit is marked as closing' do
ext_ref = "#{other_project.path_with_namespace}##{other_issue.iid}"
- allow(commit).to receive(:safe_message).and_return("Fixes ##{issue.iid} and #{ext_ref}")
+
+ allow(commit).to receive_messages(
+ safe_message: "Fixes ##{issue.iid} and #{ext_ref}",
+ committer_email: commiter.email
+ )
+
expect(commit.closes_issues).to include(issue)
expect(commit.closes_issues).to include(other_issue)
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index be29b6d66ff..b16ccc6e305 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -9,6 +9,7 @@ describe Issue, "Issuable" do
it { is_expected.to belong_to(:author) }
it { is_expected.to belong_to(:assignee) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
+ it { is_expected.to have_many(:todos).dependent(:destroy) }
end
describe "Validation" do
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index 20f0c561e44..cb33edde820 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -48,7 +48,8 @@ describe Issue, "Mentionable" do
describe '#create_new_cross_references!' do
let(:project) { create(:project) }
- let(:issues) { create_list(:issue, 2, project: project) }
+ let(:author) { create(:author) }
+ let(:issues) { create_list(:issue, 2, project: project, author: author) }
context 'before changes are persisted' do
it 'ignores pre-existing references' do
@@ -91,7 +92,7 @@ describe Issue, "Mentionable" do
end
def create_issue(description:)
- create(:issue, project: project, description: description)
+ create(:issue, project: project, description: description, author: author)
end
end
end
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
new file mode 100644
index 00000000000..47c3be673c5
--- /dev/null
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -0,0 +1,104 @@
+require 'spec_helper'
+
+describe Milestone, 'Milestoneish' do
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:project) { create(:project, :public) }
+ let(:milestone) { create(:milestone, project: project) }
+ let!(:issue) { create(:issue, project: project, milestone: milestone) }
+ let!(:security_issue_1) { create(:issue, :confidential, project: project, author: author, milestone: milestone) }
+ let!(:security_issue_2) { create(:issue, :confidential, project: project, assignee: assignee, milestone: milestone) }
+ let!(:closed_issue_1) { create(:issue, :closed, project: project, milestone: milestone) }
+ let!(:closed_issue_2) { create(:issue, :closed, project: project, milestone: milestone) }
+ let!(:closed_security_issue_1) { create(:issue, :confidential, :closed, project: project, author: author, milestone: milestone) }
+ let!(:closed_security_issue_2) { create(:issue, :confidential, :closed, project: project, assignee: assignee, milestone: milestone) }
+ let!(:closed_security_issue_3) { create(:issue, :confidential, :closed, project: project, author: author, milestone: milestone) }
+ let!(:closed_security_issue_4) { create(:issue, :confidential, :closed, project: project, assignee: assignee, milestone: milestone) }
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) }
+
+ before do
+ project.team << [member, :developer]
+ end
+
+ describe '#closed_items_count' do
+ it 'should not count confidential issues for non project members' do
+ expect(milestone.closed_items_count(non_member)).to eq 2
+ end
+
+ it 'should count confidential issues for author' do
+ expect(milestone.closed_items_count(author)).to eq 4
+ end
+
+ it 'should count confidential issues for assignee' do
+ expect(milestone.closed_items_count(assignee)).to eq 4
+ end
+
+ it 'should count confidential issues for project members' do
+ expect(milestone.closed_items_count(member)).to eq 6
+ end
+
+ it 'should count all issues for admin' do
+ expect(milestone.closed_items_count(admin)).to eq 6
+ end
+ end
+
+ describe '#total_items_count' do
+ it 'should not count confidential issues for non project members' do
+ expect(milestone.total_items_count(non_member)).to eq 4
+ end
+
+ it 'should count confidential issues for author' do
+ expect(milestone.total_items_count(author)).to eq 7
+ end
+
+ it 'should count confidential issues for assignee' do
+ expect(milestone.total_items_count(assignee)).to eq 7
+ end
+
+ it 'should count confidential issues for project members' do
+ expect(milestone.total_items_count(member)).to eq 10
+ end
+
+ it 'should count all issues for admin' do
+ expect(milestone.total_items_count(admin)).to eq 10
+ end
+ end
+
+ describe '#complete?' do
+ it 'returns false when has items opened' do
+ expect(milestone.complete?(non_member)).to eq false
+ end
+
+ it 'returns true when all items are closed' do
+ issue.close
+ merge_request.close
+
+ expect(milestone.complete?(non_member)).to eq true
+ end
+ end
+
+ describe '#percent_complete' do
+ it 'should not count confidential issues for non project members' do
+ expect(milestone.percent_complete(non_member)).to eq 50
+ end
+
+ it 'should count confidential issues for author' do
+ expect(milestone.percent_complete(author)).to eq 57
+ end
+
+ it 'should count confidential issues for assignee' do
+ expect(milestone.percent_complete(assignee)).to eq 57
+ end
+
+ it 'should count confidential issues for project members' do
+ expect(milestone.percent_complete(member)).to eq 60
+ end
+
+ it 'should count confidential issues for admin' do
+ expect(milestone.percent_complete(admin)).to eq 60
+ end
+ end
+end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index ec2a923f91b..89909c2bcd7 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -59,12 +59,74 @@ describe Event, models: true do
end
it { expect(@event.push?).to be_truthy }
- it { expect(@event.proper?).to be_truthy }
+ it { expect(@event.visible_to_user?).to be_truthy }
it { expect(@event.tag?).to be_falsey }
it { expect(@event.branch_name).to eq("master") }
it { expect(@event.author).to eq(@user) }
end
+ describe '#visible_to_user?' do
+ let(:project) { create(:empty_project, :public) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:author) { create(:author) }
+ let(:assignee) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:issue) { create(:issue, project: project, author: author, assignee: assignee) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
+ let(:note_on_issue) { create(:note_on_issue, noteable: issue, project: project) }
+ let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project) }
+ let(:event) { Event.new(project: project, target: target, author_id: author.id) }
+
+ before do
+ project.team << [member, :developer]
+ end
+
+ context 'issue event' do
+ context 'for non confidential issues' do
+ let(:target) { issue }
+
+ it { expect(event.visible_to_user?(non_member)).to eq true }
+ it { expect(event.visible_to_user?(author)).to eq true }
+ it { expect(event.visible_to_user?(assignee)).to eq true }
+ it { expect(event.visible_to_user?(member)).to eq true }
+ it { expect(event.visible_to_user?(admin)).to eq true }
+ end
+
+ context 'for confidential issues' do
+ let(:target) { confidential_issue }
+
+ it { expect(event.visible_to_user?(non_member)).to eq false }
+ it { expect(event.visible_to_user?(author)).to eq true }
+ it { expect(event.visible_to_user?(assignee)).to eq true }
+ it { expect(event.visible_to_user?(member)).to eq true }
+ it { expect(event.visible_to_user?(admin)).to eq true }
+ end
+ end
+
+ context 'note event' do
+ context 'on non confidential issues' do
+ let(:target) { note_on_issue }
+
+ it { expect(event.visible_to_user?(non_member)).to eq true }
+ it { expect(event.visible_to_user?(author)).to eq true }
+ it { expect(event.visible_to_user?(assignee)).to eq true }
+ it { expect(event.visible_to_user?(member)).to eq true }
+ it { expect(event.visible_to_user?(admin)).to eq true }
+ end
+
+ context 'on confidential issues' do
+ let(:target) { note_on_confidential_issue }
+
+ it { expect(event.visible_to_user?(non_member)).to eq false }
+ it { expect(event.visible_to_user?(author)).to eq true }
+ it { expect(event.visible_to_user?(assignee)).to eq true }
+ it { expect(event.visible_to_user?(member)).to eq true }
+ it { expect(event.visible_to_user?(admin)).to eq true }
+ end
+ end
+ end
+
describe '.limit_recent' do
let!(:event1) { create(:closed_issue_event) }
let!(:event2) { create(:closed_issue_event) }
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index c9245fc9535..7bfca1e72c3 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -56,6 +56,23 @@ describe Group, models: true do
end
end
+ describe 'scopes' do
+ let!(:private_group) { create(:group, :private) }
+ let!(:internal_group) { create(:group, :internal) }
+
+ describe 'public_only' do
+ subject { described_class.public_only.to_a }
+
+ it{ is_expected.to eq([group]) }
+ end
+
+ describe 'public_and_internal_only' do
+ subject { described_class.public_and_internal_only.to_a }
+
+ it{ is_expected.to match_array([group, internal_group]) }
+ end
+ end
+
describe '#to_reference' do
it 'returns a String reference to the object' do
expect(group.to_reference).to eq "@#{group.name}"
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index fd1513cab1b..56a9fbe9720 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -20,24 +20,27 @@ require "spec_helper"
describe SystemHook, models: true do
describe "execute" do
- before(:each) do
- @system_hook = create(:system_hook)
- WebMock.stub_request(:post, @system_hook.url)
+ let(:system_hook) { create(:system_hook) }
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+ let(:group) { create(:group) }
+
+ before do
+ WebMock.stub_request(:post, system_hook.url)
end
it "project_create hook" do
- Projects::CreateService.new(create(:user), name: 'empty').execute
- expect(WebMock).to have_requested(:post, @system_hook.url).with(
+ Projects::CreateService.new(user, name: 'empty').execute
+ expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /project_create/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once
end
it "project_destroy hook" do
- user = create(:user)
- project = create(:empty_project, namespace: user.namespace)
Projects::DestroyService.new(project, user, {}).pending_delete!
- expect(WebMock).to have_requested(:post, @system_hook.url).with(
+
+ expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /project_destroy/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once
@@ -45,37 +48,36 @@ describe SystemHook, models: true do
it "user_create hook" do
create(:user)
- expect(WebMock).to have_requested(:post, @system_hook.url).with(
+
+ expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_create/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once
end
it "user_destroy hook" do
- user = create(:user)
user.destroy
- expect(WebMock).to have_requested(:post, @system_hook.url).with(
+
+ expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_destroy/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once
end
it "project_create hook" do
- user = create(:user)
- project = create(:project)
project.team << [user, :master]
- expect(WebMock).to have_requested(:post, @system_hook.url).with(
+
+ expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_add_to_team/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once
end
it "project_destroy hook" do
- user = create(:user)
- project = create(:project)
project.team << [user, :master]
project.project_members.destroy_all
- expect(WebMock).to have_requested(:post, @system_hook.url).with(
+
+ expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_remove_from_team/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once
@@ -83,41 +85,39 @@ describe SystemHook, models: true do
it 'group create hook' do
create(:group)
- expect(WebMock).to have_requested(:post, @system_hook.url).with(
+
+ expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /group_create/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once
end
it 'group destroy hook' do
- group = create(:group)
group.destroy
- expect(WebMock).to have_requested(:post, @system_hook.url).with(
+
+ expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /group_destroy/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once
end
it 'group member create hook' do
- group = create(:group)
- user = create(:user)
group.add_master(user)
- expect(WebMock).to have_requested(:post, @system_hook.url).with(
+
+ expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_add_to_group/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once
end
it 'group member destroy hook' do
- group = create(:group)
- user = create(:user)
group.add_master(user)
group.group_members.destroy_all
- expect(WebMock).to have_requested(:post, @system_hook.url).with(
+
+ expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_remove_from_group/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once
end
-
end
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 2ccdec1eeff..15052aaca28 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -37,6 +37,11 @@ describe Issue, models: true do
subject { create(:issue) }
+ describe "act_as_paranoid" do
+ it { is_expected.to have_db_column(:deleted_at) }
+ it { is_expected.to have_db_index(:deleted_at) }
+ end
+
describe '#to_reference' do
it 'returns a String reference to the object' do
expect(subject.to_reference).to eq "##{subject.iid}"
@@ -130,12 +135,67 @@ describe Issue, models: true do
end
end
+ describe '#can_move?' do
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue) }
+ subject { issue.can_move?(user) }
+
+ context 'user is not a member of project issue belongs to' do
+ it { is_expected.to eq false}
+ end
+
+ context 'user is reporter in project issue belongs to' do
+ let(:project) { create(:project) }
+ let(:issue) { create(:issue, project: project) }
+
+ before { project.team << [user, :reporter] }
+
+ it { is_expected.to eq true }
+
+ context 'issue not persisted' do
+ let(:issue) { build(:issue, project: project) }
+ it { is_expected.to eq false }
+ end
+
+ context 'checking destination project also' do
+ subject { issue.can_move?(user, to_project) }
+ let(:to_project) { create(:project) }
+
+ context 'destination project allowed' do
+ before { to_project.team << [user, :reporter] }
+ it { is_expected.to eq true }
+ end
+
+ context 'destination project not allowed' do
+ before { to_project.team << [user, :guest] }
+ it { is_expected.to eq false }
+ end
+ end
+ end
+ end
+
+ describe '#moved?' do
+ let(:issue) { create(:issue) }
+ subject { issue.moved? }
+
+ context 'issue not moved' do
+ it { is_expected.to eq false }
+ end
+
+ context 'issue already moved' do
+ let(:moved_to_issue) { create(:issue) }
+ let(:issue) { create(:issue, moved_to: moved_to_issue) }
+
+ it { is_expected.to eq true }
+ end
+ end
+
describe '#related_branches' do
- it "should " do
+ it "selects the right branches" do
allow(subject.project.repository).to receive(:branch_names).
- and_return(["mpempe", "#{subject.iid}mepmep", subject.to_branch_name])
+ and_return(["mpempe", "#{subject.iid}mepmep", subject.to_branch_name])
- expect(subject.related_branches).to eq [subject.to_branch_name]
+ expect(subject.related_branches).to eq([subject.to_branch_name])
end
end
@@ -151,10 +211,10 @@ describe Issue, models: true do
end
describe "#to_branch_name" do
- let(:issue) { build(:issue, title: 'a' * 30) }
+ let(:issue) { create(:issue, title: 'a' * 30) }
it "starts with the issue iid" do
- expect(issue.to_branch_name).to match /\A#{issue.iid}-a+\z/
+ expect(issue.to_branch_name).to match /-#{issue.iid}\z/
end
end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 8bf68013fd2..6f5d912fe5d 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -49,6 +49,11 @@ describe MergeRequest, models: true do
it { is_expected.to include_module(Taskable) }
end
+ describe "act_as_paranoid" do
+ it { is_expected.to have_db_column(:deleted_at) }
+ it { is_expected.to have_db_index(:deleted_at) }
+ end
+
describe 'validation' do
it { is_expected.to validate_presence_of(:target_branch) }
it { is_expected.to validate_presence_of(:source_branch) }
@@ -86,6 +91,41 @@ describe MergeRequest, models: true do
end
end
+ describe '#target_sha' do
+ context 'when the target branch does not exist anymore' do
+ subject { create(:merge_request).tap { |mr| mr.update_attribute(:target_branch, 'deleted') } }
+
+ it 'returns nil' do
+ expect(subject.target_sha).to be_nil
+ end
+ end
+ end
+
+ describe '#source_sha' do
+ let(:last_branch_commit) { subject.source_project.repository.commit(subject.source_branch) }
+
+ context 'with diffs' do
+ subject { create(:merge_request, :with_diffs) }
+ it 'returns the sha of the source branch last commit' do
+ expect(subject.source_sha).to eq(last_branch_commit.sha)
+ end
+ end
+
+ context 'without diffs' do
+ subject { create(:merge_request, :without_diffs) }
+ it 'returns the sha of the source branch last commit' do
+ expect(subject.source_sha).to eq(last_branch_commit.sha)
+ end
+ end
+
+ context 'when the merge request is being created' do
+ subject { build(:merge_request, source_branch: nil, compare_commits: []) }
+ it 'returns nil' do
+ expect(subject.source_sha).to be_nil
+ end
+ end
+ end
+
describe '#to_reference' do
it 'returns a String reference to the object' do
expect(subject.to_reference).to eq "!#{subject.iid}"
@@ -150,6 +190,7 @@ describe MergeRequest, models: true do
let(:commit2) { double('commit2', safe_message: "Fixes #{issue1.to_reference}") }
before do
+ subject.project.team << [subject.author, :developer]
allow(subject).to receive(:commits).and_return([commit0, commit1, commit2])
end
@@ -180,33 +221,25 @@ describe MergeRequest, models: true do
end
describe "#work_in_progress?" do
- it "detects the 'WIP ' prefix" do
- subject.title = "WIP #{subject.title}"
- expect(subject).to be_work_in_progress
- end
-
- it "detects the 'WIP: ' prefix" do
- subject.title = "WIP: #{subject.title}"
- expect(subject).to be_work_in_progress
- end
-
- it "detects the '[WIP] ' prefix" do
- subject.title = "[WIP] #{subject.title}"
- expect(subject).to be_work_in_progress
- end
-
- it "detects the '[WIP]' prefix" do
- subject.title = "[WIP]#{subject.title}"
- expect(subject).to be_work_in_progress
+ ['WIP ', 'WIP:', 'WIP: ', '[WIP]', '[WIP] ', ' [WIP] WIP [WIP] WIP: WIP '].each do |wip_prefix|
+ it "detects the '#{wip_prefix}' prefix" do
+ subject.title = "#{wip_prefix}#{subject.title}"
+ expect(subject.work_in_progress?).to eq true
+ end
end
it "doesn't detect WIP for words starting with WIP" do
subject.title = "Wipwap #{subject.title}"
- expect(subject).not_to be_work_in_progress
+ expect(subject.work_in_progress?).to eq false
+ end
+
+ it "doesn't detect WIP for words containing with WIP" do
+ subject.title = "WupWipwap #{subject.title}"
+ expect(subject.work_in_progress?).to eq false
end
it "doesn't detect WIP by default" do
- expect(subject).not_to be_work_in_progress
+ expect(subject.work_in_progress?).to eq false
end
end
@@ -284,6 +317,18 @@ describe MergeRequest, models: true do
let(:project) { create(:project) }
let(:fork_project) { create(:project, forked_from_project: project) }
+ context 'when the target branch does not exist anymore' do
+ subject { create(:merge_request).tap { |mr| mr.update_attribute(:target_branch, 'deleted') } }
+
+ it 'does not crash' do
+ expect{ subject.diverged_commits_count }.not_to raise_error
+ end
+
+ it 'returns 0' do
+ expect(subject.diverged_commits_count).to eq(0)
+ end
+ end
+
context 'diverged on same repository' do
subject(:merge_request_with_divergence) { create(:merge_request, :diverged, source_project: project, target_project: project) }
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index de1757bf67a..72a4ea70228 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -32,6 +32,7 @@ describe Milestone, models: true do
let(:milestone) { create(:milestone) }
let(:issue) { create(:issue) }
+ let(:user) { create(:user) }
describe "unique milestone title per project" do
it "shouldn't accept the same title in a project twice" do
@@ -50,18 +51,17 @@ describe Milestone, models: true do
describe "#percent_complete" do
it "should not count open issues" do
milestone.issues << issue
- expect(milestone.percent_complete).to eq(0)
+ expect(milestone.percent_complete(user)).to eq(0)
end
it "should count closed issues" do
issue.close
milestone.issues << issue
- expect(milestone.percent_complete).to eq(100)
+ expect(milestone.percent_complete(user)).to eq(100)
end
it "should recover from dividing by zero" do
- expect(milestone.issues).to receive(:size).and_return(0)
- expect(milestone.percent_complete).to eq(0)
+ expect(milestone.percent_complete(user)).to eq(0)
end
end
@@ -103,7 +103,7 @@ describe Milestone, models: true do
)
end
- it { expect(milestone.percent_complete).to eq(75) }
+ it { expect(milestone.percent_complete(user)).to eq(75) }
end
describe :items_count do
@@ -113,23 +113,23 @@ describe Milestone, models: true do
milestone.merge_requests << create(:merge_request)
end
- it { expect(milestone.closed_items_count).to eq(1) }
- it { expect(milestone.total_items_count).to eq(3) }
- it { expect(milestone.is_empty?).to be_falsey }
+ it { expect(milestone.closed_items_count(user)).to eq(1) }
+ it { expect(milestone.total_items_count(user)).to eq(3) }
+ it { expect(milestone.is_empty?(user)).to be_falsey }
end
describe :can_be_closed? do
it { expect(milestone.can_be_closed?).to be_truthy }
end
- describe :is_empty? do
+ describe :total_items_count do
before do
create :closed_issue, milestone: milestone
create :merge_request, milestone: milestone
end
it 'Should return total count of issues and merge requests assigned to milestone' do
- expect(milestone.total_items_count).to eq 2
+ expect(milestone.total_items_count(user)).to eq 2
end
end
diff --git a/spec/models/project_security_spec.rb b/spec/models/project_security_spec.rb
index 3643ad1b052..e12258c0874 100644
--- a/spec/models/project_security_spec.rb
+++ b/spec/models/project_security_spec.rb
@@ -18,11 +18,11 @@ describe Project, models: true do
let(:report_actions) { Ability.project_report_rules }
let(:dev_actions) { Ability.project_dev_rules }
let(:master_actions) { Ability.project_master_rules }
- let(:admin_actions) { Ability.project_admin_rules }
+ let(:owner_actions) { Ability.project_owner_rules }
describe "Non member rules" do
it "should deny for non-project users any actions" do
- admin_actions.each do |action|
+ owner_actions.each do |action|
expect(@abilities.allowed?(@u1, action, @p1)).to be_falsey
end
end
@@ -90,20 +90,20 @@ describe Project, models: true do
end
end
- describe "Admin Rules" do
+ describe "Owner Rules" do
before do
@p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::DEVELOPER)
@p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::MASTER)
end
it "should deny for masters admin-specific actions" do
- [admin_actions - master_actions].each do |action|
+ [owner_actions - master_actions].each do |action|
expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey
end
end
it "should allow for project owner any admin actions" do
- admin_actions.each do |action|
+ owner_actions.each do |action|
expect(@abilities.allowed?(@u4, action, @p1)).to be_truthy
end
end
diff --git a/spec/models/project_services/slack_service/issue_message_spec.rb b/spec/models/project_services/slack_service/issue_message_spec.rb
index 97e6f03e308..f648cbe2dee 100644
--- a/spec/models/project_services/slack_service/issue_message_spec.rb
+++ b/spec/models/project_services/slack_service/issue_message_spec.rb
@@ -27,6 +27,16 @@ describe SlackService::IssueMessage, models: true do
let(:color) { '#345' }
+ context '#initialize' do
+ before do
+ args[:object_attributes][:description] = nil
+ end
+
+ it 'returns a non-null description' do
+ expect(subject.description).to eq('')
+ end
+ end
+
context 'open' do
it 'returns a message regarding opening of issues' do
expect(subject.pretext).to eq(
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index b8b9a455b83..f29c389e094 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -104,6 +104,15 @@ describe Project, models: true do
end
end
+ describe 'default_scope' do
+ it 'excludes projects pending deletion from the results' do
+ project = create(:empty_project)
+ create(:empty_project, pending_delete: true)
+
+ expect(Project.all).to eq [project]
+ end
+ end
+
describe 'project token' do
it 'should set an random token if none provided' do
project = FactoryGirl.create :empty_project, runners_token: ''
@@ -422,6 +431,12 @@ describe Project, models: true do
it { should eq "http://localhost#{avatar_path}" }
end
+
+ context 'when git repo is empty' do
+ let(:project) { create(:empty_project) }
+
+ it { should eq nil }
+ end
end
describe :ci_commit do
@@ -442,7 +457,7 @@ describe Project, models: true do
end
describe '.trending' do
- let(:group) { create(:group) }
+ let(:group) { create(:group, :public) }
let(:project1) { create(:empty_project, :public, group: group) }
let(:project2) { create(:empty_project, :public, group: group) }
@@ -571,12 +586,8 @@ describe Project, models: true do
end
context 'when checking on forked project' do
- let(:forked_project) { create :forked_project_with_submodules }
-
- before do
- forked_project.build_forked_project_link(forked_to_project_id: forked_project.id, forked_from_project_id: project.id)
- forked_project.save
- end
+ let(:project) { create(:project, :internal) }
+ let(:forked_project) { create(:project, forked_from_project: project) }
it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::PRIVATE)).to be_truthy }
it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_truthy }
@@ -720,4 +731,61 @@ describe Project, models: true do
expect(described_class.search_by_title('KITTENS')).to eq([project])
end
end
+
+ context 'when checking projects from groups' do
+ let(:private_group) { create(:group, visibility_level: 0) }
+ let(:internal_group) { create(:group, visibility_level: 10) }
+
+ let(:private_project) { create :project, :private, group: private_group }
+ let(:internal_project) { create :project, :internal, group: internal_group }
+
+ context 'when group is private project can not be internal' do
+ it { expect(private_project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_falsey }
+ end
+
+ context 'when group is internal project can not be public' do
+ it { expect(internal_project.visibility_level_allowed?(Gitlab::VisibilityLevel::PUBLIC)).to be_falsey }
+ end
+ end
+
+ describe '#create_repository' do
+ let(:project) { create(:project) }
+ let(:shell) { Gitlab::Shell.new }
+
+ before do
+ allow(project).to receive(:gitlab_shell).and_return(shell)
+ end
+
+ context 'using a regular repository' do
+ it 'creates the repository' do
+ expect(shell).to receive(:add_repository).
+ with(project.path_with_namespace).
+ and_return(true)
+
+ expect(project.repository).to receive(:after_create)
+
+ expect(project.create_repository).to eq(true)
+ end
+
+ it 'adds an error if the repository could not be created' do
+ expect(shell).to receive(:add_repository).
+ with(project.path_with_namespace).
+ and_return(false)
+
+ expect(project.repository).not_to receive(:after_create)
+
+ expect(project.create_repository).to eq(false)
+ expect(project.errors).not_to be_empty
+ end
+ end
+
+ context 'using a forked repository' do
+ it 'does nothing' do
+ expect(project).to receive(:forked?).and_return(true)
+ expect(shell).not_to receive(:add_repository)
+
+ project.create_repository
+ end
+ end
+ end
end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index a2085df5bcd..532e3f013fd 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -244,6 +244,18 @@ describe ProjectWiki, models: true do
end
end
+ describe '#create_repo!' do
+ it 'creates a repository' do
+ expect(subject).to receive(:init_repo).
+ with(subject.path_with_namespace).
+ and_return(true)
+
+ expect(subject.repository).to receive(:after_create)
+
+ expect(subject.create_repo!).to be_an_instance_of(Gollum::Wiki)
+ end
+ end
+
private
def create_temp_repo(path)
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 536fe66b21b..9242a6f1739 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe Repository, models: true do
include RepoHelpers
+ TestBlob = Struct.new(:name)
let(:repository) { create(:project).repository }
let(:user) { create(:user) }
@@ -131,7 +132,6 @@ describe Repository, models: true do
describe "#license" do
before do
repository.send(:cache).expire(:license)
- TestBlob = Struct.new(:name)
end
it 'test selection preference' do
@@ -148,6 +148,25 @@ describe Repository, models: true do
end
end
+ describe "#gitlab_ci_yml" do
+ it 'returns valid file' do
+ files = [TestBlob.new('file'), TestBlob.new('.gitlab-ci.yml'), TestBlob.new('copying')]
+ expect(repository.tree).to receive(:blobs).and_return(files)
+
+ expect(repository.gitlab_ci_yml.name).to eq('.gitlab-ci.yml')
+ end
+
+ it 'returns nil if not exists' do
+ expect(repository.tree).to receive(:blobs).and_return([])
+ expect(repository.gitlab_ci_yml).to be_nil
+ end
+
+ it 'returns nil for empty repository' do
+ expect(repository).to receive(:empty?).and_return(true)
+ expect(repository.gitlab_ci_yml).to be_nil
+ end
+ end
+
describe :add_branch do
context 'when pre hooks were successful' do
it 'should run without errors' do
@@ -537,6 +556,12 @@ describe Repository, models: true do
repository.before_delete
end
+
+ it 'flushes the exists cache' do
+ expect(repository).to receive(:expire_exists_cache).twice
+
+ repository.before_delete
+ end
end
describe 'when a repository exists' do
@@ -593,6 +618,12 @@ describe Repository, models: true do
repository.after_import
end
+
+ it 'flushes the exists cache' do
+ expect(repository).to receive(:expire_exists_cache)
+
+ repository.after_import
+ end
end
describe '#after_push_commit' do
@@ -619,6 +650,14 @@ describe Repository, models: true do
end
end
+ describe '#after_create' do
+ it 'flushes the exists cache' do
+ expect(repository).to receive(:expire_exists_cache)
+
+ repository.after_create
+ end
+ end
+
describe "#main_language" do
it 'shows the main language of the project' do
expect(repository.main_language).to eq("Ruby")
@@ -705,6 +744,12 @@ describe Repository, models: true do
end
describe '#avatar' do
+ it 'returns nil if repo does not exist' do
+ expect(repository).to receive(:exists?).and_return(false)
+
+ expect(repository.avatar).to eq(nil)
+ end
+
it 'returns the first avatar file found in the repository' do
expect(repository).to receive(:blob_at_branch).
with('master', 'logo.png').
@@ -780,4 +825,44 @@ describe Repository, models: true do
end
end
end
+
+ describe '#expire_exists_cache' do
+ let(:cache) { repository.send(:cache) }
+
+ it 'expires the cache' do
+ expect(cache).to receive(:expire).with(:exists?)
+
+ repository.expire_exists_cache
+ end
+ end
+
+ describe '#build_cache' do
+ let(:cache) { repository.send(:cache) }
+
+ it 'builds the caches if they do not already exist' do
+ expect(cache).to receive(:exist?).
+ exactly(repository.cache_keys.length).
+ times.
+ and_return(false)
+
+ repository.cache_keys.each do |key|
+ expect(repository).to receive(key)
+ end
+
+ repository.build_cache
+ end
+
+ it 'does not build any caches that already exist' do
+ expect(cache).to receive(:exist?).
+ exactly(repository.cache_keys.length).
+ times.
+ and_return(true)
+
+ repository.cache_keys.each do |key|
+ expect(repository).to_not receive(key)
+ end
+
+ repository.build_cache
+ end
+ end
end
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index fe9ea7e7d1e..d9b86b9368f 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -5,19 +5,24 @@
# id :integer not null, primary key
# user_id :integer not null
# project_id :integer not null
-# target_id :integer not null
+# target_id :integer
# target_type :string not null
# author_id :integer
-# note_id :integer
# action :integer not null
# state :string not null
# created_at :datetime
# updated_at :datetime
+# note_id :integer
+# commit_id :string
#
require 'spec_helper'
describe Todo, models: true do
+ let(:project) { create(:project) }
+ let(:commit) { project.commit }
+ let(:issue) { create(:issue) }
+
describe 'relationships' do
it { is_expected.to belong_to(:author).class_name("User") }
it { is_expected.to belong_to(:note) }
@@ -33,8 +38,22 @@ describe Todo, models: true do
describe 'validations' do
it { is_expected.to validate_presence_of(:action) }
- it { is_expected.to validate_presence_of(:target) }
+ it { is_expected.to validate_presence_of(:target_type) }
it { is_expected.to validate_presence_of(:user) }
+
+ context 'for commits' do
+ subject { described_class.new(target_type: 'Commit') }
+
+ it { is_expected.to validate_presence_of(:commit_id) }
+ it { is_expected.not_to validate_presence_of(:target_id) }
+ end
+
+ context 'for issuables' do
+ subject { described_class.new(target: issue) }
+
+ it { is_expected.to validate_presence_of(:target_id) }
+ it { is_expected.not_to validate_presence_of(:commit_id) }
+ end
end
describe '#body' do
@@ -55,15 +74,69 @@ describe Todo, models: true do
end
end
- describe '#done!' do
+ describe '#done' do
it 'changes state to done' do
todo = create(:todo, state: :pending)
- expect { todo.done! }.to change(todo, :state).from('pending').to('done')
+ expect { todo.done }.to change(todo, :state).from('pending').to('done')
end
it 'does not raise error when is already done' do
todo = create(:todo, state: :done)
- expect { todo.done! }.not_to raise_error
+ expect { todo.done }.not_to raise_error
+ end
+ end
+
+ describe '#for_commit?' do
+ it 'returns true when target is a commit' do
+ subject.target_type = 'Commit'
+ expect(subject.for_commit?).to eq true
+ end
+
+ it 'returns false when target is an issuable' do
+ subject.target_type = 'Issue'
+ expect(subject.for_commit?).to eq false
+ end
+ end
+
+ describe '#target' do
+ context 'for commits' do
+ it 'returns an instance of Commit when exists' do
+ subject.project = project
+ subject.target_type = 'Commit'
+ subject.commit_id = commit.id
+
+ expect(subject.target).to be_a(Commit)
+ expect(subject.target).to eq commit
+ end
+
+ it 'returns nil when does not exists' do
+ subject.project = project
+ subject.target_type = 'Commit'
+ subject.commit_id = 'xxxx'
+
+ expect(subject.target).to be_nil
+ end
+ end
+
+ it 'returns the issuable for issuables' do
+ subject.target_id = issue.id
+ subject.target_type = issue.class.name
+ expect(subject.target).to eq issue
+ end
+ end
+
+ describe '#target_reference' do
+ it 'returns the short commit id for commits' do
+ subject.project = project
+ subject.target_type = 'Commit'
+ subject.commit_id = commit.id
+
+ expect(subject.target_reference).to eq commit.short_id
+ end
+
+ it 'returns reference for issuables' do
+ subject.target = issue
+ expect(subject.target_reference).to eq issue.to_reference
end
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 0ab7fd88ce6..8b2fb77e28e 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -173,6 +173,13 @@ describe User, models: true do
expect(user).to be_invalid
end
end
+
+ context 'owns_notification_email' do
+ it 'accepts temp_oauth_email emails' do
+ user = build(:user, email: "temp-email-for-oauth@example.com")
+ expect(user).to be_valid
+ end
+ end
end
end
diff --git a/spec/requests/api/group_members_spec.rb b/spec/requests/api/group_members_spec.rb
index dd5baa44cb2..3e8b4aa1f88 100644
--- a/spec/requests/api/group_members_spec.rb
+++ b/spec/requests/api/group_members_spec.rb
@@ -11,7 +11,7 @@ describe API::API, api: true do
let(:stranger) { create(:user) }
let!(:group_with_members) do
- group = create(:group)
+ group = create(:group, :private)
group.add_users([reporter.id], GroupMember::REPORTER)
group.add_users([developer.id], GroupMember::DEVELOPER)
group.add_users([master.id], GroupMember::MASTER)
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 4cfa49d1566..41c9cacd455 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -9,7 +9,7 @@ describe API::API, api: true do
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!(:group2) { create(:group) }
+ let!(:group2) { create(:group, :private) }
let!(:project1) { create(:project, namespace: group1) }
let!(:project2) { create(:project, namespace: group2) }
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 571ea2dae4c..822d3ad3017 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -2,8 +2,12 @@ require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
- let(:user) { create(:user) }
- let!(:project) { create(:project, namespace: user.namespace ) }
+ let(:user) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:author) { create(:author) }
+ let(:assignee) { create(:assignee) }
+ let(:admin) { create(:user, :admin) }
+ let!(:project) { create(:project, :public, namespace: user.namespace ) }
let!(:closed_issue) do
create :closed_issue,
author: user,
@@ -12,6 +16,13 @@ describe API::API, api: true do
state: :closed,
milestone: milestone
end
+ let!(:confidential_issue) do
+ create :issue,
+ :confidential,
+ project: project,
+ author: author,
+ assignee: assignee
+ end
let!(:issue) do
create :issue,
author: user,
@@ -123,10 +134,43 @@ describe API::API, api: true do
let(:base_url) { "/projects/#{project.id}" }
let(:title) { milestone.title }
- it "should return project issues" do
+ it 'should return project issues without confidential issues for non project members' do
+ get api("#{base_url}/issues", non_member)
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'should return project confidential issues for author' do
+ get api("#{base_url}/issues", author)
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'should return project confidential issues for assignee' do
+ get api("#{base_url}/issues", assignee)
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'should return project issues with confidential issues for project members' do
get api("#{base_url}/issues", user)
expect(response.status).to eq(200)
expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'should return project confidential issues for admin' do
+ get api("#{base_url}/issues", admin)
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
expect(json_response.first['title']).to eq(issue.title)
end
@@ -206,6 +250,41 @@ describe API::API, api: true do
get api("/projects/#{project.id}/issues/54321", user)
expect(response.status).to eq(404)
end
+
+ context 'confidential issues' do
+ it "should return 404 for non project members" do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member)
+ expect(response.status).to eq(404)
+ end
+
+ it "should return confidential issue for project members" do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.id}", user)
+ expect(response.status).to eq(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+
+ it "should return confidential issue for author" do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.id}", author)
+ expect(response.status).to eq(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+
+ it "should return confidential issue for assignee" do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.id}", assignee)
+ expect(response.status).to eq(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+
+ it "should return confidential issue for admin" do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin)
+ expect(response.status).to eq(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+ end
end
describe "POST /projects/:id/issues" do
@@ -239,6 +318,17 @@ describe API::API, api: true do
'is too long (maximum is 255 characters)'
])
end
+
+ context 'when an admin or owner makes the request' do
+ it "accepts the creation date to be set" do
+ post api("/projects/#{project.id}/issues", user),
+ title: 'new issue', labels: 'label, label2', created_at: 2.weeks.ago
+
+ expect(response.status).to eq(201)
+ # this take about a second, so probably not equal
+ expect(Time.parse(json_response['created_at'])).to be <= 2.weeks.ago
+ end
+ end
end
describe 'POST /projects/:id/issues with spam filtering' do
@@ -294,6 +384,35 @@ describe API::API, api: true do
expect(response.status).to eq(400)
expect(json_response['message']['labels']['?']['title']).to eq(['is invalid'])
end
+
+ context 'confidential issues' do
+ it "should return 403 for non project members" do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member),
+ title: 'updated title'
+ expect(response.status).to eq(403)
+ end
+
+ it "should update a confidential issue for project members" do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
+ title: 'updated title'
+ expect(response.status).to eq(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it "should update a confidential issue for author" do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.id}", author),
+ title: 'updated title'
+ expect(response.status).to eq(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it "should update a confidential issue for admin" do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin),
+ title: 'updated title'
+ expect(response.status).to eq(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+ end
end
describe 'PUT /projects/:id/issues/:issue_id to update labels' do
@@ -361,9 +480,25 @@ describe API::API, api: true do
end
describe "DELETE /projects/:id/issues/:issue_id" do
- it "should delete a project issue" do
- delete api("/projects/#{project.id}/issues/#{issue.id}", user)
- expect(response.status).to eq(405)
+ it "rejects a non member from deleting an issue" do
+ delete api("/projects/#{project.id}/issues/#{issue.id}", non_member)
+ expect(response.status).to be(403)
+ end
+
+ it "rejects a developer from deleting an issue" do
+ delete api("/projects/#{project.id}/issues/#{issue.id}", author)
+ expect(response.status).to be(403)
+ end
+
+ context "when the user is project owner" do
+ let(:owner) { create(:user) }
+ let(:project) { create(:project, namespace: owner.namespace) }
+
+ it "deletes the issue if an admin requests it" do
+ delete api("/projects/#{project.id}/issues/#{issue.id}", owner)
+ expect(response.status).to eq(200)
+ expect(json_response['state']).to eq 'opened'
+ end
end
end
end
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index 667f0dbea5c..6943ff9d26c 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -23,13 +23,25 @@ describe API::API, api: true do
end
describe 'POST /projects/:id/labels' do
- it 'should return created label' do
+ it 'should return created label when all params' do
+ post api("/projects/#{project.id}/labels", user),
+ name: 'Foo',
+ color: '#FFAABB',
+ description: 'test'
+ expect(response.status).to eq(201)
+ expect(json_response['name']).to eq('Foo')
+ expect(json_response['color']).to eq('#FFAABB')
+ expect(json_response['description']).to eq('test')
+ end
+
+ it 'should return created label when only required params' do
post api("/projects/#{project.id}/labels", user),
name: 'Foo',
color: '#FFAABB'
expect(response.status).to eq(201)
expect(json_response['name']).to eq('Foo')
expect(json_response['color']).to eq('#FFAABB')
+ expect(json_response['description']).to be_nil
end
it 'should return a 400 bad request if name not given' do
@@ -94,14 +106,16 @@ describe API::API, api: true do
end
describe 'PUT /projects/:id/labels' do
- it 'should return 200 if name and colors are changed' do
+ it 'should return 200 if name and colors and description are changed' do
put api("/projects/#{project.id}/labels", user),
name: 'label1',
new_name: 'New Label',
- color: '#FFFFFF'
+ color: '#FFFFFF',
+ description: 'test'
expect(response.status).to eq(200)
expect(json_response['name']).to eq('New Label')
expect(json_response['color']).to eq('#FFFFFF')
+ expect(json_response['description']).to eq('test')
end
it 'should return 200 if name is changed' do
@@ -122,6 +136,15 @@ describe API::API, api: true do
expect(json_response['color']).to eq('#FFFFFF')
end
+ it 'should return 200 if description is changed' do
+ put api("/projects/#{project.id}/labels", user),
+ name: 'label1',
+ description: 'test'
+ expect(response.status).to eq(200)
+ expect(json_response['name']).to eq(label1.name)
+ expect(json_response['description']).to eq('test')
+ end
+
it 'should return 404 if label does not exist' do
put api("/projects/#{project.id}/labels", user),
name: 'label2',
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 4fd1df25568..25fa30b2f21 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -2,15 +2,17 @@ require "spec_helper"
describe API::API, api: true do
include ApiHelpers
- let(:base_time) { Time.now }
- let(:user) { create(:user) }
- let!(:project) {create(:project, creator_id: user.id, namespace: user.namespace) }
+ let(:base_time) { Time.now }
+ let(:user) { create(:user) }
+ let(:admin) { create(:user, :admin) }
+ let(:non_member) { create(:user) }
+ let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) }
let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) }
let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds) }
- let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
- let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
- let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
+ let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
+ let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
+ let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
before do
project.team << [user, :reporters]
@@ -116,6 +118,7 @@ describe API::API, api: true do
expect(response.status).to eq(200)
expect(json_response['title']).to eq(merge_request.title)
expect(json_response['iid']).to eq(merge_request.iid)
+ expect(json_response['work_in_progress']).to eq(false)
expect(json_response['merge_status']).to eq('can_be_merged')
end
@@ -131,6 +134,16 @@ describe API::API, api: true do
get api("/projects/#{project.id}/merge_requests/999", user)
expect(response.status).to eq(404)
end
+
+ context 'Work in Progress' do
+ let!(:merge_request_wip) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) }
+
+ it "should return merge_request" do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.id}", user)
+ expect(response.status).to eq(200)
+ expect(json_response['work_in_progress']).to eq(true)
+ end
+ end
end
describe 'GET /projects/:id/merge_requests/:merge_request_id/commits' do
@@ -315,6 +328,29 @@ describe API::API, api: true do
end
end
+ describe "DELETE /projects/:id/merge_requests/:merge_request_id" do
+ context "when the user is developer" do
+ let(:developer) { create(:user) }
+
+ before do
+ project.team << [developer, :developer]
+ end
+
+ it "denies the deletion of the merge request" do
+ delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", developer)
+ expect(response.status).to be(403)
+ end
+ end
+
+ context "when the user is project owner" do
+ it "destroys the merge request owners can destroy" do
+ delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
+
+ expect(response.status).to eq(200)
+ end
+ end
+ end
+
describe "PUT /projects/:id/merge_requests/:merge_request_id to close MR" do
it "should return merge_request" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close"
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index a6699cdc81c..be2034e0f39 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -275,6 +275,7 @@ describe API::API, api: true do
it 'should not allow a non-admin to use a restricted visibility level' do
post api('/projects', user), @project
+
expect(response.status).to eq(400)
expect(json_response['message']['visibility_level'].first).to(
match('restricted by your GitLab administrator')
@@ -947,6 +948,78 @@ describe API::API, api: true do
end
end
+ describe 'POST /projects/:id/archive' do
+ context 'on an unarchived project' do
+ it 'archives the project' do
+ post api("/projects/#{project.id}/archive", user)
+
+ expect(response.status).to eq(201)
+ expect(json_response['archived']).to be_truthy
+ end
+ end
+
+ context 'on an archived project' do
+ before do
+ project.archive!
+ end
+
+ it 'remains archived' do
+ post api("/projects/#{project.id}/archive", user)
+
+ expect(response.status).to eq(201)
+ expect(json_response['archived']).to be_truthy
+ end
+ end
+
+ context 'user without archiving rights to the project' do
+ before do
+ project.team << [user3, :developer]
+ end
+
+ it 'rejects the action' do
+ post api("/projects/#{project.id}/archive", user3)
+
+ expect(response.status).to eq(403)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/unarchive' do
+ context 'on an unarchived project' do
+ it 'remains unarchived' do
+ post api("/projects/#{project.id}/unarchive", user)
+
+ expect(response.status).to eq(201)
+ expect(json_response['archived']).to be_falsey
+ end
+ end
+
+ context 'on an archived project' do
+ before do
+ project.archive!
+ end
+
+ it 'unarchives the project' do
+ post api("/projects/#{project.id}/unarchive", user)
+
+ expect(response.status).to eq(201)
+ expect(json_response['archived']).to be_falsey
+ end
+ end
+
+ context 'user without archiving rights to the project' do
+ before do
+ project.team << [user3, :developer]
+ end
+
+ it 'rejects the action' do
+ post api("/projects/#{project.id}/unarchive", user3)
+
+ expect(response.status).to eq(403)
+ end
+ end
+ end
+
describe 'DELETE /projects/:id' do
context 'when authenticated as user' do
it 'should remove project' do
diff --git a/spec/services/create_snippet_service_spec.rb b/spec/services/create_snippet_service_spec.rb
index c800dea04fa..7a850066bf8 100644
--- a/spec/services/create_snippet_service_spec.rb
+++ b/spec/services/create_snippet_service_spec.rb
@@ -23,7 +23,7 @@ describe CreateSnippetService, services: true do
snippet = create_snippet(nil, @user, @opts)
expect(snippet.errors.messages).to have_key(:visibility_level)
expect(snippet.errors.messages[:visibility_level].first).to(
- match('Public visibility has been restricted')
+ match('has been restricted')
)
end
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index b49ca96e8e8..8490a729e51 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -215,12 +215,16 @@ describe GitPushService, services: true do
let(:commit) { project.commit }
before do
+ project.team << [commit_author, :developer]
+ project.team << [user, :developer]
+
allow(commit).to receive_messages(
safe_message: "this commit \n mentions #{issue.to_reference}",
references: [issue],
author_name: commit_author.name,
author_email: commit_author.email
)
+
allow(project.repository).to receive(:commits_between).and_return([commit])
end
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
new file mode 100644
index 00000000000..6aefb48a4e8
--- /dev/null
+++ b/spec/services/groups/create_service_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe Groups::CreateService, services: true do
+ let!(:user) { create(:user) }
+ let!(:group_params) { { path: "group_path", visibility_level: Gitlab::VisibilityLevel::PUBLIC } }
+
+ describe "execute" do
+ let!(:service) { described_class.new(user, group_params ) }
+ subject { service.execute }
+
+ context "create groups without restricted visibility level" do
+ it { is_expected.to be_persisted }
+ end
+
+ context "cannot create group with restricted visibility level" do
+ before { allow(current_application_settings).to receive(:restricted_visibility_levels).and_return([Gitlab::VisibilityLevel::PUBLIC]) }
+ it { is_expected.to_not be_persisted }
+ end
+ end
+end
diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb
new file mode 100644
index 00000000000..9c2331144a0
--- /dev/null
+++ b/spec/services/groups/update_service_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe Groups::UpdateService, services: true do
+ let!(:user) { create(:user) }
+ let!(:private_group) { create(:group, :private) }
+ let!(:internal_group) { create(:group, :internal) }
+ let!(:public_group) { create(:group, :public) }
+
+ describe "#execute" do
+ context "project visibility_level validation" do
+ context "public group with public projects" do
+ let!(:service) { described_class.new(public_group, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL ) }
+
+ before do
+ public_group.add_user(user, Gitlab::Access::MASTER)
+ create(:project, :public, group: public_group)
+ end
+
+ it "does not change permission level" do
+ service.execute
+ expect(public_group.errors.count).to eq(1)
+ end
+ end
+
+ context "internal group with internal project" do
+ let!(:service) { described_class.new(internal_group, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE ) }
+
+ before do
+ internal_group.add_user(user, Gitlab::Access::MASTER)
+ create(:project, :internal, group: internal_group)
+ end
+
+ it "does not change permission level" do
+ service.execute
+ expect(internal_group.errors.count).to eq(1)
+ end
+ end
+ end
+ end
+
+ context "unauthorized visibility_level validation" do
+ let!(:service) { described_class.new(internal_group, user, visibility_level: 99 ) }
+ before do
+ internal_group.add_user(user, Gitlab::Access::MASTER)
+ end
+
+ it "does not change permission level" do
+ service.execute
+ expect(internal_group.errors.count).to eq(1)
+ end
+ end
+end
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
new file mode 100644
index 00000000000..2a5e4ac3ec4
--- /dev/null
+++ b/spec/services/issues/move_service_spec.rb
@@ -0,0 +1,250 @@
+require 'spec_helper'
+
+describe Issues::MoveService, services: true do
+ let(:user) { create(:user) }
+ let(:author) { create(:user) }
+ let(:title) { 'Some issue' }
+ let(:description) { 'Some issue description' }
+ let(:old_project) { create(:project) }
+ let(:new_project) { create(:project) }
+
+ let(:old_issue) do
+ create(:issue, title: title, description: description,
+ project: old_project, author: author)
+ end
+
+ let(:move_service) do
+ described_class.new(old_project, user)
+ end
+
+ shared_context 'user can move issue' do
+ before do
+ old_project.team << [user, :reporter]
+ new_project.team << [user, :reporter]
+ end
+ end
+
+ describe '#execute' do
+ shared_context 'issue move executed' do
+ let!(:new_issue) { move_service.execute(old_issue, new_project) }
+ end
+
+ context 'issue movable' do
+ include_context 'user can move issue'
+
+ context 'generic issue' do
+ include_context 'issue move executed'
+
+ it 'creates a new issue in a new project' do
+ expect(new_issue.project).to eq new_project
+ end
+
+ it 'rewrites issue title' do
+ expect(new_issue.title).to eq title
+ end
+
+ it 'rewrites issue description' do
+ expect(new_issue.description).to eq description
+ end
+
+ it 'adds system note to old issue at the end' do
+ expect(old_issue.notes.last.note).to match /^Moved to/
+ end
+
+ it 'adds system note to new issue at the end' do
+ expect(new_issue.notes.last.note).to match /^Moved from/
+ end
+
+ it 'closes old issue' do
+ expect(old_issue.closed?).to be true
+ end
+
+ it 'persists new issue' do
+ expect(new_issue.persisted?).to be true
+ end
+
+ it 'persists all changes' do
+ expect(old_issue.changed?).to be false
+ expect(new_issue.changed?).to be false
+ end
+
+ it 'preserves author' do
+ expect(new_issue.author).to eq author
+ end
+
+ it 'removes data that is invalid in new context' do
+ expect(new_issue.milestone).to be_nil
+ expect(new_issue.labels).to be_empty
+ end
+
+ it 'creates a new internal id for issue' do
+ expect(new_issue.iid).to be 1
+ end
+
+ it 'marks issue as moved' do
+ expect(old_issue.moved?).to eq true
+ expect(old_issue.moved_to).to eq new_issue
+ end
+
+ it 'preserves create time' do
+ expect(old_issue.created_at).to eq new_issue.created_at
+ end
+ end
+
+ context 'issue with notes' do
+ context 'notes without references' do
+ let(:notes_params) do
+ [{ system: false, note: 'Some comment 1' },
+ { system: true, note: 'Some system note' },
+ { system: false, note: 'Some comment 2' }]
+ end
+
+ let(:notes_contents) { notes_params.map { |n| n[:note] } }
+
+ before do
+ note_params = { noteable: old_issue, project: old_project, author: author }
+ notes_params.each do |note|
+ create(:note, note_params.merge(note))
+ end
+ end
+
+ include_context 'issue move executed'
+
+ let(:all_notes) { new_issue.notes.order('id ASC') }
+ let(:system_notes) { all_notes.system }
+ let(:user_notes) { all_notes.user }
+
+ it 'rewrites existing notes in valid order' do
+ expect(all_notes.pluck(:note).first(3)).to eq notes_contents
+ end
+
+ it 'adds a system note about move after rewritten notes' do
+ expect(system_notes.last.note).to match /^Moved from/
+ end
+
+ it 'preserves orignal author of comment' do
+ expect(user_notes.pluck(:author_id)).to all(eq(author.id))
+ end
+ end
+
+ context 'note that has been updated' do
+ let!(:note) do
+ create(:note, noteable: old_issue, project: old_project,
+ author: author, updated_at: Date.yesterday,
+ created_at: Date.yesterday)
+ end
+
+ include_context 'issue move executed'
+
+ it 'preserves time when note has been created at' do
+ expect(new_issue.notes.first.created_at).to eq note.created_at
+ end
+
+ it 'preserves time when note has been updated at' do
+ expect(new_issue.notes.first.updated_at).to eq note.updated_at
+ end
+ end
+
+ context 'notes with references' do
+ before do
+ create(:merge_request, source_project: old_project)
+ create(:note, noteable: old_issue, project: old_project, author: author,
+ note: 'Note with reference to merge request !1')
+ end
+
+ include_context 'issue move executed'
+ let(:new_note) { new_issue.notes.first }
+
+ it 'rewrites references using a cross reference to old project' do
+ expect(new_note.note)
+ .to eq "Note with reference to merge request #{old_project.to_reference}!1"
+ end
+ end
+
+ context 'issue description with uploads' do
+ let(:uploader) { build(:file_uploader, project: old_project) }
+ let(:description) { "Text and #{uploader.to_markdown}" }
+
+ include_context 'issue move executed'
+
+ it 'rewrites uploads in description' do
+ expect(new_issue.description).to_not eq description
+ expect(new_issue.description)
+ .to match(/Text and #{FileUploader::MARKDOWN_PATTERN}/)
+ expect(new_issue.description).to_not include uploader.secret
+ end
+ end
+ end
+
+ describe 'rewritting references' do
+ include_context 'issue move executed'
+
+ context 'issue reference' do
+ let(:another_issue) { create(:issue, project: old_project) }
+ let(:description) { "Some description #{another_issue.to_reference}" }
+
+ it 'rewrites referenced issues creating cross project reference' do
+ expect(new_issue.description)
+ .to eq "Some description #{old_project.to_reference}#{another_issue.to_reference}"
+ end
+ end
+ end
+
+ context 'moving to same project' do
+ let(:new_project) { old_project }
+
+ it 'raises error' do
+ expect { move_service.execute(old_issue, new_project) }
+ .to raise_error(StandardError, /Cannot move issue/)
+ end
+ end
+ end
+
+ describe 'move permissions' do
+ let(:move) { move_service.execute(old_issue, new_project) }
+
+ context 'user is reporter in both projects' do
+ include_context 'user can move issue'
+ it { expect { move }.to_not raise_error }
+ end
+
+ context 'user is reporter only in new project' do
+ before { new_project.team << [user, :reporter] }
+ it { expect { move }.to raise_error(StandardError, /permissions/) }
+ end
+
+ context 'user is reporter only in old project' do
+ before { old_project.team << [user, :reporter] }
+ it { expect { move }.to raise_error(StandardError, /permissions/) }
+ end
+
+ context 'user is reporter in one project and guest in another' do
+ before do
+ new_project.team << [user, :guest]
+ old_project.team << [user, :reporter]
+ end
+
+ it { expect { move }.to raise_error(StandardError, /permissions/) }
+ end
+
+ context 'issue has already been moved' do
+ include_context 'user can move issue'
+
+ let(:moved_to_issue) { create(:issue) }
+
+ let(:old_issue) do
+ create(:issue, project: old_project, author: author,
+ moved_to: moved_to_issue)
+ end
+
+ it { expect { move }.to raise_error(StandardError, /permissions/) }
+ end
+
+ context 'issue is not persisted' do
+ include_context 'user can move issue'
+ let(:old_issue) { build(:issue, project: old_project, author: author) }
+ it { expect { move }.to raise_error(StandardError, /permissions/) }
+ end
+ end
+ end
+end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 4ffe753fef5..6b214a0d96b 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -151,7 +151,12 @@ describe Issues::UpdateService, services: true do
context 'when the issue is relabeled' do
let!(:non_subscriber) { create(:user) }
- let!(:subscriber) { create(:user).tap { |u| label.toggle_subscription(u) } }
+ let!(:subscriber) do
+ create(:user).tap do |u|
+ label.toggle_subscription(u)
+ project.team << [u, :developer]
+ end
+ end
it 'sends notifications for subscribers of newly added labels' do
opts = { label_ids: [label.id] }
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index b5407397c1d..0f2aa3ae73c 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -111,6 +111,33 @@ describe NotificationService, services: true do
end
end
+ context 'confidential issue note' do
+ let(:project) { create(:empty_project, :public) }
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
+ let(:note) { create(:note_on_issue, noteable: confidential_issue, project: project, note: "#{author.to_reference} #{assignee.to_reference} #{non_member.to_reference} #{member.to_reference} #{admin.to_reference}") }
+
+ it 'filters out users that can not read the issue' do
+ project.team << [member, :developer]
+
+ expect(SentNotification).to receive(:record).with(confidential_issue, any_args).exactly(4).times
+
+ ActionMailer::Base.deliveries.clear
+
+ notification.new_note(note)
+
+ should_not_email(non_member)
+ should_email(author)
+ should_email(assignee)
+ should_email(member)
+ should_email(admin)
+ end
+ end
+
context 'issue note mention' do
let(:project) { create(:empty_project, :public) }
let(:issue) { create(:issue, project: project, assignee: create(:user)) }
@@ -233,6 +260,36 @@ describe NotificationService, services: true do
should_email(subscriber)
end
+
+ context 'confidential issues' do
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignee: assignee) }
+
+ it "emails subscribers of the issue's labels that can read the issue" do
+ project.team << [member, :developer]
+
+ label = create(:label, issues: [confidential_issue])
+ label.toggle_subscription(non_member)
+ label.toggle_subscription(author)
+ label.toggle_subscription(assignee)
+ label.toggle_subscription(member)
+ label.toggle_subscription(admin)
+
+ ActionMailer::Base.deliveries.clear
+
+ notification.new_issue(confidential_issue, @u_disabled)
+
+ should_not_email(non_member)
+ should_not_email(author)
+ should_email(assignee)
+ should_email(member)
+ should_email(admin)
+ end
+ end
end
describe :reassigned_issue do
@@ -332,6 +389,37 @@ describe NotificationService, services: true do
should_not_email(subscriber_to_label)
should_email(subscriber_to_label2)
end
+
+ context 'confidential issues' do
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignee: assignee) }
+ let!(:label_1) { create(:label, issues: [confidential_issue]) }
+ let!(:label_2) { create(:label) }
+
+ it "emails subscribers of the issue's labels that can read the issue" do
+ project.team << [member, :developer]
+
+ label_2.toggle_subscription(non_member)
+ label_2.toggle_subscription(author)
+ label_2.toggle_subscription(assignee)
+ label_2.toggle_subscription(member)
+ label_2.toggle_subscription(admin)
+
+ ActionMailer::Base.deliveries.clear
+
+ notification.relabeled_issue(confidential_issue, [label_2], @u_disabled)
+
+ should_not_email(non_member)
+ should_email(author)
+ should_email(assignee)
+ should_email(member)
+ should_email(admin)
+ end
+ end
end
describe :close_issue do
diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb
new file mode 100644
index 00000000000..6108c26a78b
--- /dev/null
+++ b/spec/services/projects/autocomplete_service_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe Projects::AutocompleteService, services: true do
+ describe '#issues' do
+ describe 'confidential issues' do
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:project) { create(:empty_project, :public) }
+ let!(:issue) { create(:issue, project: project, title: 'Issue 1') }
+ let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) }
+ let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) }
+
+ it 'should not list project confidential issues for guests' do
+ autocomplete = described_class.new(project, nil)
+ issues = autocomplete.issues.map(&:iid)
+
+ expect(issues).to include issue.iid
+ expect(issues).not_to include security_issue_1.iid
+ expect(issues).not_to include security_issue_2.iid
+ expect(issues.count).to eq 1
+ end
+
+ it 'should not list project confidential issues for non project members' do
+ autocomplete = described_class.new(project, non_member)
+ issues = autocomplete.issues.map(&:iid)
+
+ expect(issues).to include issue.iid
+ expect(issues).not_to include security_issue_1.iid
+ expect(issues).not_to include security_issue_2.iid
+ expect(issues.count).to eq 1
+ end
+
+ it 'should list project confidential issues for author' do
+ autocomplete = described_class.new(project, author)
+ issues = autocomplete.issues.map(&:iid)
+
+ expect(issues).to include issue.iid
+ expect(issues).to include security_issue_1.iid
+ expect(issues).not_to include security_issue_2.iid
+ expect(issues.count).to eq 2
+ end
+
+ it 'should list project confidential issues for assignee' do
+ autocomplete = described_class.new(project, assignee)
+ issues = autocomplete.issues.map(&:iid)
+
+ expect(issues).to include issue.iid
+ expect(issues).not_to include security_issue_1.iid
+ expect(issues).to include security_issue_2.iid
+ expect(issues.count).to eq 2
+ end
+
+ it 'should list project confidential issues for project members' do
+ project.team << [member, :developer]
+
+ autocomplete = described_class.new(project, member)
+ issues = autocomplete.issues.map(&:iid)
+
+ expect(issues).to include issue.iid
+ expect(issues).to include security_issue_1.iid
+ expect(issues).to include security_issue_2.iid
+ expect(issues.count).to eq 3
+ end
+
+ it 'should list all project issues for admin' do
+ autocomplete = described_class.new(project, admin)
+ issues = autocomplete.issues.map(&:iid)
+
+ expect(issues).to include issue.iid
+ expect(issues).to include security_issue_1.iid
+ expect(issues).to include security_issue_2.iid
+ expect(issues.count).to eq 3
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb
index 93bf1b81fbe..4c5ced7e746 100644
--- a/spec/services/projects/housekeeping_service_spec.rb
+++ b/spec/services/projects/housekeeping_service_spec.rb
@@ -12,7 +12,7 @@ describe Projects::HousekeepingService do
it 'enqueues a sidekiq job' do
expect(subject).to receive(:try_obtain_lease).and_return(true)
- expect(GitlabShellWorker).to receive(:perform_async).with(:gc, project.path_with_namespace)
+ expect(GitlabShellOneShotWorker).to receive(:perform_async).with(:gc, project.path_with_namespace)
subject.execute
expect(project.pushes_since_gc).to eq(0)
@@ -20,7 +20,7 @@ describe Projects::HousekeepingService do
it 'does not enqueue a job when no lease can be obtained' do
expect(subject).to receive(:try_obtain_lease).and_return(false)
- expect(GitlabShellWorker).not_to receive(:perform_async)
+ expect(GitlabShellOneShotWorker).not_to receive(:perform_async)
expect { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken)
expect(project.pushes_since_gc).to eq(0)
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 8e6292014d4..240eae10052 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -453,6 +453,59 @@ describe SystemNoteService, services: true do
end
end
+ describe '.noteable_moved' do
+ let(:new_project) { create(:project) }
+ let(:new_noteable) { create(:issue, project: new_project) }
+
+ subject do
+ described_class.noteable_moved(noteable, project, new_noteable, author, direction: direction)
+ end
+
+ shared_examples 'cross project mentionable' do
+ include GitlabMarkdownHelper
+
+ it 'should contain cross reference to new noteable' do
+ expect(subject.note).to include cross_project_reference(new_project, new_noteable)
+ end
+
+ it 'should mention referenced noteable' do
+ expect(subject.note).to include new_noteable.to_reference
+ end
+
+ it 'should mention referenced project' do
+ expect(subject.note).to include new_project.to_reference
+ end
+ end
+
+ context 'moved to' do
+ let(:direction) { :to }
+
+ it_behaves_like 'cross project mentionable'
+
+ it 'should notify about noteable being moved to' do
+ expect(subject.note).to match /Moved to/
+ end
+ end
+
+ context 'moved from' do
+ let(:direction) { :from }
+
+ it_behaves_like 'cross project mentionable'
+
+ it 'should notify about noteable being moved from' do
+ expect(subject.note).to match /Moved from/
+ end
+ end
+
+ context 'invalid direction' do
+ let(:direction) { :invalid }
+
+ it 'should raise error' do
+ expect { subject }.to raise_error StandardError, /Invalid direction/
+ end
+ end
+ end
+
include JiraServiceHelper
describe 'JIRA integration' do
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 96420acb31d..82b7fbfa816 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -2,22 +2,25 @@ require 'spec_helper'
describe TodoService, services: true do
let(:author) { create(:user) }
- let(:john_doe) { create(:user, username: 'john_doe') }
- let(:michael) { create(:user, username: 'michael') }
- let(:stranger) { create(:user, username: 'stranger') }
+ let(:assignee) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:john_doe) { create(:user) }
let(:project) { create(:project) }
- let(:mentions) { [author.to_reference, john_doe.to_reference, michael.to_reference, stranger.to_reference].join(' ') }
+ let(:mentions) { [author, assignee, john_doe, member, non_member, admin].map(&:to_reference).join(' ') }
let(:service) { described_class.new }
before do
project.team << [author, :developer]
+ project.team << [member, :developer]
project.team << [john_doe, :developer]
- project.team << [michael, :developer]
end
describe 'Issues' do
let(:issue) { create(:issue, project: project, assignee: john_doe, author: author, description: mentions) }
let(:unassigned_issue) { create(:issue, project: project, assignee: nil) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee, description: mentions) }
describe '#new_issue' do
it 'creates a todo if assigned' do
@@ -37,10 +40,20 @@ describe TodoService, services: true do
it 'creates a todo for each valid mentioned user' do
service.new_issue(issue, author)
- should_create_todo(user: michael, target: issue, action: Todo::MENTIONED)
+ should_create_todo(user: member, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED)
- should_not_create_todo(user: stranger, target: issue, action: Todo::MENTIONED)
+ should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
+ end
+
+ it 'does not create todo for non project members when issue is confidential' do
+ service.new_issue(confidential_issue, john_doe)
+
+ should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::ASSIGNED)
+ should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
+ should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
+ should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
+ should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
end
end
@@ -48,16 +61,26 @@ describe TodoService, services: true do
it 'creates a todo for each valid mentioned user' do
service.update_issue(issue, author)
- should_create_todo(user: michael, target: issue, action: Todo::MENTIONED)
+ should_create_todo(user: member, target: issue, action: Todo::MENTIONED)
should_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED)
- should_not_create_todo(user: stranger, target: issue, action: Todo::MENTIONED)
+ should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
end
it 'does not create a todo if user was already mentioned' do
- create(:todo, :mentioned, user: michael, project: project, target: issue, author: author)
+ create(:todo, :mentioned, user: member, project: project, target: issue, author: author)
+
+ expect { service.update_issue(issue, author) }.not_to change(member.todos, :count)
+ end
- expect { service.update_issue(issue, author) }.not_to change(michael.todos, :count)
+ it 'does not create todo for non project members when issue is confidential' do
+ service.update_issue(confidential_issue, john_doe)
+
+ should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
+ should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
+ should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
+ should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
+ should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
end
end
@@ -109,8 +132,10 @@ describe TodoService, services: true do
describe '#new_note' do
let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
let(:note) { create(:note, project: project, noteable: issue, author: john_doe, note: mentions) }
let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) }
+ let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project, note: mentions) }
let(:note_on_project_snippet) { create(:note_on_project_snippet, project: project, author: john_doe, note: mentions) }
let(:award_note) { create(:note, :award, project: project, noteable: issue, author: john_doe, note: 'thumbsup') }
let(:system_note) { create(:system_note, project: project, noteable: issue) }
@@ -142,14 +167,29 @@ describe TodoService, services: true do
it 'creates a todo for each valid mentioned user' do
service.new_note(note, john_doe)
- should_create_todo(user: michael, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
+ should_create_todo(user: member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
should_create_todo(user: author, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
should_not_create_todo(user: john_doe, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
- should_not_create_todo(user: stranger, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
+ should_not_create_todo(user: non_member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
end
- it 'does not create todo when leaving a note on commit' do
- should_not_create_any_todo { service.new_note(note_on_commit, john_doe) }
+ it 'does not create todo for non project members when leaving a note on a confidential issue' do
+ service.new_note(note_on_confidential_issue, john_doe)
+
+ should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
+ should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
+ should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
+ should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
+ should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
+ end
+
+ it 'creates a todo for each valid mentioned user when leaving a note on commit' do
+ service.new_note(note_on_commit, john_doe)
+
+ should_create_todo(user: member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
+ should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
+ should_not_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
+ should_not_create_todo(user: non_member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
end
it 'does not create todo when leaving a note on snippet' do
@@ -180,10 +220,10 @@ describe TodoService, services: true do
it 'creates a todo for each valid mentioned user' do
service.new_merge_request(mr_assigned, author)
- should_create_todo(user: michael, target: mr_assigned, action: Todo::MENTIONED)
+ should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
- should_not_create_todo(user: stranger, target: mr_assigned, action: Todo::MENTIONED)
+ should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
end
end
@@ -191,16 +231,16 @@ describe TodoService, services: true do
it 'creates a todo for each valid mentioned user' do
service.update_merge_request(mr_assigned, author)
- should_create_todo(user: michael, target: mr_assigned, action: Todo::MENTIONED)
+ should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
should_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
- should_not_create_todo(user: stranger, target: mr_assigned, action: Todo::MENTIONED)
+ should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
end
it 'does not create a todo if user was already mentioned' do
- create(:todo, :mentioned, user: michael, project: project, target: mr_assigned, author: author)
+ create(:todo, :mentioned, user: member, project: project, target: mr_assigned, author: author)
- expect { service.update_merge_request(mr_assigned, author) }.not_to change(michael.todos, :count)
+ expect { service.update_merge_request(mr_assigned, author) }.not_to change(member.todos, :count)
end
end
diff --git a/spec/services/update_snippet_service_spec.rb b/spec/services/update_snippet_service_spec.rb
index 48d114896d0..37c2e861362 100644
--- a/spec/services/update_snippet_service_spec.rb
+++ b/spec/services/update_snippet_service_spec.rb
@@ -25,7 +25,7 @@ describe UpdateSnippetService, services: true do
update_snippet(@project, @user, @snippet, @opts)
expect(@snippet.errors.messages).to have_key(:visibility_level)
expect(@snippet.errors.messages[:visibility_level].first).to(
- match('Public visibility has been restricted')
+ match('has been restricted')
)
expect(@snippet.visibility_level).to eq(old_visibility)
end
diff --git a/spec/support/carrierwave.rb b/spec/support/carrierwave.rb
new file mode 100644
index 00000000000..aa89afd8fb3
--- /dev/null
+++ b/spec/support/carrierwave.rb
@@ -0,0 +1,7 @@
+CarrierWave.root = 'tmp/tests/uploads'
+
+RSpec.configure do |config|
+ config.after(:suite) do
+ FileUtils.rm_rf('tmp/tests/uploads')
+ end
+end
diff --git a/spec/support/filter_spec_helper.rb b/spec/support/filter_spec_helper.rb
index ef5ea7d626e..e849a9633b9 100644
--- a/spec/support/filter_spec_helper.rb
+++ b/spec/support/filter_spec_helper.rb
@@ -78,6 +78,6 @@ module FilterSpecHelper
# Shortcut to Rails' auto-generated routes helpers, to avoid including the
# module
def urls
- Gitlab::Application.routes.url_helpers
+ Gitlab::Routing.url_helpers
end
end
diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb
index 73c6792b65f..b87cd6bbca2 100644
--- a/spec/support/markdown_feature.rb
+++ b/spec/support/markdown_feature.rb
@@ -106,7 +106,7 @@ class MarkdownFeature
end
def urls
- Gitlab::Application.routes.url_helpers
+ Gitlab::Routing.url_helpers
end
def raw_markdown
diff --git a/spec/support/matchers/access_matchers.rb b/spec/support/matchers/access_matchers.rb
index 4e007c777e3..0497e391860 100644
--- a/spec/support/matchers/access_matchers.rb
+++ b/spec/support/matchers/access_matchers.rb
@@ -28,7 +28,7 @@ module AccessMatchers
if user.kind_of?(User)
# User#inspect displays too much information for RSpec's description
# messages
- "be #{type} for supplied User"
+ "be #{type} for the specified user"
else
"be #{type} for #{user}"
end
diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb
index fce91015fd4..e876d44c166 100644
--- a/spec/support/mentionable_shared_examples.rb
+++ b/spec/support/mentionable_shared_examples.rb
@@ -52,6 +52,8 @@ shared_context 'mentionable context' do
end
set_mentionable_text.call(ref_string)
+
+ project.team << [author, :developer]
end
end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 63bed2414df..05fc4c4554f 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -3,11 +3,16 @@ require 'rake'
describe 'gitlab:app namespace rake task' do
before :all do
- Rake.application.rake_require "tasks/gitlab/task_helpers"
- Rake.application.rake_require "tasks/gitlab/backup"
- Rake.application.rake_require "tasks/gitlab/shell"
+ Rake.application.rake_require 'tasks/gitlab/task_helpers'
+ Rake.application.rake_require 'tasks/gitlab/backup'
+ Rake.application.rake_require 'tasks/gitlab/shell'
+ Rake.application.rake_require 'tasks/gitlab/db'
+
# empty task as env is already loaded
Rake::Task.define_task :environment
+
+ # We need this directory to run `gitlab:backup:create` task
+ FileUtils.mkdir_p('public/uploads')
end
def run_rake_task(task_name)
@@ -37,6 +42,7 @@ describe 'gitlab:app namespace rake task' do
allow(FileUtils).to receive(:mv).and_return(true)
allow(Rake::Task["gitlab:shell:setup"]).
to receive(:invoke).and_return(true)
+ ENV['force'] = 'yes'
end
let(:gitlab_version) { Gitlab::VERSION }
@@ -52,13 +58,14 @@ describe 'gitlab:app namespace rake task' do
it 'should invoke restoration on match' do
allow(YAML).to receive(:load_file).
and_return({ gitlab_version: gitlab_version })
- expect(Rake::Task["gitlab:backup:db:restore"]).to receive(:invoke)
- expect(Rake::Task["gitlab:backup:repo:restore"]).to receive(:invoke)
- expect(Rake::Task["gitlab:backup:builds:restore"]).to receive(:invoke)
- expect(Rake::Task["gitlab:backup:uploads:restore"]).to receive(:invoke)
- expect(Rake::Task["gitlab:backup:artifacts:restore"]).to receive(:invoke)
- expect(Rake::Task["gitlab:backup:lfs:restore"]).to receive(:invoke)
- expect(Rake::Task["gitlab:shell:setup"]).to receive(:invoke)
+ expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:db:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:repo:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:builds:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:uploads:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:lfs:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:shell:setup']).to receive(:invoke)
expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error
end
end
@@ -177,17 +184,18 @@ describe 'gitlab:app namespace rake task' do
end
it 'does not invoke repositories restore' do
- allow(Rake::Task["gitlab:shell:setup"]).
+ allow(Rake::Task['gitlab:shell:setup']).
to receive(:invoke).and_return(true)
allow($stdout).to receive :write
- expect(Rake::Task["gitlab:backup:db:restore"]).to receive :invoke
- expect(Rake::Task["gitlab:backup:repo:restore"]).not_to receive :invoke
- expect(Rake::Task["gitlab:backup:uploads:restore"]).not_to receive :invoke
- expect(Rake::Task["gitlab:backup:builds:restore"]).to receive :invoke
- expect(Rake::Task["gitlab:backup:artifacts:restore"]).to receive :invoke
- expect(Rake::Task["gitlab:backup:lfs:restore"]).to receive :invoke
- expect(Rake::Task["gitlab:shell:setup"]).to receive :invoke
+ expect(Rake::Task['gitlab:db:drop_tables']).to receive :invoke
+ expect(Rake::Task['gitlab:backup:db:restore']).to receive :invoke
+ expect(Rake::Task['gitlab:backup:repo:restore']).not_to receive :invoke
+ expect(Rake::Task['gitlab:backup:uploads:restore']).not_to receive :invoke
+ expect(Rake::Task['gitlab:backup:builds:restore']).to receive :invoke
+ expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive :invoke
+ expect(Rake::Task['gitlab:backup:lfs:restore']).to receive :invoke
+ expect(Rake::Task['gitlab:shell:setup']).to receive :invoke
expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error
end
end
diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb
new file mode 100644
index 00000000000..7e59bd2fced
--- /dev/null
+++ b/spec/workers/project_cache_worker_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe ProjectCacheWorker do
+ let(:project) { create(:project) }
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ it 'updates project cache data' do
+
+ expect_any_instance_of(Repository).to receive(:size)
+ expect_any_instance_of(Repository).to receive(:commit_count)
+
+ expect_any_instance_of(Project).to receive(:update_repository_size)
+ expect_any_instance_of(Project).to receive(:update_commit_count)
+
+ subject.perform(project.id)
+ end
+
+ it 'handles missing repository data' do
+ expect_any_instance_of(Repository).to receive(:exists?).and_return(false)
+ expect_any_instance_of(Repository).not_to receive(:size)
+
+ subject.perform(project.id)
+ end
+ end
+end
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
index 172537474ee..4ef05eb29d2 100644
--- a/spec/workers/repository_fork_worker_spec.rb
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -3,12 +3,17 @@ require 'spec_helper'
describe RepositoryForkWorker do
let(:project) { create(:project) }
let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:shell) { Gitlab::Shell.new }
subject { RepositoryForkWorker.new }
+ before do
+ allow(subject).to receive(:gitlab_shell).and_return(shell)
+ end
+
describe "#perform" do
it "creates a new repository from a fork" do
- expect_any_instance_of(Gitlab::Shell).to receive(:fork_repository).with(
+ expect(shell).to receive(:fork_repository).with(
project.path_with_namespace,
fork_project.namespace.path
).and_return(true)
@@ -19,20 +24,26 @@ describe RepositoryForkWorker do
fork_project.namespace.path)
end
- it 'flushes the empty caches' do
- expect_any_instance_of(Gitlab::Shell).to receive(:fork_repository).
+ it 'flushes various caches' do
+ expect(shell).to receive(:fork_repository).
with(project.path_with_namespace, fork_project.namespace.path).
and_return(true)
expect_any_instance_of(Repository).to receive(:expire_emptiness_caches).
and_call_original
+ expect_any_instance_of(Repository).to receive(:expire_exists_cache).
+ and_call_original
+
subject.perform(project.id, project.path_with_namespace,
fork_project.namespace.path)
end
it "handles bad fork" do
- expect_any_instance_of(Gitlab::Shell).to receive(:fork_repository).and_return(false)
+ expect(shell).to receive(:fork_repository).and_return(false)
+
+ expect(subject.logger).to receive(:error)
+
subject.perform(
project.id,
project.path_with_namespace,
diff --git a/vendor/assets/javascripts/cropper.js b/vendor/assets/javascripts/cropper.js
new file mode 100644
index 00000000000..805485904a5
--- /dev/null
+++ b/vendor/assets/javascripts/cropper.js
@@ -0,0 +1,2993 @@
+/*!
+ * Cropper v2.3.0
+ * https://github.com/fengyuanchen/cropper
+ *
+ * Copyright (c) 2014-2016 Fengyuan Chen and contributors
+ * Released under the MIT license
+ *
+ * Date: 2016-02-22T02:13:13.332Z
+ */
+
+(function (factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as anonymous module.
+ define(['jquery'], factory);
+ } else if (typeof exports === 'object') {
+ // Node / CommonJS
+ factory(require('jquery'));
+ } else {
+ // Browser globals.
+ factory(jQuery);
+ }
+})(function ($) {
+
+ 'use strict';
+
+ // Globals
+ var $window = $(window);
+ var $document = $(document);
+ var location = window.location;
+ var navigator = window.navigator;
+ var ArrayBuffer = window.ArrayBuffer;
+ var Uint8Array = window.Uint8Array;
+ var DataView = window.DataView;
+ var btoa = window.btoa;
+
+ // Constants
+ var NAMESPACE = 'cropper';
+
+ // Classes
+ var CLASS_MODAL = 'cropper-modal';
+ var CLASS_HIDE = 'cropper-hide';
+ var CLASS_HIDDEN = 'cropper-hidden';
+ var CLASS_INVISIBLE = 'cropper-invisible';
+ var CLASS_MOVE = 'cropper-move';
+ var CLASS_CROP = 'cropper-crop';
+ var CLASS_DISABLED = 'cropper-disabled';
+ var CLASS_BG = 'cropper-bg';
+
+ // Events
+ var EVENT_MOUSE_DOWN = 'mousedown touchstart pointerdown MSPointerDown';
+ var EVENT_MOUSE_MOVE = 'mousemove touchmove pointermove MSPointerMove';
+ var EVENT_MOUSE_UP = 'mouseup touchend touchcancel pointerup pointercancel MSPointerUp MSPointerCancel';
+ var EVENT_WHEEL = 'wheel mousewheel DOMMouseScroll';
+ var EVENT_DBLCLICK = 'dblclick';
+ var EVENT_LOAD = 'load.' + NAMESPACE;
+ var EVENT_ERROR = 'error.' + NAMESPACE;
+ var EVENT_RESIZE = 'resize.' + NAMESPACE; // Bind to window with namespace
+ var EVENT_BUILD = 'build.' + NAMESPACE;
+ var EVENT_BUILT = 'built.' + NAMESPACE;
+ var EVENT_CROP_START = 'cropstart.' + NAMESPACE;
+ var EVENT_CROP_MOVE = 'cropmove.' + NAMESPACE;
+ var EVENT_CROP_END = 'cropend.' + NAMESPACE;
+ var EVENT_CROP = 'crop.' + NAMESPACE;
+ var EVENT_ZOOM = 'zoom.' + NAMESPACE;
+
+ // RegExps
+ var REGEXP_ACTIONS = /e|w|s|n|se|sw|ne|nw|all|crop|move|zoom/;
+ var REGEXP_DATA_URL = /^data\:/;
+ var REGEXP_DATA_URL_HEAD = /^data\:([^\;]+)\;base64,/;
+ var REGEXP_DATA_URL_JPEG = /^data\:image\/jpeg.*;base64,/;
+
+ // Data keys
+ var DATA_PREVIEW = 'preview';
+ var DATA_ACTION = 'action';
+
+ // Actions
+ var ACTION_EAST = 'e';
+ var ACTION_WEST = 'w';
+ var ACTION_SOUTH = 's';
+ var ACTION_NORTH = 'n';
+ var ACTION_SOUTH_EAST = 'se';
+ var ACTION_SOUTH_WEST = 'sw';
+ var ACTION_NORTH_EAST = 'ne';
+ var ACTION_NORTH_WEST = 'nw';
+ var ACTION_ALL = 'all';
+ var ACTION_CROP = 'crop';
+ var ACTION_MOVE = 'move';
+ var ACTION_ZOOM = 'zoom';
+ var ACTION_NONE = 'none';
+
+ // Supports
+ var SUPPORT_CANVAS = $.isFunction($('<canvas>')[0].getContext);
+ var IS_SAFARI = navigator && /safari/i.test(navigator.userAgent) && /apple computer/i.test(navigator.vendor);
+
+ // Maths
+ var num = Number;
+ var min = Math.min;
+ var max = Math.max;
+ var abs = Math.abs;
+ var sin = Math.sin;
+ var cos = Math.cos;
+ var sqrt = Math.sqrt;
+ var round = Math.round;
+ var floor = Math.floor;
+
+ // Utilities
+ var fromCharCode = String.fromCharCode;
+
+ function isNumber(n) {
+ return typeof n === 'number' && !isNaN(n);
+ }
+
+ function isUndefined(n) {
+ return typeof n === 'undefined';
+ }
+
+ function toArray(obj, offset) {
+ var args = [];
+
+ // This is necessary for IE8
+ if (isNumber(offset)) {
+ args.push(offset);
+ }
+
+ return args.slice.apply(obj, args);
+ }
+
+ // Custom proxy to avoid jQuery's guid
+ function proxy(fn, context) {
+ var args = toArray(arguments, 2);
+
+ return function () {
+ return fn.apply(context, args.concat(toArray(arguments)));
+ };
+ }
+
+ function isCrossOriginURL(url) {
+ var parts = url.match(/^(https?:)\/\/([^\:\/\?#]+):?(\d*)/i);
+
+ return parts && (
+ parts[1] !== location.protocol ||
+ parts[2] !== location.hostname ||
+ parts[3] !== location.port
+ );
+ }
+
+ function addTimestamp(url) {
+ var timestamp = 'timestamp=' + (new Date()).getTime();
+
+ return (url + (url.indexOf('?') === -1 ? '?' : '&') + timestamp);
+ }
+
+ function getCrossOrigin(crossOrigin) {
+ return crossOrigin ? ' crossOrigin="' + crossOrigin + '"' : '';
+ }
+
+ function getImageSize(image, callback) {
+ var newImage;
+
+ // Modern browsers (ignore Safari, #120 & #509)
+ if (image.naturalWidth && !IS_SAFARI) {
+ return callback(image.naturalWidth, image.naturalHeight);
+ }
+
+ // IE8: Don't use `new Image()` here (#319)
+ newImage = document.createElement('img');
+
+ newImage.onload = function () {
+ callback(this.width, this.height);
+ };
+
+ newImage.src = image.src;
+ }
+
+ function getTransform(options) {
+ var transforms = [];
+ var rotate = options.rotate;
+ var scaleX = options.scaleX;
+ var scaleY = options.scaleY;
+
+ if (isNumber(rotate)) {
+ transforms.push('rotate(' + rotate + 'deg)');
+ }
+
+ if (isNumber(scaleX) && isNumber(scaleY)) {
+ transforms.push('scale(' + scaleX + ',' + scaleY + ')');
+ }
+
+ return transforms.length ? transforms.join(' ') : 'none';
+ }
+
+ function getRotatedSizes(data, isReversed) {
+ var deg = abs(data.degree) % 180;
+ var arc = (deg > 90 ? (180 - deg) : deg) * Math.PI / 180;
+ var sinArc = sin(arc);
+ var cosArc = cos(arc);
+ var width = data.width;
+ var height = data.height;
+ var aspectRatio = data.aspectRatio;
+ var newWidth;
+ var newHeight;
+
+ if (!isReversed) {
+ newWidth = width * cosArc + height * sinArc;
+ newHeight = width * sinArc + height * cosArc;
+ } else {
+ newWidth = width / (cosArc + sinArc / aspectRatio);
+ newHeight = newWidth / aspectRatio;
+ }
+
+ return {
+ width: newWidth,
+ height: newHeight
+ };
+ }
+
+ function getSourceCanvas(image, data) {
+ var canvas = $('<canvas>')[0];
+ var context = canvas.getContext('2d');
+ var dstX = 0;
+ var dstY = 0;
+ var dstWidth = data.naturalWidth;
+ var dstHeight = data.naturalHeight;
+ var rotate = data.rotate;
+ var scaleX = data.scaleX;
+ var scaleY = data.scaleY;
+ var scalable = isNumber(scaleX) && isNumber(scaleY) && (scaleX !== 1 || scaleY !== 1);
+ var rotatable = isNumber(rotate) && rotate !== 0;
+ var advanced = rotatable || scalable;
+ var canvasWidth = dstWidth * abs(scaleX || 1);
+ var canvasHeight = dstHeight * abs(scaleY || 1);
+ var translateX;
+ var translateY;
+ var rotated;
+
+ if (scalable) {
+ translateX = canvasWidth / 2;
+ translateY = canvasHeight / 2;
+ }
+
+ if (rotatable) {
+ rotated = getRotatedSizes({
+ width: canvasWidth,
+ height: canvasHeight,
+ degree: rotate
+ });
+
+ canvasWidth = rotated.width;
+ canvasHeight = rotated.height;
+ translateX = canvasWidth / 2;
+ translateY = canvasHeight / 2;
+ }
+
+ canvas.width = canvasWidth;
+ canvas.height = canvasHeight;
+
+ if (advanced) {
+ dstX = -dstWidth / 2;
+ dstY = -dstHeight / 2;
+
+ context.save();
+ context.translate(translateX, translateY);
+ }
+
+ if (rotatable) {
+ context.rotate(rotate * Math.PI / 180);
+ }
+
+ // Should call `scale` after rotated
+ if (scalable) {
+ context.scale(scaleX, scaleY);
+ }
+
+ context.drawImage(image, floor(dstX), floor(dstY), floor(dstWidth), floor(dstHeight));
+
+ if (advanced) {
+ context.restore();
+ }
+
+ return canvas;
+ }
+
+ function getTouchesCenter(touches) {
+ var length = touches.length;
+ var pageX = 0;
+ var pageY = 0;
+
+ if (length) {
+ $.each(touches, function (i, touch) {
+ pageX += touch.pageX;
+ pageY += touch.pageY;
+ });
+
+ pageX /= length;
+ pageY /= length;
+ }
+
+ return {
+ pageX: pageX,
+ pageY: pageY
+ };
+ }
+
+ function getStringFromCharCode(dataView, start, length) {
+ var str = '';
+ var i;
+
+ for (i = start, length += start; i < length; i++) {
+ str += fromCharCode(dataView.getUint8(i));
+ }
+
+ return str;
+ }
+
+ function getOrientation(arrayBuffer) {
+ var dataView = new DataView(arrayBuffer);
+ var length = dataView.byteLength;
+ var orientation;
+ var exifIDCode;
+ var tiffOffset;
+ var firstIFDOffset;
+ var littleEndian;
+ var endianness;
+ var app1Start;
+ var ifdStart;
+ var offset;
+ var i;
+
+ // Only handle JPEG image (start by 0xFFD8)
+ if (dataView.getUint8(0) === 0xFF && dataView.getUint8(1) === 0xD8) {
+ offset = 2;
+
+ while (offset < length) {
+ if (dataView.getUint8(offset) === 0xFF && dataView.getUint8(offset + 1) === 0xE1) {
+ app1Start = offset;
+ break;
+ }
+
+ offset++;
+ }
+ }
+
+ if (app1Start) {
+ exifIDCode = app1Start + 4;
+ tiffOffset = app1Start + 10;
+
+ if (getStringFromCharCode(dataView, exifIDCode, 4) === 'Exif') {
+ endianness = dataView.getUint16(tiffOffset);
+ littleEndian = endianness === 0x4949;
+
+ if (littleEndian || endianness === 0x4D4D /* bigEndian */) {
+ if (dataView.getUint16(tiffOffset + 2, littleEndian) === 0x002A) {
+ firstIFDOffset = dataView.getUint32(tiffOffset + 4, littleEndian);
+
+ if (firstIFDOffset >= 0x00000008) {
+ ifdStart = tiffOffset + firstIFDOffset;
+ }
+ }
+ }
+ }
+ }
+
+ if (ifdStart) {
+ length = dataView.getUint16(ifdStart, littleEndian);
+
+ for (i = 0; i < length; i++) {
+ offset = ifdStart + i * 12 + 2;
+
+ if (dataView.getUint16(offset, littleEndian) === 0x0112 /* Orientation */) {
+
+ // 8 is the offset of the current tag's value
+ offset += 8;
+
+ // Get the original orientation value
+ orientation = dataView.getUint16(offset, littleEndian);
+
+ // Override the orientation with its default value for Safari (#120)
+ if (IS_SAFARI) {
+ dataView.setUint16(offset, 1, littleEndian);
+ }
+
+ break;
+ }
+ }
+ }
+
+ return orientation;
+ }
+
+ function dataURLToArrayBuffer(dataURL) {
+ var base64 = dataURL.replace(REGEXP_DATA_URL_HEAD, '');
+ var binary = atob(base64);
+ var length = binary.length;
+ var arrayBuffer = new ArrayBuffer(length);
+ var dataView = new Uint8Array(arrayBuffer);
+ var i;
+
+ for (i = 0; i < length; i++) {
+ dataView[i] = binary.charCodeAt(i);
+ }
+
+ return arrayBuffer;
+ }
+
+ // Only available for JPEG image
+ function arrayBufferToDataURL(arrayBuffer) {
+ var dataView = new Uint8Array(arrayBuffer);
+ var length = dataView.length;
+ var base64 = '';
+ var i;
+
+ for (i = 0; i < length; i++) {
+ base64 += fromCharCode(dataView[i]);
+ }
+
+ return 'data:image/jpeg;base64,' + btoa(base64);
+ }
+
+ function Cropper(element, options) {
+ this.$element = $(element);
+ this.options = $.extend({}, Cropper.DEFAULTS, $.isPlainObject(options) && options);
+ this.isLoaded = false;
+ this.isBuilt = false;
+ this.isCompleted = false;
+ this.isRotated = false;
+ this.isCropped = false;
+ this.isDisabled = false;
+ this.isReplaced = false;
+ this.isLimited = false;
+ this.wheeling = false;
+ this.isImg = false;
+ this.originalUrl = '';
+ this.canvas = null;
+ this.cropBox = null;
+ this.init();
+ }
+
+ Cropper.prototype = {
+ constructor: Cropper,
+
+ init: function () {
+ var $this = this.$element;
+ var url;
+
+ if ($this.is('img')) {
+ this.isImg = true;
+
+ // Should use `$.fn.attr` here. e.g.: "img/picture.jpg"
+ this.originalUrl = url = $this.attr('src');
+
+ // Stop when it's a blank image
+ if (!url) {
+ return;
+ }
+
+ // Should use `$.fn.prop` here. e.g.: "http://example.com/img/picture.jpg"
+ url = $this.prop('src');
+ } else if ($this.is('canvas') && SUPPORT_CANVAS) {
+ url = $this[0].toDataURL();
+ }
+
+ this.load(url);
+ },
+
+ // A shortcut for triggering custom events
+ trigger: function (type, data) {
+ var e = $.Event(type, data);
+
+ this.$element.trigger(e);
+
+ return e;
+ },
+
+ load: function (url) {
+ var options = this.options;
+ var $this = this.$element;
+ var read;
+ var xhr;
+
+ if (!url) {
+ return;
+ }
+
+ // Trigger build event first
+ $this.one(EVENT_BUILD, options.build);
+
+ if (this.trigger(EVENT_BUILD).isDefaultPrevented()) {
+ return;
+ }
+
+ this.url = url;
+ this.image = {};
+
+ if (!options.checkOrientation || !ArrayBuffer) {
+ return this.clone();
+ }
+
+ read = $.proxy(this.read, this);
+
+ // XMLHttpRequest disallows to open a Data URL in some browsers like IE11 and Safari
+ if (REGEXP_DATA_URL.test(url)) {
+ return REGEXP_DATA_URL_JPEG.test(url) ?
+ read(dataURLToArrayBuffer(url)) :
+ this.clone();
+ }
+
+ xhr = new XMLHttpRequest();
+
+ xhr.onerror = xhr.onabort = $.proxy(function () {
+ this.clone();
+ }, this);
+
+ xhr.onload = function () {
+ read(this.response);
+ };
+
+ xhr.open('get', url);
+ xhr.responseType = 'arraybuffer';
+ xhr.send();
+ },
+
+ read: function (arrayBuffer) {
+ var options = this.options;
+ var orientation = getOrientation(arrayBuffer);
+ var image = this.image;
+ var rotate;
+ var scaleX;
+ var scaleY;
+
+ if (orientation > 1) {
+ this.url = arrayBufferToDataURL(arrayBuffer);
+
+ switch (orientation) {
+
+ // flip horizontal
+ case 2:
+ scaleX = -1;
+ break;
+
+ // rotate left 180°
+ case 3:
+ rotate = -180;
+ break;
+
+ // flip vertical
+ case 4:
+ scaleY = -1;
+ break;
+
+ // flip vertical + rotate right 90°
+ case 5:
+ rotate = 90;
+ scaleY = -1;
+ break;
+
+ // rotate right 90°
+ case 6:
+ rotate = 90;
+ break;
+
+ // flip horizontal + rotate right 90°
+ case 7:
+ rotate = 90;
+ scaleX = -1;
+ break;
+
+ // rotate left 90°
+ case 8:
+ rotate = -90;
+ break;
+ }
+ }
+
+ if (options.rotatable) {
+ image.rotate = rotate;
+ }
+
+ if (options.scalable) {
+ image.scaleX = scaleX;
+ image.scaleY = scaleY;
+ }
+
+ this.clone();
+ },
+
+ clone: function () {
+ var options = this.options;
+ var $this = this.$element;
+ var url = this.url;
+ var crossOrigin = '';
+ var crossOriginUrl;
+ var $clone;
+
+ if (options.checkCrossOrigin && isCrossOriginURL(url)) {
+ crossOrigin = $this.prop('crossOrigin');
+
+ if (crossOrigin) {
+ crossOriginUrl = url;
+ } else {
+ crossOrigin = 'anonymous';
+
+ // Bust cache (#148) when there is not a "crossOrigin" property
+ crossOriginUrl = addTimestamp(url);
+ }
+ }
+
+ this.crossOrigin = crossOrigin;
+ this.crossOriginUrl = crossOriginUrl;
+ this.$clone = $clone = $('<img' + getCrossOrigin(crossOrigin) + ' src="' + (crossOriginUrl || url) + '">');
+
+ if (this.isImg) {
+ if ($this[0].complete) {
+ this.start();
+ } else {
+ $this.one(EVENT_LOAD, $.proxy(this.start, this));
+ }
+ } else {
+ $clone.
+ one(EVENT_LOAD, $.proxy(this.start, this)).
+ one(EVENT_ERROR, $.proxy(this.stop, this)).
+ addClass(CLASS_HIDE).
+ insertAfter($this);
+ }
+ },
+
+ start: function () {
+ var $image = this.$element;
+ var $clone = this.$clone;
+
+ if (!this.isImg) {
+ $clone.off(EVENT_ERROR, this.stop);
+ $image = $clone;
+ }
+
+ getImageSize($image[0], $.proxy(function (naturalWidth, naturalHeight) {
+ $.extend(this.image, {
+ naturalWidth: naturalWidth,
+ naturalHeight: naturalHeight,
+ aspectRatio: naturalWidth / naturalHeight
+ });
+
+ this.isLoaded = true;
+ this.build();
+ }, this));
+ },
+
+ stop: function () {
+ this.$clone.remove();
+ this.$clone = null;
+ },
+
+ build: function () {
+ var options = this.options;
+ var $this = this.$element;
+ var $clone = this.$clone;
+ var $cropper;
+ var $cropBox;
+ var $face;
+
+ if (!this.isLoaded) {
+ return;
+ }
+
+ // Unbuild first when replace
+ if (this.isBuilt) {
+ this.unbuild();
+ }
+
+ // Create cropper elements
+ this.$container = $this.parent();
+ this.$cropper = $cropper = $(Cropper.TEMPLATE);
+ this.$canvas = $cropper.find('.cropper-canvas').append($clone);
+ this.$dragBox = $cropper.find('.cropper-drag-box');
+ this.$cropBox = $cropBox = $cropper.find('.cropper-crop-box');
+ this.$viewBox = $cropper.find('.cropper-view-box');
+ this.$face = $face = $cropBox.find('.cropper-face');
+
+ // Hide the original image
+ $this.addClass(CLASS_HIDDEN).after($cropper);
+
+ // Show the clone image if is hidden
+ if (!this.isImg) {
+ $clone.removeClass(CLASS_HIDE);
+ }
+
+ this.initPreview();
+ this.bind();
+
+ options.aspectRatio = max(0, options.aspectRatio) || NaN;
+ options.viewMode = max(0, min(3, round(options.viewMode))) || 0;
+
+ if (options.autoCrop) {
+ this.isCropped = true;
+
+ if (options.modal) {
+ this.$dragBox.addClass(CLASS_MODAL);
+ }
+ } else {
+ $cropBox.addClass(CLASS_HIDDEN);
+ }
+
+ if (!options.guides) {
+ $cropBox.find('.cropper-dashed').addClass(CLASS_HIDDEN);
+ }
+
+ if (!options.center) {
+ $cropBox.find('.cropper-center').addClass(CLASS_HIDDEN);
+ }
+
+ if (options.cropBoxMovable) {
+ $face.addClass(CLASS_MOVE).data(DATA_ACTION, ACTION_ALL);
+ }
+
+ if (!options.highlight) {
+ $face.addClass(CLASS_INVISIBLE);
+ }
+
+ if (options.background) {
+ $cropper.addClass(CLASS_BG);
+ }
+
+ if (!options.cropBoxResizable) {
+ $cropBox.find('.cropper-line, .cropper-point').addClass(CLASS_HIDDEN);
+ }
+
+ this.setDragMode(options.dragMode);
+ this.render();
+ this.isBuilt = true;
+ this.setData(options.data);
+ $this.one(EVENT_BUILT, options.built);
+
+ // Trigger the built event asynchronously to keep `data('cropper')` is defined
+ setTimeout($.proxy(function () {
+ this.trigger(EVENT_BUILT);
+ this.isCompleted = true;
+ }, this), 0);
+ },
+
+ unbuild: function () {
+ if (!this.isBuilt) {
+ return;
+ }
+
+ this.isBuilt = false;
+ this.isCompleted = false;
+ this.initialImage = null;
+
+ // Clear `initialCanvas` is necessary when replace
+ this.initialCanvas = null;
+ this.initialCropBox = null;
+ this.container = null;
+ this.canvas = null;
+
+ // Clear `cropBox` is necessary when replace
+ this.cropBox = null;
+ this.unbind();
+
+ this.resetPreview();
+ this.$preview = null;
+
+ this.$viewBox = null;
+ this.$cropBox = null;
+ this.$dragBox = null;
+ this.$canvas = null;
+ this.$container = null;
+
+ this.$cropper.remove();
+ this.$cropper = null;
+ },
+
+ render: function () {
+ this.initContainer();
+ this.initCanvas();
+ this.initCropBox();
+
+ this.renderCanvas();
+
+ if (this.isCropped) {
+ this.renderCropBox();
+ }
+ },
+
+ initContainer: function () {
+ var options = this.options;
+ var $this = this.$element;
+ var $container = this.$container;
+ var $cropper = this.$cropper;
+
+ $cropper.addClass(CLASS_HIDDEN);
+ $this.removeClass(CLASS_HIDDEN);
+
+ $cropper.css((this.container = {
+ width: max($container.width(), num(options.minContainerWidth) || 200),
+ height: max($container.height(), num(options.minContainerHeight) || 100)
+ }));
+
+ $this.addClass(CLASS_HIDDEN);
+ $cropper.removeClass(CLASS_HIDDEN);
+ },
+
+ // Canvas (image wrapper)
+ initCanvas: function () {
+ var viewMode = this.options.viewMode;
+ var container = this.container;
+ var containerWidth = container.width;
+ var containerHeight = container.height;
+ var image = this.image;
+ var imageNaturalWidth = image.naturalWidth;
+ var imageNaturalHeight = image.naturalHeight;
+ var is90Degree = abs(image.rotate) === 90;
+ var naturalWidth = is90Degree ? imageNaturalHeight : imageNaturalWidth;
+ var naturalHeight = is90Degree ? imageNaturalWidth : imageNaturalHeight;
+ var aspectRatio = naturalWidth / naturalHeight;
+ var canvasWidth = containerWidth;
+ var canvasHeight = containerHeight;
+ var canvas;
+
+ if (containerHeight * aspectRatio > containerWidth) {
+ if (viewMode === 3) {
+ canvasWidth = containerHeight * aspectRatio;
+ } else {
+ canvasHeight = containerWidth / aspectRatio;
+ }
+ } else {
+ if (viewMode === 3) {
+ canvasHeight = containerWidth / aspectRatio;
+ } else {
+ canvasWidth = containerHeight * aspectRatio;
+ }
+ }
+
+ canvas = {
+ naturalWidth: naturalWidth,
+ naturalHeight: naturalHeight,
+ aspectRatio: aspectRatio,
+ width: canvasWidth,
+ height: canvasHeight
+ };
+
+ canvas.oldLeft = canvas.left = (containerWidth - canvasWidth) / 2;
+ canvas.oldTop = canvas.top = (containerHeight - canvasHeight) / 2;
+
+ this.canvas = canvas;
+ this.isLimited = (viewMode === 1 || viewMode === 2);
+ this.limitCanvas(true, true);
+ this.initialImage = $.extend({}, image);
+ this.initialCanvas = $.extend({}, canvas);
+ },
+
+ limitCanvas: function (isSizeLimited, isPositionLimited) {
+ var options = this.options;
+ var viewMode = options.viewMode;
+ var container = this.container;
+ var containerWidth = container.width;
+ var containerHeight = container.height;
+ var canvas = this.canvas;
+ var aspectRatio = canvas.aspectRatio;
+ var cropBox = this.cropBox;
+ var isCropped = this.isCropped && cropBox;
+ var minCanvasWidth;
+ var minCanvasHeight;
+ var newCanvasLeft;
+ var newCanvasTop;
+
+ if (isSizeLimited) {
+ minCanvasWidth = num(options.minCanvasWidth) || 0;
+ minCanvasHeight = num(options.minCanvasHeight) || 0;
+
+ if (viewMode) {
+ if (viewMode > 1) {
+ minCanvasWidth = max(minCanvasWidth, containerWidth);
+ minCanvasHeight = max(minCanvasHeight, containerHeight);
+
+ if (viewMode === 3) {
+ if (minCanvasHeight * aspectRatio > minCanvasWidth) {
+ minCanvasWidth = minCanvasHeight * aspectRatio;
+ } else {
+ minCanvasHeight = minCanvasWidth / aspectRatio;
+ }
+ }
+ } else {
+ if (minCanvasWidth) {
+ minCanvasWidth = max(minCanvasWidth, isCropped ? cropBox.width : 0);
+ } else if (minCanvasHeight) {
+ minCanvasHeight = max(minCanvasHeight, isCropped ? cropBox.height : 0);
+ } else if (isCropped) {
+ minCanvasWidth = cropBox.width;
+ minCanvasHeight = cropBox.height;
+
+ if (minCanvasHeight * aspectRatio > minCanvasWidth) {
+ minCanvasWidth = minCanvasHeight * aspectRatio;
+ } else {
+ minCanvasHeight = minCanvasWidth / aspectRatio;
+ }
+ }
+ }
+ }
+
+ if (minCanvasWidth && minCanvasHeight) {
+ if (minCanvasHeight * aspectRatio > minCanvasWidth) {
+ minCanvasHeight = minCanvasWidth / aspectRatio;
+ } else {
+ minCanvasWidth = minCanvasHeight * aspectRatio;
+ }
+ } else if (minCanvasWidth) {
+ minCanvasHeight = minCanvasWidth / aspectRatio;
+ } else if (minCanvasHeight) {
+ minCanvasWidth = minCanvasHeight * aspectRatio;
+ }
+
+ canvas.minWidth = minCanvasWidth;
+ canvas.minHeight = minCanvasHeight;
+ canvas.maxWidth = Infinity;
+ canvas.maxHeight = Infinity;
+ }
+
+ if (isPositionLimited) {
+ if (viewMode) {
+ newCanvasLeft = containerWidth - canvas.width;
+ newCanvasTop = containerHeight - canvas.height;
+
+ canvas.minLeft = min(0, newCanvasLeft);
+ canvas.minTop = min(0, newCanvasTop);
+ canvas.maxLeft = max(0, newCanvasLeft);
+ canvas.maxTop = max(0, newCanvasTop);
+
+ if (isCropped && this.isLimited) {
+ canvas.minLeft = min(
+ cropBox.left,
+ cropBox.left + cropBox.width - canvas.width
+ );
+ canvas.minTop = min(
+ cropBox.top,
+ cropBox.top + cropBox.height - canvas.height
+ );
+ canvas.maxLeft = cropBox.left;
+ canvas.maxTop = cropBox.top;
+
+ if (viewMode === 2) {
+ if (canvas.width >= containerWidth) {
+ canvas.minLeft = min(0, newCanvasLeft);
+ canvas.maxLeft = max(0, newCanvasLeft);
+ }
+
+ if (canvas.height >= containerHeight) {
+ canvas.minTop = min(0, newCanvasTop);
+ canvas.maxTop = max(0, newCanvasTop);
+ }
+ }
+ }
+ } else {
+ canvas.minLeft = -canvas.width;
+ canvas.minTop = -canvas.height;
+ canvas.maxLeft = containerWidth;
+ canvas.maxTop = containerHeight;
+ }
+ }
+ },
+
+ renderCanvas: function (isChanged) {
+ var canvas = this.canvas;
+ var image = this.image;
+ var rotate = image.rotate;
+ var naturalWidth = image.naturalWidth;
+ var naturalHeight = image.naturalHeight;
+ var aspectRatio;
+ var rotated;
+
+ if (this.isRotated) {
+ this.isRotated = false;
+
+ // Computes rotated sizes with image sizes
+ rotated = getRotatedSizes({
+ width: image.width,
+ height: image.height,
+ degree: rotate
+ });
+
+ aspectRatio = rotated.width / rotated.height;
+
+ if (aspectRatio !== canvas.aspectRatio) {
+ canvas.left -= (rotated.width - canvas.width) / 2;
+ canvas.top -= (rotated.height - canvas.height) / 2;
+ canvas.width = rotated.width;
+ canvas.height = rotated.height;
+ canvas.aspectRatio = aspectRatio;
+ canvas.naturalWidth = naturalWidth;
+ canvas.naturalHeight = naturalHeight;
+
+ // Computes rotated sizes with natural image sizes
+ if (rotate % 180) {
+ rotated = getRotatedSizes({
+ width: naturalWidth,
+ height: naturalHeight,
+ degree: rotate
+ });
+
+ canvas.naturalWidth = rotated.width;
+ canvas.naturalHeight = rotated.height;
+ }
+
+ this.limitCanvas(true, false);
+ }
+ }
+
+ if (canvas.width > canvas.maxWidth || canvas.width < canvas.minWidth) {
+ canvas.left = canvas.oldLeft;
+ }
+
+ if (canvas.height > canvas.maxHeight || canvas.height < canvas.minHeight) {
+ canvas.top = canvas.oldTop;
+ }
+
+ canvas.width = min(max(canvas.width, canvas.minWidth), canvas.maxWidth);
+ canvas.height = min(max(canvas.height, canvas.minHeight), canvas.maxHeight);
+
+ this.limitCanvas(false, true);
+
+ canvas.oldLeft = canvas.left = min(max(canvas.left, canvas.minLeft), canvas.maxLeft);
+ canvas.oldTop = canvas.top = min(max(canvas.top, canvas.minTop), canvas.maxTop);
+
+ this.$canvas.css({
+ width: canvas.width,
+ height: canvas.height,
+ left: canvas.left,
+ top: canvas.top
+ });
+
+ this.renderImage();
+
+ if (this.isCropped && this.isLimited) {
+ this.limitCropBox(true, true);
+ }
+
+ if (isChanged) {
+ this.output();
+ }
+ },
+
+ renderImage: function (isChanged) {
+ var canvas = this.canvas;
+ var image = this.image;
+ var reversed;
+
+ if (image.rotate) {
+ reversed = getRotatedSizes({
+ width: canvas.width,
+ height: canvas.height,
+ degree: image.rotate,
+ aspectRatio: image.aspectRatio
+ }, true);
+ }
+
+ $.extend(image, reversed ? {
+ width: reversed.width,
+ height: reversed.height,
+ left: (canvas.width - reversed.width) / 2,
+ top: (canvas.height - reversed.height) / 2
+ } : {
+ width: canvas.width,
+ height: canvas.height,
+ left: 0,
+ top: 0
+ });
+
+ this.$clone.css({
+ width: image.width,
+ height: image.height,
+ marginLeft: image.left,
+ marginTop: image.top,
+ transform: getTransform(image)
+ });
+
+ if (isChanged) {
+ this.output();
+ }
+ },
+
+ initCropBox: function () {
+ var options = this.options;
+ var canvas = this.canvas;
+ var aspectRatio = options.aspectRatio;
+ var autoCropArea = num(options.autoCropArea) || 0.8;
+ var cropBox = {
+ width: canvas.width,
+ height: canvas.height
+ };
+
+ if (aspectRatio) {
+ if (canvas.height * aspectRatio > canvas.width) {
+ cropBox.height = cropBox.width / aspectRatio;
+ } else {
+ cropBox.width = cropBox.height * aspectRatio;
+ }
+ }
+
+ this.cropBox = cropBox;
+ this.limitCropBox(true, true);
+
+ // Initialize auto crop area
+ cropBox.width = min(max(cropBox.width, cropBox.minWidth), cropBox.maxWidth);
+ cropBox.height = min(max(cropBox.height, cropBox.minHeight), cropBox.maxHeight);
+
+ // The width of auto crop area must large than "minWidth", and the height too. (#164)
+ cropBox.width = max(cropBox.minWidth, cropBox.width * autoCropArea);
+ cropBox.height = max(cropBox.minHeight, cropBox.height * autoCropArea);
+ cropBox.oldLeft = cropBox.left = canvas.left + (canvas.width - cropBox.width) / 2;
+ cropBox.oldTop = cropBox.top = canvas.top + (canvas.height - cropBox.height) / 2;
+
+ this.initialCropBox = $.extend({}, cropBox);
+ },
+
+ limitCropBox: function (isSizeLimited, isPositionLimited) {
+ var options = this.options;
+ var aspectRatio = options.aspectRatio;
+ var container = this.container;
+ var containerWidth = container.width;
+ var containerHeight = container.height;
+ var canvas = this.canvas;
+ var cropBox = this.cropBox;
+ var isLimited = this.isLimited;
+ var minCropBoxWidth;
+ var minCropBoxHeight;
+ var maxCropBoxWidth;
+ var maxCropBoxHeight;
+
+ if (isSizeLimited) {
+ minCropBoxWidth = num(options.minCropBoxWidth) || 0;
+ minCropBoxHeight = num(options.minCropBoxHeight) || 0;
+
+ // The min/maxCropBoxWidth/Height must be less than containerWidth/Height
+ minCropBoxWidth = min(minCropBoxWidth, containerWidth);
+ minCropBoxHeight = min(minCropBoxHeight, containerHeight);
+ maxCropBoxWidth = min(containerWidth, isLimited ? canvas.width : containerWidth);
+ maxCropBoxHeight = min(containerHeight, isLimited ? canvas.height : containerHeight);
+
+ if (aspectRatio) {
+ if (minCropBoxWidth && minCropBoxHeight) {
+ if (minCropBoxHeight * aspectRatio > minCropBoxWidth) {
+ minCropBoxHeight = minCropBoxWidth / aspectRatio;
+ } else {
+ minCropBoxWidth = minCropBoxHeight * aspectRatio;
+ }
+ } else if (minCropBoxWidth) {
+ minCropBoxHeight = minCropBoxWidth / aspectRatio;
+ } else if (minCropBoxHeight) {
+ minCropBoxWidth = minCropBoxHeight * aspectRatio;
+ }
+
+ if (maxCropBoxHeight * aspectRatio > maxCropBoxWidth) {
+ maxCropBoxHeight = maxCropBoxWidth / aspectRatio;
+ } else {
+ maxCropBoxWidth = maxCropBoxHeight * aspectRatio;
+ }
+ }
+
+ // The minWidth/Height must be less than maxWidth/Height
+ cropBox.minWidth = min(minCropBoxWidth, maxCropBoxWidth);
+ cropBox.minHeight = min(minCropBoxHeight, maxCropBoxHeight);
+ cropBox.maxWidth = maxCropBoxWidth;
+ cropBox.maxHeight = maxCropBoxHeight;
+ }
+
+ if (isPositionLimited) {
+ if (isLimited) {
+ cropBox.minLeft = max(0, canvas.left);
+ cropBox.minTop = max(0, canvas.top);
+ cropBox.maxLeft = min(containerWidth, canvas.left + canvas.width) - cropBox.width;
+ cropBox.maxTop = min(containerHeight, canvas.top + canvas.height) - cropBox.height;
+ } else {
+ cropBox.minLeft = 0;
+ cropBox.minTop = 0;
+ cropBox.maxLeft = containerWidth - cropBox.width;
+ cropBox.maxTop = containerHeight - cropBox.height;
+ }
+ }
+ },
+
+ renderCropBox: function () {
+ var options = this.options;
+ var container = this.container;
+ var containerWidth = container.width;
+ var containerHeight = container.height;
+ var cropBox = this.cropBox;
+
+ if (cropBox.width > cropBox.maxWidth || cropBox.width < cropBox.minWidth) {
+ cropBox.left = cropBox.oldLeft;
+ }
+
+ if (cropBox.height > cropBox.maxHeight || cropBox.height < cropBox.minHeight) {
+ cropBox.top = cropBox.oldTop;
+ }
+
+ cropBox.width = min(max(cropBox.width, cropBox.minWidth), cropBox.maxWidth);
+ cropBox.height = min(max(cropBox.height, cropBox.minHeight), cropBox.maxHeight);
+
+ this.limitCropBox(false, true);
+
+ cropBox.oldLeft = cropBox.left = min(max(cropBox.left, cropBox.minLeft), cropBox.maxLeft);
+ cropBox.oldTop = cropBox.top = min(max(cropBox.top, cropBox.minTop), cropBox.maxTop);
+
+ if (options.movable && options.cropBoxMovable) {
+
+ // Turn to move the canvas when the crop box is equal to the container
+ this.$face.data(DATA_ACTION, (cropBox.width === containerWidth && cropBox.height === containerHeight) ? ACTION_MOVE : ACTION_ALL);
+ }
+
+ this.$cropBox.css({
+ width: cropBox.width,
+ height: cropBox.height,
+ left: cropBox.left,
+ top: cropBox.top
+ });
+
+ if (this.isCropped && this.isLimited) {
+ this.limitCanvas(true, true);
+ }
+
+ if (!this.isDisabled) {
+ this.output();
+ }
+ },
+
+ output: function () {
+ this.preview();
+
+ if (this.isCompleted) {
+ this.trigger(EVENT_CROP, this.getData());
+ } else if (!this.isBuilt) {
+
+ // Only trigger one crop event before complete
+ this.$element.one(EVENT_BUILT, $.proxy(function () {
+ this.trigger(EVENT_CROP, this.getData());
+ }, this));
+ }
+ },
+
+ initPreview: function () {
+ var crossOrigin = getCrossOrigin(this.crossOrigin);
+ var url = crossOrigin ? this.crossOriginUrl : this.url;
+ var $clone2;
+
+ this.$preview = $(this.options.preview);
+ this.$clone2 = $clone2 = $('<img' + crossOrigin + ' src="' + url + '">');
+ this.$viewBox.html($clone2);
+ this.$preview.each(function () {
+ var $this = $(this);
+
+ // Save the original size for recover
+ $this.data(DATA_PREVIEW, {
+ width: $this.width(),
+ height: $this.height(),
+ html: $this.html()
+ });
+
+ /**
+ * Override img element styles
+ * Add `display:block` to avoid margin top issue
+ * (Occur only when margin-top <= -height)
+ */
+ $this.html(
+ '<img' + crossOrigin + ' src="' + url + '" style="' +
+ 'display:block;width:100%;height:auto;' +
+ 'min-width:0!important;min-height:0!important;' +
+ 'max-width:none!important;max-height:none!important;' +
+ 'image-orientation:0deg!important;">'
+ );
+ });
+ },
+
+ resetPreview: function () {
+ this.$preview.each(function () {
+ var $this = $(this);
+ var data = $this.data(DATA_PREVIEW);
+
+ $this.css({
+ width: data.width,
+ height: data.height
+ }).html(data.html).removeData(DATA_PREVIEW);
+ });
+ },
+
+ preview: function () {
+ var image = this.image;
+ var canvas = this.canvas;
+ var cropBox = this.cropBox;
+ var cropBoxWidth = cropBox.width;
+ var cropBoxHeight = cropBox.height;
+ var width = image.width;
+ var height = image.height;
+ var left = cropBox.left - canvas.left - image.left;
+ var top = cropBox.top - canvas.top - image.top;
+
+ if (!this.isCropped || this.isDisabled) {
+ return;
+ }
+
+ this.$clone2.css({
+ width: width,
+ height: height,
+ marginLeft: -left,
+ marginTop: -top,
+ transform: getTransform(image)
+ });
+
+ this.$preview.each(function () {
+ var $this = $(this);
+ var data = $this.data(DATA_PREVIEW);
+ var originalWidth = data.width;
+ var originalHeight = data.height;
+ var newWidth = originalWidth;
+ var newHeight = originalHeight;
+ var ratio = 1;
+
+ if (cropBoxWidth) {
+ ratio = originalWidth / cropBoxWidth;
+ newHeight = cropBoxHeight * ratio;
+ }
+
+ if (cropBoxHeight && newHeight > originalHeight) {
+ ratio = originalHeight / cropBoxHeight;
+ newWidth = cropBoxWidth * ratio;
+ newHeight = originalHeight;
+ }
+
+ $this.css({
+ width: newWidth,
+ height: newHeight
+ }).find('img').css({
+ width: width * ratio,
+ height: height * ratio,
+ marginLeft: -left * ratio,
+ marginTop: -top * ratio,
+ transform: getTransform(image)
+ });
+ });
+ },
+
+ bind: function () {
+ var options = this.options;
+ var $this = this.$element;
+ var $cropper = this.$cropper;
+
+ if ($.isFunction(options.cropstart)) {
+ $this.on(EVENT_CROP_START, options.cropstart);
+ }
+
+ if ($.isFunction(options.cropmove)) {
+ $this.on(EVENT_CROP_MOVE, options.cropmove);
+ }
+
+ if ($.isFunction(options.cropend)) {
+ $this.on(EVENT_CROP_END, options.cropend);
+ }
+
+ if ($.isFunction(options.crop)) {
+ $this.on(EVENT_CROP, options.crop);
+ }
+
+ if ($.isFunction(options.zoom)) {
+ $this.on(EVENT_ZOOM, options.zoom);
+ }
+
+ $cropper.on(EVENT_MOUSE_DOWN, $.proxy(this.cropStart, this));
+
+ if (options.zoomable && options.zoomOnWheel) {
+ $cropper.on(EVENT_WHEEL, $.proxy(this.wheel, this));
+ }
+
+ if (options.toggleDragModeOnDblclick) {
+ $cropper.on(EVENT_DBLCLICK, $.proxy(this.dblclick, this));
+ }
+
+ $document.
+ on(EVENT_MOUSE_MOVE, (this._cropMove = proxy(this.cropMove, this))).
+ on(EVENT_MOUSE_UP, (this._cropEnd = proxy(this.cropEnd, this)));
+
+ if (options.responsive) {
+ $window.on(EVENT_RESIZE, (this._resize = proxy(this.resize, this)));
+ }
+ },
+
+ unbind: function () {
+ var options = this.options;
+ var $this = this.$element;
+ var $cropper = this.$cropper;
+
+ if ($.isFunction(options.cropstart)) {
+ $this.off(EVENT_CROP_START, options.cropstart);
+ }
+
+ if ($.isFunction(options.cropmove)) {
+ $this.off(EVENT_CROP_MOVE, options.cropmove);
+ }
+
+ if ($.isFunction(options.cropend)) {
+ $this.off(EVENT_CROP_END, options.cropend);
+ }
+
+ if ($.isFunction(options.crop)) {
+ $this.off(EVENT_CROP, options.crop);
+ }
+
+ if ($.isFunction(options.zoom)) {
+ $this.off(EVENT_ZOOM, options.zoom);
+ }
+
+ $cropper.off(EVENT_MOUSE_DOWN, this.cropStart);
+
+ if (options.zoomable && options.zoomOnWheel) {
+ $cropper.off(EVENT_WHEEL, this.wheel);
+ }
+
+ if (options.toggleDragModeOnDblclick) {
+ $cropper.off(EVENT_DBLCLICK, this.dblclick);
+ }
+
+ $document.
+ off(EVENT_MOUSE_MOVE, this._cropMove).
+ off(EVENT_MOUSE_UP, this._cropEnd);
+
+ if (options.responsive) {
+ $window.off(EVENT_RESIZE, this._resize);
+ }
+ },
+
+ resize: function () {
+ var restore = this.options.restore;
+ var $container = this.$container;
+ var container = this.container;
+ var canvasData;
+ var cropBoxData;
+ var ratio;
+
+ // Check `container` is necessary for IE8
+ if (this.isDisabled || !container) {
+ return;
+ }
+
+ ratio = $container.width() / container.width;
+
+ // Resize when width changed or height changed
+ if (ratio !== 1 || $container.height() !== container.height) {
+ if (restore) {
+ canvasData = this.getCanvasData();
+ cropBoxData = this.getCropBoxData();
+ }
+
+ this.render();
+
+ if (restore) {
+ this.setCanvasData($.each(canvasData, function (i, n) {
+ canvasData[i] = n * ratio;
+ }));
+ this.setCropBoxData($.each(cropBoxData, function (i, n) {
+ cropBoxData[i] = n * ratio;
+ }));
+ }
+ }
+ },
+
+ dblclick: function () {
+ if (this.isDisabled) {
+ return;
+ }
+
+ if (this.$dragBox.hasClass(CLASS_CROP)) {
+ this.setDragMode(ACTION_MOVE);
+ } else {
+ this.setDragMode(ACTION_CROP);
+ }
+ },
+
+ wheel: function (event) {
+ var e = event.originalEvent || event;
+ var ratio = num(this.options.wheelZoomRatio) || 0.1;
+ var delta = 1;
+
+ if (this.isDisabled) {
+ return;
+ }
+
+ event.preventDefault();
+
+ // Limit wheel speed to prevent zoom too fast
+ if (this.wheeling) {
+ return;
+ }
+
+ this.wheeling = true;
+
+ setTimeout($.proxy(function () {
+ this.wheeling = false;
+ }, this), 50);
+
+ if (e.deltaY) {
+ delta = e.deltaY > 0 ? 1 : -1;
+ } else if (e.wheelDelta) {
+ delta = -e.wheelDelta / 120;
+ } else if (e.detail) {
+ delta = e.detail > 0 ? 1 : -1;
+ }
+
+ this.zoom(-delta * ratio, event);
+ },
+
+ cropStart: function (event) {
+ var options = this.options;
+ var originalEvent = event.originalEvent;
+ var touches = originalEvent && originalEvent.touches;
+ var e = event;
+ var touchesLength;
+ var action;
+
+ if (this.isDisabled) {
+ return;
+ }
+
+ if (touches) {
+ touchesLength = touches.length;
+
+ if (touchesLength > 1) {
+ if (options.zoomable && options.zoomOnTouch && touchesLength === 2) {
+ e = touches[1];
+ this.startX2 = e.pageX;
+ this.startY2 = e.pageY;
+ action = ACTION_ZOOM;
+ } else {
+ return;
+ }
+ }
+
+ e = touches[0];
+ }
+
+ action = action || $(e.target).data(DATA_ACTION);
+
+ if (REGEXP_ACTIONS.test(action)) {
+ if (this.trigger(EVENT_CROP_START, {
+ originalEvent: originalEvent,
+ action: action
+ }).isDefaultPrevented()) {
+ return;
+ }
+
+ event.preventDefault();
+
+ this.action = action;
+ this.cropping = false;
+
+ // IE8 has `event.pageX/Y`, but not `event.originalEvent.pageX/Y`
+ // IE10 has `event.originalEvent.pageX/Y`, but not `event.pageX/Y`
+ this.startX = e.pageX || originalEvent && originalEvent.pageX;
+ this.startY = e.pageY || originalEvent && originalEvent.pageY;
+
+ if (action === ACTION_CROP) {
+ this.cropping = true;
+ this.$dragBox.addClass(CLASS_MODAL);
+ }
+ }
+ },
+
+ cropMove: function (event) {
+ var options = this.options;
+ var originalEvent = event.originalEvent;
+ var touches = originalEvent && originalEvent.touches;
+ var e = event;
+ var action = this.action;
+ var touchesLength;
+
+ if (this.isDisabled) {
+ return;
+ }
+
+ if (touches) {
+ touchesLength = touches.length;
+
+ if (touchesLength > 1) {
+ if (options.zoomable && options.zoomOnTouch && touchesLength === 2) {
+ e = touches[1];
+ this.endX2 = e.pageX;
+ this.endY2 = e.pageY;
+ } else {
+ return;
+ }
+ }
+
+ e = touches[0];
+ }
+
+ if (action) {
+ if (this.trigger(EVENT_CROP_MOVE, {
+ originalEvent: originalEvent,
+ action: action
+ }).isDefaultPrevented()) {
+ return;
+ }
+
+ event.preventDefault();
+
+ this.endX = e.pageX || originalEvent && originalEvent.pageX;
+ this.endY = e.pageY || originalEvent && originalEvent.pageY;
+
+ this.change(e.shiftKey, action === ACTION_ZOOM ? event : null);
+ }
+ },
+
+ cropEnd: function (event) {
+ var originalEvent = event.originalEvent;
+ var action = this.action;
+
+ if (this.isDisabled) {
+ return;
+ }
+
+ if (action) {
+ event.preventDefault();
+
+ if (this.cropping) {
+ this.cropping = false;
+ this.$dragBox.toggleClass(CLASS_MODAL, this.isCropped && this.options.modal);
+ }
+
+ this.action = '';
+
+ this.trigger(EVENT_CROP_END, {
+ originalEvent: originalEvent,
+ action: action
+ });
+ }
+ },
+
+ change: function (shiftKey, event) {
+ var options = this.options;
+ var aspectRatio = options.aspectRatio;
+ var action = this.action;
+ var container = this.container;
+ var canvas = this.canvas;
+ var cropBox = this.cropBox;
+ var width = cropBox.width;
+ var height = cropBox.height;
+ var left = cropBox.left;
+ var top = cropBox.top;
+ var right = left + width;
+ var bottom = top + height;
+ var minLeft = 0;
+ var minTop = 0;
+ var maxWidth = container.width;
+ var maxHeight = container.height;
+ var renderable = true;
+ var offset;
+ var range;
+
+ // Locking aspect ratio in "free mode" by holding shift key (#259)
+ if (!aspectRatio && shiftKey) {
+ aspectRatio = width && height ? width / height : 1;
+ }
+
+ if (this.limited) {
+ minLeft = cropBox.minLeft;
+ minTop = cropBox.minTop;
+ maxWidth = minLeft + min(container.width, canvas.left + canvas.width);
+ maxHeight = minTop + min(container.height, canvas.top + canvas.height);
+ }
+
+ range = {
+ x: this.endX - this.startX,
+ y: this.endY - this.startY
+ };
+
+ if (aspectRatio) {
+ range.X = range.y * aspectRatio;
+ range.Y = range.x / aspectRatio;
+ }
+
+ switch (action) {
+ // Move crop box
+ case ACTION_ALL:
+ left += range.x;
+ top += range.y;
+ break;
+
+ // Resize crop box
+ case ACTION_EAST:
+ if (range.x >= 0 && (right >= maxWidth || aspectRatio &&
+ (top <= minTop || bottom >= maxHeight))) {
+
+ renderable = false;
+ break;
+ }
+
+ width += range.x;
+
+ if (aspectRatio) {
+ height = width / aspectRatio;
+ top -= range.Y / 2;
+ }
+
+ if (width < 0) {
+ action = ACTION_WEST;
+ width = 0;
+ }
+
+ break;
+
+ case ACTION_NORTH:
+ if (range.y <= 0 && (top <= minTop || aspectRatio &&
+ (left <= minLeft || right >= maxWidth))) {
+
+ renderable = false;
+ break;
+ }
+
+ height -= range.y;
+ top += range.y;
+
+ if (aspectRatio) {
+ width = height * aspectRatio;
+ left += range.X / 2;
+ }
+
+ if (height < 0) {
+ action = ACTION_SOUTH;
+ height = 0;
+ }
+
+ break;
+
+ case ACTION_WEST:
+ if (range.x <= 0 && (left <= minLeft || aspectRatio &&
+ (top <= minTop || bottom >= maxHeight))) {
+
+ renderable = false;
+ break;
+ }
+
+ width -= range.x;
+ left += range.x;
+
+ if (aspectRatio) {
+ height = width / aspectRatio;
+ top += range.Y / 2;
+ }
+
+ if (width < 0) {
+ action = ACTION_EAST;
+ width = 0;
+ }
+
+ break;
+
+ case ACTION_SOUTH:
+ if (range.y >= 0 && (bottom >= maxHeight || aspectRatio &&
+ (left <= minLeft || right >= maxWidth))) {
+
+ renderable = false;
+ break;
+ }
+
+ height += range.y;
+
+ if (aspectRatio) {
+ width = height * aspectRatio;
+ left -= range.X / 2;
+ }
+
+ if (height < 0) {
+ action = ACTION_NORTH;
+ height = 0;
+ }
+
+ break;
+
+ case ACTION_NORTH_EAST:
+ if (aspectRatio) {
+ if (range.y <= 0 && (top <= minTop || right >= maxWidth)) {
+ renderable = false;
+ break;
+ }
+
+ height -= range.y;
+ top += range.y;
+ width = height * aspectRatio;
+ } else {
+ if (range.x >= 0) {
+ if (right < maxWidth) {
+ width += range.x;
+ } else if (range.y <= 0 && top <= minTop) {
+ renderable = false;
+ }
+ } else {
+ width += range.x;
+ }
+
+ if (range.y <= 0) {
+ if (top > minTop) {
+ height -= range.y;
+ top += range.y;
+ }
+ } else {
+ height -= range.y;
+ top += range.y;
+ }
+ }
+
+ if (width < 0 && height < 0) {
+ action = ACTION_SOUTH_WEST;
+ height = 0;
+ width = 0;
+ } else if (width < 0) {
+ action = ACTION_NORTH_WEST;
+ width = 0;
+ } else if (height < 0) {
+ action = ACTION_SOUTH_EAST;
+ height = 0;
+ }
+
+ break;
+
+ case ACTION_NORTH_WEST:
+ if (aspectRatio) {
+ if (range.y <= 0 && (top <= minTop || left <= minLeft)) {
+ renderable = false;
+ break;
+ }
+
+ height -= range.y;
+ top += range.y;
+ width = height * aspectRatio;
+ left += range.X;
+ } else {
+ if (range.x <= 0) {
+ if (left > minLeft) {
+ width -= range.x;
+ left += range.x;
+ } else if (range.y <= 0 && top <= minTop) {
+ renderable = false;
+ }
+ } else {
+ width -= range.x;
+ left += range.x;
+ }
+
+ if (range.y <= 0) {
+ if (top > minTop) {
+ height -= range.y;
+ top += range.y;
+ }
+ } else {
+ height -= range.y;
+ top += range.y;
+ }
+ }
+
+ if (width < 0 && height < 0) {
+ action = ACTION_SOUTH_EAST;
+ height = 0;
+ width = 0;
+ } else if (width < 0) {
+ action = ACTION_NORTH_EAST;
+ width = 0;
+ } else if (height < 0) {
+ action = ACTION_SOUTH_WEST;
+ height = 0;
+ }
+
+ break;
+
+ case ACTION_SOUTH_WEST:
+ if (aspectRatio) {
+ if (range.x <= 0 && (left <= minLeft || bottom >= maxHeight)) {
+ renderable = false;
+ break;
+ }
+
+ width -= range.x;
+ left += range.x;
+ height = width / aspectRatio;
+ } else {
+ if (range.x <= 0) {
+ if (left > minLeft) {
+ width -= range.x;
+ left += range.x;
+ } else if (range.y >= 0 && bottom >= maxHeight) {
+ renderable = false;
+ }
+ } else {
+ width -= range.x;
+ left += range.x;
+ }
+
+ if (range.y >= 0) {
+ if (bottom < maxHeight) {
+ height += range.y;
+ }
+ } else {
+ height += range.y;
+ }
+ }
+
+ if (width < 0 && height < 0) {
+ action = ACTION_NORTH_EAST;
+ height = 0;
+ width = 0;
+ } else if (width < 0) {
+ action = ACTION_SOUTH_EAST;
+ width = 0;
+ } else if (height < 0) {
+ action = ACTION_NORTH_WEST;
+ height = 0;
+ }
+
+ break;
+
+ case ACTION_SOUTH_EAST:
+ if (aspectRatio) {
+ if (range.x >= 0 && (right >= maxWidth || bottom >= maxHeight)) {
+ renderable = false;
+ break;
+ }
+
+ width += range.x;
+ height = width / aspectRatio;
+ } else {
+ if (range.x >= 0) {
+ if (right < maxWidth) {
+ width += range.x;
+ } else if (range.y >= 0 && bottom >= maxHeight) {
+ renderable = false;
+ }
+ } else {
+ width += range.x;
+ }
+
+ if (range.y >= 0) {
+ if (bottom < maxHeight) {
+ height += range.y;
+ }
+ } else {
+ height += range.y;
+ }
+ }
+
+ if (width < 0 && height < 0) {
+ action = ACTION_NORTH_WEST;
+ height = 0;
+ width = 0;
+ } else if (width < 0) {
+ action = ACTION_SOUTH_WEST;
+ width = 0;
+ } else if (height < 0) {
+ action = ACTION_NORTH_EAST;
+ height = 0;
+ }
+
+ break;
+
+ // Move canvas
+ case ACTION_MOVE:
+ this.move(range.x, range.y);
+ renderable = false;
+ break;
+
+ // Zoom canvas
+ case ACTION_ZOOM:
+ this.zoom((function (x1, y1, x2, y2) {
+ var z1 = sqrt(x1 * x1 + y1 * y1);
+ var z2 = sqrt(x2 * x2 + y2 * y2);
+
+ return (z2 - z1) / z1;
+ })(
+ abs(this.startX - this.startX2),
+ abs(this.startY - this.startY2),
+ abs(this.endX - this.endX2),
+ abs(this.endY - this.endY2)
+ ), event);
+ this.startX2 = this.endX2;
+ this.startY2 = this.endY2;
+ renderable = false;
+ break;
+
+ // Create crop box
+ case ACTION_CROP:
+ if (!range.x || !range.y) {
+ renderable = false;
+ break;
+ }
+
+ offset = this.$cropper.offset();
+ left = this.startX - offset.left;
+ top = this.startY - offset.top;
+ width = cropBox.minWidth;
+ height = cropBox.minHeight;
+
+ if (range.x > 0) {
+ action = range.y > 0 ? ACTION_SOUTH_EAST : ACTION_NORTH_EAST;
+ } else if (range.x < 0) {
+ left -= width;
+ action = range.y > 0 ? ACTION_SOUTH_WEST : ACTION_NORTH_WEST;
+ }
+
+ if (range.y < 0) {
+ top -= height;
+ }
+
+ // Show the crop box if is hidden
+ if (!this.isCropped) {
+ this.$cropBox.removeClass(CLASS_HIDDEN);
+ this.isCropped = true;
+
+ if (this.limited) {
+ this.limitCropBox(true, true);
+ }
+ }
+
+ break;
+
+ // No default
+ }
+
+ if (renderable) {
+ cropBox.width = width;
+ cropBox.height = height;
+ cropBox.left = left;
+ cropBox.top = top;
+ this.action = action;
+
+ this.renderCropBox();
+ }
+
+ // Override
+ this.startX = this.endX;
+ this.startY = this.endY;
+ },
+
+ // Show the crop box manually
+ crop: function () {
+ if (!this.isBuilt || this.isDisabled) {
+ return;
+ }
+
+ if (!this.isCropped) {
+ this.isCropped = true;
+ this.limitCropBox(true, true);
+
+ if (this.options.modal) {
+ this.$dragBox.addClass(CLASS_MODAL);
+ }
+
+ this.$cropBox.removeClass(CLASS_HIDDEN);
+ }
+
+ this.setCropBoxData(this.initialCropBox);
+ },
+
+ // Reset the image and crop box to their initial states
+ reset: function () {
+ if (!this.isBuilt || this.isDisabled) {
+ return;
+ }
+
+ this.image = $.extend({}, this.initialImage);
+ this.canvas = $.extend({}, this.initialCanvas);
+ this.cropBox = $.extend({}, this.initialCropBox);
+
+ this.renderCanvas();
+
+ if (this.isCropped) {
+ this.renderCropBox();
+ }
+ },
+
+ // Clear the crop box
+ clear: function () {
+ if (!this.isCropped || this.isDisabled) {
+ return;
+ }
+
+ $.extend(this.cropBox, {
+ left: 0,
+ top: 0,
+ width: 0,
+ height: 0
+ });
+
+ this.isCropped = false;
+ this.renderCropBox();
+
+ this.limitCanvas(true, true);
+
+ // Render canvas after crop box rendered
+ this.renderCanvas();
+
+ this.$dragBox.removeClass(CLASS_MODAL);
+ this.$cropBox.addClass(CLASS_HIDDEN);
+ },
+
+ /**
+ * Replace the image's src and rebuild the cropper
+ *
+ * @param {String} url
+ * @param {Boolean} onlyColorChanged (optional)
+ */
+ replace: function (url, onlyColorChanged) {
+ if (!this.isDisabled && url) {
+ if (this.isImg) {
+ this.$element.attr('src', url);
+ }
+
+ if (onlyColorChanged) {
+ this.url = url;
+ this.$clone.attr('src', url);
+
+ if (this.isBuilt) {
+ this.$preview.find('img').add(this.$clone2).attr('src', url);
+ }
+ } else {
+ if (this.isImg) {
+ this.isReplaced = true;
+ }
+
+ // Clear previous data
+ this.options.data = null;
+ this.load(url);
+ }
+ }
+ },
+
+ // Enable (unfreeze) the cropper
+ enable: function () {
+ if (this.isBuilt) {
+ this.isDisabled = false;
+ this.$cropper.removeClass(CLASS_DISABLED);
+ }
+ },
+
+ // Disable (freeze) the cropper
+ disable: function () {
+ if (this.isBuilt) {
+ this.isDisabled = true;
+ this.$cropper.addClass(CLASS_DISABLED);
+ }
+ },
+
+ // Destroy the cropper and remove the instance from the image
+ destroy: function () {
+ var $this = this.$element;
+
+ if (this.isLoaded) {
+ if (this.isImg && this.isReplaced) {
+ $this.attr('src', this.originalUrl);
+ }
+
+ this.unbuild();
+ $this.removeClass(CLASS_HIDDEN);
+ } else {
+ if (this.isImg) {
+ $this.off(EVENT_LOAD, this.start);
+ } else if (this.$clone) {
+ this.$clone.remove();
+ }
+ }
+
+ $this.removeData(NAMESPACE);
+ },
+
+ /**
+ * Move the canvas with relative offsets
+ *
+ * @param {Number} offsetX
+ * @param {Number} offsetY (optional)
+ */
+ move: function (offsetX, offsetY) {
+ var canvas = this.canvas;
+
+ this.moveTo(
+ isUndefined(offsetX) ? offsetX : canvas.left + num(offsetX),
+ isUndefined(offsetY) ? offsetY : canvas.top + num(offsetY)
+ );
+ },
+
+ /**
+ * Move the canvas to an absolute point
+ *
+ * @param {Number} x
+ * @param {Number} y (optional)
+ */
+ moveTo: function (x, y) {
+ var canvas = this.canvas;
+ var isChanged = false;
+
+ // If "y" is not present, its default value is "x"
+ if (isUndefined(y)) {
+ y = x;
+ }
+
+ x = num(x);
+ y = num(y);
+
+ if (this.isBuilt && !this.isDisabled && this.options.movable) {
+ if (isNumber(x)) {
+ canvas.left = x;
+ isChanged = true;
+ }
+
+ if (isNumber(y)) {
+ canvas.top = y;
+ isChanged = true;
+ }
+
+ if (isChanged) {
+ this.renderCanvas(true);
+ }
+ }
+ },
+
+ /**
+ * Zoom the canvas with a relative ratio
+ *
+ * @param {Number} ratio
+ * @param {jQuery Event} _event (private)
+ */
+ zoom: function (ratio, _event) {
+ var canvas = this.canvas;
+
+ ratio = num(ratio);
+
+ if (ratio < 0) {
+ ratio = 1 / (1 - ratio);
+ } else {
+ ratio = 1 + ratio;
+ }
+
+ this.zoomTo(canvas.width * ratio / canvas.naturalWidth, _event);
+ },
+
+ /**
+ * Zoom the canvas to an absolute ratio
+ *
+ * @param {Number} ratio
+ * @param {jQuery Event} _event (private)
+ */
+ zoomTo: function (ratio, _event) {
+ var options = this.options;
+ var canvas = this.canvas;
+ var width = canvas.width;
+ var height = canvas.height;
+ var naturalWidth = canvas.naturalWidth;
+ var naturalHeight = canvas.naturalHeight;
+ var originalEvent;
+ var newWidth;
+ var newHeight;
+ var offset;
+ var center;
+
+ ratio = num(ratio);
+
+ if (ratio >= 0 && this.isBuilt && !this.isDisabled && options.zoomable) {
+ newWidth = naturalWidth * ratio;
+ newHeight = naturalHeight * ratio;
+
+ if (_event) {
+ originalEvent = _event.originalEvent;
+ }
+
+ if (this.trigger(EVENT_ZOOM, {
+ originalEvent: originalEvent,
+ oldRatio: width / naturalWidth,
+ ratio: newWidth / naturalWidth
+ }).isDefaultPrevented()) {
+ return;
+ }
+
+ if (originalEvent) {
+ offset = this.$cropper.offset();
+ center = originalEvent.touches ? getTouchesCenter(originalEvent.touches) : {
+ pageX: _event.pageX || originalEvent.pageX || 0,
+ pageY: _event.pageY || originalEvent.pageY || 0
+ };
+
+ // Zoom from the triggering point of the event
+ canvas.left -= (newWidth - width) * (
+ ((center.pageX - offset.left) - canvas.left) / width
+ );
+ canvas.top -= (newHeight - height) * (
+ ((center.pageY - offset.top) - canvas.top) / height
+ );
+ } else {
+
+ // Zoom from the center of the canvas
+ canvas.left -= (newWidth - width) / 2;
+ canvas.top -= (newHeight - height) / 2;
+ }
+
+ canvas.width = newWidth;
+ canvas.height = newHeight;
+ this.renderCanvas(true);
+ }
+ },
+
+ /**
+ * Rotate the canvas with a relative degree
+ *
+ * @param {Number} degree
+ */
+ rotate: function (degree) {
+ this.rotateTo((this.image.rotate || 0) + num(degree));
+ },
+
+ /**
+ * Rotate the canvas to an absolute degree
+ * https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function#rotate()
+ *
+ * @param {Number} degree
+ */
+ rotateTo: function (degree) {
+ degree = num(degree);
+
+ if (isNumber(degree) && this.isBuilt && !this.isDisabled && this.options.rotatable) {
+ this.image.rotate = degree % 360;
+ this.isRotated = true;
+ this.renderCanvas(true);
+ }
+ },
+
+ /**
+ * Scale the image
+ * https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function#scale()
+ *
+ * @param {Number} scaleX
+ * @param {Number} scaleY (optional)
+ */
+ scale: function (scaleX, scaleY) {
+ var image = this.image;
+ var isChanged = false;
+
+ // If "scaleY" is not present, its default value is "scaleX"
+ if (isUndefined(scaleY)) {
+ scaleY = scaleX;
+ }
+
+ scaleX = num(scaleX);
+ scaleY = num(scaleY);
+
+ if (this.isBuilt && !this.isDisabled && this.options.scalable) {
+ if (isNumber(scaleX)) {
+ image.scaleX = scaleX;
+ isChanged = true;
+ }
+
+ if (isNumber(scaleY)) {
+ image.scaleY = scaleY;
+ isChanged = true;
+ }
+
+ if (isChanged) {
+ this.renderImage(true);
+ }
+ }
+ },
+
+ /**
+ * Scale the abscissa of the image
+ *
+ * @param {Number} scaleX
+ */
+ scaleX: function (scaleX) {
+ var scaleY = this.image.scaleY;
+
+ this.scale(scaleX, isNumber(scaleY) ? scaleY : 1);
+ },
+
+ /**
+ * Scale the ordinate of the image
+ *
+ * @param {Number} scaleY
+ */
+ scaleY: function (scaleY) {
+ var scaleX = this.image.scaleX;
+
+ this.scale(isNumber(scaleX) ? scaleX : 1, scaleY);
+ },
+
+ /**
+ * Get the cropped area position and size data (base on the original image)
+ *
+ * @param {Boolean} isRounded (optional)
+ * @return {Object} data
+ */
+ getData: function (isRounded) {
+ var options = this.options;
+ var image = this.image;
+ var canvas = this.canvas;
+ var cropBox = this.cropBox;
+ var ratio;
+ var data;
+
+ if (this.isBuilt && this.isCropped) {
+ data = {
+ x: cropBox.left - canvas.left,
+ y: cropBox.top - canvas.top,
+ width: cropBox.width,
+ height: cropBox.height
+ };
+
+ ratio = image.width / image.naturalWidth;
+
+ $.each(data, function (i, n) {
+ n = n / ratio;
+ data[i] = isRounded ? round(n) : n;
+ });
+
+ } else {
+ data = {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0
+ };
+ }
+
+ if (options.rotatable) {
+ data.rotate = image.rotate || 0;
+ }
+
+ if (options.scalable) {
+ data.scaleX = image.scaleX || 1;
+ data.scaleY = image.scaleY || 1;
+ }
+
+ return data;
+ },
+
+ /**
+ * Set the cropped area position and size with new data
+ *
+ * @param {Object} data
+ */
+ setData: function (data) {
+ var options = this.options;
+ var image = this.image;
+ var canvas = this.canvas;
+ var cropBoxData = {};
+ var isRotated;
+ var isScaled;
+ var ratio;
+
+ if ($.isFunction(data)) {
+ data = data.call(this.element);
+ }
+
+ if (this.isBuilt && !this.isDisabled && $.isPlainObject(data)) {
+ if (options.rotatable) {
+ if (isNumber(data.rotate) && data.rotate !== image.rotate) {
+ image.rotate = data.rotate;
+ this.isRotated = isRotated = true;
+ }
+ }
+
+ if (options.scalable) {
+ if (isNumber(data.scaleX) && data.scaleX !== image.scaleX) {
+ image.scaleX = data.scaleX;
+ isScaled = true;
+ }
+
+ if (isNumber(data.scaleY) && data.scaleY !== image.scaleY) {
+ image.scaleY = data.scaleY;
+ isScaled = true;
+ }
+ }
+
+ if (isRotated) {
+ this.renderCanvas();
+ } else if (isScaled) {
+ this.renderImage();
+ }
+
+ ratio = image.width / image.naturalWidth;
+
+ if (isNumber(data.x)) {
+ cropBoxData.left = data.x * ratio + canvas.left;
+ }
+
+ if (isNumber(data.y)) {
+ cropBoxData.top = data.y * ratio + canvas.top;
+ }
+
+ if (isNumber(data.width)) {
+ cropBoxData.width = data.width * ratio;
+ }
+
+ if (isNumber(data.height)) {
+ cropBoxData.height = data.height * ratio;
+ }
+
+ this.setCropBoxData(cropBoxData);
+ }
+ },
+
+ /**
+ * Get the container size data
+ *
+ * @return {Object} data
+ */
+ getContainerData: function () {
+ return this.isBuilt ? this.container : {};
+ },
+
+ /**
+ * Get the image position and size data
+ *
+ * @return {Object} data
+ */
+ getImageData: function () {
+ return this.isLoaded ? this.image : {};
+ },
+
+ /**
+ * Get the canvas position and size data
+ *
+ * @return {Object} data
+ */
+ getCanvasData: function () {
+ var canvas = this.canvas;
+ var data = {};
+
+ if (this.isBuilt) {
+ $.each([
+ 'left',
+ 'top',
+ 'width',
+ 'height',
+ 'naturalWidth',
+ 'naturalHeight'
+ ], function (i, n) {
+ data[n] = canvas[n];
+ });
+ }
+
+ return data;
+ },
+
+ /**
+ * Set the canvas position and size with new data
+ *
+ * @param {Object} data
+ */
+ setCanvasData: function (data) {
+ var canvas = this.canvas;
+ var aspectRatio = canvas.aspectRatio;
+
+ if ($.isFunction(data)) {
+ data = data.call(this.$element);
+ }
+
+ if (this.isBuilt && !this.isDisabled && $.isPlainObject(data)) {
+ if (isNumber(data.left)) {
+ canvas.left = data.left;
+ }
+
+ if (isNumber(data.top)) {
+ canvas.top = data.top;
+ }
+
+ if (isNumber(data.width)) {
+ canvas.width = data.width;
+ canvas.height = data.width / aspectRatio;
+ } else if (isNumber(data.height)) {
+ canvas.height = data.height;
+ canvas.width = data.height * aspectRatio;
+ }
+
+ this.renderCanvas(true);
+ }
+ },
+
+ /**
+ * Get the crop box position and size data
+ *
+ * @return {Object} data
+ */
+ getCropBoxData: function () {
+ var cropBox = this.cropBox;
+ var data;
+
+ if (this.isBuilt && this.isCropped) {
+ data = {
+ left: cropBox.left,
+ top: cropBox.top,
+ width: cropBox.width,
+ height: cropBox.height
+ };
+ }
+
+ return data || {};
+ },
+
+ /**
+ * Set the crop box position and size with new data
+ *
+ * @param {Object} data
+ */
+ setCropBoxData: function (data) {
+ var cropBox = this.cropBox;
+ var aspectRatio = this.options.aspectRatio;
+ var isWidthChanged;
+ var isHeightChanged;
+
+ if ($.isFunction(data)) {
+ data = data.call(this.$element);
+ }
+
+ if (this.isBuilt && this.isCropped && !this.isDisabled && $.isPlainObject(data)) {
+
+ if (isNumber(data.left)) {
+ cropBox.left = data.left;
+ }
+
+ if (isNumber(data.top)) {
+ cropBox.top = data.top;
+ }
+
+ if (isNumber(data.width)) {
+ isWidthChanged = true;
+ cropBox.width = data.width;
+ }
+
+ if (isNumber(data.height)) {
+ isHeightChanged = true;
+ cropBox.height = data.height;
+ }
+
+ if (aspectRatio) {
+ if (isWidthChanged) {
+ cropBox.height = cropBox.width / aspectRatio;
+ } else if (isHeightChanged) {
+ cropBox.width = cropBox.height * aspectRatio;
+ }
+ }
+
+ this.renderCropBox();
+ }
+ },
+
+ /**
+ * Get a canvas drawn the cropped image
+ *
+ * @param {Object} options (optional)
+ * @return {HTMLCanvasElement} canvas
+ */
+ getCroppedCanvas: function (options) {
+ var originalWidth;
+ var originalHeight;
+ var canvasWidth;
+ var canvasHeight;
+ var scaledWidth;
+ var scaledHeight;
+ var scaledRatio;
+ var aspectRatio;
+ var canvas;
+ var context;
+ var data;
+
+ if (!this.isBuilt || !this.isCropped || !SUPPORT_CANVAS) {
+ return;
+ }
+
+ if (!$.isPlainObject(options)) {
+ options = {};
+ }
+
+ data = this.getData();
+ originalWidth = data.width;
+ originalHeight = data.height;
+ aspectRatio = originalWidth / originalHeight;
+
+ if ($.isPlainObject(options)) {
+ scaledWidth = options.width;
+ scaledHeight = options.height;
+
+ if (scaledWidth) {
+ scaledHeight = scaledWidth / aspectRatio;
+ scaledRatio = scaledWidth / originalWidth;
+ } else if (scaledHeight) {
+ scaledWidth = scaledHeight * aspectRatio;
+ scaledRatio = scaledHeight / originalHeight;
+ }
+ }
+
+ // The canvas element will use `Math.floor` on a float number, so floor first
+ canvasWidth = floor(scaledWidth || originalWidth);
+ canvasHeight = floor(scaledHeight || originalHeight);
+
+ canvas = $('<canvas>')[0];
+ canvas.width = canvasWidth;
+ canvas.height = canvasHeight;
+ context = canvas.getContext('2d');
+
+ if (options.fillColor) {
+ context.fillStyle = options.fillColor;
+ context.fillRect(0, 0, canvasWidth, canvasHeight);
+ }
+
+ // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D.drawImage
+ context.drawImage.apply(context, (function () {
+ var source = getSourceCanvas(this.$clone[0], this.image);
+ var sourceWidth = source.width;
+ var sourceHeight = source.height;
+ var canvas = this.canvas;
+ var params = [source];
+
+ // Source canvas
+ var srcX = data.x + canvas.naturalWidth * (abs(data.scaleX || 1) - 1) / 2;
+ var srcY = data.y + canvas.naturalHeight * (abs(data.scaleY || 1) - 1) / 2;
+ var srcWidth;
+ var srcHeight;
+
+ // Destination canvas
+ var dstX;
+ var dstY;
+ var dstWidth;
+ var dstHeight;
+
+ if (srcX <= -originalWidth || srcX > sourceWidth) {
+ srcX = srcWidth = dstX = dstWidth = 0;
+ } else if (srcX <= 0) {
+ dstX = -srcX;
+ srcX = 0;
+ srcWidth = dstWidth = min(sourceWidth, originalWidth + srcX);
+ } else if (srcX <= sourceWidth) {
+ dstX = 0;
+ srcWidth = dstWidth = min(originalWidth, sourceWidth - srcX);
+ }
+
+ if (srcWidth <= 0 || srcY <= -originalHeight || srcY > sourceHeight) {
+ srcY = srcHeight = dstY = dstHeight = 0;
+ } else if (srcY <= 0) {
+ dstY = -srcY;
+ srcY = 0;
+ srcHeight = dstHeight = min(sourceHeight, originalHeight + srcY);
+ } else if (srcY <= sourceHeight) {
+ dstY = 0;
+ srcHeight = dstHeight = min(originalHeight, sourceHeight - srcY);
+ }
+
+ // All the numerical parameters should be integer for `drawImage` (#476)
+ params.push(floor(srcX), floor(srcY), floor(srcWidth), floor(srcHeight));
+
+ // Scale destination sizes
+ if (scaledRatio) {
+ dstX *= scaledRatio;
+ dstY *= scaledRatio;
+ dstWidth *= scaledRatio;
+ dstHeight *= scaledRatio;
+ }
+
+ // Avoid "IndexSizeError" in IE and Firefox
+ if (dstWidth > 0 && dstHeight > 0) {
+ params.push(floor(dstX), floor(dstY), floor(dstWidth), floor(dstHeight));
+ }
+
+ return params;
+ }).call(this));
+
+ return canvas;
+ },
+
+ /**
+ * Change the aspect ratio of the crop box
+ *
+ * @param {Number} aspectRatio
+ */
+ setAspectRatio: function (aspectRatio) {
+ var options = this.options;
+
+ if (!this.isDisabled && !isUndefined(aspectRatio)) {
+
+ // 0 -> NaN
+ options.aspectRatio = max(0, aspectRatio) || NaN;
+
+ if (this.isBuilt) {
+ this.initCropBox();
+
+ if (this.isCropped) {
+ this.renderCropBox();
+ }
+ }
+ }
+ },
+
+ /**
+ * Change the drag mode
+ *
+ * @param {String} mode (optional)
+ */
+ setDragMode: function (mode) {
+ var options = this.options;
+ var croppable;
+ var movable;
+
+ if (this.isLoaded && !this.isDisabled) {
+ croppable = mode === ACTION_CROP;
+ movable = options.movable && mode === ACTION_MOVE;
+ mode = (croppable || movable) ? mode : ACTION_NONE;
+
+ this.$dragBox.
+ data(DATA_ACTION, mode).
+ toggleClass(CLASS_CROP, croppable).
+ toggleClass(CLASS_MOVE, movable);
+
+ if (!options.cropBoxMovable) {
+
+ // Sync drag mode to crop box when it is not movable(#300)
+ this.$face.
+ data(DATA_ACTION, mode).
+ toggleClass(CLASS_CROP, croppable).
+ toggleClass(CLASS_MOVE, movable);
+ }
+ }
+ }
+ };
+
+ Cropper.DEFAULTS = {
+
+ // Define the view mode of the cropper
+ viewMode: 0, // 0, 1, 2, 3
+
+ // Define the dragging mode of the cropper
+ dragMode: 'crop', // 'crop', 'move' or 'none'
+
+ // Define the aspect ratio of the crop box
+ aspectRatio: NaN,
+
+ // An object with the previous cropping result data
+ data: null,
+
+ // A jQuery selector for adding extra containers to preview
+ preview: '',
+
+ // Re-render the cropper when resize the window
+ responsive: true,
+
+ // Restore the cropped area after resize the window
+ restore: true,
+
+ // Check if the current image is a cross-origin image
+ checkCrossOrigin: true,
+
+ // Check the current image's Exif Orientation information
+ checkOrientation: true,
+
+ // Show the black modal
+ modal: true,
+
+ // Show the dashed lines for guiding
+ guides: true,
+
+ // Show the center indicator for guiding
+ center: true,
+
+ // Show the white modal to highlight the crop box
+ highlight: true,
+
+ // Show the grid background
+ background: true,
+
+ // Enable to crop the image automatically when initialize
+ autoCrop: true,
+
+ // Define the percentage of automatic cropping area when initializes
+ autoCropArea: 0.8,
+
+ // Enable to move the image
+ movable: true,
+
+ // Enable to rotate the image
+ rotatable: true,
+
+ // Enable to scale the image
+ scalable: true,
+
+ // Enable to zoom the image
+ zoomable: true,
+
+ // Enable to zoom the image by dragging touch
+ zoomOnTouch: true,
+
+ // Enable to zoom the image by wheeling mouse
+ zoomOnWheel: true,
+
+ // Define zoom ratio when zoom the image by wheeling mouse
+ wheelZoomRatio: 0.1,
+
+ // Enable to move the crop box
+ cropBoxMovable: true,
+
+ // Enable to resize the crop box
+ cropBoxResizable: true,
+
+ // Toggle drag mode between "crop" and "move" when click twice on the cropper
+ toggleDragModeOnDblclick: true,
+
+ // Size limitation
+ minCanvasWidth: 0,
+ minCanvasHeight: 0,
+ minCropBoxWidth: 0,
+ minCropBoxHeight: 0,
+ minContainerWidth: 200,
+ minContainerHeight: 100,
+
+ // Shortcuts of events
+ build: null,
+ built: null,
+ cropstart: null,
+ cropmove: null,
+ cropend: null,
+ crop: null,
+ zoom: null
+ };
+
+ Cropper.setDefaults = function (options) {
+ $.extend(Cropper.DEFAULTS, options);
+ };
+
+ Cropper.TEMPLATE = (
+ '<div class="cropper-container">' +
+ '<div class="cropper-wrap-box">' +
+ '<div class="cropper-canvas"></div>' +
+ '</div>' +
+ '<div class="cropper-drag-box"></div>' +
+ '<div class="cropper-crop-box">' +
+ '<span class="cropper-view-box"></span>' +
+ '<span class="cropper-dashed dashed-h"></span>' +
+ '<span class="cropper-dashed dashed-v"></span>' +
+ '<span class="cropper-center"></span>' +
+ '<span class="cropper-face"></span>' +
+ '<span class="cropper-line line-e" data-action="e"></span>' +
+ '<span class="cropper-line line-n" data-action="n"></span>' +
+ '<span class="cropper-line line-w" data-action="w"></span>' +
+ '<span class="cropper-line line-s" data-action="s"></span>' +
+ '<span class="cropper-point point-e" data-action="e"></span>' +
+ '<span class="cropper-point point-n" data-action="n"></span>' +
+ '<span class="cropper-point point-w" data-action="w"></span>' +
+ '<span class="cropper-point point-s" data-action="s"></span>' +
+ '<span class="cropper-point point-ne" data-action="ne"></span>' +
+ '<span class="cropper-point point-nw" data-action="nw"></span>' +
+ '<span class="cropper-point point-sw" data-action="sw"></span>' +
+ '<span class="cropper-point point-se" data-action="se"></span>' +
+ '</div>' +
+ '</div>'
+ );
+
+ // Save the other cropper
+ Cropper.other = $.fn.cropper;
+
+ // Register as jQuery plugin
+ $.fn.cropper = function (option) {
+ var args = toArray(arguments, 1);
+ var result;
+
+ this.each(function () {
+ var $this = $(this);
+ var data = $this.data(NAMESPACE);
+ var options;
+ var fn;
+
+ if (!data) {
+ if (/destroy/.test(option)) {
+ return;
+ }
+
+ options = $.extend({}, $this.data(), $.isPlainObject(option) && option);
+ $this.data(NAMESPACE, (data = new Cropper(this, options)));
+ }
+
+ if (typeof option === 'string' && $.isFunction(fn = data[option])) {
+ result = fn.apply(data, args);
+ }
+ });
+
+ return isUndefined(result) ? this : result;
+ };
+
+ $.fn.cropper.Constructor = Cropper;
+ $.fn.cropper.setDefaults = Cropper.setDefaults;
+
+ // No conflict
+ $.fn.cropper.noConflict = function () {
+ $.fn.cropper = Cropper.other;
+ return this;
+ };
+
+});
diff --git a/vendor/assets/stylesheets/animate.css b/vendor/assets/stylesheets/animate.css
new file mode 100644
index 00000000000..b6f61295392
--- /dev/null
+++ b/vendor/assets/stylesheets/animate.css
@@ -0,0 +1,11 @@
+@charset "UTF-8";
+
+/*!
+ * animate.css -http://daneden.me/animate
+ * Version - 3.5.1
+ * Licensed under the MIT license - http://opensource.org/licenses/MIT
+ *
+ * Copyright (c) 2016 Daniel Eden
+ */
+
+.animated{-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-fill-mode:both;animation-fill-mode:both}.animated.infinite{-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.animated.hinge{-webkit-animation-duration:2s;animation-duration:2s}.animated.bounceIn,.animated.bounceOut,.animated.flipOutX,.animated.flipOutY{-webkit-animation-duration:.75s;animation-duration:.75s}@-webkit-keyframes bounce{0%,20%,53%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-transform:translate3d(0,-30px,0);transform:translate3d(0,-30px,0)}40%,43%,70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06)}70%{-webkit-transform:translate3d(0,-15px,0);transform:translate3d(0,-15px,0)}90%{-webkit-transform:translate3d(0,-4px,0);transform:translate3d(0,-4px,0)}}@keyframes bounce{0%,20%,53%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-transform:translate3d(0,-30px,0);transform:translate3d(0,-30px,0)}40%,43%,70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06)}70%{-webkit-transform:translate3d(0,-15px,0);transform:translate3d(0,-15px,0)}90%{-webkit-transform:translate3d(0,-4px,0);transform:translate3d(0,-4px,0)}}.bounce{-webkit-animation-name:bounce;animation-name:bounce;-webkit-transform-origin:center bottom;transform-origin:center bottom}@-webkit-keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}@keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}.flash{-webkit-animation-name:flash;animation-name:flash}@-webkit-keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.pulse{-webkit-animation-name:pulse;animation-name:pulse}@-webkit-keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.rubberBand{-webkit-animation-name:rubberBand;animation-name:rubberBand}@-webkit-keyframes shake{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}@keyframes shake{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}.shake{-webkit-animation-name:shake;animation-name:shake}@-webkit-keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}.headShake{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-name:headShake;animation-name:headShake}@-webkit-keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}.swing{-webkit-transform-origin:top center;transform-origin:top center;-webkit-animation-name:swing;animation-name:swing}@-webkit-keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.tada{-webkit-animation-name:tada;animation-name:tada}@-webkit-keyframes wobble{0%{-webkit-transform:none;transform:none}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:none;transform:none}}@keyframes wobble{0%{-webkit-transform:none;transform:none}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:none;transform:none}}.wobble{-webkit-animation-name:wobble;animation-name:wobble}@-webkit-keyframes jello{0%,11.1%,to{-webkit-transform:none;transform:none}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}@keyframes jello{0%,11.1%,to{-webkit-transform:none;transform:none}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}.jello{-webkit-animation-name:jello;animation-name:jello;-webkit-transform-origin:center;transform-origin:center}@-webkit-keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}.bounceIn{-webkit-animation-name:bounceIn;animation-name:bounceIn}@-webkit-keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0);transform:translate3d(0,-3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0);transform:translate3d(0,25px,0)}75%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}90%{-webkit-transform:translate3d(0,5px,0);transform:translate3d(0,5px,0)}to{-webkit-transform:none;transform:none}}@keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0);transform:translate3d(0,-3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0);transform:translate3d(0,25px,0)}75%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}90%{-webkit-transform:translate3d(0,5px,0);transform:translate3d(0,5px,0)}to{-webkit-transform:none;transform:none}}.bounceInDown{-webkit-animation-name:bounceInDown;animation-name:bounceInDown}@-webkit-keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0);transform:translate3d(-3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0);transform:translate3d(25px,0,0)}75%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}90%{-webkit-transform:translate3d(5px,0,0);transform:translate3d(5px,0,0)}to{-webkit-transform:none;transform:none}}@keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0);transform:translate3d(-3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0);transform:translate3d(25px,0,0)}75%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}90%{-webkit-transform:translate3d(5px,0,0);transform:translate3d(5px,0,0)}to{-webkit-transform:none;transform:none}}.bounceInLeft{-webkit-animation-name:bounceInLeft;animation-name:bounceInLeft}@-webkit-keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}to{-webkit-transform:none;transform:none}}@keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}to{-webkit-transform:none;transform:none}}.bounceInRight{-webkit-animation-name:bounceInRight;animation-name:bounceInRight}@-webkit-keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0);transform:translate3d(0,3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}75%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}90%{-webkit-transform:translate3d(0,-5px,0);transform:translate3d(0,-5px,0)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0);transform:translate3d(0,3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}75%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}90%{-webkit-transform:translate3d(0,-5px,0);transform:translate3d(0,-5px,0)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.bounceInUp{-webkit-animation-name:bounceInUp;animation-name:bounceInUp}@-webkit-keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}@keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}.bounceOut{-webkit-animation-name:bounceOut;animation-name:bounceOut}@-webkit-keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}.bounceOutDown{-webkit-animation-name:bounceOutDown;animation-name:bounceOutDown}@-webkit-keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0);transform:translate3d(20px,0,0)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0);transform:translate3d(20px,0,0)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}.bounceOutLeft{-webkit-animation-name:bounceOutLeft;animation-name:bounceOutLeft}@-webkit-keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.bounceOutRight{-webkit-animation-name:bounceOutRight;animation-name:bounceOutRight}@-webkit-keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0);transform:translate3d(0,20px,0)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0);transform:translate3d(0,20px,0)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}.bounceOutUp{-webkit-animation-name:bounceOutUp;animation-name:bounceOutUp}@-webkit-keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn}@-webkit-keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInDown{-webkit-animation-name:fadeInDown;animation-name:fadeInDown}@-webkit-keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInDownBig{-webkit-animation-name:fadeInDownBig;animation-name:fadeInDownBig}@-webkit-keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInLeft{-webkit-animation-name:fadeInLeft;animation-name:fadeInLeft}@-webkit-keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInLeftBig{-webkit-animation-name:fadeInLeftBig;animation-name:fadeInLeftBig}@-webkit-keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInRight{-webkit-animation-name:fadeInRight;animation-name:fadeInRight}@-webkit-keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInRightBig{-webkit-animation-name:fadeInRightBig;animation-name:fadeInRightBig}@-webkit-keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInUp{-webkit-animation-name:fadeInUp;animation-name:fadeInUp}@-webkit-keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInUpBig{-webkit-animation-name:fadeInUpBig;animation-name:fadeInUpBig}@-webkit-keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.fadeOut{-webkit-animation-name:fadeOut;animation-name:fadeOut}@-webkit-keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.fadeOutDown{-webkit-animation-name:fadeOutDown;animation-name:fadeOutDown}@-webkit-keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}.fadeOutDownBig{-webkit-animation-name:fadeOutDownBig;animation-name:fadeOutDownBig}@-webkit-keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.fadeOutLeft{-webkit-animation-name:fadeOutLeft;animation-name:fadeOutLeft}@-webkit-keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}.fadeOutLeftBig{-webkit-animation-name:fadeOutLeftBig;animation-name:fadeOutLeftBig}@-webkit-keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.fadeOutRight{-webkit-animation-name:fadeOutRight;animation-name:fadeOutRight}@-webkit-keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.fadeOutRightBig{-webkit-animation-name:fadeOutRightBig;animation-name:fadeOutRightBig}@-webkit-keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.fadeOutUp{-webkit-animation-name:fadeOutUp;animation-name:fadeOutUp}@-webkit-keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}.fadeOutUpBig{-webkit-animation-name:fadeOutUpBig;animation-name:fadeOutUpBig}@-webkit-keyframes flip{0%{-webkit-transform:perspective(400px) rotateY(-1turn);transform:perspective(400px) rotateY(-1turn)}0%,40%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-190deg);transform:perspective(400px) translateZ(150px) rotateY(-190deg)}50%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-170deg);transform:perspective(400px) translateZ(150px) rotateY(-170deg)}50%,80%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95);transform:perspective(400px) scale3d(.95,.95,.95)}to{-webkit-transform:perspective(400px);transform:perspective(400px);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}@keyframes flip{0%{-webkit-transform:perspective(400px) rotateY(-1turn);transform:perspective(400px) rotateY(-1turn)}0%,40%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-190deg);transform:perspective(400px) translateZ(150px) rotateY(-190deg)}50%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-170deg);transform:perspective(400px) translateZ(150px) rotateY(-170deg)}50%,80%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95);transform:perspective(400px) scale3d(.95,.95,.95)}to{-webkit-transform:perspective(400px);transform:perspective(400px);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}.animated.flip{-webkit-backface-visibility:visible;backface-visibility:visible;-webkit-animation-name:flip;animation-name:flip}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg)}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg)}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.flipInX{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInX;animation-name:flipInX}@-webkit-keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg)}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg)}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.flipInY{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInY;animation-name:flipInY}@-webkit-keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}@keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}.flipOutX{-webkit-animation-name:flipOutX;animation-name:flipOutX;-webkit-backface-visibility:visible!important;backface-visibility:visible!important}@-webkit-keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}@keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}.flipOutY{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipOutY;animation-name:flipOutY}@-webkit-keyframes lightSpeedIn{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg)}60%,80%{opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:none;transform:none;opacity:1}}@keyframes lightSpeedIn{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg)}60%,80%{opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:none;transform:none;opacity:1}}.lightSpeedIn{-webkit-animation-name:lightSpeedIn;animation-name:lightSpeedIn;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}@-webkit-keyframes lightSpeedOut{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}@keyframes lightSpeedOut{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}.lightSpeedOut{-webkit-animation-name:lightSpeedOut;animation-name:lightSpeedOut;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}@-webkit-keyframes rotateIn{0%{transform-origin:center;-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateIn{0%{transform-origin:center;-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:none;transform:none;opacity:1}}.rotateIn{-webkit-animation-name:rotateIn;animation-name:rotateIn}@-webkit-keyframes rotateInDownLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInDownLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInDownLeft{-webkit-animation-name:rotateInDownLeft;animation-name:rotateInDownLeft}@-webkit-keyframes rotateInDownRight{0%{transform-origin:right bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInDownRight{0%{transform-origin:right bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInDownRight{-webkit-animation-name:rotateInDownRight;animation-name:rotateInDownRight}@-webkit-keyframes rotateInUpLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInUpLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInUpLeft{-webkit-animation-name:rotateInUpLeft;animation-name:rotateInUpLeft}@-webkit-keyframes rotateInUpRight{0%{transform-origin:right bottom;-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInUpRight{0%{transform-origin:right bottom;-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInUpRight{-webkit-animation-name:rotateInUpRight;animation-name:rotateInUpRight}@-webkit-keyframes rotateOut{0%{transform-origin:center;opacity:1}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}@keyframes rotateOut{0%{transform-origin:center;opacity:1}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}.rotateOut{-webkit-animation-name:rotateOut;animation-name:rotateOut}@-webkit-keyframes rotateOutDownLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}@keyframes rotateOutDownLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}.rotateOutDownLeft{-webkit-animation-name:rotateOutDownLeft;animation-name:rotateOutDownLeft}@-webkit-keyframes rotateOutDownRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutDownRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.rotateOutDownRight{-webkit-animation-name:rotateOutDownRight;animation-name:rotateOutDownRight}@-webkit-keyframes rotateOutUpLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutUpLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.rotateOutUpLeft{-webkit-animation-name:rotateOutUpLeft;animation-name:rotateOutUpLeft}@-webkit-keyframes rotateOutUpRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}@keyframes rotateOutUpRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}.rotateOutUpRight{-webkit-animation-name:rotateOutUpRight;animation-name:rotateOutUpRight}@-webkit-keyframes hinge{0%{transform-origin:top left}0%,20%,60%{-webkit-transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);transform-origin:top left}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}@keyframes hinge{0%{transform-origin:top left}0%,20%,60%{-webkit-transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);transform-origin:top left}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}.hinge{-webkit-animation-name:hinge;animation-name:hinge}@-webkit-keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:none;transform:none}}.rollIn{-webkit-animation-name:rollIn;animation-name:rollIn}@-webkit-keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}@keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}.rollOut{-webkit-animation-name:rollOut;animation-name:rollOut}@-webkit-keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}@keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}.zoomIn{-webkit-animation-name:zoomIn;animation-name:zoomIn}@-webkit-keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInDown{-webkit-animation-name:zoomInDown;animation-name:zoomInDown}@-webkit-keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInLeft{-webkit-animation-name:zoomInLeft;animation-name:zoomInLeft}@-webkit-keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInRight{-webkit-animation-name:zoomInRight;animation-name:zoomInRight}@-webkit-keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInUp{-webkit-animation-name:zoomInUp;animation-name:zoomInUp}@-webkit-keyframes zoomOut{0%{opacity:1}50%{-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%,to{opacity:0}}@keyframes zoomOut{0%{opacity:1}50%{-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%,to{opacity:0}}.zoomOut{-webkit-animation-name:zoomOut;animation-name:zoomOut}@-webkit-keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomOutDown{-webkit-animation-name:zoomOutDown;animation-name:zoomOutDown}@-webkit-keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0);-webkit-transform-origin:left center;transform-origin:left center}}@keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0);-webkit-transform-origin:left center;transform-origin:left center}}.zoomOutLeft{-webkit-animation-name:zoomOutLeft;animation-name:zoomOutLeft}@-webkit-keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0);-webkit-transform-origin:right center;transform-origin:right center}}@keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0);-webkit-transform-origin:right center;transform-origin:right center}}.zoomOutRight{-webkit-animation-name:zoomOutRight;animation-name:zoomOutRight}@-webkit-keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomOutUp{-webkit-animation-name:zoomOutUp;animation-name:zoomOutUp}@-webkit-keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInDown{-webkit-animation-name:slideInDown;animation-name:slideInDown}@-webkit-keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInLeft{-webkit-animation-name:slideInLeft;animation-name:slideInLeft}@-webkit-keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInRight{-webkit-animation-name:slideInRight;animation-name:slideInRight}@-webkit-keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInUp{-webkit-animation-name:slideInUp;animation-name:slideInUp}@-webkit-keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.slideOutDown{-webkit-animation-name:slideOutDown;animation-name:slideOutDown}@-webkit-keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.slideOutLeft{-webkit-animation-name:slideOutLeft;animation-name:slideOutLeft}@-webkit-keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.slideOutRight{-webkit-animation-name:slideOutRight;animation-name:slideOutRight}@-webkit-keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.slideOutUp{-webkit-animation-name:slideOutUp;animation-name:slideOutUp} \ No newline at end of file
diff --git a/vendor/assets/stylesheets/cropper.css b/vendor/assets/stylesheets/cropper.css
new file mode 100644
index 00000000000..8668c7c049a
--- /dev/null
+++ b/vendor/assets/stylesheets/cropper.css
@@ -0,0 +1,379 @@
+/*!
+ * Cropper v2.3.0
+ * https://github.com/fengyuanchen/cropper
+ *
+ * Copyright (c) 2014-2016 Fengyuan Chen and contributors
+ * Released under the MIT license
+ *
+ * Date: 2016-02-22T02:13:13.332Z
+ */
+.cropper-container {
+ font-size: 0;
+ line-height: 0;
+
+ position: relative;
+
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+
+ direction: ltr !important;
+ -ms-touch-action: none;
+ touch-action: none;
+ -webkit-tap-highlight-color: transparent;
+ -webkit-touch-callout: none;
+}
+
+.cropper-container img {
+ display: block;
+
+ width: 100%;
+ min-width: 0 !important;
+ max-width: none !important;
+ height: 100%;
+ min-height: 0 !important;
+ max-height: none !important;
+
+ image-orientation: 0deg !important;
+}
+
+.cropper-wrap-box,
+.cropper-canvas,
+.cropper-drag-box,
+.cropper-crop-box,
+.cropper-modal {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+}
+
+.cropper-wrap-box {
+ overflow: hidden;
+}
+
+.cropper-drag-box {
+ opacity: 0;
+ background-color: #fff;
+
+ filter: alpha(opacity=0);
+}
+
+.cropper-modal {
+ opacity: .5;
+ background-color: #000;
+
+ filter: alpha(opacity=50);
+}
+
+.cropper-view-box {
+ display: block;
+ overflow: hidden;
+
+ width: 100%;
+ height: 100%;
+
+ outline: 1px solid #39f;
+ outline-color: rgba(51, 153, 255, .75);
+}
+
+.cropper-dashed {
+ position: absolute;
+
+ display: block;
+
+ opacity: .5;
+ border: 0 dashed #eee;
+
+ filter: alpha(opacity=50);
+}
+
+.cropper-dashed.dashed-h {
+ top: 33.33333%;
+ left: 0;
+
+ width: 100%;
+ height: 33.33333%;
+
+ border-top-width: 1px;
+ border-bottom-width: 1px;
+}
+
+.cropper-dashed.dashed-v {
+ top: 0;
+ left: 33.33333%;
+
+ width: 33.33333%;
+ height: 100%;
+
+ border-right-width: 1px;
+ border-left-width: 1px;
+}
+
+.cropper-center {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+
+ display: block;
+
+ width: 0;
+ height: 0;
+
+ opacity: .75;
+
+ filter: alpha(opacity=75);
+}
+
+.cropper-center:before,
+.cropper-center:after {
+ position: absolute;
+
+ display: block;
+
+ content: ' ';
+
+ background-color: #eee;
+}
+
+.cropper-center:before {
+ top: 0;
+ left: -3px;
+
+ width: 7px;
+ height: 1px;
+}
+
+.cropper-center:after {
+ top: -3px;
+ left: 0;
+
+ width: 1px;
+ height: 7px;
+}
+
+.cropper-face,
+.cropper-line,
+.cropper-point {
+ position: absolute;
+
+ display: block;
+
+ width: 100%;
+ height: 100%;
+
+ opacity: .1;
+
+ filter: alpha(opacity=10);
+}
+
+.cropper-face {
+ top: 0;
+ left: 0;
+
+ background-color: #fff;
+}
+
+.cropper-line {
+ background-color: #39f;
+}
+
+.cropper-line.line-e {
+ top: 0;
+ right: -3px;
+
+ width: 5px;
+
+ cursor: e-resize;
+}
+
+.cropper-line.line-n {
+ top: -3px;
+ left: 0;
+
+ height: 5px;
+
+ cursor: n-resize;
+}
+
+.cropper-line.line-w {
+ top: 0;
+ left: -3px;
+
+ width: 5px;
+
+ cursor: w-resize;
+}
+
+.cropper-line.line-s {
+ bottom: -3px;
+ left: 0;
+
+ height: 5px;
+
+ cursor: s-resize;
+}
+
+.cropper-point {
+ width: 5px;
+ height: 5px;
+
+ opacity: .75;
+ background-color: #39f;
+
+ filter: alpha(opacity=75);
+}
+
+.cropper-point.point-e {
+ top: 50%;
+ right: -3px;
+
+ margin-top: -3px;
+
+ cursor: e-resize;
+}
+
+.cropper-point.point-n {
+ top: -3px;
+ left: 50%;
+
+ margin-left: -3px;
+
+ cursor: n-resize;
+}
+
+.cropper-point.point-w {
+ top: 50%;
+ left: -3px;
+
+ margin-top: -3px;
+
+ cursor: w-resize;
+}
+
+.cropper-point.point-s {
+ bottom: -3px;
+ left: 50%;
+
+ margin-left: -3px;
+
+ cursor: s-resize;
+}
+
+.cropper-point.point-ne {
+ top: -3px;
+ right: -3px;
+
+ cursor: ne-resize;
+}
+
+.cropper-point.point-nw {
+ top: -3px;
+ left: -3px;
+
+ cursor: nw-resize;
+}
+
+.cropper-point.point-sw {
+ bottom: -3px;
+ left: -3px;
+
+ cursor: sw-resize;
+}
+
+.cropper-point.point-se {
+ right: -3px;
+ bottom: -3px;
+
+ width: 20px;
+ height: 20px;
+
+ cursor: se-resize;
+
+ opacity: 1;
+
+ filter: alpha(opacity=100);
+}
+
+.cropper-point.point-se:before {
+ position: absolute;
+ right: -50%;
+ bottom: -50%;
+
+ display: block;
+
+ width: 200%;
+ height: 200%;
+
+ content: ' ';
+
+ opacity: 0;
+ background-color: #39f;
+
+ filter: alpha(opacity=0);
+}
+
+@media (min-width: 768px) {
+ .cropper-point.point-se {
+ width: 15px;
+ height: 15px;
+ }
+}
+
+@media (min-width: 992px) {
+ .cropper-point.point-se {
+ width: 10px;
+ height: 10px;
+ }
+}
+
+@media (min-width: 1200px) {
+ .cropper-point.point-se {
+ width: 5px;
+ height: 5px;
+
+ opacity: .75;
+
+ filter: alpha(opacity=75);
+ }
+}
+
+.cropper-invisible {
+ opacity: 0;
+
+ filter: alpha(opacity=0);
+}
+
+.cropper-bg {
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC');
+}
+
+.cropper-hide {
+ position: absolute;
+
+ display: block;
+
+ width: 0;
+ height: 0;
+}
+
+.cropper-hidden {
+ display: none !important;
+}
+
+.cropper-move {
+ cursor: move;
+}
+
+.cropper-crop {
+ cursor: crosshair;
+}
+
+.cropper-disabled .cropper-drag-box,
+.cropper-disabled .cropper-face,
+.cropper-disabled .cropper-line,
+.cropper-disabled .cropper-point {
+ cursor: not-allowed;
+}